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_ ...@@ -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_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_AD_ADBUTLER_ON=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__ 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 # api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__ 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_BASE_PATH=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_BASE_PATH__
NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__ NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__
NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__ 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_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__
NEXT_PUBLIC_VISUALIZE_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_VISUALIZE_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__ NEXT_PUBLIC_API_SPEC_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_SPEC_URL__
......
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
}, },
{ {
"type": "shell", "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": [], "problemMatcher": [],
"label": "dev server: goerli optimism", "label": "dev server: goerli optimism",
"detail": "start local dev server for Goerli Optimism network", "detail": "start local dev server for Goerli Optimism network",
...@@ -379,5 +379,15 @@ ...@@ -379,5 +379,15 @@
], ],
"default": "" "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 */ /* eslint-disable no-restricted-properties */
import type { WalletType } from 'types/client/wallets';
import type { NetworkExplorer } from 'types/networks'; import type { NetworkExplorer } from 'types/networks';
import type { ChainIndicatorId } from 'ui/home/indicators/types'; import type { ChainIndicatorId } from 'ui/home/indicators/types';
...@@ -11,6 +12,15 @@ const parseEnvJson = <DataType>(env: string | undefined): DataType | null => { ...@@ -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 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 env = process.env.NODE_ENV;
const isDev = env === 'development'; const isDev = env === 'development';
...@@ -36,6 +46,8 @@ const apiEndpoint = apiHost ? [ ...@@ -36,6 +46,8 @@ const apiEndpoint = apiHost ? [
apiPort && ':' + apiPort, apiPort && ':' + apiPort,
].filter(Boolean).join('') : 'https://blockscout.com'; ].filter(Boolean).join('') : 'https://blockscout.com';
const socketSchema = getEnvValue(process.env.NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL) || 'wss';
const logoutUrl = (() => { const logoutUrl = (() => {
try { try {
const envUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_URL); const envUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_URL);
...@@ -104,10 +116,14 @@ const config = Object.freeze({ ...@@ -104,10 +116,14 @@ const config = Object.freeze({
domainWithAd: getEnvValue(process.env.NEXT_PUBLIC_AD_DOMAIN_WITH_AD) || 'blockscout.com', domainWithAd: getEnvValue(process.env.NEXT_PUBLIC_AD_DOMAIN_WITH_AD) || 'blockscout.com',
adButlerOn: getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_ON) === 'true', 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: { api: {
host: apiHost, host: apiHost,
endpoint: apiEndpoint, 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) || ''), basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
}, },
L2: { L2: {
......
...@@ -3,6 +3,8 @@ NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/front ...@@ -3,6 +3,8 @@ NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/front
NEXT_PUBLIC_NETWORK_EXPLORERS= 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_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_GRAPHIQL_TRANSACTION=0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_WEB3_DEFAULT_WALLET=coinbase
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=true
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=Base Göerli NEXT_PUBLIC_NETWORK_NAME=Base Göerli
...@@ -10,14 +12,14 @@ NEXT_PUBLIC_NETWORK_SHORT_NAME=Base ...@@ -10,14 +12,14 @@ NEXT_PUBLIC_NETWORK_SHORT_NAME=Base
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=optimism 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_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_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_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS= NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation 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_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_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
......
...@@ -4,8 +4,8 @@ NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking ...@@ -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_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_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_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_GRADIENT=linear-gradient(136.9deg, \#235643 1.5%, \#16191E 77.77%)
#NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#DCFE76' #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_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 NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/poa.svg
......
blockscout: blockscout:
environment: environment:
INDEXER_OPTIMISM_L1_RPC:
_default: ENC[AES256_GCM,data:a02FoR3U/KlxsFVFiSGSLdFOFIDwS5eBgw==,iv:rmT8bVh3xyqKeebtnT+/eIC0bSGWKZhJ9H52cUsqFxM=,tag:OsRo1D2DtfMIgCGjAvqobw==,type:str]
ACCOUNT_USERNAME: ACCOUNT_USERNAME:
_default: ENC[AES256_GCM,data:n9Wc7xjBFdWHJNaKBwpVVykz3FbBtqicKdf6yD/kMLKuts/0Rv8vfQ20gSahIvSbbno=,iv:FRyRAwelWF1PHqbIJX09MH+VVqW53luYraLYq/A21j4=,tag:YUejqoXCZjWNUhRZ9emd+g==,type:str] _default: ENC[AES256_GCM,data:n9Wc7xjBFdWHJNaKBwpVVykz3FbBtqicKdf6yD/kMLKuts/0Rv8vfQ20gSahIvSbbno=,iv:FRyRAwelWF1PHqbIJX09MH+VVqW53luYraLYq/A21j4=,tag:YUejqoXCZjWNUhRZ9emd+g==,type:str]
ACCOUNT_PASSWORD: ACCOUNT_PASSWORD:
...@@ -46,8 +48,6 @@ blockscout: ...@@ -46,8 +48,6 @@ blockscout:
_default: ENC[AES256_GCM,data:fHIsaJQY6YrvoJKFDFZlBuunFD6QKYdUUOoW+aLV/44VCsWhrXbMUQ==,iv:teJEbP6pVC4WHeJwptf/DfRbp5Y8x/0OExTfClfLPyU=,tag:5nkNFZcJEFg5v0br2JUpnA==,type:str] _default: ENC[AES256_GCM,data:fHIsaJQY6YrvoJKFDFZlBuunFD6QKYdUUOoW+aLV/44VCsWhrXbMUQ==,iv:teJEbP6pVC4WHeJwptf/DfRbp5Y8x/0OExTfClfLPyU=,tag:5nkNFZcJEFg5v0br2JUpnA==,type:str]
RE_CAPTCHA_CLIENT_KEY: RE_CAPTCHA_CLIENT_KEY:
_default: ENC[AES256_GCM,data:ROEBG5XrOwAofN2ZnFnwekuqvHjDFu1Dp5V20Ud9gvqV1xc324InYA==,iv:EubA1HilDAtNaqdpTbFWaWSAf8kCiWgStada1dKpOD8=,tag:ry4796qJAekQKlz4sWGQKQ==,type:str] _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: scVerifier:
environment: environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY: SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
...@@ -88,8 +88,8 @@ sops: ...@@ -88,8 +88,8 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-04-25T13:14:30Z" lastmodified: "2023-04-27T16:27:47Z"
mac: ENC[AES256_GCM,data:/m/B6bN4MadVUtnZQUuZyCQX+X75mUP2pcQjUwZRGRbFcCzYYQLPV9fxgySNBbR+af0LXTHv4AilpOmiCCvCT0Yi6Q01+ac1YWJZ5U0kEBkhIXyOujYUHYeongWR7qzZmnujeV4d68ydEobteOH/EEmQymO7FrEiYJfQUOvomjU=,iv:/wFoZ0z12aSNpdG2kGGdORDdu6taDgG/yCAfY6bUiW8=,tag:TWza4eHHmq8EHJ41RiX01w==,type:str] 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: pgp:
- created_at: "2022-09-22T09:52:10Z" - created_at: "2022-09-22T09:52:10Z"
enc: | enc: |
......
...@@ -113,8 +113,6 @@ blockscout: ...@@ -113,8 +113,6 @@ blockscout:
_default: '4677000' _default: '4677000'
DISABLE_REALTIME_INDEXER: DISABLE_REALTIME_INDEXER:
_default: 'false' _default: 'false'
INDEXER_OPTIMISM_L1_RPC:
_default: http://65.108.226.29:8545
INDEXER_OPTIMISM_L1_PORTAL_CONTRACT: INDEXER_OPTIMISM_L1_PORTAL_CONTRACT:
_default: 0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383 _default: 0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383
INDEXER_OPTIMISM_L1_WITHDRAWALS_START_BLOCK: INDEXER_OPTIMISM_L1_WITHDRAWALS_START_BLOCK:
...@@ -279,7 +277,7 @@ frontend: ...@@ -279,7 +277,7 @@ frontend:
app: blockscout app: blockscout
enabled: true enabled: true
image: image:
_default: ghcr.io/blockscout/frontend:main _default: ghcr.io/blockscout/frontend:v1.0.8
ingress: ingress:
enabled: true enabled: true
# annotations: # annotations:
...@@ -393,6 +391,10 @@ frontend: ...@@ -393,6 +391,10 @@ frontend:
_default: '' _default: ''
NEXT_PUBLIC_NETWORK_RPC_URL: NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://goerli.optimism.io _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: NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs']" _default: "['daily_txs']"
NEXT_PUBLIC_IS_TESTNET: NEXT_PUBLIC_IS_TESTNET:
......
blockscout: blockscout:
environment: 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: ACCOUNT_USERNAME:
_default: ENC[AES256_GCM,data:xJ0hxcuU8u+QwiXciZ8qe1sV1GuxZV8Z9iYgnoWCu0ueuEmNE80jwe+vqviNb/UbOQs=,iv:uRNP5RTG/oxUngEoBbvLg9GzS5gYiHlL42yttgYWPAc=,tag:plQfCpG9pN7d28ooZ3aHrQ==,type:str] _default: ENC[AES256_GCM,data:xJ0hxcuU8u+QwiXciZ8qe1sV1GuxZV8Z9iYgnoWCu0ueuEmNE80jwe+vqviNb/UbOQs=,iv:uRNP5RTG/oxUngEoBbvLg9GzS5gYiHlL42yttgYWPAc=,tag:plQfCpG9pN7d28ooZ3aHrQ==,type:str]
ACCOUNT_PASSWORD: ACCOUNT_PASSWORD:
...@@ -33,9 +42,13 @@ blockscout: ...@@ -33,9 +42,13 @@ blockscout:
ACCOUNT_AUTH0_CLIENT_ID: ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str] _default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET: 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: ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str] _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: RE_CAPTCHA_SECRET_KEY:
_default: ENC[AES256_GCM,data:e9jfs4PwY+UPk0EuXZxERylKUFSUo6zsAz8oly3YwwybipP+A5rSBQ==,iv:TvrD/a+6+11IGDVFLUCn8U+H3v1YfIRrcndOVdt/taI=,tag:OAFVIznkeeeVX9UsigfP4A==,type:str] _default: ENC[AES256_GCM,data:e9jfs4PwY+UPk0EuXZxERylKUFSUo6zsAz8oly3YwwybipP+A5rSBQ==,iv:TvrD/a+6+11IGDVFLUCn8U+H3v1YfIRrcndOVdt/taI=,tag:OAFVIznkeeeVX9UsigfP4A==,type:str]
RE_CAPTCHA_CLIENT_KEY: RE_CAPTCHA_CLIENT_KEY:
...@@ -140,8 +153,8 @@ sops: ...@@ -140,8 +153,8 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-04-25T13:14:48Z" lastmodified: "2023-04-27T16:38:24Z"
mac: ENC[AES256_GCM,data:V1q4PteIRNS0Ly6Whh6sVFn41K305tJsPSfRW2IU2e/JC5ei565mvT8HBy6Z6OrjSTrMv60fmpA5LSPAmzIab9jqFcm35uIGZY39PtP1Zigb/90BXqydCwQkbfXaNAcigZ9PgSs2MiqrmRI6T9hAOP0pxR9UpjC5kpQjqWuvAtM=,iv:bdNpxqPWdbaySRHBvxkuhQTrPJCfdLWxhJhQXI8qr2g=,tag:r8aLUTNNH8WQWz2W7qG0xQ==,type:str] mac: ENC[AES256_GCM,data:/4nW+SI0HmMDkgMLptALsZoRg7vYXaYBCUOcKgbf44DLLOseeUpaiQszwG0SpAOu1gVm/aQTJRVnOmUvWKL0Pzd20vL+gc4KBMKlu/43k0jLz9H3lcO25UFa966qWt541noKgByoaRD1UMPJqTVjifAmrQOhtrg2BAbNEOgYmfY=,iv:RvsDkpSQim4EU7QtsalkUU/yYEUYeBf7ApyKvysCvkE=,tag:ENpb8eRDVRGCumVxCM4T2g==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
...@@ -63,15 +63,6 @@ blockscout: ...@@ -63,15 +63,6 @@ blockscout:
app: blockscout-prod app: blockscout-prod
# Blockscout environment variables # Blockscout environment variables
environment: 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: BLOCKSCOUT_VERSION:
_default: v5.1.2-beta _default: v5.1.2-beta
ECTO_USE_SSL: ECTO_USE_SSL:
......
...@@ -119,6 +119,10 @@ frontend: ...@@ -119,6 +119,10 @@ frontend:
_default: '' _default: ''
NEXT_PUBLIC_NETWORK_RPC_URL: NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://goerli.optimism.io _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: NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs']" _default: "['daily_txs']"
NEXT_PUBLIC_IS_TESTNET: NEXT_PUBLIC_IS_TESTNET:
......
...@@ -42,14 +42,16 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -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_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_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_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_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 | - | `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_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_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_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_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_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_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_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 ### Marketplace app configuration properties
...@@ -93,6 +95,7 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i ...@@ -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` | | 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` | | 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` | | 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 ### Network explorer configuration properties
...@@ -119,8 +122,10 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i ...@@ -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 | | 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_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_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_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` | | 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 = { type CPreferences = {
zone: string; zone: string;
...@@ -7,8 +7,10 @@ type CPreferences = { ...@@ -7,8 +7,10 @@ type CPreferences = {
} }
declare global { declare global {
interface Window { export interface Window {
ethereum: MetaMaskInpageProvider; ethereum?: {
providers?: Array<ExternalProvider>;
};
coinzilla_display: Array<CPreferences>; 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 = { ...@@ -344,6 +344,12 @@ export const RESOURCES = {
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
filterFields: [], 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
homepage_stats: { homepage_stats: {
...@@ -491,7 +497,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -491,7 +497,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_logs' | 'address_tokens' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
'token_instance_transfers' | 'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' | 'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | 'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals'; 'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
...@@ -548,6 +554,7 @@ Q extends 'token_holders' ? TokenHolders : ...@@ -548,6 +554,7 @@ Q extends 'token_holders' ? TokenHolders :
Q extends 'token_instance' ? TokenInstance : Q extends 'token_instance' ? TokenInstance :
Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount : Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount :
Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse : Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_instance_holders' ? TokenHolders :
Q extends 'token_inventory' ? TokenInventoryResponse : Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse : Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
......
...@@ -5,6 +5,7 @@ function generateCspPolicy() { ...@@ -5,6 +5,7 @@ function generateCspPolicy() {
const policyDescriptor = mergeDescriptors( const policyDescriptor = mergeDescriptors(
descriptors.app(), descriptors.app(),
descriptors.ad(), descriptors.ad(),
descriptors.cloudFlare(),
descriptors.googleAnalytics(), descriptors.googleAnalytics(),
descriptors.googleFonts(), descriptors.googleFonts(),
descriptors.googleReCaptcha(), 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 { ad } from './ad';
export { app } from './app'; export { app } from './app';
export { cloudFlare } from './cloudFlare';
export { googleAnalytics } from './googleAnalytics'; export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts'; export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha'; 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) { ...@@ -30,6 +30,8 @@ export function middleware(req: NextRequest) {
const res = NextResponse.next(); const res = NextResponse.next();
res.headers.append('Content-Security-Policy', cspPolicy); res.headers.append('Content-Security-Policy', cspPolicy);
res.headers.append('Server-Timing', `middleware;dur=${ end - start }`); 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; return res;
} }
......
...@@ -179,3 +179,8 @@ export const withRichMetadata: TokenInstance = { ...@@ -179,3 +179,8 @@ export const withRichMetadata: TokenInstance = {
status: null, 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 { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -73,7 +72,9 @@ test('token', async({ mount, page }) => { ...@@ -73,7 +72,9 @@ test('token', async({ mount, page }) => {
}), { times: 1 }); }), { times: 1 });
await page.evaluate(() => { await page.evaluate(() => {
window.ethereum = { } as MetaMaskInpageProvider; window.ethereum = {
providers: [ { isMetaMask: true } ],
};
}); });
const component = await mount( const component = await mount(
......
...@@ -8,11 +8,13 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -8,11 +8,13 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { tokenTabsByType } from 'ui/pages/Address'; import { tokenTabsByType } from 'ui/pages/Address';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; 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 TokenBalances from './tokens/TokenBalances';
import TokensWithIds from './tokens/TokensWithIds';
import TokensWithoutIds from './tokens/TokensWithoutIds';
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
marginBottom: 0, marginBottom: 0,
...@@ -32,21 +34,59 @@ const AddressTokens = () => { ...@@ -32,21 +34,59 @@ const AddressTokens = () => {
const scrollRef = React.useRef<HTMLDivElement>(null); 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', resourceName: 'address_tokens',
pathParams: { hash: router.query.hash?.toString() }, pathParams: { hash: router.query.hash?.toString() },
filters: { type: tokenType }, filters: { type: 'ERC-20' },
scrollRef, 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 = [ const tabs = [
{ id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <TokensWithoutIds tokensQuery={ tokensQuery }/> }, { id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <ERC20Tokens tokensQuery={ erc20Query }/> },
{ id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: <TokensWithoutIds tokensQuery={ tokensQuery }/> }, { id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: <ERC721Tokens tokensQuery={ erc721Query }/> },
{ id: tokenTabsByType['ERC-1155'], title: 'ERC-1155', component: <TokensWithIds tokensQuery={ tokensQuery }/> }, { 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 ( return (
<> <>
<TokenBalances/> <TokenBalances/>
...@@ -58,7 +98,7 @@ const AddressTokens = () => { ...@@ -58,7 +98,7 @@ const AddressTokens = () => {
colorScheme="gray" colorScheme="gray"
size="sm" size="sm"
tabListProps={ isMobile ? TAB_LIST_PROPS_MOBILE : TAB_LIST_PROPS } 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 } stickyEnabled={ !isMobile }
/> />
</> </>
......
...@@ -27,13 +27,12 @@ const AddressCoinBalanceTableItem = (props: Props) => { ...@@ -27,13 +27,12 @@ const AddressCoinBalanceTableItem = (props: Props) => {
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal> <LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal>
</Td> </Td>
<Td> <Td>
{ props.transaction_hash ? { props.transaction_hash &&
( (
<Address w="150px" fontWeight="700"> <Address w="150px" fontWeight="700">
<AddressLink hash={ props.transaction_hash } type="transaction"/> <AddressLink hash={ props.transaction_hash } type="transaction"/>
</Address> </Address>
) : )
<Text fontWeight="700">-</Text>
} }
</Td> </Td>
<Td> <Td>
......
...@@ -61,10 +61,12 @@ const TxInternalsListItem = ({ ...@@ -61,10 +61,12 @@ const TxInternalsListItem = ({
<InOutTag isIn={ isIn } isOut={ isOut }/> : <InOutTag isIn={ isIn } isOut={ isOut }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
} }
<Address width="calc((100% - 48px) / 2)"> { toData && (
<AddressIcon address={ toData }/> <Address width="calc((100% - 48px) / 2)">
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/> <AddressIcon address={ toData }/>
</Address> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/>
</Address>
) }
</Box> </Box>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text> <Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
......
...@@ -73,10 +73,12 @@ const AddressIntTxsTableItem = ({ ...@@ -73,10 +73,12 @@ const AddressIntTxsTableItem = ({
} }
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> { toData && (
<AddressIcon address={ toData }/> <Address display="inline-flex" maxW="100%">
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/> <AddressIcon address={ toData }/>
</Address> <AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/>
</Address>
) }
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() } { BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
......
...@@ -19,7 +19,7 @@ type Props = { ...@@ -19,7 +19,7 @@ type Props = {
}; };
} }
const TokensWithIds = ({ tokensQuery }: Props) => { const ERC1155Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery; const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
...@@ -69,4 +69,4 @@ const TokensWithIds = ({ tokensQuery }: Props) => { ...@@ -69,4 +69,4 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
); );
}; };
export default TokensWithIds; export default ERC1155Tokens;
...@@ -10,8 +10,8 @@ import DataListDisplay from 'ui/shared/DataListDisplay'; ...@@ -10,8 +10,8 @@ import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import TokensListItem from './TokensListItem'; import ERC20TokensListItem from './ERC20TokensListItem';
import TokensTable from './TokensTable'; import ERC20TokensTable from './ERC20TokensTable';
type Props = { type Props = {
tokensQuery: UseQueryResult<AddressTokensResponse> & { tokensQuery: UseQueryResult<AddressTokensResponse> & {
...@@ -20,7 +20,7 @@ type Props = { ...@@ -20,7 +20,7 @@ type Props = {
}; };
} }
const TokensWithoutIds = ({ tokensQuery }: Props) => { const ERC20Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery; const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
...@@ -33,8 +33,8 @@ const TokensWithoutIds = ({ tokensQuery }: Props) => { ...@@ -33,8 +33,8 @@ const TokensWithoutIds = ({ tokensQuery }: Props) => {
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Hide below="lg" ssr={ false }><TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide> <Hide below="lg" ssr={ false }><ERC20TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <TokensListItem key={ item.token.address } { ...item }/>) }</Show></> <Show below="lg" ssr={ false }>{ data.items.map(item => <ERC20TokensListItem key={ item.token.address } { ...item }/>) }</Show></>
) : null; ) : null;
return ( return (
...@@ -54,4 +54,4 @@ const TokensWithoutIds = ({ tokensQuery }: Props) => { ...@@ -54,4 +54,4 @@ const TokensWithoutIds = ({ tokensQuery }: Props) => {
}; };
export default TokensWithoutIds; export default ERC20Tokens;
...@@ -4,16 +4,15 @@ import React from 'react'; ...@@ -4,16 +4,15 @@ import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import AddressAddToMetaMask from '../details/AddressAddToMetaMask';
type Props = AddressTokenBalance; type Props = AddressTokenBalance;
const TokensListItem = ({ token, value }: Props) => { const ERC20TokensListItem = ({ token, value }: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' '); const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
...@@ -31,7 +30,7 @@ const TokensListItem = ({ token, value }: Props) => { ...@@ -31,7 +30,7 @@ const TokensListItem = ({ token, value }: Props) => {
<Flex alignItems="center" pl={ 8 }> <Flex alignItems="center" pl={ 8 }>
<AddressLink hash={ token.address } type="address" truncation="constant"/> <AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/> <CopyToClipboard text={ token.address } ml={ 1 }/>
<AddressAddToMetaMask token={ token } ml={ 2 }/> <AddressAddToWallet token={ token } ml={ 2 }/>
</Flex> </Flex>
{ token.exchange_rate !== undefined && token.exchange_rate !== null && ( { token.exchange_rate !== undefined && token.exchange_rate !== null && (
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
...@@ -53,4 +52,4 @@ const TokensListItem = ({ token, value }: Props) => { ...@@ -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'; ...@@ -5,14 +5,14 @@ import type { AddressTokenBalance } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import TokensTableItem from './TokensTableItem'; import ERC20TokensTableItem from './ERC20TokensTableItem';
interface Props { interface Props {
data: Array<AddressTokenBalance>; data: Array<AddressTokenBalance>;
top: number; top: number;
} }
const TokensTable = ({ data, top }: Props) => { const ERC20TokensTable = ({ data, top }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
<Thead top={ top }> <Thead top={ top }>
...@@ -26,11 +26,11 @@ const TokensTable = ({ data, top }: Props) => { ...@@ -26,11 +26,11 @@ const TokensTable = ({ data, top }: Props) => {
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item) => (
<TokensTableItem key={ item.token.address } { ...item }/> <ERC20TokensTableItem key={ item.token.address } { ...item }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
); );
}; };
export default TokensTable; export default ERC20TokensTable;
...@@ -4,15 +4,14 @@ import React from 'react'; ...@@ -4,15 +4,14 @@ import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import AddressAddToMetaMask from '../details/AddressAddToMetaMask';
type Props = AddressTokenBalance; type Props = AddressTokenBalance;
const TokensTableItem = ({ const ERC20TokensTableItem = ({
token, token,
value, value,
}: Props) => { }: Props) => {
...@@ -38,20 +37,20 @@ const TokensTableItem = ({ ...@@ -38,20 +37,20 @@ const TokensTableItem = ({
<AddressLink hash={ token.address } type="address" truncation="constant"/> <AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/> <CopyToClipboard text={ token.address } ml={ 1 }/>
</Flex> </Flex>
<AddressAddToMetaMask token={ token } ml={ 4 }/> <AddressAddToWallet token={ token } ml={ 4 }/>
</Flex> </Flex>
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ token.exchange_rate ? `$${ token.exchange_rate }` : '-' } { token.exchange_rate && `$${ token.exchange_rate }` }
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ tokenQuantity } { tokenQuantity }
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ tokenValue ? `$${ tokenValue }` : '-' } { tokenValue && `$${ tokenValue }` }
</Td> </Td>
</Tr> </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'; ...@@ -18,7 +18,7 @@ import LinkInternal from 'ui/shared/LinkInternal';
const WithdrawalsTableItem = ({ item }: Props) => { const WithdrawalsTableItem = ({ item }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : 'N/A'; 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 ( return (
<Tr> <Tr>
......
...@@ -151,7 +151,7 @@ const TokenPageContent = () => { ...@@ -151,7 +151,7 @@ const TokenPageContent = () => {
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> }, { 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') ? (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ?
{ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } : { id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } :
undefined, undefined,
...@@ -175,7 +175,7 @@ const TokenPageContent = () => { ...@@ -175,7 +175,7 @@ const TokenPageContent = () => {
].filter(Boolean); ].filter(Boolean);
let hasPagination; let hasPagination;
let pagination; let pagination: PaginationProps | undefined;
if (!router.query.tab || router.query.tab === 'token_transfers') { if (!router.query.tab || router.query.tab === 'token_transfers') {
hasPagination = transfersQuery.isPaginationVisible; hasPagination = transfersQuery.isPaginationVisible;
...@@ -231,7 +231,7 @@ const TokenPageContent = () => { ...@@ -231,7 +231,7 @@ const TokenPageContent = () => {
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ tabListProps } tabListProps={ tabListProps }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null } rightSlot={ !isMobile && hasPagination && pagination ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
) } ) }
......
...@@ -6,9 +6,9 @@ import type { TokenInfo } from 'types/api/token'; ...@@ -6,9 +6,9 @@ import type { TokenInfo } from 'types/api/token';
import config from 'configs/app/config'; import config from 'configs/app/config';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode'; import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -37,7 +37,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props ...@@ -37,7 +37,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props
isLoading={ isLoading } isLoading={ isLoading }
/> />
<CopyToClipboard text={ address.hash } 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 && ( { !isLoading && !address.is_contract && config.isAccountSupported && (
<AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/> <AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/>
) } ) }
......
...@@ -34,6 +34,7 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => { ...@@ -34,6 +34,7 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => {
icon={ <CopyIcon/> } icon={ <CopyIcon/> }
w="20px" w="20px"
h="20px" h="20px"
color="gray.500"
variant="simple" variant="simple"
display="inline-block" display="inline-block"
flexShrink={ 0 } 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 = ({ ...@@ -32,14 +32,6 @@ const TokenTransferTableItem = ({
timestamp, timestamp,
enableTimeIncrement, enableTimeIncrement,
}: Props) => { }: 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); const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
return ( return (
...@@ -57,7 +49,7 @@ const TokenTransferTableItem = ({ ...@@ -57,7 +49,7 @@ const TokenTransferTableItem = ({
</Flex> </Flex>
</Td> </Td>
<Td lineHeight="30px"> <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> </Td>
{ showTxInfo && txHash && ( { showTxInfo && txHash && (
<Td> <Td>
...@@ -85,7 +77,7 @@ const TokenTransferTableItem = ({ ...@@ -85,7 +77,7 @@ const TokenTransferTableItem = ({
</Address> </Address>
</Td> </Td>
<Td isNumeric verticalAlign="top" lineHeight="30px"> <Td isNumeric verticalAlign="top" lineHeight="30px">
{ value } { 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -3,20 +3,23 @@ import React from 'react'; ...@@ -3,20 +3,23 @@ import React from 'react';
import type { TokenInfo } from 'types/api/token'; 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 useToast from 'lib/hooks/useToast';
import useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets';
interface Props { interface Props {
className?: string; className?: string;
token: TokenInfo; token: TokenInfo;
} }
const AddressAddToMetaMask = ({ className, token }: Props) => { const AddressAddToWallet = ({ className, token }: Props) => {
const toast = useToast(); const toast = useToast();
const provider = useProvider();
const handleClick = React.useCallback(async() => { const handleClick = React.useCallback(async() => {
try { try {
const wasAdded = await window.ethereum.request?.({ const wasAdded = await provider?.request?.({
method: 'wallet_watchAsset', method: 'wallet_watchAsset',
params: { params: {
type: 'ERC20', // Initially only supports ERC20, but eventually more! type: 'ERC20', // Initially only supports ERC20, but eventually more!
...@@ -24,6 +27,8 @@ const AddressAddToMetaMask = ({ className, token }: Props) => { ...@@ -24,6 +27,8 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
address: token.address, address: token.address,
symbol: token.symbol, symbol: token.symbol,
decimals: Number(token.decimals) || 18, 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) => { ...@@ -32,7 +37,7 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
toast({ toast({
position: 'top-right', position: 'top-right',
title: 'Success', title: 'Success',
description: 'Successfully added token to MetaMask', description: 'Successfully added token to your wallet',
status: 'success', status: 'success',
variant: 'subtle', variant: 'subtle',
isClosable: true, isClosable: true,
...@@ -48,19 +53,21 @@ const AddressAddToMetaMask = ({ className, token }: Props) => { ...@@ -48,19 +53,21 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
isClosable: true, isClosable: true,
}); });
} }
}, [ toast, token ]); }, [ toast, token, provider ]);
if (!('ethereum' in window)) { if (!provider) {
return null; return null;
} }
const defaultWallet = appConfig.web3.defaultWallet;
return ( 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 }> <Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Icon as={ metamaskIcon } boxSize={ 6 }/> <Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 6 }/>
</Box> </Box>
</Tooltip> </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 React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -7,6 +7,7 @@ import statsIcon from 'icons/social/stats.svg'; ...@@ -7,6 +7,7 @@ import statsIcon from 'icons/social/stats.svg';
import tgIcon from 'icons/social/telega.svg'; import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg'; import twIcon from 'icons/social/tweet.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
const SOCIAL_LINKS = [ const SOCIAL_LINKS = [
{ link: appConfig.footerLinks.github, icon: ghIcon, label: 'Github link' }, { link: appConfig.footerLinks.github, icon: ghIcon, label: 'Github link' },
...@@ -35,11 +36,9 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => { ...@@ -35,11 +36,9 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
return ( return (
<VStack <VStack
as="footer" as="footer"
spacing={ 8 }
borderTop="1px solid" borderTop="1px solid"
borderColor="divider" borderColor="divider"
width={{ base: '100%', lg: isExpanded ? '180px' : '20px', xl: isCollapsed ? '20px' : '180px' }} width={{ base: '100%', lg: isExpanded ? '180px' : '20px', xl: isCollapsed ? '20px' : '180px' }}
paddingTop={{ base: 6, lg: 8 }}
marginTop={ marginTop } marginTop={ marginTop }
alignItems="flex-start" alignItems="flex-start"
alignSelf="center" alignSelf="center"
...@@ -47,23 +46,28 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => { ...@@ -47,23 +46,28 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
fontSize="xs" fontSize="xs"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) } { ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
> >
{ SOCIAL_LINKS.length > 0 && ( <Stack
<Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}> direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}
{ SOCIAL_LINKS.map(sl => { mt={{ base: 6, lg: 8 }}
return ( _empty={{
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank"> display: 'none',
<Icon as={ sl.icon } boxSize={ 5 }/> }}
</Link> >
); <NetworkAddToWallet/>
}) } { SOCIAL_LINKS.map(sl => {
</Stack> 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' }}> <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. Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text> </Text>
{ appConfig.blockScoutVersion && { 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> </Box>
</VStack> </VStack>
); );
......
...@@ -26,7 +26,13 @@ const test = base.extend({ ...@@ -26,7 +26,13 @@ const test = base.extend({
]) as any, ]) 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( const component = await mount(
<TestApp> <TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch"> <Flex w="100%" minH="100vh" alignItems="stretch">
......
...@@ -15,17 +15,17 @@ import TokenHoldersList from './TokenHoldersList'; ...@@ -15,17 +15,17 @@ import TokenHoldersList from './TokenHoldersList';
import TokenHoldersTable from './TokenHoldersTable'; import TokenHoldersTable from './TokenHoldersTable';
type Props = { type Props = {
tokenQuery: UseQueryResult<TokenInfo>; token?: TokenInfo;
holdersQuery: UseQueryResult<TokenHolders> & { holdersQuery: UseQueryResult<TokenHolders> & {
pagination: PaginationProps; pagination: PaginationProps;
isPaginationVisible: boolean; isPaginationVisible: boolean;
}; };
} }
const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => { const TokenHoldersContent = ({ holdersQuery, token }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (holdersQuery.isError || tokenQuery.isError) { if (holdersQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
...@@ -37,21 +37,21 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => { ...@@ -37,21 +37,21 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
const items = holdersQuery.data?.items; const items = holdersQuery.data?.items;
const content = items && tokenQuery.data ? ( const content = items && token ? (
<> <>
<Box display={{ base: 'none', lg: 'block' }}> <Box display={{ base: 'none', lg: 'block' }}>
<TokenHoldersTable <TokenHoldersTable
data={ items } data={ items }
token={ tokenQuery.data } token={ token }
top={ holdersQuery.isPaginationVisible ? 80 : 0 } top={ holdersQuery.isPaginationVisible ? 80 : 0 }
isLoading={ tokenQuery.isPlaceholderData || holdersQuery.isPlaceholderData } isLoading={ holdersQuery.isPlaceholderData }
/> />
</Box> </Box>
<Box display={{ base: 'block', lg: 'none' }}> <Box display={{ base: 'block', lg: 'none' }}>
<TokenHoldersList <TokenHoldersList
data={ items } data={ items }
token={ tokenQuery.data } token={ token }
isLoading={ tokenQuery.isPlaceholderData || holdersQuery.isPlaceholderData } isLoading={ holdersQuery.isPlaceholderData }
/> />
</Box> </Box>
</> </>
...@@ -59,7 +59,7 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => { ...@@ -59,7 +59,7 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ holdersQuery.isError || tokenQuery.isError } isError={ holdersQuery.isError }
isLoading={ false } isLoading={ false }
items={ holdersQuery.data?.items } items={ holdersQuery.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }} skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }}
......
...@@ -25,14 +25,6 @@ const TokenTransferTableItem = ({ ...@@ -25,14 +25,6 @@ const TokenTransferTableItem = ({
tokenId, tokenId,
isLoading, isLoading,
}: Props) => { }: 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, true); const timeAgo = useTimeAgoIncrement(timestamp, true);
return ( return (
...@@ -105,14 +97,14 @@ const TokenTransferTableItem = ({ ...@@ -105,14 +97,14 @@ const TokenTransferTableItem = ({
isDisabled={ Boolean(tokenId && tokenId === total.token_id) } isDisabled={ Boolean(tokenId && tokenId === total.token_id) }
isLoading={ isLoading } isLoading={ isLoading }
/> />
) : '-' ) : ''
} }
</Td> </Td>
) } ) }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && ( { (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Td isNumeric verticalAlign="top"> <Td isNumeric verticalAlign="top">
<Skeleton isLoaded={ !isLoading } my="7px"> <Skeleton isLoaded={ !isLoading } my="7px">
{ value || '-' } { 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Skeleton> </Skeleton>
</Td> </Td>
) } ) }
......
...@@ -14,7 +14,10 @@ import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; ...@@ -14,7 +14,10 @@ import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
import TokenInstanceDetails from './TokenInstanceDetails'; import TokenInstanceDetails from './TokenInstanceDetails';
...@@ -50,16 +53,28 @@ const TokenInstanceContent = () => { ...@@ -50,16 +53,28 @@ const TokenInstanceContent = () => {
}, },
}); });
const shouldFetchHolders = tokenInstanceQuery.data && !tokenInstanceQuery.data.is_unique;
const holdersQuery = useQueryWithPages({
resourceName: 'token_instance_holders',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && (!tab || tab === 'holders') && shouldFetchHolders),
},
});
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ {
id: 'token_transfers', id: 'token_transfers',
title: 'Token transfers', title: 'Token transfers',
component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id } token={ tokenInstanceQuery.data?.token }/>, component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id } token={ tokenInstanceQuery.data?.token }/>,
}, },
// there is no api for this tab yet shouldFetchHolders ?
// { id: 'holders', title: 'Holders', component: <span>Holders</span> }, { id: 'holders', title: 'Holders', component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenInstanceQuery.data?.token }/> } :
undefined,
{ id: 'metadata', title: 'Metadata', component: <TokenInstanceMetadata data={ tokenInstanceQuery.data?.metadata }/> }, { id: 'metadata', title: 'Metadata', component: <TokenInstanceMetadata data={ tokenInstanceQuery.data?.metadata }/> },
]; ].filter(Boolean);
if (tokenInstanceQuery.isError) { if (tokenInstanceQuery.isError) {
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error }); throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
...@@ -104,6 +119,17 @@ const TokenInstanceContent = () => { ...@@ -104,6 +119,17 @@ const TokenInstanceContent = () => {
} }
})(); })();
let pagination: PaginationProps | undefined;
let isPaginationVisible;
if (tab === 'token_transfers') {
pagination = transfersQuery.pagination;
isPaginationVisible = transfersQuery.isPaginationVisible;
} else if (tab === 'holders') {
pagination = holdersQuery.pagination;
isPaginationVisible = holdersQuery.isPaginationVisible;
}
return ( return (
<> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
...@@ -124,12 +150,14 @@ const TokenInstanceContent = () => { ...@@ -124,12 +150,14 @@ const TokenInstanceContent = () => {
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
<RoutedTabs { tokenInstanceQuery.isLoading ? <SkeletonTabs/> : (
tabs={ tabs } <RoutedTabs
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } } tabs={ tabs }
rightSlot={ !isMobile && transfersQuery.isPaginationVisible && tab !== 'metadata' ? <Pagination { ...transfersQuery.pagination }/> : null } tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
stickyEnabled={ !isMobile } rightSlot={ !isMobile && isPaginationVisible && pagination ? <Pagination { ...pagination }/> : null }
/> stickyEnabled={ !isMobile }
/>
) }
{ !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> } { !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</> </>
......
...@@ -11,8 +11,8 @@ import TokenInstanceDetails from './TokenInstanceDetails'; ...@@ -11,8 +11,8 @@ import TokenInstanceDetails from './TokenInstanceDetails';
const API_URL_ADDRESS = buildApiUrl('address', { hash: tokenInstanceMock.base.token.address }); const API_URL_ADDRESS = buildApiUrl('address', { hash: tokenInstanceMock.base.token.address });
const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', { const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', {
id: tokenInstanceMock.base.id, id: tokenInstanceMock.unique.id,
hash: tokenInstanceMock.base.token.address, hash: tokenInstanceMock.unique.token.address,
}); });
test('base view +@dark-mode +@mobile', async({ mount, page }) => { test('base view +@dark-mode +@mobile', async({ mount, page }) => {
...@@ -27,7 +27,7 @@ test('base view +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -27,7 +27,7 @@ test('base view +@dark-mode +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TokenInstanceDetails data={ tokenInstanceMock.base }/> <TokenInstanceDetails data={ tokenInstanceMock.unique }/>
</TestApp>, </TestApp>,
); );
......
...@@ -60,7 +60,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -60,7 +60,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
> >
<TokenSnippet hash={ data.token.address } name={ data.token.name }/> <TokenSnippet hash={ data.token.address } name={ data.token.name }/>
</DetailsInfoItem> </DetailsInfoItem>
{ data.owner && ( { data.is_unique && data.owner && (
<DetailsInfoItem <DetailsInfoItem
title="Owner" title="Owner"
hint="Current owner of this token instance" hint="Current owner of this token instance"
......
...@@ -42,7 +42,7 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => { ...@@ -42,7 +42,7 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
href={ url } href={ url }
onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined } onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined }
> >
{ transfersCountQuery.data.transfers_count } { transfersCountQuery.data.transfers_count.toLocaleString() }
</LinkInternal> </LinkInternal>
</DetailsInfoItem> </DetailsInfoItem>
); );
......
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask'; import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
...@@ -60,13 +60,13 @@ const TokensTableItem = ({ ...@@ -60,13 +60,13 @@ const TokensTableItem = ({
<AddressLink fontSize="sm" hash={ address } type="address" truncation="constant"/> <AddressLink fontSize="sm" hash={ address } type="address" truncation="constant"/>
<CopyToClipboard text={ address } ml={ 1 }/> <CopyToClipboard text={ address } ml={ 1 }/>
</Flex> </Flex>
<AddressAddToMetaMask token={ token }/> <AddressAddToWallet token={ token }/>
</Flex> </Flex>
</Flex> </Flex>
{ exchangeRate && ( { exchangeRate && (
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Price</Text> <Text fontSize="sm" fontWeight={ 500 }>Price</Text>
<Text fontSize="sm" variant="secondary">{ exchangeRate || '-' }</Text> <Text fontSize="sm" variant="secondary">{ exchangeRate }</Text>
</HStack> </HStack>
) } ) }
{ totalValue?.usd && ( { totalValue?.usd && (
......
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask'; import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
...@@ -61,15 +61,15 @@ const TokensTableItem = ({ ...@@ -61,15 +61,15 @@ const TokensTableItem = ({
<AddressLink fontSize="sm" hash={ address } type="address" truncation="constant" fontWeight={ 500 }/> <AddressLink fontSize="sm" hash={ address } type="address" truncation="constant" fontWeight={ 500 }/>
<CopyToClipboard text={ address } ml={ 1 }/> <CopyToClipboard text={ address } ml={ 1 }/>
</Flex> </Flex>
<AddressAddToMetaMask token={ token }/> <AddressAddToWallet token={ token }/>
</Flex> </Flex>
<Tag flexShrink={ 0 } ml={ 8 } mt={ 3 }>{ type }</Tag> <Tag flexShrink={ 0 } ml={ 8 } mt={ 3 }>{ type }</Tag>
</Box> </Box>
</Flex> </Flex>
</Td> </Td>
<Td isNumeric><Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ exchangeRate ? `$${ exchangeRate }` : '-' }</Text></Td> <Td isNumeric><Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ exchangeRate && `$${ exchangeRate }` }</Text></Td>
<Td isNumeric maxWidth="300px" width="300px"> <Td isNumeric maxWidth="300px" width="300px">
<Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ totalValue?.usd ? `$${ totalValue.usd }` : '-' }</Text> <Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ totalValue?.usd && `$${ totalValue.usd }` }</Text>
</Td> </Td>
<Td isNumeric><Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ Number(holders).toLocaleString() }</Text></Td> <Td isNumeric><Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ Number(holders).toLocaleString() }</Text></Td>
</Tr> </Tr>
......
...@@ -31,10 +31,12 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -31,10 +31,12 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit:
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address> </Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<Address width="calc((100% - 48px) / 2)"> { toData && (
<AddressIcon address={ toData }/> <Address width="calc((100% - 48px) / 2)">
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash }/> <AddressIcon address={ toData }/>
</Address> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash }/>
</Address>
) }
</Box> </Box>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text> <Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
......
...@@ -40,10 +40,12 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -40,10 +40,12 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit:
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> { toData && (
<AddressIcon address={ toData }/> <Address display="inline-flex" maxW="100%">
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/> <AddressIcon address={ toData }/>
</Address> <AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/>
</Address>
) }
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() } { BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
......
...@@ -88,13 +88,13 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -88,13 +88,13 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
</VStack> </VStack>
</Td> </Td>
<Td whiteSpace="nowrap"> <Td whiteSpace="nowrap">
{ tx.method ? ( { tx.method && (
<TruncatedTextTooltip label={ tx.method }> <TruncatedTextTooltip label={ tx.method }>
<Tag colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }> <Tag colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }>
{ tx.method } { tx.method }
</Tag> </Tag>
</TruncatedTextTooltip> </TruncatedTextTooltip>
) : '-' } ) }
</Td> </Td>
{ showBlockInfo && ( { showBlockInfo && (
<Td> <Td>
......
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