Commit f8f97421 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into auto-skeletons

parents 3d95b092 9c38cc4f
......@@ -44,12 +44,15 @@ NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_
NEXT_PUBLIC_AD_DOMAIN_WITH_AD=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_DOMAIN_WITH_AD__
NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__
NEXT_PUBLIC_WEB3_DEFAULT_WALLET=__PLACEHOLDER_FOR_NEXT_PUBLIC_WEB3_DEFAULT_WALLET__
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=__PLACEHOLDER_FOR_NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET__
# api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
NEXT_PUBLIC_API_BASE_PATH=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_BASE_PATH__
NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__
NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL__
NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__
NEXT_PUBLIC_VISUALIZE_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_VISUALIZE_API_HOST__
NEXT_PUBLIC_API_SPEC_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_SPEC_URL__
......
......@@ -68,7 +68,7 @@
},
{
"type": "shell",
"command": "NEXT_PUBLIC_L1_BASE_URL=https://${input:goerliApiHost} yarn dev:goerli:optimism",
"command": "NEXT_PUBLIC_API_HOST=${input:L2ApiHost} NEXT_PUBLIC_L1_BASE_URL=https://${input:goerliApiHost} yarn dev:goerli:optimism",
"problemMatcher": [],
"label": "dev server: goerli optimism",
"detail": "start local dev server for Goerli Optimism network",
......@@ -379,5 +379,15 @@
],
"default": ""
},
{
"type": "pickString",
"id": "L2ApiHost",
"description": "Choose L2 API host:",
"options": [
"blockscout-optimism-goerli.test.aws-k8s.blockscout.com",
"base-goerli.blockscout.com",
],
"default": ""
},
],
}
\ No newline at end of file
/* eslint-disable no-restricted-properties */
import type { WalletType } from 'types/client/wallets';
import type { NetworkExplorer } from 'types/networks';
import type { ChainIndicatorId } from 'ui/home/indicators/types';
......@@ -11,6 +12,15 @@ const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
}
};
const stripTrailingSlash = (str: string) => str[str.length - 1] === '/' ? str.slice(0, -1) : str;
const getWeb3DefaultWallet = (): WalletType => {
const envValue = getEnvValue(process.env.NEXT_PUBLIC_WEB3_DEFAULT_WALLET);
const SUPPORTED_WALLETS: Array<WalletType> = [
'metamask',
'coinbase',
];
return (envValue && SUPPORTED_WALLETS.includes(envValue) ? envValue : 'metamask') as WalletType;
};
const env = process.env.NODE_ENV;
const isDev = env === 'development';
......@@ -36,6 +46,8 @@ const apiEndpoint = apiHost ? [
apiPort && ':' + apiPort,
].filter(Boolean).join('') : 'https://blockscout.com';
const socketSchema = getEnvValue(process.env.NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL) || 'wss';
const logoutUrl = (() => {
try {
const envUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_URL);
......@@ -104,10 +116,14 @@ const config = Object.freeze({
domainWithAd: getEnvValue(process.env.NEXT_PUBLIC_AD_DOMAIN_WITH_AD) || 'blockscout.com',
adButlerOn: getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_ON) === 'true',
},
web3: {
defaultWallet: getWeb3DefaultWallet(),
disableAddTokenToWallet: getEnvValue(process.env.NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET) === 'true',
},
api: {
host: apiHost,
endpoint: apiEndpoint,
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
socket: apiHost ? `${ socketSchema }://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
},
L2: {
......
......@@ -3,6 +3,8 @@ NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/front
NEXT_PUBLIC_NETWORK_EXPLORERS=
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_WEB3_DEFAULT_WALLET=coinbase
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=true
# network config
NEXT_PUBLIC_NETWORK_NAME=Base Göerli
......@@ -10,14 +12,14 @@ NEXT_PUBLIC_NETWORK_SHORT_NAME=Base
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=optimism
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/base.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/base.svg
NEXT_PUBLIC_NETWORK_ID=420
NEXT_PUBLIC_NETWORK_ID=84531
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://goerli.optimism.io
NEXT_PUBLIC_NETWORK_RPC_URL=https://goerli.base.org
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
......
......@@ -4,8 +4,8 @@ NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
#NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT='linear-gradient(136.9deg, #235643 1.5%, #16191E 77.77%)'
#NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#DCFE76'
#NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=linear-gradient(136.9deg, \#235643 1.5%, \#16191E 77.77%)
#NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=\#DCFE76
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/poa.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/poa.svg
......
blockscout:
environment:
INDEXER_OPTIMISM_L1_RPC:
_default: ENC[AES256_GCM,data:a02FoR3U/KlxsFVFiSGSLdFOFIDwS5eBgw==,iv:rmT8bVh3xyqKeebtnT+/eIC0bSGWKZhJ9H52cUsqFxM=,tag:OsRo1D2DtfMIgCGjAvqobw==,type:str]
ACCOUNT_USERNAME:
_default: ENC[AES256_GCM,data:n9Wc7xjBFdWHJNaKBwpVVykz3FbBtqicKdf6yD/kMLKuts/0Rv8vfQ20gSahIvSbbno=,iv:FRyRAwelWF1PHqbIJX09MH+VVqW53luYraLYq/A21j4=,tag:YUejqoXCZjWNUhRZ9emd+g==,type:str]
ACCOUNT_PASSWORD:
......@@ -46,8 +48,6 @@ blockscout:
_default: ENC[AES256_GCM,data:fHIsaJQY6YrvoJKFDFZlBuunFD6QKYdUUOoW+aLV/44VCsWhrXbMUQ==,iv:teJEbP6pVC4WHeJwptf/DfRbp5Y8x/0OExTfClfLPyU=,tag:5nkNFZcJEFg5v0br2JUpnA==,type:str]
RE_CAPTCHA_CLIENT_KEY:
_default: ENC[AES256_GCM,data:ROEBG5XrOwAofN2ZnFnwekuqvHjDFu1Dp5V20Ud9gvqV1xc324InYA==,iv:EubA1HilDAtNaqdpTbFWaWSAf8kCiWgStada1dKpOD8=,tag:ry4796qJAekQKlz4sWGQKQ==,type:str]
INDEXER_OPTIMISM_RPC_L1:
_default: ENC[AES256_GCM,data:y93o/DxAWlQKSu5NTStN9czzoQdy4Fo/Dg==,iv:LgVC0FQ/IEs3x876p6s47FaXbdxNuDOzUdoAOjnWzpU=,tag:A68lXRAZFZ5N3HNwppqV1A==,type:str]
scVerifier:
environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
......@@ -88,8 +88,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-04-25T13:14:30Z"
mac: ENC[AES256_GCM,data:/m/B6bN4MadVUtnZQUuZyCQX+X75mUP2pcQjUwZRGRbFcCzYYQLPV9fxgySNBbR+af0LXTHv4AilpOmiCCvCT0Yi6Q01+ac1YWJZ5U0kEBkhIXyOujYUHYeongWR7qzZmnujeV4d68ydEobteOH/EEmQymO7FrEiYJfQUOvomjU=,iv:/wFoZ0z12aSNpdG2kGGdORDdu6taDgG/yCAfY6bUiW8=,tag:TWza4eHHmq8EHJ41RiX01w==,type:str]
lastmodified: "2023-04-27T16:27:47Z"
mac: ENC[AES256_GCM,data:TUIxPt/HMLzkWlPdBcM2mKGlNU+6ob+EWvtfKWAHZaVhH2qwoi+HI+Ncx+j3H9/MR6q9Uhhr611s8rCF0LdwbsK+LGdOBBql+45vfmCE1XRl/DS0LFdZxkZn4ub49C9Uf8g0DOVt02NJQNh3ZNaBa+l2nIirh643hLvzFCCqBM8=,iv:tqRFfzSk0W+NzEADzIWy08aUnpJgcIPBHhUhM0Kf+VA=,tag:0IiapCTs0UITa38cwVyZqQ==,type:str]
pgp:
- created_at: "2022-09-22T09:52:10Z"
enc: |
......
......@@ -113,8 +113,6 @@ blockscout:
_default: '4677000'
DISABLE_REALTIME_INDEXER:
_default: 'false'
INDEXER_OPTIMISM_L1_RPC:
_default: http://65.108.226.29:8545
INDEXER_OPTIMISM_L1_PORTAL_CONTRACT:
_default: 0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383
INDEXER_OPTIMISM_L1_WITHDRAWALS_START_BLOCK:
......@@ -279,7 +277,7 @@ frontend:
app: blockscout
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
_default: ghcr.io/blockscout/frontend:v1.0.8
ingress:
enabled: true
# annotations:
......@@ -393,6 +391,10 @@ frontend:
_default: ''
NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://goerli.optimism.io
NEXT_PUBLIC_WEB3_DEFAULT_WALLET:
_default: coinbase
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET:
_default: true
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs']"
NEXT_PUBLIC_IS_TESTNET:
......
blockscout:
environment:
ETHEREUM_JSONRPC_TRACE_URL:
#ENC[AES256_GCM,data:Lh7FyFwnnbk8Lpo3e5XunDK2iRyXsFtUOyF49/eoww==,iv:rTfCtp+xZkWtYewH77Xqx5lTAbfS2Xr/bIcMeETieKw=,tag:cviW/zIbNzpZ2ReFIxLxxw==,type:comment]
_default: ENC[AES256_GCM,data:hzvsDY9ff0zagkoZsuqdPa7ePPe0Td+wlg==,iv:dfiFF+vjPzPLJz7oWe0ahEjFlhc+/SC4vubE9DssRMk=,tag:5wW63FHQa6HCiA8jAxO71w==,type:str]
ETHEREUM_JSONRPC_HTTP_URL:
#ENC[AES256_GCM,data:Q5ZCHA/BJ+uTdiNyXF9m2Awa+wjkBWca01QusH2Tvw==,iv:JscgpCcF9IbOr2TOGE3D6GrQCmnKxyPoH2hdoC+NYR4=,tag:AFUqdNTheLggMRx6DzotMg==,type:comment]
_default: ENC[AES256_GCM,data:yGFoleqbrSeu8VOQsczabh8AZH0BZJiNvw==,iv:ViStcIKLzBVjUIMp53sg+aylQ/4Cyh8/E/UwPfpoZCo=,tag:MXKebydM5XOhy/INqYJhww==,type:str]
#ENC[AES256_GCM,data:xhwTQI1yVw7fJPeMPIwjwUkkwgb9LBQfJw==,iv:CAW4UNSA1SVhLsPkN2T+FVtGPD/5ayJz8afzePTcYr8=,tag:eWbOvyG6rb+RkHTxxSbzmA==,type:comment]
#ENC[AES256_GCM,data:EyiYQwB5sVt/gv+Wchv3Ty7Xzt44NuYtOfaPuxUOsNB8,iv:C5zDeLnhFLo/D+78k9GV9l/Ehr53Wpo2a+q/98rARoU=,tag:TTdqnUmhOLP4YCyVZ5TIVg==,type:comment]
#ENC[AES256_GCM,data:f0ESzURFheRh+DqaqiGNXwVyBnSTsTDAwhbZ7TFZnSzqnvMRzZthkZvDW6nSM5d23asAeYuXwRY=,iv:HYyVReU8eedUxnSQ3YPEBnohq6a5L+ef3HIm132aqjs=,tag:tpQ272l070s+KbTQ4v0xww==,type:comment]
ACCOUNT_USERNAME:
_default: ENC[AES256_GCM,data:xJ0hxcuU8u+QwiXciZ8qe1sV1GuxZV8Z9iYgnoWCu0ueuEmNE80jwe+vqviNb/UbOQs=,iv:uRNP5RTG/oxUngEoBbvLg9GzS5gYiHlL42yttgYWPAc=,tag:plQfCpG9pN7d28ooZ3aHrQ==,type:str]
ACCOUNT_PASSWORD:
......@@ -33,9 +42,13 @@ blockscout:
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:jr/MLQ1Rq1xjBVAb/fZFbvak23hEcmRkh5nnG4WNCcORR+xEWhEv7ncP87ClpsfxedDN5pOcFrdUkF7u80OkKA==,iv:TIxP5v9K7mcApYmSKRByKNPwYw0djRWHojYfHvSWqZM=,tag:LSLmwapH044CEzOvlce9Og==,type:str]
_default: ENC[AES256_GCM,data:iYnptgxESZs216/m0ArCciXfirPTxVjt5urKTATyYv3uadokC54ofzMZagG/ZVvNY48+Uxh4YGzwySyLOOM0SA==,iv:UQoZf3tesEjAJ7E5YruoZmVclsYfS5EmSo4z90wfHfE=,tag:er+hARrkCrjoQugqvRr4ow==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:mTY6sjNKZ+VEvH47eyyoUHt//beWvuxyreu+1WrmMvkdkwR6jgXnSXh+1pTuiG8e3it96Egxraz0hjZxlkY9kSmE91/dfoqKTns=,iv:op8OuOXXeBmmUnVkCL14j4HrfjHHoN3n2XZqLdg03Mw=,tag:bbFQYi0Aa1sUdfoUjuQ/+w==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:IZFfi6pn+hy7g0wnEtP9TYHH1fNiC2gqgRHVdgm4C9smPerEvS0pq9dBwVY=,iv:BxbSInFQ6GE2loTv+IzdYr25PlyzdWZI1wdT6r+uvBg=,tag:psujCU372k59OGmlRdH9Fg==,type:str]
RE_CAPTCHA_SECRET_KEY:
_default: ENC[AES256_GCM,data:e9jfs4PwY+UPk0EuXZxERylKUFSUo6zsAz8oly3YwwybipP+A5rSBQ==,iv:TvrD/a+6+11IGDVFLUCn8U+H3v1YfIRrcndOVdt/taI=,tag:OAFVIznkeeeVX9UsigfP4A==,type:str]
RE_CAPTCHA_CLIENT_KEY:
......@@ -140,8 +153,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-04-25T13:14:48Z"
mac: ENC[AES256_GCM,data:V1q4PteIRNS0Ly6Whh6sVFn41K305tJsPSfRW2IU2e/JC5ei565mvT8HBy6Z6OrjSTrMv60fmpA5LSPAmzIab9jqFcm35uIGZY39PtP1Zigb/90BXqydCwQkbfXaNAcigZ9PgSs2MiqrmRI6T9hAOP0pxR9UpjC5kpQjqWuvAtM=,iv:bdNpxqPWdbaySRHBvxkuhQTrPJCfdLWxhJhQXI8qr2g=,tag:r8aLUTNNH8WQWz2W7qG0xQ==,type:str]
lastmodified: "2023-04-27T16:38:24Z"
mac: ENC[AES256_GCM,data:/4nW+SI0HmMDkgMLptALsZoRg7vYXaYBCUOcKgbf44DLLOseeUpaiQszwG0SpAOu1gVm/aQTJRVnOmUvWKL0Pzd20vL+gc4KBMKlu/43k0jLz9H3lcO25UFa966qWt541noKgByoaRD1UMPJqTVjifAmrQOhtrg2BAbNEOgYmfY=,iv:RvsDkpSQim4EU7QtsalkUU/yYEUYeBf7ApyKvysCvkE=,tag:ENpb8eRDVRGCumVxCM4T2g==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -63,15 +63,6 @@ blockscout:
app: blockscout-prod
# Blockscout environment variables
environment:
ETHEREUM_JSONRPC_TRACE_URL:
# _default: http://geth-svc:8545
_default: http://65.108.226.29:8545
ETHEREUM_JSONRPC_HTTP_URL:
# _default: http://geth-svc:8545
_default: http://65.108.226.29:8545
# ETHEREUM_JSONRPC_WS_URL:
# # _default: ws://geth-svc:8546
# _default: ws://geth-svc.goerli.svc.cluster.local:8546
BLOCKSCOUT_VERSION:
_default: v5.1.2-beta
ECTO_USE_SSL:
......
......@@ -119,6 +119,10 @@ frontend:
_default: ''
NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://goerli.optimism.io
NEXT_PUBLIC_WEB3_DEFAULT_WALLET:
_default: coinbase
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET:
_default: true
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs']"
NEXT_PUBLIC_IS_TESTNET:
......
......@@ -42,14 +42,16 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_LOGOUT_URL | `string` | Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` |
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` | Account logout return url. Required if account is supported for the app instance. | - | - | `https://blockscout.com/poa/core/auth/logout` |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cup']` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage | `#FFFFFF` | `'#DCFE76'` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` | Gradient value for hero plate on the homepage | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes) | `\#FFFFFF \| rgb(220, 254, 118)` | `\#DCFE76` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` | Gradient value for hero plate on the homepage (escape "#" symbol if you use HEX color codes) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` |
| NEXT_PUBLIC_AD_DOMAIN_WITH_AD | `string` | The domain on which we display ads | - | - | `blockscout.com` |
| NEXT_PUBLIC_AD_ADBUTLER_ON | `boolean` | Set to true to show Adbutler banner instead of Coinzilla banner | - | `false` | `true` |
| NEXT_PUBLIC_API_SPEC_URL | `string` | Spec to be displayed on api-docs page | - | - | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` |
| NEXT_PUBLIC_GRAPHIQL_TRANSACTION | `string` | Txn hash for default query at GraphQl playground page | - | - | `0x69e3923eef50eada197c3336d546936d0c994211492c9f947a24c02827568f9f` |
| NEXT_PUBLIC_WEB3_DEFAULT_WALLET | `metamask` \| `coinbase`| Type of Web3 wallet which will be used by default to add tokens or chains to | - | `metamask` | `coinbase` |
| NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET | `boolean`| Set to `true` to hide icon "Add to your wallet" next to token addresses | - | `false` | `true` |
### Marketplace app configuration properties
......@@ -93,6 +95,7 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
| group | `Mainnets \| Testnets \| Other` | Indicates in which tab network appears in the menu | yes | - | `Mainnets` |
| icon | `string` | Network icon; if not provided, the common placeholder will be shown; *Note* that icon size should be at least 60px by 60px | - | - | `https://placekitten.com/60/60` |
| isActive | `boolean` | Pass `true` if item should be shonw as active in the menu | - | - | `true` |
| invertIconInDarkMode | `boolean` | Pass `true` if icon colors should be inverted in dark mode | - | - | `true` |
### Network explorer configuration properties
......@@ -119,8 +122,10 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
| Variable | Type| Description | Is required | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_API_PROTOCOL | `http \| https` | Main API protocol | - | `https` | `http` |
| NEXT_PUBLIC_API_HOST | `string` | Main API host | - | `blockscout.com` | `my-host.com` |
| NEXT_PUBLIC_API_BASE_PATH | `string` | Base path for Main API endpoint url | - | - | `/poa/core` |
| NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL | `ws \| wss` | Main API websocket protocol | - | `wss` | `ws` |
| NEXT_PUBLIC_STATS_API_HOST | `string` | Stats API endpoint url | - | - | `https://my-host.com` |
| NEXT_PUBLIC_VISUALIZE_API_HOST | `string` | Visualize API endpoint url | - | - | `https://my-host.com` |
......
import type { MetaMaskInpageProvider } from '@metamask/providers';
import type { ExternalProvider } from 'types/client/wallets';
type CPreferences = {
zone: string;
......@@ -7,8 +7,10 @@ type CPreferences = {
}
declare global {
interface Window {
ethereum: MetaMaskInpageProvider;
export interface Window {
ethereum?: {
providers?: Array<ExternalProvider>;
};
coinzilla_display: Array<CPreferences>;
}
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2500 2500">
<rect fill="none"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#0052FF" d="M520.7 0h1458.5C2266.9 0 2500 250.8 2500 560.2v1379.6c0 309.4-233.1 560.2-520.7 560.2H520.7C233.1 2500 0 2249.2 0 1939.8V560.2C0 250.8 233.1 0 520.7 0z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFF" d="M1250 362.1c490.4 0 887.9 397.5 887.9 887.9s-397.5 887.9-887.9 887.9-887.9-397.5-887.9-887.9S759.6 362.1 1250 362.1z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#0052FF" d="M1031.3 966.2h437.3c36 0 65.1 31.4 65.1 70v427.5c0 38.7-29.2 70-65.1 70h-437.3c-36 0-65.1-31.4-65.1-70v-427.5c0-38.6 29.2-70 65.1-70z"/>
</svg>
......@@ -344,6 +344,12 @@ export const RESOURCES = {
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
filterFields: [],
},
token_instance_holders: {
path: '/api/v2/tokens/:hash/instances/:id/holders',
pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'items_count' as const, 'token_id' as const, 'value' as const ],
filterFields: [],
},
// HOMEPAGE
homepage_stats: {
......@@ -491,7 +497,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'search' |
'address_logs' | 'address_tokens' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
'token_instance_transfers' |
'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
......@@ -548,6 +554,7 @@ Q extends 'token_holders' ? TokenHolders :
Q extends 'token_instance' ? TokenInstance :
Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount :
Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_instance_holders' ? TokenHolders :
Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult :
......
......@@ -5,6 +5,7 @@ function generateCspPolicy() {
const policyDescriptor = mergeDescriptors(
descriptors.app(),
descriptors.ad(),
descriptors.cloudFlare(),
descriptors.googleAnalytics(),
descriptors.googleFonts(),
descriptors.googleReCaptcha(),
......
import type CspDev from 'csp-dev';
import { KEY_WORDS } from '../utils';
// CloudFlare analytics
export function cloudFlare(): CspDev.DirectiveDescriptor {
return {
'script-src': [
'static.cloudflareinsights.com',
],
'style-src': [
KEY_WORDS.DATA,
],
};
}
export { ad } from './ad';
export { app } from './app';
export { cloudFlare } from './cloudFlare';
export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha';
......
import React from 'react';
import type { ExternalProvider } from 'types/client/wallets';
import appConfig from 'configs/app/config';
export default function useProvider() {
const [ provider, setProvider ] = React.useState<ExternalProvider>();
React.useEffect(() => {
if (!('ethereum' in window)) {
return;
}
window.ethereum?.providers?.forEach(async(provider) => {
if (appConfig.web3.defaultWallet === 'coinbase' && provider.isCoinbaseWallet) {
return setProvider(provider);
}
if (appConfig.web3.defaultWallet === 'metamask' && provider.isMetaMask) {
return setProvider(provider);
}
});
}, []);
return provider;
}
import type { WalletType, WalletInfo } from 'types/client/wallets';
import coinbaseIcon from 'icons/wallets/coinbase.svg';
import metamaskIcon from 'icons/wallets/metamask.svg';
export const WALLETS_INFO: Record<WalletType, WalletInfo> = {
metamask: {
add_token_text: 'Add token to MetaMask',
add_network_text: 'Add network to MetaMask',
icon: metamaskIcon,
},
coinbase: {
add_token_text: 'Add token to Coinbase Wallet',
add_network_text: 'Add network to Coinbase Wallet',
icon: coinbaseIcon,
},
};
......@@ -30,6 +30,8 @@ export function middleware(req: NextRequest) {
const res = NextResponse.next();
res.headers.append('Content-Security-Policy', cspPolicy);
res.headers.append('Server-Timing', `middleware;dur=${ end - start }`);
// eslint-disable-next-line no-restricted-properties
res.headers.append('Docker-ID', process.env.HOSTNAME || '');
return res;
}
......
......@@ -179,3 +179,8 @@ export const withRichMetadata: TokenInstance = {
status: null,
},
};
export const unique: TokenInstance = {
...base,
is_unique: true,
};
import type { providers } from 'ethers';
export type WalletType = 'metamask' | 'coinbase';
export interface WalletInfo {
add_token_text: string;
add_network_text: string;
icon: React.ElementType;
}
export interface ExternalProvider extends providers.ExternalProvider {
isCoinbaseWallet?: boolean;
// have to patch ethers here, since params could be not only an array
// eslint-disable-next-line @typescript-eslint/no-explicit-any
request?: (request: { method: string; params?: any }) => Promise<any>;
}
import type { MetaMaskInpageProvider } from '@metamask/providers';
import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -73,7 +72,9 @@ test('token', async({ mount, page }) => {
}), { times: 1 });
await page.evaluate(() => {
window.ethereum = { } as MetaMaskInpageProvider;
window.ethereum = {
providers: [ { isMetaMask: true } ],
};
});
const component = await mount(
......
......@@ -8,11 +8,13 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { tokenTabsByType } from 'ui/pages/Address';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import ERC1155Tokens from './tokens/ERC1155Tokens';
import ERC20Tokens from './tokens/ERC20Tokens';
import ERC721Tokens from './tokens/ERC721Tokens';
import TokenBalances from './tokens/TokenBalances';
import TokensWithIds from './tokens/TokensWithIds';
import TokensWithoutIds from './tokens/TokensWithoutIds';
const TAB_LIST_PROPS = {
marginBottom: 0,
......@@ -32,21 +34,59 @@ const AddressTokens = () => {
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenType: TokenType = (Object.keys(tokenTabsByType) as Array<TokenType>).find(key => tokenTabsByType[key] === router.query.tab) || 'ERC-20';
const tab = router.query.tab?.toString();
const tokenType: TokenType = (Object.keys(tokenTabsByType) as Array<TokenType>).find(key => tokenTabsByType[key] === tab) || 'ERC-20';
const tokensQuery = useQueryWithPages({
const erc20Query = useQueryWithPages({
resourceName: 'address_tokens',
pathParams: { hash: router.query.hash?.toString() },
filters: { type: tokenType },
filters: { type: 'ERC-20' },
scrollRef,
options: {
enabled: tokenType === 'ERC-20',
},
});
const erc721Query = useQueryWithPages({
resourceName: 'address_tokens',
pathParams: { hash: router.query.hash?.toString() },
filters: { type: 'ERC-721' },
scrollRef,
options: {
enabled: tokenType === 'ERC-721',
},
});
const erc1155Query = useQueryWithPages({
resourceName: 'address_tokens',
pathParams: { hash: router.query.hash?.toString() },
filters: { type: 'ERC-1155' },
scrollRef,
options: {
enabled: tokenType === 'ERC-1155',
},
});
const tabs = [
{ id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <TokensWithoutIds tokensQuery={ tokensQuery }/> },
{ id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: <TokensWithoutIds tokensQuery={ tokensQuery }/> },
{ id: tokenTabsByType['ERC-1155'], title: 'ERC-1155', component: <TokensWithIds tokensQuery={ tokensQuery }/> },
{ id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <ERC20Tokens tokensQuery={ erc20Query }/> },
{ id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: <ERC721Tokens tokensQuery={ erc721Query }/> },
{ id: tokenTabsByType['ERC-1155'], title: 'ERC-1155', component: <ERC1155Tokens tokensQuery={ erc1155Query }/> },
];
let isPaginationVisible;
let pagination: PaginationProps | undefined;
if (tab === tokenTabsByType['ERC-1155']) {
isPaginationVisible = erc1155Query.isPaginationVisible;
pagination = erc1155Query.pagination;
} else if (tab === tokenTabsByType['ERC-721']) {
isPaginationVisible = erc721Query.isPaginationVisible;
pagination = erc721Query.pagination;
} else {
isPaginationVisible = erc20Query.isPaginationVisible;
pagination = erc20Query.pagination;
}
return (
<>
<TokenBalances/>
......@@ -58,7 +98,7 @@ const AddressTokens = () => {
colorScheme="gray"
size="sm"
tabListProps={ isMobile ? TAB_LIST_PROPS_MOBILE : TAB_LIST_PROPS }
rightSlot={ tokensQuery.isPaginationVisible && !isMobile ? <Pagination { ...tokensQuery.pagination }/> : null }
rightSlot={ isPaginationVisible && !isMobile ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile }
/>
</>
......
......@@ -27,13 +27,12 @@ const AddressCoinBalanceTableItem = (props: Props) => {
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal>
</Td>
<Td>
{ props.transaction_hash ?
{ props.transaction_hash &&
(
<Address w="150px" fontWeight="700">
<AddressLink hash={ props.transaction_hash } type="transaction"/>
</Address>
) :
<Text fontWeight="700">-</Text>
)
}
</Td>
<Td>
......
......@@ -61,10 +61,12 @@ const TxInternalsListItem = ({
<InOutTag isIn={ isIn } isOut={ isOut }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
}
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/>
</Address>
{ toData && (
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/>
</Address>
) }
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
......
......@@ -73,10 +73,12 @@ const AddressIntTxsTableItem = ({
}
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/>
</Address>
{ toData && (
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/>
</Address>
) }
</Td>
<Td isNumeric verticalAlign="middle">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
......
......@@ -19,7 +19,7 @@ type Props = {
};
}
const TokensWithIds = ({ tokensQuery }: Props) => {
const ERC1155Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
......@@ -69,4 +69,4 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
);
};
export default TokensWithIds;
export default ERC1155Tokens;
......@@ -10,8 +10,8 @@ import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import TokensListItem from './TokensListItem';
import TokensTable from './TokensTable';
import ERC20TokensListItem from './ERC20TokensListItem';
import ERC20TokensTable from './ERC20TokensTable';
type Props = {
tokensQuery: UseQueryResult<AddressTokensResponse> & {
......@@ -20,7 +20,7 @@ type Props = {
};
}
const TokensWithoutIds = ({ tokensQuery }: Props) => {
const ERC20Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
......@@ -33,8 +33,8 @@ const TokensWithoutIds = ({ tokensQuery }: Props) => {
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }><TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <TokensListItem key={ item.token.address } { ...item }/>) }</Show></>
<Hide below="lg" ssr={ false }><ERC20TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <ERC20TokensListItem key={ item.token.address } { ...item }/>) }</Show></>
) : null;
return (
......@@ -54,4 +54,4 @@ const TokensWithoutIds = ({ tokensQuery }: Props) => {
};
export default TokensWithoutIds;
export default ERC20Tokens;
......@@ -4,16 +4,15 @@ import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
import AddressAddToMetaMask from '../details/AddressAddToMetaMask';
type Props = AddressTokenBalance;
const TokensListItem = ({ token, value }: Props) => {
const ERC20TokensListItem = ({ token, value }: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
......@@ -31,7 +30,7 @@ const TokensListItem = ({ token, value }: Props) => {
<Flex alignItems="center" pl={ 8 }>
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
<AddressAddToMetaMask token={ token } ml={ 2 }/>
<AddressAddToWallet token={ token } ml={ 2 }/>
</Flex>
{ token.exchange_rate !== undefined && token.exchange_rate !== null && (
<HStack spacing={ 3 }>
......@@ -53,4 +52,4 @@ const TokensListItem = ({ token, value }: Props) => {
);
};
export default TokensListItem;
export default ERC20TokensListItem;
......@@ -5,14 +5,14 @@ import type { AddressTokenBalance } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokensTableItem from './TokensTableItem';
import ERC20TokensTableItem from './ERC20TokensTableItem';
interface Props {
data: Array<AddressTokenBalance>;
top: number;
}
const TokensTable = ({ data, top }: Props) => {
const ERC20TokensTable = ({ data, top }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
......@@ -26,11 +26,11 @@ const TokensTable = ({ data, top }: Props) => {
</Thead>
<Tbody>
{ data.map((item) => (
<TokensTableItem key={ item.token.address } { ...item }/>
<ERC20TokensTableItem key={ item.token.address } { ...item }/>
)) }
</Tbody>
</Table>
);
};
export default TokensTable;
export default ERC20TokensTable;
......@@ -4,15 +4,14 @@ import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo';
import AddressAddToMetaMask from '../details/AddressAddToMetaMask';
type Props = AddressTokenBalance;
const TokensTableItem = ({
const ERC20TokensTableItem = ({
token,
value,
}: Props) => {
......@@ -38,20 +37,20 @@ const TokensTableItem = ({
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
</Flex>
<AddressAddToMetaMask token={ token } ml={ 4 }/>
<AddressAddToWallet token={ token } ml={ 4 }/>
</Flex>
</Td>
<Td isNumeric verticalAlign="middle">
{ token.exchange_rate ? `$${ token.exchange_rate }` : '-' }
{ token.exchange_rate && `$${ token.exchange_rate }` }
</Td>
<Td isNumeric verticalAlign="middle">
{ tokenQuantity }
</Td>
<Td isNumeric verticalAlign="middle">
{ tokenValue ? `$${ tokenValue }` : '-' }
{ tokenValue && `$${ tokenValue }` }
</Td>
</Tr>
);
};
export default React.memo(TokensTableItem);
export default React.memo(ERC20TokensTableItem);
import { Show, Hide } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { AddressTokensResponse } from 'types/api/address';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import ERC721TokensListItem from './ERC721TokensListItem';
import ERC721TokensTable from './ERC721TokensTable';
type Props = {
tokensQuery: UseQueryResult<AddressTokensResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const ERC721Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
const actionBar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }><ERC721TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <ERC721TokensListItem key={ item.token.address } { ...item }/>) }</Show></>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '40%', '40%', '20%' ],
}}
emptyText="There are no tokens of selected type."
content={ content }
actionBar={ actionBar }
/>
);
};
export default ERC721Tokens;
import { Flex, HStack, Text } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance;
const ERC721TokensListItem = ({ token, value }: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
return (
<ListItemMobile rowGap={ 2 }>
<Flex alignItems="center" width="100%">
<TokenLogo hash={ token.address } name={ token.name } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/>
</Flex>
<Flex alignItems="center" pl={ 8 }>
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
<AddressAddToWallet token={ token } ml={ 2 }/>
</Flex>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Quantity</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text>
</HStack>
</ListItemMobile>
);
};
export default ERC721TokensListItem;
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky';
import ERC721TokensTableItem from './ERC721TokensTableItem';
interface Props {
data: Array<AddressTokenBalance>;
top: number;
}
const ERC721TokensTable = ({ data, top }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="40%">Asset</Th>
<Th width="40%">Contract address</Th>
<Th width="20%" isNumeric>Quantity</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<ERC721TokensTableItem key={ item.token.address } { ...item }/>
)) }
</Tbody>
</Table>
);
};
export default ERC721TokensTable;
import { Tr, Td, Flex } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance;
const ERC721TokensTableItem = ({
token,
value,
}: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
return (
<Tr>
<Td verticalAlign="middle">
<Flex alignItems="center">
<TokenLogo hash={ token.address } name={ token.name } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/>
</Flex>
</Td>
<Td verticalAlign="middle">
<Flex alignItems="center" width="150px" justifyContent="space-between">
<Flex alignItems="center">
<AddressLink hash={ token.address } type="address" truncation="dynamic"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
</Flex>
<AddressAddToWallet token={ token } ml={ 4 }/>
</Flex>
</Td>
<Td isNumeric verticalAlign="middle">
{ value }
</Td>
</Tr>
);
};
export default React.memo(ERC721TokensTableItem);
......@@ -18,7 +18,7 @@ import LinkInternal from 'ui/shared/LinkInternal';
const WithdrawalsTableItem = ({ item }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : 'N/A';
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : '-';
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : '';
return (
<Tr>
......
......@@ -151,7 +151,7 @@ const TokenPageContent = () => {
const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery }/> },
(tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ?
{ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } :
undefined,
......@@ -175,7 +175,7 @@ const TokenPageContent = () => {
].filter(Boolean);
let hasPagination;
let pagination;
let pagination: PaginationProps | undefined;
if (!router.query.tab || router.query.tab === 'token_transfers') {
hasPagination = transfersQuery.isPaginationVisible;
......@@ -231,7 +231,7 @@ const TokenPageContent = () => {
<RoutedTabs
tabs={ tabs }
tabListProps={ tabListProps }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
rightSlot={ !isMobile && hasPagination && pagination ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile }
/>
) }
......
......@@ -6,9 +6,9 @@ import type { TokenInfo } from 'types/api/token';
import config from 'configs/app/config';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
......@@ -37,7 +37,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props
isLoading={ isLoading }
/>
<CopyToClipboard text={ address.hash } isLoading={ isLoading }/>
{ !isLoading && address.is_contract && token && <AddressAddToMetaMask ml={ 2 } token={ token }/> }
{ !isLoading && address.is_contract && token && <AddressAddToWallet ml={ 2 } token={ token }/> }
{ !isLoading && !address.is_contract && config.isAccountSupported && (
<AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/>
) }
......
......@@ -34,6 +34,7 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => {
icon={ <CopyIcon/> }
w="20px"
h="20px"
color="gray.500"
variant="simple"
display="inline-block"
flexShrink={ 0 }
......
import { Box, Icon, Tooltip, chakra } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import useToast from 'lib/hooks/useToast';
import useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets';
interface Props {
className?: string;
}
const NetworkAddToWallet = ({ className }: Props) => {
const toast = useToast();
const provider = useProvider();
const handleClick = React.useCallback(async() => {
try {
const hexadecimalChainId = '0x' + Number(appConfig.network.id).toString(16);
const config = {
method: 'wallet_addEthereumChain',
params: [ {
chainId: hexadecimalChainId,
chainName: appConfig.network.name,
nativeCurrency: {
name: appConfig.network.currency.name,
symbol: appConfig.network.currency.symbol,
decimals: appConfig.network.currency.decimals,
},
rpcUrls: [ appConfig.network.rpcUrl ],
blockExplorerUrls: [ appConfig.baseUrl ],
} ],
};
await provider?.request?.(config);
toast({
position: 'top-right',
title: 'Success',
description: 'Successfully added network to your wallet',
status: 'success',
variant: 'subtle',
isClosable: true,
});
} catch (error) {
toast({
position: 'top-right',
title: 'Error',
description: (error as Error)?.message || 'Something went wrong',
status: 'error',
variant: 'subtle',
isClosable: true,
});
}
}, [ provider, toast ]);
if (!provider) {
return null;
}
const defaultWallet = appConfig.web3.defaultWallet;
return (
<Tooltip label={ WALLETS_INFO[defaultWallet].add_network_text }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 5 }/>
</Box>
</Tooltip>
);
};
export default React.memo(chakra(NetworkAddToWallet));
......@@ -32,14 +32,6 @@ const TokenTransferTableItem = ({
timestamp,
enableTimeIncrement,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
return '-';
}
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
return (
......@@ -57,7 +49,7 @@ const TokenTransferTableItem = ({
</Flex>
</Td>
<Td lineHeight="30px">
{ 'token_id' in total ? <TokenTransferNft hash={ token.address } id={ total.token_id }/> : '-' }
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
</Td>
{ showTxInfo && txHash && (
<Td>
......@@ -85,7 +77,7 @@ const TokenTransferTableItem = ({
</Address>
</Td>
<Td isNumeric verticalAlign="top" lineHeight="30px">
{ value }
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Td>
</Tr>
);
......
......@@ -3,20 +3,23 @@ import React from 'react';
import type { TokenInfo } from 'types/api/token';
import metamaskIcon from 'icons/metamask.svg';
import appConfig from 'configs/app/config';
import useToast from 'lib/hooks/useToast';
import useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets';
interface Props {
className?: string;
token: TokenInfo;
}
const AddressAddToMetaMask = ({ className, token }: Props) => {
const AddressAddToWallet = ({ className, token }: Props) => {
const toast = useToast();
const provider = useProvider();
const handleClick = React.useCallback(async() => {
try {
const wasAdded = await window.ethereum.request?.({
const wasAdded = await provider?.request?.({
method: 'wallet_watchAsset',
params: {
type: 'ERC20', // Initially only supports ERC20, but eventually more!
......@@ -24,6 +27,8 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
address: token.address,
symbol: token.symbol,
decimals: Number(token.decimals) || 18,
// TODO: add token image when we have it in API
// image: ''
},
},
});
......@@ -32,7 +37,7 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
toast({
position: 'top-right',
title: 'Success',
description: 'Successfully added token to MetaMask',
description: 'Successfully added token to your wallet',
status: 'success',
variant: 'subtle',
isClosable: true,
......@@ -48,19 +53,21 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
isClosable: true,
});
}
}, [ toast, token ]);
}, [ toast, token, provider ]);
if (!('ethereum' in window)) {
if (!provider) {
return null;
}
const defaultWallet = appConfig.web3.defaultWallet;
return (
<Tooltip label="Add token to MetaMask">
<Tooltip label={ WALLETS_INFO[defaultWallet].add_token_text }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Icon as={ metamaskIcon } boxSize={ 6 }/>
<Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 6 }/>
</Box>
</Tooltip>
);
};
export default React.memo(chakra(AddressAddToMetaMask));
export default React.memo(chakra(AddressAddToWallet));
import { Box, VStack, Text, Stack, Icon, Link } from '@chakra-ui/react';
import { Box, Text, Stack, Icon, Link, VStack } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
......@@ -7,6 +7,7 @@ import statsIcon from 'icons/social/stats.svg';
import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
const SOCIAL_LINKS = [
{ link: appConfig.footerLinks.github, icon: ghIcon, label: 'Github link' },
......@@ -35,11 +36,9 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
return (
<VStack
as="footer"
spacing={ 8 }
borderTop="1px solid"
borderColor="divider"
width={{ base: '100%', lg: isExpanded ? '180px' : '20px', xl: isCollapsed ? '20px' : '180px' }}
paddingTop={{ base: 6, lg: 8 }}
marginTop={ marginTop }
alignItems="flex-start"
alignSelf="center"
......@@ -47,23 +46,28 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
fontSize="xs"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
>
{ SOCIAL_LINKS.length > 0 && (
<Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank">
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
}) }
</Stack>
) }
<Stack
direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}
mt={{ base: 6, lg: 8 }}
_empty={{
display: 'none',
}}
>
<NetworkAddToWallet/>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank">
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
}) }
</Stack>
<Box display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}>
<Text variant="secondary" mb={ 8 }>
<Text variant="secondary" mt={ 8 }>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
{ appConfig.blockScoutVersion &&
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text> }
<Text variant="secondary" mt={ 8 }>Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text> }
</Box>
</VStack>
);
......
......@@ -26,7 +26,13 @@ const test = base.extend({
]) as any,
});
test('no auth +@desktop-xl +@dark-mode-xl', async({ mount }) => {
test('no auth +@desktop-xl +@dark-mode-xl', async({ page, mount }) => {
await page.evaluate(() => {
window.ethereum = {
providers: [ { isMetaMask: true } ],
};
});
const component = await mount(
<TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch">
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment