Commit 2cc75359 authored by Max Alekseenko's avatar Max Alekseenko

Merge branch 'main' into action-button

parents 6354b5db fdc8b808
...@@ -10,6 +10,7 @@ on: ...@@ -10,6 +10,7 @@ on:
type: choice type: choice
options: options:
- none - none
- base
- gnosis - gnosis
- eth - eth
- eth_sepolia - eth_sepolia
......
...@@ -339,6 +339,7 @@ ...@@ -339,6 +339,7 @@
"main", "main",
"main.L2", "main.L2",
"localhost", "localhost",
"base",
"gnosis", "gnosis",
"eth", "eth",
"eth_goerli", "eth_goerli",
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const apiHost = getEnvValue('NEXT_PUBLIC_METADATA_SERVICE_API_HOST');
const title = 'Address metadata';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiHost) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
export { default as account } from './account'; export { default as account } from './account';
export { default as addressVerification } from './addressVerification'; export { default as addressVerification } from './addressVerification';
export { default as addressMetadata } from './addressMetadata';
export { default as adsBanner } from './adsBanner'; export { default as adsBanner } from './adsBanner';
export { default as adsText } from './adsText'; export { default as adsText } from './adsText';
export { default as beaconChain } from './beaconChain'; export { default as beaconChain } from './beaconChain';
......
...@@ -10,6 +10,9 @@ const meta = Object.freeze({ ...@@ -10,6 +10,9 @@ const meta = Object.freeze({
imageUrl: app.baseUrl + (getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl), imageUrl: app.baseUrl + (getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl),
enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED') === 'true', enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED') === 'true',
}, },
seo: {
enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED') === 'true',
},
}); });
export default meta; export default meta;
# Set of ENVs for Base network explorer
# https://base.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=Base Mainnet
NEXT_PUBLIC_NETWORK_SHORT_NAME=Base
NEXT_PUBLIC_NETWORK_ID=8453
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.base.org/
# api configuration
NEXT_PUBLIC_API_HOST=base.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(136.9deg,rgb(107,94,236)1.5%,rgb(0,82,255)56.84%,rgb(82,62,231)98.54%)
## sidebar
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/base.svg
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/base.svg
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/base-mainnet.json
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://base.drpc.org?ref=559183','text':'Public RPC'}]
## footer
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/base-mainnet.json
##views
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'},{'name':'LooksRare','collection_url':'https://looksrare.org/collections/{hash}','instance_url':'https://looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'l2scan','baseUrl':'https://base.l2scan.co/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}},{'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/base'}},{'title':'3xpl','baseUrl':'https://3xpl.com/','paths':{'tx':'/base/transaction','address':'/base/address'}}]
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>BNS & DAppscout collaboration: <a href='https://base.blockscout.com/apps/base-name-service?utm_source=blockscout&utm_medium=bns_campaign' target='_blank'>earn BNS points</a> for purchasing .base domains! <a href='https://www.blog.blockscout.com/exploring-dappscout-on-base-with-basenameservice-bns?utm_source=blockscout&utm_medium=bns_campaign' target='_blank'>Check here for more details </a></p>
# app features
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xfd5c5dae7b69fe29e61d19b9943e688aa0f1be1e983c4fba8fe985f90ff69d5f
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST=https://stats-l2-base-mainnet.k8s-prod-1.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_AD_BANNER_PROVIDER=hype
NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'728301','width':'728','height':'90'}
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'728301','width':'320','height':'100'}
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_SWAP_BUTTON_URL=aerodrome
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://bridge.base.org/withdraw
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-base.safe.global
#meta
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png
...@@ -55,3 +55,5 @@ NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true ...@@ -55,3 +55,5 @@ NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
#meta #meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true
...@@ -60,6 +60,7 @@ NEXT_PUBLIC_AD_BANNER_PROVIDER=getit ...@@ -60,6 +60,7 @@ NEXT_PUBLIC_AD_BANNER_PROVIDER=getit
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata-test.k8s-dev.blockscout.com
#meta #meta
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png
...@@ -19,8 +19,6 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.optimism.io ...@@ -19,8 +19,6 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.optimism.io
# api configuration # api configuration
NEXT_PUBLIC_API_HOST=optimism.blockscout.com NEXT_PUBLIC_API_HOST=optimism.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
# ui config # ui config
......
...@@ -18,8 +18,6 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://zkevm-rpc.com ...@@ -18,8 +18,6 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://zkevm-rpc.com
# api configuration # api configuration
NEXT_PUBLIC_API_HOST=zkevm.blockscout.com NEXT_PUBLIC_API_HOST=zkevm.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
# ui config # ui config
...@@ -47,4 +45,4 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.co ...@@ -47,4 +45,4 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.co
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
# rollup # rollup
NEXT_PUBLIC_ROLLUP_TYPE=zkEvm NEXT_PUBLIC_ROLLUP_TYPE=zkEvm
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://polygon.blockscout.com NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com
...@@ -18,8 +18,6 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.era.zksync.io ...@@ -18,8 +18,6 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.era.zksync.io
# api configuration # api configuration
NEXT_PUBLIC_API_HOST=zksync.blockscout.com NEXT_PUBLIC_API_HOST=zksync.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
# ui config # ui config
......
...@@ -21,6 +21,7 @@ export_envs_from_preset() { ...@@ -21,6 +21,7 @@ export_envs_from_preset() {
"NEXT_PUBLIC_APP_HOST" "NEXT_PUBLIC_APP_HOST"
"NEXT_PUBLIC_APP_PORT" "NEXT_PUBLIC_APP_PORT"
"NEXT_PUBLIC_APP_ENV" "NEXT_PUBLIC_APP_ENV"
"NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL"
) )
while IFS='=' read -r name value; do while IFS='=' read -r name value; do
......
...@@ -583,6 +583,7 @@ const schema = yup ...@@ -583,6 +583,7 @@ const schema = yup
NEXT_PUBLIC_VISUALIZE_API_BASE_PATH: yup.string(), NEXT_PUBLIC_VISUALIZE_API_BASE_PATH: yup.string(),
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_METADATA_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP), NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP),
NEXT_PUBLIC_WEB3_WALLETS: yup NEXT_PUBLIC_WEB3_WALLETS: yup
.mixed() .mixed()
...@@ -603,6 +604,7 @@ const schema = yup ...@@ -603,6 +604,7 @@ const schema = yup
NEXT_PUBLIC_OG_DESCRIPTION: yup.string(), NEXT_PUBLIC_OG_DESCRIPTION: yup.string(),
NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest), NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest),
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: yup.boolean(), NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: yup.boolean(),
NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED: yup.boolean(),
NEXT_PUBLIC_SAFE_TX_SERVICE_URL: yup.string().test(urlTest), NEXT_PUBLIC_SAFE_TX_SERVICE_URL: yup.string().test(urlTest),
NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(), NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(),
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(), NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
......
...@@ -25,6 +25,7 @@ NEXT_PUBLIC_GAS_TRACKER_ENABLED=true ...@@ -25,6 +25,7 @@ NEXT_PUBLIC_GAS_TRACKER_ENABLED=true
NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei'] NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei']
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='<a href="#">Hello</a>' NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='<a href="#">Hello</a>'
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://example.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
...@@ -41,6 +42,7 @@ NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation ...@@ -41,6 +42,7 @@ NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_OG_DESCRIPTION='Hello world!' NEXT_PUBLIC_OG_DESCRIPTION='Hello world!'
NEXT_PUBLIC_OG_IMAGE_URL=https://example.com/image.png NEXT_PUBLIC_OG_IMAGE_URL=https://example.com/image.png
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://blockscout.com','text':'Blockscout'}] NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://blockscout.com','text':'Blockscout'}]
NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE=true NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE=true
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
......
...@@ -4,9 +4,9 @@ imagePullSecrets: ...@@ -4,9 +4,9 @@ imagePullSecrets:
- name: regcred - name: regcred
config: config:
network: network:
id: 5 id: "11155111"
name: Göerli name: Sepolia
shortname: Göerli shortname: Sepolia
currency: currency:
name: Ether name: Ether
symbol: ETH symbol: ETH
...@@ -54,11 +54,10 @@ blockscout: ...@@ -54,11 +54,10 @@ blockscout:
cpu: "3" cpu: "3"
# Blockscout environment variables # Blockscout environment variables
env: env:
BLOCKSCOUT_VERSION: v5.3.0-beta
ETHEREUM_JSONRPC_VARIANT: geth ETHEREUM_JSONRPC_VARIANT: geth
HEART_BEAT_TIMEOUT: 30 HEART_BEAT_TIMEOUT: 30
SUBNETWORK: Ethereum SUBNETWORK: Ethereum
NETWORK: (Goerli) NETWORK: (Sepolia)
NETWORK_ICON: _network_icon.html NETWORK_ICON: _network_icon.html
LOGO: /images/goerli_logo.svg LOGO: /images/goerli_logo.svg
TXS_STATS_DAYS_TO_COMPILE_AT_INIT: 1 TXS_STATS_DAYS_TO_COMPILE_AT_INIT: 1
...@@ -83,10 +82,10 @@ blockscout: ...@@ -83,10 +82,10 @@ blockscout:
INDEXER_TOKEN_BALANCES_CONCURRENCY: 4 INDEXER_TOKEN_BALANCES_CONCURRENCY: 4
DISABLE_EXCHANGE_RATES: 'true' DISABLE_EXCHANGE_RATES: 'true'
DISABLE_INDEXER: 'false' DISABLE_INDEXER: 'false'
FIRST_BLOCK: '8739119' FIRST_BLOCK: '5780052'
LAST_BLOCK: '8739119' LAST_BLOCK: '5780052'
TRACE_FIRST_BLOCK: '8739119' TRACE_FIRST_BLOCK: '5780052'
TRACE_LAST_BLOCK: '8739119' TRACE_LAST_BLOCK: '5780052'
envFromSecret: envFromSecret:
ETHEREUM_JSONRPC_TRACE_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ETHEREUM_JSONRPC_TRACE_URL ETHEREUM_JSONRPC_TRACE_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ETHEREUM_JSONRPC_TRACE_URL
ETHEREUM_JSONRPC_HTTP_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ETHEREUM_JSONRPC_HTTP_URL ETHEREUM_JSONRPC_HTTP_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ETHEREUM_JSONRPC_HTTP_URL
...@@ -102,7 +101,6 @@ blockscout: ...@@ -102,7 +101,6 @@ blockscout:
ACCOUNT_CLOAK_KEY: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ACCOUNT_CLOAK_KEY ACCOUNT_CLOAK_KEY: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ACCOUNT_CLOAK_KEY
SECRET_KEY_BASE: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SECRET_KEY_BASE SECRET_KEY_BASE: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SECRET_KEY_BASE
DATABASE_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/DATABASE_URL DATABASE_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/DATABASE_URL
DATABASE_READ_ONLY_API_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/DATABASE_READ_ONLY_API_URL
API_SENSITIVE_ENDPOINTS_KEY: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/API_SENSITIVE_ENDPOINTS_KEY API_SENSITIVE_ENDPOINTS_KEY: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/API_SENSITIVE_ENDPOINTS_KEY
ACCOUNT_DATABASE_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ACCOUNT_DATABASE_URL ACCOUNT_DATABASE_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ACCOUNT_DATABASE_URL
ACCOUNT_REDIS_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ACCOUNT_REDIS_URL ACCOUNT_REDIS_URL: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/ACCOUNT_REDIS_URL
...@@ -141,11 +139,11 @@ frontend: ...@@ -141,11 +139,11 @@ frontend:
env: env:
# ui config # ui config
NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json
NEXT_PUBLIC_NETWORK_EXPLORERS: "[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]" NEXT_PUBLIC_NETWORK_EXPLORERS: "[{'title':'Etherscan','baseUrl':'https://sepolia.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/sepolia'}} ]"
# network config # network config
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.svg
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation
NEXT_PUBLIC_MARKETPLACE_ENABLED: true NEXT_PUBLIC_MARKETPLACE_ENABLED: true
...@@ -159,14 +157,14 @@ frontend: ...@@ -159,14 +157,14 @@ frontend:
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli NEXT_PUBLIC_NETWORK_RPC_URL: https://sepolia.drpc.org
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']" NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']"
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json #NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-sepolia.json
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json #NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']" NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']"
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]" #NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]"
NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]" NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-sepolia.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]"
NEXT_PUBLIC_SWAP_BUTTON_URL: uniswap NEXT_PUBLIC_SWAP_BUTTON_URL: uniswap
envFromSecret: envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
......
...@@ -59,9 +59,9 @@ frontend: ...@@ -59,9 +59,9 @@ frontend:
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_METADATA_SERVICE_API_HOST: https://metadata-test.k8s-dev.blockscout.com
NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_ENABLED: true NEXT_PUBLIC_MARKETPLACE_ENABLED: true
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM: https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL: https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL: https://gist.githubusercontent.com/maxaleks/ce5c7e3de53e8f5b240b88265daf5839/raw/328383c958a8f7ecccf6d50c953bcdf8ab3faa0a/security_reports_goerli_test.json
......
...@@ -49,6 +49,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -49,6 +49,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Transaction interpretation](ENVS.md#transaction-interpretation) - [Transaction interpretation](ENVS.md#transaction-interpretation)
- [Verified tokens info](ENVS.md#verified-tokens-info) - [Verified tokens info](ENVS.md#verified-tokens-info)
- [Name service integration](ENVS.md#name-service-integration) - [Name service integration](ENVS.md#name-service-integration)
- [Metadata service integration](ENVS.md#metadata-service-integration)
- [Bridged tokens](ENVS.md#bridged-tokens) - [Bridged tokens](ENVS.md#bridged-tokens)
- [Safe{Core} address tags](ENVS.md#safecore-address-tags) - [Safe{Core} address tags](ENVS.md#safecore-address-tags)
- [SUAVE chain](ENVS.md#suave-chain) - [SUAVE chain](ENVS.md#suave-chain)
...@@ -171,7 +172,7 @@ By default, the app has generic favicon. You can override this behavior by provi ...@@ -171,7 +172,7 @@ By default, the app has generic favicon. You can override this behavior by provi
### Meta ### Meta
Settings for meta tags and OG tags Settings for meta tags, OG tags and SEO
| Variable | Type| Description | Compulsoriness | Default value | Example value | | Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
...@@ -179,6 +180,7 @@ Settings for meta tags and OG tags ...@@ -179,6 +180,7 @@ Settings for meta tags and OG tags
| NEXT_PUBLIC_OG_DESCRIPTION | `string` | Custom OG description | - | - | `Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks.` | | NEXT_PUBLIC_OG_DESCRIPTION | `string` | Custom OG description | - | - | `Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks.` |
| NEXT_PUBLIC_OG_IMAGE_URL | `string` | OG image url. Minimum image size is 200 x 20 pixels (recommended: 1200 x 600); maximum supported file size is 8 MB; 2:1 aspect ratio; supported formats: image/jpeg, image/gif, image/png | - | `static/og_placeholder.png` | `https://placekitten.com/1200/600` | | NEXT_PUBLIC_OG_IMAGE_URL | `string` | OG image url. Minimum image size is 200 x 20 pixels (recommended: 1200 x 600); maximum supported file size is 8 MB; 2:1 aspect ratio; supported formats: image/jpeg, image/gif, image/png | - | `static/og_placeholder.png` | `https://placekitten.com/1200/600` |
| NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to populate OG tags (title, description) with API data for social preview robot requests | - | `false` | `true` | | NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to populate OG tags (title, description) with API data for social preview robot requests | - | `false` | `true` |
| NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED | `boolean` | Set to `true` to pre-render page titles (e.g Token page) on the server side and inject page h1-tag to the markup before it is sent to the browser. | - | `false` | `true` |
&nbsp; &nbsp;
...@@ -388,7 +390,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -388,7 +390,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'shibarium' \| 'zkEvm' \| 'zkSync' ` | Rollup chain type | Required | - | `'optimistic'` | | NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'shibarium' \| 'zkEvm' \| 'zkSync' ` | Rollup chain type | Required | - | `'optimistic'` |
| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | | NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` |
| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | - | - | `https://app.optimism.io/bridge/withdraw` | | NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | Required only for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` |
&nbsp; &nbsp;
...@@ -550,6 +552,16 @@ This feature allows resolving blockchain addresses using human-readable domain n ...@@ -550,6 +552,16 @@ This feature allows resolving blockchain addresses using human-readable domain n
&nbsp; &nbsp;
### Metadata service integration
This feature allows name tags and other public tags for addresses.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` |
&nbsp;
### Data Availability ### Data Availability
This feature enables views related to blob transactions (EIP-4844), such as the Blob Txns tab on the Transactions page and the Blob details page. This feature enables views related to blob transactions (EIP-4844), such as the Blob Txns tab on the Transactions page and the Blob details page.
......
<svg viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" fill="none">
<circle cx="5" cy="5" r="5" fill="#D9DBE0"/> <circle cx="5" cy="5.438" r="5" fill="#E2E8F0"/>
<circle cx="5" cy="5" r="2.5" fill="#707886"/> <circle cx="5" cy="5.438" r="2.5" fill="#4A5568"/>
<circle cx="5" cy="5.438" r="2.5" fill="#000" fill-opacity=".2"/>
</svg> </svg>
import type { AddressMetadataTag } from 'types/api/addressMetadata';
import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
export default function parseMetaPayload(meta: AddressMetadataTag['meta']): AddressMetadataTagFormatted['meta'] {
try {
const parsedMeta = JSON.parse(meta || '');
if (typeof parsedMeta !== 'object' || parsedMeta === null || Array.isArray(parsedMeta)) {
throw new Error('Invalid JSON');
}
const result: AddressMetadataTagFormatted['meta'] = {};
if ('textColor' in parsedMeta && typeof parsedMeta.textColor === 'string') {
result.textColor = parsedMeta.textColor;
}
if ('bgColor' in parsedMeta && typeof parsedMeta.bgColor === 'string') {
result.bgColor = parsedMeta.bgColor;
}
if ('actionURL' in parsedMeta && typeof parsedMeta.actionURL === 'string') {
result.actionURL = parsedMeta.actionURL;
}
return result;
} catch (error) {
return null;
}
}
import { useQuery } from '@tanstack/react-query';
import type { AddressMetadataInfo } from 'types/api/addressMetadata';
import type { AddressMetadataInfoFormatted, AddressMetadataTagFormatted } from 'types/client/addressMetadata';
import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery';
import parseMetaPayload from './parseMetaPayload';
export default function useAddressMetadataInfoQuery(addresses: Array<string>) {
const apiFetch = useApiFetch();
const queryParams = {
addresses,
chainId: config.chain.id,
tagsLimit: '20',
};
const resource = 'address_metadata_info';
// TODO @tom2drum: Improve the typing here
// since we are formatting the API data in the select function here
// we cannot use the useApiQuery hook because of its current typing
// enhance useApiQuery so it can accept an API data and the formatted data types
return useQuery<AddressMetadataInfo, unknown, AddressMetadataInfoFormatted>({
queryKey: getResourceKey(resource, { queryParams }),
queryFn: async() => {
return apiFetch(resource, { queryParams }) as Promise<AddressMetadataInfo>;
},
enabled: addresses.length > 0 && config.features.addressMetadata.isEnabled,
select: (data) => {
const addresses = Object.entries(data.addresses)
.map(([ address, { tags, reputation } ]) => {
const formattedTags: Array<AddressMetadataTagFormatted> = tags.map((tag) => ({ ...tag, meta: parseMetaPayload(tag.meta) }));
return [ address.toLowerCase(), { tags: formattedTags, reputation } ] as const;
})
.reduce((result, item) => {
result[item[0]] = item[1];
return result;
}, {} as AddressMetadataInfoFormatted['addresses']);
return { addresses };
},
});
}
...@@ -32,6 +32,7 @@ import type { ...@@ -32,6 +32,7 @@ import type {
AddressCoinBalanceHistoryChartOld, AddressCoinBalanceHistoryChartOld,
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses'; import type { AddressesResponse } from 'types/api/addresses';
import type { AddressMetadataInfo } from 'types/api/addressMetadata';
import type { TxBlobs, Blob } from 'types/api/blobs'; import type { TxBlobs, Blob } from 'types/api/blobs';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
...@@ -98,7 +99,14 @@ import type { ValidatorsCountersResponse, ValidatorsFilters, ValidatorsResponse, ...@@ -98,7 +99,14 @@ import type { ValidatorsCountersResponse, ValidatorsFilters, ValidatorsResponse,
import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
import type { VisualizedContract } from 'types/api/visualization'; import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2TxnBatchesResponse, ZkEvmL2TxnBatchTxs } from 'types/api/zkEvmL2'; import type {
ZkEvmL2DepositsResponse,
ZkEvmL2TxnBatch,
ZkEvmL2TxnBatchesItem,
ZkEvmL2TxnBatchesResponse,
ZkEvmL2TxnBatchTxs,
ZkEvmL2WithdrawalsResponse,
} from 'types/api/zkEvmL2';
import type { ZkSyncBatch, ZkSyncBatchesResponse, ZkSyncBatchTxs } from 'types/api/zkSyncL2'; import type { ZkSyncBatch, ZkSyncBatchesResponse, ZkSyncBatchTxs } from 'types/api/zkSyncL2';
import type { MarketplaceAppOverview } from 'types/client/marketplace'; import type { MarketplaceAppOverview } from 'types/client/marketplace';
import type { ArrayElement } from 'types/utils'; import type { ArrayElement } from 'types/utils';
...@@ -237,6 +245,18 @@ export const RESOURCES = { ...@@ -237,6 +245,18 @@ export const RESOURCES = {
filterFields: [ 'name' as const, 'only_active' as const ], filterFields: [ 'name' as const, 'only_active' as const ],
}, },
// METADATA SERVICE
address_metadata_info: {
path: '/api/v1/metadata',
endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath,
},
address_metadata_tag_search: {
path: '/api/v1/tags:search',
endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath,
},
// VISUALIZATION // VISUALIZATION
visualize_sol2uml: { visualize_sol2uml: {
path: '/api/v1/solidity\\:visualize-contracts', path: '/api/v1/solidity\\:visualize-contracts',
...@@ -588,43 +608,61 @@ export const RESOURCES = { ...@@ -588,43 +608,61 @@ export const RESOURCES = {
}, },
// optimistic L2 // optimistic L2
l2_deposits: { optimistic_l2_deposits: {
path: '/api/v2/optimism/deposits', path: '/api/v2/optimism/deposits',
filterFields: [], filterFields: [],
}, },
l2_deposits_count: { optimistic_l2_deposits_count: {
path: '/api/v2/optimism/deposits/count', path: '/api/v2/optimism/deposits/count',
}, },
l2_withdrawals: { optimistic_l2_withdrawals: {
path: '/api/v2/optimism/withdrawals', path: '/api/v2/optimism/withdrawals',
filterFields: [], filterFields: [],
}, },
l2_withdrawals_count: { optimistic_l2_withdrawals_count: {
path: '/api/v2/optimism/withdrawals/count', path: '/api/v2/optimism/withdrawals/count',
}, },
l2_output_roots: { optimistic_l2_output_roots: {
path: '/api/v2/optimism/output-roots', path: '/api/v2/optimism/output-roots',
filterFields: [], filterFields: [],
}, },
l2_output_roots_count: { optimistic_l2_output_roots_count: {
path: '/api/v2/optimism/output-roots/count', path: '/api/v2/optimism/output-roots/count',
}, },
l2_txn_batches: { optimistic_l2_txn_batches: {
path: '/api/v2/optimism/txn-batches', path: '/api/v2/optimism/txn-batches',
filterFields: [], filterFields: [],
}, },
l2_txn_batches_count: { optimistic_l2_txn_batches_count: {
path: '/api/v2/optimism/txn-batches/count', path: '/api/v2/optimism/txn-batches/count',
}, },
// zkEvm L2 // zkEvm L2
zkevm_l2_deposits: {
path: '/api/v2/zkevm/deposits',
filterFields: [],
},
zkevm_l2_deposits_count: {
path: '/api/v2/zkevm/deposits/count',
},
zkevm_l2_withdrawals: {
path: '/api/v2/zkevm/withdrawals',
filterFields: [],
},
zkevm_l2_withdrawals_count: {
path: '/api/v2/zkevm/withdrawals/count',
},
zkevm_l2_txn_batches: { zkevm_l2_txn_batches: {
path: '/api/v2/zkevm/batches', path: '/api/v2/zkevm/batches',
filterFields: [], filterFields: [],
...@@ -739,6 +777,12 @@ export const RESOURCES = { ...@@ -739,6 +777,12 @@ export const RESOURCES = {
path: '/api/v2/config/backend-version', path: '/api/v2/config/backend-version',
}, },
// CSV EXPORT
csv_export_token_holders: {
path: '/api/v2/tokens/:hash/holders/csv',
pathParams: [ 'hash' as const ],
},
// OTHER // OTHER
api_v2_key: { api_v2_key: {
path: '/api/v2/key', path: '/api/v2/key',
...@@ -802,9 +846,9 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -802,9 +846,9 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' | 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
'token_instance_transfers' | 'token_instance_holders' | 'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' | 'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | 'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' |
'shibarium_deposits' | 'shibarium_withdrawals' | 'shibarium_deposits' | 'shibarium_withdrawals' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'zkevm_l2_deposits' | 'zkevm_l2_withdrawals' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' | 'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' |
...@@ -902,19 +946,16 @@ Q extends 'visualize_sol2uml' ? VisualizedContract : ...@@ -902,19 +946,16 @@ Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig : Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
Q extends 'withdrawals' ? WithdrawalsResponse : Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters : Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'l2_output_roots' ? OptimisticL2OutputRootsResponse : Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse :
Q extends 'l2_withdrawals' ? OptimisticL2WithdrawalsResponse : Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
Q extends 'l2_deposits' ? OptimisticL2DepositsResponse : Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse :
Q extends 'l2_txn_batches' ? OptimisticL2TxnBatchesResponse : Q extends 'optimistic_l2_txn_batches' ? OptimisticL2TxnBatchesResponse :
Q extends 'l2_output_roots_count' ? number : Q extends 'optimistic_l2_output_roots_count' ? number :
Q extends 'l2_withdrawals_count' ? number : Q extends 'optimistic_l2_withdrawals_count' ? number :
Q extends 'l2_deposits_count' ? number : Q extends 'optimistic_l2_deposits_count' ? number :
Q extends 'l2_txn_batches_count' ? number : Q extends 'optimistic_l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse :
Q extends 'zkevm_l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch :
Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs :
Q extends 'config_backend_version' ? BackendVersionConfig : Q extends 'config_backend_version' ? BackendVersionConfig :
Q extends 'address_metadata_info' ? AddressMetadataInfo :
never; never;
// !!! IMPORTANT !!! // !!! IMPORTANT !!!
// See comment above // See comment above
...@@ -931,6 +972,14 @@ Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse : ...@@ -931,6 +972,14 @@ Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse :
Q extends 'shibarium_deposits' ? ShibariumDepositsResponse : Q extends 'shibarium_deposits' ? ShibariumDepositsResponse :
Q extends 'shibarium_withdrawals_count' ? number : Q extends 'shibarium_withdrawals_count' ? number :
Q extends 'shibarium_deposits_count' ? number : Q extends 'shibarium_deposits_count' ? number :
Q extends 'zkevm_l2_deposits' ? ZkEvmL2DepositsResponse :
Q extends 'zkevm_l2_deposits_count' ? number :
Q extends 'zkevm_l2_withdrawals' ? ZkEvmL2WithdrawalsResponse :
Q extends 'zkevm_l2_withdrawals_count' ? number :
Q extends 'zkevm_l2_txn_batches' ? ZkEvmL2TxnBatchesResponse :
Q extends 'zkevm_l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch :
Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs :
Q extends 'zksync_l2_txn_batches' ? ZkSyncBatchesResponse : Q extends 'zksync_l2_txn_batches' ? ZkSyncBatchesResponse :
Q extends 'zksync_l2_txn_batches_count' ? number : Q extends 'zksync_l2_txn_batches_count' ? number :
Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch : Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch :
......
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import type { Route } from 'nextjs-routes';
import type { Props as PageProps } from 'nextjs/getServerSideProps'; import type { Props as PageProps } from 'nextjs/getServerSideProps';
type Props = { type Props = {
...@@ -23,6 +24,6 @@ export function AppContextProvider({ children, pageProps }: Props) { ...@@ -23,6 +24,6 @@ export function AppContextProvider({ children, pageProps }: Props) {
); );
} }
export function useAppContext() { export function useAppContext<Pathname extends Route['pathname'] = never>() {
return useContext(AppContext); return useContext<PageProps<Pathname>>(AppContext);
} }
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import * as stubs from 'stubs/contract';
import ContractCode from 'ui/address/contract/ContractCode'; import ContractCode from 'ui/address/contract/ContractCode';
import ContractRead from 'ui/address/contract/ContractRead'; import ContractRead from 'ui/address/contract/ContractRead';
import ContractWrite from 'ui/address/contract/ContractWrite'; import ContractWrite from 'ui/address/contract/ContractWrite';
export default function useContractTabs(data: Address | undefined) { const CONTRACT_TAB_IDS = [
'contract_code',
'read_contract',
'read_proxy',
'read_custom_methods',
'write_contract',
'write_proxy',
'write_custom_methods',
] as const;
interface ContractTab {
id: typeof CONTRACT_TAB_IDS[number];
title: string;
component: JSX.Element;
}
interface ReturnType {
tabs: Array<ContractTab>;
isLoading: boolean;
}
export default function useContractTabs(data: Address | undefined, isPlaceholderData: boolean): ReturnType {
const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false);
const router = useRouter();
const tab = getQueryParamString(router.query.tab);
const isEnabled = Boolean(data?.hash) && !isPlaceholderData && CONTRACT_TAB_IDS.concat('contract' as never).includes(tab);
const enableQuery = React.useCallback(() => {
setIsQueryEnabled(true);
}, []);
const contractQuery = useApiQuery('contract', {
pathParams: { hash: data?.hash },
queryOptions: {
enabled: isEnabled && isQueryEnabled,
refetchOnMount: false,
placeholderData: data?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
},
});
const channel = useSocketChannel({
topic: `addresses:${ data?.hash?.toLowerCase() }`,
isDisabled: !isEnabled,
onJoin: enableQuery,
onSocketError: enableQuery,
});
return React.useMemo(() => { return React.useMemo(() => {
return [ return {
{ id: 'contact_code', title: 'Code', component: <ContractCode addressHash={ data?.hash }/> }, tabs: [
// this is not implemented in api yet {
// data?.has_decompiled_code ? id: 'contract_code' as const,
// { id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } : title: 'Code',
// undefined, component: <ContractCode contractQuery={ contractQuery } channel={ channel } addressHash={ data?.hash }/>,
data?.has_methods_read ? },
{ id: 'read_contract', title: 'Read contract', component: <ContractRead/> } : contractQuery.data?.has_methods_read ?
{ id: 'read_contract' as const, title: 'Read contract', component: <ContractRead isLoading={ contractQuery.isPlaceholderData }/> } :
undefined, undefined,
data?.has_methods_read_proxy ? contractQuery.data?.has_methods_read_proxy ?
{ id: 'read_proxy', title: 'Read proxy', component: <ContractRead/> } : { id: 'read_proxy' as const, title: 'Read proxy', component: <ContractRead isLoading={ contractQuery.isPlaceholderData }/> } :
undefined, undefined,
data?.has_custom_methods_read ? contractQuery.data?.has_custom_methods_read ?
{ id: 'read_custom_methods', title: 'Read custom', component: <ContractRead/> } : { id: 'read_custom_methods' as const, title: 'Read custom', component: <ContractRead isLoading={ contractQuery.isPlaceholderData }/> } :
undefined, undefined,
data?.has_methods_write ? contractQuery.data?.has_methods_write ?
{ id: 'write_contract', title: 'Write contract', component: <ContractWrite/> } : { id: 'write_contract' as const, title: 'Write contract', component: <ContractWrite isLoading={ contractQuery.isPlaceholderData }/> } :
undefined, undefined,
data?.has_methods_write_proxy ? contractQuery.data?.has_methods_write_proxy ?
{ id: 'write_proxy', title: 'Write proxy', component: <ContractWrite/> } : { id: 'write_proxy' as const, title: 'Write proxy', component: <ContractWrite isLoading={ contractQuery.isPlaceholderData }/> } :
undefined, undefined,
data?.has_custom_methods_write ? contractQuery.data?.has_custom_methods_write ?
{ id: 'write_custom_methods', title: 'Write custom', component: <ContractWrite/> } : { id: 'write_custom_methods' as const, title: 'Write custom', component: <ContractWrite isLoading={ contractQuery.isPlaceholderData }/> } :
undefined, undefined,
].filter(Boolean); ].filter(Boolean),
}, [ data ]); isLoading: contractQuery.isPlaceholderData,
};
}, [ contractQuery, channel, data?.hash ]);
} }
...@@ -72,45 +72,45 @@ export default function useNavItems(): ReturnType { ...@@ -72,45 +72,45 @@ export default function useNavItems(): ReturnType {
icon: 'validator', icon: 'validator',
isActive: pathname === '/validators', isActive: pathname === '/validators',
} : null; } : null;
const rollupDeposits = {
const rollupFeature = config.features.rollup; text: `Deposits (L1${ rightLineArrow }L2)`,
nextRoute: { pathname: '/deposits' as const },
if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') { icon: 'arrows/south-east',
blockchainNavItems = [ isActive: pathname === '/deposits',
[ };
txs, const rollupWithdrawals = {
userOps, text: `Withdrawals (L2${ rightLineArrow }L1)`,
blocks, nextRoute: { pathname: '/withdrawals' as const },
{ icon: 'arrows/north-east',
isActive: pathname === '/withdrawals',
};
const rollupTxnBatches = {
text: 'Txn batches', text: 'Txn batches',
nextRoute: { pathname: '/batches' as const }, nextRoute: { pathname: '/batches' as const },
icon: 'txn_batches', icon: 'txn_batches',
isActive: pathname === '/batches' || pathname === '/batches/[number]', isActive: pathname === '/batches',
}, };
].filter(Boolean), const rollupOutputRoots = {
[ text: 'Output roots',
topAccounts, nextRoute: { pathname: '/output-roots' as const },
validators, icon: 'output_roots',
verifiedContracts, isActive: pathname === '/output-roots',
ensLookup, };
].filter(Boolean),
]; const rollupFeature = config.features.rollup;
} else if (rollupFeature.isEnabled && rollupFeature.type === 'optimistic') {
if (rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'zkEvm')) {
blockchainNavItems = [ blockchainNavItems = [
[ [
txs, txs,
// eslint-disable-next-line max-len rollupDeposits,
{ text: `Deposits (L1${ rightLineArrow }L2)`, nextRoute: { pathname: '/deposits' as const }, icon: 'arrows/south-east', isActive: pathname === '/deposits' }, rollupWithdrawals,
// eslint-disable-next-line max-len
{ text: `Withdrawals (L2${ rightLineArrow }L1)`, nextRoute: { pathname: '/withdrawals' as const }, icon: 'arrows/north-east', isActive: pathname === '/withdrawals' },
], ],
[ [
blocks, blocks,
// eslint-disable-next-line max-len rollupTxnBatches,
{ text: 'Txn batches', nextRoute: { pathname: '/batches' as const }, icon: 'txn_batches', isActive: pathname === '/batches' }, rollupFeature.type === 'optimistic' ? rollupOutputRoots : undefined,
// eslint-disable-next-line max-len ].filter(Boolean),
{ text: 'Output roots', nextRoute: { pathname: '/output-roots' as const }, icon: 'output_roots', isActive: pathname === '/output-roots' },
],
[ [
userOps, userOps,
topAccounts, topAccounts,
...@@ -123,10 +123,8 @@ export default function useNavItems(): ReturnType { ...@@ -123,10 +123,8 @@ export default function useNavItems(): ReturnType {
blockchainNavItems = [ blockchainNavItems = [
[ [
txs, txs,
// eslint-disable-next-line max-len rollupDeposits,
{ text: `Deposits (L1${ rightLineArrow }L2)`, nextRoute: { pathname: '/deposits' as const }, icon: 'arrows/south-east', isActive: pathname === '/deposits' }, rollupWithdrawals,
// eslint-disable-next-line max-len
{ text: `Withdrawals (L2${ rightLineArrow }L1)`, nextRoute: { pathname: '/withdrawals' as const }, icon: 'arrows/north-east', isActive: pathname === '/withdrawals' },
], ],
[ [
blocks, blocks,
...@@ -142,12 +140,7 @@ export default function useNavItems(): ReturnType { ...@@ -142,12 +140,7 @@ export default function useNavItems(): ReturnType {
txs, txs,
userOps, userOps,
blocks, blocks,
{ rollupTxnBatches,
text: 'Txn batches',
nextRoute: { pathname: '/batches' as const },
icon: 'txn_batches',
isActive: pathname === '/batches' || pathname === '/batches/[number]',
},
].filter(Boolean), ].filter(Boolean),
[ [
topAccounts, topAccounts,
......
import type { TokenInfo } from 'types/api/token';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
/* eslint-disable @typescript-eslint/indent */ /* eslint-disable @typescript-eslint/indent */
export type ApiData<Pathname extends Route['pathname']> = export type ApiData<Pathname extends Route['pathname']> =
( (
Pathname extends '/address/[hash]' ? { domain_name: string } : Pathname extends '/address/[hash]' ? { domain_name: string } :
Pathname extends '/token/[hash]' ? { symbol: string } : Pathname extends '/token/[hash]' ? TokenInfo :
Pathname extends '/token/[hash]/instance/[id]' ? { symbol: string } : Pathname extends '/token/[hash]/instance/[id]' ? { symbol: string } :
Pathname extends '/apps/[id]' ? { app_name: string } : Pathname extends '/apps/[id]' ? { app_name: string } :
never never
......
...@@ -59,14 +59,8 @@ export const token: Address = { ...@@ -59,14 +59,8 @@ export const token: Address = {
creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72', creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72',
exchange_rate: null, exchange_rate: null,
implementation_address: null, implementation_address: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false, has_decompiled_code: false,
has_logs: false, has_logs: false,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: true, has_token_transfers: true,
has_tokens: true, has_tokens: true,
has_validated_blocks: false, has_validated_blocks: false,
...@@ -79,14 +73,8 @@ export const contract: Address = { ...@@ -79,14 +73,8 @@ export const contract: Address = {
creation_tx_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e', creation_tx_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e',
creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943', creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943',
exchange_rate: '0.04311', exchange_rate: '0.04311',
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false, has_decompiled_code: false,
has_logs: true, has_logs: true,
has_methods_read: true,
has_methods_read_proxy: true,
has_methods_write: true,
has_methods_write_proxy: true,
has_token_transfers: false, has_token_transfers: false,
has_tokens: false, has_tokens: false,
has_validated_blocks: false, has_validated_blocks: false,
...@@ -110,14 +98,8 @@ export const validator: Address = { ...@@ -110,14 +98,8 @@ export const validator: Address = {
creation_tx_hash: null, creation_tx_hash: null,
creator_address_hash: null, creator_address_hash: null,
exchange_rate: '0.00432018', exchange_rate: '0.00432018',
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false, has_decompiled_code: false,
has_logs: false, has_logs: false,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false, has_token_transfers: false,
has_tokens: false, has_tokens: false,
has_validated_blocks: true, has_validated_blocks: true,
......
/* eslint-disable max-len */ /* eslint-disable max-len */
import type { SmartContract } from 'types/api/contract'; import type { SmartContract } from 'types/api/contract';
export const verified: Partial<SmartContract> = { export const verified: SmartContract = {
abi: [ { anonymous: false, inputs: [ { indexed: true, internalType: 'address', name: 'src', type: 'address' }, { indexed: true, internalType: 'address', name: 'guy', type: 'address' }, { indexed: false, internalType: 'uint256', name: 'wad', type: 'uint256' } ], name: 'Approval', type: 'event' } ], abi: [ { anonymous: false, inputs: [ { indexed: true, internalType: 'address', name: 'src', type: 'address' }, { indexed: true, internalType: 'address', name: 'guy', type: 'address' }, { indexed: false, internalType: 'uint256', name: 'wad', type: 'uint256' } ], name: 'Approval', type: 'event' } ],
can_be_visualized_via_sol2uml: true, can_be_visualized_via_sol2uml: true,
compiler_version: 'v0.5.16+commit.9c3226ce', compiler_version: 'v0.5.16+commit.9c3226ce',
...@@ -32,9 +32,26 @@ export const verified: Partial<SmartContract> = { ...@@ -32,9 +32,26 @@ export const verified: Partial<SmartContract> = {
], ],
language: 'solidity', language: 'solidity',
license_type: 'gnu_gpl_v3', license_type: 'gnu_gpl_v3',
has_methods_read: true,
has_methods_read_proxy: false,
has_methods_write: true,
has_methods_write_proxy: false,
has_custom_methods_read: false,
has_custom_methods_write: false,
is_self_destructed: false,
is_verified_via_eth_bytecode_db: null,
is_changed_bytecode: null,
is_verified_via_sourcify: null,
is_fully_verified: null,
is_partially_verified: null,
sourcify_repo_url: null,
file_path: '',
additional_sources: [],
verified_twin_address_hash: null,
minimal_proxy_address_hash: null,
}; };
export const withMultiplePaths: Partial<SmartContract> = { export const withMultiplePaths: SmartContract = {
...verified, ...verified,
file_path: './simple_storage.sol', file_path: './simple_storage.sol',
additional_sources: [ additional_sources: [
...@@ -45,7 +62,7 @@ export const withMultiplePaths: Partial<SmartContract> = { ...@@ -45,7 +62,7 @@ export const withMultiplePaths: Partial<SmartContract> = {
], ],
}; };
export const verifiedViaSourcify: Partial<SmartContract> = { export const verifiedViaSourcify: SmartContract = {
...verified, ...verified,
is_verified_via_sourcify: true, is_verified_via_sourcify: true,
is_fully_verified: false, is_fully_verified: false,
...@@ -53,36 +70,67 @@ export const verifiedViaSourcify: Partial<SmartContract> = { ...@@ -53,36 +70,67 @@ export const verifiedViaSourcify: Partial<SmartContract> = {
sourcify_repo_url: 'https://repo.sourcify.dev/contracts//full_match/99/0x51891596E158b2857e5356DC847e2D15dFbCF2d0/', sourcify_repo_url: 'https://repo.sourcify.dev/contracts//full_match/99/0x51891596E158b2857e5356DC847e2D15dFbCF2d0/',
}; };
export const verifiedViaEthBytecodeDb: Partial<SmartContract> = { export const verifiedViaEthBytecodeDb: SmartContract = {
...verified, ...verified,
is_verified_via_eth_bytecode_db: true, is_verified_via_eth_bytecode_db: true,
}; };
export const withTwinAddress: Partial<SmartContract> = { export const withTwinAddress: SmartContract = {
...verified, ...verified,
is_verified: false, is_verified: false,
verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
}; };
export const withProxyAddress: Partial<SmartContract> = { export const withProxyAddress: SmartContract = {
...verified, ...verified,
is_verified: false, is_verified: false,
verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
minimal_proxy_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8', minimal_proxy_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
}; };
export const selfDestructed: Partial<SmartContract> = { export const selfDestructed: SmartContract = {
...verified, ...verified,
is_self_destructed: true, is_self_destructed: true,
}; };
export const withChangedByteCode: Partial<SmartContract> = { export const withChangedByteCode: SmartContract = {
...verified, ...verified,
is_changed_bytecode: true, is_changed_bytecode: true,
}; };
export const nonVerified: Partial<SmartContract> = { export const nonVerified: SmartContract = {
is_verified: false, is_verified: false,
creation_bytecode: 'creation_bytecode', creation_bytecode: 'creation_bytecode',
deployed_bytecode: 'deployed_bytecode', deployed_bytecode: 'deployed_bytecode',
is_self_destructed: false,
abi: null,
compiler_version: null,
evm_version: null,
optimization_enabled: null,
optimization_runs: null,
name: null,
verified_at: null,
is_verified_via_eth_bytecode_db: null,
is_changed_bytecode: null,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_custom_methods_read: false,
has_custom_methods_write: false,
is_verified_via_sourcify: null,
is_fully_verified: null,
is_partially_verified: null,
sourcify_repo_url: null,
source_code: null,
constructor_args: null,
decoded_constructor_args: null,
can_be_visualized_via_sol2uml: null,
file_path: '',
additional_sources: [],
external_libraries: null,
verified_twin_address_hash: null,
minimal_proxy_address_hash: null,
language: null,
license_type: null,
}; };
import type { AddressMetadataInfo, AddressMetadataTag } from 'types/api/addressMetadata';
import { hash } from '../address/address';
export const nameTag1: AddressMetadataTag = {
slug: 'ethermineru',
name: 'Ethermine.ru',
tagType: 'name',
ordinal: 0,
meta: null,
};
export const genericTag1: AddressMetadataTag = {
slug: 'ethermine.ru',
name: 'Ethermine.ru',
tagType: 'generic',
ordinal: 0,
meta: null,
};
export const protocolTag1: AddressMetadataTag = {
slug: 'aerodrome',
name: 'Aerodrome',
tagType: 'protocol',
ordinal: 0,
meta: null,
};
export const baseInfo: AddressMetadataInfo = {
addresses: {
[hash]: {
tags: [ nameTag1, genericTag1, protocolTag1 ],
reputation: null,
},
},
};
import type { ZkEvmL2DepositsResponse } from 'types/api/zkEvmL2';
export const baseResponse: ZkEvmL2DepositsResponse = {
items: [
{
block_number: 19681943,
index: 182177,
l1_transaction_hash: '0x29074452f976064aca1ca5c6e7c82d890c10454280693e6eca0257ae000c8e85',
l2_transaction_hash: null,
symbol: 'DAI',
timestamp: '2022-04-18T11:08:11.000000Z',
value: '0.003',
},
{
block_number: 19681894,
index: 182176,
l1_transaction_hash: '0x0b7d58c0a6b4695ba28d99df928591fb931c812c0aab6d0093ff5040d2f9bc5e',
l2_transaction_hash: '0x210d9f70f411de1079e32a98473b04345a5ea6ff2340a8511ebc2df641274436',
symbol: 'ETH',
timestamp: '2022-04-18T10:58:23.000000Z',
value: '0.0046651390188845',
},
],
next_page_params: {
items_count: 50,
index: 1,
},
};
import type { ZkEvmL2TxnBatch } from 'types/api/zkEvmL2'; import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvmL2';
export const txnBatchData: ZkEvmL2TxnBatch = { export const txnBatchData: ZkEvmL2TxnBatch = {
acc_input_hash: '0x4bf88aabe33713b7817266d7860912c58272d808da7397cdc627ca53b296fad3', acc_input_hash: '0x4bf88aabe33713b7817266d7860912c58272d808da7397cdc627ca53b296fad3',
...@@ -13,3 +13,28 @@ export const txnBatchData: ZkEvmL2TxnBatch = { ...@@ -13,3 +13,28 @@ export const txnBatchData: ZkEvmL2TxnBatch = {
], ],
verify_tx_hash: '0x6f7eeaa0eb966e63d127bba6bf8f9046d303c2a1185b542f0b5083f682a0e87f', verify_tx_hash: '0x6f7eeaa0eb966e63d127bba6bf8f9046d303c2a1185b542f0b5083f682a0e87f',
}; };
export const txnBatchesData: ZkEvmL2TxnBatchesResponse = {
items: [
{
timestamp: '2023-06-01T14:46:48.000000Z',
status: 'Finalized',
verify_tx_hash: '0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8',
sequence_tx_hash: '0x6aa081e8e33a085e4ec7124fcd8a5f7d36aac0828f176e80d4b70e313a11695b',
number: 5218590,
tx_count: 9,
},
{
timestamp: '2023-06-01T14:46:48.000000Z',
status: 'Unfinalized',
verify_tx_hash: null,
sequence_tx_hash: null,
number: 5218591,
tx_count: 9,
},
],
next_page_params: {
number: 5902834,
items_count: 50,
},
};
import type { ZkEvmL2WithdrawalsResponse } from 'types/api/zkEvmL2';
export const baseResponse: ZkEvmL2WithdrawalsResponse = {
items: [
{
block_number: 11722417,
index: 47040,
l1_transaction_hash: null,
l2_transaction_hash: '0x68c378e412e51553524545ef1d3f00f69496fb37827c0b3b7e0870d245970408',
symbol: 'ETH',
timestamp: '2022-04-18T09:20:37.000000Z',
value: '0.025',
},
{
block_number: 11722480,
index: 47041,
l1_transaction_hash: '0xbf76feb85b8b8f24dacb17f962dd359f82efc512928d7b11ffca92fb812ad6a5',
l2_transaction_hash: '0xfe3c168ac1751b8399f1e819f1d83ee4cf764128bc604d454abee29114dabf49',
symbol: 'ETH',
timestamp: '2022-04-18T09:23:45.000000Z',
value: '4',
},
],
next_page_params: {
items_count: 50,
index: 1,
},
};
import type { ZkEvmL2TxnBatchesResponse } from 'types/api/zkEvmL2';
export const txnBatchesData: ZkEvmL2TxnBatchesResponse = {
items: [
{
timestamp: '2023-06-01T14:46:48.000000Z',
status: 'Finalized',
verify_tx_hash: '0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8',
sequence_tx_hash: '0x6aa081e8e33a085e4ec7124fcd8a5f7d36aac0828f176e80d4b70e313a11695b',
number: 5218590,
tx_count: 9,
},
{
timestamp: '2023-06-01T14:46:48.000000Z',
status: 'Unfinalized',
verify_tx_hash: null,
sequence_tx_hash: null,
number: 5218591,
tx_count: 9,
},
],
next_page_params: {
number: 5902834,
items_count: 50,
},
};
...@@ -16,6 +16,10 @@ export function walletConnect(): CspDev.DirectiveDescriptor { ...@@ -16,6 +16,10 @@ export function walletConnect(): CspDev.DirectiveDescriptor {
'wss://relay.walletconnect.com', 'wss://relay.walletconnect.com',
'wss://www.walletlink.org', 'wss://www.walletlink.org',
], ],
'frame-ancestors': [
'*.walletconnect.org',
'*.walletconnect.com',
],
'img-src': [ 'img-src': [
KEY_WORDS.BLOB, KEY_WORDS.BLOB,
'*.walletconnect.com', '*.walletconnect.com',
......
...@@ -67,7 +67,7 @@ export const verifiedAddresses: GetServerSideProps<Props> = async(context) => { ...@@ -67,7 +67,7 @@ export const verifiedAddresses: GetServerSideProps<Props> = async(context) => {
}; };
export const deposits: GetServerSideProps<Props> = async(context) => { export const deposits: GetServerSideProps<Props> = async(context) => {
if (!(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium'))) { if (!(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium' || rollupFeature.type === 'zkEvm'))) {
return { return {
notFound: true, notFound: true,
}; };
...@@ -79,7 +79,7 @@ export const deposits: GetServerSideProps<Props> = async(context) => { ...@@ -79,7 +79,7 @@ export const deposits: GetServerSideProps<Props> = async(context) => {
export const withdrawals: GetServerSideProps<Props> = async(context) => { export const withdrawals: GetServerSideProps<Props> = async(context) => {
if ( if (
!config.features.beaconChain.isEnabled && !config.features.beaconChain.isEnabled &&
!(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium')) !(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium' || rollupFeature.type === 'zkEvm'))
) { ) {
return { return {
notFound: true, notFound: true,
......
...@@ -16,7 +16,11 @@ const Deposits = dynamic(() => { ...@@ -16,7 +16,11 @@ const Deposits = dynamic(() => {
return import('ui/pages/ShibariumDeposits'); return import('ui/pages/ShibariumDeposits');
} }
throw new Error('Withdrawals feature is not enabled.'); if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') {
return import('ui/pages/ZkEvmL2Deposits');
}
throw new Error('Deposits feature is not enabled.');
}, { ssr: false }); }, { ssr: false });
const Page: NextPage = () => { const Page: NextPage = () => {
......
import type { GetServerSideProps, NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
...@@ -11,8 +10,7 @@ import fetchApi from 'nextjs/utils/fetchApi'; ...@@ -11,8 +10,7 @@ import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app'; import config from 'configs/app';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import Token from 'ui/pages/Token';
const Token = dynamic(() => import('ui/pages/Token'), { ssr: false });
const pathname: Route['pathname'] = '/token/[hash]'; const pathname: Route['pathname'] = '/token/[hash]';
...@@ -29,19 +27,18 @@ export default Page; ...@@ -29,19 +27,18 @@ export default Page;
export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => { export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
const baseResponse = await gSSP.base<typeof pathname>(ctx); const baseResponse = await gSSP.base<typeof pathname>(ctx);
if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) { if ('props' in baseResponse) {
const botInfo = detectBotRequest(ctx.req); if (
config.meta.seo.enhancedDataEnabled ||
if (botInfo?.type === 'social_preview') { (config.meta.og.enhancedDataEnabled && detectBotRequest(ctx.req)?.type === 'social_preview')
) {
const tokenData = await fetchApi({ const tokenData = await fetchApi({
resource: 'token', resource: 'token',
pathParams: { hash: getQueryParamString(ctx.query.hash) }, pathParams: { hash: getQueryParamString(ctx.query.hash) },
timeout: 1_000, timeout: 500,
}); });
(await baseResponse.props).apiData = tokenData && tokenData.symbol ? { (await baseResponse.props).apiData = tokenData ?? null;
symbol: tokenData.symbol,
} : null;
} }
} }
......
...@@ -17,6 +17,10 @@ const Withdrawals = dynamic(() => { ...@@ -17,6 +17,10 @@ const Withdrawals = dynamic(() => {
return import('ui/pages/ShibariumWithdrawals'); return import('ui/pages/ShibariumWithdrawals');
} }
if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') {
return import('ui/pages/ZkEvmL2Withdrawals');
}
if (beaconChainFeature.isEnabled) { if (beaconChainFeature.isEnabled) {
return import('ui/pages/BeaconChainWithdrawals'); return import('ui/pages/BeaconChainWithdrawals');
} }
......
...@@ -2,14 +2,16 @@ import { ChakraProvider } from '@chakra-ui/react'; ...@@ -2,14 +2,16 @@ import { ChakraProvider } from '@chakra-ui/react';
import { GrowthBookProvider } from '@growthbook/growthbook-react'; import { GrowthBookProvider } from '@growthbook/growthbook-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { WagmiProvider } from 'wagmi'; import { http } from 'viem';
import { WagmiProvider, createConfig } from 'wagmi';
import { sepolia } from 'wagmi/chains';
import { mock } from 'wagmi/connectors';
import type { Props as PageProps } from 'nextjs/getServerSideProps'; import type { Props as PageProps } from 'nextjs/getServerSideProps';
import config from 'configs/app'; import config from 'configs/app';
import { AppContextProvider } from 'lib/contexts/app'; import { AppContextProvider } from 'lib/contexts/app';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import wagmiConfig from 'lib/web3/wagmiConfig';
import * as app from 'playwright/utils/app'; import * as app from 'playwright/utils/app';
import theme from 'theme'; import theme from 'theme';
...@@ -31,6 +33,20 @@ const defaultAppContext = { ...@@ -31,6 +33,20 @@ const defaultAppContext = {
}, },
}; };
const wagmiConfig = createConfig({
chains: [ sepolia ],
connectors: [
mock({
accounts: [
'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
],
}),
],
transports: {
[sepolia.id]: http(),
},
});
const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => { const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({ const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
...@@ -47,7 +63,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props ...@@ -47,7 +63,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ app.socketPort }` : undefined }> <SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ app.socketPort }` : undefined }>
<AppContextProvider { ...appContext }> <AppContextProvider { ...appContext }>
<GrowthBookProvider> <GrowthBookProvider>
<WagmiProvider config={ wagmiConfig! }> <WagmiProvider config={ wagmiConfig }>
{ children } { children }
</WagmiProvider> </WagmiProvider>
</GrowthBookProvider> </GrowthBookProvider>
......
...@@ -20,6 +20,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -20,6 +20,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
[ 'NEXT_PUBLIC_ROLLUP_TYPE', 'shibarium' ], [ 'NEXT_PUBLIC_ROLLUP_TYPE', 'shibarium' ],
[ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ], [ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ],
], ],
zkEvmRollup: [
[ 'NEXT_PUBLIC_ROLLUP_TYPE', 'zkEvm' ],
[ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ],
],
bridgedTokens: [ bridgedTokens: [
[ 'NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', '[{"id":"1","title":"Ethereum","short_title":"ETH","base_url":"https://eth.blockscout.com/token/"},{"id":"56","title":"Binance Smart Chain","short_title":"BSC","base_url":"https://bscscan.com/token/"},{"id":"99","title":"POA","short_title":"POA","base_url":"https://blockscout.com/poa/core/token/"}]' ], [ 'NEXT_PUBLIC_BRIDGED_TOKENS_CHAINS', '[{"id":"1","title":"Ethereum","short_title":"ETH","base_url":"https://eth.blockscout.com/token/"},{"id":"56","title":"Binance Smart Chain","short_title":"BSC","base_url":"https://bscscan.com/token/"},{"id":"99","title":"POA","short_title":"POA","base_url":"https://blockscout.com/poa/core/token/"}]' ],
[ 'NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES', '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]' ], [ 'NEXT_PUBLIC_BRIDGED_TOKENS_BRIDGES', '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]' ],
...@@ -27,4 +31,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -27,4 +31,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
userOps: [ userOps: [
[ 'NEXT_PUBLIC_HAS_USER_OPS', 'true' ], [ 'NEXT_PUBLIC_HAS_USER_OPS', 'true' ],
], ],
hasContractAuditReports: [
[ 'NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS', 'true' ],
],
blockHiddenFields: [
[ 'NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS', '["burnt_fees", "total_reward", "nonce"]' ],
],
}; };
import type { TestFixture, Page } from '@playwright/test';
import * as textAdMock from 'mocks/ad/textAd';
export type MockTextAdFixture = () => Promise<void>;
const fixture: TestFixture<MockTextAdFixture, { page: Page }> = async({ page }, use) => {
await use(async() => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: JSON.stringify(textAdMock.duck),
}));
await page.route(textAdMock.duck.ad.thumbnail, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
});
});
});
};
export default fixture;
/* eslint-disable no-console */ /* eslint-disable no-console */
import { test as base } from '@playwright/experimental-ct-react'; import { test as base } from '@playwright/experimental-ct-react';
import * as textAdMock from 'mocks/ad/textAd';
import * as injectMetaMaskProvider from './fixtures/injectMetaMaskProvider'; import * as injectMetaMaskProvider from './fixtures/injectMetaMaskProvider';
import * as mockApiResponse from './fixtures/mockApiResponse'; import * as mockApiResponse from './fixtures/mockApiResponse';
import * as mockAssetResponse from './fixtures/mockAssetResponse'; import * as mockAssetResponse from './fixtures/mockAssetResponse';
import * as mockConfigResponse from './fixtures/mockConfigResponse'; import * as mockConfigResponse from './fixtures/mockConfigResponse';
import * as mockEnvs from './fixtures/mockEnvs'; import * as mockEnvs from './fixtures/mockEnvs';
import * as mockFeatures from './fixtures/mockFeatures'; import * as mockFeatures from './fixtures/mockFeatures';
import * as mockTextAd from './fixtures/mockTextAd';
import * as render from './fixtures/render'; import * as render from './fixtures/render';
import * as socketServer from './fixtures/socketServer'; import * as socketServer from './fixtures/socketServer';
...@@ -21,6 +20,7 @@ interface Fixtures { ...@@ -21,6 +20,7 @@ interface Fixtures {
mockFeatures: mockFeatures.MockFeaturesFixture; mockFeatures: mockFeatures.MockFeaturesFixture;
createSocket: socketServer.CreateSocketFixture; createSocket: socketServer.CreateSocketFixture;
injectMetaMaskProvider: injectMetaMaskProvider.InjectMetaMaskProvider; injectMetaMaskProvider: injectMetaMaskProvider.InjectMetaMaskProvider;
mockTextAd: mockTextAd.MockTextAdFixture;
} }
const test = base.extend<Fixtures>({ const test = base.extend<Fixtures>({
...@@ -30,11 +30,15 @@ const test = base.extend<Fixtures>({ ...@@ -30,11 +30,15 @@ const test = base.extend<Fixtures>({
mockConfigResponse: mockConfigResponse.default, mockConfigResponse: mockConfigResponse.default,
mockEnvs: mockEnvs.default, mockEnvs: mockEnvs.default,
mockFeatures: mockFeatures.default, mockFeatures: mockFeatures.default,
// FIXME: for some reason Playwright does not intercept requests to text ad provider when running multiple tests in parallel
// even if we have a global request interceptor (maybe it is related to service worker issue, maybe not)
// so we have to inject mockTextAd fixture in each test and mock the response where it is needed
mockTextAd: mockTextAd.default,
createSocket: socketServer.createSocket, createSocket: socketServer.createSocket,
injectMetaMaskProvider: injectMetaMaskProvider.default, injectMetaMaskProvider: injectMetaMaskProvider.default,
}); });
test.beforeEach(async({ page }) => { test.beforeEach(async({ page, mockTextAd }) => {
// debug // debug
const isDebug = process.env.PWDEBUG === '1'; const isDebug = process.env.PWDEBUG === '1';
...@@ -56,16 +60,7 @@ test.beforeEach(async({ page }) => { ...@@ -56,16 +60,7 @@ test.beforeEach(async({ page }) => {
// with few exceptions: // with few exceptions:
// 1. mock text AD requests // 1. mock text AD requests
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ await mockTextAd();
status: 200,
body: JSON.stringify(textAdMock.duck),
}));
await page.route(textAdMock.duck.ad.thumbnail, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
});
});
}); });
export * from '@playwright/experimental-ct-react'; export * from '@playwright/experimental-ct-react';
......
...@@ -47,12 +47,6 @@ export const viewsEnvs = { ...@@ -47,12 +47,6 @@ export const viewsEnvs = {
}, },
}; };
export const UIEnvs = {
hasContractAuditReports: [
{ name: 'NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS', value: 'true' },
],
};
export const stabilityEnvs = [ export const stabilityEnvs = [
{ name: 'NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS', value: '["top_accounts"]' }, { name: 'NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS', value: '["top_accounts"]' },
{ name: 'NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS', value: '["value","fee_currency","gas_price","gas_fees","burnt_fees"]' }, { name: 'NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS', value: '["value","fee_currency","gas_price","gas_fees","burnt_fees"]' },
......
...@@ -19,22 +19,16 @@ export const ADDRESS_INFO: Address = { ...@@ -19,22 +19,16 @@ export const ADDRESS_INFO: Address = {
creation_tx_hash: null, creation_tx_hash: null,
creator_address_hash: ADDRESS_HASH, creator_address_hash: ADDRESS_HASH,
exchange_rate: null, exchange_rate: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false, has_decompiled_code: false,
has_logs: true, 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_token_transfers: false,
has_tokens: false, has_tokens: false,
has_validated_blocks: false, has_validated_blocks: false,
hash: ADDRESS_HASH, hash: ADDRESS_HASH,
implementation_address: null, implementation_address: null,
implementation_name: null, implementation_name: null,
is_contract: false, is_contract: true,
is_verified: false, is_verified: true,
name: 'ChainLink Token (goerli)', name: 'ChainLink Token (goerli)',
token: TOKEN_INFO_ERC_20, token: TOKEN_INFO_ERC_20,
private_tags: [], private_tags: [],
......
...@@ -7,6 +7,12 @@ export const CONTRACT_CODE_UNVERIFIED = { ...@@ -7,6 +7,12 @@ export const CONTRACT_CODE_UNVERIFIED = {
creation_bytecode: '0x60806040526e', creation_bytecode: '0x60806040526e',
deployed_bytecode: '0x608060405233', deployed_bytecode: '0x608060405233',
is_self_destructed: false, is_self_destructed: false,
has_methods_read: true,
has_methods_read_proxy: true,
has_methods_write: true,
has_methods_write_proxy: true,
has_custom_methods_read: true,
has_custom_methods_write: true,
} as SmartContract; } as SmartContract;
export const CONTRACT_CODE_VERIFIED = { export const CONTRACT_CODE_VERIFIED = {
...@@ -41,6 +47,12 @@ export const CONTRACT_CODE_VERIFIED = { ...@@ -41,6 +47,12 @@ export const CONTRACT_CODE_VERIFIED = {
source_code: 'source_code', source_code: 'source_code',
verified_at: '2023-02-21T14:39:16.906760Z', verified_at: '2023-02-21T14:39:16.906760Z',
license_type: 'mit', license_type: 'mit',
has_methods_read: true,
has_methods_read_proxy: true,
has_methods_write: true,
has_methods_write_proxy: true,
has_custom_methods_read: true,
has_custom_methods_write: true,
} as unknown as SmartContract; } as unknown as SmartContract;
export const VERIFIED_CONTRACT_INFO: VerifiedContract = { export const VERIFIED_CONTRACT_INFO: VerifiedContract = {
......
import type { ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem } from 'types/api/zkEvmL2'; import type { ZkEvmL2DepositsItem, ZkEvmL2TxnBatch, ZkEvmL2TxnBatchesItem, ZkEvmL2WithdrawalsItem } from 'types/api/zkEvmL2';
import { TX_HASH } from './tx'; import { TX_HASH } from './tx';
export const ZKEVM_DEPOSITS_ITEM: ZkEvmL2DepositsItem = {
block_number: 19674901,
index: 181920,
l1_transaction_hash: '0xa74edfa5824a07a5f95ca1145140ed589df7f05bb17796bf18090b14c4566b5d',
l2_transaction_hash: '0x436d1c7ada270466ca0facdb96ecc22934d68d13b8a08f541b8df11b222967b5',
symbol: 'ETH',
timestamp: '2023-06-01T14:46:48.000000Z',
value: '0.13040262',
};
export const ZKEVM_WITHDRAWALS_ITEM: ZkEvmL2WithdrawalsItem = {
block_number: 11692968,
index: 47003,
l1_transaction_hash: '0x230cf46dabea287ac7d0ba83b8ea120bb83c1de58a81d34f44788f0459096c52',
l2_transaction_hash: '0x519d9f025ec47f08a48d708964d177189d2246ddf988686c481f5debcf097e34',
symbol: 'ETH',
timestamp: '2024-04-17T08:51:58.000000Z',
value: '110.35',
};
export const ZKEVM_L2_TXN_BATCHES_ITEM: ZkEvmL2TxnBatchesItem = { export const ZKEVM_L2_TXN_BATCHES_ITEM: ZkEvmL2TxnBatchesItem = {
timestamp: '2023-06-01T14:46:48.000000Z', timestamp: '2023-06-01T14:46:48.000000Z',
status: 'Finalized', status: 'Finalized',
......
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { mode, transparentize } from '@chakra-ui/theme-tools'; import { mode } from '@chakra-ui/theme-tools';
const baseStyle = defineStyle({ const baseStyle = defineStyle({
fontSize: 'xs', fontSize: 'xs',
...@@ -8,19 +8,25 @@ const baseStyle = defineStyle({ ...@@ -8,19 +8,25 @@ const baseStyle = defineStyle({
}); });
const variantSubtle = defineStyle((props) => { const variantSubtle = defineStyle((props) => {
const { colorScheme: c, theme } = props; const { colorScheme: c } = props;
const darkBg = transparentize(`${ c }.200`, 0.16)(theme);
if (c === 'gray') { if (c === 'gray') {
return { return {
bg: mode('blackAlpha.100', 'whiteAlpha.400')(props), bg: mode('blackAlpha.50', 'whiteAlpha.100')(props),
color: mode('gray.600', 'gray.50')(props), color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
}; };
} }
if (c === 'gray-blue') {
return { return {
bg: mode(`${ c }.50`, darkBg)(props), bg: mode('gray.100', 'gray.800')(props),
color: mode(`${ c }.500`, `${ c }.200`)(props), color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
};
}
return {
bg: mode(`${ c }.50`, `${ c }.800`)(props),
color: mode(`${ c }.500`, `${ c }.100`)(props),
}; };
}); });
......
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
[ 'blue', 'gray', 'orange', 'green', 'purple', 'cyan', 'teal' ].forEach((colorScheme) => { [ 'blue', 'gray', 'gray-blue', 'orange', 'green', 'purple', 'cyan', 'teal' ].forEach((colorScheme) => {
test(`${ colorScheme } color scheme +@dark-mode`, async({ mount }) => { test(`${ colorScheme } color scheme +@dark-mode`, async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
......
...@@ -18,7 +18,7 @@ const variants = { ...@@ -18,7 +18,7 @@ const variants = {
}; };
const sizes = { const sizes = {
md: definePartsStyle({ sm: definePartsStyle({
container: { container: {
minH: 6, minH: 6,
minW: 6, minW: 6,
...@@ -48,7 +48,7 @@ const Tag = defineMultiStyleConfig({ ...@@ -48,7 +48,7 @@ const Tag = defineMultiStyleConfig({
variants, variants,
sizes, sizes,
defaultProps: { defaultProps: {
size: 'md', size: 'sm',
variant: 'subtle', variant: 'subtle',
colorScheme: 'gray', colorScheme: 'gray',
}, },
......
...@@ -15,14 +15,8 @@ export interface Address extends UserTags { ...@@ -15,14 +15,8 @@ export interface Address extends UserTags {
ens_domain_name: string | null; ens_domain_name: string | null;
// TODO: if we are happy with tabs-counters method, should we delete has_something fields? // TODO: if we are happy with tabs-counters method, should we delete has_something fields?
has_beacon_chain_withdrawals?: boolean; has_beacon_chain_withdrawals?: boolean;
has_custom_methods_read: boolean;
has_custom_methods_write: boolean;
has_decompiled_code: boolean; has_decompiled_code: boolean;
has_logs: boolean; has_logs: boolean;
has_methods_read: boolean;
has_methods_read_proxy: boolean;
has_methods_write: boolean;
has_methods_write_proxy: boolean;
has_token_transfers: boolean; has_token_transfers: boolean;
has_tokens: boolean; has_tokens: boolean;
has_validated_blocks: boolean; has_validated_blocks: boolean;
......
export interface AddressMetadataInfo {
addresses: Record<string, {
tags: Array<AddressMetadataTag>;
reputation: number | null;
}>;
}
export type AddressMetadataTagType = 'name' | 'generic' | 'classifier' | 'information' | 'note' | 'protocol';
export interface AddressMetadataTag {
slug: string;
name: string;
tagType: AddressMetadataTagType;
ordinal: number;
meta: string | null;
}
...@@ -33,6 +33,14 @@ export interface SmartContract { ...@@ -33,6 +33,14 @@ export interface SmartContract {
is_verified: boolean | null; is_verified: boolean | null;
is_verified_via_eth_bytecode_db: boolean | null; is_verified_via_eth_bytecode_db: boolean | null;
is_changed_bytecode: boolean | null; is_changed_bytecode: boolean | null;
has_methods_read: boolean;
has_methods_read_proxy: boolean;
has_methods_write: boolean;
has_methods_write_proxy: boolean;
has_custom_methods_read: boolean;
has_custom_methods_write: boolean;
// sourcify info >>> // sourcify info >>>
is_verified_via_sourcify: boolean | null; is_verified_via_sourcify: boolean | null;
is_fully_verified: boolean | null; is_fully_verified: boolean | null;
......
import type { Transaction } from './transaction'; import type { Transaction } from './transaction';
export type ZkEvmL2DepositsItem = {
block_number: number;
index: number;
l1_transaction_hash: string;
l2_transaction_hash: string | null;
timestamp: string;
value: string;
symbol: string;
}
export type ZkEvmL2DepositsResponse = {
items: Array<ZkEvmL2DepositsItem>;
next_page_params: {
items_count: number;
index: number;
};
}
export type ZkEvmL2WithdrawalsItem = {
block_number: number;
index: number;
l1_transaction_hash: string | null;
l2_transaction_hash: string;
timestamp: string;
value: string;
symbol: string;
}
export type ZkEvmL2WithdrawalsResponse = {
items: Array<ZkEvmL2WithdrawalsItem>;
next_page_params: {
items_count: number;
index: number;
};
}
export type ZkEvmL2TxnBatchesItem = { export type ZkEvmL2TxnBatchesItem = {
number: number; number: number;
verify_tx_hash: string | null; verify_tx_hash: string | null;
......
...@@ -8,4 +8,8 @@ export type CsvExportParams = { ...@@ -8,4 +8,8 @@ export type CsvExportParams = {
type: 'logs'; type: 'logs';
filterType?: 'topic'; filterType?: 'topic';
filterValue?: string; filterValue?: string;
} | {
type: 'holders';
filterType?: undefined;
filterValue?: undefined;
} }
import type { AddressMetadataTagType } from 'types/api/addressMetadata';
export interface AddressMetadataInfoFormatted {
addresses: Record<string, {
tags: Array<AddressMetadataTagFormatted>;
reputation: number | null;
}>;
}
export interface AddressMetadataTagFormatted {
slug: string;
name: string;
tagType: AddressMetadataTagType;
ordinal: number;
meta: {
textColor?: string;
bgColor?: string;
actionURL?: string;
} | null;
}
...@@ -7,24 +7,29 @@ import Web3ModalProvider from 'ui/shared/Web3ModalProvider'; ...@@ -7,24 +7,29 @@ import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
interface Props { interface Props {
tabs: Array<RoutedSubTab>; tabs: Array<RoutedSubTab>;
addressHash?: string; isLoading: boolean;
shouldRender?: boolean;
} }
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
columnGap: 3, columnGap: 3,
}; };
const AddressContract = ({ tabs }: Props) => { const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => {
const fallback = React.useCallback(() => { const fallback = React.useCallback(() => {
const noProviderTabs = tabs.filter(({ id }) => id === 'contact_code' || id.startsWith('read_')); const noProviderTabs = tabs.filter(({ id }) => id === 'contract_code' || id.startsWith('read_'));
return ( return (
<RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/> <RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
); );
}, [ tabs ]); }, [ isLoading, tabs ]);
if (!shouldRender) {
return null;
}
return ( return (
<Web3ModalProvider fallback={ fallback }> <Web3ModalProvider fallback={ fallback }>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/> <RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
</Web3ModalProvider> </Web3ModalProvider>
); );
}; };
......
This diff is collapsed.
import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box, useColorModeValue } from '@chakra-ui/react'; import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box, useColorModeValue } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import type { Channel } from 'phoenix';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Address as AddressInfo } from 'types/api/address'; import type { Address as AddressInfo } from 'types/api/address';
import type { SmartContract } from 'types/api/contract';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import type { ResourceError } from 'lib/api/resources';
import { getResourceKey } from 'lib/api/useApiQuery';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import * as stubs from 'stubs/contract';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
...@@ -26,8 +28,8 @@ import ContractSourceCode from './ContractSourceCode'; ...@@ -26,8 +28,8 @@ import ContractSourceCode from './ContractSourceCode';
type Props = { type Props = {
addressHash?: string; addressHash?: string;
// prop for pw tests only contractQuery: UseQueryResult<SmartContract, ResourceError<unknown>>;
noSocket?: boolean; channel: Channel | undefined;
} }
type InfoItemProps = { type InfoItemProps = {
...@@ -57,21 +59,13 @@ const InfoItem = chakra(({ label, content, hint, className, isLoading }: InfoIte ...@@ -57,21 +59,13 @@ const InfoItem = chakra(({ label, content, hint, className, isLoading }: InfoIte
</GridItem> </GridItem>
)); ));
const ContractCode = ({ addressHash, noSocket }: Props) => { const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false);
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>(); const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } })); const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
const { data, isPlaceholderData, isError } = useApiQuery('contract', { const { data, isPlaceholderData, isError } = contractQuery;
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash) && (noSocket || isQueryEnabled),
refetchOnMount: false,
placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
},
});
const handleChangedBytecodeMessage: SocketMessage.AddressChangedBytecode['handler'] = React.useCallback(() => { const handleChangedBytecodeMessage: SocketMessage.AddressChangedBytecode['handler'] = React.useCallback(() => {
setIsChangedBytecodeSocket(true); setIsChangedBytecodeSocket(true);
...@@ -86,14 +80,6 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -86,14 +80,6 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
}); });
}, [ addressHash, queryClient ]); }, [ addressHash, queryClient ]);
const enableQuery = React.useCallback(() => setIsQueryEnabled(true), []);
const channel = useSocketChannel({
topic: `addresses:${ addressHash?.toLowerCase() }`,
isDisabled: !addressHash,
onJoin: enableQuery,
onSocketError: enableQuery,
});
useSocketMessage({ useSocketMessage({
channel, channel,
event: 'changed_bytecode', event: 'changed_bytecode',
......
...@@ -19,7 +19,11 @@ import ContractReadResult from './ContractReadResult'; ...@@ -19,7 +19,11 @@ import ContractReadResult from './ContractReadResult';
import ContractMethodForm from './methodForm/ContractMethodForm'; import ContractMethodForm from './methodForm/ContractMethodForm';
import useWatchAccount from './useWatchAccount'; import useWatchAccount from './useWatchAccount';
const ContractRead = () => { interface Props {
isLoading?: boolean;
}
const ContractRead = ({ isLoading }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const account = useWatchAccount(); const account = useWatchAccount();
const router = useRouter(); const router = useRouter();
...@@ -36,7 +40,7 @@ const ContractRead = () => { ...@@ -36,7 +40,7 @@ const ContractRead = () => {
from: account?.address, from: account?.address,
}, },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash), enabled: !isLoading,
}, },
}); });
......
...@@ -19,7 +19,11 @@ import ContractMethodForm from './methodForm/ContractMethodForm'; ...@@ -19,7 +19,11 @@ import ContractMethodForm from './methodForm/ContractMethodForm';
import useContractAbi from './useContractAbi'; import useContractAbi from './useContractAbi';
import { getNativeCoinValue, prepareAbi } from './utils'; import { getNativeCoinValue, prepareAbi } from './utils';
const ContractWrite = () => { interface Props {
isLoading?: boolean;
}
const ContractWrite = ({ isLoading }: Props) => {
const { data: walletClient } = useWalletClient(); const { data: walletClient } = useWalletClient();
const { isConnected, chainId } = useAccount(); const { isConnected, chainId } = useAccount();
const { switchChainAsync } = useSwitchChain(); const { switchChainAsync } = useSwitchChain();
...@@ -37,7 +41,7 @@ const ContractWrite = () => { ...@@ -37,7 +41,7 @@ const ContractWrite = () => {
is_custom_abi: isCustomAbi ? 'true' : 'false', is_custom_abi: isCustomAbi ? 'true' : 'false',
}, },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash), enabled: !isLoading,
refetchOnMount: false, refetchOnMount: false,
}, },
}); });
......
import { useRouter } from 'next/router';
import useApiQuery from 'lib/api/useApiQuery';
import useContractTabs from 'lib/hooks/useContractTabs';
import getQueryParamString from 'lib/router/getQueryParamString';
const ContractCode = () => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const addressQuery = useApiQuery('address', { pathParams: { hash } });
const { tabs } = useContractTabs(addressQuery.data, false);
const content = tabs.find(({ id }) => id === 'contract_code')?.component;
return content ?? null;
};
export default ContractCode;
...@@ -39,7 +39,7 @@ export default function useContractAbi({ addressHash, isProxy, isCustomAbi }: Pa ...@@ -39,7 +39,7 @@ export default function useContractAbi({ addressHash, isProxy, isCustomAbi }: Pa
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryParams: { is_custom_abi: 'true' }, queryParams: { is_custom_abi: 'true' },
queryOptions: { queryOptions: {
enabled: Boolean(addressInfo?.has_custom_methods_write), enabled: Boolean(contractInfo?.has_custom_methods_write),
refetchOnMount: false, refetchOnMount: false,
}, },
}); });
......
...@@ -74,14 +74,8 @@ export default function useAddressQuery({ hash }: Params): AddressQuery { ...@@ -74,14 +74,8 @@ export default function useAddressQuery({ hash }: Params): AddressQuery {
creation_tx_hash: null, creation_tx_hash: null,
exchange_rate: null, exchange_rate: null,
ens_domain_name: null, ens_domain_name: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false, has_decompiled_code: false,
has_logs: false, has_logs: false,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false, has_token_transfers: false,
has_tokens: false, has_tokens: false,
has_validated_blocks: false, has_validated_blocks: false,
......
...@@ -21,9 +21,10 @@ interface Props { ...@@ -21,9 +21,10 @@ interface Props {
filterType?: CsvExportParams['filterType'] | null; filterType?: CsvExportParams['filterType'] | null;
filterValue?: CsvExportParams['filterValue'] | null; filterValue?: CsvExportParams['filterValue'] | null;
fileNameTemplate: string; fileNameTemplate: string;
exportType: CsvExportParams['type'] | undefined;
} }
const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTemplate }: Props) => { const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTemplate, exportType }: Props) => {
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: { defaultValues: {
...@@ -36,10 +37,10 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -36,10 +37,10 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
try { try {
const url = buildUrl(resource, undefined, { const url = buildUrl(resource, { hash } as never, {
address_id: hash, address_id: hash,
from_period: data.from, from_period: exportType !== 'holders' ? data.from : null,
to_period: data.to, to_period: exportType !== 'holders' ? data.to : null,
filter_type: filterType, filter_type: filterType,
filter_value: filterValue, filter_value: filterValue,
recaptcha_response: data.reCaptcha, recaptcha_response: data.reCaptcha,
...@@ -56,11 +57,11 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -56,11 +57,11 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
} }
const blob = await response.blob(); const blob = await response.blob();
downloadBlob( const fileName = exportType === 'holders' ?
blob, `${ fileNameTemplate }_${ hash }.csv` :
`${ fileNameTemplate }_${ hash }_${ data.from }_${ data.to } // eslint-disable-next-line max-len
${ filterType && filterValue ? '_with_filter_type_' + filterType + '_value_' + filterValue : '' }.csv`, `${ fileNameTemplate }_${ hash }_${ data.from }_${ data.to }${ filterType && filterValue ? '_with_filter_type_' + filterType + '_value_' + filterValue : '' }.csv`;
); downloadBlob(blob, fileName);
} catch (error) { } catch (error) {
toast({ toast({
...@@ -73,7 +74,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -73,7 +74,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
}); });
} }
}, [ fileNameTemplate, hash, resource, filterType, filterValue, toast ]); }, [ resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]);
return ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
...@@ -82,8 +83,8 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -82,8 +83,8 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
onSubmit={ handleSubmit(onFormSubmit) } onSubmit={ handleSubmit(onFormSubmit) }
> >
<Flex columnGap={ 5 } rowGap={ 3 } flexDir={{ base: 'column', lg: 'row' }} alignItems={{ base: 'flex-start', lg: 'center' }} flexWrap="wrap"> <Flex columnGap={ 5 } rowGap={ 3 } flexDir={{ base: 'column', lg: 'row' }} alignItems={{ base: 'flex-start', lg: 'center' }} flexWrap="wrap">
<CsvExportFormField name="from" formApi={ formApi }/> { exportType !== 'holders' && <CsvExportFormField name="from" formApi={ formApi }/> }
<CsvExportFormField name="to" formApi={ formApi }/> { exportType !== 'holders' && <CsvExportFormField name="to" formApi={ formApi }/> }
<CsvExportFormReCaptcha formApi={ formApi }/> <CsvExportFormReCaptcha formApi={ formApi }/>
</Flex> </Flex>
<Button <Button
......
import { Skeleton, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { ZkEvmL2DepositsItem } from 'types/api/zkEvmL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const rollupFeature = config.features.rollup;
type Props = { item: ZkEvmL2DepositsItem; isLoading?: boolean };
const ZkEvmL2DepositsListItem = ({ item, isLoading }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkEvm') {
return null;
}
const timeAgo = dayjs(item.timestamp).fromNow();
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntityL1
number={ item.block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Index</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ item.index }
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TxEntityL1
isLoading={ isLoading }
hash={ item.l1_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.l2_transaction_hash ? (
<TxEntity
isLoading={ isLoading }
hash={ item.l2_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
) : (
<chakra.span color="text_secondary">
Pending Claim
</chakra.span>
) }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ BigNumber(item.value).toFormat() }
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Token</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ item.symbol }
</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default ZkEvmL2DepositsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { ZkEvmL2DepositsItem } from 'types/api/zkEvmL2';
import { default as Thead } from 'ui/shared/TheadSticky';
import ZkEvmL2DepositsTableItem from './ZkEvmL2DepositsTableItem';
type Props = {
items: Array<ZkEvmL2DepositsItem>;
top: number;
isLoading?: boolean;
}
const ZkEvmL2DepositsTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
<Th>L1 block</Th>
<Th>Index</Th>
<Th>L1 txn hash</Th>
<Th>Age</Th>
<Th>L2 txn hash</Th>
<Th isNumeric>Value</Th>
<Th>Token</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<ZkEvmL2DepositsTableItem key={ String(item.index) + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
);
};
export default ZkEvmL2DepositsTable;
import { Td, Tr, Skeleton, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { ZkEvmL2DepositsItem } from 'types/api/zkEvmL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
const rollupFeature = config.features.rollup;
type Props = { item: ZkEvmL2DepositsItem; isLoading?: boolean };
const ZkEvmL2DepositsTableItem = ({ item, isLoading }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'zkEvm') {
return null;
}
const timeAgo = dayjs(item.timestamp).fromNow();
return (
<Tr>
<Td verticalAlign="middle">
<BlockEntityL1
number={ item.block_number }
isLoading={ isLoading }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
/>
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading }>
<span>{ item.index }</span>
</Skeleton>
</Td>
<Td verticalAlign="middle">
<TxEntityL1
isLoading={ isLoading }
hash={ item.l1_transaction_hash }
truncation="constant_long"
noIcon
fontSize="sm"
lineHeight={ 5 }
/>
</Td>
<Td verticalAlign="middle" pr={ 12 }>
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>{ timeAgo }</span>
</Skeleton>
</Td>
<Td verticalAlign="middle">
{ item.l2_transaction_hash ? (
<TxEntity
isLoading={ isLoading }
hash={ item.l2_transaction_hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
) : (
<chakra.span color="text_secondary">
Pending Claim
</chakra.span>
) }
</Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<span>{ BigNumber(item.value).toFormat() }</span>
</Skeleton>
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } display="inline-block">
<span>{ item.symbol }</span>
</Skeleton>
</Td>
</Tr>
);
};
export default ZkEvmL2DepositsTableItem;
import { test as base, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { txnBatchesData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatches'; import { txnBatchesData } from 'mocks/zkEvm/txnBatches';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
......
...@@ -72,7 +72,7 @@ const AddressPageContent = () => { ...@@ -72,7 +72,7 @@ const AddressPageContent = () => {
const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined); const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined);
const safeIconColor = useColorModeValue('black', 'white'); const safeIconColor = useColorModeValue('black', 'white');
const contractTabs = useContractTabs(addressQuery.data); const contractTabs = useContractTabs(addressQuery.data, addressQuery.isPlaceholderData);
const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData); const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData);
const isTabsLoading = isLoading || addressTabsCountersQuery.isPlaceholderData; const isTabsLoading = isLoading || addressTabsCountersQuery.isPlaceholderData;
...@@ -163,8 +163,8 @@ const AddressPageContent = () => { ...@@ -163,8 +163,8 @@ const AddressPageContent = () => {
return 'Contract'; return 'Contract';
}, },
component: <AddressContract tabs={ contractTabs }/>, component: <AddressContract tabs={ contractTabs.tabs } shouldRender={ !isTabsLoading } isLoading={ contractTabs.isLoading }/>,
subTabs: contractTabs.map(tab => tab.id), subTabs: contractTabs.tabs.map(tab => tab.id),
} : undefined, } : undefined,
].filter(Boolean); ].filter(Boolean);
}, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data, isTabsLoading ]); }, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data, isTabsLoading ]);
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import * as blockMock from 'mocks/blocks/block'; import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index'; import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import { test, expect, devices } from 'playwright/lib'; import { test, expect, devices } from 'playwright/lib';
import * as configs from 'playwright/utils/configs'; import * as configs from 'playwright/utils/configs';
...@@ -21,6 +22,10 @@ const hooksConfig = { ...@@ -21,6 +22,10 @@ const hooksConfig = {
// test cases which use socket cannot run in parallel since the socket server always run on the same port // test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' }); test.describe.configure({ mode: 'serial' });
test.beforeEach(async({ mockTextAd }) => {
await mockTextAd();
});
test('base view +@dark-mode', async({ render, mockApiResponse }) => { test('base view +@dark-mode', async({ render, mockApiResponse }) => {
await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } }); await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } });
await mockApiResponse('stats', statsMock.base); await mockApiResponse('stats', statsMock.base);
...@@ -30,11 +35,8 @@ test('base view +@dark-mode', async({ render, mockApiResponse }) => { ...@@ -30,11 +35,8 @@ test('base view +@dark-mode', async({ render, mockApiResponse }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
const hiddenFieldsTest = test.extend<{ context: BrowserContext }>({ test('hidden fields', async({ render, mockApiResponse, mockEnvs }) => {
context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields), await mockEnvs(ENVS_MAP.blockHiddenFields);
});
hiddenFieldsTest('hidden fields', async({ render, mockApiResponse }) => {
await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } }); await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } });
await mockApiResponse('stats', statsMock.base); await mockApiResponse('stats', statsMock.base);
......
import { test, expect } from '@playwright/experimental-ct-react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import TestApp from 'playwright/TestApp'; import * as tokenMock from 'mocks/tokens/tokenInfo';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import { test, expect } from 'playwright/lib';
import * as configs from 'playwright/utils/configs'; import * as configs from 'playwright/utils/configs';
import CsvExport from './CsvExport'; import CsvExport from './CsvExport';
const ADDRESS_API_URL = buildApiUrl('address', { hash: addressMock.hash }); test('base view +@mobile +@dark-mode', async({ render, page, mockApiResponse }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { address: addressMock.hash, type: 'transactions', filterType: 'address', filterValue: 'from' }, query: { address: addressMock.hash, type: 'transactions', filterType: 'address', filterValue: 'from' },
isReady: true,
}, },
}; };
await mockApiResponse('address', addressMock.validator, { pathParams: { hash: addressMock.hash } });
test.beforeEach(async({ page }) => { const component = await render(<Box sx={{ '.recaptcha': { w: '304px', h: '78px' } }}><CsvExport/></Box>, { hooksConfig });
await page.route(ADDRESS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.withName),
}));
});
test('base view +@mobile +@dark-mode', async({ mount, page }) => { await expect(component).toHaveScreenshot({
mask: [ page.locator('.recaptcha') ],
maskColor: configs.maskColor,
});
});
const component = await mount( test('token holders', async({ render, page, mockApiResponse }) => {
<TestApp> const hooksConfig = {
<CsvExport/> router: {
</TestApp>, query: { address: addressMock.hash, type: 'holders' },
{ hooksConfig }, },
); };
await mockApiResponse('address', addressMock.token, { pathParams: { hash: addressMock.hash } });
await mockApiResponse('token', tokenMock.tokenInfo, { pathParams: { hash: addressMock.hash } });
await page.waitForResponse('https://www.google.com/recaptcha/api2/**'); const component = await render(<Box sx={{ '.recaptcha': { w: '304px', h: '78px' } }}><CsvExport/></Box>, { hooksConfig });
await expect(component).toHaveScreenshot({ await expect(component).toHaveScreenshot({
mask: [ page.locator('.recaptcha') ], mask: [ page.locator('.recaptcha') ],
......
...@@ -15,6 +15,7 @@ import { nbsp } from 'lib/html-entities'; ...@@ -15,6 +15,7 @@ import { nbsp } from 'lib/html-entities';
import CsvExportForm from 'ui/csvExport/CsvExportForm'; import CsvExportForm from 'ui/csvExport/CsvExportForm';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
interface ExportTypeEntity { interface ExportTypeEntity {
...@@ -53,6 +54,11 @@ const EXPORT_TYPES: Record<CsvExportParams['type'], ExportTypeEntity> = { ...@@ -53,6 +54,11 @@ const EXPORT_TYPES: Record<CsvExportParams['type'], ExportTypeEntity> = {
fileNameTemplate: 'logs', fileNameTemplate: 'logs',
filterType: 'topic', filterType: 'topic',
}, },
holders: {
text: 'holders',
resource: 'csv_export_token_holders',
fileNameTemplate: 'holders',
},
}; };
const isCorrectExportType = (type: string): type is CsvExportParams['type'] => Object.keys(EXPORT_TYPES).includes(type); const isCorrectExportType = (type: string): type is CsvExportParams['type'] => Object.keys(EXPORT_TYPES).includes(type);
...@@ -75,6 +81,15 @@ const CsvExport = () => { ...@@ -75,6 +81,15 @@ const CsvExport = () => {
}, },
}); });
const tokenQuery = useApiQuery('token', {
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash) && exportTypeParam === 'holders',
},
});
const isLoading = addressQuery.isPending || (exportTypeParam === 'holders' && tokenQuery.isPending);
const backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address');
...@@ -111,7 +126,7 @@ const CsvExport = () => { ...@@ -111,7 +126,7 @@ const CsvExport = () => {
const content = (() => { const content = (() => {
throwOnResourceLoadError(addressQuery); throwOnResourceLoadError(addressQuery);
if (addressQuery.isPending) { if (isLoading) {
return <ContentLoader/>; return <ContentLoader/>;
} }
...@@ -119,6 +134,7 @@ const CsvExport = () => { ...@@ -119,6 +134,7 @@ const CsvExport = () => {
<CsvExportForm <CsvExportForm
hash={ addressHash } hash={ addressHash }
resource={ exportType.resource } resource={ exportType.resource }
exportType={ isCorrectExportType(exportTypeParam) ? exportTypeParam : undefined }
filterType={ filterType } filterType={ filterType }
filterValue={ filterValue } filterValue={ filterValue }
fileNameTemplate={ exportType.fileNameTemplate } fileNameTemplate={ exportType.fileNameTemplate }
...@@ -126,16 +142,33 @@ const CsvExport = () => { ...@@ -126,16 +142,33 @@ const CsvExport = () => {
); );
})(); })();
const description = (() => {
if (isLoading) {
return null;
}
if (exportTypeParam === 'holders') {
return ( return (
<> <Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap">
<PageTitle <span>Export { exportType.text } for token </span>
title="Export data to CSV file" <TokenEntity
backLink={ backLink } token={ tokenQuery.data }
truncation={ isMobile ? 'constant' : 'dynamic' }
w="min-content"
noCopy
noSymbol
/> />
<span> to CSV file. </span>
<span>Exports are limited to the last 10K { exportType.text }.</span>
</Flex>
);
}
return (
<Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap"> <Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap">
<span>Export { exportType.text } for address </span> <span>Export { exportType.text } for address </span>
<AddressEntity <AddressEntity
address={{ hash: addressHash, is_contract: true, implementation_name: null }} address={ addressQuery.data }
truncation={ isMobile ? 'constant' : 'dynamic' } truncation={ isMobile ? 'constant' : 'dynamic' }
noCopy noCopy
/> />
...@@ -144,6 +177,16 @@ const CsvExport = () => { ...@@ -144,6 +177,16 @@ const CsvExport = () => {
<span>to CSV file. </span> <span>to CSV file. </span>
<span>Exports are limited to the last 10K { exportType.text }.</span> <span>Exports are limited to the last 10K { exportType.text }.</span>
</Flex> </Flex>
);
})();
return (
<>
<PageTitle
title="Export data to CSV file"
backLink={ backLink }
/>
{ description }
{ content } { content }
</> </>
); );
......
...@@ -9,8 +9,8 @@ import * as configs from 'playwright/utils/configs'; ...@@ -9,8 +9,8 @@ import * as configs from 'playwright/utils/configs';
import OptimisticL2Deposits from './OptimisticL2Deposits'; import OptimisticL2Deposits from './OptimisticL2Deposits';
const DEPOSITS_API_URL = buildApiUrl('l2_deposits'); const DEPOSITS_API_URL = buildApiUrl('optimistic_l2_deposits');
const DEPOSITS_COUNT_API_URL = buildApiUrl('l2_deposits_count'); const DEPOSITS_COUNT_API_URL = buildApiUrl('optimistic_l2_deposits_count');
const test = base.extend({ const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
......
...@@ -14,9 +14,9 @@ import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText'; ...@@ -14,9 +14,9 @@ import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
const OptimisticL2Deposits = () => { const OptimisticL2Deposits = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_deposits', resourceName: 'optimistic_l2_deposits',
options: { options: {
placeholderData: generateListStub<'l2_deposits'>( placeholderData: generateListStub<'optimistic_l2_deposits'>(
L2_DEPOSIT_ITEM, L2_DEPOSIT_ITEM,
50, 50,
{ {
...@@ -30,7 +30,7 @@ const OptimisticL2Deposits = () => { ...@@ -30,7 +30,7 @@ const OptimisticL2Deposits = () => {
}, },
}); });
const countersQuery = useApiQuery('l2_deposits_count', { const countersQuery = useApiQuery('optimistic_l2_deposits_count', {
queryOptions: { queryOptions: {
placeholderData: 1927029, placeholderData: 1927029,
}, },
......
...@@ -14,8 +14,8 @@ const test = base.extend({ ...@@ -14,8 +14,8 @@ const test = base.extend({
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any, context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
}); });
const OUTPUT_ROOTS_API_URL = buildApiUrl('l2_output_roots'); const OUTPUT_ROOTS_API_URL = buildApiUrl('optimistic_l2_output_roots');
const OUTPUT_ROOTS_COUNT_API_URL = buildApiUrl('l2_output_roots_count'); const OUTPUT_ROOTS_COUNT_API_URL = buildApiUrl('optimistic_l2_output_roots_count');
test('base view +@mobile', async({ mount, page }) => { test('base view +@mobile', async({ mount, page }) => {
// test on mobile is flaky // test on mobile is flaky
......
...@@ -13,9 +13,9 @@ import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText'; ...@@ -13,9 +13,9 @@ import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
const OptimisticL2OutputRoots = () => { const OptimisticL2OutputRoots = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_output_roots', resourceName: 'optimistic_l2_output_roots',
options: { options: {
placeholderData: generateListStub<'l2_output_roots'>( placeholderData: generateListStub<'optimistic_l2_output_roots'>(
L2_OUTPUT_ROOTS_ITEM, L2_OUTPUT_ROOTS_ITEM,
50, 50,
{ {
...@@ -28,7 +28,7 @@ const OptimisticL2OutputRoots = () => { ...@@ -28,7 +28,7 @@ const OptimisticL2OutputRoots = () => {
}, },
}); });
const countersQuery = useApiQuery('l2_output_roots_count', { const countersQuery = useApiQuery('optimistic_l2_output_roots_count', {
queryOptions: { queryOptions: {
placeholderData: 50617, placeholderData: 50617,
}, },
......
...@@ -14,8 +14,8 @@ const test = base.extend({ ...@@ -14,8 +14,8 @@ const test = base.extend({
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any, context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
}); });
const TXN_BATCHES_API_URL = buildApiUrl('l2_txn_batches'); const TXN_BATCHES_API_URL = buildApiUrl('optimistic_l2_txn_batches');
const TXN_BATCHES_COUNT_API_URL = buildApiUrl('l2_txn_batches_count'); const TXN_BATCHES_COUNT_API_URL = buildApiUrl('optimistic_l2_txn_batches_count');
test('base view +@mobile', async({ mount, page }) => { test('base view +@mobile', async({ mount, page }) => {
// test on mobile is flaky // test on mobile is flaky
......
...@@ -14,9 +14,9 @@ import OptimisticL2TxnBatchesTable from 'ui/txnBatches/optimisticL2/OptimisticL2 ...@@ -14,9 +14,9 @@ import OptimisticL2TxnBatchesTable from 'ui/txnBatches/optimisticL2/OptimisticL2
const OptimisticL2TxnBatches = () => { const OptimisticL2TxnBatches = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_txn_batches', resourceName: 'optimistic_l2_txn_batches',
options: { options: {
placeholderData: generateListStub<'l2_txn_batches'>( placeholderData: generateListStub<'optimistic_l2_txn_batches'>(
L2_TXN_BATCHES_ITEM, L2_TXN_BATCHES_ITEM,
50, 50,
{ {
...@@ -29,7 +29,7 @@ const OptimisticL2TxnBatches = () => { ...@@ -29,7 +29,7 @@ const OptimisticL2TxnBatches = () => {
}, },
}); });
const countersQuery = useApiQuery('l2_txn_batches_count', { const countersQuery = useApiQuery('optimistic_l2_txn_batches_count', {
queryOptions: { queryOptions: {
placeholderData: 5231746, placeholderData: 5231746,
}, },
......
...@@ -14,8 +14,8 @@ const test = base.extend({ ...@@ -14,8 +14,8 @@ const test = base.extend({
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any, context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
}); });
const WITHDRAWALS_API_URL = buildApiUrl('l2_withdrawals'); const WITHDRAWALS_API_URL = buildApiUrl('optimistic_l2_withdrawals');
const WITHDRAWALS_COUNT_API_URL = buildApiUrl('l2_withdrawals_count'); const WITHDRAWALS_COUNT_API_URL = buildApiUrl('optimistic_l2_withdrawals_count');
test('base view +@mobile', async({ mount, page }) => { test('base view +@mobile', async({ mount, page }) => {
// test on mobile is flaky // test on mobile is flaky
......
...@@ -14,9 +14,9 @@ import OptimisticL2WithdrawalsTable from 'ui/withdrawals/optimisticL2/Optimistic ...@@ -14,9 +14,9 @@ import OptimisticL2WithdrawalsTable from 'ui/withdrawals/optimisticL2/Optimistic
const OptimisticL2Withdrawals = () => { const OptimisticL2Withdrawals = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({ const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_withdrawals', resourceName: 'optimistic_l2_withdrawals',
options: { options: {
placeholderData: generateListStub<'l2_withdrawals'>( placeholderData: generateListStub<'optimistic_l2_withdrawals'>(
L2_WITHDRAWAL_ITEM, L2_WITHDRAWAL_ITEM,
50, 50,
{ {
...@@ -29,7 +29,7 @@ const OptimisticL2Withdrawals = () => { ...@@ -29,7 +29,7 @@ const OptimisticL2Withdrawals = () => {
}, },
}); });
const countersQuery = useApiQuery('l2_withdrawals_count', { const countersQuery = useApiQuery('optimistic_l2_withdrawals_count', {
queryOptions: { queryOptions: {
placeholderData: 23700, placeholderData: 23700,
}, },
......
...@@ -6,7 +6,8 @@ import { test, expect } from 'playwright/lib'; ...@@ -6,7 +6,8 @@ import { test, expect } from 'playwright/lib';
import ShibariumDeposits from './ShibariumDeposits'; import ShibariumDeposits from './ShibariumDeposits';
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs }) => { test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockTextAd();
await mockEnvs(ENVS_MAP.shibariumRollup); await mockEnvs(ENVS_MAP.shibariumRollup);
await mockApiResponse('shibarium_deposits', depositsData); await mockApiResponse('shibarium_deposits', depositsData);
await mockApiResponse('shibarium_deposits_count', 3971111); await mockApiResponse('shibarium_deposits_count', 3971111);
......
...@@ -6,12 +6,13 @@ import { test, expect } from 'playwright/lib'; ...@@ -6,12 +6,13 @@ import { test, expect } from 'playwright/lib';
import ShibariumWithdrawals from './ShibariumWithdrawals'; import ShibariumWithdrawals from './ShibariumWithdrawals';
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs }) => { test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
// test on mobile is flaky // test on mobile is flaky
// my assumption is there is not enough time to calculate hashes truncation so component is unstable // my assumption is there is not enough time to calculate hashes truncation so component is unstable
// so I raised the test timeout to check if it helps // so I raised the test timeout to check if it helps
test.slow(); test.slow();
await mockTextAd();
await mockEnvs(ENVS_MAP.shibariumRollup); await mockEnvs(ENVS_MAP.shibariumRollup);
await mockApiResponse('shibarium_withdrawals', withdrawalsData); await mockApiResponse('shibarium_withdrawals', withdrawalsData);
await mockApiResponse('shibarium_withdrawals_count', 397); await mockApiResponse('shibarium_withdrawals_count', 397);
......
import React from 'react'; import React from 'react';
import config from 'configs/app';
import * as verifiedAddressesMocks from 'mocks/account/verifiedAddresses'; import * as verifiedAddressesMocks from 'mocks/account/verifiedAddresses';
import { token as contract } from 'mocks/address/address'; import { token as contract } from 'mocks/address/address';
import { tokenInfo, tokenCounters, bridgedTokenA } from 'mocks/tokens/tokenInfo'; import { tokenInfo, tokenCounters, bridgedTokenA } from 'mocks/tokens/tokenInfo';
...@@ -10,9 +11,12 @@ import * as configs from 'playwright/utils/configs'; ...@@ -10,9 +11,12 @@ import * as configs from 'playwright/utils/configs';
import Token from './Token'; import Token from './Token';
const hash = tokenInfo.address;
const chainId = config.chain.id;
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: '1', tab: 'token_transfers' }, query: { hash, tab: 'token_transfers' },
isReady: true, isReady: true,
}, },
}; };
...@@ -21,18 +25,19 @@ const hooksConfig = { ...@@ -21,18 +25,19 @@ const hooksConfig = {
// test cases which use socket cannot run in parallel since the socket server always run on the same port // test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' }); test.describe.configure({ mode: 'serial' });
test.beforeEach(async({ mockApiResponse }) => { test.beforeEach(async({ mockApiResponse, mockTextAd }) => {
await mockApiResponse('token', tokenInfo, { pathParams: { hash: '1' } }); await mockApiResponse('token', tokenInfo, { pathParams: { hash } });
await mockApiResponse('address', contract, { pathParams: { hash: '1' } }); await mockApiResponse('address', contract, { pathParams: { hash } });
await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash: '1' } }); await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash } });
await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash: '1' } }); await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash } });
await mockTextAd();
}); });
test('base view', async({ render, page, createSocket }) => { test('base view', async({ render, page, createSocket }) => {
const component = await render(<Token/>, { hooksConfig }, { withSocket: true }); const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'tokens:1'); const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`);
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
await expect(component).toHaveScreenshot({ await expect(component).toHaveScreenshot({
...@@ -42,13 +47,13 @@ test('base view', async({ render, page, createSocket }) => { ...@@ -42,13 +47,13 @@ test('base view', async({ render, page, createSocket }) => {
}); });
test('with verified info', async({ render, page, createSocket, mockApiResponse, mockAssetResponse }) => { test('with verified info', async({ render, page, createSocket, mockApiResponse, mockAssetResponse }) => {
await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId: '1', hash: '1' } }); await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId, hash } });
await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg'); await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg');
const component = await render(<Token/>, { hooksConfig }, { withSocket: true }); const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'tokens:1'); const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`);
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
await page.getByRole('button', { name: /project info/i }).click(); await page.getByRole('button', { name: /project info/i }).click();
...@@ -60,17 +65,24 @@ test('with verified info', async({ render, page, createSocket, mockApiResponse, ...@@ -60,17 +65,24 @@ test('with verified info', async({ render, page, createSocket, mockApiResponse,
}); });
test('bridged token', async({ render, page, createSocket, mockApiResponse, mockAssetResponse, mockEnvs }) => { test('bridged token', async({ render, page, createSocket, mockApiResponse, mockAssetResponse, mockEnvs }) => {
const hash = bridgedTokenA.address;
const hooksConfig = {
router: {
query: { hash, tab: 'token_transfers' },
},
};
await mockEnvs(ENVS_MAP.bridgedTokens); await mockEnvs(ENVS_MAP.bridgedTokens);
await mockApiResponse('token', bridgedTokenA, { pathParams: { hash: '1' } }); await mockApiResponse('token', bridgedTokenA, { pathParams: { hash } });
await mockApiResponse('address', contract, { pathParams: { hash: '1' } }); await mockApiResponse('address', contract, { pathParams: { hash } });
await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash: '1' } }); await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash } });
await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash: '1' } }); await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash } });
await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId: '1', hash: '1' } }); await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId, hash } });
await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg'); await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg');
const component = await render(<Token/>, { hooksConfig }, { withSocket: true }); const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'tokens:1'); const channel = await socketServer.joinChannel(socket, `tokens:${ hash.toLowerCase() }`);
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
await expect(component).toHaveScreenshot({ await expect(component).toHaveScreenshot({
...@@ -85,7 +97,7 @@ test.describe('mobile', () => { ...@@ -85,7 +97,7 @@ test.describe('mobile', () => {
test('base view', async({ render, page, createSocket }) => { test('base view', async({ render, page, createSocket }) => {
const component = await render(<Token/>, { hooksConfig }, { withSocket: true }); const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'tokens:1'); const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`);
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
await expect(component).toHaveScreenshot({ await expect(component).toHaveScreenshot({
...@@ -95,12 +107,12 @@ test.describe('mobile', () => { ...@@ -95,12 +107,12 @@ test.describe('mobile', () => {
}); });
test('with verified info', async({ render, page, createSocket, mockApiResponse, mockAssetResponse }) => { test('with verified info', async({ render, page, createSocket, mockApiResponse, mockAssetResponse }) => {
await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId: '1', hash: '1' } }); await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId, hash } });
await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg'); await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg');
const component = await render(<Token/>, { hooksConfig }, { withSocket: true }); const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'tokens:1'); const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`);
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
await expect(component).toHaveScreenshot({ await expect(component).toHaveScreenshot({
......
This diff is collapsed.
...@@ -7,6 +7,10 @@ import { test, expect } from 'playwright/lib'; ...@@ -7,6 +7,10 @@ import { test, expect } from 'playwright/lib';
import Tokens from './Tokens'; import Tokens from './Tokens';
test.beforeEach(async({ mockTextAd }) => {
await mockTextAd();
});
test('base view +@mobile +@dark-mode', async({ render, mockApiResponse }) => { test('base view +@mobile +@dark-mode', async({ render, mockApiResponse }) => {
const allTokens = { const allTokens = {
items: [ items: [
......
...@@ -7,12 +7,13 @@ import Validators from './Validators'; ...@@ -7,12 +7,13 @@ import Validators from './Validators';
const chainType = 'stability'; const chainType = 'stability';
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs }) => { test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockEnvs([ await mockEnvs([
[ 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', chainType ], [ 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', chainType ],
]); ]);
await mockApiResponse('validators', validatorsMock.validatorsResponse, { pathParams: { chainType } }); await mockApiResponse('validators', validatorsMock.validatorsResponse, { pathParams: { chainType } });
await mockApiResponse('validators_counters', validatorsMock.validatorsCountersResponse, { pathParams: { chainType } }); await mockApiResponse('validators_counters', validatorsMock.validatorsCountersResponse, { pathParams: { chainType } });
await mockTextAd();
const component = await render(<Validators/>); const component = await render(<Validators/>);
......
import React from 'react';
import * as depositsMock from 'mocks/zkEvm/deposits';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import ZkEvmL2Deposits from './ZkEvmL2Deposits';
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockTextAd();
await mockEnvs(ENVS_MAP.zkEvmRollup);
await mockApiResponse('zkevm_l2_deposits', depositsMock.baseResponse);
await mockApiResponse('zkevm_l2_deposits_count', 3971111);
const component = await render(<ZkEvmL2Deposits/>);
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Skeleton } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { rightLineArrow, nbsp } from 'lib/html-entities';
import { generateListStub } from 'stubs/utils';
import { ZKEVM_DEPOSITS_ITEM } from 'stubs/zkEvmL2';
import ZkEvmL2DepositsListItem from 'ui/deposits/zkEvmL2/ZkEvmL2DepositsListItem';
import ZkEvmL2DepositsTable from 'ui/deposits/zkEvmL2/ZkEvmL2DepositsTable';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
const ZkEvmL2Deposits = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'zkevm_l2_deposits',
options: {
placeholderData: generateListStub<'zkevm_l2_deposits'>(
ZKEVM_DEPOSITS_ITEM,
50,
{ next_page_params: { items_count: 50, index: 1 } },
),
},
});
const countersQuery = useApiQuery('zkevm_l2_deposits_count', {
queryOptions: {
placeholderData: 1927029,
},
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<ZkEvmL2DepositsListItem
key={ String(item.index) + (isPlaceholderData ? index : '') }
isLoading={ isPlaceholderData }
item={ item }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<ZkEvmL2DepositsTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
const text = (() => {
if (countersQuery.isError) {
return null;
}
return (
<Skeleton
isLoaded={ !countersQuery.isPlaceholderData }
display="inline-block"
>
A total of { countersQuery.data?.toLocaleString() } deposits found
</Skeleton>
);
})();
const actionBar = <StickyPaginationWithText text={ text } pagination={ pagination }/>;
return (
<>
<PageTitle title={ `Deposits (L1${ nbsp }${ rightLineArrow }${ nbsp }L2)` } withTextAd/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no deposits."
content={ content }
actionBar={ actionBar }
/>
</>
);
};
export default ZkEvmL2Deposits;
import { test as base, expect, devices } from '@playwright/experimental-ct-react'; import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { txnBatchData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatch'; import { txnBatchData } from 'mocks/zkEvm/txnBatches';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
......
import { test as base, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { txnBatchesData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatches'; import { txnBatchesData } from 'mocks/zkEvm/txnBatches';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
......
import React from 'react';
import * as withdrawalsMock from 'mocks/zkEvm/withdrawals';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import ZkEvmL2Withdrawals from './ZkEvmL2Withdrawals';
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockTextAd();
await mockEnvs(ENVS_MAP.zkEvmRollup);
await mockApiResponse('zkevm_l2_withdrawals', withdrawalsMock.baseResponse);
await mockApiResponse('zkevm_l2_withdrawals_count', 3971111);
const component = await render(<ZkEvmL2Withdrawals/>);
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Skeleton } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { rightLineArrow, nbsp } from 'lib/html-entities';
import { generateListStub } from 'stubs/utils';
import { ZKEVM_WITHDRAWALS_ITEM } from 'stubs/zkEvmL2';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
import ZkEvmL2WithdrawalsListItem from 'ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsListItem';
import ZkEvmL2WithdrawalsTable from 'ui/withdrawals/zkEvmL2/ZkEvmL2WithdrawalsTable';
const ZkEvmL2Withdrawals = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'zkevm_l2_withdrawals',
options: {
placeholderData: generateListStub<'zkevm_l2_withdrawals'>(
ZKEVM_WITHDRAWALS_ITEM,
50,
{ next_page_params: { items_count: 50, index: 1 } },
),
},
});
const countersQuery = useApiQuery('zkevm_l2_withdrawals_count', {
queryOptions: {
placeholderData: 1927029,
},
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<ZkEvmL2WithdrawalsListItem
key={ String(item.index) + (isPlaceholderData ? index : '') }
isLoading={ isPlaceholderData }
item={ item }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<ZkEvmL2WithdrawalsTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
const text = (() => {
if (countersQuery.isError) {
return null;
}
return (
<Skeleton
isLoaded={ !countersQuery.isPlaceholderData }
display="inline-block"
>
A total of { countersQuery.data?.toLocaleString() } withdrawals found
</Skeleton>
);
})();
const actionBar = <StickyPaginationWithText text={ text } pagination={ pagination }/>;
return (
<>
<PageTitle title={ `Withdrawals (L2${ nbsp }${ rightLineArrow }${ nbsp }L1)` } withTextAd/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no withdrawals."
content={ content }
actionBar={ actionBar }
/>
</>
);
};
export default ZkEvmL2Withdrawals;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment