Commit 18fe01d3 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into feat/verified-tokens

parents 4f84209a cc425ddb
...@@ -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_CONTRACT_INFO_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_CONTRACT_INFO_API_HOST__ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_CONTRACT_INFO_API_HOST__
...@@ -67,3 +70,6 @@ NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_GOOGLE_AN ...@@ -67,3 +70,6 @@ NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_GOOGLE_AN
NEXT_PUBLIC_IS_L2_NETWORK=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_L2_NETWORKL__ NEXT_PUBLIC_IS_L2_NETWORK=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_L2_NETWORKL__
NEXT_PUBLIC_L1_BASE_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_L1_BASE_URL__ NEXT_PUBLIC_L1_BASE_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_L1_BASE_URL__
NEXT_PUBLIC_L2_WITHDRAWAL_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_L2_WITHDRAWAL_URL__ NEXT_PUBLIC_L2_WITHDRAWAL_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_L2_WITHDRAWAL_URL__
# beacon chain config
NEXT_PUBLIC_HAS_BEACON_CHAIN=__PLACEHOLDER_FOR_NEXT_PUBLIC_HAS_BEACON_CHAIN__
...@@ -199,7 +199,7 @@ module.exports = { ...@@ -199,7 +199,7 @@ module.exports = {
groups: [ groups: [
'module', 'module',
'/types/', '/types/',
[ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^mocks/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ], [ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^mocks/', '/^pages/', '/^playwright/', '/^stubs/', '/^theme/', '/^ui/' ],
[ 'parent', 'sibling', 'index' ], [ 'parent', 'sibling', 'index' ],
], ],
alphabetize: { order: 'asc', ignoreCase: true }, alphabetize: { order: 'asc', ignoreCase: true },
......
...@@ -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: {
...@@ -115,6 +131,9 @@ const config = Object.freeze({ ...@@ -115,6 +131,9 @@ const config = Object.freeze({
L1BaseUrl: getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_URL), L1BaseUrl: getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_URL),
withdrawalUrl: getEnvValue(process.env.NEXT_PUBLIC_L2_WITHDRAWAL_URL) || '', withdrawalUrl: getEnvValue(process.env.NEXT_PUBLIC_L2_WITHDRAWAL_URL) || '',
}, },
beaconChain: {
hasBeaconChain: getEnvValue(process.env.NEXT_PUBLIC_HAS_BEACON_CHAIN) === 'true',
},
statsApi: { statsApi: {
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST), endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
basePath: '', basePath: '',
......
...@@ -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:
...@@ -16,10 +18,6 @@ blockscout: ...@@ -16,10 +18,6 @@ blockscout:
_default: ENC[AES256_GCM,data:bgP4VwZ91eMzJVQ3/+fkqNCwBIAv3P12qRoGWu9QkfCKpj3e+Dwt1qjsYV1iNkcjQAJx3jOpVGlsbgdcBGFzHw==,iv:gpa6tbkxHv56wwI3Owdnr5MArJYdiVO3A0UMOsrgCls=,tag:CIgupR1ZNjsVtFYVG2OtHA==,type:str] _default: ENC[AES256_GCM,data:bgP4VwZ91eMzJVQ3/+fkqNCwBIAv3P12qRoGWu9QkfCKpj3e+Dwt1qjsYV1iNkcjQAJx3jOpVGlsbgdcBGFzHw==,iv:gpa6tbkxHv56wwI3Owdnr5MArJYdiVO3A0UMOsrgCls=,tag:CIgupR1ZNjsVtFYVG2OtHA==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL: ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:+EoMWtXdJA5DnRprCaps38VbQlLcJikKvB7FA0VMhaw+aq1d/rh5yIxl/CQBIo2eO8EESoVz6eUjzvgchbmv2IaiGgG3DnZx,iv:nfPmwEZMXM2SOIZ3OMr5u7GTbX6ni5VuFpn5SyHKMfE=,tag:4ePZ5NZ+rdBZf8xWDfEbzA==,type:str] _default: ENC[AES256_GCM,data:+EoMWtXdJA5DnRprCaps38VbQlLcJikKvB7FA0VMhaw+aq1d/rh5yIxl/CQBIo2eO8EESoVz6eUjzvgchbmv2IaiGgG3DnZx,iv:nfPmwEZMXM2SOIZ3OMr5u7GTbX6ni5VuFpn5SyHKMfE=,tag:4ePZ5NZ+rdBZf8xWDfEbzA==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:Kw/WWdVpNWAOjwMLM2GJtQs0PaLQNth2w6qEU6vY86A6r0kdGO1If5XA14h8OiGIfGQ+KNizNaB5eTxmjUMPug==,iv:9idS4yx/ETwGaEGyUBsgcf8siyRo0aF0rGSOzO0uzw4=,tag:Z7fqs0YEavA0FdAEwSwskQ==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:FpIq9cXpqmBXpmVBj9seZB7vpbuJ5q+Iz5ChgL7+g6dgosufrO93tl4wsQk=,iv:DM4qKBrIyeQOmMMSPhEKiW26kEL0yHVA58na8tXZX0o=,tag:kTEy1KUinGUfe/cq1UARQA==,type:str]
ACCOUNT_SENDGRID_API_KEY: ACCOUNT_SENDGRID_API_KEY:
_default: ENC[AES256_GCM,data:i08k9qiHA/31nyAri5pIm8MqCUZXLWjvQgcYpAyrcsToszt/L+QsYVcdCEerI0udtd2gJvLuRz3k8GcpZ9OfQB2Y0kJn,iv:Pt3rg7GjhfDw5S4VV9HpLSDsO4AhXlGIsNhdc5rYqCQ=,tag:zL/bLVgSS41kgnv69GxLMg==,type:str] _default: ENC[AES256_GCM,data:i08k9qiHA/31nyAri5pIm8MqCUZXLWjvQgcYpAyrcsToszt/L+QsYVcdCEerI0udtd2gJvLuRz3k8GcpZ9OfQB2Y0kJn,iv:Pt3rg7GjhfDw5S4VV9HpLSDsO4AhXlGIsNhdc5rYqCQ=,tag:zL/bLVgSS41kgnv69GxLMg==,type:str]
ACCOUNT_SENDGRID_SENDER: ACCOUNT_SENDGRID_SENDER:
...@@ -50,8 +48,6 @@ blockscout: ...@@ -50,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:
...@@ -92,8 +88,8 @@ sops: ...@@ -92,8 +88,8 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-03-01T17:32:26Z" lastmodified: "2023-04-27T16:27:47Z"
mac: ENC[AES256_GCM,data:X1CPQWSFgieHg/elCyqPqQekMYsK/Go4R4sOsNE4XACseUzQr725vlo7XqZqPmRnnXz5bJu6MZAjSTbGCrPzybZV7ZUv94j9uPQSWH4x9SJicNe9ZHApeWapb/+PDz6xO9k8Tdug8WgVgy5kilHdYi6uGoIgeAwC7RwNG7TfyeQ=,iv:MpGf7GkzmVw5MZn0IMCDBAg0WQO1mzDc2ptHUxQNHUk=,tag:hsHMaU4MMc7df6Twc8dqSQ==,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:
...@@ -24,6 +33,8 @@ blockscout: ...@@ -24,6 +33,8 @@ blockscout:
_default: ENC[AES256_GCM,data:Cqga1O4fbdtx5GfEQjJEPYWqeig7SBAdKiIif9sGCNrdy5FSHr43uskfdOdiS3uhaHm925jhPf+/nvs7VRzaqSAJCB2HBrVjJLOTQItVEw5rGus9Ma2plubDdrXh2CHO14mjE6f1/QOq2MLC1S79MqeHxpgqS0sgMflKopWa3/4=,iv:z1mBiInLa3kutmPFuX+R96rUSGcjFr/UH6cn/UbM0tg=,tag:8TRBcnTENdguj2BOs4Qrmw==,type:str] _default: ENC[AES256_GCM,data:Cqga1O4fbdtx5GfEQjJEPYWqeig7SBAdKiIif9sGCNrdy5FSHr43uskfdOdiS3uhaHm925jhPf+/nvs7VRzaqSAJCB2HBrVjJLOTQItVEw5rGus9Ma2plubDdrXh2CHO14mjE6f1/QOq2MLC1S79MqeHxpgqS0sgMflKopWa3/4=,iv:z1mBiInLa3kutmPFuX+R96rUSGcjFr/UH6cn/UbM0tg=,tag:8TRBcnTENdguj2BOs4Qrmw==,type:str]
DATABASE_URL: DATABASE_URL:
_default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str] _default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str]
API_SENSITIVE_ENDPOINTS_KEY:
_default: ENC[AES256_GCM,data:cghX4B0yt18xT8kd/buPd6L/zLo/jIZzyVSDPoDGHiDMXoQRxHuHtITQbCjD+w0UdFZqDvDvAC6VUc5veCfITw==,iv:h9iTyEEhxSmStgA8ycncELR1A6AcWezk+zYIyo5Fe/Q=,tag:cC/ye9+Z9MvNR3h5o7JBJg==,type:str]
ACCOUNT_DATABASE_URL: ACCOUNT_DATABASE_URL:
_default: ENC[AES256_GCM,data:HF5y8ezV5TiLqeh98WDp4rXQeUfSBETyWVHOyNZNy5pt4MdiKTdFBLauOJpD6YHWynMFsd8IJLRNLrBn4qGe3RfwprR6v3WN9Q==,iv:B9AJXO7EJexsPgDHb5s5tzpadVYoZ79fyaL8NOYXSEw=,tag:lyNKQ13Q+pn+3DrAXIzIKQ==,type:str] _default: ENC[AES256_GCM,data:HF5y8ezV5TiLqeh98WDp4rXQeUfSBETyWVHOyNZNy5pt4MdiKTdFBLauOJpD6YHWynMFsd8IJLRNLrBn4qGe3RfwprR6v3WN9Q==,iv:B9AJXO7EJexsPgDHb5s5tzpadVYoZ79fyaL8NOYXSEw=,tag:lyNKQ13Q+pn+3DrAXIzIKQ==,type:str]
ACCOUNT_REDIS_URL: ACCOUNT_REDIS_URL:
...@@ -33,7 +44,7 @@ blockscout: ...@@ -33,7 +44,7 @@ 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: ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
...@@ -144,8 +155,8 @@ sops: ...@@ -144,8 +155,8 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-04-18T09:26:42Z" lastmodified: "2023-05-09T16:11:07Z"
mac: ENC[AES256_GCM,data:4MdP3Fi3SDxyXjhyI7jogYdigziLkp7oh4M6BthtT2FuUu+XAGnONAdccxULJfXtKS6ue27BtV/pKWhdKeOqKIN23BJERqa4cFgYlyd85XAyL400/fEKEO4nCe2c9VIqNRUQls28/WkRjzRjvzQMMTIRIAJvfrJUYRpIabXWqAo=,iv:Z4shgbH8uJNuoJvmyyQjQ+fZsXuSC3nIGYb6GYO/nac=,tag:IDZvpGiZH41IwAgSidF3xQ==,type:str] mac: ENC[AES256_GCM,data:JHnHHGNOw84O5No7UDTLSGnLwUJHUUZaY/UBPetpa99OFDMlzFvEnjIh4DjIqhVLXR5DBZJr7BTibQLyo6y/aXlWKDxHvLklLGunKQaQ1Xl/JzDKRTYlAs/JsJRR0ElzzyPmbCKbSJOX0tAtNAuhH6EFKYg+HLYmPHtYcvM1Kck=,iv:OsKv7efW0xJ4ZT/38Uyq0feqVC4i2x4srzG+mqd2Avg=,tag:MWfjO8r3iJSMGluqIHc9jA==,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` |
| NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` | Contract Info API endpoint url | - | - | `https://my-host.com` | | NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` | Contract Info API endpoint url | - | - | `https://my-host.com` |
...@@ -147,6 +152,15 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i ...@@ -147,6 +152,15 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
| NEXT_PUBLIC_L1_BASE_URL | `string` | Base Blockscout URL for L1 network | yes | - | `'http://eth-goerli.blockscout.com'` | | NEXT_PUBLIC_L1_BASE_URL | `string` | Base Blockscout URL for L1 network | yes | - | `'http://eth-goerli.blockscout.com'` |
| NEXT_PUBLIC_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | yes | - | `https://app.optimism.io/bridge/withdraw` | | NEXT_PUBLIC_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | yes | - | `https://app.optimism.io/bridge/withdraw` |
## Beacon chain configuration
| Variable | Type| Description | Is required | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_HAS_BEACON_CHAIN | `boolean` | Set to true for networks with the beacon chain | - | - | `true` |
# How to add new environment variable # How to add new environment variable
If the variable should be exposed to the browser don't forget to add prefix `NEXT_PUBLIC_` to its name. If the variable should be exposed to the browser don't forget to add prefix `NEXT_PUBLIC_` to its name.
......
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 fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 20"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.645 19.375h-10A1.875 1.875 0 0 1 3.77 17.5V5.625a.625.625 0 0 1 1.25 0V17.5a.625.625 0 0 0 .625.625h10a.625.625 0 0 0 .625-.625V5.625a.625.625 0 0 1 1.25 0V17.5a1.875 1.875 0 0 1-1.875 1.875Zm2.5-15h-15a.625.625 0 0 1 0-1.25h15a.625.625 0 1 1 0 1.25Z"/> <path d="M15 19.375H5A1.875 1.875 0 0 1 3.125 17.5V5.625a.625.625 0 0 1 1.25 0V17.5a.625.625 0 0 0 .625.625h10a.624.624 0 0 0 .625-.625V5.625a.625.625 0 1 1 1.25 0V17.5A1.875 1.875 0 0 1 15 19.375Zm2.5-15h-15a.625.625 0 0 1 0-1.25h15a.625.625 0 1 1 0 1.25Z" fill="currentColor"/>
<path d="M13.145 4.375a.625.625 0 0 1-.625-.625V1.875H8.77V3.75a.625.625 0 0 1-1.25 0v-2.5a.625.625 0 0 1 .625-.625h5a.625.625 0 0 1 .625.625v2.5a.625.625 0 0 1-.625.625Zm-2.5 11.875a.625.625 0 0 1-.625-.625v-8.75a.625.625 0 0 1 1.25 0v8.75a.625.625 0 0 1-.625.625ZM13.77 15a.625.625 0 0 1-.625-.625v-6.25a.625.625 0 0 1 1.25 0v6.25a.625.625 0 0 1-.625.625Zm-6.25 0a.625.625 0 0 1-.625-.625v-6.25a.625.625 0 0 1 1.25 0v6.25A.625.625 0 0 1 7.52 15Z"/> <path d="M12.5 4.375a.625.625 0 0 1-.625-.625V1.875h-3.75V3.75a.625.625 0 0 1-1.25 0v-2.5A.625.625 0 0 1 7.5.625h5a.625.625 0 0 1 .625.625v2.5a.625.625 0 0 1-.625.625ZM10 16.25a.625.625 0 0 1-.625-.625v-8.75a.625.625 0 0 1 1.25 0v8.75a.624.624 0 0 1-.625.625ZM13.125 15a.624.624 0 0 1-.625-.625v-6.25a.625.625 0 1 1 1.25 0v6.25a.624.624 0 0 1-.625.625Zm-6.25 0a.625.625 0 0 1-.625-.625v-6.25a.625.625 0 0 1 1.25 0v6.25a.625.625 0 0 1-.625.625Z" fill="currentColor"/>
</svg> </svg>
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.433 3.6 16.399.569A1.926 1.926 0 0 0 15.03 0c-.518 0-1.005.202-1.37.568L.961 13.264a.779.779 0 0 0-.221.446l-.734 5.406a.78.78 0 0 0 .877.877l5.406-.734a.78.78 0 0 0 .446-.221L19.433 6.342c.366-.366.567-.853.567-1.37 0-.518-.201-1.005-.567-1.371ZM5.82 17.75l-4.131.561.56-4.131 8.997-8.997 3.571 3.57L5.82 17.75ZM18.33 5.24l-2.41 2.412-3.57-3.57 2.411-2.413a.379.379 0 0 1 .538 0l3.033 3.033a.379.379 0 0 1 0 .538Z"/> <path d="M19.432 3.6 16.4.569A1.925 1.925 0 0 0 15.03 0c-.518 0-1.005.202-1.371.568L.962 13.264a.779.779 0 0 0-.221.446l-.734 5.406a.779.779 0 0 0 .877.877l5.406-.734a.778.778 0 0 0 .446-.221L19.432 6.342c.366-.366.568-.853.568-1.37 0-.518-.202-1.005-.568-1.371ZM5.82 17.75l-4.132.561.561-4.131 8.997-8.997 3.57 3.57L5.82 17.75ZM18.33 5.24l-2.41 2.412-3.571-3.57L14.76 1.67a.379.379 0 0 1 .537 0l3.034 3.032a.378.378 0 0 1 0 .538Z" fill="currentColor"/>
</svg> </svg>
<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>
...@@ -23,9 +23,10 @@ import type { ...@@ -23,9 +23,10 @@ import type {
AddressTokenTransferFilters, AddressTokenTransferFilters,
AddressTokensFilter, AddressTokensFilter,
AddressTokensResponse, AddressTokensResponse,
AddressWithdrawalsResponse,
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses'; import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts'; import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
...@@ -54,6 +55,7 @@ import type { TransactionsResponseValidated, TransactionsResponsePending, Transa ...@@ -54,6 +55,7 @@ import type { TransactionsResponseValidated, TransactionsResponsePending, Transa
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges'; import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization'; import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse } from 'types/api/withdrawals';
import type { ArrayElement } from 'types/utils'; import type { ArrayElement } from 'types/utils';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -168,6 +170,12 @@ export const RESOURCES = { ...@@ -168,6 +170,12 @@ export const RESOURCES = {
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
block_withdrawals: {
path: '/api/v2/blocks/:height/withdrawals',
pathParams: [ 'height' as const ],
paginationFields: [ 'items_count' as const, 'index' as const ],
filterFields: [],
},
txs_validated: { txs_validated: {
path: '/api/v2/transactions', path: '/api/v2/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'filter' as const, 'index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'filter' as const, 'index' as const ],
...@@ -208,6 +216,11 @@ export const RESOURCES = { ...@@ -208,6 +216,11 @@ export const RESOURCES = {
path: '/api/v2/transactions/:hash/state-changes', path: '/api/v2/transactions/:hash/state-changes',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
}, },
withdrawals: {
path: '/api/v2/withdrawals',
paginationFields: [ 'index' as const, 'items_count' as const ],
filterFields: [],
},
// ADDRESSES // ADDRESSES
addresses: { addresses: {
...@@ -275,6 +288,12 @@ export const RESOURCES = { ...@@ -275,6 +288,12 @@ export const RESOURCES = {
paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const ], paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
address_withdrawals: {
path: '/api/v2/addresses/:hash/withdrawals',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'index' as const ],
filterFields: [],
},
// CONTRACT // CONTRACT
contract: { contract: {
...@@ -372,6 +391,12 @@ export const RESOURCES = { ...@@ -372,6 +391,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: {
...@@ -519,9 +544,10 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -519,9 +544,10 @@ 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';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -550,6 +576,7 @@ Q extends 'stats_line' ? StatsChart : ...@@ -550,6 +576,7 @@ Q extends 'stats_line' ? StatsChart :
Q extends 'blocks' ? BlocksResponse : Q extends 'blocks' ? BlocksResponse :
Q extends 'block' ? Block : Q extends 'block' ? Block :
Q extends 'block_txs' ? BlockTransactionsResponse : Q extends 'block_txs' ? BlockTransactionsResponse :
Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
Q extends 'txs_validated' ? TransactionsResponseValidated : Q extends 'txs_validated' ? TransactionsResponseValidated :
Q extends 'txs_pending' ? TransactionsResponsePending : Q extends 'txs_pending' ? TransactionsResponsePending :
Q extends 'tx' ? Transaction : Q extends 'tx' ? Transaction :
...@@ -569,6 +596,7 @@ Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse : ...@@ -569,6 +596,7 @@ Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress : Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse : Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'address_withdrawals' ? AddressWithdrawalsResponse :
Q extends 'token' ? TokenInfo : Q extends 'token' ? TokenInfo :
Q extends 'token_verified_info' ? TokenVerifiedInfo : Q extends 'token_verified_info' ? TokenVerifiedInfo :
Q extends 'token_counters' ? TokenCounters : Q extends 'token_counters' ? TokenCounters :
...@@ -577,6 +605,7 @@ Q extends 'token_holders' ? TokenHolders : ...@@ -577,6 +605,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 :
...@@ -590,6 +619,7 @@ Q extends 'verified_contracts' ? VerifiedContractsResponse : ...@@ -590,6 +619,7 @@ Q extends 'verified_contracts' ? VerifiedContractsResponse :
Q extends 'verified_contracts_counters' ? VerifiedContractsCounters : Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
Q extends 'visualize_sol2uml' ? VisualizedContract : Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig : Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'l2_output_roots' ? L2OutputRootsResponse : Q extends 'l2_output_roots' ? L2OutputRootsResponse :
Q extends 'l2_withdrawals' ? L2WithdrawalsResponse : Q extends 'l2_withdrawals' ? L2WithdrawalsResponse :
Q extends 'l2_deposits' ? L2DepositsResponse : Q extends 'l2_deposits' ? L2DepositsResponse :
......
...@@ -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';
......
...@@ -125,7 +125,14 @@ export default function useNavItems(): ReturnType { ...@@ -125,7 +125,14 @@ export default function useNavItems(): ReturnType {
blocks, blocks,
topAccounts, topAccounts,
verifiedContracts, verifiedContracts,
]; appConfig.beaconChain.hasBeaconChain && {
text: 'Withdrawals',
nextRoute: { pathname: '/withdrawals' as const },
icon: withdrawalsIcon,
isActive: pathname === '/withdrawals',
isNewUi: true,
},
].filter(Boolean);
} }
const otherNavItems: Array<NavItem> = [ const otherNavItems: Array<NavItem> = [
......
import type { GetServerSideProps } from 'next';
import appConfig from 'configs/app/config';
import type { Props } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
export const getServerSideProps: GetServerSideProps<Props> = async(args) => {
if (!appConfig.beaconChain.hasBeaconChain) {
return {
notFound: true,
};
}
return getServerSidePropsBase(args);
};
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,
};
export const data = {
items: [
{
amount: '192175',
block_number: 43242,
index: 11688,
receiver: {
hash: '0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
},
timestamp: '2022-06-07T18:12:24.000000Z',
validator_index: 49622,
},
{
amount: '192175',
block_number: 43242,
index: 11687,
receiver: {
hash: '0xf97e987c050e5Ab072211Ad2C213Eb5AEE4DF134',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
},
timestamp: '2022-05-07T18:12:24.000000Z',
validator_index: 49621,
},
{
amount: '182773',
block_number: 43242,
index: 11686,
receiver: {
hash: '0xf97e123c050e5Ab072211Ad2C213Eb5AEE4DF134',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
},
timestamp: '2022-04-07T18:12:24.000000Z',
validator_index: 49620,
},
],
next_page_params: {
index: 11639,
items_count: 50,
},
};
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/token/types'; import type { PageParams } from 'lib/next/token/types';
import getSeo from 'lib/next/token/getSeo'; import getSeo from 'lib/next/token/getSeo';
import Token from 'ui/pages/Token'; import Page from 'ui/shared/Page/Page';
const Token = dynamic(() => import('ui/pages/Token'), { ssr: false });
const TokenPage: NextPage<PageParams> = ({ hash }: PageParams) => { const TokenPage: NextPage<PageParams> = ({ hash }: PageParams) => {
const { title, description } = getSeo({ hash }); const { title, description } = getSeo({ hash });
...@@ -16,7 +19,9 @@ const TokenPage: NextPage<PageParams> = ({ hash }: PageParams) => { ...@@ -16,7 +19,9 @@ const TokenPage: NextPage<PageParams> = ({ hash }: PageParams) => {
<title>{ title }</title> <title>{ title }</title>
<meta name="description" content={ description }/> <meta name="description" content={ description }/>
</Head> </Head>
<Page>
<Token/> <Token/>
</Page>
</> </>
); );
}; };
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Withdrawals from 'ui/pages/Withdrawals';
const WithdrawalsPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Withdrawals/>
</>
);
};
export default WithdrawalsPage;
export { getServerSideProps } from 'lib/next/getServerSidePropsBeacon';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
export const PRIVATE_TAG_ADDRESS = {
address: ADDRESS_PARAMS,
address_hash: ADDRESS_HASH,
id: '4',
name: 'placeholder',
};
import type { Address } from 'types/api/address';
import { ADDRESS_HASH } from './addressParams';
import { TOKEN_INFO_ERC_20 } from './token';
export const ADDRESS_INFO: Address = {
block_number_balance_updated_at: 8774377,
coin_balance: '0',
creation_tx_hash: null,
creator_address_hash: null,
exchange_rate: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: true,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false,
has_tokens: false,
has_validated_blocks: false,
hash: ADDRESS_HASH,
implementation_address: null,
implementation_name: null,
is_contract: false,
is_verified: false,
name: 'ChainLink Token (goerli)',
token: TOKEN_INFO_ERC_20,
private_tags: [],
public_tags: [],
watchlist_names: [],
watchlist_address_id: null,
};
import type { AddressParam } from 'types/api/addressParams';
export const ADDRESS_HASH = '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a';
export const ADDRESS_PARAMS: AddressParam = {
hash: ADDRESS_HASH,
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
};
export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70';
import type { SmartContract } from 'types/api/contract';
export const CONTRACT_CODE_UNVERIFIED = {
creation_bytecode: '0x60806040526e',
deployed_bytecode: '0x608060405233',
is_self_destructed: false,
} as SmartContract;
export const CONTRACT_CODE_VERIFIED = {
abi: [],
additional_sources: [],
can_be_visualized_via_sol2uml: true,
compiler_settings: {
compilationTarget: {
'contracts/StubContract.sol': 'StubContract',
},
evmVersion: 'london',
libraries: {},
metadata: {
bytecodeHash: 'ipfs',
},
optimizer: {
enabled: false,
runs: 200,
},
remappings: [],
},
compiler_version: 'v0.8.7+commit.e28d00a7',
creation_bytecode: '0x6080604052348',
deployed_bytecode: '0x60806040',
evm_version: 'london',
external_libraries: [],
file_path: 'contracts/StubContract.sol',
is_verified: true,
name: 'StubContract',
optimization_enabled: false,
optimization_runs: 200,
source_code: 'source_code',
verified_at: '2023-02-21T14:39:16.906760Z',
} as unknown as SmartContract;
import type { TokenCounters, TokenHolder, TokenHolders, TokenInfo, TokenInstance, TokenInventoryResponse, TokenType } from 'types/api/token';
import type { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransfer';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
import { BLOCK_HASH } from './block';
import { TX_HASH } from './tx';
export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
address: ADDRESS_HASH,
decimals: '18',
exchange_rate: null,
holders: '16026',
name: 'Stub Token (goerli)',
symbol: 'STUB',
total_supply: '6000000000000000000',
type: 'ERC-20',
};
export const TOKEN_INFO_ERC_721: TokenInfo<'ERC-721'> = {
...TOKEN_INFO_ERC_20,
type: 'ERC-721',
};
export const TOKEN_INFO_ERC_1155: TokenInfo<'ERC-1155'> = {
...TOKEN_INFO_ERC_20,
type: 'ERC-1155',
};
export const TOKEN_COUNTERS: TokenCounters = {
token_holders_count: '123456',
transfers_count: '123456',
};
export const TOKEN_HOLDER: TokenHolder = {
address: ADDRESS_PARAMS,
value: '1021378038331138520',
};
export const TOKEN_HOLDERS: TokenHolders = { items: Array(50).fill(TOKEN_HOLDER), next_page_params: null };
export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
block_hash: BLOCK_HASH,
from: ADDRESS_PARAMS,
log_index: '4',
method: 'addLiquidity',
timestamp: '2022-06-24T10:22:11.000000Z',
to: ADDRESS_PARAMS,
token: TOKEN_INFO_ERC_20,
total: {
decimals: '18',
value: '9851351626684503',
},
tx_hash: TX_HASH,
type: 'token_minting',
};
export const TOKEN_TRANSFER_ERC_721: TokenTransfer = {
...TOKEN_TRANSFER_ERC_20,
total: {
token_id: '35870',
},
token: TOKEN_INFO_ERC_721,
};
export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = {
...TOKEN_TRANSFER_ERC_20,
total: {
token_id: '35870',
value: '123',
decimals: '18',
},
token: TOKEN_INFO_ERC_1155,
};
export const getTokenTransfersStub = (type?: TokenType): TokenTransferResponse => {
switch (type) {
case 'ERC-721':
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_721), next_page_params: null };
case 'ERC-1155':
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_1155), next_page_params: null };
default:
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_20), next_page_params: null };
}
};
export const TOKEN_INSTANCE: TokenInstance = {
animation_url: null,
external_app_url: 'https://vipsland.com/nft/collections/genesis/188882',
id: '188882',
image_url: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif',
is_unique: true,
metadata: {
attributes: Array(3).fill({ trait_type: 'skin', value: '6' }),
description: '**GENESIS #188882**, **8a77ca1bcaa4036f** :: *844th* generation of *#57806 and #57809* :: **eGenetic Hash Code (eDNA)** = *2822355e953a462d*',
external_url: 'https://vipsland.com/nft/collections/genesis/188882',
image: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif',
name: 'GENESIS #188882, 8a77ca1bcaa4036f. Blockchain pixel PFP NFT + "on music video" trait inspired by God',
},
owner: ADDRESS_PARAMS,
token: TOKEN_INFO_ERC_1155,
holder_address_hash: ADDRESS_HASH,
};
export const TOKEN_INSTANCES: TokenInventoryResponse = {
items: Array(50).fill(TOKEN_INSTANCE),
next_page_params: null,
};
export const TX_HASH = '0x3ed9d81e7c1001bdda1caa1dc62c0acbbe3d2c671cdc20dc1e65efdaa4186967';
...@@ -26,7 +26,7 @@ const baseStyle = defineStyle((props) => { ...@@ -26,7 +26,7 @@ const baseStyle = defineStyle((props) => {
return { return {
opacity: 1, opacity: 1,
borderRadius: 'base', borderRadius: 'md',
borderColor: start, borderColor: start,
background: `linear-gradient(90deg, ${ start } 8%, ${ end } 18%, ${ start } 33%)`, background: `linear-gradient(90deg, ${ start } 8%, ${ end } 18%, ${ start } 33%)`,
backgroundSize: '200% 100%', backgroundSize: '200% 100%',
......
...@@ -12,6 +12,7 @@ export interface Address { ...@@ -12,6 +12,7 @@ export interface Address {
creator_address_hash: string | null; creator_address_hash: string | null;
creation_tx_hash: string | null; creation_tx_hash: string | null;
exchange_rate: string | null; exchange_rate: string | null;
has_beacon_chain_withdrawals?: boolean;
has_custom_methods_read: boolean; has_custom_methods_read: boolean;
has_custom_methods_write: boolean; has_custom_methods_write: boolean;
has_decompiled_code: boolean; has_decompiled_code: boolean;
...@@ -128,3 +129,19 @@ export interface AddressInternalTxsResponse { ...@@ -128,3 +129,19 @@ export interface AddressInternalTxsResponse {
transaction_index: number; transaction_index: number;
} | null; } | null;
} }
export type AddressWithdrawalsResponse = {
items: Array<AddressWithdrawalsItem>;
next_page_params: {
index: number;
items_count: number;
};
}
export type AddressWithdrawalsItem = {
amount: string;
block_number: number;
index: number;
timestamp: string;
validator_index: number;
}
...@@ -10,6 +10,7 @@ export interface Block { ...@@ -10,6 +10,7 @@ export interface Block {
tx_count: number; tx_count: number;
miner: AddressParam; miner: AddressParam;
size: number; size: number;
has_beacon_chain_withdrawals?: boolean;
hash: string; hash: string;
parent_hash: string; parent_hash: string;
difficulty: string; difficulty: string;
...@@ -56,3 +57,18 @@ export interface NewBlockSocketResponse { ...@@ -56,3 +57,18 @@ export interface NewBlockSocketResponse {
export interface BlockFilters { export interface BlockFilters {
type?: BlockType; type?: BlockType;
} }
export type BlockWithdrawalsResponse = {
items: Array<BlockWithdrawalsItem>;
next_page_params: {
index: number;
items_count: number;
};
}
export type BlockWithdrawalsItem = {
amount: string;
index: number;
receiver: AddressParam;
validator_index: number;
}
...@@ -21,7 +21,7 @@ export interface TokenCounters { ...@@ -21,7 +21,7 @@ export interface TokenCounters {
export interface TokenHolders { export interface TokenHolders {
items: Array<TokenHolder>; items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination; next_page_params: TokenHoldersPagination | null;
} }
export type TokenHolder = { export type TokenHolder = {
...@@ -52,7 +52,7 @@ export interface TokenInstanceTransfersCount { ...@@ -52,7 +52,7 @@ export interface TokenInstanceTransfersCount {
export interface TokenInventoryResponse { export interface TokenInventoryResponse {
items: Array<TokenInstance>; items: Array<TokenInstance>;
next_page_params: TokenInventoryPagination; next_page_params: TokenInventoryPagination | null;
} }
export type TokenInventoryPagination = { export type TokenInventoryPagination = {
......
import type { AddressParam } from './addressParams';
export type WithdrawalsResponse = {
items: Array<WithdrawalsItem>;
next_page_params: {
index: number;
items_count: number;
};
}
export type WithdrawalsItem = {
amount: string;
block_number: number;
index: number;
receiver: AddressParam;
timestamp: string;
validator_index: number;
}
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>;
}
...@@ -42,7 +42,8 @@ declare module "nextjs-routes" { ...@@ -42,7 +42,8 @@ declare module "nextjs-routes" {
| DynamicRoute<"/tx/[hash]", { "hash": string }> | DynamicRoute<"/tx/[hash]", { "hash": string }>
| StaticRoute<"/txs"> | StaticRoute<"/txs">
| StaticRoute<"/verified-contracts"> | StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml">; | StaticRoute<"/visualize/sol2uml">
| StaticRoute<"/withdrawals">;
interface StaticRoute<Pathname> { interface StaticRoute<Pathname> {
pathname: Pathname; pathname: Pathname;
......
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 }
/> />
</> </>
......
import { Show, Hide } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_withdrawals',
pathParams: { hash },
scrollRef,
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map((item) => <WithdrawalsListItem item={ item } key={ item.index } view="address"/>) }
</Show>
<Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ data.items } view="address" top={ isPaginationVisible ? 80 : 0 }/>
</Hide>
</>
) : null ;
const actionBar = isPaginationVisible ? (
<ActionBar mt={ -6 } showShadow={ isLoading }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(5).fill(`${ 100 / 5 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals for this address."
content={ content }
actionBar={ actionBar }
/>
);
};
export default AddressWithdrawals;
...@@ -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>
......
import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react'; import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Address as AddressInfo } from 'types/api/address';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import * as stubs from 'stubs/contract';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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';
...@@ -24,10 +27,10 @@ type Props = { ...@@ -24,10 +27,10 @@ type Props = {
noSocket?: boolean; noSocket?: boolean;
} }
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => ( const InfoItem = chakra(({ label, value, className, isLoading }: { label: string; value: string; className?: string; isLoading: boolean }) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }> <GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className } alignItems="baseline">
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text> <Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Skeleton>
<Text>{ value }</Text> <Skeleton isLoaded={ !isLoading }>{ value }</Skeleton>
</GridItem> </GridItem>
)); ));
...@@ -35,11 +38,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -35,11 +38,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
const [ isSocketOpen, setIsSocketOpen ] = React.useState(false); const [ isSocketOpen, setIsSocketOpen ] = React.useState(false);
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>(); const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
const { data, isLoading, isError } = useApiQuery('contract', { const queryClient = useQueryClient();
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
const { data, isPlaceholderData, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash) && (noSocket || isSocketOpen), enabled: Boolean(addressHash) && (noSocket || isSocketOpen),
refetchOnMount: false, refetchOnMount: false,
placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
}, },
}); });
...@@ -62,24 +69,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -62,24 +69,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (isLoading) { const verificationButton = isPlaceholderData ? <Skeleton w="130px" h={ 8 } mr={ 3 } ml="auto" borderRadius="base"/> : (
return (
<>
<Flex justifyContent="space-between" mb={ 2 }>
<Skeleton w="180px" h={ 5 } borderRadius="full"/>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="250px" borderRadius="md"/>
<Flex justifyContent="space-between" mb={ 2 } mt={ 6 }>
<Skeleton w="180px" h={ 5 } borderRadius="full"/>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="400px" borderRadius="md"/>
</>
);
}
const verificationButton = (
<Button <Button
size="sm" size="sm"
ml="auto" ml="auto"
...@@ -92,8 +82,8 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -92,8 +82,8 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
); );
const constructorArgs = (() => { const constructorArgs = (() => {
if (!data.decoded_constructor_args) { if (!data?.decoded_constructor_args) {
return data.constructor_args; return data?.constructor_args;
} }
const decoded = data.decoded_constructor_args const decoded = data.decoded_constructor_args
...@@ -119,7 +109,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -119,7 +109,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
})(); })();
const externalLibraries = (() => { const externalLibraries = (() => {
if (!data.external_libraries || data.external_libraries.length === 0) { if (!data?.external_libraries || data?.external_libraries.length === 0) {
return null; return null;
} }
...@@ -134,19 +124,23 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -134,19 +124,23 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
return ( return (
<> <>
<Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}> <Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}>
{ data.is_verified && <Alert status="success">Contract Source Code Verified (Exact Match)</Alert> } { data?.is_verified && (
{ data.is_verified_via_sourcify && ( <Skeleton isLoaded={ !isPlaceholderData }>
<Alert status="success">Contract Source Code Verified (Exact Match)</Alert>
</Skeleton>
) }
{ data?.is_verified_via_sourcify && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap"> <Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span> <span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span>
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> } { data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
</Alert> </Alert>
) } ) }
{ (data.is_changed_bytecode || isChangedBytecodeSocket) && ( { (data?.is_changed_bytecode || isChangedBytecodeSocket) && (
<Alert status="warning"> <Alert status="warning">
Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky. Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert> </Alert>
) } ) }
{ !data.is_verified && data.verified_twin_address_hash && !data.minimal_proxy_address_hash && ( { !data?.is_verified && data?.verified_twin_address_hash && !data?.minimal_proxy_address_hash && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap"> <Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span> <span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<Address> <Address>
...@@ -160,7 +154,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -160,7 +154,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
<span> page</span> <span> page</span>
</Alert> </Alert>
) } ) }
{ data.minimal_proxy_address_hash && ( { data?.minimal_proxy_address_hash && (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap"> <Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
<span>Minimal Proxy Contract for </span> <span>Minimal Proxy Contract for </span>
<Address> <Address>
...@@ -175,14 +169,16 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -175,14 +169,16 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
</Alert> </Alert>
) } ) }
</Flex> </Flex>
{ data.is_verified && ( { data?.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }> <Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name }/> } { data.name && <InfoItem label="Contract name" value={ data.name } isLoading={ isPlaceholderData }/> }
{ data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version }/> } { data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version } isLoading={ isPlaceholderData }/> }
{ data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize"/> } { data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ typeof data.optimization_enabled === 'boolean' && <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' }/> } { typeof data.optimization_enabled === 'boolean' &&
{ data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) }/> } <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ data.verified_at && <InfoItem label="Verified at" value={ dayjs(data.verified_at).format('LLLL') } wordBreak="break-word"/> } { data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) } isLoading={ isPlaceholderData }/> }
{ data.verified_at &&
<InfoItem label="Verified at" value={ dayjs(data.verified_at).format('LLLL') } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
</Grid> </Grid>
) } ) }
<Flex flexDir="column" rowGap={ 6 }> <Flex flexDir="column" rowGap={ 6 }>
...@@ -191,9 +187,10 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -191,9 +187,10 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
data={ constructorArgs } data={ constructorArgs }
title="Constructor Arguments" title="Constructor Arguments"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ data.source_code && ( { data?.source_code && (
<ContractSourceCode <ContractSourceCode
data={ data.source_code } data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) } hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
...@@ -202,23 +199,26 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -202,23 +199,26 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
filePath={ data.file_path } filePath={ data.file_path }
additionalSource={ data.additional_sources } additionalSource={ data.additional_sources }
remappings={ data.compiler_settings?.remappings } remappings={ data.compiler_settings?.remappings }
isLoading={ isPlaceholderData }
/> />
) } ) }
{ Boolean(data.compiler_settings) && ( { data?.compiler_settings ? (
<RawDataSnippet <RawDataSnippet
data={ JSON.stringify(data.compiler_settings) } data={ JSON.stringify(data.compiler_settings) }
title="Compiler Settings" title="Compiler Settings"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) : null }
{ data.abi && ( { data?.abi && (
<RawDataSnippet <RawDataSnippet
data={ JSON.stringify(data.abi) } data={ JSON.stringify(data.abi) }
title="Contract ABI" title="Contract ABI"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ data.creation_bytecode && ( { data?.creation_bytecode && (
<RawDataSnippet <RawDataSnippet
data={ data.creation_bytecode } data={ data.creation_bytecode }
title="Contract creation code" title="Contract creation code"
...@@ -230,13 +230,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -230,13 +230,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
</Alert> </Alert>
) : null } ) : null }
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ data.deployed_bytecode && ( { data?.deployed_bytecode && (
<RawDataSnippet <RawDataSnippet
data={ data.deployed_bytecode } data={ data.deployed_bytecode }
title="Deployed ByteCode" title="Deployed ByteCode"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ externalLibraries && ( { externalLibraries && (
...@@ -244,6 +246,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -244,6 +246,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
data={ externalLibraries } data={ externalLibraries }
title="External Libraries" title="External Libraries"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
</Flex> </Flex>
......
import { Flex, Text, Tooltip } from '@chakra-ui/react'; import { Flex, Skeleton, Text, Tooltip } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -17,14 +17,15 @@ interface Props { ...@@ -17,14 +17,15 @@ interface Props {
filePath?: string; filePath?: string;
additionalSource?: SmartContract['additional_sources']; additionalSource?: SmartContract['additional_sources'];
remappings?: Array<string>; remappings?: Array<string>;
isLoading?: boolean;
} }
const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource, remappings }: Props) => { const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource, remappings, isLoading }: Props) => {
const heading = ( const heading = (
<Text fontWeight={ 500 }> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
<span>Contract source code</span> <span>Contract source code</span>
<Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text> <Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text>
</Text> </Skeleton>
); );
const diagramLink = hasSol2Yml && address ? ( const diagramLink = hasSol2Yml && address ? (
...@@ -32,9 +33,10 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -32,9 +33,10 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
<LinkInternal <LinkInternal
href={ route({ pathname: '/visualize/sol2uml', query: { address } }) } href={ route({ pathname: '/visualize/sol2uml', query: { address } }) }
ml="auto" ml="auto"
mr={ 3 }
> >
<Skeleton isLoaded={ !isLoading }>
View UML diagram View UML diagram
</Skeleton>
</LinkInternal> </LinkInternal>
</Tooltip> </Tooltip>
) : null; ) : null;
...@@ -47,7 +49,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -47,7 +49,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
}, [ additionalSource, data, filePath, isViper ]); }, [ additionalSource, data, filePath, isViper ]);
const copyToClipboard = editorData.length === 1 ? const copyToClipboard = editorData.length === 1 ?
<CopyToClipboard text={ editorData[0].source_code }/> : <CopyToClipboard text={ editorData[0].source_code } isLoading={ isLoading } ml={ 3 }/> :
null; null;
return ( return (
...@@ -57,7 +59,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -57,7 +59,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
{ diagramLink } { diagramLink }
{ copyToClipboard } { copyToClipboard }
</Flex> </Flex>
<CodeEditor data={ editorData } remappings={ remappings }/> { isLoading ? <Skeleton h="557px" w="100%"/> : <CodeEditor data={ editorData } remappings={ remappings }/> }
</section> </section>
); );
}; };
......
import { chakra, Alert, Icon, Modal, ModalBody, ModalContent, ModalCloseButton, ModalOverlay, Box, useDisclosure, Tooltip, IconButton } from '@chakra-ui/react'; import {
chakra,
Alert,
Icon,
Modal,
ModalBody,
ModalContent,
ModalCloseButton,
ModalOverlay,
Box,
useDisclosure,
Tooltip,
IconButton,
Skeleton,
} from '@chakra-ui/react';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import React from 'react'; import React from 'react';
...@@ -13,9 +27,10 @@ const SVG_OPTIONS = { ...@@ -13,9 +27,10 @@ const SVG_OPTIONS = {
interface Props { interface Props {
className?: string; className?: string;
hash: string; hash: string;
isLoading?: boolean;
} }
const AddressQrCode = ({ hash, className }: Props) => { const AddressQrCode = ({ hash, className, isLoading }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [ qr, setQr ] = React.useState(''); const [ qr, setQr ] = React.useState('');
...@@ -36,6 +51,10 @@ const AddressQrCode = ({ hash, className }: Props) => { ...@@ -36,6 +51,10 @@ const AddressQrCode = ({ hash, className }: Props) => {
} }
}, [ hash, isOpen, onClose ]); }, [ hash, isOpen, onClose ]);
if (isLoading) {
return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
}
return ( return (
<> <>
<Tooltip label="Click to view QR code"> <Tooltip label="Click to view QR code">
......
...@@ -11,6 +11,7 @@ import dayjs from 'lib/date/dayjs'; ...@@ -11,6 +11,7 @@ import dayjs from 'lib/date/dayjs';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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 InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
...@@ -56,15 +57,19 @@ const TxInternalsListItem = ({ ...@@ -56,15 +57,19 @@ const TxInternalsListItem = ({
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut }/>
{ isIn && <CopyToClipboard text={ from.hash }/> }
</Address> </Address>
{ (isIn || isOut) ? { (isIn || isOut) ?
<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"/>
} }
{ toData && (
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/> <AddressIcon address={ toData }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/>
{ isOut && <CopyToClipboard text={ toData.hash }/> }
</Address> </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>
......
...@@ -11,6 +11,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -11,6 +11,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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 InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
...@@ -64,6 +65,7 @@ const AddressIntTxsTableItem = ({ ...@@ -64,6 +65,7 @@ const AddressIntTxsTableItem = ({
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ isOut }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ isOut }/>
{ isIn && <CopyToClipboard text={ from.hash }/> }
</Address> </Address>
</Td> </Td>
<Td px={ 0 } verticalAlign="middle"> <Td px={ 0 } verticalAlign="middle">
...@@ -73,10 +75,13 @@ const AddressIntTxsTableItem = ({ ...@@ -73,10 +75,13 @@ const AddressIntTxsTableItem = ({
} }
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
{ toData && (
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/> <AddressIcon address={ toData }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/> <AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/>
{ isOut && <CopyToClipboard text={ toData.hash }/> }
</Address> </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);
import { Show, Hide } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { BlockWithdrawalsResponse } from 'types/api/block';
import DataListDisplay from 'ui/shared/DataListDisplay';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
type QueryResult = UseQueryResult<BlockWithdrawalsResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
type Props = {
blockWithdrawalsQuery: QueryResult;
}
const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
const content = blockWithdrawalsQuery.data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ blockWithdrawalsQuery.data.items.map((item) => <WithdrawalsListItem item={ item } key={ item.index } view="block"/>) }
</Show>
<Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ blockWithdrawalsQuery.data.items } view="block" top={ blockWithdrawalsQuery.isPaginationVisible ? 80 : 0 }/>
</Hide>
</>
) : null ;
return (
<DataListDisplay
isError={ blockWithdrawalsQuery.isError }
isLoading={ blockWithdrawalsQuery.isLoading }
items={ blockWithdrawalsQuery.data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(4).fill(`${ 100 / 4 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals for this block."
content={ content }
/>
);
};
export default BlockWithdrawals;
...@@ -19,7 +19,7 @@ const TxnBatchesListItem = ({ item }: Props) => { ...@@ -19,7 +19,7 @@ const TxnBatchesListItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
return ( return (
<ListItemMobileGrid.Container> <ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label> <ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
......
...@@ -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>
......
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
import iconSuccess from 'icons/status/success.svg'; import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
...@@ -19,6 +20,7 @@ import AddressLogs from 'ui/address/AddressLogs'; ...@@ -19,6 +20,7 @@ import AddressLogs from 'ui/address/AddressLogs';
import AddressTokens from 'ui/address/AddressTokens'; import AddressTokens from 'ui/address/AddressTokens';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers'; import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs'; import AddressTxs from 'ui/address/AddressTxs';
import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -62,6 +64,9 @@ const AddressPageContent = () => { ...@@ -62,6 +64,9 @@ const AddressPageContent = () => {
const tabs: Array<RoutedTab> = React.useMemo(() => { const tabs: Array<RoutedTab> = React.useMemo(() => {
return [ return [
{ id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> }, { id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> },
appConfig.beaconChain.hasBeaconChain && addressQuery.data?.has_beacon_chain_withdrawals ?
{ id: 'withdrawals', title: 'Withdrawals', component: <AddressWithdrawals scrollRef={ tabsScrollRef }/> } :
undefined,
addressQuery.data?.has_token_transfers ? addressQuery.data?.has_token_transfers ?
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } : { id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } :
undefined, undefined,
......
...@@ -4,16 +4,20 @@ import React from 'react'; ...@@ -4,16 +4,20 @@ import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import BlockDetails from 'ui/block/BlockDetails'; import BlockDetails from 'ui/block/BlockDetails';
import BlockWithdrawals from 'ui/block/BlockWithdrawals';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination 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 TxsContent from 'ui/txs/TxsContent'; import TxsContent from 'ui/txs/TxsContent';
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
...@@ -42,6 +46,14 @@ const BlockPageContent = () => { ...@@ -42,6 +46,14 @@ const BlockPageContent = () => {
}, },
}); });
const blockWithdrawalsQuery = useQueryWithPages({
resourceName: 'block_withdrawals',
pathParams: { height },
options: {
enabled: Boolean(blockQuery.data?.height && appConfig.beaconChain.hasBeaconChain && tab === 'withdrawals'),
},
});
if (!height) { if (!height) {
throw new Error('Block not found', { cause: { status: 404 } }); throw new Error('Block not found', { cause: { status: 404 } });
} }
...@@ -53,9 +65,22 @@ const BlockPageContent = () => { ...@@ -53,9 +65,22 @@ const BlockPageContent = () => {
const tabs: Array<RoutedTab> = React.useMemo(() => ([ const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> }, { id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> }, { id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
]), [ blockQuery, blockTxsQuery ]); appConfig.beaconChain.hasBeaconChain && blockQuery.data?.has_beacon_chain_withdrawals ?
{ id: 'withdrawals', title: 'Withdrawals', component: <BlockWithdrawals blockWithdrawalsQuery={ blockWithdrawalsQuery }/> } :
null,
].filter(Boolean)), [ blockQuery, blockTxsQuery, blockWithdrawalsQuery ]);
const hasPagination = !isMobile && tab === 'txs' && blockTxsQuery.isPaginationVisible; const hasPagination = !isMobile && (
(tab === 'txs' && blockTxsQuery.isPaginationVisible) ||
(tab === 'withdrawals' && blockWithdrawalsQuery.isPaginationVisible)
);
let pagination;
if (tab === 'txs') {
pagination = blockTxsQuery.pagination;
} else if (tab === 'withdrawals') {
pagination = blockWithdrawalsQuery.pagination;
}
const backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/blocks'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/blocks');
...@@ -81,12 +106,14 @@ const BlockPageContent = () => { ...@@ -81,12 +106,14 @@ const BlockPageContent = () => {
backLink={ backLink } backLink={ backLink }
/> />
) } ) }
{ blockQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination ? <Pagination { ...blockTxsQuery.pagination }/> : null } rightSlot={ hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
stickyEnabled={ hasPagination } stickyEnabled={ hasPagination }
/> />
) }
</> </>
); );
}; };
......
...@@ -49,7 +49,7 @@ const Home = () => { ...@@ -49,7 +49,7 @@ const Home = () => {
</Box> </Box>
<Stats/> <Stats/>
<ChainIndicators/> <ChainIndicators/>
<AdBanner mt={{ base: 6, lg: 8 }} justifyContent="center"/> <AdBanner mt={{ base: 6, lg: 8 }} mx="auto" display="flex" justifyContent="center"/>
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }> <Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }>
<LatestBlocks/> <LatestBlocks/>
<Box flexGrow={ 1 }> <Box flexGrow={ 1 }>
......
...@@ -68,7 +68,7 @@ test('base view', async({ mount, page, createSocket }) => { ...@@ -68,7 +68,7 @@ test('base view', async({ mount, page, createSocket }) => {
await insertAdPlaceholder(page); await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with verified info', async({ mount, page, createSocket }) => { test('with verified info', async({ mount, page, createSocket }) => {
...@@ -109,7 +109,7 @@ test.describe('mobile', () => { ...@@ -109,7 +109,7 @@ test.describe('mobile', () => {
await insertAdPlaceholder(page); await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with verified info', async({ mount, page, createSocket }) => { test('with verified info', async({ mount, page, createSocket }) => {
......
import { Skeleton, Box, Flex, SkeletonCircle, Icon, Tag } from '@chakra-ui/react'; import { Box, Icon, Flex } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
...@@ -18,9 +18,11 @@ import getQueryParamString from 'lib/router/getQueryParamString'; ...@@ -18,9 +18,11 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import * as addressStubs from 'stubs/address';
import * as tokenStubs from 'stubs/token';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Tag from 'ui/shared/chakra/Tag';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
...@@ -52,7 +54,18 @@ const TokenPageContent = () => { ...@@ -52,7 +54,18 @@ const TokenPageContent = () => {
const tokenQuery = useApiQuery('token', { const tokenQuery = useApiQuery('token', {
pathParams: { hash: hashString }, pathParams: { hash: hashString },
queryOptions: { enabled: isSocketOpen && Boolean(router.query.hash) }, queryOptions: {
enabled: Boolean(router.query.hash),
placeholderData: tokenStubs.TOKEN_INFO_ERC_20,
},
});
const contractQuery = useApiQuery('address', {
pathParams: { hash: hashString },
queryOptions: {
enabled: isSocketOpen && Boolean(router.query.hash),
placeholderData: addressStubs.ADDRESS_INFO,
},
}); });
React.useEffect(() => { React.useEffect(() => {
...@@ -89,7 +102,7 @@ const TokenPageContent = () => { ...@@ -89,7 +102,7 @@ const TokenPageContent = () => {
}); });
useEffect(() => { useEffect(() => {
if (tokenQuery.data) { if (tokenQuery.data && !tokenQuery.isPlaceholderData) {
const tokenSymbol = tokenQuery.data.symbol ? ` (${ tokenQuery.data.symbol })` : ''; const tokenSymbol = tokenQuery.data.symbol ? ` (${ tokenQuery.data.symbol })` : '';
const tokenName = `${ tokenQuery.data.name || 'Unnamed' }${ tokenSymbol }`; const tokenName = `${ tokenQuery.data.name || 'Unnamed' }${ tokenSymbol }`;
const title = document.getElementsByTagName('title')[0]; const title = document.getElementsByTagName('title')[0];
...@@ -101,14 +114,17 @@ const TokenPageContent = () => { ...@@ -101,14 +114,17 @@ const TokenPageContent = () => {
description.content = description.content.replace(tokenQuery.data.address, tokenName) || description.content; description.content = description.content.replace(tokenQuery.data.address, tokenName) || description.content;
} }
} }
}, [ tokenQuery.data ]); }, [ tokenQuery.data, tokenQuery.isPlaceholderData ]);
const hasData = (tokenQuery.data && !tokenQuery.isPlaceholderData) && (contractQuery.data && !contractQuery.isPlaceholderData);
const transfersQuery = useQueryWithPages({ const transfersQuery = useQueryWithPages({
resourceName: 'token_transfers', resourceName: 'token_transfers',
pathParams: { hash: hashString }, pathParams: { hash: hashString },
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(router.query.hash && (!router.query.tab || router.query.tab === 'token_transfers') && tokenQuery.data), enabled: Boolean(hashString && (!router.query.tab || router.query.tab === 'token_transfers') && hasData),
placeholderData: tokenStubs.getTokenTransfersStub(tokenQuery.data?.type),
}, },
}); });
...@@ -117,7 +133,8 @@ const TokenPageContent = () => { ...@@ -117,7 +133,8 @@ const TokenPageContent = () => {
pathParams: { hash: hashString }, pathParams: { hash: hashString },
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data), enabled: Boolean(router.query.hash && router.query.tab === 'holders' && hasData),
placeholderData: tokenStubs.TOKEN_HOLDERS,
}, },
}); });
...@@ -126,15 +143,11 @@ const TokenPageContent = () => { ...@@ -126,15 +143,11 @@ const TokenPageContent = () => {
pathParams: { hash: hashString }, pathParams: { hash: hashString },
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && tokenQuery.data), enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && hasData),
placeholderData: tokenStubs.TOKEN_INSTANCES,
}, },
}); });
const contractQuery = useApiQuery('address', {
pathParams: { hash: hashString },
queryOptions: { enabled: Boolean(router.query.hash) },
});
const isVerifiedInfoEnabled = Boolean(appConfig.contractInfoApi.endpoint); const isVerifiedInfoEnabled = Boolean(appConfig.contractInfoApi.endpoint);
const verifiedInfoQuery = useApiQuery('token_verified_info', { const verifiedInfoQuery = useApiQuery('token_verified_info', {
pathParams: { hash: hashString, chainId: appConfig.network.id }, pathParams: { hash: hashString, chainId: appConfig.network.id },
...@@ -144,15 +157,15 @@ const TokenPageContent = () => { ...@@ -144,15 +157,15 @@ const TokenPageContent = () => {
const contractTabs = useContractTabs(contractQuery.data); const contractTabs = useContractTabs(contractQuery.data);
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery }/> }, { 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,
contractQuery.data?.is_contract ? { contractQuery.data?.is_contract ? {
id: 'contract', id: 'contract',
title: () => { title: () => {
if (contractQuery.data.is_verified) { if (contractQuery.data?.is_verified) {
return ( return (
<> <>
<span>Contract</span> <span>Contract</span>
...@@ -169,7 +182,7 @@ const TokenPageContent = () => { ...@@ -169,7 +182,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;
...@@ -225,23 +238,14 @@ const TokenPageContent = () => { ...@@ -225,23 +238,14 @@ const TokenPageContent = () => {
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null; const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
return ( return (
<Page>
{ tokenQuery.isLoading ? (
<>
<Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/>
<Flex alignItems="center" mb={ 6 }>
<SkeletonCircle w={ 6 } h={ 6 } mr={ 3 }/>
<Skeleton w="500px" h={ 10 }/>
</Flex>
</>
) : (
<> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
isLoading={ tokenQuery.isPlaceholderData }
text={ `${ tokenQuery.data?.name || 'Unnamed' }${ tokenSymbolText } token` } text={ `${ tokenQuery.data?.name || 'Unnamed' }${ tokenSymbolText } token` }
backLink={ backLink } backLink={ backLink }
additionalsLeft={ ( additionalsLeft={ (
<TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 }/> <TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 } isLoading={ tokenQuery.isPlaceholderData }/>
) } ) }
additionalsRight={ tagsNode } additionalsRight={ tagsNode }
afterTitle={ afterTitle={
...@@ -250,25 +254,25 @@ const TokenPageContent = () => { ...@@ -250,25 +254,25 @@ const TokenPageContent = () => {
<Box boxSize={ 4 } display="inline-block"/> <Box boxSize={ 4 } display="inline-block"/>
} }
/> />
</> <TokenContractInfo tokenQuery={ tokenQuery } contractQuery={ contractQuery }/>
) }
<TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenVerifiedInfo hash={ hashString } verifiedInfoQuery={ verifiedInfoQuery } isVerifiedInfoEnabled={ isVerifiedInfoEnabled }/> <TokenVerifiedInfo hash={ hashString } verifiedInfoQuery={ verifiedInfoQuery } isVerifiedInfoEnabled={ isVerifiedInfoEnabled }/>
<TokenDetails tokenQuery={ tokenQuery }/> <TokenDetails tokenQuery={ tokenQuery }/>
{ /* 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>
{ tokenQuery.isLoading || contractQuery.isLoading ? <SkeletonTabs/> : ( { tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData ?
<SkeletonTabs tabs={ tabs }/> :
(
<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 }
/> />
) } ) }
{ !tokenQuery.isLoading && !tokenQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> } { !tokenQuery.isLoading && !tokenQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</Page> </>
); );
}; };
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { data as withdrawalsData } from 'mocks/withdrawals/withdrawals';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Withdrawals from './Withdrawals';
const WITHDRAWALS_API_URL = buildApiUrl('withdrawals');
test('base view +@mobile', async({ mount, page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
await page.route(WITHDRAWALS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withdrawalsData),
}));
const component = await mount(
<TestApp>
<Withdrawals/>
</TestApp>,
);
await expect(component.locator('main')).toHaveScreenshot();
});
import { Flex, Hide, Show } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
const Withdrawals = () => {
const isMobile = useIsMobile();
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'withdrawals',
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>{ data.items.map((item => <WithdrawalsListItem key={ item.index } item={ item } view="list"/>)) }</Show>
<Hide below="lg" ssr={ false }><WithdrawalsTable items={ data.items } view="list" top={ isPaginationVisible ? 80 : 0 }/></Hide>
</>
) : null;
const actionBar = isPaginationVisible ? (
<ActionBar mt={ -6 }>
<Flex alignItems="center" justifyContent="space-between" w="100%">
{ !isMobile }
<Pagination ml="auto" { ...pagination }/>
</Flex>
</ActionBar>
) : null;
return (
<Page>
<PageTitle text="Withdrawals" withTextAd/>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: Array(6).fill(`${ 100 / 6 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals."
content={ content }
actionBar={ actionBar }
/>
</Page>
);
};
export default Withdrawals;
import { Tag, Flex, HStack, Text } from '@chakra-ui/react'; import { Tag, Flex, HStack, Text, Skeleton } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { AddressTag } from 'types/api/account'; import type { AddressTag } from 'types/api/account';
...@@ -11,9 +11,10 @@ interface Props { ...@@ -11,9 +11,10 @@ interface Props {
item: AddressTag; item: AddressTag;
onEditClick: (data: AddressTag) => void; onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void; onDeleteClick: (data: AddressTag) => void;
isLoading?: boolean;
} }
const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { const AddressTagListItem = ({ item, onEditClick, onDeleteClick, isLoading }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -25,15 +26,17 @@ const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -25,15 +26,17 @@ const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<ListItemMobile> <ListItemMobile>
<Flex alignItems="flex-start" flexDirection="column" maxW="100%"> <Flex alignItems="flex-start" flexDirection="column" maxW="100%">
<AddressSnippet address={ item.address }/> <AddressSnippet address={ item.address } isLoading={ isLoading }/>
<HStack spacing={ 3 } mt={ 4 }> <HStack spacing={ 3 } mt={ 4 }>
<Text fontSize="sm" fontWeight={ 500 }>Private tag</Text> <Text fontSize="sm" fontWeight={ 500 }>Private tag</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="sm">
<Tag> <Tag>
{ item.name } { item.name }
</Tag> </Tag>
</Skeleton>
</HStack> </HStack>
</Flex> </Flex>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -12,28 +12,30 @@ import type { AddressTags, AddressTag } from 'types/api/account'; ...@@ -12,28 +12,30 @@ import type { AddressTags, AddressTag } from 'types/api/account';
import AddressTagTableItem from './AddressTagTableItem'; import AddressTagTableItem from './AddressTagTableItem';
interface Props { interface Props {
data: AddressTags; data?: AddressTags;
onEditClick: (data: AddressTag) => void; onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void; onDeleteClick: (data: AddressTag) => void;
isLoading: boolean;
} }
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table variant="simple" minWidth="600px">
<Thead> <Thead>
<Tr> <Tr>
<Th width="60%">Address</Th> <Th width="60%">Address</Th>
<Th width="40%">Private tag</Th> <Th width="40%">Private tag</Th>
<Th width="108px"></Th> <Th width="116px"></Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item: AddressTag) => ( { data?.map((item: AddressTag, index: number) => (
<AddressTagTableItem <AddressTagTableItem
item={ item } item={ item }
key={ item.id } key={ item.id + (isLoading ? index : '') }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
isLoading={ isLoading }
/> />
)) } )) }
</Tbody> </Tbody>
......
import { import {
Tag,
Tr, Tr,
Td, Td,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
...@@ -8,16 +7,17 @@ import React, { useCallback } from 'react'; ...@@ -8,16 +7,17 @@ import React, { useCallback } from 'react';
import type { AddressTag } from 'types/api/account'; import type { AddressTag } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import Tag from 'ui/shared/chakra/Tag';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
item: AddressTag; item: AddressTag;
onEditClick: (data: AddressTag) => void; onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void; onDeleteClick: (data: AddressTag) => void;
isLoading: boolean;
} }
const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const AddressTagTableItem = ({ item, onEditClick, onDeleteClick, isLoading }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -29,17 +29,13 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -29,17 +29,13 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.id }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<AddressSnippet address={ item.address }/> <AddressSnippet address={ item.address } isLoading={ isLoading }/>
</Td> </Td>
<Td whiteSpace="nowrap"> <Td whiteSpace="nowrap">
<TruncatedTextTooltip label={ item.name }> <Tag isLoading={ isLoading } isTruncated>{ item.name }</Tag>
<Tag>
{ item.name }
</Tag>
</TruncatedTextTooltip>
</Td> </Td>
<Td> <Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -5,10 +5,9 @@ import type { AddressTag } from 'types/api/account'; ...@@ -5,10 +5,9 @@ import type { AddressTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import AddressModal from './AddressModal/AddressModal'; import AddressModal from './AddressModal/AddressModal';
import AddressTagListItem from './AddressTagTable/AddressTagListItem'; import AddressTagListItem from './AddressTagTable/AddressTagListItem';
...@@ -16,7 +15,12 @@ import AddressTagTable from './AddressTagTable/AddressTagTable'; ...@@ -16,7 +15,12 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal'; import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => { const PrivateAddressTags = () => {
const { data: addressTagsData, isLoading, isError, refetch } = useApiQuery('private_tags_address', { queryOptions: { refetchOnMount: false } }); const { data: addressTagsData, isError, isPlaceholderData, refetch } = useApiQuery('private_tags_address', {
queryOptions: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
},
});
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
...@@ -49,46 +53,25 @@ const PrivateAddressTags = () => { ...@@ -49,46 +53,25 @@ const PrivateAddressTags = () => {
deleteModalProps.onClose(); deleteModalProps.onClose();
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const description = (
<AccountPageDescription>
Use private address tags to track any addresses of interest.
Private tags are saved in your account and are only visible when you are logged in.
</AccountPageDescription>
);
if (isLoading && !addressTagsData) {
const loader = isMobile ? <SkeletonListAccount/> : (
<>
<SkeletonTable columns={ [ '60%', '40%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</>
);
return (
<>
{ description }
{ loader }
</>
);
}
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const list = isMobile ? ( const list = isMobile ? (
<Box> <Box>
{ addressTagsData.map((item: AddressTag) => ( { addressTagsData?.map((item: AddressTag, index: number) => (
<AddressTagListItem <AddressTagListItem
item={ item } item={ item }
key={ item.id } key={ item.id + (isPlaceholderData ? index : '') }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
isLoading={ isPlaceholderData }
/> />
)) } )) }
</Box> </Box>
) : ( ) : (
<AddressTagTable <AddressTagTable
isLoading={ isPlaceholderData }
data={ addressTagsData } data={ addressTagsData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
...@@ -97,15 +80,20 @@ const PrivateAddressTags = () => { ...@@ -97,15 +80,20 @@ const PrivateAddressTags = () => {
return ( return (
<> <>
{ description } <AccountPageDescription>
Use private address tags to track any addresses of interest.
Private tags are saved in your account and are only visible when you are logged in.
</AccountPageDescription>
{ Boolean(addressTagsData?.length) && list } { Boolean(addressTagsData?.length) && list }
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Skeleton isLoaded={ !isPlaceholderData } display="inline-block">
<Button <Button
size="lg" size="lg"
onClick={ addressModalProps.onOpen } onClick={ addressModalProps.onOpen }
> >
Add address tag Add address tag
</Button> </Button>
</Skeleton>
</Box> </Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData } onSuccess={ onAddOrEditSuccess }/> <AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData } onSuccess={ onAddOrEditSuccess }/>
{ deleteModalData && ( { deleteModalData && (
......
...@@ -6,9 +6,9 @@ import type { TokenInfo } from 'types/api/token'; ...@@ -6,9 +6,9 @@ import type { TokenInfo } from 'types/api/token';
import appConfig from 'configs/app/config'; import appConfig 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 AddressActionsMenu from 'ui/shared/AddressActions/Menu'; import AddressActionsMenu from 'ui/shared/AddressActions/Menu';
...@@ -18,14 +18,15 @@ interface Props { ...@@ -18,14 +18,15 @@ interface Props {
address: Pick<Address, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names' | 'watchlist_address_id'>; address: Pick<Address, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names' | 'watchlist_address_id'>;
token?: TokenInfo | null; token?: TokenInfo | null;
isLinkDisabled?: boolean; isLinkDisabled?: boolean;
isLoading?: boolean;
} }
const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => { const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
return ( return (
<Flex alignItems="center"> <Flex alignItems="center">
<AddressIcon address={ address }/> <AddressIcon address={ address } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ address.hash } hash={ address.hash }
...@@ -34,13 +35,14 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => { ...@@ -34,13 +35,14 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => {
fontWeight={ 500 } fontWeight={ 500 }
truncation={ isMobile ? 'constant' : 'none' } truncation={ isMobile ? 'constant' : 'none' }
isDisabled={ isLinkDisabled } isDisabled={ isLinkDisabled }
isLoading={ isLoading }
/> />
<CopyToClipboard text={ address.hash }/> <CopyToClipboard text={ address.hash } isLoading={ isLoading }/>
{ address.is_contract && token && <AddressAddToMetaMask ml={ 2 } token={ token }/> } { !isLoading && address.is_contract && token && <AddressAddToWallet ml={ 2 } token={ token }/> }
{ !address.is_contract && appConfig.isAccountSupported && ( { !isLoading && !address.is_contract && appConfig.isAccountSupported && (
<AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/> <AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/>
) } ) }
<AddressQrCode hash={ address.hash } ml={ 2 }/> <AddressQrCode hash={ address.hash } ml={ 2 } isLoading={ isLoading }/>
{ appConfig.isAccountSupported && <AddressActionsMenu/> } { appConfig.isAccountSupported && <AddressActionsMenu/> }
</Flex> </Flex>
); );
......
...@@ -11,15 +11,16 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard'; ...@@ -11,15 +11,16 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name'>; address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name'>;
subtitle?: string; subtitle?: string;
isLoading?: boolean;
} }
const AddressSnippet = ({ address, subtitle }: Props) => { const AddressSnippet = ({ address, subtitle, isLoading }: Props) => {
return ( return (
<Box maxW="100%"> <Box maxW="100%">
<Address> <Address>
<AddressIcon address={ address }/> <AddressIcon address={ address } isLoading={ isLoading }/>
<AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 }/> <AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 } isLoading={ isLoading }/>
<CopyToClipboard text={ address.hash } ml={ 3 }/> <CopyToClipboard text={ address.hash } isLoading={ isLoading }/>
</Address> </Address>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> } { subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> }
</Box> </Box>
......
import { IconButton, Tooltip, useClipboard, chakra, useDisclosure } from '@chakra-ui/react'; import { IconButton, Tooltip, useClipboard, chakra, useDisclosure, Skeleton } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import CopyIcon from 'icons/copy.svg'; import CopyIcon from 'icons/copy.svg';
const CopyToClipboard = ({ text, className }: {text: string; className?: string}) => { interface Props {
text: string;
className?: string;
isLoading?: boolean;
}
const CopyToClipboard = ({ text, className, isLoading }: Props) => {
const { hasCopied, onCopy } = useClipboard(text, 1000); const { hasCopied, onCopy } = useClipboard(text, 1000);
const [ copied, setCopied ] = useState(false); const [ copied, setCopied ] = useState(false);
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 // have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
...@@ -17,6 +23,10 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string} ...@@ -17,6 +23,10 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
} }
}, [ hasCopied ]); }, [ hasCopied ]);
if (isLoading) {
return <Skeleton boxSize={ 5 } className={ className } borderRadius="sm" flexShrink={ 0 }/>;
}
return ( return (
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen || copied }> <Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen || copied }>
<IconButton <IconButton
...@@ -24,6 +34,7 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string} ...@@ -24,6 +34,7 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
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 }
...@@ -31,6 +42,7 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string} ...@@ -31,6 +42,7 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
className={ className } className={ className }
onMouseEnter={ onOpen } onMouseEnter={ onOpen }
onMouseLeave={ onClose } onMouseLeave={ onClose }
ml={ 1 }
/> />
</Tooltip> </Tooltip>
); );
......
import { GridItem, Flex, Text } from '@chakra-ui/react'; import { GridItem, Flex, Text, Skeleton } from '@chakra-ui/react';
import type { HTMLChakraProps } from '@chakra-ui/system'; import type { HTMLChakraProps } from '@chakra-ui/system';
import React from 'react'; import React from 'react';
...@@ -9,18 +9,21 @@ interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> { ...@@ -9,18 +9,21 @@ interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
hint: string; hint: string;
children: React.ReactNode; children: React.ReactNode;
note?: string; note?: string;
isLoading?: boolean;
} }
const DetailsInfoItem = ({ title, hint, note, children, id, ...styles }: Props) => { const DetailsInfoItem = ({ title, hint, note, children, id, isLoading, ...styles }: Props) => {
return ( return (
<> <>
<GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}> <GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}>
<Flex columnGap={ 2 } alignItems="flex-start"> <Flex columnGap={ 2 } alignItems="flex-start">
<Hint label={ hint }/> <Hint label={ hint } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading }>
<Text fontWeight={{ base: 700, lg: 500 }}> <Text fontWeight={{ base: 700, lg: 500 }}>
{ title } { title }
{ note && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">{ note }</Text> } { note && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">{ note }</Text> }
</Text> </Text>
</Skeleton>
</Flex> </Flex>
</GridItem> </GridItem>
<GridItem <GridItem
......
...@@ -7,18 +7,22 @@ import isSelfHosted from 'lib/isSelfHosted'; ...@@ -7,18 +7,22 @@ import isSelfHosted from 'lib/isSelfHosted';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
const DetailsSponsoredItem = () => { interface Props {
isLoading?: boolean;
}
const DetailsSponsoredItem = ({ isLoading }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED); const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED);
if (hasAdblockCookie || !isSelfHosted()) { if (!isSelfHosted() || hasAdblockCookie) {
return null; return null;
} }
if (isMobile) { if (isMobile) {
return ( return (
<GridItem mt={ 5 }> <GridItem mt={ 5 }>
<AdBanner justifyContent="center"/> <AdBanner mx="auto" isLoading={ isLoading } display="flex" justifyContent="center"/>
</GridItem> </GridItem>
); );
} }
...@@ -27,8 +31,9 @@ const DetailsSponsoredItem = () => { ...@@ -27,8 +31,9 @@ const DetailsSponsoredItem = () => {
<DetailsInfoItem <DetailsInfoItem
title="Sponsored" title="Sponsored"
hint="Sponsored banner advertisement" hint="Sponsored banner advertisement"
isLoading={ isLoading }
> >
<AdBanner/> <AdBanner isLoading={ isLoading }/>
</DetailsInfoItem> </DetailsInfoItem>
); );
}; };
......
import type { TooltipProps } from '@chakra-ui/react'; import type { TooltipProps } from '@chakra-ui/react';
import { chakra, IconButton, Tooltip, useDisclosure } from '@chakra-ui/react'; import { chakra, IconButton, Tooltip, useDisclosure, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import InfoIcon from 'icons/info.svg'; import InfoIcon from 'icons/info.svg';
...@@ -8,9 +8,10 @@ interface Props { ...@@ -8,9 +8,10 @@ interface Props {
label: string | React.ReactNode; label: string | React.ReactNode;
className?: string; className?: string;
tooltipProps?: Partial<TooltipProps>; tooltipProps?: Partial<TooltipProps>;
isLoading?: boolean;
} }
const Hint = ({ label, className, tooltipProps }: Props) => { const Hint = ({ label, className, tooltipProps, isLoading }: Props) => {
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 // have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
...@@ -19,6 +20,10 @@ const Hint = ({ label, className, tooltipProps }: Props) => { ...@@ -19,6 +20,10 @@ const Hint = ({ label, className, tooltipProps }: Props) => {
onToggle(); onToggle();
}, [ onToggle ]); }, [ onToggle ]);
if (isLoading) {
return <Skeleton boxSize={ 5 } borderRadius="sm"/>;
}
return ( return (
<Tooltip <Tooltip
label={ label } label={ label }
......
...@@ -26,6 +26,8 @@ const ListItemMobile = ({ children, className, isAnimated }: Props) => { ...@@ -26,6 +26,8 @@ const ListItemMobile = ({ children, className, isAnimated }: Props) => {
borderBottomWidth: '1px', borderBottomWidth: '1px',
}} }}
className={ className } className={ className }
fontSize="16px"
lineHeight="20px"
> >
{ children } { children }
</Flex> </Flex>
......
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));
import { Heading, Flex, Grid, Tooltip, Icon, Link, chakra } from '@chakra-ui/react'; import { Heading, Flex, Grid, Tooltip, Icon, Link, chakra, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
...@@ -15,10 +15,12 @@ type Props = { ...@@ -15,10 +15,12 @@ type Props = {
className?: string; className?: string;
backLink?: BackLinkProp; backLink?: BackLinkProp;
afterTitle?: React.ReactNode; afterTitle?: React.ReactNode;
isLoading?: boolean;
} }
const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLink, className, afterTitle }: Props) => { const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLink, className, isLoading, afterTitle }: Props) => {
const title = ( const title = (
<Skeleton isLoaded={ !isLoading }>
<Heading <Heading
as="h1" as="h1"
size="lg" size="lg"
...@@ -28,6 +30,7 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi ...@@ -28,6 +30,7 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi
{ text } { text }
{ afterTitle } { afterTitle }
</Heading> </Heading>
</Skeleton>
); );
const backLinkComponent = (() => { const backLinkComponent = (() => {
......
import { Box, Flex, Text, chakra, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, chakra, useColorModeValue, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import CopyToClipboard from './CopyToClipboard'; import CopyToClipboard from './CopyToClipboard';
...@@ -11,33 +11,36 @@ interface Props { ...@@ -11,33 +11,36 @@ interface Props {
beforeSlot?: React.ReactNode; beforeSlot?: React.ReactNode;
textareaMaxHeight?: string; textareaMaxHeight?: string;
showCopy?: boolean; showCopy?: boolean;
isLoading?: boolean;
} }
const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true }: Props) => { const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true, isLoading }: Props) => {
// see issue in theme/components/Textarea.ts // see issue in theme/components/Textarea.ts
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b'); const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
return ( return (
<Box className={ className } as="section" title={ title }> <Box className={ className } as="section" title={ title }>
{ (title || rightSlot || showCopy) && ( { (title || rightSlot || showCopy) && (
<Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }> <Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }>
{ title && <Text fontWeight={ 500 }>{ title }</Text> } { title && <Skeleton fontWeight={ 500 } isLoaded={ !isLoading }>{ title }</Skeleton> }
{ rightSlot } { rightSlot }
{ typeof data === 'string' && showCopy && <CopyToClipboard text={ data }/> } { typeof data === 'string' && showCopy && <CopyToClipboard text={ data } isLoading={ isLoading }/> }
</Flex> </Flex>
) } ) }
{ beforeSlot } { beforeSlot }
<Box <Skeleton
p={ 4 } p={ 4 }
bgColor={ bgColor } bgColor={ isLoading ? 'inherit' : bgColor }
maxH={ textareaMaxHeight || '400px' } maxH={ textareaMaxHeight || '400px' }
minH={ isLoading ? '200px' : undefined }
fontSize="sm" fontSize="sm"
borderRadius="md" borderRadius="md"
wordBreak="break-all" wordBreak="break-all"
whiteSpace="pre-wrap" whiteSpace="pre-wrap"
overflowY="auto" overflowY="auto"
isLoaded={ !isLoading }
> >
{ data } { data }
</Box> </Skeleton>
</Box> </Box>
); );
}; };
......
...@@ -175,6 +175,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -175,6 +175,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
} }
onItemClick={ handleTabChange } onItemClick={ handleTabChange }
buttonRef={ tabsRefs[index] } buttonRef={ tabsRefs[index] }
size={ themeProps.size || 'md' }
/> />
); );
} }
......
import type {
ButtonProps } from '@chakra-ui/react';
import { Popover, import { Popover,
PopoverTrigger, PopoverTrigger,
PopoverContent, PopoverContent,
...@@ -20,9 +22,10 @@ interface Props { ...@@ -20,9 +22,10 @@ interface Props {
styles?: StyleProps; styles?: StyleProps;
onItemClick: (index: number) => void; onItemClick: (index: number) => void;
buttonRef: React.RefObject<HTMLButtonElement>; buttonRef: React.RefObject<HTMLButtonElement>;
size: ButtonProps['size'];
} }
const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab }: Props) => { const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab, size }: Props) => {
const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen, onClose, onOpen } = useDisclosure();
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => { const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
...@@ -40,6 +43,7 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe ...@@ -40,6 +43,7 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe
variant="ghost" variant="ghost"
isActive={ isOpen || isActive } isActive={ isOpen || isActive }
ref={ buttonRef } ref={ buttonRef }
size={ size }
{ ...styles } { ...styles }
> >
{ menuButton.title } { menuButton.title }
......
import { Alert, Link, Text, chakra, useTheme, useColorModeValue } from '@chakra-ui/react'; import { Alert, Link, Text, chakra, useTheme, useColorModeValue, Skeleton, Tr, Td } from '@chakra-ui/react';
import { transparentize } from '@chakra-ui/theme-tools'; import { transparentize } from '@chakra-ui/theme-tools';
import React from 'react'; import React from 'react';
...@@ -13,9 +13,10 @@ interface Props { ...@@ -13,9 +13,10 @@ interface Props {
url: string; url: string;
alert?: string; alert?: string;
num?: number; num?: number;
isLoading?: boolean;
} }
const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'transaction' }: Props) => { const SocketNewItemsNotice = chakra(({ children, className, url, num, alert, type = 'transaction', isLoading }: Props) => {
const theme = useTheme(); const theme = useTheme();
const alertContent = (() => { const alertContent = (() => {
...@@ -49,7 +50,10 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr ...@@ -49,7 +50,10 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr
); );
})(); })();
const content = ( const color = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
const bgColor = useColorModeValue('orange.50', transparentize('orange.200', 0.16)(theme));
const content = !isLoading ? (
<Alert <Alert
className={ className } className={ className }
status="warning" status="warning"
...@@ -57,14 +61,39 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr ...@@ -57,14 +61,39 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr
py="6px" py="6px"
fontWeight={ 400 } fontWeight={ 400 }
fontSize="sm" fontSize="sm"
bgColor={ useColorModeValue('orange.50', transparentize('orange.200', 0.16)(theme)) } bgColor={ bgColor }
color={ useColorModeValue('blackAlpha.800', 'whiteAlpha.800') } color={ color }
> >
{ alertContent } { alertContent }
</Alert> </Alert>
); ) : <Skeleton className={ className } h="33px"/>;
return children ? children({ content }) : content; return children ? children({ content }) : content;
});
export default SocketNewItemsNotice;
export const Desktop = ({ ...props }: Props) => {
return (
<SocketNewItemsNotice
borderRadius={ props.isLoading ? 'sm' : 0 }
h={ props.isLoading ? 4 : 'auto' }
maxW={ props.isLoading ? '215px' : undefined }
w="100%"
mx={ props.isLoading ? 4 : 0 }
my={ props.isLoading ? '6px' : 0 }
{ ...props }
>
{ ({ content }) => <Tr><Td colSpan={ 100 } p={ 0 }>{ content }</Td></Tr> }
</SocketNewItemsNotice>
);
}; };
export default chakra(SocketNewItemsNotice); export const Mobile = ({ ...props }: Props) => {
return (
<SocketNewItemsNotice
borderBottomRadius={ 0 }
{ ...props }
/>
);
};
import { Tooltip, IconButton, Icon, HStack } from '@chakra-ui/react'; import { Tooltip, IconButton, HStack, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import DeleteIcon from 'icons/delete.svg'; import DeleteIcon from 'icons/delete.svg';
...@@ -8,33 +8,47 @@ import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModa ...@@ -8,33 +8,47 @@ import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModa
type Props = { type Props = {
onEditClick: () => void; onEditClick: () => void;
onDeleteClick: () => void; onDeleteClick: () => void;
isLoading?: boolean;
} }
const TableItemActionButtons = ({ onEditClick, onDeleteClick }: Props) => { const TableItemActionButtons = ({ onEditClick, onDeleteClick, isLoading }: Props) => {
const onFocusCapture = usePreventFocusAfterModalClosing(); const onFocusCapture = usePreventFocusAfterModalClosing();
if (isLoading) {
return (
<HStack spacing={ 6 } alignSelf="flex-end">
<Skeleton boxSize={ 5 } flexShrink={ 0 } borderRadius="base"/>
<Skeleton boxSize={ 5 } flexShrink={ 0 } borderRadius="base"/>
</HStack>
);
}
return ( return (
<HStack spacing={ 6 } alignSelf="flex-end"> <HStack spacing={ 6 } alignSelf="flex-end">
<Tooltip label="Edit"> <Tooltip label="Edit">
<IconButton <IconButton
aria-label="edit" aria-label="edit"
variant="simple" variant="simple"
w="30px" boxSize={ 5 }
h="30px"
onClick={ onEditClick } onClick={ onEditClick }
icon={ <Icon as={ EditIcon } w="20px" h="20px"/> } icon={ <EditIcon/> }
onFocusCapture={ onFocusCapture } onFocusCapture={ onFocusCapture }
display="inline-block"
flexShrink={ 0 }
borderRadius="none"
/> />
</Tooltip> </Tooltip>
<Tooltip label="Delete"> <Tooltip label="Delete">
<IconButton <IconButton
aria-label="delete" aria-label="delete"
variant="simple" variant="simple"
w="30px" boxSize={ 5 }
h="30px"
onClick={ onDeleteClick } onClick={ onDeleteClick }
icon={ <Icon as={ DeleteIcon } w="20px" h="20px"/> } icon={ <DeleteIcon/> }
onFocusCapture={ onFocusCapture } onFocusCapture={ onFocusCapture }
display="inline-block"
flexShrink={ 0 }
borderRadius="none"
/> />
</Tooltip> </Tooltip>
</HStack> </HStack>
......
import { Image, chakra } from '@chakra-ui/react'; import { Image, chakra, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -8,9 +8,15 @@ interface Props { ...@@ -8,9 +8,15 @@ interface Props {
hash?: string; hash?: string;
name?: string | null; name?: string | null;
className?: string; className?: string;
isLoading?: boolean;
} }
const TokenLogo = ({ hash, name, className }: Props) => { const TokenLogo = ({ hash, name, className, isLoading }: Props) => {
if (isLoading) {
return <Skeleton className={ className } borderRadius="base"/>;
}
const logoSrc = appConfig.network.assetsPathname && hash ? [ const logoSrc = appConfig.network.assetsPathname && hash ? [
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/', 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/',
appConfig.network.assetsPathname, appConfig.network.assetsPathname,
......
...@@ -17,6 +17,8 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers'; ...@@ -17,6 +17,8 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import CopyToClipboard from '../CopyToClipboard';
type Props = TokenTransfer & { type Props = TokenTransfer & {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
...@@ -84,6 +86,7 @@ const TokenTransferListItem = ({ ...@@ -84,6 +86,7 @@ const TokenTransferListItem = ({
<Address width={ addressWidth }> <Address width={ addressWidth }>
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash }/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash }/> }
</Address> </Address>
{ baseAddress ? { baseAddress ?
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> : <InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> :
...@@ -92,6 +95,7 @@ const TokenTransferListItem = ({ ...@@ -92,6 +95,7 @@ const TokenTransferListItem = ({
<Address width={ addressWidth }> <Address width={ addressWidth }>
<AddressIcon address={ to }/> <AddressIcon address={ to }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash }/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash }/> }
</Address> </Address>
</Flex> </Flex>
{ value && ( { value && (
......
import { Box, Icon, chakra } from '@chakra-ui/react'; import { Box, Icon, chakra, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -13,10 +13,11 @@ interface Props { ...@@ -13,10 +13,11 @@ interface Props {
className?: string; className?: string;
isDisabled?: boolean; isDisabled?: boolean;
truncation?: 'dynamic' | 'constant'; truncation?: 'dynamic' | 'constant';
isLoading?: boolean;
} }
const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynamic' }: Props) => { const TokenTransferNft = ({ hash, id, className, isDisabled, isLoading, truncation = 'dynamic' }: Props) => {
const Component = isDisabled ? Box : LinkInternal; const Component = isDisabled || isLoading ? Box : LinkInternal;
return ( return (
<Component <Component
...@@ -28,10 +29,12 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynam ...@@ -28,10 +29,12 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynam
w="100%" w="100%"
className={ className } className={ className }
> >
<Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/> <Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 1 } borderRadius="base">
<Box maxW="calc(100% - 34px)"> <Icon as={ nftPlaceholder } boxSize="30px" color="inherit"/>
</Skeleton>
<Skeleton isLoaded={ !isLoading } maxW="calc(100% - 34px)">
{ truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> } { truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> }
</Box> </Skeleton>
</Component> </Component>
); );
}; };
......
...@@ -14,6 +14,8 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers'; ...@@ -14,6 +14,8 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import CopyToClipboard from '../CopyToClipboard';
type Props = TokenTransfer & { type Props = TokenTransfer & {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
...@@ -32,14 +34,6 @@ const TokenTransferTableItem = ({ ...@@ -32,14 +34,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 +51,7 @@ const TokenTransferTableItem = ({ ...@@ -57,7 +51,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>
...@@ -71,6 +65,7 @@ const TokenTransferTableItem = ({ ...@@ -71,6 +65,7 @@ const TokenTransferTableItem = ({
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ baseAddress === from.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ baseAddress === from.hash }/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash }/> }
</Address> </Address>
</Td> </Td>
{ baseAddress && ( { baseAddress && (
...@@ -82,10 +77,11 @@ const TokenTransferTableItem = ({ ...@@ -82,10 +77,11 @@ const TokenTransferTableItem = ({
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ to }/> <AddressIcon address={ to }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 } isDisabled={ baseAddress === to.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 } isDisabled={ baseAddress === to.hash }/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash }/> }
</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>
); );
......
import { Box, Flex, Text, chakra, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, chakra, useColorModeValue, Skeleton } from '@chakra-ui/react';
import clamp from 'lodash/clamp'; import clamp from 'lodash/clamp';
import React from 'react'; import React from 'react';
...@@ -6,21 +6,28 @@ interface Props { ...@@ -6,21 +6,28 @@ interface Props {
className?: string; className?: string;
value: number; value: number;
colorScheme?: 'green' | 'gray'; colorScheme?: 'green' | 'gray';
isLoading?: boolean;
} }
const WIDTH = 50; const WIDTH = 50;
const Utilization = ({ className, value, colorScheme = 'green' }: Props) => { const Utilization = ({ className, value, colorScheme = 'green', isLoading }: Props) => {
const valueString = (clamp(value * 100 || 0, 0, 100)).toLocaleString(undefined, { maximumFractionDigits: 2 }) + '%'; const valueString = (clamp(value * 100 || 0, 0, 100)).toLocaleString(undefined, { maximumFractionDigits: 2 }) + '%';
const colorGrayScheme = useColorModeValue('gray.500', 'gray.400'); const colorGrayScheme = useColorModeValue('gray.500', 'gray.400');
const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500'; const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500';
return ( return (
<Flex className={ className } alignItems="center"> <Flex className={ className } alignItems="center" columnGap="10px">
<Box bg={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden"> <Skeleton isLoaded={ !isLoading } w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden">
<Box bg={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } h="100%">
<Box bg={ color } w={ valueString } h="100%"/> <Box bg={ color } w={ valueString } h="100%"/>
</Box> </Box>
<Text color={ color } ml="10px" fontWeight="bold">{ valueString }</Text> </Skeleton>
<Skeleton isLoaded={ !isLoading } color={ color } fontWeight="bold">
<span>
{ valueString }
</span>
</Skeleton>
</Flex> </Flex>
); );
}; };
......
import { chakra } from '@chakra-ui/react'; import { chakra, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -9,18 +9,26 @@ import isSelfHosted from 'lib/isSelfHosted'; ...@@ -9,18 +9,26 @@ import isSelfHosted from 'lib/isSelfHosted';
import AdbutlerBanner from './AdbutlerBanner'; import AdbutlerBanner from './AdbutlerBanner';
import CoinzillaBanner from './CoinzillaBanner'; import CoinzillaBanner from './CoinzillaBanner';
const AdBanner = ({ className }: { className?: string }) => { const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: boolean }) => {
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies); const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
if (!isSelfHosted() || hasAdblockCookie) { if (!isSelfHosted() || hasAdblockCookie) {
return null; return null;
} }
if (appConfig.ad.adButlerOn) { const content = appConfig.ad.adButlerOn ? <AdbutlerBanner/> : <CoinzillaBanner/>;
return <AdbutlerBanner className={ className }/>;
}
return <CoinzillaBanner className={ className }/>; return (
<Skeleton
className={ className }
isLoaded={ !isLoading }
borderRadius="none"
maxW={ appConfig.ad.adButlerOn ? '760px' : '728px' }
w="100%"
>
{ content }
</Skeleton>
);
}; };
export default chakra(AdBanner); export default chakra(AdBanner);
import { Box, Image, Link, Text, chakra } from '@chakra-ui/react'; import { Box, Image, Link, Text, chakra, Skeleton } from '@chakra-ui/react';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
...@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => { ...@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => {
} }
if (isLoading) { if (isLoading) {
return <Box className={ className } h={{ base: 12, lg: 6 }}/>; return <Skeleton className={ className } h={{ base: 12, lg: 6 }} maxW="1000px"/>;
} }
if (!adData) { if (!adData) {
......
...@@ -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, chakra, Tooltip } from '@chakra-ui/react'; import { Box, chakra, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon'; import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
...@@ -9,9 +9,14 @@ import AddressContractIcon from 'ui/shared/address/AddressContractIcon'; ...@@ -9,9 +9,14 @@ import AddressContractIcon from 'ui/shared/address/AddressContractIcon';
type Props = { type Props = {
address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name'>; address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name'>;
className?: string; className?: string;
isLoading?: boolean;
} }
const AddressIcon = ({ address, className }: Props) => { const AddressIcon = ({ address, className, isLoading }: Props) => {
if (isLoading) {
return <Skeleton boxSize={ 6 } className={ className } borderRadius="full" flexShrink={ 0 }/>;
}
if (address.is_contract) { if (address.is_contract) {
return ( return (
<AddressContractIcon className={ className }/> <AddressContractIcon className={ className }/>
...@@ -20,7 +25,7 @@ const AddressIcon = ({ address, className }: Props) => { ...@@ -20,7 +25,7 @@ const AddressIcon = ({ address, className }: Props) => {
return ( return (
<Tooltip label={ address.implementation_name }> <Tooltip label={ address.implementation_name }>
<Box className={ className } width="24px" display="inline-flex"> <Box className={ className } boxSize={ 6 } display="inline-flex">
<Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address.hash) }/> <Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address.hash) }/>
</Box> </Box>
</Tooltip> </Tooltip>
......
import { chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react'; import { chakra, shouldForwardProp, Tooltip, Box, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import type { HTMLAttributeAnchorTarget } from 'react'; import type { HTMLAttributeAnchorTarget } from 'react';
import React from 'react'; import React from 'react';
...@@ -17,6 +17,7 @@ type CommonProps = { ...@@ -17,6 +17,7 @@ type CommonProps = {
isDisabled?: boolean; isDisabled?: boolean;
fontWeight?: string; fontWeight?: string;
alias?: string | null; alias?: string | null;
isLoading?: boolean;
} }
type AddressTokenTxProps = { type AddressTokenTxProps = {
...@@ -39,7 +40,7 @@ type AddressTokenProps = { ...@@ -39,7 +40,7 @@ type AddressTokenProps = {
type Props = CommonProps & (AddressTokenTxProps | BlockProps | AddressTokenProps); type Props = CommonProps & (AddressTokenTxProps | BlockProps | AddressTokenProps);
const AddressLink = (props: Props) => { const AddressLink = (props: Props) => {
const { alias, type, className, truncation = 'dynamic', hash, fontWeight, target = '_self', isDisabled } = props; const { alias, type, className, truncation = 'dynamic', hash, fontWeight, target = '_self', isDisabled, isLoading } = props;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
let url; let url;
...@@ -81,6 +82,10 @@ const AddressLink = (props: Props) => { ...@@ -81,6 +82,10 @@ const AddressLink = (props: Props) => {
} }
})(); })();
if (isLoading) {
return <Skeleton className={ className } overflow="hidden" whiteSpace="nowrap">{ content }</Skeleton>;
}
if (isDisabled) { if (isDisabled) {
return ( return (
<chakra.span <chakra.span
......
import { Skeleton, Tag as ChakraTag } from '@chakra-ui/react';
import type { TagProps } from '@chakra-ui/react';
import React from 'react';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props extends TagProps {
isLoading?: boolean;
}
const Tag = ({ isLoading, ...props }: Props) => {
if (props.isTruncated && typeof props.children === 'string') {
if (!props.children) {
return null;
}
return (
<Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="sm" maxW="100%">
<TruncatedTextTooltip label={ props.children }>
<ChakraTag { ...props }/>
</TruncatedTextTooltip>
</Skeleton>
);
}
return (
<Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="sm" maxW="100%">
<ChakraTag { ...props }/>
</Skeleton>
);
};
export default React.memo(Tag);
...@@ -11,13 +11,14 @@ interface Props { ...@@ -11,13 +11,14 @@ interface Props {
imageUrl: string | null; imageUrl: string | null;
animationUrl: string | null; animationUrl: string | null;
className?: string; className?: string;
isLoading?: boolean;
} }
const NftMedia = ({ imageUrl, animationUrl, className }: Props) => { const NftMedia = ({ imageUrl, animationUrl, className, isLoading }: Props) => {
const [ type, setType ] = React.useState<MediaType | undefined>(!animationUrl ? 'image' : undefined); const [ type, setType ] = React.useState<MediaType | undefined>(!animationUrl ? 'image' : undefined);
React.useEffect(() => { React.useEffect(() => {
if (!animationUrl) { if (!animationUrl || isLoading) {
return; return;
} }
...@@ -45,9 +46,9 @@ const NftMedia = ({ imageUrl, animationUrl, className }: Props) => { ...@@ -45,9 +46,9 @@ const NftMedia = ({ imageUrl, animationUrl, className }: Props) => {
setType('image'); setType('image');
}); });
}, [ animationUrl ]); }, [ animationUrl, isLoading ]);
if (!type) { if (!type || isLoading) {
return ( return (
<AspectRatio <AspectRatio
className={ className } className={ className }
......
import { Flex, Skeleton, chakra } from '@chakra-ui/react'; import { Flex, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { RoutedTab } from '../RoutedTabs/types';
interface Props { interface Props {
className?: string; className?: string;
tabs?: Array<RoutedTab>;
size?: 'sm' | 'md';
} }
const SkeletonTabs = ({ className }: Props) => { const SkeletonTabs = ({ className, tabs, size = 'md' }: Props) => {
if (tabs) {
if (tabs.length === 1) {
return null;
}
const paddingHor = size === 'sm' ? 3 : 4;
const paddingVert = size === 'sm' ? 1 : 2;
return (
<Flex className={ className } my={ 8 } alignItems="center">
{ tabs.map(({ title, id }, index) => (
<Skeleton
key={ id }
py={ index === 0 ? paddingVert : 0 }
px={ index === 0 ? paddingHor : 0 }
mx={ index === 0 ? 0 : paddingHor }
borderRadius="base"
fontWeight={ 600 }
borderWidth={ size === 'sm' ? '2px' : 0 }
>
{ typeof title === 'string' ? title : title() }
</Skeleton>
)) }
</Flex>
);
}
return ( return (
<Flex my={ 8 } className={ className }> <Flex my={ 8 } className={ className }>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="100px"/> <Skeleton h={ 6 } my={ 2 } mx={ 4 } w="100px"/>
......
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,8 +46,14 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => { ...@@ -47,8 +46,14 @@ 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' }}
mt={{ base: 6, lg: 8 }}
_empty={{
display: 'none',
}}
>
<NetworkAddToWallet/>
{ SOCIAL_LINKS.map(sl => { { SOCIAL_LINKS.map(sl => {
return ( return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank"> <Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank">
...@@ -57,13 +62,12 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => { ...@@ -57,13 +62,12 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
); );
}) } }) }
</Stack> </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">
......
import { Flex, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { Address } from 'types/api/address';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
interface Props { interface Props {
tokenQuery: UseQueryResult<TokenInfo>; tokenQuery: UseQueryResult<TokenInfo>;
contractQuery: UseQueryResult<Address>;
} }
const TokenContractInfo = ({ tokenQuery }: Props) => { const TokenContractInfo = ({ tokenQuery, contractQuery }: Props) => {
const router = useRouter();
const contractQuery = useApiQuery('address', {
pathParams: { hash: router.query.hash?.toString() },
queryOptions: { enabled: Boolean(router.query.hash) },
});
if (tokenQuery.isLoading || contractQuery.isLoading) {
return (
<Flex alignItems="center">
<SkeletonCircle boxSize={ 6 }/>
<Skeleton w="400px" h={ 5 } ml={ 2 }/>
<Skeleton w={ 5 } h={ 5 } ml={ 1 }/>
<Skeleton w={ 9 } h={ 8 } ml={ 2 }/>
<Skeleton w={ 9 } h={ 8 } ml={ 2 }/>
</Flex>
);
}
// we show error in parent component, this is only for TS // we show error in parent component, this is only for TS
if (tokenQuery.isError) { if (tokenQuery.isError) {
return null; return null;
} }
const address = { const address = {
hash: tokenQuery.data.address, hash: tokenQuery.data?.address || '',
is_contract: true, is_contract: true,
implementation_name: null, implementation_name: null,
watchlist_names: [], watchlist_names: [],
...@@ -49,6 +29,7 @@ const TokenContractInfo = ({ tokenQuery }: Props) => { ...@@ -49,6 +29,7 @@ const TokenContractInfo = ({ tokenQuery }: Props) => {
<AddressHeadingInfo <AddressHeadingInfo
address={ address } address={ address }
token={ contractQuery.data?.token } token={ contractQuery.data?.token }
isLoading={ tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData }
/> />
); );
}; };
......
...@@ -8,11 +8,11 @@ import type { TokenInfo } from 'types/api/token'; ...@@ -8,11 +8,11 @@ import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import { TOKEN_COUNTERS } from 'stubs/token';
import type { TokenTabs } from 'ui/pages/Token'; import type { TokenTabs } from 'ui/pages/Token';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem'; import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
interface Props { interface Props {
tokenQuery: UseQueryResult<TokenInfo>; tokenQuery: UseQueryResult<TokenInfo>;
...@@ -23,7 +23,7 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -23,7 +23,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
const tokenCountersQuery = useApiQuery('token_counters', { const tokenCountersQuery = useApiQuery('token_counters', {
pathParams: { hash: router.query.hash?.toString() }, pathParams: { hash: router.query.hash?.toString() },
queryOptions: { enabled: Boolean(router.query.hash) }, queryOptions: { enabled: Boolean(router.query.hash), placeholderData: TOKEN_COUNTERS },
}); });
const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => { const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => {
...@@ -56,32 +56,19 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -56,32 +56,19 @@ const TokenDetails = ({ tokenQuery }: Props) => {
throw Error('Token fetch error', { cause: tokenQuery.error as unknown as Error }); throw Error('Token fetch error', { cause: tokenQuery.error as unknown as Error });
} }
if (tokenQuery.isLoading) {
return (
<Grid mt={ 10 } columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="20%"/>
<DetailsSkeletonRow w="20%"/>
<DetailsSkeletonRow w="10%"/>
</Grid>
);
}
const { const {
exchange_rate: exchangeRate, exchange_rate: exchangeRate,
total_supply: totalSupply, total_supply: totalSupply,
decimals, decimals,
symbol, symbol,
type, type,
} = tokenQuery.data; } = tokenQuery.data || {};
let marketcap; let marketcap;
let totalSupplyValue; let totalSupplyValue;
if (type === 'ERC-20') { if (type === 'ERC-20') {
const totalValue = totalSupply !== null ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined; const totalValue = totalSupply ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined;
marketcap = totalValue?.usd; marketcap = totalValue?.usd;
totalSupplyValue = totalValue?.valueStr; totalSupplyValue = totalValue?.valueStr;
} else { } else {
...@@ -119,40 +106,50 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -119,40 +106,50 @@ const TokenDetails = ({ tokenQuery }: Props) => {
alignSelf="center" alignSelf="center"
wordBreak="break-word" wordBreak="break-word"
whiteSpace="pre-wrap" whiteSpace="pre-wrap"
isLoading={ tokenQuery.isPlaceholderData }
> >
<Skeleton isLoaded={ !tokenQuery.isPlaceholderData }>
<Flex w="100%"> <Flex w="100%">
<Box whiteSpace="nowrap" overflow="hidden"> <Box whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ totalSupplyValue || '0' }/> <HashStringShortenDynamic hash={ totalSupplyValue || '0' }/>
</Box> </Box>
<Box flexShrink={ 0 }> { symbol || '' }</Box> <Box flexShrink={ 0 }> { symbol || '' }</Box>
</Flex> </Flex>
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Holders" title="Holders"
hint="Number of accounts holding the token" hint="Number of accounts holding the token"
alignSelf="center" alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
> >
{ tokenCountersQuery.isLoading && <Skeleton w={ 20 } h={ 4 }/> } <Skeleton isLoaded={ !tokenCountersQuery.isPlaceholderData }>
{ !tokenCountersQuery.isLoading && countersItem('token_holders_count') } { countersItem('token_holders_count') }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Transfers" title="Transfers"
hint="Number of transfer for the token" hint="Number of transfer for the token"
alignSelf="center" alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
> >
{ tokenCountersQuery.isLoading && <Skeleton w={ 20 } h={ 4 }/> } <Skeleton isLoaded={ !tokenCountersQuery.isPlaceholderData }>
{ !tokenCountersQuery.isLoading && countersItem('transfers_count') } { countersItem('transfers_count') }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
{ decimals && ( { decimals && (
<DetailsInfoItem <DetailsInfoItem
title="Decimals" title="Decimals"
hint="Number of digits that come after the decimal place when displaying token value" hint="Number of digits that come after the decimal place when displaying token value"
alignSelf="center" alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
> >
<Skeleton isLoaded={ !tokenQuery.isPlaceholderData } minW={ 6 }>
{ decimals } { decimals }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsSponsoredItem/> <DetailsSponsoredItem isLoading={ tokenQuery.isPlaceholderData }/>
</Grid> </Grid>
); );
}; };
......
import { Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -14,17 +15,17 @@ import TokenHoldersList from './TokenHoldersList'; ...@@ -14,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/>;
} }
...@@ -36,17 +37,30 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => { ...@@ -36,17 +37,30 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
const items = holdersQuery.data?.items; const items = holdersQuery.data?.items;
const content = items && tokenQuery.data ? ( const content = items && token ? (
<> <>
{ !isMobile && <TokenHoldersTable data={ items } token={ tokenQuery.data } top={ holdersQuery.isPaginationVisible ? 80 : 0 }/> } <Box display={{ base: 'none', lg: 'block' }}>
{ isMobile && <TokenHoldersList data={ items } token={ tokenQuery.data }/> } <TokenHoldersTable
data={ items }
token={ token }
top={ holdersQuery.isPaginationVisible ? 80 : 0 }
isLoading={ holdersQuery.isPlaceholderData }
/>
</Box>
<Box display={{ base: 'block', lg: 'none' }}>
<TokenHoldersList
data={ items }
token={ token }
isLoading={ holdersQuery.isPlaceholderData }
/>
</Box>
</> </>
) : null; ) : null;
return ( return (
<DataListDisplay <DataListDisplay
isError={ holdersQuery.isError || tokenQuery.isError } isError={ holdersQuery.isError }
isLoading={ holdersQuery.isLoading || tokenQuery.isLoading } isLoading={ false }
items={ holdersQuery.data?.items } items={ holdersQuery.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }} skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }}
emptyText="There are no holders for this token." emptyText="There are no holders for this token."
......
...@@ -8,16 +8,18 @@ import TokenHoldersListItem from './TokenHoldersListItem'; ...@@ -8,16 +8,18 @@ import TokenHoldersListItem from './TokenHoldersListItem';
interface Props { interface Props {
data: Array<TokenHolder>; data: Array<TokenHolder>;
token: TokenInfo; token: TokenInfo;
isLoading?: boolean;
} }
const TokenHoldersList = ({ data, token }: Props) => { const TokenHoldersList = ({ data, token, isLoading }: Props) => {
return ( return (
<Box> <Box>
{ data.map((item) => ( { data.map((item, index) => (
<TokenHoldersListItem <TokenHoldersListItem
key={ item.address.hash } key={ item.address.hash + (isLoading ? index : '') }
token={ token } token={ token }
holder={ item } holder={ item }
isLoading={ isLoading }
/> />
)) } )) }
</Box> </Box>
......
import { Flex } from '@chakra-ui/react'; import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -7,30 +7,44 @@ import type { TokenHolder, TokenInfo } from 'types/api/token'; ...@@ -7,30 +7,44 @@ import type { TokenHolder, TokenInfo } from 'types/api/token';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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 ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
interface Props { interface Props {
holder: TokenHolder; holder: TokenHolder;
token: TokenInfo; token: TokenInfo;
isLoading?: boolean;
} }
const TokenHoldersListItem = ({ holder, token }: Props) => { const TokenHoldersListItem = ({ holder, token, isLoading }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).dp(6).toFormat(); const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).dp(6).toFormat();
return ( return (
<ListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ holder.address }/> <AddressIcon address={ holder.address } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/> <AddressLink
type="address"
ml={ 2 }
fontWeight="700"
hash={ holder.address.hash }
alias={ holder.address.name }
flexGrow={ 1 }
isLoading={ isLoading }
/>
<CopyToClipboard text={ holder.address.hash } isLoading={ isLoading }/>
</Address> </Address>
<Flex justifyContent="space-between" alignItems="center" width="100%"> <Flex justifyContent="space-between" alignItems="center" width="100%">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ quantity } { quantity }
</Skeleton>
{ token.total_supply && ( { token.total_supply && (
<Utilization <Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() } value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green" colorScheme="green"
ml={ 6 } ml={ 6 }
isLoading={ isLoading }
/> />
) } ) }
</Flex> </Flex>
......
...@@ -10,9 +10,10 @@ interface Props { ...@@ -10,9 +10,10 @@ interface Props {
data: Array<TokenHolder>; data: Array<TokenHolder>;
token: TokenInfo; token: TokenInfo;
top: number; top: number;
isLoading?: boolean;
} }
const TokenHoldersTable = ({ data, token, top }: Props) => { const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
<Thead top={ top }> <Thead top={ top }>
...@@ -23,8 +24,8 @@ const TokenHoldersTable = ({ data, token, top }: Props) => { ...@@ -23,8 +24,8 @@ const TokenHoldersTable = ({ data, token, top }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item, index) => (
<TokenHoldersTableItem key={ item.address.hash } holder={ item } token={ token }/> <TokenHoldersTableItem key={ item.address.hash + (isLoading ? index : '') } holder={ item } token={ token } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td } from '@chakra-ui/react'; import { Tr, Td, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -7,33 +7,47 @@ import type { TokenHolder, TokenInfo } from 'types/api/token'; ...@@ -7,33 +7,47 @@ import type { TokenHolder, TokenInfo } from 'types/api/token';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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 Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
type Props = { type Props = {
holder: TokenHolder; holder: TokenHolder;
token: TokenInfo; token: TokenInfo;
isLoading?: boolean;
} }
const TokenTransferTableItem = ({ holder, token }: Props) => { const TokenTransferTableItem = ({ holder, token, isLoading }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).toFormat(); const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).toFormat();
return ( return (
<Tr> <Tr>
<Td> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ holder.address }/> <AddressIcon address={ holder.address } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/> <AddressLink
type="address"
ml={ 2 }
fontWeight="700"
hash={ holder.address.hash }
alias={ holder.address.name }
flexGrow={ 1 }
isLoading={ isLoading }
/>
<CopyToClipboard text={ holder.address.hash } isLoading={ isLoading }/>
</Address> </Address>
</Td> </Td>
<Td isNumeric> <Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ quantity } { quantity }
</Skeleton>
</Td> </Td>
{ token.total_supply && ( { token.total_supply && (
<Td isNumeric> <Td verticalAlign="middle" isNumeric>
<Utilization <Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() } value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green" colorScheme="green"
display="inline-flex" display="inline-flex"
isLoading={ isLoading }
/> />
</Td> </Td>
) } ) }
......
import { Grid, Skeleton } from '@chakra-ui/react'; import { Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -28,21 +28,6 @@ const TokenInventory = ({ inventoryQuery }: Props) => { ...@@ -28,21 +28,6 @@ const TokenInventory = ({ inventoryQuery }: Props) => {
</ActionBar> </ActionBar>
); );
const skeleton = (
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
);
const items = inventoryQuery.data?.items; const items = inventoryQuery.data?.items;
const content = items ? ( const content = items ? (
...@@ -52,19 +37,25 @@ const TokenInventory = ({ inventoryQuery }: Props) => { ...@@ -52,19 +37,25 @@ const TokenInventory = ({ inventoryQuery }: Props) => {
rowGap={{ base: 3, lg: 6 }} rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }} gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
> >
{ items.map((item) => <TokenInventoryItem key={ item.token.address + '_' + item.id } item={ item }/>) } { items.map((item, index) => (
<TokenInventoryItem
key={ item.token.address + '_' + item.id + (inventoryQuery.isPlaceholderData ? '_' + index : '') }
item={ item }
isLoading={ inventoryQuery.isPlaceholderData }
/>
)) }
</Grid> </Grid>
) : null; ) : null;
return ( return (
<DataListDisplay <DataListDisplay
isError={ inventoryQuery.isError } isError={ inventoryQuery.isError }
isLoading={ inventoryQuery.isLoading } isLoading={ false }
items={ items } items={ items }
emptyText="There are no tokens." emptyText="There are no tokens."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
skeletonProps={{ customSkeleton: skeleton }} skeletonProps={{ customSkeleton: null }}
/> />
); );
}; };
......
import { Flex, Text, LinkBox, LinkOverlay, useColorModeValue, Hide } from '@chakra-ui/react'; import { Flex, Text, LinkBox, LinkOverlay, useColorModeValue, Hide, Skeleton } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
...@@ -11,9 +11,9 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -11,9 +11,9 @@ import LinkInternal from 'ui/shared/LinkInternal';
import NftMedia from 'ui/shared/nft/NftMedia'; import NftMedia from 'ui/shared/nft/NftMedia';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = { item: TokenInstance }; type Props = { item: TokenInstance; isLoading: boolean };
const NFTItem = ({ item }: Props) => { const NFTItem = ({ item, isLoading }: Props) => {
return ( return (
<LinkBox <LinkBox
w={{ base: '100%', lg: '210px' }} w={{ base: '100%', lg: '210px' }}
...@@ -32,6 +32,7 @@ const NFTItem = ({ item }: Props) => { ...@@ -32,6 +32,7 @@ const NFTItem = ({ item }: Props) => {
mb="18px" mb="18px"
imageUrl={ item.image_url } imageUrl={ item.image_url }
animationUrl={ item.animation_url } animationUrl={ item.animation_url }
isLoading={ isLoading }
/> />
</LinkOverlay> </LinkOverlay>
</NextLink> </NextLink>
...@@ -39,13 +40,16 @@ const NFTItem = ({ item }: Props) => { ...@@ -39,13 +40,16 @@ const NFTItem = ({ item }: Props) => {
<Flex mb={ 2 } ml={ 1 }> <Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text> <Text whiteSpace="pre" variant="secondary">ID# </Text>
<TruncatedTextTooltip label={ item.id }> <TruncatedTextTooltip label={ item.id }>
<Skeleton isLoaded={ !isLoading } overflow="hidden">
<LinkInternal <LinkInternal
overflow="hidden" overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis" textOverflow="ellipsis"
whiteSpace="nowrap"
display="block"
> >
{ item.id } { item.id }
</LinkInternal> </LinkInternal>
</Skeleton>
</TruncatedTextTooltip> </TruncatedTextTooltip>
</Flex> </Flex>
) } ) }
...@@ -53,8 +57,8 @@ const NFTItem = ({ item }: Props) => { ...@@ -53,8 +57,8 @@ const NFTItem = ({ item }: Props) => {
<Flex mb={ 2 } ml={ 1 }> <Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary" mr={ 2 } lineHeight="24px">Owner</Text> <Text whiteSpace="pre" variant="secondary" mr={ 2 } lineHeight="24px">Owner</Text>
<Address> <Address>
<Hide below="lg" ssr={ false }><AddressIcon address={ item.owner } mr={ 1 }/></Hide> <Hide below="lg" ssr={ false }><AddressIcon address={ item.owner } mr={ 1 } isLoading={ isLoading }/></Hide>
<AddressLink hash={ item.owner.hash } alias={ item.owner.name } type="address" truncation="constant"/> <AddressLink hash={ item.owner.hash } alias={ item.owner.name } type="address" truncation="constant" isLoading={ isLoading }/>
</Address> </Address>
</Flex> </Flex>
) } ) }
......
import { Hide, Show } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/token';
import type { TokenTransferResponse } from 'types/api/tokenTransfer'; import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import useGradualIncrement from 'lib/hooks/useGradualIncrement'; import useGradualIncrement from 'lib/hooks/useGradualIncrement';
...@@ -14,7 +15,7 @@ import ActionBar from 'ui/shared/ActionBar'; ...@@ -14,7 +15,7 @@ import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; 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 SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable';
...@@ -24,12 +25,13 @@ type Props = { ...@@ -24,12 +25,13 @@ type Props = {
isPaginationVisible: boolean; isPaginationVisible: boolean;
}; };
tokenId?: string; tokenId?: string;
token?: TokenInfo;
} }
const TokenTransfer = ({ transfersQuery, tokenId }: Props) => { const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter(); const router = useRouter();
const { isError, isLoading, data, pagination, isPaginationVisible } = transfersQuery; const { isError, isLoading, isPlaceholderData, data, pagination, isPaginationVisible } = transfersQuery;
const [ newItemsCount, setNewItemsCount ] = useGradualIncrement(0); const [ newItemsCount, setNewItemsCount ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
...@@ -61,7 +63,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => { ...@@ -61,7 +63,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Hide below="lg" ssr={ false }> <Box display={{ base: 'none', lg: 'block' }}>
<TokenTransferTable <TokenTransferTable
data={ data?.items } data={ data?.items }
top={ isPaginationVisible ? 80 : 0 } top={ isPaginationVisible ? 80 : 0 }
...@@ -69,20 +71,22 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => { ...@@ -69,20 +71,22 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
tokenId={ tokenId } tokenId={ tokenId }
token={ token }
isLoading={ isPlaceholderData }
/> />
</Hide> </Box>
<Show below="lg" ssr={ false }> <Box display={{ base: 'block', lg: 'none' }}>
{ pagination.page === 1 && ( { pagination.page === 1 && (
<SocketNewItemsNotice <SocketNewItemsNotice.Mobile
url={ window.location.href } url={ window.location.href }
num={ newItemsCount } num={ newItemsCount }
alert={ socketAlert } alert={ socketAlert }
type="token_transfer" type="token_transfer"
borderBottomRadius={ 0 } isLoading={ isPlaceholderData }
/> />
) } ) }
<TokenTransferList data={ data?.items } tokenId={ tokenId }/> <TokenTransferList data={ data?.items } tokenId={ tokenId } isLoading={ isPlaceholderData }/>
</Show> </Box>
</> </>
) : null; ) : null;
...@@ -95,7 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => { ...@@ -95,7 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ !isPlaceholderData && isLoading }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonProps={{
isLongSkeleton: true, isLongSkeleton: true,
......
...@@ -8,9 +8,10 @@ import TokenTransferListItem from 'ui/token/TokenTransfer/TokenTransferListItem' ...@@ -8,9 +8,10 @@ import TokenTransferListItem from 'ui/token/TokenTransfer/TokenTransferListItem'
interface Props { interface Props {
data: Array<TokenTransfer>; data: Array<TokenTransfer>;
tokenId?: string; tokenId?: string;
isLoading?: boolean;
} }
const TokenTransferList = ({ data, tokenId }: Props) => { const TokenTransferList = ({ data, tokenId, isLoading }: Props) => {
return ( return (
<Box> <Box>
{ data.map((item, index) => ( { data.map((item, index) => (
...@@ -18,6 +19,7 @@ const TokenTransferList = ({ data, tokenId }: Props) => { ...@@ -18,6 +19,7 @@ const TokenTransferList = ({ data, tokenId }: Props) => {
key={ index } key={ index }
{ ...item } { ...item }
tokenId={ tokenId } tokenId={ tokenId }
isLoading={ isLoading }
/> />
)) } )) }
</Box> </Box>
......
import { Text, Flex, Tag, Icon, useColorModeValue } from '@chakra-ui/react'; import { Text, Flex, Icon, useColorModeValue, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -11,10 +11,12 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol'; ...@@ -11,10 +11,12 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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 Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & {tokenId?: string}; type Props = TokenTransfer & { tokenId?: string; isLoading?: boolean };
const TokenTransferListItem = ({ const TokenTransferListItem = ({
token, token,
...@@ -25,6 +27,7 @@ const TokenTransferListItem = ({ ...@@ -25,6 +27,7 @@ const TokenTransferListItem = ({
method, method,
timestamp, timestamp,
tokenId, tokenId,
isLoading,
}: Props) => { }: Props) => {
const value = (() => { const value = (() => {
if (!('value' in total)) { if (!('value' in total)) {
...@@ -42,44 +45,68 @@ const TokenTransferListItem = ({ ...@@ -42,44 +45,68 @@ const TokenTransferListItem = ({
<ListItemMobile rowGap={ 3 } isAnimated> <ListItemMobile rowGap={ 3 } isAnimated>
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%"> <Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Flex> <Flex>
<Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 2 }>
<Icon <Icon
as={ transactionIcon } as={ transactionIcon }
boxSize="30px" boxSize="30px"
mr={ 2 }
color={ iconColor } color={ iconColor }
/> />
</Skeleton>
<Address width="100%"> <Address width="100%">
<AddressLink <AddressLink
hash={ txHash } hash={ txHash }
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
</Address> </Address>
</Flex> </Flex>
{ timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> } { timestamp && (
<Text variant="secondary" fontWeight="400" fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
<span>
{ timeAgo }
</span>
</Skeleton>
</Text>
) }
</Flex> </Flex>
{ method && <Tag colorScheme="gray">{ method }</Tag> } { method && <Tag isLoading={ isLoading }>{ method }</Tag> }
<Flex w="100%" columnGap={ 3 }> <Flex w="100%" columnGap={ 3 }>
<Address width="50%"> <Address width="50%">
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/>
<CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address> </Address>
<Skeleton isLoaded={ !isLoading } boxSize={ 6 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Skeleton>
<Address width="50%"> <Address width="50%">
<AddressIcon address={ to }/> <AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address }/> <AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/>
<CopyToClipboard text={ to.hash } isLoading={ isLoading }/>
</Address> </Address>
</Flex> </Flex>
{ value && (token.type === 'ERC-20' || token.type === 'ERC-1155') && ( { value && (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text> <Skeleton isLoaded={ !isLoading } flexShrink={ 0 } fontWeight={ 500 }>
<Text variant="secondary">{ value }</Text> Value
<Text>{ trimTokenSymbol(token.symbol) }</Text> </Skeleton>
<Skeleton isLoaded={ !isLoading } variant="secondary">
{ value }
</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ trimTokenSymbol(token.symbol) }</Skeleton>
</Flex> </Flex>
) } ) }
{ 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') && { 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
<TokenTransferNft hash={ token.address } id={ total.token_id } isDisabled={ Boolean(tokenId && tokenId === total.token_id) }/> } <TokenTransferNft
hash={ token.address }
id={ total.token_id }
isDisabled={ Boolean(tokenId && tokenId === total.token_id) }
isLoading={ isLoading }
/>
) }
</ListItemMobile> </ListItemMobile>
); );
}; };
......
import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableItem'; import TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableItem';
...@@ -15,11 +16,12 @@ interface Props { ...@@ -15,11 +16,12 @@ interface Props {
socketInfoAlert?: string; socketInfoAlert?: string;
socketInfoNum?: number; socketInfoNum?: number;
tokenId?: string; tokenId?: string;
isLoading?: boolean;
token?: TokenInfo;
} }
const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socketInfoNum, tokenId }: Props) => { const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socketInfoNum, tokenId, isLoading, token }: Props) => {
const tokenType = data[0].token.type; const tokenType = data[0].token.type;
const tokenSymbol = data[0].token.symbol;
return ( return (
<Table variant="simple" size="sm" minW="950px"> <Table variant="simple" size="sm" minW="950px">
...@@ -27,30 +29,26 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket ...@@ -27,30 +29,26 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket
<Tr> <Tr>
<Th width={ tokenType === 'ERC-1155' ? '60%' : '80%' }>Txn hash</Th> <Th width={ tokenType === 'ERC-1155' ? '60%' : '80%' }>Txn hash</Th>
<Th width="164px">Method</Th> <Th width="164px">Method</Th>
<Th width="148px">From</Th> <Th width="160px">From</Th>
<Th width="36px" px={ 0 }/> <Th width="36px" px={ 0 }/>
<Th width="218px" >To</Th> <Th width="218px" >To</Th>
{ (tokenType === 'ERC-721' || tokenType === 'ERC-1155') && <Th width="20%" isNumeric={ tokenType === 'ERC-721' }>Token ID</Th> } { (tokenType === 'ERC-721' || tokenType === 'ERC-1155') && <Th width="20%" isNumeric={ tokenType === 'ERC-721' }>Token ID</Th> }
{ (tokenType === 'ERC-20' || tokenType === 'ERC-1155') && <Th width="20%" isNumeric whiteSpace="nowrap">Value { trimTokenSymbol(tokenSymbol) }</Th> } { (tokenType === 'ERC-20' || tokenType === 'ERC-1155') &&
<Th width="20%" isNumeric whiteSpace="nowrap">Value { trimTokenSymbol(token?.symbol || '') }</Th> }
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ showSocketInfo && ( { showSocketInfo && (
<Tr> <SocketNewItemsNotice.Desktop
<Td colSpan={ 10 } p={ 0 }>
<SocketNewItemsNotice
borderRadius={ 0 }
pl="10px"
url={ window.location.href } url={ window.location.href }
alert={ socketInfoAlert } alert={ socketInfoAlert }
num={ socketInfoNum } num={ socketInfoNum }
type="token_transfer" type="token_transfer"
isLoading={ isLoading }
/> />
</Td>
</Tr>
) } ) }
{ data.map((item, index) => ( { data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item } tokenId={ tokenId }/> <TokenTransferTableItem key={ index } { ...item } tokenId={ tokenId } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Tag, Text, Icon, Grid } from '@chakra-ui/react'; import { Tr, Td, Icon, Grid, Skeleton, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -9,9 +9,11 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -9,9 +9,11 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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 Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & { tokenId?: string } type Props = TokenTransfer & { tokenId?: string; isLoading?: boolean }
const TokenTransferTableItem = ({ const TokenTransferTableItem = ({
token, token,
...@@ -22,33 +24,36 @@ const TokenTransferTableItem = ({ ...@@ -22,33 +24,36 @@ const TokenTransferTableItem = ({
method, method,
timestamp, timestamp,
tokenId, tokenId,
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 (
<Tr alignItems="top"> <Tr alignItems="top">
<Td> <Td>
<Grid alignItems="center" gridTemplateColumns="auto 130px" width="fit-content"> <Grid alignItems="center" gridTemplateColumns="auto 130px" width="fit-content" py="7px">
<Address display="inline-flex" fontWeight={ 600 } lineHeight="30px"> <Address display="inline-flex" fontWeight={ 600 }>
<AddressLink type="transaction" hash={ txHash }/> <AddressLink type="transaction" hash={ txHash } isLoading={ isLoading }/>
</Address> </Address>
{ timestamp && <Text color="gray.500" fontWeight="400" ml="10px">{ timeAgo }</Text> } { timestamp && (
<Skeleton isLoaded={ !isLoading } display="inline-block" color="gray.500" fontWeight="400" ml="10px">
<span>
{ timeAgo }
</span>
</Skeleton>
) }
</Grid> </Grid>
</Td> </Td>
<Td> <Td>
{ method ? <Tag colorScheme="gray">{ method }</Tag> : '-' } { method ? (
<Box my="3px">
<Tag isLoading={ isLoading } isTruncated>{ method }</Tag>
</Box>
) : null }
</Td> </Td>
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" py="3px">
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink <AddressLink
ml={ 2 } ml={ 2 }
flexGrow={ 1 } flexGrow={ 1 }
...@@ -58,15 +63,19 @@ const TokenTransferTableItem = ({ ...@@ -58,15 +63,19 @@ const TokenTransferTableItem = ({
alias={ from.name } alias={ from.name }
tokenHash={ token.address } tokenHash={ token.address }
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
<CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address> </Address>
</Td> </Td>
<Td px={ 0 }> <Td px={ 0 }>
<Skeleton isLoaded={ !isLoading } boxSize={ 6 } my="3px">
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Skeleton>
</Td> </Td>
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" py="3px">
<AddressIcon address={ to }/> <AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink <AddressLink
ml={ 2 } ml={ 2 }
flexGrow={ 1 } flexGrow={ 1 }
...@@ -76,25 +85,30 @@ const TokenTransferTableItem = ({ ...@@ -76,25 +85,30 @@ const TokenTransferTableItem = ({
alias={ to.name } alias={ to.name }
tokenHash={ token.address } tokenHash={ token.address }
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
<CopyToClipboard text={ to.hash } isLoading={ isLoading }/>
</Address> </Address>
</Td> </Td>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && ( { (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
<Td lineHeight="30px"> <Td>
{ 'token_id' in total ? ( { 'token_id' in total ? (
<TokenTransferNft <TokenTransferNft
hash={ token.address } hash={ token.address }
id={ total.token_id } id={ total.token_id }
justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' } justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' }
isDisabled={ Boolean(tokenId && tokenId === total.token_id) } isDisabled={ Boolean(tokenId && tokenId === total.token_id) }
isLoading={ isLoading }
/> />
) : '-' ) : ''
} }
</Td> </Td>
) } ) }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && ( { (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Td isNumeric verticalAlign="top" lineHeight="30px"> <Td isNumeric verticalAlign="top">
{ value || '-' } <Skeleton isLoaded={ !isLoading } my="7px">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Skeleton>
</Td> </Td>
) } ) }
</Tr> </Tr>
......
...@@ -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';
...@@ -48,6 +51,17 @@ const TokenInstanceContent = () => { ...@@ -48,6 +51,17 @@ 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 backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance');
...@@ -62,11 +76,16 @@ const TokenInstanceContent = () => { ...@@ -62,11 +76,16 @@ const TokenInstanceContent = () => {
}, [ appProps.referrer, hash ]); }, [ appProps.referrer, hash ]);
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> }, {
// there is no api for this tab yet id: 'token_transfers',
// { id: 'holders', title: 'Holders', component: <span>Holders</span> }, title: 'Token transfers',
component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id } token={ tokenInstanceQuery.data?.token }/>,
},
shouldFetchHolders ?
{ 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 });
...@@ -111,6 +130,17 @@ const TokenInstanceContent = () => { ...@@ -111,6 +130,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 }/>
...@@ -130,12 +160,14 @@ const TokenInstanceContent = () => { ...@@ -130,12 +160,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>
{ tokenInstanceQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } } tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && transfersQuery.isPaginationVisible && tab !== 'metadata' ? <Pagination { ...transfersQuery.pagination }/> : null } rightSlot={ !isMobile && isPaginationVisible && pagination ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile } 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>
......
...@@ -9,6 +9,7 @@ import eastArrowIcon from 'icons/arrows/east.svg'; ...@@ -9,6 +9,7 @@ import eastArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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 ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
...@@ -29,12 +30,16 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -29,12 +30,16 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit:
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash }/>
<CopyToClipboard text={ from.hash }/>
</Address> </Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
{ toData && (
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/> <AddressIcon address={ toData }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash }/>
<CopyToClipboard text={ toData.hash }/>
</Address> </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>
......
...@@ -9,6 +9,7 @@ import rightArrowIcon from 'icons/arrows/east.svg'; ...@@ -9,6 +9,7 @@ import rightArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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 TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
...@@ -34,16 +35,20 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -34,16 +35,20 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit:
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
<CopyToClipboard text={ from.hash }/>
</Address> </Address>
</Td> </Td>
<Td px={ 0 } verticalAlign="middle"> <Td px={ 0 } verticalAlign="middle">
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
{ toData && (
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/> <AddressIcon address={ toData }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/> <AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/>
<CopyToClipboard text={ toData.hash }/>
</Address> </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() }
......
...@@ -18,6 +18,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -18,6 +18,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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 InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
...@@ -102,6 +103,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: ...@@ -102,6 +103,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
ml={ 2 } ml={ 2 }
isDisabled={ isOut } isDisabled={ isOut }
/> />
{ !isOut && <CopyToClipboard text={ tx.from.hash }/> }
</Address> </Address>
{ (isIn || isOut) ? { (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut } width="48px" mx={ 2 }/> : ( <InOutTag isIn={ isIn } isOut={ isOut } width="48px" mx={ 2 }/> : (
...@@ -123,6 +125,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: ...@@ -123,6 +125,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
ml={ 2 } ml={ 2 }
isDisabled={ isIn } isDisabled={ isIn }
/> />
{ !isIn && <CopyToClipboard text={ dataTo.hash }/> }
</Address> </Address>
) : '-' } ) : '-' }
</Flex> </Flex>
......
...@@ -46,9 +46,9 @@ const TxsTable = ({ ...@@ -46,9 +46,9 @@ const TxsTable = ({
<Th width="160px">Type</Th> <Th width="160px">Type</Th>
<Th width="20%">Method</Th> <Th width="20%">Method</Th>
{ showBlockInfo && <Th width="18%">Block</Th> } { showBlockInfo && <Th width="18%">Block</Th> }
<Th width={{ xl: '132px', base: '66px' }}>From</Th> <Th width={{ xl: '152px', base: '86px' }}>From</Th>
<Th width={{ xl: currentAddress ? '48px' : '36px', base: currentAddress ? '52px' : '28px' }}></Th> <Th width={{ xl: currentAddress ? '48px' : '36px', base: currentAddress ? '52px' : '28px' }}></Th>
<Th width={{ xl: '132px', base: '66px' }}>To</Th> <Th width={{ xl: '152px', base: '86px' }}>To</Th>
<Th width="20%" isNumeric> <Th width="20%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end"> <Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> } { sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
......
...@@ -20,6 +20,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -20,6 +20,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
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 CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -47,6 +48,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -47,6 +48,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
<Address w="100%"> <Address w="100%">
<AddressIcon address={ tx.from }/> <AddressIcon address={ tx.from }/>
<AddressLink type="address" hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isOut }/> <AddressLink type="address" hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isOut }/>
{ !isOut && <CopyToClipboard text={ tx.from.hash }/> }
</Address> </Address>
); );
...@@ -54,6 +56,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -54,6 +56,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
<Address w="100%"> <Address w="100%">
<AddressIcon address={ dataTo }/> <AddressIcon address={ dataTo }/>
<AddressLink type="address" hash={ dataTo.hash } alias={ dataTo.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isIn }/> <AddressLink type="address" hash={ dataTo.hash } alias={ dataTo.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isIn }/>
{ !isIn && <CopyToClipboard text={ dataTo.hash }/> }
</Address> </Address>
) : '-'; ) : '-';
...@@ -88,13 +91,13 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -88,13 +91,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>
......
import { Icon } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { AddressWithdrawalsItem } from 'types/api/address';
import type { BlockWithdrawalsItem } from 'types/api/block';
import type { WithdrawalsItem } from 'types/api/withdrawals';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import dayjs from 'lib/date/dayjs';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = {
item: WithdrawalsItem;
view: 'list';
} | {
item: AddressWithdrawalsItem;
view: 'address';
} | {
item: BlockWithdrawalsItem;
view: 'block';
};
const WithdrawalsListItem = ({ item, view }: Props) => {
return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label>Index</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.index }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Validator index</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.validator_index }
</ListItemMobileGrid.Value>
{ view !== 'block' && (
<>
<ListItemMobileGrid.Label>Block</ListItemMobileGrid.Label><ListItemMobileGrid.Value>
<LinkInternal
href={ route({ pathname: '/block/[height]', query: { height: item.block_number.toString() } }) }
display="flex"
width="fit-content"
alignItems="center"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
{ item.block_number }
</LinkInternal>
</ListItemMobileGrid.Value>
</>
) }
{ view !== 'address' && (
<>
<ListItemMobileGrid.Label>To</ListItemMobileGrid.Label><ListItemMobileGrid.Value>
<Address>
<AddressIcon address={ item.receiver }/>
<AddressLink type="address" hash={ item.receiver.hash } truncation="dynamic" ml={ 2 }/>
</Address>
</ListItemMobileGrid.Value>
</>
) }
{ view !== 'block' && (
<>
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ dayjs(item.timestamp).fromNow() }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<CurrencyValue value={ item.amount } currency={ appConfig.network.currency.symbol }/>
</ListItemMobileGrid.Value>
</>
) }
</ListItemMobileGrid.Container>
);
};
export default WithdrawalsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { AddressWithdrawalsItem } from 'types/api/address';
import type { BlockWithdrawalsItem } from 'types/api/block';
import type { WithdrawalsItem } from 'types/api/withdrawals';
import appConfig from 'configs/app/config';
import { default as Thead } from 'ui/shared/TheadSticky';
import WithdrawalsTableItem from './WithdrawalsTableItem';
type Props = {
top: number;
} & ({
items: Array<WithdrawalsItem>;
view: 'list';
} | {
items: Array<AddressWithdrawalsItem>;
view: 'address';
} | {
items: Array<BlockWithdrawalsItem>;
view: 'block';
});
const WithdrawalsTable = ({ items, top, view = 'list' }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
<Th>Index</Th>
<Th>Validator index</Th>
{ view !== 'block' && <Th>Block</Th> }
{ view !== 'address' && <Th>To</Th> }
{ view !== 'block' && <Th>Age</Th> }
<Th>{ `Value ${ appConfig.network.currency.symbol }` }</Th>
</Tr>
</Thead>
<Tbody>
{ view === 'list' && (items as Array<WithdrawalsItem>).map((item) => (
<WithdrawalsTableItem key={ item.index } item={ item } view="list"/>
)) }
{ view === 'address' && (items as Array<AddressWithdrawalsItem>).map((item) => (
<WithdrawalsTableItem key={ item.index } item={ item } view="address"/>
)) }
{ view === 'block' && (items as Array<BlockWithdrawalsItem>).map((item) => (
<WithdrawalsTableItem key={ item.index } item={ item } view="block"/>
)) }
</Tbody>
</Table>
);
};
export default WithdrawalsTable;
import { Td, Tr, Text, Icon } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { AddressWithdrawalsItem } from 'types/api/address';
import type { BlockWithdrawalsItem } from 'types/api/block';
import type { WithdrawalsItem } from 'types/api/withdrawals';
import blockIcon from 'icons/block.svg';
import dayjs from 'lib/date/dayjs';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = {
item: WithdrawalsItem;
view: 'list';
} | {
item: AddressWithdrawalsItem;
view: 'address';
} | {
item: BlockWithdrawalsItem;
view: 'block';
};
const WithdrawalsTableItem = ({ item, view }: Props) => {
return (
<Tr>
<Td verticalAlign="middle">
<Text>{ item.index }</Text>
</Td>
<Td verticalAlign="middle">
<Text>{ item.validator_index }</Text>
</Td>
{ view !== 'block' && (
<Td verticalAlign="middle">
<LinkInternal
href={ route({ pathname: '/block/[height]', query: { height: item.block_number.toString() } }) }
display="flex"
width="fit-content"
alignItems="center"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
{ item.block_number }
</LinkInternal>
</Td>
) }
{ view !== 'address' && (
<Td verticalAlign="middle">
<Address>
<AddressIcon address={ item.receiver }/>
<AddressLink type="address" hash={ item.receiver.hash } truncation="constant" ml={ 2 }/>
</Address>
</Td>
) }
{ view !== 'block' && (
<Td verticalAlign="middle" pr={ 12 }>
<Text variant="secondary">{ dayjs(item.timestamp).fromNow() }</Text>
</Td>
) }
<Td verticalAlign="middle">
<CurrencyValue value={ item.amount }/>
</Td>
</Tr>
);
};
export default WithdrawalsTableItem;
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