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

Merge branch 'main' into action-button

parents 6354b5db fdc8b808
......@@ -10,6 +10,7 @@ on:
type: choice
options:
- none
- base
- gnosis
- eth
- eth_sepolia
......
......@@ -339,6 +339,7 @@
"main",
"main.L2",
"localhost",
"base",
"gnosis",
"eth",
"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 addressVerification } from './addressVerification';
export { default as addressMetadata } from './addressMetadata';
export { default as adsBanner } from './adsBanner';
export { default as adsText } from './adsText';
export { default as beaconChain } from './beaconChain';
......
......@@ -10,6 +10,9 @@ const meta = Object.freeze({
imageUrl: app.baseUrl + (getExternalAssetFilePath('NEXT_PUBLIC_OG_IMAGE_URL') || defaultImageUrl),
enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED') === 'true',
},
seo: {
enhancedDataEnabled: getEnvValue('NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED') === 'true',
},
});
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
#meta
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
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
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_METADATA_SERVICE_API_HOST=https://metadata-test.k8s-dev.blockscout.com
#meta
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
# api configuration
NEXT_PUBLIC_API_HOST=optimism.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
......
......@@ -18,8 +18,6 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://zkevm-rpc.com
# api configuration
NEXT_PUBLIC_API_HOST=zkevm.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
......@@ -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
# rollup
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
# api configuration
NEXT_PUBLIC_API_HOST=zksync.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
......
......@@ -21,6 +21,7 @@ export_envs_from_preset() {
"NEXT_PUBLIC_APP_HOST"
"NEXT_PUBLIC_APP_PORT"
"NEXT_PUBLIC_APP_ENV"
"NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL"
)
while IFS='=' read -r name value; do
......
......@@ -583,6 +583,7 @@ const schema = yup
NEXT_PUBLIC_VISUALIZE_API_BASE_PATH: yup.string(),
NEXT_PUBLIC_CONTRACT_INFO_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_WEB3_WALLETS: yup
.mixed()
......@@ -603,6 +604,7 @@ const schema = yup
NEXT_PUBLIC_OG_DESCRIPTION: yup.string(),
NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest),
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_IS_SUAVE_CHAIN: yup.boolean(),
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
......
......@@ -25,6 +25,7 @@ NEXT_PUBLIC_GAS_TRACKER_ENABLED=true
NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei']
NEXT_PUBLIC_IS_TESTNET=true
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_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
......@@ -41,6 +42,7 @@ NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_OG_DESCRIPTION='Hello world!'
NEXT_PUBLIC_OG_IMAGE_URL=https://example.com/image.png
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_PROMOTE_BLOCKSCOUT_IN_TITLE=true
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
......
......@@ -4,9 +4,9 @@ imagePullSecrets:
- name: regcred
config:
network:
id: 5
name: Göerli
shortname: Göerli
id: "11155111"
name: Sepolia
shortname: Sepolia
currency:
name: Ether
symbol: ETH
......@@ -54,11 +54,10 @@ blockscout:
cpu: "3"
# Blockscout environment variables
env:
BLOCKSCOUT_VERSION: v5.3.0-beta
ETHEREUM_JSONRPC_VARIANT: geth
HEART_BEAT_TIMEOUT: 30
SUBNETWORK: Ethereum
NETWORK: (Goerli)
NETWORK: (Sepolia)
NETWORK_ICON: _network_icon.html
LOGO: /images/goerli_logo.svg
TXS_STATS_DAYS_TO_COMPILE_AT_INIT: 1
......@@ -83,10 +82,10 @@ blockscout:
INDEXER_TOKEN_BALANCES_CONCURRENCY: 4
DISABLE_EXCHANGE_RATES: 'true'
DISABLE_INDEXER: 'false'
FIRST_BLOCK: '8739119'
LAST_BLOCK: '8739119'
TRACE_FIRST_BLOCK: '8739119'
TRACE_LAST_BLOCK: '8739119'
FIRST_BLOCK: '5780052'
LAST_BLOCK: '5780052'
TRACE_FIRST_BLOCK: '5780052'
TRACE_LAST_BLOCK: '5780052'
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_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:
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
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
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
......@@ -141,11 +139,11 @@ frontend:
env:
# ui config
NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-goerli.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_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json
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
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/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/sepolia.svg
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: validation
NEXT_PUBLIC_MARKETPLACE_ENABLED: true
......@@ -159,14 +157,14 @@ frontend:
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_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_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.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_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
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_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_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-sepolia.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]"
NEXT_PUBLIC_SWAP_BUTTON_URL: uniswap
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
......
......@@ -59,9 +59,9 @@ frontend:
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_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_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_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
......
......@@ -49,6 +49,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Transaction interpretation](ENVS.md#transaction-interpretation)
- [Verified tokens info](ENVS.md#verified-tokens-info)
- [Name service integration](ENVS.md#name-service-integration)
- [Metadata service integration](ENVS.md#metadata-service-integration)
- [Bridged tokens](ENVS.md#bridged-tokens)
- [Safe{Core} address tags](ENVS.md#safecore-address-tags)
- [SUAVE chain](ENVS.md#suave-chain)
......@@ -171,7 +172,7 @@ By default, the app has generic favicon. You can override this behavior by provi
### Meta
Settings for meta tags and OG tags
Settings for meta tags, OG tags and SEO
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
......@@ -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_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_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;
......@@ -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_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;
......@@ -550,6 +552,16 @@ This feature allows resolving blockchain addresses using human-readable domain n
&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
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">
<circle cx="5" cy="5" r="5" fill="#D9DBE0"/>
<circle cx="5" cy="5" r="2.5" fill="#707886"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" fill="none">
<circle cx="5" cy="5.438" r="5" fill="#E2E8F0"/>
<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>
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 {
AddressCoinBalanceHistoryChartOld,
} from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses';
import type { AddressMetadataInfo } from 'types/api/addressMetadata';
import type { TxBlobs, Blob } from 'types/api/blobs';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
......@@ -98,7 +99,14 @@ import type { ValidatorsCountersResponse, ValidatorsFilters, ValidatorsResponse,
import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
import type { VisualizedContract } from 'types/api/visualization';
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 { MarketplaceAppOverview } from 'types/client/marketplace';
import type { ArrayElement } from 'types/utils';
......@@ -237,6 +245,18 @@ export const RESOURCES = {
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
visualize_sol2uml: {
path: '/api/v1/solidity\\:visualize-contracts',
......@@ -588,43 +608,61 @@ export const RESOURCES = {
},
// optimistic L2
l2_deposits: {
optimistic_l2_deposits: {
path: '/api/v2/optimism/deposits',
filterFields: [],
},
l2_deposits_count: {
optimistic_l2_deposits_count: {
path: '/api/v2/optimism/deposits/count',
},
l2_withdrawals: {
optimistic_l2_withdrawals: {
path: '/api/v2/optimism/withdrawals',
filterFields: [],
},
l2_withdrawals_count: {
optimistic_l2_withdrawals_count: {
path: '/api/v2/optimism/withdrawals/count',
},
l2_output_roots: {
optimistic_l2_output_roots: {
path: '/api/v2/optimism/output-roots',
filterFields: [],
},
l2_output_roots_count: {
optimistic_l2_output_roots_count: {
path: '/api/v2/optimism/output-roots/count',
},
l2_txn_batches: {
optimistic_l2_txn_batches: {
path: '/api/v2/optimism/txn-batches',
filterFields: [],
},
l2_txn_batches_count: {
optimistic_l2_txn_batches_count: {
path: '/api/v2/optimism/txn-batches/count',
},
// 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: {
path: '/api/v2/zkevm/batches',
filterFields: [],
......@@ -739,6 +777,12 @@ export const RESOURCES = {
path: '/api/v2/config/backend-version',
},
// CSV EXPORT
csv_export_token_holders: {
path: '/api/v2/tokens/:hash/holders/csv',
pathParams: [ 'hash' as const ],
},
// OTHER
api_v2_key: {
path: '/api/v2/key',
......@@ -802,9 +846,9 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
'token_instance_transfers' | 'token_instance_holders' |
'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' |
'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' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx' |
......@@ -902,19 +946,16 @@ Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'l2_output_roots' ? OptimisticL2OutputRootsResponse :
Q extends 'l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
Q extends 'l2_deposits' ? OptimisticL2DepositsResponse :
Q extends 'l2_txn_batches' ? OptimisticL2TxnBatchesResponse :
Q extends 'l2_output_roots_count' ? number :
Q extends 'l2_withdrawals_count' ? number :
Q extends 'l2_deposits_count' ? number :
Q extends '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 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse :
Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse :
Q extends 'optimistic_l2_txn_batches' ? OptimisticL2TxnBatchesResponse :
Q extends 'optimistic_l2_output_roots_count' ? number :
Q extends 'optimistic_l2_withdrawals_count' ? number :
Q extends 'optimistic_l2_deposits_count' ? number :
Q extends 'optimistic_l2_txn_batches_count' ? number :
Q extends 'config_backend_version' ? BackendVersionConfig :
Q extends 'address_metadata_info' ? AddressMetadataInfo :
never;
// !!! IMPORTANT !!!
// See comment above
......@@ -931,6 +972,14 @@ Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse :
Q extends 'shibarium_deposits' ? ShibariumDepositsResponse :
Q extends 'shibarium_withdrawals_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_count' ? number :
Q extends 'zksync_l2_txn_batch' ? ZkSyncBatch :
......
import React, { createContext, useContext } from 'react';
import type { Route } from 'nextjs-routes';
import type { Props as PageProps } from 'nextjs/getServerSideProps';
type Props = {
......@@ -23,6 +24,6 @@ export function AppContextProvider({ children, pageProps }: Props) {
);
}
export function useAppContext() {
return useContext(AppContext);
export function useAppContext<Pathname extends Route['pathname'] = never>() {
return useContext<PageProps<Pathname>>(AppContext);
}
import { useRouter } from 'next/router';
import React from 'react';
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 ContractRead from 'ui/address/contract/ContractRead';
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 [
{ id: 'contact_code', title: 'Code', component: <ContractCode addressHash={ data?.hash }/> },
// this is not implemented in api yet
// data?.has_decompiled_code ?
// { id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } :
// undefined,
data?.has_methods_read ?
{ id: 'read_contract', title: 'Read contract', component: <ContractRead/> } :
return {
tabs: [
{
id: 'contract_code' as const,
title: 'Code',
component: <ContractCode contractQuery={ contractQuery } channel={ channel } addressHash={ data?.hash }/>,
},
contractQuery.data?.has_methods_read ?
{ id: 'read_contract' as const, title: 'Read contract', component: <ContractRead isLoading={ contractQuery.isPlaceholderData }/> } :
undefined,
data?.has_methods_read_proxy ?
{ id: 'read_proxy', title: 'Read proxy', component: <ContractRead/> } :
contractQuery.data?.has_methods_read_proxy ?
{ id: 'read_proxy' as const, title: 'Read proxy', component: <ContractRead isLoading={ contractQuery.isPlaceholderData }/> } :
undefined,
data?.has_custom_methods_read ?
{ id: 'read_custom_methods', title: 'Read custom', component: <ContractRead/> } :
contractQuery.data?.has_custom_methods_read ?
{ id: 'read_custom_methods' as const, title: 'Read custom', component: <ContractRead isLoading={ contractQuery.isPlaceholderData }/> } :
undefined,
data?.has_methods_write ?
{ id: 'write_contract', title: 'Write contract', component: <ContractWrite/> } :
contractQuery.data?.has_methods_write ?
{ id: 'write_contract' as const, title: 'Write contract', component: <ContractWrite isLoading={ contractQuery.isPlaceholderData }/> } :
undefined,
data?.has_methods_write_proxy ?
{ id: 'write_proxy', title: 'Write proxy', component: <ContractWrite/> } :
contractQuery.data?.has_methods_write_proxy ?
{ id: 'write_proxy' as const, title: 'Write proxy', component: <ContractWrite isLoading={ contractQuery.isPlaceholderData }/> } :
undefined,
data?.has_custom_methods_write ?
{ id: 'write_custom_methods', title: 'Write custom', component: <ContractWrite/> } :
contractQuery.data?.has_custom_methods_write ?
{ id: 'write_custom_methods' as const, title: 'Write custom', component: <ContractWrite isLoading={ contractQuery.isPlaceholderData }/> } :
undefined,
].filter(Boolean);
}, [ data ]);
].filter(Boolean),
isLoading: contractQuery.isPlaceholderData,
};
}, [ contractQuery, channel, data?.hash ]);
}
......@@ -72,45 +72,45 @@ export default function useNavItems(): ReturnType {
icon: 'validator',
isActive: pathname === '/validators',
} : null;
const rollupFeature = config.features.rollup;
if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') {
blockchainNavItems = [
[
txs,
userOps,
blocks,
{
const rollupDeposits = {
text: `Deposits (L1${ rightLineArrow }L2)`,
nextRoute: { pathname: '/deposits' as const },
icon: 'arrows/south-east',
isActive: pathname === '/deposits',
};
const rollupWithdrawals = {
text: `Withdrawals (L2${ rightLineArrow }L1)`,
nextRoute: { pathname: '/withdrawals' as const },
icon: 'arrows/north-east',
isActive: pathname === '/withdrawals',
};
const rollupTxnBatches = {
text: 'Txn batches',
nextRoute: { pathname: '/batches' as const },
icon: 'txn_batches',
isActive: pathname === '/batches' || pathname === '/batches/[number]',
},
].filter(Boolean),
[
topAccounts,
validators,
verifiedContracts,
ensLookup,
].filter(Boolean),
];
} else if (rollupFeature.isEnabled && rollupFeature.type === 'optimistic') {
isActive: pathname === '/batches',
};
const rollupOutputRoots = {
text: 'Output roots',
nextRoute: { pathname: '/output-roots' as const },
icon: 'output_roots',
isActive: pathname === '/output-roots',
};
const rollupFeature = config.features.rollup;
if (rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'zkEvm')) {
blockchainNavItems = [
[
txs,
// eslint-disable-next-line max-len
{ text: `Deposits (L1${ rightLineArrow }L2)`, nextRoute: { pathname: '/deposits' as const }, icon: 'arrows/south-east', isActive: pathname === '/deposits' },
// eslint-disable-next-line max-len
{ text: `Withdrawals (L2${ rightLineArrow }L1)`, nextRoute: { pathname: '/withdrawals' as const }, icon: 'arrows/north-east', isActive: pathname === '/withdrawals' },
rollupDeposits,
rollupWithdrawals,
],
[
blocks,
// eslint-disable-next-line max-len
{ text: 'Txn batches', nextRoute: { pathname: '/batches' as const }, icon: 'txn_batches', isActive: pathname === '/batches' },
// eslint-disable-next-line max-len
{ text: 'Output roots', nextRoute: { pathname: '/output-roots' as const }, icon: 'output_roots', isActive: pathname === '/output-roots' },
],
rollupTxnBatches,
rollupFeature.type === 'optimistic' ? rollupOutputRoots : undefined,
].filter(Boolean),
[
userOps,
topAccounts,
......@@ -123,10 +123,8 @@ export default function useNavItems(): ReturnType {
blockchainNavItems = [
[
txs,
// eslint-disable-next-line max-len
{ text: `Deposits (L1${ rightLineArrow }L2)`, nextRoute: { pathname: '/deposits' as const }, icon: 'arrows/south-east', isActive: pathname === '/deposits' },
// eslint-disable-next-line max-len
{ text: `Withdrawals (L2${ rightLineArrow }L1)`, nextRoute: { pathname: '/withdrawals' as const }, icon: 'arrows/north-east', isActive: pathname === '/withdrawals' },
rollupDeposits,
rollupWithdrawals,
],
[
blocks,
......@@ -142,12 +140,7 @@ export default function useNavItems(): ReturnType {
txs,
userOps,
blocks,
{
text: 'Txn batches',
nextRoute: { pathname: '/batches' as const },
icon: 'txn_batches',
isActive: pathname === '/batches' || pathname === '/batches/[number]',
},
rollupTxnBatches,
].filter(Boolean),
[
topAccounts,
......
import type { TokenInfo } from 'types/api/token';
import type { Route } from 'nextjs-routes';
/* eslint-disable @typescript-eslint/indent */
export type ApiData<Pathname extends Route['pathname']> =
(
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 '/apps/[id]' ? { app_name: string } :
never
......
......@@ -59,14 +59,8 @@ export const token: Address = {
creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72',
exchange_rate: null,
implementation_address: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: 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_tokens: true,
has_validated_blocks: false,
......@@ -79,14 +73,8 @@ export const contract: Address = {
creation_tx_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e',
creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943',
exchange_rate: '0.04311',
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
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_tokens: false,
has_validated_blocks: false,
......@@ -110,14 +98,8 @@ export const validator: Address = {
creation_tx_hash: null,
creator_address_hash: null,
exchange_rate: '0.00432018',
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: 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_tokens: false,
has_validated_blocks: true,
......
/* eslint-disable max-len */
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' } ],
can_be_visualized_via_sol2uml: true,
compiler_version: 'v0.5.16+commit.9c3226ce',
......@@ -32,9 +32,26 @@ export const verified: Partial<SmartContract> = {
],
language: 'solidity',
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,
file_path: './simple_storage.sol',
additional_sources: [
......@@ -45,7 +62,7 @@ export const withMultiplePaths: Partial<SmartContract> = {
],
};
export const verifiedViaSourcify: Partial<SmartContract> = {
export const verifiedViaSourcify: SmartContract = {
...verified,
is_verified_via_sourcify: true,
is_fully_verified: false,
......@@ -53,36 +70,67 @@ export const verifiedViaSourcify: Partial<SmartContract> = {
sourcify_repo_url: 'https://repo.sourcify.dev/contracts//full_match/99/0x51891596E158b2857e5356DC847e2D15dFbCF2d0/',
};
export const verifiedViaEthBytecodeDb: Partial<SmartContract> = {
export const verifiedViaEthBytecodeDb: SmartContract = {
...verified,
is_verified_via_eth_bytecode_db: true,
};
export const withTwinAddress: Partial<SmartContract> = {
export const withTwinAddress: SmartContract = {
...verified,
is_verified: false,
verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
};
export const withProxyAddress: Partial<SmartContract> = {
export const withProxyAddress: SmartContract = {
...verified,
is_verified: false,
verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
minimal_proxy_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
};
export const selfDestructed: Partial<SmartContract> = {
export const selfDestructed: SmartContract = {
...verified,
is_self_destructed: true,
};
export const withChangedByteCode: Partial<SmartContract> = {
export const withChangedByteCode: SmartContract = {
...verified,
is_changed_bytecode: true,
};
export const nonVerified: Partial<SmartContract> = {
export const nonVerified: SmartContract = {
is_verified: false,
creation_bytecode: 'creation_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 = {
acc_input_hash: '0x4bf88aabe33713b7817266d7860912c58272d808da7397cdc627ca53b296fad3',
......@@ -13,3 +13,28 @@ export const txnBatchData: ZkEvmL2TxnBatch = {
],
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 {
'wss://relay.walletconnect.com',
'wss://www.walletlink.org',
],
'frame-ancestors': [
'*.walletconnect.org',
'*.walletconnect.com',
],
'img-src': [
KEY_WORDS.BLOB,
'*.walletconnect.com',
......
......@@ -67,7 +67,7 @@ export const verifiedAddresses: 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 {
notFound: true,
};
......@@ -79,7 +79,7 @@ export const deposits: GetServerSideProps<Props> = async(context) => {
export const withdrawals: GetServerSideProps<Props> = async(context) => {
if (
!config.features.beaconChain.isEnabled &&
!(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium'))
!(rollupFeature.isEnabled && (rollupFeature.type === 'optimistic' || rollupFeature.type === 'shibarium' || rollupFeature.type === 'zkEvm'))
) {
return {
notFound: true,
......
......@@ -16,7 +16,11 @@ const Deposits = dynamic(() => {
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 });
const Page: NextPage = () => {
......
import type { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Route } from 'nextjs-routes';
......@@ -11,8 +10,7 @@ import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app';
import getQueryParamString from 'lib/router/getQueryParamString';
const Token = dynamic(() => import('ui/pages/Token'), { ssr: false });
import Token from 'ui/pages/Token';
const pathname: Route['pathname'] = '/token/[hash]';
......@@ -29,19 +27,18 @@ export default Page;
export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
const baseResponse = await gSSP.base<typeof pathname>(ctx);
if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) {
const botInfo = detectBotRequest(ctx.req);
if (botInfo?.type === 'social_preview') {
if ('props' in baseResponse) {
if (
config.meta.seo.enhancedDataEnabled ||
(config.meta.og.enhancedDataEnabled && detectBotRequest(ctx.req)?.type === 'social_preview')
) {
const tokenData = await fetchApi({
resource: 'token',
pathParams: { hash: getQueryParamString(ctx.query.hash) },
timeout: 1_000,
timeout: 500,
});
(await baseResponse.props).apiData = tokenData && tokenData.symbol ? {
symbol: tokenData.symbol,
} : null;
(await baseResponse.props).apiData = tokenData ?? null;
}
}
......
......@@ -17,6 +17,10 @@ const Withdrawals = dynamic(() => {
return import('ui/pages/ShibariumWithdrawals');
}
if (rollupFeature.isEnabled && rollupFeature.type === 'zkEvm') {
return import('ui/pages/ZkEvmL2Withdrawals');
}
if (beaconChainFeature.isEnabled) {
return import('ui/pages/BeaconChainWithdrawals');
}
......
......@@ -2,14 +2,16 @@ import { ChakraProvider } from '@chakra-ui/react';
import { GrowthBookProvider } from '@growthbook/growthbook-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
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 config from 'configs/app';
import { AppContextProvider } from 'lib/contexts/app';
import { SocketProvider } from 'lib/socket/context';
import wagmiConfig from 'lib/web3/wagmiConfig';
import * as app from 'playwright/utils/app';
import theme from 'theme';
......@@ -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 [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
......@@ -47,7 +63,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ app.socketPort }` : undefined }>
<AppContextProvider { ...appContext }>
<GrowthBookProvider>
<WagmiProvider config={ wagmiConfig! }>
<WagmiProvider config={ wagmiConfig }>
{ children }
</WagmiProvider>
</GrowthBookProvider>
......
......@@ -20,6 +20,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
[ 'NEXT_PUBLIC_ROLLUP_TYPE', 'shibarium' ],
[ '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: [
[ '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"}]' ],
......@@ -27,4 +31,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
userOps: [
[ '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 */
import { test as base } from '@playwright/experimental-ct-react';
import * as textAdMock from 'mocks/ad/textAd';
import * as injectMetaMaskProvider from './fixtures/injectMetaMaskProvider';
import * as mockApiResponse from './fixtures/mockApiResponse';
import * as mockAssetResponse from './fixtures/mockAssetResponse';
import * as mockConfigResponse from './fixtures/mockConfigResponse';
import * as mockEnvs from './fixtures/mockEnvs';
import * as mockFeatures from './fixtures/mockFeatures';
import * as mockTextAd from './fixtures/mockTextAd';
import * as render from './fixtures/render';
import * as socketServer from './fixtures/socketServer';
......@@ -21,6 +20,7 @@ interface Fixtures {
mockFeatures: mockFeatures.MockFeaturesFixture;
createSocket: socketServer.CreateSocketFixture;
injectMetaMaskProvider: injectMetaMaskProvider.InjectMetaMaskProvider;
mockTextAd: mockTextAd.MockTextAdFixture;
}
const test = base.extend<Fixtures>({
......@@ -30,11 +30,15 @@ const test = base.extend<Fixtures>({
mockConfigResponse: mockConfigResponse.default,
mockEnvs: mockEnvs.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,
injectMetaMaskProvider: injectMetaMaskProvider.default,
});
test.beforeEach(async({ page }) => {
test.beforeEach(async({ page, mockTextAd }) => {
// debug
const isDebug = process.env.PWDEBUG === '1';
......@@ -56,16 +60,7 @@ test.beforeEach(async({ page }) => {
// with few exceptions:
// 1. mock text AD requests
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',
});
});
await mockTextAd();
});
export * from '@playwright/experimental-ct-react';
......
......@@ -47,12 +47,6 @@ export const viewsEnvs = {
},
};
export const UIEnvs = {
hasContractAuditReports: [
{ name: 'NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS', value: 'true' },
],
};
export const stabilityEnvs = [
{ 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"]' },
......
......@@ -19,22 +19,16 @@ export const ADDRESS_INFO: Address = {
creation_tx_hash: null,
creator_address_hash: ADDRESS_HASH,
exchange_rate: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: true,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false,
has_tokens: false,
has_validated_blocks: false,
hash: ADDRESS_HASH,
implementation_address: null,
implementation_name: null,
is_contract: false,
is_verified: false,
is_contract: true,
is_verified: true,
name: 'ChainLink Token (goerli)',
token: TOKEN_INFO_ERC_20,
private_tags: [],
......
......@@ -7,6 +7,12 @@ export const CONTRACT_CODE_UNVERIFIED = {
creation_bytecode: '0x60806040526e',
deployed_bytecode: '0x608060405233',
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;
export const CONTRACT_CODE_VERIFIED = {
......@@ -41,6 +47,12 @@ export const CONTRACT_CODE_VERIFIED = {
source_code: 'source_code',
verified_at: '2023-02-21T14:39:16.906760Z',
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;
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';
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 = {
timestamp: '2023-06-01T14:46:48.000000Z',
status: 'Finalized',
......
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({
fontSize: 'xs',
......@@ -8,19 +8,25 @@ const baseStyle = defineStyle({
});
const variantSubtle = defineStyle((props) => {
const { colorScheme: c, theme } = props;
const darkBg = transparentize(`${ c }.200`, 0.16)(theme);
const { colorScheme: c } = props;
if (c === 'gray') {
return {
bg: mode('blackAlpha.100', 'whiteAlpha.400')(props),
color: mode('gray.600', 'gray.50')(props),
bg: mode('blackAlpha.50', 'whiteAlpha.100')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
};
}
if (c === 'gray-blue') {
return {
bg: mode(`${ c }.50`, darkBg)(props),
color: mode(`${ c }.500`, `${ c }.200`)(props),
bg: mode('gray.100', 'gray.800')(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';
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 }) => {
const component = await mount(
<TestApp>
......
......@@ -18,7 +18,7 @@ const variants = {
};
const sizes = {
md: definePartsStyle({
sm: definePartsStyle({
container: {
minH: 6,
minW: 6,
......@@ -48,7 +48,7 @@ const Tag = defineMultiStyleConfig({
variants,
sizes,
defaultProps: {
size: 'md',
size: 'sm',
variant: 'subtle',
colorScheme: 'gray',
},
......
......@@ -15,14 +15,8 @@ export interface Address extends UserTags {
ens_domain_name: string | null;
// TODO: if we are happy with tabs-counters method, should we delete has_something fields?
has_beacon_chain_withdrawals?: boolean;
has_custom_methods_read: boolean;
has_custom_methods_write: boolean;
has_decompiled_code: 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_tokens: 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 {
is_verified: boolean | null;
is_verified_via_eth_bytecode_db: 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 >>>
is_verified_via_sourcify: boolean | null;
is_fully_verified: boolean | null;
......
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 = {
number: number;
verify_tx_hash: string | null;
......
......@@ -8,4 +8,8 @@ export type CsvExportParams = {
type: 'logs';
filterType?: 'topic';
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';
interface Props {
tabs: Array<RoutedSubTab>;
addressHash?: string;
isLoading: boolean;
shouldRender?: boolean;
}
const TAB_LIST_PROPS = {
columnGap: 3,
};
const AddressContract = ({ tabs }: Props) => {
const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => {
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 (
<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 (
<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>
);
};
......
This diff is collapsed.
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 type { Channel } from 'phoenix';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address as AddressInfo } from 'types/api/address';
import type { SmartContract } from 'types/api/contract';
import { route } from 'nextjs-routes';
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 dayjs from 'lib/date/dayjs';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import * as stubs from 'stubs/contract';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import Hint from 'ui/shared/Hint';
......@@ -26,8 +28,8 @@ import ContractSourceCode from './ContractSourceCode';
type Props = {
addressHash?: string;
// prop for pw tests only
noSocket?: boolean;
contractQuery: UseQueryResult<SmartContract, ResourceError<unknown>>;
channel: Channel | undefined;
}
type InfoItemProps = {
......@@ -57,21 +59,13 @@ const InfoItem = chakra(({ label, content, hint, className, isLoading }: InfoIte
</GridItem>
));
const ContractCode = ({ addressHash, noSocket }: Props) => {
const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false);
const ContractCode = ({ addressHash, contractQuery, channel }: Props) => {
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
const queryClient = useQueryClient();
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
const { data, isPlaceholderData, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash) && (noSocket || isQueryEnabled),
refetchOnMount: false,
placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
},
});
const { data, isPlaceholderData, isError } = contractQuery;
const handleChangedBytecodeMessage: SocketMessage.AddressChangedBytecode['handler'] = React.useCallback(() => {
setIsChangedBytecodeSocket(true);
......@@ -86,14 +80,6 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
});
}, [ addressHash, queryClient ]);
const enableQuery = React.useCallback(() => setIsQueryEnabled(true), []);
const channel = useSocketChannel({
topic: `addresses:${ addressHash?.toLowerCase() }`,
isDisabled: !addressHash,
onJoin: enableQuery,
onSocketError: enableQuery,
});
useSocketMessage({
channel,
event: 'changed_bytecode',
......
......@@ -19,7 +19,11 @@ import ContractReadResult from './ContractReadResult';
import ContractMethodForm from './methodForm/ContractMethodForm';
import useWatchAccount from './useWatchAccount';
const ContractRead = () => {
interface Props {
isLoading?: boolean;
}
const ContractRead = ({ isLoading }: Props) => {
const apiFetch = useApiFetch();
const account = useWatchAccount();
const router = useRouter();
......@@ -36,7 +40,7 @@ const ContractRead = () => {
from: account?.address,
},
queryOptions: {
enabled: Boolean(addressHash),
enabled: !isLoading,
},
});
......
......@@ -19,7 +19,11 @@ import ContractMethodForm from './methodForm/ContractMethodForm';
import useContractAbi from './useContractAbi';
import { getNativeCoinValue, prepareAbi } from './utils';
const ContractWrite = () => {
interface Props {
isLoading?: boolean;
}
const ContractWrite = ({ isLoading }: Props) => {
const { data: walletClient } = useWalletClient();
const { isConnected, chainId } = useAccount();
const { switchChainAsync } = useSwitchChain();
......@@ -37,7 +41,7 @@ const ContractWrite = () => {
is_custom_abi: isCustomAbi ? 'true' : 'false',
},
queryOptions: {
enabled: Boolean(addressHash),
enabled: !isLoading,
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
pathParams: { hash: addressHash },
queryParams: { is_custom_abi: 'true' },
queryOptions: {
enabled: Boolean(addressInfo?.has_custom_methods_write),
enabled: Boolean(contractInfo?.has_custom_methods_write),
refetchOnMount: false,
},
});
......
......@@ -74,14 +74,8 @@ export default function useAddressQuery({ hash }: Params): AddressQuery {
creation_tx_hash: null,
exchange_rate: null,
ens_domain_name: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: 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_tokens: false,
has_validated_blocks: false,
......
......@@ -21,9 +21,10 @@ interface Props {
filterType?: CsvExportParams['filterType'] | null;
filterValue?: CsvExportParams['filterValue'] | null;
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>({
mode: 'onBlur',
defaultValues: {
......@@ -36,10 +37,10 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
try {
const url = buildUrl(resource, undefined, {
const url = buildUrl(resource, { hash } as never, {
address_id: hash,
from_period: data.from,
to_period: data.to,
from_period: exportType !== 'holders' ? data.from : null,
to_period: exportType !== 'holders' ? data.to : null,
filter_type: filterType,
filter_value: filterValue,
recaptcha_response: data.reCaptcha,
......@@ -56,11 +57,11 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
}
const blob = await response.blob();
downloadBlob(
blob,
`${ fileNameTemplate }_${ hash }_${ data.from }_${ data.to }
${ filterType && filterValue ? '_with_filter_type_' + filterType + '_value_' + filterValue : '' }.csv`,
);
const fileName = exportType === 'holders' ?
`${ fileNameTemplate }_${ hash }.csv` :
// eslint-disable-next-line max-len
`${ fileNameTemplate }_${ hash }_${ data.from }_${ data.to }${ filterType && filterValue ? '_with_filter_type_' + filterType + '_value_' + filterValue : '' }.csv`;
downloadBlob(blob, fileName);
} catch (error) {
toast({
......@@ -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 (
<FormProvider { ...formApi }>
......@@ -82,8 +83,8 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
onSubmit={ handleSubmit(onFormSubmit) }
>
<Flex columnGap={ 5 } rowGap={ 3 } flexDir={{ base: 'column', lg: 'row' }} alignItems={{ base: 'flex-start', lg: 'center' }} flexWrap="wrap">
<CsvExportFormField name="from" formApi={ formApi }/>
<CsvExportFormField name="to" formApi={ formApi }/>
{ exportType !== 'holders' && <CsvExportFormField name="from" formApi={ formApi }/> }
{ exportType !== 'holders' && <CsvExportFormField name="to" formApi={ formApi }/> }
<CsvExportFormReCaptcha formApi={ formApi }/>
</Flex>
<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 React from 'react';
import { txnBatchesData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatches';
import { txnBatchesData } from 'mocks/zkEvm/txnBatches';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
......
......@@ -72,7 +72,7 @@ const AddressPageContent = () => {
const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined);
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 isTabsLoading = isLoading || addressTabsCountersQuery.isPlaceholderData;
......@@ -163,8 +163,8 @@ const AddressPageContent = () => {
return 'Contract';
},
component: <AddressContract tabs={ contractTabs }/>,
subTabs: contractTabs.map(tab => tab.id),
component: <AddressContract tabs={ contractTabs.tabs } shouldRender={ !isTabsLoading } isLoading={ contractTabs.isLoading }/>,
subTabs: contractTabs.tabs.map(tab => tab.id),
} : undefined,
].filter(Boolean);
}, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data, isTabsLoading ]);
......
......@@ -4,6 +4,7 @@ import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import * as socketServer from 'playwright/fixtures/socketServer';
import { test, expect, devices } from 'playwright/lib';
import * as configs from 'playwright/utils/configs';
......@@ -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.describe.configure({ mode: 'serial' });
test.beforeEach(async({ mockTextAd }) => {
await mockTextAd();
});
test('base view +@dark-mode', async({ render, mockApiResponse }) => {
await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } });
await mockApiResponse('stats', statsMock.base);
......@@ -30,11 +35,8 @@ test('base view +@dark-mode', async({ render, mockApiResponse }) => {
await expect(component).toHaveScreenshot();
});
const hiddenFieldsTest = test.extend<{ context: BrowserContext }>({
context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields),
});
hiddenFieldsTest('hidden fields', async({ render, mockApiResponse }) => {
test('hidden fields', async({ render, mockApiResponse, mockEnvs }) => {
await mockEnvs(ENVS_MAP.blockHiddenFields);
await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } });
await mockApiResponse('stats', statsMock.base);
......
import { test, expect } from '@playwright/experimental-ct-react';
import { Box } from '@chakra-ui/react';
import React from 'react';
import * as addressMock from 'mocks/address/address';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as tokenMock from 'mocks/tokens/tokenInfo';
import { test, expect } from 'playwright/lib';
import * as configs from 'playwright/utils/configs';
import CsvExport from './CsvExport';
const ADDRESS_API_URL = buildApiUrl('address', { hash: addressMock.hash });
const hooksConfig = {
test('base view +@mobile +@dark-mode', async({ render, page, mockApiResponse }) => {
const hooksConfig = {
router: {
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 }) => {
await page.route(ADDRESS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.withName),
}));
});
const component = await render(<Box sx={{ '.recaptcha': { w: '304px', h: '78px' } }}><CsvExport/></Box>, { hooksConfig });
test('base view +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot({
mask: [ page.locator('.recaptcha') ],
maskColor: configs.maskColor,
});
});
const component = await mount(
<TestApp>
<CsvExport/>
</TestApp>,
{ hooksConfig },
);
test('token holders', async({ render, page, mockApiResponse }) => {
const hooksConfig = {
router: {
query: { address: addressMock.hash, type: 'holders' },
},
};
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({
mask: [ page.locator('.recaptcha') ],
......
......@@ -15,6 +15,7 @@ import { nbsp } from 'lib/html-entities';
import CsvExportForm from 'ui/csvExport/CsvExportForm';
import ContentLoader from 'ui/shared/ContentLoader';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import PageTitle from 'ui/shared/Page/PageTitle';
interface ExportTypeEntity {
......@@ -53,6 +54,11 @@ const EXPORT_TYPES: Record<CsvExportParams['type'], ExportTypeEntity> = {
fileNameTemplate: 'logs',
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);
......@@ -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 hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address');
......@@ -111,7 +126,7 @@ const CsvExport = () => {
const content = (() => {
throwOnResourceLoadError(addressQuery);
if (addressQuery.isPending) {
if (isLoading) {
return <ContentLoader/>;
}
......@@ -119,6 +134,7 @@ const CsvExport = () => {
<CsvExportForm
hash={ addressHash }
resource={ exportType.resource }
exportType={ isCorrectExportType(exportTypeParam) ? exportTypeParam : undefined }
filterType={ filterType }
filterValue={ filterValue }
fileNameTemplate={ exportType.fileNameTemplate }
......@@ -126,16 +142,33 @@ const CsvExport = () => {
);
})();
const description = (() => {
if (isLoading) {
return null;
}
if (exportTypeParam === 'holders') {
return (
<>
<PageTitle
title="Export data to CSV file"
backLink={ backLink }
<Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap">
<span>Export { exportType.text } for token </span>
<TokenEntity
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">
<span>Export { exportType.text } for address </span>
<AddressEntity
address={{ hash: addressHash, is_contract: true, implementation_name: null }}
address={ addressQuery.data }
truncation={ isMobile ? 'constant' : 'dynamic' }
noCopy
/>
......@@ -144,6 +177,16 @@ const CsvExport = () => {
<span>to CSV file. </span>
<span>Exports are limited to the last 10K { exportType.text }.</span>
</Flex>
);
})();
return (
<>
<PageTitle
title="Export data to CSV file"
backLink={ backLink }
/>
{ description }
{ content }
</>
);
......
......@@ -9,8 +9,8 @@ import * as configs from 'playwright/utils/configs';
import OptimisticL2Deposits from './OptimisticL2Deposits';
const DEPOSITS_API_URL = buildApiUrl('l2_deposits');
const DEPOSITS_COUNT_API_URL = buildApiUrl('l2_deposits_count');
const DEPOSITS_API_URL = buildApiUrl('optimistic_l2_deposits');
const DEPOSITS_COUNT_API_URL = buildApiUrl('optimistic_l2_deposits_count');
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
......
......@@ -14,9 +14,9 @@ import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
const OptimisticL2Deposits = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_deposits',
resourceName: 'optimistic_l2_deposits',
options: {
placeholderData: generateListStub<'l2_deposits'>(
placeholderData: generateListStub<'optimistic_l2_deposits'>(
L2_DEPOSIT_ITEM,
50,
{
......@@ -30,7 +30,7 @@ const OptimisticL2Deposits = () => {
},
});
const countersQuery = useApiQuery('l2_deposits_count', {
const countersQuery = useApiQuery('optimistic_l2_deposits_count', {
queryOptions: {
placeholderData: 1927029,
},
......
......@@ -14,8 +14,8 @@ const test = base.extend({
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
});
const OUTPUT_ROOTS_API_URL = buildApiUrl('l2_output_roots');
const OUTPUT_ROOTS_COUNT_API_URL = buildApiUrl('l2_output_roots_count');
const OUTPUT_ROOTS_API_URL = buildApiUrl('optimistic_l2_output_roots');
const OUTPUT_ROOTS_COUNT_API_URL = buildApiUrl('optimistic_l2_output_roots_count');
test('base view +@mobile', async({ mount, page }) => {
// test on mobile is flaky
......
......@@ -13,9 +13,9 @@ import StickyPaginationWithText from 'ui/shared/StickyPaginationWithText';
const OptimisticL2OutputRoots = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_output_roots',
resourceName: 'optimistic_l2_output_roots',
options: {
placeholderData: generateListStub<'l2_output_roots'>(
placeholderData: generateListStub<'optimistic_l2_output_roots'>(
L2_OUTPUT_ROOTS_ITEM,
50,
{
......@@ -28,7 +28,7 @@ const OptimisticL2OutputRoots = () => {
},
});
const countersQuery = useApiQuery('l2_output_roots_count', {
const countersQuery = useApiQuery('optimistic_l2_output_roots_count', {
queryOptions: {
placeholderData: 50617,
},
......
......@@ -14,8 +14,8 @@ const test = base.extend({
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
});
const TXN_BATCHES_API_URL = buildApiUrl('l2_txn_batches');
const TXN_BATCHES_COUNT_API_URL = buildApiUrl('l2_txn_batches_count');
const TXN_BATCHES_API_URL = buildApiUrl('optimistic_l2_txn_batches');
const TXN_BATCHES_COUNT_API_URL = buildApiUrl('optimistic_l2_txn_batches_count');
test('base view +@mobile', async({ mount, page }) => {
// test on mobile is flaky
......
......@@ -14,9 +14,9 @@ import OptimisticL2TxnBatchesTable from 'ui/txnBatches/optimisticL2/OptimisticL2
const OptimisticL2TxnBatches = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_txn_batches',
resourceName: 'optimistic_l2_txn_batches',
options: {
placeholderData: generateListStub<'l2_txn_batches'>(
placeholderData: generateListStub<'optimistic_l2_txn_batches'>(
L2_TXN_BATCHES_ITEM,
50,
{
......@@ -29,7 +29,7 @@ const OptimisticL2TxnBatches = () => {
},
});
const countersQuery = useApiQuery('l2_txn_batches_count', {
const countersQuery = useApiQuery('optimistic_l2_txn_batches_count', {
queryOptions: {
placeholderData: 5231746,
},
......
......@@ -14,8 +14,8 @@ const test = base.extend({
context: contextWithEnvs(configs.featureEnvs.optimisticRollup) as any,
});
const WITHDRAWALS_API_URL = buildApiUrl('l2_withdrawals');
const WITHDRAWALS_COUNT_API_URL = buildApiUrl('l2_withdrawals_count');
const WITHDRAWALS_API_URL = buildApiUrl('optimistic_l2_withdrawals');
const WITHDRAWALS_COUNT_API_URL = buildApiUrl('optimistic_l2_withdrawals_count');
test('base view +@mobile', async({ mount, page }) => {
// test on mobile is flaky
......
......@@ -14,9 +14,9 @@ import OptimisticL2WithdrawalsTable from 'ui/withdrawals/optimisticL2/Optimistic
const OptimisticL2Withdrawals = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
resourceName: 'l2_withdrawals',
resourceName: 'optimistic_l2_withdrawals',
options: {
placeholderData: generateListStub<'l2_withdrawals'>(
placeholderData: generateListStub<'optimistic_l2_withdrawals'>(
L2_WITHDRAWAL_ITEM,
50,
{
......@@ -29,7 +29,7 @@ const OptimisticL2Withdrawals = () => {
},
});
const countersQuery = useApiQuery('l2_withdrawals_count', {
const countersQuery = useApiQuery('optimistic_l2_withdrawals_count', {
queryOptions: {
placeholderData: 23700,
},
......
......@@ -6,7 +6,8 @@ import { test, expect } from 'playwright/lib';
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 mockApiResponse('shibarium_deposits', depositsData);
await mockApiResponse('shibarium_deposits_count', 3971111);
......
......@@ -6,12 +6,13 @@ import { test, expect } from 'playwright/lib';
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
// 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
test.slow();
await mockTextAd();
await mockEnvs(ENVS_MAP.shibariumRollup);
await mockApiResponse('shibarium_withdrawals', withdrawalsData);
await mockApiResponse('shibarium_withdrawals_count', 397);
......
import React from 'react';
import config from 'configs/app';
import * as verifiedAddressesMocks from 'mocks/account/verifiedAddresses';
import { token as contract } from 'mocks/address/address';
import { tokenInfo, tokenCounters, bridgedTokenA } from 'mocks/tokens/tokenInfo';
......@@ -10,9 +11,12 @@ import * as configs from 'playwright/utils/configs';
import Token from './Token';
const hash = tokenInfo.address;
const chainId = config.chain.id;
const hooksConfig = {
router: {
query: { hash: '1', tab: 'token_transfers' },
query: { hash, tab: 'token_transfers' },
isReady: true,
},
};
......@@ -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.describe.configure({ mode: 'serial' });
test.beforeEach(async({ mockApiResponse }) => {
await mockApiResponse('token', tokenInfo, { pathParams: { hash: '1' } });
await mockApiResponse('address', contract, { pathParams: { hash: '1' } });
await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash: '1' } });
await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash: '1' } });
test.beforeEach(async({ mockApiResponse, mockTextAd }) => {
await mockApiResponse('token', tokenInfo, { pathParams: { hash } });
await mockApiResponse('address', contract, { pathParams: { hash } });
await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash } });
await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash } });
await mockTextAd();
});
test('base view', async({ render, page, createSocket }) => {
const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
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 });
await expect(component).toHaveScreenshot({
......@@ -42,13 +47,13 @@ test('base view', async({ render, page, createSocket }) => {
});
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');
const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
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 });
await page.getByRole('button', { name: /project info/i }).click();
......@@ -60,17 +65,24 @@ test('with verified info', async({ render, page, createSocket, mockApiResponse,
});
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 mockApiResponse('token', bridgedTokenA, { pathParams: { hash: '1' } });
await mockApiResponse('address', contract, { pathParams: { hash: '1' } });
await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash: '1' } });
await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash: '1' } });
await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId: '1', hash: '1' } });
await mockApiResponse('token', bridgedTokenA, { pathParams: { hash } });
await mockApiResponse('address', contract, { pathParams: { hash } });
await mockApiResponse('token_counters', tokenCounters, { pathParams: { hash } });
await mockApiResponse('token_transfers', { items: [], next_page_params: null }, { pathParams: { hash } });
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');
const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
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 });
await expect(component).toHaveScreenshot({
......@@ -85,7 +97,7 @@ test.describe('mobile', () => {
test('base view', async({ render, page, createSocket }) => {
const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
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 });
await expect(component).toHaveScreenshot({
......@@ -95,12 +107,12 @@ test.describe('mobile', () => {
});
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');
const component = await render(<Token/>, { hooksConfig }, { withSocket: true });
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 });
await expect(component).toHaveScreenshot({
......
This diff is collapsed.
......@@ -7,6 +7,10 @@ import { test, expect } from 'playwright/lib';
import Tokens from './Tokens';
test.beforeEach(async({ mockTextAd }) => {
await mockTextAd();
});
test('base view +@mobile +@dark-mode', async({ render, mockApiResponse }) => {
const allTokens = {
items: [
......
......@@ -7,12 +7,13 @@ import Validators from './Validators';
const chainType = 'stability';
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs }) => {
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', chainType ],
]);
await mockApiResponse('validators', validatorsMock.validatorsResponse, { pathParams: { chainType } });
await mockApiResponse('validators_counters', validatorsMock.validatorsCountersResponse, { pathParams: { chainType } });
await mockTextAd();
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 React from 'react';
import { txnBatchData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatch';
import { txnBatchData } from 'mocks/zkEvm/txnBatches';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { txnBatchesData } from 'mocks/zkevmL2txnBatches/zkevmL2txnBatches';
import { txnBatchesData } from 'mocks/zkEvm/txnBatches';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
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