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_
NEXT_PUBLIC_AD_DOMAIN_WITH_AD=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_DOMAIN_WITH_AD__
NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__
NEXT_PUBLIC_WEB3_DEFAULT_WALLET=__PLACEHOLDER_FOR_NEXT_PUBLIC_WEB3_DEFAULT_WALLET__
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=__PLACEHOLDER_FOR_NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET__
# api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
NEXT_PUBLIC_API_BASE_PATH=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_BASE_PATH__
NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__
NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL__
NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__
NEXT_PUBLIC_VISUALIZE_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_VISUALIZE_API_HOST__
NEXT_PUBLIC_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
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_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 = {
groups: [
'module',
'/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' ],
],
alphabetize: { order: 'asc', ignoreCase: true },
......
......@@ -68,7 +68,7 @@
},
{
"type": "shell",
"command": "NEXT_PUBLIC_L1_BASE_URL=https://${input:goerliApiHost} yarn dev:goerli:optimism",
"command": "NEXT_PUBLIC_API_HOST=${input:L2ApiHost} NEXT_PUBLIC_L1_BASE_URL=https://${input:goerliApiHost} yarn dev:goerli:optimism",
"problemMatcher": [],
"label": "dev server: goerli optimism",
"detail": "start local dev server for Goerli Optimism network",
......@@ -379,5 +379,15 @@
],
"default": ""
},
{
"type": "pickString",
"id": "L2ApiHost",
"description": "Choose L2 API host:",
"options": [
"blockscout-optimism-goerli.test.aws-k8s.blockscout.com",
"base-goerli.blockscout.com",
],
"default": ""
},
],
}
\ No newline at end of file
/* eslint-disable no-restricted-properties */
import type { WalletType } from 'types/client/wallets';
import type { NetworkExplorer } from 'types/networks';
import type { ChainIndicatorId } from 'ui/home/indicators/types';
......@@ -11,6 +12,15 @@ const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
}
};
const stripTrailingSlash = (str: string) => str[str.length - 1] === '/' ? str.slice(0, -1) : str;
const getWeb3DefaultWallet = (): WalletType => {
const envValue = getEnvValue(process.env.NEXT_PUBLIC_WEB3_DEFAULT_WALLET);
const SUPPORTED_WALLETS: Array<WalletType> = [
'metamask',
'coinbase',
];
return (envValue && SUPPORTED_WALLETS.includes(envValue) ? envValue : 'metamask') as WalletType;
};
const env = process.env.NODE_ENV;
const isDev = env === 'development';
......@@ -36,6 +46,8 @@ const apiEndpoint = apiHost ? [
apiPort && ':' + apiPort,
].filter(Boolean).join('') : 'https://blockscout.com';
const socketSchema = getEnvValue(process.env.NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL) || 'wss';
const logoutUrl = (() => {
try {
const envUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_URL);
......@@ -104,10 +116,14 @@ const config = Object.freeze({
domainWithAd: getEnvValue(process.env.NEXT_PUBLIC_AD_DOMAIN_WITH_AD) || 'blockscout.com',
adButlerOn: getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_ON) === 'true',
},
web3: {
defaultWallet: getWeb3DefaultWallet(),
disableAddTokenToWallet: getEnvValue(process.env.NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET) === 'true',
},
api: {
host: apiHost,
endpoint: apiEndpoint,
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
socket: apiHost ? `${ socketSchema }://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
},
L2: {
......@@ -115,6 +131,9 @@ const config = Object.freeze({
L1BaseUrl: getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_URL),
withdrawalUrl: getEnvValue(process.env.NEXT_PUBLIC_L2_WITHDRAWAL_URL) || '',
},
beaconChain: {
hasBeaconChain: getEnvValue(process.env.NEXT_PUBLIC_HAS_BEACON_CHAIN) === 'true',
},
statsApi: {
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
basePath: '',
......
......@@ -3,6 +3,8 @@ NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/front
NEXT_PUBLIC_NETWORK_EXPLORERS=
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_WEB3_DEFAULT_WALLET=coinbase
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=true
# network config
NEXT_PUBLIC_NETWORK_NAME=Base Göerli
......@@ -10,14 +12,14 @@ NEXT_PUBLIC_NETWORK_SHORT_NAME=Base
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=optimism
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/base.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/base.svg
NEXT_PUBLIC_NETWORK_ID=420
NEXT_PUBLIC_NETWORK_ID=84531
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://goerli.optimism.io
NEXT_PUBLIC_NETWORK_RPC_URL=https://goerli.base.org
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
......
......@@ -4,8 +4,8 @@ NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
#NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT='linear-gradient(136.9deg, #235643 1.5%, #16191E 77.77%)'
#NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#DCFE76'
#NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=linear-gradient(136.9deg, \#235643 1.5%, \#16191E 77.77%)
#NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=\#DCFE76
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/poa.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/poa.svg
......
blockscout:
environment:
INDEXER_OPTIMISM_L1_RPC:
_default: ENC[AES256_GCM,data:a02FoR3U/KlxsFVFiSGSLdFOFIDwS5eBgw==,iv:rmT8bVh3xyqKeebtnT+/eIC0bSGWKZhJ9H52cUsqFxM=,tag:OsRo1D2DtfMIgCGjAvqobw==,type:str]
ACCOUNT_USERNAME:
_default: ENC[AES256_GCM,data:n9Wc7xjBFdWHJNaKBwpVVykz3FbBtqicKdf6yD/kMLKuts/0Rv8vfQ20gSahIvSbbno=,iv:FRyRAwelWF1PHqbIJX09MH+VVqW53luYraLYq/A21j4=,tag:YUejqoXCZjWNUhRZ9emd+g==,type:str]
ACCOUNT_PASSWORD:
......@@ -16,10 +18,6 @@ blockscout:
_default: ENC[AES256_GCM,data:bgP4VwZ91eMzJVQ3/+fkqNCwBIAv3P12qRoGWu9QkfCKpj3e+Dwt1qjsYV1iNkcjQAJx3jOpVGlsbgdcBGFzHw==,iv:gpa6tbkxHv56wwI3Owdnr5MArJYdiVO3A0UMOsrgCls=,tag:CIgupR1ZNjsVtFYVG2OtHA==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_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:
_default: ENC[AES256_GCM,data:i08k9qiHA/31nyAri5pIm8MqCUZXLWjvQgcYpAyrcsToszt/L+QsYVcdCEerI0udtd2gJvLuRz3k8GcpZ9OfQB2Y0kJn,iv:Pt3rg7GjhfDw5S4VV9HpLSDsO4AhXlGIsNhdc5rYqCQ=,tag:zL/bLVgSS41kgnv69GxLMg==,type:str]
ACCOUNT_SENDGRID_SENDER:
......@@ -50,8 +48,6 @@ blockscout:
_default: ENC[AES256_GCM,data:fHIsaJQY6YrvoJKFDFZlBuunFD6QKYdUUOoW+aLV/44VCsWhrXbMUQ==,iv:teJEbP6pVC4WHeJwptf/DfRbp5Y8x/0OExTfClfLPyU=,tag:5nkNFZcJEFg5v0br2JUpnA==,type:str]
RE_CAPTCHA_CLIENT_KEY:
_default: ENC[AES256_GCM,data:ROEBG5XrOwAofN2ZnFnwekuqvHjDFu1Dp5V20Ud9gvqV1xc324InYA==,iv:EubA1HilDAtNaqdpTbFWaWSAf8kCiWgStada1dKpOD8=,tag:ry4796qJAekQKlz4sWGQKQ==,type:str]
INDEXER_OPTIMISM_RPC_L1:
_default: ENC[AES256_GCM,data:y93o/DxAWlQKSu5NTStN9czzoQdy4Fo/Dg==,iv:LgVC0FQ/IEs3x876p6s47FaXbdxNuDOzUdoAOjnWzpU=,tag:A68lXRAZFZ5N3HNwppqV1A==,type:str]
scVerifier:
environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
......@@ -92,8 +88,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-03-01T17:32:26Z"
mac: ENC[AES256_GCM,data:X1CPQWSFgieHg/elCyqPqQekMYsK/Go4R4sOsNE4XACseUzQr725vlo7XqZqPmRnnXz5bJu6MZAjSTbGCrPzybZV7ZUv94j9uPQSWH4x9SJicNe9ZHApeWapb/+PDz6xO9k8Tdug8WgVgy5kilHdYi6uGoIgeAwC7RwNG7TfyeQ=,iv:MpGf7GkzmVw5MZn0IMCDBAg0WQO1mzDc2ptHUxQNHUk=,tag:hsHMaU4MMc7df6Twc8dqSQ==,type:str]
lastmodified: "2023-04-27T16:27:47Z"
mac: ENC[AES256_GCM,data:TUIxPt/HMLzkWlPdBcM2mKGlNU+6ob+EWvtfKWAHZaVhH2qwoi+HI+Ncx+j3H9/MR6q9Uhhr611s8rCF0LdwbsK+LGdOBBql+45vfmCE1XRl/DS0LFdZxkZn4ub49C9Uf8g0DOVt02NJQNh3ZNaBa+l2nIirh643hLvzFCCqBM8=,iv:tqRFfzSk0W+NzEADzIWy08aUnpJgcIPBHhUhM0Kf+VA=,tag:0IiapCTs0UITa38cwVyZqQ==,type:str]
pgp:
- created_at: "2022-09-22T09:52:10Z"
enc: |
......
......@@ -113,8 +113,6 @@ blockscout:
_default: '4677000'
DISABLE_REALTIME_INDEXER:
_default: 'false'
INDEXER_OPTIMISM_L1_RPC:
_default: http://65.108.226.29:8545
INDEXER_OPTIMISM_L1_PORTAL_CONTRACT:
_default: 0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383
INDEXER_OPTIMISM_L1_WITHDRAWALS_START_BLOCK:
......@@ -279,7 +277,7 @@ frontend:
app: blockscout
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
_default: ghcr.io/blockscout/frontend:v1.0.8
ingress:
enabled: true
# annotations:
......@@ -393,6 +391,10 @@ frontend:
_default: ''
NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://goerli.optimism.io
NEXT_PUBLIC_WEB3_DEFAULT_WALLET:
_default: coinbase
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET:
_default: true
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs']"
NEXT_PUBLIC_IS_TESTNET:
......
blockscout:
environment:
ETHEREUM_JSONRPC_TRACE_URL:
#ENC[AES256_GCM,data:Lh7FyFwnnbk8Lpo3e5XunDK2iRyXsFtUOyF49/eoww==,iv:rTfCtp+xZkWtYewH77Xqx5lTAbfS2Xr/bIcMeETieKw=,tag:cviW/zIbNzpZ2ReFIxLxxw==,type:comment]
_default: ENC[AES256_GCM,data:hzvsDY9ff0zagkoZsuqdPa7ePPe0Td+wlg==,iv:dfiFF+vjPzPLJz7oWe0ahEjFlhc+/SC4vubE9DssRMk=,tag:5wW63FHQa6HCiA8jAxO71w==,type:str]
ETHEREUM_JSONRPC_HTTP_URL:
#ENC[AES256_GCM,data:Q5ZCHA/BJ+uTdiNyXF9m2Awa+wjkBWca01QusH2Tvw==,iv:JscgpCcF9IbOr2TOGE3D6GrQCmnKxyPoH2hdoC+NYR4=,tag:AFUqdNTheLggMRx6DzotMg==,type:comment]
_default: ENC[AES256_GCM,data:yGFoleqbrSeu8VOQsczabh8AZH0BZJiNvw==,iv:ViStcIKLzBVjUIMp53sg+aylQ/4Cyh8/E/UwPfpoZCo=,tag:MXKebydM5XOhy/INqYJhww==,type:str]
#ENC[AES256_GCM,data:xhwTQI1yVw7fJPeMPIwjwUkkwgb9LBQfJw==,iv:CAW4UNSA1SVhLsPkN2T+FVtGPD/5ayJz8afzePTcYr8=,tag:eWbOvyG6rb+RkHTxxSbzmA==,type:comment]
#ENC[AES256_GCM,data:EyiYQwB5sVt/gv+Wchv3Ty7Xzt44NuYtOfaPuxUOsNB8,iv:C5zDeLnhFLo/D+78k9GV9l/Ehr53Wpo2a+q/98rARoU=,tag:TTdqnUmhOLP4YCyVZ5TIVg==,type:comment]
#ENC[AES256_GCM,data:f0ESzURFheRh+DqaqiGNXwVyBnSTsTDAwhbZ7TFZnSzqnvMRzZthkZvDW6nSM5d23asAeYuXwRY=,iv:HYyVReU8eedUxnSQ3YPEBnohq6a5L+ef3HIm132aqjs=,tag:tpQ272l070s+KbTQ4v0xww==,type:comment]
ACCOUNT_USERNAME:
_default: ENC[AES256_GCM,data:xJ0hxcuU8u+QwiXciZ8qe1sV1GuxZV8Z9iYgnoWCu0ueuEmNE80jwe+vqviNb/UbOQs=,iv:uRNP5RTG/oxUngEoBbvLg9GzS5gYiHlL42yttgYWPAc=,tag:plQfCpG9pN7d28ooZ3aHrQ==,type:str]
ACCOUNT_PASSWORD:
......@@ -24,6 +33,8 @@ blockscout:
_default: ENC[AES256_GCM,data:Cqga1O4fbdtx5GfEQjJEPYWqeig7SBAdKiIif9sGCNrdy5FSHr43uskfdOdiS3uhaHm925jhPf+/nvs7VRzaqSAJCB2HBrVjJLOTQItVEw5rGus9Ma2plubDdrXh2CHO14mjE6f1/QOq2MLC1S79MqeHxpgqS0sgMflKopWa3/4=,iv:z1mBiInLa3kutmPFuX+R96rUSGcjFr/UH6cn/UbM0tg=,tag:8TRBcnTENdguj2BOs4Qrmw==,type:str]
DATABASE_URL:
_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:
_default: ENC[AES256_GCM,data:HF5y8ezV5TiLqeh98WDp4rXQeUfSBETyWVHOyNZNy5pt4MdiKTdFBLauOJpD6YHWynMFsd8IJLRNLrBn4qGe3RfwprR6v3WN9Q==,iv:B9AJXO7EJexsPgDHb5s5tzpadVYoZ79fyaL8NOYXSEw=,tag:lyNKQ13Q+pn+3DrAXIzIKQ==,type:str]
ACCOUNT_REDIS_URL:
......@@ -33,7 +44,7 @@ blockscout:
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:jr/MLQ1Rq1xjBVAb/fZFbvak23hEcmRkh5nnG4WNCcORR+xEWhEv7ncP87ClpsfxedDN5pOcFrdUkF7u80OkKA==,iv:TIxP5v9K7mcApYmSKRByKNPwYw0djRWHojYfHvSWqZM=,tag:LSLmwapH044CEzOvlce9Og==,type:str]
_default: ENC[AES256_GCM,data:iYnptgxESZs216/m0ArCciXfirPTxVjt5urKTATyYv3uadokC54ofzMZagG/ZVvNY48+Uxh4YGzwySyLOOM0SA==,iv:UQoZf3tesEjAJ7E5YruoZmVclsYfS5EmSo4z90wfHfE=,tag:er+hARrkCrjoQugqvRr4ow==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
......@@ -144,8 +155,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-04-18T09:26:42Z"
mac: ENC[AES256_GCM,data:4MdP3Fi3SDxyXjhyI7jogYdigziLkp7oh4M6BthtT2FuUu+XAGnONAdccxULJfXtKS6ue27BtV/pKWhdKeOqKIN23BJERqa4cFgYlyd85XAyL400/fEKEO4nCe2c9VIqNRUQls28/WkRjzRjvzQMMTIRIAJvfrJUYRpIabXWqAo=,iv:Z4shgbH8uJNuoJvmyyQjQ+fZsXuSC3nIGYb6GYO/nac=,tag:IDZvpGiZH41IwAgSidF3xQ==,type:str]
lastmodified: "2023-05-09T16:11:07Z"
mac: ENC[AES256_GCM,data:JHnHHGNOw84O5No7UDTLSGnLwUJHUUZaY/UBPetpa99OFDMlzFvEnjIh4DjIqhVLXR5DBZJr7BTibQLyo6y/aXlWKDxHvLklLGunKQaQ1Xl/JzDKRTYlAs/JsJRR0ElzzyPmbCKbSJOX0tAtNAuhH6EFKYg+HLYmPHtYcvM1Kck=,iv:OsKv7efW0xJ4ZT/38Uyq0feqVC4i2x4srzG+mqd2Avg=,tag:MWfjO8r3iJSMGluqIHc9jA==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -63,15 +63,6 @@ blockscout:
app: blockscout-prod
# Blockscout environment variables
environment:
ETHEREUM_JSONRPC_TRACE_URL:
# _default: http://geth-svc:8545
_default: http://65.108.226.29:8545
ETHEREUM_JSONRPC_HTTP_URL:
# _default: http://geth-svc:8545
_default: http://65.108.226.29:8545
# ETHEREUM_JSONRPC_WS_URL:
# # _default: ws://geth-svc:8546
# _default: ws://geth-svc.goerli.svc.cluster.local:8546
BLOCKSCOUT_VERSION:
_default: v5.1.2-beta
ECTO_USE_SSL:
......
......@@ -119,6 +119,10 @@ frontend:
_default: ''
NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://goerli.optimism.io
NEXT_PUBLIC_WEB3_DEFAULT_WALLET:
_default: coinbase
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET:
_default: true
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs']"
NEXT_PUBLIC_IS_TESTNET:
......
......@@ -42,14 +42,16 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_LOGOUT_URL | `string` | Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` |
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` | Account logout return url. Required if account is supported for the app instance. | - | - | `https://blockscout.com/poa/core/auth/logout` |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cup']` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage | `#FFFFFF` | `'#DCFE76'` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` | Gradient value for hero plate on the homepage | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes) | `\#FFFFFF \| rgb(220, 254, 118)` | `\#DCFE76` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` | Gradient value for hero plate on the homepage (escape "#" symbol if you use HEX color codes) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` |
| NEXT_PUBLIC_AD_DOMAIN_WITH_AD | `string` | The domain on which we display ads | - | - | `blockscout.com` |
| NEXT_PUBLIC_AD_ADBUTLER_ON | `boolean` | Set to true to show Adbutler banner instead of Coinzilla banner | - | `false` | `true` |
| NEXT_PUBLIC_API_SPEC_URL | `string` | Spec to be displayed on api-docs page | - | - | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` |
| NEXT_PUBLIC_GRAPHIQL_TRANSACTION | `string` | Txn hash for default query at GraphQl playground page | - | - | `0x69e3923eef50eada197c3336d546936d0c994211492c9f947a24c02827568f9f` |
| NEXT_PUBLIC_WEB3_DEFAULT_WALLET | `metamask` \| `coinbase`| Type of Web3 wallet which will be used by default to add tokens or chains to | - | `metamask` | `coinbase` |
| NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET | `boolean`| Set to `true` to hide icon "Add to your wallet" next to token addresses | - | `false` | `true` |
### Marketplace app configuration properties
......@@ -93,6 +95,7 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
| group | `Mainnets \| Testnets \| Other` | Indicates in which tab network appears in the menu | yes | - | `Mainnets` |
| icon | `string` | Network icon; if not provided, the common placeholder will be shown; *Note* that icon size should be at least 60px by 60px | - | - | `https://placekitten.com/60/60` |
| isActive | `boolean` | Pass `true` if item should be shonw as active in the menu | - | - | `true` |
| invertIconInDarkMode | `boolean` | Pass `true` if icon colors should be inverted in dark mode | - | - | `true` |
### Network explorer configuration properties
......@@ -119,8 +122,10 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
| Variable | Type| Description | Is required | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_API_PROTOCOL | `http \| https` | Main API protocol | - | `https` | `http` |
| NEXT_PUBLIC_API_HOST | `string` | Main API host | - | `blockscout.com` | `my-host.com` |
| NEXT_PUBLIC_API_BASE_PATH | `string` | Base path for Main API endpoint url | - | - | `/poa/core` |
| NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL | `ws \| wss` | Main API websocket protocol | - | `wss` | `ws` |
| NEXT_PUBLIC_STATS_API_HOST | `string` | Stats API endpoint url | - | - | `https://my-host.com` |
| NEXT_PUBLIC_VISUALIZE_API_HOST | `string` | Visualize API endpoint url | - | - | `https://my-host.com` |
| 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
| 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` |
## 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
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 = {
zone: string;
......@@ -7,8 +7,10 @@ type CPreferences = {
}
declare global {
interface Window {
ethereum: MetaMaskInpageProvider;
export interface Window {
ethereum?: {
providers?: Array<ExternalProvider>;
};
coinzilla_display: Array<CPreferences>;
}
}
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 20">
<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="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"/>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<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="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 fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<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"/>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<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 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 {
AddressTokenTransferFilters,
AddressTokensFilter,
AddressTokensResponse,
AddressWithdrawalsResponse,
} from 'types/api/address';
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 { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
......@@ -54,6 +55,7 @@ import type { TransactionsResponseValidated, TransactionsResponsePending, Transa
import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse } from 'types/api/withdrawals';
import type { ArrayElement } from 'types/utils';
import appConfig from 'configs/app/config';
......@@ -168,6 +170,12 @@ export const RESOURCES = {
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
block_withdrawals: {
path: '/api/v2/blocks/:height/withdrawals',
pathParams: [ 'height' as const ],
paginationFields: [ 'items_count' as const, 'index' as const ],
filterFields: [],
},
txs_validated: {
path: '/api/v2/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'filter' as const, 'index' as const ],
......@@ -208,6 +216,11 @@ export const RESOURCES = {
path: '/api/v2/transactions/:hash/state-changes',
pathParams: [ 'hash' as const ],
},
withdrawals: {
path: '/api/v2/withdrawals',
paginationFields: [ 'index' as const, 'items_count' as const ],
filterFields: [],
},
// ADDRESSES
addresses: {
......@@ -275,6 +288,12 @@ export const RESOURCES = {
paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' 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: {
......@@ -372,6 +391,12 @@ export const RESOURCES = {
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
filterFields: [],
},
token_instance_holders: {
path: '/api/v2/tokens/:hash/instances/:id/holders',
pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'items_count' as const, 'token_id' as const, 'value' as const ],
filterFields: [],
},
// HOMEPAGE
homepage_stats: {
......@@ -519,9 +544,10 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'search' |
'address_logs' | 'address_tokens' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
'token_instance_transfers' |
'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits';
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -550,6 +576,7 @@ Q extends 'stats_line' ? StatsChart :
Q extends 'blocks' ? BlocksResponse :
Q extends 'block' ? Block :
Q extends 'block_txs' ? BlockTransactionsResponse :
Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
Q extends 'txs_validated' ? TransactionsResponseValidated :
Q extends 'txs_pending' ? TransactionsResponsePending :
Q extends 'tx' ? Transaction :
......@@ -569,6 +596,7 @@ Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'address_withdrawals' ? AddressWithdrawalsResponse :
Q extends 'token' ? TokenInfo :
Q extends 'token_verified_info' ? TokenVerifiedInfo :
Q extends 'token_counters' ? TokenCounters :
......@@ -577,6 +605,7 @@ Q extends 'token_holders' ? TokenHolders :
Q extends 'token_instance' ? TokenInstance :
Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount :
Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_instance_holders' ? TokenHolders :
Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult :
......@@ -590,6 +619,7 @@ Q extends 'verified_contracts' ? VerifiedContractsResponse :
Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'l2_output_roots' ? L2OutputRootsResponse :
Q extends 'l2_withdrawals' ? L2WithdrawalsResponse :
Q extends 'l2_deposits' ? L2DepositsResponse :
......
......@@ -5,6 +5,7 @@ function generateCspPolicy() {
const policyDescriptor = mergeDescriptors(
descriptors.app(),
descriptors.ad(),
descriptors.cloudFlare(),
descriptors.googleAnalytics(),
descriptors.googleFonts(),
descriptors.googleReCaptcha(),
......
import type CspDev from 'csp-dev';
import { KEY_WORDS } from '../utils';
// CloudFlare analytics
export function cloudFlare(): CspDev.DirectiveDescriptor {
return {
'script-src': [
'static.cloudflareinsights.com',
],
'style-src': [
KEY_WORDS.DATA,
],
};
}
export { ad } from './ad';
export { app } from './app';
export { cloudFlare } from './cloudFlare';
export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha';
......
......@@ -125,7 +125,14 @@ export default function useNavItems(): ReturnType {
blocks,
topAccounts,
verifiedContracts,
];
appConfig.beaconChain.hasBeaconChain && {
text: 'Withdrawals',
nextRoute: { pathname: '/withdrawals' as const },
icon: withdrawalsIcon,
isActive: pathname === '/withdrawals',
isNewUi: true,
},
].filter(Boolean);
}
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) {
const res = NextResponse.next();
res.headers.append('Content-Security-Policy', cspPolicy);
res.headers.append('Server-Timing', `middleware;dur=${ end - start }`);
// eslint-disable-next-line no-restricted-properties
res.headers.append('Docker-ID', process.env.HOSTNAME || '');
return res;
}
......
......@@ -179,3 +179,8 @@ export const withRichMetadata: TokenInstance = {
status: null,
},
};
export const unique: TokenInstance = {
...base,
is_unique: true,
};
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 dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from 'lib/next/token/types';
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 { title, description } = getSeo({ hash });
......@@ -16,7 +19,9 @@ const TokenPage: NextPage<PageParams> = ({ hash }: PageParams) => {
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Token/>
<Page>
<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) => {
return {
opacity: 1,
borderRadius: 'base',
borderRadius: 'md',
borderColor: start,
background: `linear-gradient(90deg, ${ start } 8%, ${ end } 18%, ${ start } 33%)`,
backgroundSize: '200% 100%',
......
......@@ -12,6 +12,7 @@ export interface Address {
creator_address_hash: string | null;
creation_tx_hash: string | null;
exchange_rate: string | null;
has_beacon_chain_withdrawals?: boolean;
has_custom_methods_read: boolean;
has_custom_methods_write: boolean;
has_decompiled_code: boolean;
......@@ -128,3 +129,19 @@ export interface AddressInternalTxsResponse {
transaction_index: number;
} | 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 {
tx_count: number;
miner: AddressParam;
size: number;
has_beacon_chain_withdrawals?: boolean;
hash: string;
parent_hash: string;
difficulty: string;
......@@ -56,3 +57,18 @@ export interface NewBlockSocketResponse {
export interface BlockFilters {
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 {
export interface TokenHolders {
items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination;
next_page_params: TokenHoldersPagination | null;
}
export type TokenHolder = {
......@@ -52,7 +52,7 @@ export interface TokenInstanceTransfersCount {
export interface TokenInventoryResponse {
items: Array<TokenInstance>;
next_page_params: TokenInventoryPagination;
next_page_params: TokenInventoryPagination | null;
}
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" {
| DynamicRoute<"/tx/[hash]", { "hash": string }>
| StaticRoute<"/txs">
| StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml">;
| StaticRoute<"/visualize/sol2uml">
| StaticRoute<"/withdrawals">;
interface StaticRoute<Pathname> {
pathname: Pathname;
......
import type { MetaMaskInpageProvider } from '@metamask/providers';
import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -73,7 +72,9 @@ test('token', async({ mount, page }) => {
}), { times: 1 });
await page.evaluate(() => {
window.ethereum = { } as MetaMaskInpageProvider;
window.ethereum = {
providers: [ { isMetaMask: true } ],
};
});
const component = await mount(
......
......@@ -8,11 +8,13 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { tokenTabsByType } from 'ui/pages/Address';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import ERC1155Tokens from './tokens/ERC1155Tokens';
import ERC20Tokens from './tokens/ERC20Tokens';
import ERC721Tokens from './tokens/ERC721Tokens';
import TokenBalances from './tokens/TokenBalances';
import TokensWithIds from './tokens/TokensWithIds';
import TokensWithoutIds from './tokens/TokensWithoutIds';
const TAB_LIST_PROPS = {
marginBottom: 0,
......@@ -32,21 +34,59 @@ const AddressTokens = () => {
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenType: TokenType = (Object.keys(tokenTabsByType) as Array<TokenType>).find(key => tokenTabsByType[key] === router.query.tab) || 'ERC-20';
const tab = router.query.tab?.toString();
const tokenType: TokenType = (Object.keys(tokenTabsByType) as Array<TokenType>).find(key => tokenTabsByType[key] === tab) || 'ERC-20';
const tokensQuery = useQueryWithPages({
const erc20Query = useQueryWithPages({
resourceName: 'address_tokens',
pathParams: { hash: router.query.hash?.toString() },
filters: { type: tokenType },
filters: { type: 'ERC-20' },
scrollRef,
options: {
enabled: tokenType === 'ERC-20',
},
});
const erc721Query = useQueryWithPages({
resourceName: 'address_tokens',
pathParams: { hash: router.query.hash?.toString() },
filters: { type: 'ERC-721' },
scrollRef,
options: {
enabled: tokenType === 'ERC-721',
},
});
const erc1155Query = useQueryWithPages({
resourceName: 'address_tokens',
pathParams: { hash: router.query.hash?.toString() },
filters: { type: 'ERC-1155' },
scrollRef,
options: {
enabled: tokenType === 'ERC-1155',
},
});
const tabs = [
{ id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <TokensWithoutIds tokensQuery={ tokensQuery }/> },
{ id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: <TokensWithoutIds tokensQuery={ tokensQuery }/> },
{ id: tokenTabsByType['ERC-1155'], title: 'ERC-1155', component: <TokensWithIds tokensQuery={ tokensQuery }/> },
{ id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <ERC20Tokens tokensQuery={ erc20Query }/> },
{ id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: <ERC721Tokens tokensQuery={ erc721Query }/> },
{ id: tokenTabsByType['ERC-1155'], title: 'ERC-1155', component: <ERC1155Tokens tokensQuery={ erc1155Query }/> },
];
let isPaginationVisible;
let pagination: PaginationProps | undefined;
if (tab === tokenTabsByType['ERC-1155']) {
isPaginationVisible = erc1155Query.isPaginationVisible;
pagination = erc1155Query.pagination;
} else if (tab === tokenTabsByType['ERC-721']) {
isPaginationVisible = erc721Query.isPaginationVisible;
pagination = erc721Query.pagination;
} else {
isPaginationVisible = erc20Query.isPaginationVisible;
pagination = erc20Query.pagination;
}
return (
<>
<TokenBalances/>
......@@ -58,7 +98,7 @@ const AddressTokens = () => {
colorScheme="gray"
size="sm"
tabListProps={ isMobile ? TAB_LIST_PROPS_MOBILE : TAB_LIST_PROPS }
rightSlot={ tokensQuery.isPaginationVisible && !isMobile ? <Pagination { ...tokensQuery.pagination }/> : null }
rightSlot={ isPaginationVisible && !isMobile ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile }
/>
</>
......
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) => {
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal>
</Td>
<Td>
{ props.transaction_hash ?
{ props.transaction_hash &&
(
<Address w="150px" fontWeight="700">
<AddressLink hash={ props.transaction_hash } type="transaction"/>
</Address>
) :
<Text fontWeight="700">-</Text>
)
}
</Td>
<Td>
......
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 React from 'react';
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 useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import * as stubs from 'stubs/contract';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -24,10 +27,10 @@ type Props = {
noSocket?: boolean;
}
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }>
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text>
<Text>{ value }</Text>
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 } alignItems="baseline">
<Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ value }</Skeleton>
</GridItem>
));
......@@ -35,11 +38,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
const [ isSocketOpen, setIsSocketOpen ] = React.useState(false);
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 },
queryOptions: {
enabled: Boolean(addressHash) && (noSocket || isSocketOpen),
refetchOnMount: false,
placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
},
});
......@@ -62,24 +69,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
return <DataFetchAlert/>;
}
if (isLoading) {
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 = (
const verificationButton = isPlaceholderData ? <Skeleton w="130px" h={ 8 } mr={ 3 } ml="auto" borderRadius="base"/> : (
<Button
size="sm"
ml="auto"
......@@ -92,8 +82,8 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
);
const constructorArgs = (() => {
if (!data.decoded_constructor_args) {
return data.constructor_args;
if (!data?.decoded_constructor_args) {
return data?.constructor_args;
}
const decoded = data.decoded_constructor_args
......@@ -119,7 +109,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
})();
const externalLibraries = (() => {
if (!data.external_libraries || data.external_libraries.length === 0) {
if (!data?.external_libraries || data?.external_libraries.length === 0) {
return null;
}
......@@ -134,19 +124,23 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
return (
<>
<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_via_sourcify && (
{ data?.is_verified && (
<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">
<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> }
</Alert>
) }
{ (data.is_changed_bytecode || isChangedBytecodeSocket) && (
{ (data?.is_changed_bytecode || isChangedBytecodeSocket) && (
<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.
</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">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<Address>
......@@ -160,7 +154,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
<span> page</span>
</Alert>
) }
{ data.minimal_proxy_address_hash && (
{ data?.minimal_proxy_address_hash && (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
<span>Minimal Proxy Contract for </span>
<Address>
......@@ -175,14 +169,16 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
</Alert>
) }
</Flex>
{ data.is_verified && (
{ data?.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name }/> }
{ data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version }/> }
{ data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize"/> }
{ typeof data.optimization_enabled === 'boolean' && <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' }/> }
{ data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) }/> }
{ data.verified_at && <InfoItem label="Verified at" value={ dayjs(data.verified_at).format('LLLL') } wordBreak="break-word"/> }
{ data.name && <InfoItem label="Contract name" value={ data.name } isLoading={ isPlaceholderData }/> }
{ 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" isLoading={ isPlaceholderData }/> }
{ typeof data.optimization_enabled === 'boolean' &&
<InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ 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>
) }
<Flex flexDir="column" rowGap={ 6 }>
......@@ -191,9 +187,10 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
data={ constructorArgs }
title="Constructor Arguments"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.source_code && (
{ data?.source_code && (
<ContractSourceCode
data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
......@@ -202,23 +199,26 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
filePath={ data.file_path }
additionalSource={ data.additional_sources }
remappings={ data.compiler_settings?.remappings }
isLoading={ isPlaceholderData }
/>
) }
{ Boolean(data.compiler_settings) && (
{ data?.compiler_settings ? (
<RawDataSnippet
data={ JSON.stringify(data.compiler_settings) }
title="Compiler Settings"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.abi && (
) : null }
{ data?.abi && (
<RawDataSnippet
data={ JSON.stringify(data.abi) }
title="Contract ABI"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.creation_bytecode && (
{ data?.creation_bytecode && (
<RawDataSnippet
data={ data.creation_bytecode }
title="Contract creation code"
......@@ -230,13 +230,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
</Alert>
) : null }
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.deployed_bytecode && (
{ data?.deployed_bytecode && (
<RawDataSnippet
data={ data.deployed_bytecode }
title="Deployed ByteCode"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ externalLibraries && (
......@@ -244,6 +246,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
data={ externalLibraries }
title="External Libraries"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
</Flex>
......
import { Flex, Text, Tooltip } from '@chakra-ui/react';
import { Flex, Skeleton, Text, Tooltip } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -17,14 +17,15 @@ interface Props {
filePath?: string;
additionalSource?: SmartContract['additional_sources'];
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 = (
<Text fontWeight={ 500 }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
<span>Contract source code</span>
<Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text>
</Text>
</Skeleton>
);
const diagramLink = hasSol2Yml && address ? (
......@@ -32,9 +33,10 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
<LinkInternal
href={ route({ pathname: '/visualize/sol2uml', query: { address } }) }
ml="auto"
mr={ 3 }
>
View UML diagram
<Skeleton isLoaded={ !isLoading }>
View UML diagram
</Skeleton>
</LinkInternal>
</Tooltip>
) : null;
......@@ -47,7 +49,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
}, [ additionalSource, data, filePath, isViper ]);
const copyToClipboard = editorData.length === 1 ?
<CopyToClipboard text={ editorData[0].source_code }/> :
<CopyToClipboard text={ editorData[0].source_code } isLoading={ isLoading } ml={ 3 }/> :
null;
return (
......@@ -57,7 +59,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
{ diagramLink }
{ copyToClipboard }
</Flex>
<CodeEditor data={ editorData } remappings={ remappings }/>
{ isLoading ? <Skeleton h="557px" w="100%"/> : <CodeEditor data={ editorData } remappings={ remappings }/> }
</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 QRCode from 'qrcode';
import React from 'react';
......@@ -13,9 +27,10 @@ const SVG_OPTIONS = {
interface Props {
className?: string;
hash: string;
isLoading?: boolean;
}
const AddressQrCode = ({ hash, className }: Props) => {
const AddressQrCode = ({ hash, className, isLoading }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const isMobile = useIsMobile();
const [ qr, setQr ] = React.useState('');
......@@ -36,6 +51,10 @@ const AddressQrCode = ({ hash, className }: Props) => {
}
}, [ hash, isOpen, onClose ]);
if (isLoading) {
return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
}
return (
<>
<Tooltip label="Click to view QR code">
......
......@@ -11,6 +11,7 @@ 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 CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
......@@ -56,15 +57,19 @@ const TxInternalsListItem = ({
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut }/>
{ isIn && <CopyToClipboard text={ from.hash }/> }
</Address>
{ (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
}
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/>
</Address>
{ toData && (
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/>
{ isOut && <CopyToClipboard text={ toData.hash }/> }
</Address>
) }
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
......
......@@ -11,6 +11,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
import TxStatus from 'ui/shared/TxStatus';
......@@ -64,6 +65,7 @@ const AddressIntTxsTableItem = ({
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ isOut }/>
{ isIn && <CopyToClipboard text={ from.hash }/> }
</Address>
</Td>
<Td px={ 0 } verticalAlign="middle">
......@@ -73,10 +75,13 @@ const AddressIntTxsTableItem = ({
}
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/>
</Address>
{ toData && (
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/>
{ isOut && <CopyToClipboard text={ toData.hash }/> }
</Address>
) }
</Td>
<Td isNumeric verticalAlign="middle">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
......
......@@ -19,7 +19,7 @@ type Props = {
};
}
const TokensWithIds = ({ tokensQuery }: Props) => {
const ERC1155Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
......@@ -69,4 +69,4 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
);
};
export default TokensWithIds;
export default ERC1155Tokens;
......@@ -10,8 +10,8 @@ import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import TokensListItem from './TokensListItem';
import TokensTable from './TokensTable';
import ERC20TokensListItem from './ERC20TokensListItem';
import ERC20TokensTable from './ERC20TokensTable';
type Props = {
tokensQuery: UseQueryResult<AddressTokensResponse> & {
......@@ -20,7 +20,7 @@ type Props = {
};
}
const TokensWithoutIds = ({ tokensQuery }: Props) => {
const ERC20Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
......@@ -33,8 +33,8 @@ const TokensWithoutIds = ({ tokensQuery }: Props) => {
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }><TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <TokensListItem key={ item.token.address } { ...item }/>) }</Show></>
<Hide below="lg" ssr={ false }><ERC20TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <ERC20TokensListItem key={ item.token.address } { ...item }/>) }</Show></>
) : null;
return (
......@@ -54,4 +54,4 @@ const TokensWithoutIds = ({ tokensQuery }: Props) => {
};
export default TokensWithoutIds;
export default ERC20Tokens;
......@@ -4,16 +4,15 @@ import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
import AddressAddToMetaMask from '../details/AddressAddToMetaMask';
type Props = AddressTokenBalance;
const TokensListItem = ({ token, value }: Props) => {
const ERC20TokensListItem = ({ token, value }: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
......@@ -31,7 +30,7 @@ const TokensListItem = ({ token, value }: Props) => {
<Flex alignItems="center" pl={ 8 }>
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
<AddressAddToMetaMask token={ token } ml={ 2 }/>
<AddressAddToWallet token={ token } ml={ 2 }/>
</Flex>
{ token.exchange_rate !== undefined && token.exchange_rate !== null && (
<HStack spacing={ 3 }>
......@@ -53,4 +52,4 @@ const TokensListItem = ({ token, value }: Props) => {
);
};
export default TokensListItem;
export default ERC20TokensListItem;
......@@ -5,14 +5,14 @@ import type { AddressTokenBalance } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokensTableItem from './TokensTableItem';
import ERC20TokensTableItem from './ERC20TokensTableItem';
interface Props {
data: Array<AddressTokenBalance>;
top: number;
}
const TokensTable = ({ data, top }: Props) => {
const ERC20TokensTable = ({ data, top }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
......@@ -26,11 +26,11 @@ const TokensTable = ({ data, top }: Props) => {
</Thead>
<Tbody>
{ data.map((item) => (
<TokensTableItem key={ item.token.address } { ...item }/>
<ERC20TokensTableItem key={ item.token.address } { ...item }/>
)) }
</Tbody>
</Table>
);
};
export default TokensTable;
export default ERC20TokensTable;
......@@ -4,15 +4,14 @@ import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo';
import AddressAddToMetaMask from '../details/AddressAddToMetaMask';
type Props = AddressTokenBalance;
const TokensTableItem = ({
const ERC20TokensTableItem = ({
token,
value,
}: Props) => {
......@@ -38,20 +37,20 @@ const TokensTableItem = ({
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
</Flex>
<AddressAddToMetaMask token={ token } ml={ 4 }/>
<AddressAddToWallet token={ token } ml={ 4 }/>
</Flex>
</Td>
<Td isNumeric verticalAlign="middle">
{ token.exchange_rate ? `$${ token.exchange_rate }` : '-' }
{ token.exchange_rate && `$${ token.exchange_rate }` }
</Td>
<Td isNumeric verticalAlign="middle">
{ tokenQuantity }
</Td>
<Td isNumeric verticalAlign="middle">
{ tokenValue ? `$${ tokenValue }` : '-' }
{ tokenValue && `$${ tokenValue }` }
</Td>
</Tr>
);
};
export default React.memo(TokensTableItem);
export default React.memo(ERC20TokensTableItem);
import { Show, Hide } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { AddressTokensResponse } from 'types/api/address';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import ERC721TokensListItem from './ERC721TokensListItem';
import ERC721TokensTable from './ERC721TokensTable';
type Props = {
tokensQuery: UseQueryResult<AddressTokensResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const ERC721Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
const actionBar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }><ERC721TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <ERC721TokensListItem key={ item.token.address } { ...item }/>) }</Show></>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '40%', '40%', '20%' ],
}}
emptyText="There are no tokens of selected type."
content={ content }
actionBar={ actionBar }
/>
);
};
export default ERC721Tokens;
import { Flex, HStack, Text } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance;
const ERC721TokensListItem = ({ token, value }: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
return (
<ListItemMobile rowGap={ 2 }>
<Flex alignItems="center" width="100%">
<TokenLogo hash={ token.address } name={ token.name } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/>
</Flex>
<Flex alignItems="center" pl={ 8 }>
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
<AddressAddToWallet token={ token } ml={ 2 }/>
</Flex>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Quantity</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text>
</HStack>
</ListItemMobile>
);
};
export default ERC721TokensListItem;
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky';
import ERC721TokensTableItem from './ERC721TokensTableItem';
interface Props {
data: Array<AddressTokenBalance>;
top: number;
}
const ERC721TokensTable = ({ data, top }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="40%">Asset</Th>
<Th width="40%">Contract address</Th>
<Th width="20%" isNumeric>Quantity</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<ERC721TokensTableItem key={ item.token.address } { ...item }/>
)) }
</Tbody>
</Table>
);
};
export default ERC721TokensTable;
import { Tr, Td, Flex } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance;
const ERC721TokensTableItem = ({
token,
value,
}: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
return (
<Tr>
<Td verticalAlign="middle">
<Flex alignItems="center">
<TokenLogo hash={ token.address } name={ token.name } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/>
</Flex>
</Td>
<Td verticalAlign="middle">
<Flex alignItems="center" width="150px" justifyContent="space-between">
<Flex alignItems="center">
<AddressLink hash={ token.address } type="address" truncation="dynamic"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
</Flex>
<AddressAddToWallet token={ token } ml={ 4 }/>
</Flex>
</Td>
<Td isNumeric verticalAlign="middle">
{ value }
</Td>
</Tr>
);
};
export default React.memo(ERC721TokensTableItem);
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) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
......
......@@ -18,7 +18,7 @@ import LinkInternal from 'ui/shared/LinkInternal';
const WithdrawalsTableItem = ({ item }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : 'N/A';
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : '-';
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : '';
return (
<Tr>
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { TokenType } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
......@@ -19,6 +20,7 @@ import AddressLogs from 'ui/address/AddressLogs';
import AddressTokens from 'ui/address/AddressTokens';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs';
import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -62,6 +64,9 @@ const AddressPageContent = () => {
const tabs: Array<RoutedTab> = React.useMemo(() => {
return [
{ 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 ?
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } :
undefined,
......
......@@ -4,16 +4,20 @@ import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import BlockDetails from 'ui/block/BlockDetails';
import BlockWithdrawals from 'ui/block/BlockWithdrawals';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import TxsContent from 'ui/txs/TxsContent';
const TAB_LIST_PROPS = {
......@@ -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) {
throw new Error('Block not found', { cause: { status: 404 } });
}
......@@ -53,9 +65,22 @@ const BlockPageContent = () => {
const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> },
{ 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 hasGoBackLink = appProps.referrer && appProps.referrer.includes('/blocks');
......@@ -81,12 +106,14 @@ const BlockPageContent = () => {
backLink={ backLink }
/>
) }
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination ? <Pagination { ...blockTxsQuery.pagination }/> : null }
stickyEnabled={ hasPagination }
/>
{ blockQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
stickyEnabled={ hasPagination }
/>
) }
</>
);
};
......
......@@ -49,7 +49,7 @@ const Home = () => {
</Box>
<Stats/>
<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 }>
<LatestBlocks/>
<Box flexGrow={ 1 }>
......
......@@ -68,7 +68,7 @@ test('base view', async({ mount, page, createSocket }) => {
await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot();
await expect(component).toHaveScreenshot();
});
test('with verified info', async({ mount, page, createSocket }) => {
......@@ -109,7 +109,7 @@ test.describe('mobile', () => {
await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot();
await expect(component).toHaveScreenshot();
});
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 { useRouter } from 'next/router';
import React, { useEffect } from 'react';
......@@ -18,9 +18,11 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
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 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 type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
......@@ -52,7 +54,18 @@ const TokenPageContent = () => {
const tokenQuery = useApiQuery('token', {
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(() => {
......@@ -89,7 +102,7 @@ const TokenPageContent = () => {
});
useEffect(() => {
if (tokenQuery.data) {
if (tokenQuery.data && !tokenQuery.isPlaceholderData) {
const tokenSymbol = tokenQuery.data.symbol ? ` (${ tokenQuery.data.symbol })` : '';
const tokenName = `${ tokenQuery.data.name || 'Unnamed' }${ tokenSymbol }`;
const title = document.getElementsByTagName('title')[0];
......@@ -101,14 +114,17 @@ const TokenPageContent = () => {
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({
resourceName: 'token_transfers',
pathParams: { hash: hashString },
scrollRef,
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 = () => {
pathParams: { hash: hashString },
scrollRef,
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 = () => {
pathParams: { hash: hashString },
scrollRef,
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 verifiedInfoQuery = useApiQuery('token_verified_info', {
pathParams: { hash: hashString, chainId: appConfig.network.id },
......@@ -144,15 +157,15 @@ const TokenPageContent = () => {
const contractTabs = useContractTabs(contractQuery.data);
const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery }/> },
(tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ?
{ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } :
undefined,
contractQuery.data?.is_contract ? {
id: 'contract',
title: () => {
if (contractQuery.data.is_verified) {
if (contractQuery.data?.is_verified) {
return (
<>
<span>Contract</span>
......@@ -169,7 +182,7 @@ const TokenPageContent = () => {
].filter(Boolean);
let hasPagination;
let pagination;
let pagination: PaginationProps | undefined;
if (!router.query.tab || router.query.tab === 'token_transfers') {
hasPagination = transfersQuery.isPaginationVisible;
......@@ -225,50 +238,41 @@ const TokenPageContent = () => {
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
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 }/>
<PageTitle
text={ `${ tokenQuery.data?.name || 'Unnamed' }${ tokenSymbolText } token` }
backLink={ backLink }
additionalsLeft={ (
<TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 }/>
) }
additionalsRight={ tagsNode }
afterTitle={
verifiedInfoQuery.data ?
<Icon as={ iconSuccess } color="green.500" boxSize={ 4 } verticalAlign="top"/> :
<Box boxSize={ 4 } display="inline-block"/>
}
/>
</>
) }
<TokenContractInfo tokenQuery={ tokenQuery }/>
<>
<TextAd mb={ 6 }/>
<PageTitle
isLoading={ tokenQuery.isPlaceholderData }
text={ `${ tokenQuery.data?.name || 'Unnamed' }${ tokenSymbolText } token` }
backLink={ backLink }
additionalsLeft={ (
<TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 } isLoading={ tokenQuery.isPlaceholderData }/>
) }
additionalsRight={ tagsNode }
afterTitle={
verifiedInfoQuery.data ?
<Icon as={ iconSuccess } color="green.500" boxSize={ 4 } verticalAlign="top"/> :
<Box boxSize={ 4 } display="inline-block"/>
}
/>
<TokenContractInfo tokenQuery={ tokenQuery } contractQuery={ contractQuery }/>
<TokenVerifiedInfo hash={ hashString } verifiedInfoQuery={ verifiedInfoQuery } isVerifiedInfoEnabled={ isVerifiedInfoEnabled }/>
<TokenDetails tokenQuery={ tokenQuery }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
{ tokenQuery.isLoading || contractQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ tabListProps }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
stickyEnabled={ !isMobile }
/>
) }
{ tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData ?
<SkeletonTabs tabs={ tabs }/> :
(
<RoutedTabs
tabs={ tabs }
tabListProps={ tabListProps }
rightSlot={ !isMobile && hasPagination && pagination ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile }
/>
) }
{ !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 type { AddressTag } from 'types/api/account';
......@@ -11,9 +11,10 @@ interface Props {
item: AddressTag;
onEditClick: (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(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
......@@ -25,15 +26,17 @@ const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return (
<ListItemMobile>
<Flex alignItems="flex-start" flexDirection="column" maxW="100%">
<AddressSnippet address={ item.address }/>
<AddressSnippet address={ item.address } isLoading={ isLoading }/>
<HStack spacing={ 3 } mt={ 4 }>
<Text fontSize="sm" fontWeight={ 500 }>Private tag</Text>
<Tag>
{ item.name }
</Tag>
<Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="sm">
<Tag>
{ item.name }
</Tag>
</Skeleton>
</HStack>
</Flex>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</ListItemMobile>
);
};
......
......@@ -12,28 +12,30 @@ import type { AddressTags, AddressTag } from 'types/api/account';
import AddressTagTableItem from './AddressTagTableItem';
interface Props {
data: AddressTags;
data?: AddressTags;
onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void;
isLoading: boolean;
}
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="60%">Address</Th>
<Th width="40%">Private tag</Th>
<Th width="108px"></Th>
<Th width="116px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item: AddressTag) => (
{ data?.map((item: AddressTag, index: number) => (
<AddressTagTableItem
item={ item }
key={ item.id }
key={ item.id + (isLoading ? index : '') }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
isLoading={ isLoading }
/>
)) }
</Tbody>
......
import {
Tag,
Tr,
Td,
} from '@chakra-ui/react';
......@@ -8,16 +7,17 @@ import React, { useCallback } from 'react';
import type { AddressTag } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet';
import Tag from 'ui/shared/chakra/Tag';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props {
item: AddressTag;
onEditClick: (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(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
......@@ -29,17 +29,13 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return (
<Tr alignItems="top" key={ item.id }>
<Td>
<AddressSnippet address={ item.address }/>
<AddressSnippet address={ item.address } isLoading={ isLoading }/>
</Td>
<Td whiteSpace="nowrap">
<TruncatedTextTooltip label={ item.name }>
<Tag>
{ item.name }
</Tag>
</TruncatedTextTooltip>
<Tag isLoading={ isLoading } isTruncated>{ item.name }</Tag>
</Td>
<Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Td>
</Tr>
);
......
......@@ -5,10 +5,9 @@ import type { AddressTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
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 AddressTagListItem from './AddressTagTable/AddressTagListItem';
......@@ -16,7 +15,12 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal';
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 deleteModalProps = useDisclosure();
......@@ -49,46 +53,25 @@ const PrivateAddressTags = () => {
deleteModalProps.onClose();
}, [ 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) {
return <DataFetchAlert/>;
}
const list = isMobile ? (
<Box>
{ addressTagsData.map((item: AddressTag) => (
{ addressTagsData?.map((item: AddressTag, index: number) => (
<AddressTagListItem
item={ item }
key={ item.id }
key={ item.id + (isPlaceholderData ? index : '') }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
isLoading={ isPlaceholderData }
/>
)) }
</Box>
) : (
<AddressTagTable
isLoading={ isPlaceholderData }
data={ addressTagsData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
......@@ -97,15 +80,20 @@ const PrivateAddressTags = () => {
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 }
<Box marginTop={ 8 }>
<Button
size="lg"
onClick={ addressModalProps.onOpen }
>
Add address tag
</Button>
<Skeleton isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
onClick={ addressModalProps.onOpen }
>
Add address tag
</Button>
</Skeleton>
</Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData } onSuccess={ onAddOrEditSuccess }/>
{ deleteModalData && (
......
......@@ -6,9 +6,9 @@ import type { TokenInfo } from 'types/api/token';
import appConfig from 'configs/app/config';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import AddressActionsMenu from 'ui/shared/AddressActions/Menu';
......@@ -18,14 +18,15 @@ interface Props {
address: Pick<Address, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names' | 'watchlist_address_id'>;
token?: TokenInfo | null;
isLinkDisabled?: boolean;
isLoading?: boolean;
}
const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => {
const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props) => {
const isMobile = useIsMobile();
return (
<Flex alignItems="center">
<AddressIcon address={ address }/>
<AddressIcon address={ address } isLoading={ isLoading }/>
<AddressLink
type="address"
hash={ address.hash }
......@@ -34,13 +35,14 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => {
fontWeight={ 500 }
truncation={ isMobile ? 'constant' : 'none' }
isDisabled={ isLinkDisabled }
isLoading={ isLoading }
/>
<CopyToClipboard text={ address.hash }/>
{ address.is_contract && token && <AddressAddToMetaMask ml={ 2 } token={ token }/> }
{ !address.is_contract && appConfig.isAccountSupported && (
<CopyToClipboard text={ address.hash } isLoading={ isLoading }/>
{ !isLoading && address.is_contract && token && <AddressAddToWallet ml={ 2 } token={ token }/> }
{ !isLoading && !address.is_contract && appConfig.isAccountSupported && (
<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/> }
</Flex>
);
......
......@@ -11,15 +11,16 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name'>;
subtitle?: string;
isLoading?: boolean;
}
const AddressSnippet = ({ address, subtitle }: Props) => {
const AddressSnippet = ({ address, subtitle, isLoading }: Props) => {
return (
<Box maxW="100%">
<Address>
<AddressIcon address={ address }/>
<AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 }/>
<CopyToClipboard text={ address.hash } ml={ 3 }/>
<AddressIcon address={ address } isLoading={ isLoading }/>
<AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 } isLoading={ isLoading }/>
<CopyToClipboard text={ address.hash } isLoading={ isLoading }/>
</Address>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> }
</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 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 [ copied, setCopied ] = useState(false);
// 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}
}
}, [ hasCopied ]);
if (isLoading) {
return <Skeleton boxSize={ 5 } className={ className } borderRadius="sm" flexShrink={ 0 }/>;
}
return (
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen || copied }>
<IconButton
......@@ -24,6 +34,7 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
icon={ <CopyIcon/> }
w="20px"
h="20px"
color="gray.500"
variant="simple"
display="inline-block"
flexShrink={ 0 }
......@@ -31,6 +42,7 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
className={ className }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
ml={ 1 }
/>
</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 React from 'react';
......@@ -9,18 +9,21 @@ interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
hint: string;
children: React.ReactNode;
note?: string;
isLoading?: boolean;
}
const DetailsInfoItem = ({ title, hint, note, children, id, ...styles }: Props) => {
const DetailsInfoItem = ({ title, hint, note, children, id, isLoading, ...styles }: Props) => {
return (
<>
<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">
<Hint label={ hint }/>
<Text fontWeight={{ base: 700, lg: 500 }}>
{ title }
{ note && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">{ note }</Text> }
</Text>
<Hint label={ hint } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading }>
<Text fontWeight={{ base: 700, lg: 500 }}>
{ title }
{ note && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">{ note }</Text> }
</Text>
</Skeleton>
</Flex>
</GridItem>
<GridItem
......
......@@ -7,18 +7,22 @@ import isSelfHosted from 'lib/isSelfHosted';
import AdBanner from 'ui/shared/ad/AdBanner';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
const DetailsSponsoredItem = () => {
interface Props {
isLoading?: boolean;
}
const DetailsSponsoredItem = ({ isLoading }: Props) => {
const isMobile = useIsMobile();
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED);
if (hasAdblockCookie || !isSelfHosted()) {
if (!isSelfHosted() || hasAdblockCookie) {
return null;
}
if (isMobile) {
return (
<GridItem mt={ 5 }>
<AdBanner justifyContent="center"/>
<AdBanner mx="auto" isLoading={ isLoading } display="flex" justifyContent="center"/>
</GridItem>
);
}
......@@ -27,8 +31,9 @@ const DetailsSponsoredItem = () => {
<DetailsInfoItem
title="Sponsored"
hint="Sponsored banner advertisement"
isLoading={ isLoading }
>
<AdBanner/>
<AdBanner isLoading={ isLoading }/>
</DetailsInfoItem>
);
};
......
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 InfoIcon from 'icons/info.svg';
......@@ -8,9 +8,10 @@ interface Props {
label: string | React.ReactNode;
className?: string;
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
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
......@@ -19,6 +20,10 @@ const Hint = ({ label, className, tooltipProps }: Props) => {
onToggle();
}, [ onToggle ]);
if (isLoading) {
return <Skeleton boxSize={ 5 } borderRadius="sm"/>;
}
return (
<Tooltip
label={ label }
......
......@@ -26,6 +26,8 @@ const ListItemMobile = ({ children, className, isAnimated }: Props) => {
borderBottomWidth: '1px',
}}
className={ className }
fontSize="16px"
lineHeight="20px"
>
{ children }
</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 eastArrowIcon from 'icons/arrows/east.svg';
......@@ -15,19 +15,22 @@ type Props = {
className?: string;
backLink?: BackLinkProp;
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 = (
<Heading
as="h1"
size="lg"
flex="none"
wordBreak="break-word"
>
{ text }
{ afterTitle }
</Heading>
<Skeleton isLoaded={ !isLoading }>
<Heading
as="h1"
size="lg"
flex="none"
wordBreak="break-word"
>
{ text }
{ afterTitle }
</Heading>
</Skeleton>
);
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 CopyToClipboard from './CopyToClipboard';
......@@ -11,33 +11,36 @@ interface Props {
beforeSlot?: React.ReactNode;
textareaMaxHeight?: string;
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
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
return (
<Box className={ className } as="section" title={ title }>
{ (title || rightSlot || showCopy) && (
<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 }
{ typeof data === 'string' && showCopy && <CopyToClipboard text={ data }/> }
{ typeof data === 'string' && showCopy && <CopyToClipboard text={ data } isLoading={ isLoading }/> }
</Flex>
) }
{ beforeSlot }
<Box
<Skeleton
p={ 4 }
bgColor={ bgColor }
bgColor={ isLoading ? 'inherit' : bgColor }
maxH={ textareaMaxHeight || '400px' }
minH={ isLoading ? '200px' : undefined }
fontSize="sm"
borderRadius="md"
wordBreak="break-all"
whiteSpace="pre-wrap"
overflowY="auto"
isLoaded={ !isLoading }
>
{ data }
</Box>
</Skeleton>
</Box>
);
};
......
......@@ -175,6 +175,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
}
onItemClick={ handleTabChange }
buttonRef={ tabsRefs[index] }
size={ themeProps.size || 'md' }
/>
);
}
......
import type {
ButtonProps } from '@chakra-ui/react';
import { Popover,
PopoverTrigger,
PopoverContent,
......@@ -20,9 +22,10 @@ interface Props {
styles?: StyleProps;
onItemClick: (index: number) => void;
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 handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
......@@ -40,6 +43,7 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe
variant="ghost"
isActive={ isOpen || isActive }
ref={ buttonRef }
size={ size }
{ ...styles }
>
{ 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 React from 'react';
......@@ -13,9 +13,10 @@ interface Props {
url: string;
alert?: string;
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 alertContent = (() => {
......@@ -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
className={ className }
status="warning"
......@@ -57,14 +61,39 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr
py="6px"
fontWeight={ 400 }
fontSize="sm"
bgColor={ useColorModeValue('orange.50', transparentize('orange.200', 0.16)(theme)) }
color={ useColorModeValue('blackAlpha.800', 'whiteAlpha.800') }
bgColor={ bgColor }
color={ color }
>
{ alertContent }
</Alert>
);
) : <Skeleton className={ className } h="33px"/>;
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 DeleteIcon from 'icons/delete.svg';
......@@ -8,33 +8,47 @@ import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModa
type Props = {
onEditClick: () => void;
onDeleteClick: () => void;
isLoading?: boolean;
}
const TableItemActionButtons = ({ onEditClick, onDeleteClick }: Props) => {
const TableItemActionButtons = ({ onEditClick, onDeleteClick, isLoading }: Props) => {
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 (
<HStack spacing={ 6 } alignSelf="flex-end">
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
w="30px"
h="30px"
boxSize={ 5 }
onClick={ onEditClick }
icon={ <Icon as={ EditIcon } w="20px" h="20px"/> }
icon={ <EditIcon/> }
onFocusCapture={ onFocusCapture }
display="inline-block"
flexShrink={ 0 }
borderRadius="none"
/>
</Tooltip>
<Tooltip label="Delete">
<IconButton
aria-label="delete"
variant="simple"
w="30px"
h="30px"
boxSize={ 5 }
onClick={ onDeleteClick }
icon={ <Icon as={ DeleteIcon } w="20px" h="20px"/> }
icon={ <DeleteIcon/> }
onFocusCapture={ onFocusCapture }
display="inline-block"
flexShrink={ 0 }
borderRadius="none"
/>
</Tooltip>
</HStack>
......
import { Image, chakra } from '@chakra-ui/react';
import { Image, chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
......@@ -8,9 +8,15 @@ interface Props {
hash?: string;
name?: string | null;
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 ? [
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/',
appConfig.network.assetsPathname,
......
......@@ -17,6 +17,8 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import CopyToClipboard from '../CopyToClipboard';
type Props = TokenTransfer & {
baseAddress?: string;
showTxInfo?: boolean;
......@@ -84,6 +86,7 @@ const TokenTransferListItem = ({
<Address width={ addressWidth }>
<AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash }/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash }/> }
</Address>
{ baseAddress ?
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> :
......@@ -92,6 +95,7 @@ const TokenTransferListItem = ({
<Address width={ addressWidth }>
<AddressIcon address={ to }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash }/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash }/> }
</Address>
</Flex>
{ value && (
......
import { Box, Icon, chakra } from '@chakra-ui/react';
import { Box, Icon, chakra, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -13,10 +13,11 @@ interface Props {
className?: string;
isDisabled?: boolean;
truncation?: 'dynamic' | 'constant';
isLoading?: boolean;
}
const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynamic' }: Props) => {
const Component = isDisabled ? Box : LinkInternal;
const TokenTransferNft = ({ hash, id, className, isDisabled, isLoading, truncation = 'dynamic' }: Props) => {
const Component = isDisabled || isLoading ? Box : LinkInternal;
return (
<Component
......@@ -28,10 +29,12 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynam
w="100%"
className={ className }
>
<Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/>
<Box maxW="calc(100% - 34px)">
<Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 1 } borderRadius="base">
<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 }/> }
</Box>
</Skeleton>
</Component>
);
};
......
......@@ -14,6 +14,8 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import CopyToClipboard from '../CopyToClipboard';
type Props = TokenTransfer & {
baseAddress?: string;
showTxInfo?: boolean;
......@@ -32,14 +34,6 @@ const TokenTransferTableItem = ({
timestamp,
enableTimeIncrement,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
return '-';
}
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
return (
......@@ -57,7 +51,7 @@ const TokenTransferTableItem = ({
</Flex>
</Td>
<Td lineHeight="30px">
{ 'token_id' in total ? <TokenTransferNft hash={ token.address } id={ total.token_id }/> : '-' }
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
</Td>
{ showTxInfo && txHash && (
<Td>
......@@ -71,6 +65,7 @@ const TokenTransferTableItem = ({
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ from }/>
<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>
</Td>
{ baseAddress && (
......@@ -82,10 +77,11 @@ const TokenTransferTableItem = ({
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ to }/>
<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>
</Td>
<Td isNumeric verticalAlign="top" lineHeight="30px">
{ value }
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Td>
</Tr>
);
......
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 React from 'react';
......@@ -6,21 +6,28 @@ interface Props {
className?: string;
value: number;
colorScheme?: 'green' | 'gray';
isLoading?: boolean;
}
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 colorGrayScheme = useColorModeValue('gray.500', 'gray.400');
const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500';
return (
<Flex className={ className } alignItems="center">
<Box bg={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden">
<Box bg={ color } w={ valueString } h="100%"/>
</Box>
<Text color={ color } ml="10px" fontWeight="bold">{ valueString }</Text>
<Flex className={ className } alignItems="center" columnGap="10px">
<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>
</Skeleton>
<Skeleton isLoaded={ !isLoading } color={ color } fontWeight="bold">
<span>
{ valueString }
</span>
</Skeleton>
</Flex>
);
};
......
import { chakra } from '@chakra-ui/react';
import { chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
......@@ -9,18 +9,26 @@ import isSelfHosted from 'lib/isSelfHosted';
import AdbutlerBanner from './AdbutlerBanner';
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);
if (!isSelfHosted() || hasAdblockCookie) {
return null;
}
if (appConfig.ad.adButlerOn) {
return <AdbutlerBanner className={ className }/>;
}
const content = appConfig.ad.adButlerOn ? <AdbutlerBanner/> : <CoinzillaBanner/>;
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);
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 { useAppContext } from 'lib/appContext';
......@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => {
}
if (isLoading) {
return <Box className={ className } h={{ base: 12, lg: 6 }}/>;
return <Skeleton className={ className } h={{ base: 12, lg: 6 }} maxW="1000px"/>;
}
if (!adData) {
......
......@@ -3,20 +3,23 @@ import React from 'react';
import type { TokenInfo } from 'types/api/token';
import metamaskIcon from 'icons/metamask.svg';
import appConfig from 'configs/app/config';
import useToast from 'lib/hooks/useToast';
import useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets';
interface Props {
className?: string;
token: TokenInfo;
}
const AddressAddToMetaMask = ({ className, token }: Props) => {
const AddressAddToWallet = ({ className, token }: Props) => {
const toast = useToast();
const provider = useProvider();
const handleClick = React.useCallback(async() => {
try {
const wasAdded = await window.ethereum.request?.({
const wasAdded = await provider?.request?.({
method: 'wallet_watchAsset',
params: {
type: 'ERC20', // Initially only supports ERC20, but eventually more!
......@@ -24,6 +27,8 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
address: token.address,
symbol: token.symbol,
decimals: Number(token.decimals) || 18,
// TODO: add token image when we have it in API
// image: ''
},
},
});
......@@ -32,7 +37,7 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
toast({
position: 'top-right',
title: 'Success',
description: 'Successfully added token to MetaMask',
description: 'Successfully added token to your wallet',
status: 'success',
variant: 'subtle',
isClosable: true,
......@@ -48,19 +53,21 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
isClosable: true,
});
}
}, [ toast, token ]);
}, [ toast, token, provider ]);
if (!('ethereum' in window)) {
if (!provider) {
return null;
}
const defaultWallet = appConfig.web3.defaultWallet;
return (
<Tooltip label="Add token to MetaMask">
<Tooltip label={ WALLETS_INFO[defaultWallet].add_token_text }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Icon as={ metamaskIcon } boxSize={ 6 }/>
<Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 6 }/>
</Box>
</Tooltip>
);
};
export default React.memo(chakra(AddressAddToMetaMask));
export default React.memo(chakra(AddressAddToWallet));
import { Box, chakra, Tooltip } from '@chakra-ui/react';
import { Box, chakra, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
......@@ -9,9 +9,14 @@ import AddressContractIcon from 'ui/shared/address/AddressContractIcon';
type Props = {
address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name'>;
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) {
return (
<AddressContractIcon className={ className }/>
......@@ -20,7 +25,7 @@ const AddressIcon = ({ address, className }: Props) => {
return (
<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) }/>
</Box>
</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 type { HTMLAttributeAnchorTarget } from 'react';
import React from 'react';
......@@ -17,6 +17,7 @@ type CommonProps = {
isDisabled?: boolean;
fontWeight?: string;
alias?: string | null;
isLoading?: boolean;
}
type AddressTokenTxProps = {
......@@ -39,7 +40,7 @@ type AddressTokenProps = {
type Props = CommonProps & (AddressTokenTxProps | BlockProps | AddressTokenProps);
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();
let url;
......@@ -81,6 +82,10 @@ const AddressLink = (props: Props) => {
}
})();
if (isLoading) {
return <Skeleton className={ className } overflow="hidden" whiteSpace="nowrap">{ content }</Skeleton>;
}
if (isDisabled) {
return (
<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 {
imageUrl: string | null;
animationUrl: string | null;
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);
React.useEffect(() => {
if (!animationUrl) {
if (!animationUrl || isLoading) {
return;
}
......@@ -45,9 +46,9 @@ const NftMedia = ({ imageUrl, animationUrl, className }: Props) => {
setType('image');
});
}, [ animationUrl ]);
}, [ animationUrl, isLoading ]);
if (!type) {
if (!type || isLoading) {
return (
<AspectRatio
className={ className }
......
import { Flex, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import type { RoutedTab } from '../RoutedTabs/types';
interface Props {
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 (
<Flex my={ 8 } className={ className }>
<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 appConfig from 'configs/app/config';
......@@ -7,6 +7,7 @@ import statsIcon from 'icons/social/stats.svg';
import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
const SOCIAL_LINKS = [
{ link: appConfig.footerLinks.github, icon: ghIcon, label: 'Github link' },
......@@ -35,11 +36,9 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
return (
<VStack
as="footer"
spacing={ 8 }
borderTop="1px solid"
borderColor="divider"
width={{ base: '100%', lg: isExpanded ? '180px' : '20px', xl: isCollapsed ? '20px' : '180px' }}
paddingTop={{ base: 6, lg: 8 }}
marginTop={ marginTop }
alignItems="flex-start"
alignSelf="center"
......@@ -47,23 +46,28 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
fontSize="xs"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
>
{ SOCIAL_LINKS.length > 0 && (
<Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank">
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
}) }
</Stack>
) }
<Stack
direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}
mt={{ base: 6, lg: 8 }}
_empty={{
display: 'none',
}}
>
<NetworkAddToWallet/>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank">
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
}) }
</Stack>
<Box display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}>
<Text variant="secondary" mb={ 8 }>
<Text variant="secondary" mt={ 8 }>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
{ appConfig.blockScoutVersion &&
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text> }
<Text variant="secondary" mt={ 8 }>Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text> }
</Box>
</VStack>
);
......
......@@ -26,7 +26,13 @@ const test = base.extend({
]) as any,
});
test('no auth +@desktop-xl +@dark-mode-xl', async({ mount }) => {
test('no auth +@desktop-xl +@dark-mode-xl', async({ page, mount }) => {
await page.evaluate(() => {
window.ethereum = {
providers: [ { isMetaMask: true } ],
};
});
const component = await mount(
<TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch">
......
import { Flex, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { Address } from 'types/api/address';
import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
interface Props {
tokenQuery: UseQueryResult<TokenInfo>;
contractQuery: UseQueryResult<Address>;
}
const TokenContractInfo = ({ tokenQuery }: 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>
);
}
const TokenContractInfo = ({ tokenQuery, contractQuery }: Props) => {
// we show error in parent component, this is only for TS
if (tokenQuery.isError) {
return null;
}
const address = {
hash: tokenQuery.data.address,
hash: tokenQuery.data?.address || '',
is_contract: true,
implementation_name: null,
watchlist_names: [],
......@@ -49,6 +29,7 @@ const TokenContractInfo = ({ tokenQuery }: Props) => {
<AddressHeadingInfo
address={ address }
token={ contractQuery.data?.token }
isLoading={ tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData }
/>
);
};
......
......@@ -8,11 +8,11 @@ import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue';
import { TOKEN_COUNTERS } from 'stubs/token';
import type { TokenTabs } from 'ui/pages/Token';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
interface Props {
tokenQuery: UseQueryResult<TokenInfo>;
......@@ -23,7 +23,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
const tokenCountersQuery = useApiQuery('token_counters', {
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) => () => {
......@@ -56,32 +56,19 @@ const TokenDetails = ({ tokenQuery }: Props) => {
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 {
exchange_rate: exchangeRate,
total_supply: totalSupply,
decimals,
symbol,
type,
} = tokenQuery.data;
} = tokenQuery.data || {};
let marketcap;
let totalSupplyValue;
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;
totalSupplyValue = totalValue?.valueStr;
} else {
......@@ -119,40 +106,50 @@ const TokenDetails = ({ tokenQuery }: Props) => {
alignSelf="center"
wordBreak="break-word"
whiteSpace="pre-wrap"
isLoading={ tokenQuery.isPlaceholderData }
>
<Flex w="100%">
<Box whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ totalSupplyValue || '0' }/>
</Box>
<Box flexShrink={ 0 }> { symbol || '' }</Box>
</Flex>
<Skeleton isLoaded={ !tokenQuery.isPlaceholderData }>
<Flex w="100%">
<Box whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ totalSupplyValue || '0' }/>
</Box>
<Box flexShrink={ 0 }> { symbol || '' }</Box>
</Flex>
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem
title="Holders"
hint="Number of accounts holding the token"
alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
>
{ tokenCountersQuery.isLoading && <Skeleton w={ 20 } h={ 4 }/> }
{ !tokenCountersQuery.isLoading && countersItem('token_holders_count') }
<Skeleton isLoaded={ !tokenCountersQuery.isPlaceholderData }>
{ countersItem('token_holders_count') }
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem
title="Transfers"
hint="Number of transfer for the token"
alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
>
{ tokenCountersQuery.isLoading && <Skeleton w={ 20 } h={ 4 }/> }
{ !tokenCountersQuery.isLoading && countersItem('transfers_count') }
<Skeleton isLoaded={ !tokenCountersQuery.isPlaceholderData }>
{ countersItem('transfers_count') }
</Skeleton>
</DetailsInfoItem>
{ decimals && (
<DetailsInfoItem
title="Decimals"
hint="Number of digits that come after the decimal place when displaying token value"
alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
>
{ decimals }
<Skeleton isLoaded={ !tokenQuery.isPlaceholderData } minW={ 6 }>
{ decimals }
</Skeleton>
</DetailsInfoItem>
) }
<DetailsSponsoredItem/>
<DetailsSponsoredItem isLoading={ tokenQuery.isPlaceholderData }/>
</Grid>
);
};
......
import { Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -14,17 +15,17 @@ import TokenHoldersList from './TokenHoldersList';
import TokenHoldersTable from './TokenHoldersTable';
type Props = {
tokenQuery: UseQueryResult<TokenInfo>;
token?: TokenInfo;
holdersQuery: UseQueryResult<TokenHolders> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
const TokenHoldersContent = ({ holdersQuery, token }: Props) => {
const isMobile = useIsMobile();
if (holdersQuery.isError || tokenQuery.isError) {
if (holdersQuery.isError) {
return <DataFetchAlert/>;
}
......@@ -36,17 +37,30 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
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 }/> }
{ isMobile && <TokenHoldersList data={ items } token={ tokenQuery.data }/> }
<Box display={{ base: 'none', lg: 'block' }}>
<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;
return (
<DataListDisplay
isError={ holdersQuery.isError || tokenQuery.isError }
isLoading={ holdersQuery.isLoading || tokenQuery.isLoading }
isError={ holdersQuery.isError }
isLoading={ false }
items={ holdersQuery.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }}
emptyText="There are no holders for this token."
......
......@@ -8,16 +8,18 @@ import TokenHoldersListItem from './TokenHoldersListItem';
interface Props {
data: Array<TokenHolder>;
token: TokenInfo;
isLoading?: boolean;
}
const TokenHoldersList = ({ data, token }: Props) => {
const TokenHoldersList = ({ data, token, isLoading }: Props) => {
return (
<Box>
{ data.map((item) => (
{ data.map((item, index) => (
<TokenHoldersListItem
key={ item.address.hash }
key={ item.address.hash + (isLoading ? index : '') }
token={ token }
holder={ item }
isLoading={ isLoading }
/>
)) }
</Box>
......
import { Flex } from '@chakra-ui/react';
import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -7,30 +7,44 @@ import type { TokenHolder, TokenInfo } from 'types/api/token';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
holder: TokenHolder;
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();
return (
<ListItemMobile rowGap={ 3 }>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/>
<AddressLink type="address" ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ holder.address } isLoading={ isLoading }/>
<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>
<Flex justifyContent="space-between" alignItems="center" width="100%">
{ quantity }
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ quantity }
</Skeleton>
{ token.total_supply && (
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green"
ml={ 6 }
isLoading={ isLoading }
/>
) }
</Flex>
......
......@@ -10,9 +10,10 @@ interface Props {
data: Array<TokenHolder>;
token: TokenInfo;
top: number;
isLoading?: boolean;
}
const TokenHoldersTable = ({ data, token, top }: Props) => {
const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
......@@ -23,8 +24,8 @@ const TokenHoldersTable = ({ data, token, top }: Props) => {
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TokenHoldersTableItem key={ item.address.hash } holder={ item } token={ token }/>
{ data.map((item, index) => (
<TokenHoldersTableItem key={ item.address.hash + (isLoading ? index : '') } holder={ item } token={ token } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
......
import { Tr, Td } from '@chakra-ui/react';
import { Tr, Td, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -7,33 +7,47 @@ import type { TokenHolder, TokenInfo } from 'types/api/token';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import Utilization from 'ui/shared/Utilization/Utilization';
type Props = {
holder: TokenHolder;
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();
return (
<Tr>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/>
<AddressLink type="address" ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ holder.address } isLoading={ isLoading }/>
<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>
</Td>
<Td isNumeric>
{ quantity }
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ quantity }
</Skeleton>
</Td>
{ token.total_supply && (
<Td isNumeric>
<Td verticalAlign="middle" isNumeric>
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green"
display="inline-flex"
isLoading={ isLoading }
/>
</Td>
) }
......
import { Grid, Skeleton } from '@chakra-ui/react';
import { Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -28,21 +28,6 @@ const TokenInventory = ({ inventoryQuery }: Props) => {
</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 content = items ? (
......@@ -52,19 +37,25 @@ const TokenInventory = ({ inventoryQuery }: Props) => {
rowGap={{ base: 3, lg: 6 }}
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>
) : null;
return (
<DataListDisplay
isError={ inventoryQuery.isError }
isLoading={ inventoryQuery.isLoading }
isLoading={ false }
items={ items }
emptyText="There are no tokens."
content={ content }
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 React from 'react';
......@@ -11,9 +11,9 @@ import LinkInternal from 'ui/shared/LinkInternal';
import NftMedia from 'ui/shared/nft/NftMedia';
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 (
<LinkBox
w={{ base: '100%', lg: '210px' }}
......@@ -32,6 +32,7 @@ const NFTItem = ({ item }: Props) => {
mb="18px"
imageUrl={ item.image_url }
animationUrl={ item.animation_url }
isLoading={ isLoading }
/>
</LinkOverlay>
</NextLink>
......@@ -39,13 +40,16 @@ const NFTItem = ({ item }: Props) => {
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text>
<TruncatedTextTooltip label={ item.id }>
<LinkInternal
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ item.id }
</LinkInternal>
<Skeleton isLoaded={ !isLoading } overflow="hidden">
<LinkInternal
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
display="block"
>
{ item.id }
</LinkInternal>
</Skeleton>
</TruncatedTextTooltip>
</Flex>
) }
......@@ -53,8 +57,8 @@ const NFTItem = ({ item }: Props) => {
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary" mr={ 2 } lineHeight="24px">Owner</Text>
<Address>
<Hide below="lg" ssr={ false }><AddressIcon address={ item.owner } mr={ 1 }/></Hide>
<AddressLink hash={ item.owner.hash } alias={ item.owner.name } type="address" truncation="constant"/>
<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" isLoading={ isLoading }/>
</Address>
</Flex>
) }
......
import { Hide, Show } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/token';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
......@@ -14,7 +15,7 @@ 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 SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable';
......@@ -24,12 +25,13 @@ type Props = {
isPaginationVisible: boolean;
};
tokenId?: string;
token?: TokenInfo;
}
const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
const isMobile = useIsMobile();
const router = useRouter();
const { isError, isLoading, data, pagination, isPaginationVisible } = transfersQuery;
const { isError, isLoading, isPlaceholderData, data, pagination, isPaginationVisible } = transfersQuery;
const [ newItemsCount, setNewItemsCount ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
......@@ -61,7 +63,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<Box display={{ base: 'none', lg: 'block' }}>
<TokenTransferTable
data={ data?.items }
top={ isPaginationVisible ? 80 : 0 }
......@@ -69,20 +71,22 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
tokenId={ tokenId }
token={ token }
isLoading={ isPlaceholderData }
/>
</Hide>
<Show below="lg" ssr={ false }>
</Box>
<Box display={{ base: 'block', lg: 'none' }}>
{ pagination.page === 1 && (
<SocketNewItemsNotice
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="token_transfer"
borderBottomRadius={ 0 }
isLoading={ isPlaceholderData }
/>
) }
<TokenTransferList data={ data?.items } tokenId={ tokenId }/>
</Show>
<TokenTransferList data={ data?.items } tokenId={ tokenId } isLoading={ isPlaceholderData }/>
</Box>
</>
) : null;
......@@ -95,7 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ !isPlaceholderData && isLoading }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
......
......@@ -8,9 +8,10 @@ import TokenTransferListItem from 'ui/token/TokenTransfer/TokenTransferListItem'
interface Props {
data: Array<TokenTransfer>;
tokenId?: string;
isLoading?: boolean;
}
const TokenTransferList = ({ data, tokenId }: Props) => {
const TokenTransferList = ({ data, tokenId, isLoading }: Props) => {
return (
<Box>
{ data.map((item, index) => (
......@@ -18,6 +19,7 @@ const TokenTransferList = ({ data, tokenId }: Props) => {
key={ index }
{ ...item }
tokenId={ tokenId }
isLoading={ isLoading }
/>
)) }
</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 React from 'react';
......@@ -11,10 +11,12 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
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 TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & {tokenId?: string};
type Props = TokenTransfer & { tokenId?: string; isLoading?: boolean };
const TokenTransferListItem = ({
token,
......@@ -25,6 +27,7 @@ const TokenTransferListItem = ({
method,
timestamp,
tokenId,
isLoading,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
......@@ -42,44 +45,68 @@ const TokenTransferListItem = ({
<ListItemMobile rowGap={ 3 } isAnimated>
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Flex>
<Icon
as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color={ iconColor }
/>
<Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 2 }>
<Icon
as={ transactionIcon }
boxSize="30px"
color={ iconColor }
/>
</Skeleton>
<Address width="100%">
<AddressLink
hash={ txHash }
type="transaction"
fontWeight="700"
truncation="constant"
isLoading={ isLoading }
/>
</Address>
</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>
{ method && <Tag colorScheme="gray">{ method }</Tag> }
{ method && <Tag isLoading={ isLoading }>{ method }</Tag> }
<Flex w="100%" columnGap={ 3 }>
<Address width="50%">
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address }/>
<AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/>
<CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<Skeleton isLoaded={ !isLoading } boxSize={ 6 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Skeleton>
<Address width="50%">
<AddressIcon address={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address }/>
<AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/>
<CopyToClipboard text={ to.hash } isLoading={ isLoading }/>
</Address>
</Flex>
{ value && (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text>
<Text variant="secondary">{ value }</Text>
<Text>{ trimTokenSymbol(token.symbol) }</Text>
<Skeleton isLoaded={ !isLoading } flexShrink={ 0 } fontWeight={ 500 }>
Value
</Skeleton>
<Skeleton isLoaded={ !isLoading } variant="secondary">
{ value }
</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ trimTokenSymbol(token.symbol) }</Skeleton>
</Flex>
) }
{ '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) }/> }
{ '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) }
isLoading={ isLoading }
/>
) }
</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 type { TokenInfo } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer';
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 TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableItem';
......@@ -15,11 +16,12 @@ interface Props {
socketInfoAlert?: string;
socketInfoNum?: number;
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 tokenSymbol = data[0].token.symbol;
return (
<Table variant="simple" size="sm" minW="950px">
......@@ -27,30 +29,26 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket
<Tr>
<Th width={ tokenType === 'ERC-1155' ? '60%' : '80%' }>Txn hash</Th>
<Th width="164px">Method</Th>
<Th width="148px">From</Th>
<Th width="160px">From</Th>
<Th width="36px" px={ 0 }/>
<Th width="218px" >To</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>
</Thead>
<Tbody>
{ showSocketInfo && (
<Tr>
<Td colSpan={ 10 } p={ 0 }>
<SocketNewItemsNotice
borderRadius={ 0 }
pl="10px"
url={ window.location.href }
alert={ socketInfoAlert }
num={ socketInfoNum }
type="token_transfer"
/>
</Td>
</Tr>
<SocketNewItemsNotice.Desktop
url={ window.location.href }
alert={ socketInfoAlert }
num={ socketInfoNum }
type="token_transfer"
isLoading={ isLoading }
/>
) }
{ data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item } tokenId={ tokenId }/>
<TokenTransferTableItem key={ index } { ...item } tokenId={ tokenId } isLoading={ isLoading }/>
)) }
</Tbody>
</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 React from 'react';
......@@ -9,9 +9,11 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
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';
type Props = TokenTransfer & { tokenId?: string }
type Props = TokenTransfer & { tokenId?: string; isLoading?: boolean }
const TokenTransferTableItem = ({
token,
......@@ -22,33 +24,36 @@ const TokenTransferTableItem = ({
method,
timestamp,
tokenId,
isLoading,
}: 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);
return (
<Tr alignItems="top">
<Td>
<Grid alignItems="center" gridTemplateColumns="auto 130px" width="fit-content">
<Address display="inline-flex" fontWeight={ 600 } lineHeight="30px">
<AddressLink type="transaction" hash={ txHash }/>
<Grid alignItems="center" gridTemplateColumns="auto 130px" width="fit-content" py="7px">
<Address display="inline-flex" fontWeight={ 600 }>
<AddressLink type="transaction" hash={ txHash } isLoading={ isLoading }/>
</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>
</Td>
<Td>
{ method ? <Tag colorScheme="gray">{ method }</Tag> : '-' }
{ method ? (
<Box my="3px">
<Tag isLoading={ isLoading } isTruncated>{ method }</Tag>
</Box>
) : null }
</Td>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ from }/>
<Address display="inline-flex" maxW="100%" py="3px">
<AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink
ml={ 2 }
flexGrow={ 1 }
......@@ -58,15 +63,19 @@ const TokenTransferTableItem = ({
alias={ from.name }
tokenHash={ token.address }
truncation="constant"
isLoading={ isLoading }
/>
<CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address>
</Td>
<Td px={ 0 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<Skeleton isLoaded={ !isLoading } boxSize={ 6 } my="3px">
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Skeleton>
</Td>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ to }/>
<Address display="inline-flex" maxW="100%" py="3px">
<AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink
ml={ 2 }
flexGrow={ 1 }
......@@ -76,25 +85,30 @@ const TokenTransferTableItem = ({
alias={ to.name }
tokenHash={ token.address }
truncation="constant"
isLoading={ isLoading }
/>
<CopyToClipboard text={ to.hash } isLoading={ isLoading }/>
</Address>
</Td>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
<Td lineHeight="30px">
<Td>
{ 'token_id' in total ? (
<TokenTransferNft
hash={ token.address }
id={ total.token_id }
justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' }
isDisabled={ Boolean(tokenId && tokenId === total.token_id) }
isLoading={ isLoading }
/>
) : '-'
) : ''
}
</Td>
) }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Td isNumeric verticalAlign="top" lineHeight="30px">
{ value || '-' }
<Td isNumeric verticalAlign="top">
<Skeleton isLoaded={ !isLoading } my="7px">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Skeleton>
</Td>
) }
</Tr>
......
......@@ -14,7 +14,10 @@ import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
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 TokenInstanceDetails from './TokenInstanceDetails';
......@@ -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 hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance');
......@@ -62,11 +76,16 @@ const TokenInstanceContent = () => {
}, [ appProps.referrer, hash ]);
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: 'holders', title: 'Holders', component: <span>Holders</span> },
{
id: 'token_transfers',
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 }/> },
];
].filter(Boolean);
if (tokenInstanceQuery.isError) {
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
......@@ -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 (
<>
<TextAd mb={ 6 }/>
......@@ -130,12 +160,14 @@ const TokenInstanceContent = () => {
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && transfersQuery.isPaginationVisible && tab !== 'metadata' ? <Pagination { ...transfersQuery.pagination }/> : null }
stickyEnabled={ !isMobile }
/>
{ tokenInstanceQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && isPaginationVisible && pagination ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile }
/>
) }
{ !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</>
......
......@@ -11,8 +11,8 @@ import TokenInstanceDetails from './TokenInstanceDetails';
const API_URL_ADDRESS = buildApiUrl('address', { hash: tokenInstanceMock.base.token.address });
const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', {
id: tokenInstanceMock.base.id,
hash: tokenInstanceMock.base.token.address,
id: tokenInstanceMock.unique.id,
hash: tokenInstanceMock.unique.token.address,
});
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(
<TestApp>
<TokenInstanceDetails data={ tokenInstanceMock.base }/>
<TokenInstanceDetails data={ tokenInstanceMock.unique }/>
</TestApp>,
);
......
......@@ -60,7 +60,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
>
<TokenSnippet hash={ data.token.address } name={ data.token.name }/>
</DetailsInfoItem>
{ data.owner && (
{ data.is_unique && data.owner && (
<DetailsInfoItem
title="Owner"
hint="Current owner of this token instance"
......
......@@ -42,7 +42,7 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
href={ url }
onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined }
>
{ transfersCountQuery.data.transfers_count }
{ transfersCountQuery.data.transfers_count.toLocaleString() }
</LinkInternal>
</DetailsInfoItem>
);
......
......@@ -4,7 +4,7 @@ import React from 'react';
import type { TokenInfo } from 'types/api/token';
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 CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
......@@ -60,13 +60,13 @@ const TokensTableItem = ({
<AddressLink fontSize="sm" hash={ address } type="address" truncation="constant"/>
<CopyToClipboard text={ address } ml={ 1 }/>
</Flex>
<AddressAddToMetaMask token={ token }/>
<AddressAddToWallet token={ token }/>
</Flex>
</Flex>
{ exchangeRate && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Price</Text>
<Text fontSize="sm" variant="secondary">{ exchangeRate || '-' }</Text>
<Text fontSize="sm" variant="secondary">{ exchangeRate }</Text>
</HStack>
) }
{ totalValue?.usd && (
......
......@@ -4,7 +4,7 @@ import React from 'react';
import type { TokenInfo } from 'types/api/token';
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 CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo';
......@@ -61,15 +61,15 @@ const TokensTableItem = ({
<AddressLink fontSize="sm" hash={ address } type="address" truncation="constant" fontWeight={ 500 }/>
<CopyToClipboard text={ address } ml={ 1 }/>
</Flex>
<AddressAddToMetaMask token={ token }/>
<AddressAddToWallet token={ token }/>
</Flex>
<Tag flexShrink={ 0 } ml={ 8 } mt={ 3 }>{ type }</Tag>
</Box>
</Flex>
</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">
<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 isNumeric><Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ Number(holders).toLocaleString() }</Text></Td>
</Tr>
......
......@@ -9,6 +9,7 @@ import eastArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
......@@ -29,12 +30,16 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit:
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash }/>
<CopyToClipboard text={ from.hash }/>
</Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash }/>
</Address>
{ toData && (
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash }/>
<CopyToClipboard text={ toData.hash }/>
</Address>
) }
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
......
......@@ -9,6 +9,7 @@ import rightArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
......@@ -34,16 +35,20 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit:
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
<CopyToClipboard text={ from.hash }/>
</Address>
</Td>
<Td px={ 0 } verticalAlign="middle">
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/>
</Address>
{ toData && (
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/>
<CopyToClipboard text={ toData.hash }/>
</Address>
) }
</Td>
<Td isNumeric verticalAlign="middle">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
......
......@@ -18,6 +18,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
......@@ -102,6 +103,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
ml={ 2 }
isDisabled={ isOut }
/>
{ !isOut && <CopyToClipboard text={ tx.from.hash }/> }
</Address>
{ (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut } width="48px" mx={ 2 }/> : (
......@@ -123,6 +125,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
ml={ 2 }
isDisabled={ isIn }
/>
{ !isIn && <CopyToClipboard text={ dataTo.hash }/> }
</Address>
) : '-' }
</Flex>
......
......@@ -46,9 +46,9 @@ const TxsTable = ({
<Th width="160px">Type</Th>
<Th width="20%">Method</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: '132px', base: '66px' }}>To</Th>
<Th width={{ xl: '152px', base: '86px' }}>To</Th>
<Th width="20%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
......
......@@ -20,6 +20,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import CurrencyValue from 'ui/shared/CurrencyValue';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -47,6 +48,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
<Address w="100%">
<AddressIcon address={ tx.from }/>
<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>
);
......@@ -54,6 +56,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
<Address w="100%">
<AddressIcon address={ dataTo }/>
<AddressLink type="address" hash={ dataTo.hash } alias={ dataTo.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isIn }/>
{ !isIn && <CopyToClipboard text={ dataTo.hash }/> }
</Address>
) : '-';
......@@ -88,13 +91,13 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
</VStack>
</Td>
<Td whiteSpace="nowrap">
{ tx.method ? (
{ tx.method && (
<TruncatedTextTooltip label={ tx.method }>
<Tag colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }>
{ tx.method }
</Tag>
</TruncatedTextTooltip>
) : '-' }
) }
</Td>
{ showBlockInfo && (
<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