Commit 89fac78e authored by Max Alekseenko's avatar Max Alekseenko

Merge branch 'main' into dapp-rating

parents 3d295216 51e0ca1c
......@@ -5,9 +5,19 @@ const RESTRICTED_MODULES = {
{ name: '@metamask/providers', message: 'Please lazy-load @metamask/providers or use useProvider hook instead' },
{ name: '@metamask/post-message-stream', message: 'Please lazy-load @metamask/post-message-stream or use useProvider hook instead' },
{ name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
{
name: '@chakra-ui/react',
importNames: [ 'Popover', 'Menu', 'useToast' ],
message: 'Please use corresponding component or hook from ui/shared/chakra component instead',
},
{
name: 'lodash',
message: 'Please use `import [package] from \'lodash/[package]\'` instead.',
},
],
patterns: [
'icons/*',
'!lodash/*',
],
};
......
......@@ -12,6 +12,7 @@ on:
- none
- arbitrum
- base
- celo_alfajores
- gnosis
- eth
- eth_sepolia
......
......@@ -339,7 +339,9 @@
"main",
"main.L2",
"localhost",
"arbitrum",
"base",
"celo_alfajores",
"gnosis",
"eth",
"eth_goerli",
......
......@@ -15,6 +15,7 @@ const chain = Object.freeze({
secondaryCoin: {
symbol: getEnvValue('NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL'),
},
hasMultipleGasCurrencies: getEnvValue('NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES') === 'true',
tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC',
rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'),
isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true',
......
# Set of ENVs for Celo Alfajjores network explorer
# https://celo-alfajores.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=Celo Alfajores
NEXT_PUBLIC_NETWORK_SHORT_NAME=Alfajores
NEXT_PUBLIC_NETWORK_ID=44787
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Celo
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=CELO
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES=true
NEXT_PUBLIC_NETWORK_RPC_URL=https://alfajores-forno.celo-testnet.org
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_IS_TESTNET=true
# api configuration
NEXT_PUBLIC_API_HOST=celo-alfajores.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(0,0,0,1)
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(252,255,82,1)
## sidebar
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-light.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-dark.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/celo-icon-light.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/celo-icon-dark.svg
## footer
##views
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'}]
## misc
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'}}]
## 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_CONTRACT_SOLIDITYSCAN_ENABLED=true
# app features
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
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_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
# NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.k8s-dev.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/celo.png
......@@ -43,7 +43,7 @@ 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_PROVIDER=slise
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
......
......@@ -91,11 +91,11 @@ brace-expansion@^1.1.7:
concat-map "0.0.1"
braces@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.0.1"
fill-range "^7.1.1"
color-name@^1.1.4:
version "1.1.4"
......@@ -276,10 +276,10 @@ filing-cabinet@^4.1.6:
tsconfig-paths "^4.2.0"
typescript "^5.0.4"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
......
......@@ -273,7 +273,7 @@ const rollupSchema = yup
.when('NEXT_PUBLIC_ROLLUP_TYPE', {
is: (value: string) => value === 'optimistic',
then: (schema) => schema.test(urlTest).required(),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL cannot not be used if NEXT_PUBLIC_ROLLUP_TYPE is not defined'),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL can be used only if NEXT_PUBLIC_ROLLUP_TYPE is set to \'optimistic\' '),
}),
});
......@@ -507,6 +507,7 @@ const schema = yup
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: yup.number().integer().positive(),
NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL: yup.string(),
NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES: yup.boolean(),
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: yup.string<NetworkVerificationType>().oneOf([ 'validation', 'mining' ]),
NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME: yup.string(),
NEXT_PUBLIC_IS_TESTNET: yup.boolean(),
......
......@@ -47,6 +47,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Explorer','baseUrl':'https://example.com/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO
NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES=true
NEXT_PUBLIC_NETWORK_ICON=https://example.com/icon.png
NEXT_PUBLIC_NETWORK_ICON_DARK=https://example.com/icon.png
NEXT_PUBLIC_NETWORK_LOGO=https://example.com/logo.png
......
......@@ -262,11 +262,11 @@ ansi-styles@^4.1.0:
color-convert "^2.0.1"
braces@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.0.1"
fill-range "^7.1.1"
browserslist@^4.14.5:
version "4.21.9"
......@@ -439,10 +439,10 @@ fastest-levenshtein@^1.0.12:
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
......
......@@ -68,7 +68,6 @@ frontend:
NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']"
NEXT_PUBLIC_NETWORK_RPC_URL: https://eth-sepolia.public.blastapi.io
NEXT_PUBLIC_NETWORK_ID: '11155111'
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','logo':'https://github.com/blockscout/frontend-configs/blob/main/configs/explorer-logos/etherscan.png?raw=true','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]"
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_FEATURED_APP: zkbob-wallet
......@@ -77,22 +76,15 @@ frontend:
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']"
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: gradient_avatar
NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS: "['top_accounts']"
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED: true
NEXT_PUBLIC_VIEWS_TX_HIDDEN_FIELDS: "['value','fee_currency','gas_price','gas_fees','burnt_fees']"
NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS: "['fee_per_gas']"
NEXT_PUBLIC_USE_NEXT_JS_PROXY: true
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_HAS_USER_OPS: true
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_TRANSACTION_INTERPRETATION_PROVIDER: blockscout
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true
NEXT_PUBLIC_AD_BANNER_PROVIDER: getit
NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER: adbutler
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: "{ \"id\": \"632019\", \"width\": \"728\", \"height\": \"90\" }"
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: "{ \"id\": \"632018\", \"width\": \"320\", \"height\": \"100\" }"
NEXT_PUBLIC_AD_BANNER_PROVIDER: slise
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/apps']"
envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
......
......@@ -91,6 +91,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | `string` | Network secondary coin symbol. | - | - | `GNO` | v1.29.0+ |
| NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES | `boolean` | Set to `true` for networks where users can pay transaction fees in either the native coin or ERC-20 tokens. | - | `false` | `true` | v1.33.0+ |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` | Verification type in the network | - | `mining` | `validation` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME | `string` | Name of the standard for creating tokens | - | `ERC` | `BEP` | v1.31.0+ |
| NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` | v1.0.x+ |
......@@ -359,6 +360,7 @@ This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_GAS_T
### Banner ads
This feature is **enabled by default** with the `slise` ads provider. To switch it off pass `NEXT_PUBLIC_AD_BANNER_PROVIDER=none`.
*Note* that the `getit` ad provider is temporary disabled.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
......@@ -402,8 +404,8 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'arbitrum' \| 'shibarium' \| 'zkEvm' \| 'zkSync' ` | Rollup chain type | Required | - | `'optimistic'` | v1.24.0+ |
| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0+ |
| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals | Required only for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ |
| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (OP stack only) | - | - | `true` | v1.31.0+ |
| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals (Optimistic stack only) | Required for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ |
| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ |
&nbsp;
......
<svg viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 11a7.5 7.5 0 1 1 15 0 7.5 7.5 0 0 1-15 0ZM11 1C5.477 1 1 5.477 1 11s4.477 10 10 10 10-4.477 10-10S16.523 1 11 1Zm1.25 5a1.25 1.25 0 1 0-2.5 0v5c0 .69.56 1.25 1.25 1.25h5a1.25 1.25 0 1 0 0-2.5h-3.75V6Z" fill="currentColor" stroke="transparent" stroke-width=".6" stroke-linecap="round" stroke-linejoin="round"/>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 .3C4.643.3.3 4.643.3 10c0 5.357 4.343 9.7 9.7 9.7 5.357 0 9.7-4.343 9.7-9.7 0-5.357-4.343-9.7-9.7-9.7ZM2.2 10a7.8 7.8 0 1 1 15.6 0 7.8 7.8 0 0 1-15.6 0ZM10 4.05a.95.95 0 0 0-.95.95v5c0 .525.425.95.95.95h5a.95.95 0 1 0 0-1.9h-3.75a.3.3 0 0 1-.3-.3V5a.95.95 0 0 0-.95-.95Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.392.45C3.644.163 3.984 0 4.34 0h8.038a.63.63 0 0 1 .474.225L17.54 5.61a.83.83 0 0 1 .196.544v12.308c0 .408-.141.799-.393 1.087-.25.289-.592.451-.947.451H4.34c-.356 0-.696-.162-.948-.45A1.661 1.661 0 0 1 3 18.461V1.538c0-.408.141-.799.392-1.087Zm.948 1.088h6.87v4.497c0 .388.315.702.702.702h4.485v11.725H4.34V1.538Zm8.274.59 2.791 3.205h-2.791V2.128Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.233 8.8a2.293 2.293 0 0 0-1.613.644l-.006.006-.595.591a.564.564 0 1 0 .795.8l.592-.589a1.166 1.166 0 0 1 1.649 1.648l-1.034 1.034a1.168 1.168 0 0 1-1.758-.126.564.564 0 1 0-.903.676 2.293 2.293 0 0 0 3.458.247l1.038-1.037.007-.007a2.294 2.294 0 0 0-1.63-3.887Zm-2.259 2.403a2.294 2.294 0 0 0-1.786.666l-1.037 1.037-.007.008a2.293 2.293 0 0 0 3.243 3.242l.006-.007.592-.591a.564.564 0 0 0-.797-.797l-.588.587A1.166 1.166 0 0 1 7.952 13.7l1.034-1.034a1.167 1.167 0 0 1 1.758.126.564.564 0 0 0 .903-.675 2.293 2.293 0 0 0-1.673-.914Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 23.75h1.25v2.5H2.5v-2.5h1.25V5A1.25 1.25 0 0 1 5 3.75h11.25A1.25 1.25 0 0 1 17.5 5v10H20a2.5 2.5 0 0 1 2.5 2.5v5a1.25 1.25 0 0 0 2.5 0v-8.75h-2.5a1.25 1.25 0 0 1-1.25-1.25V8.018l-2.071-2.072 1.767-1.767 6.188 6.187a1.244 1.244 0 0 1 .366.884V22.5a3.75 3.75 0 0 1-7.5 0v-5h-2.5v6.25Zm-11.25 0H15v-7.5H6.25v7.5Zm0-17.5v7.5H15v-7.5H6.25Z" fill="currentColor"/>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.666 15.833h.834V17.5H1.666v-1.667H2.5v-12.5a.833.833 0 0 1 .833-.833h7.5a.833.833 0 0 1 .833.833V10h1.667A1.667 1.667 0 0 1 15 11.667V15a.833.833 0 0 0 1.666 0V9.167H15a.833.833 0 0 1-.834-.834V5.345l-1.38-1.38 1.178-1.18 4.125 4.126a.831.831 0 0 1 .244.589V15a2.5 2.5 0 0 1-5 0v-3.333h-1.667v4.166Zm-7.5 0H10v-5H4.167v5Zm0-11.666v5H10v-5H4.167Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.16 3A2.16 2.16 0 0 0 3 5.16v10.18a2.16 2.16 0 0 0 2.16 2.16h10.18a2.16 2.16 0 0 0 2.16-2.16v-3.272h-1.614v3.114c0 .389-.315.704-.704.704H5.318a.705.705 0 0 1-.704-.704V5.318c0-.389.315-.704.704-.704h3.114V3H5.159Zm6.135 0a.41.41 0 0 0-.41.41v.793c0 .226.184.409.41.409h3.453l-4.244 4.245a.409.409 0 0 0 0 .578l.56.561c.16.16.42.16.58 0l4.244-4.245v3.454c0 .226.183.41.41.41h.793a.41.41 0 0 0 .409-.41V3h-6.205Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.16 3A2.16 2.16 0 0 0 3 5.16v10.18a2.16 2.16 0 0 0 2.16 2.16h10.18a2.16 2.16 0 0 0 2.16-2.16v-3.272h-1.614v3.114a.704.704 0 0 1-.704.704H5.318a.705.705 0 0 1-.704-.704V5.318c0-.389.315-.704.704-.704h3.114V3H5.159Zm6.135 0a.41.41 0 0 0-.41.41v.793a.41.41 0 0 0 .41.409h3.453l-4.244 4.245a.409.409 0 0 0 0 .578l.56.561c.16.16.42.16.58 0l4.244-4.245v3.454c0 .226.183.41.41.41h.793a.41.41 0 0 0 .409-.41V3h-6.205Z" fill="currentColor"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 31 30">
<g fill="currentColor" clip-path="url(#wallet_svg__a)">
<path d="M22.75 27.188h-15A4.688 4.688 0 0 1 3.062 22.5V9.375A.937.937 0 0 1 4 8.437h18.75a4.688 4.688 0 0 1 4.688 4.688V22.5a4.688 4.688 0 0 1-4.688 4.688ZM4.937 10.313V22.5a2.812 2.812 0 0 0 2.813 2.813h15a2.812 2.812 0 0 0 2.813-2.813v-9.375a2.812 2.812 0 0 0-2.813-2.813H4.937Z"/>
<path d="M24.625 10.312a.937.937 0 0 1-.937-.937V6.797a2.184 2.184 0 0 0-.732-1.754 1.82 1.82 0 0 0-1.565-.3L5.669 8.315a.938.938 0 0 0-.731.938.938.938 0 0 1-1.875 0 2.813 2.813 0 0 1 2.184-2.766l15.731-3.572a3.656 3.656 0 0 1 3.15.666 4.069 4.069 0 0 1 1.435 3.216v2.578a.938.938 0 0 1-.938.937ZM26.5 21.563h-6.563a3.75 3.75 0 1 1 0-7.5H26.5a.938.938 0 0 1 .938.937v5.625a.938.938 0 0 1-.938.938Zm-6.563-5.625a1.875 1.875 0 1 0 0 3.75h5.625v-3.75h-5.625Z"/>
</g>
<defs>
<clipPath id="wallet_svg__a">
<path fill="#fff" d="M.25 0h30v30h-30z"/>
</clipPath>
</defs>
<svg viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.139 20H4.83a3.847 3.847 0 0 1-3.846-3.845V5.39a.769.769 0 0 1 .77-.769h15.384a3.847 3.847 0 0 1 3.846 3.845v7.69A3.844 3.844 0 0 1 17.139 20ZM2.524 6.16v9.996a2.307 2.307 0 0 0 2.307 2.307H17.14a2.308 2.308 0 0 0 2.308-2.307v-7.69A2.306 2.306 0 0 0 17.14 6.16H2.524Z" fill="currentColor"/>
<path d="M18.677 6.159a.77.77 0 0 1-.769-.77V3.276a1.79 1.79 0 0 0-.6-1.438 1.493 1.493 0 0 0-1.284-.246l-12.9 2.93a.77.77 0 0 0-.6.769.769.769 0 0 1-1.539 0A2.306 2.306 0 0 1 2.778 3.02L15.685.091a3 3 0 0 1 2.585.546 3.338 3.338 0 0 1 1.177 2.638V5.39a.77.77 0 0 1-.77.769Zm1.539 9.227H14.83a3.077 3.077 0 0 1-2.175-5.25 3.074 3.074 0 0 1 2.175-.902h5.385a.77.77 0 0 1 .769.77v4.613a.77.77 0 0 1-.77.769Zm-5.385-4.614a1.539 1.539 0 1 0 0 3.076h4.616v-3.076H14.83Z" fill="currentColor"/>
</svg>
......@@ -60,9 +60,9 @@ export function AddressHighlightProvider({ children }: AddressHighlightProviderP
);
}
export function useAddressHighlightContext() {
export function useAddressHighlightContext(disabled?: boolean) {
const context = React.useContext(AddressHighlightContext);
if (context === undefined) {
if (context === undefined || disabled) {
return null;
}
return context;
......
......@@ -17,7 +17,7 @@ const TEST_URLS: Record<AdBannerProviders, string> = {
adbutler: 'https://servedbyadbutler.com/app.js',
hype: 'https://api.hypelab.com/v1/scripts/hp-sdk.js',
// I don't have an url for getit to test
getit: DEFAULT_URL,
// getit: DEFAULT_URL,
none: DEFAULT_URL,
};
......
......@@ -2,7 +2,7 @@ import type { UseToastOptions, ToastProps } from '@chakra-ui/react';
import { createToastFn, useChakra } from '@chakra-ui/react';
import React from 'react';
import Toast from 'ui/shared/Toast';
import Toast from 'ui/shared/chakra/Toast';
// there is no toastComponent prop in UseToastOptions type
// but these options will be passed to createRenderToast under the hood
......@@ -14,6 +14,7 @@ const defaultOptions: UseToastOptions & { toastComponent?: React.FC<ToastProps>
containerStyle: {
margin: 8,
},
variant: 'subtle',
};
export default function useToastModified() {
......
import { uniq } from 'lodash';
import _uniq from 'lodash/uniq';
import isBrowser from './isBrowser';
......@@ -27,7 +27,7 @@ export function saveToRecentKeywords(value: string) {
}
const keywordsArr = getRecentSearchKeywords();
const result = uniq([ value, ...keywordsArr ]).slice(0, MAX_KEYWORDS_NUMBER - 1);
const result = _uniq([ value, ...keywordsArr ]).slice(0, MAX_KEYWORDS_NUMBER - 1);
window.localStorage.setItem(RECENT_KEYWORDS_LS_KEY, JSON.stringify(result));
}
......
export const multiple = [
{ address: '0xA84d24bD8ACE4d349C5f8c5DeeDd8bc071Ce5e2b', name: null },
{ address: '0xc9e91eDeA9DC16604022e4E5b437Df9c64EdB05A', name: 'Diamond' },
{ address: '0x2041832c62C0F89426b48B5868146C0b1fcd23E7', name: null },
{ address: '0x5f7DC6ECcF05594429671F83cc0e42EE18bC0974', name: 'VariablePriceFacet' },
{ address: '0x7abC92E242e88e4B0d6c5Beb4Df80e94D2c8A78c', name: null },
{ address: '0x84178a0c58A860eCCFB7E3aeA64a09543062A356', name: 'MultiSaleFacet' },
{ address: '0x33aD95537e63e9f09d96dE201e10715Ed40D9400', name: 'SVGTemplatesFacet' },
{ address: '0xfd86Aa7f902185a8Df9859c25E4BF52D3DaDd9FA', name: 'ERC721AReceiverFacet' },
{ address: '0x6945a35df18e59Ce09fec4B6cD3C4F9cFE6369de', name: null },
];
......@@ -299,7 +299,7 @@ export const stabilityTx: Transaction = {
decimals: '18',
exchange_rate: '123.567',
holders: '92',
icon_url: null,
icon_url: 'https://example.com/icon.png',
name: 'Stability Gas',
symbol: 'GAS',
total_supply: '10000000000000000000000000',
......@@ -321,6 +321,24 @@ export const stabilityTx: Transaction = {
},
};
export const celoTxn: Transaction = {
...base,
celo: {
gas_token: {
address: '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1',
circulating_market_cap: null,
decimals: '18',
exchange_rate: '0.42',
holders: '205738',
icon_url: 'https://example.com/icon.png',
name: 'Celo Dollar',
symbol: 'cUSD',
total_supply: '7145754483836626799435133',
type: 'ERC-20',
},
},
};
export const base2 = {
...base,
hash: '0x02d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
......
......@@ -39,7 +39,8 @@ export function ad(): CspDev.DirectiveDescriptor {
// adbutler
'servedbyadbutler.com',
`'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd ?? '')) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd(undefined) ?? '')) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd('mobile') ?? '')) }'`,
// slise
'*.slise.xyz',
......
......@@ -70,13 +70,13 @@
"chakra-react-select": "^4.4.3",
"crypto-js": "^4.2.0",
"d3": "^7.6.1",
"dappscout-iframe": "0.2.1",
"dappscout-iframe": "0.2.2",
"dayjs": "^1.11.5",
"dom-to-image": "^2.6.0",
"focus-visible": "^5.2.0",
"framer-motion": "^6.5.1",
"getit-sdk": "^1.0.4",
"gradient-avatar": "^1.0.2",
"gradient-avatar": "git+https://github.com/blockscout/gradient-avatar.git",
"graphiql": "^2.2.0",
"graphql": "^16.8.1",
"graphql-ws": "^5.11.3",
......
import type { TestFixture, Page } from '@playwright/test';
import _isEqual from 'lodash/isEqual';
import type { PublicRpcSchema } from 'viem';
import { getEnvValue } from 'configs/app/utils';
type Params = PublicRpcSchema[number];
export type MockRpcResponseFixture = (params: Params) => Promise<void>;
// WIP
const fixture: TestFixture<MockRpcResponseFixture, { page: Page }> = async({ page }, use) => {
await use(async({ Method, ReturnType }) => {
const rpcUrl = getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL');
if (!rpcUrl) {
return;
}
await page.route(rpcUrl, (route) => {
const method = route.request().method();
if (method !== 'POST') {
route.continue();
return;
}
const json = route.request().postDataJSON();
const id = json?.id;
const payload = {
id,
jsonrpc: '2.0',
method: Method,
// TODO: add params to match actual payload
};
if (_isEqual(json, payload) && id !== undefined) {
return route.fulfill({
status: 200,
body: JSON.stringify({
id,
jsonrpc: '2.0',
result: ReturnType,
}),
});
}
});
});
};
export default fixture;
......@@ -8,6 +8,7 @@ import * as mockConfigResponse from './fixtures/mockConfigResponse';
import * as mockContractReadResponse from './fixtures/mockContractReadResponse';
import * as mockEnvs from './fixtures/mockEnvs';
import * as mockFeatures from './fixtures/mockFeatures';
import * as mockRpcResponse from './fixtures/mockRpcResponse';
import * as mockTextAd from './fixtures/mockTextAd';
import * as render from './fixtures/render';
import * as socketServer from './fixtures/socketServer';
......@@ -20,6 +21,7 @@ interface Fixtures {
mockContractReadResponse: mockContractReadResponse.MockContractReadResponseFixture;
mockEnvs: mockEnvs.MockEnvsFixture;
mockFeatures: mockFeatures.MockFeaturesFixture;
mockRpcResponse: mockRpcResponse.MockRpcResponseFixture;
createSocket: socketServer.CreateSocketFixture;
injectMetaMaskProvider: injectMetaMaskProvider.InjectMetaMaskProvider;
mockTextAd: mockTextAd.MockTextAdFixture;
......@@ -33,6 +35,7 @@ const test = base.extend<Fixtures>({
mockContractReadResponse: mockContractReadResponse.default,
mockEnvs: mockEnvs.default,
mockFeatures: mockFeatures.default,
mockRpcResponse: mockRpcResponse.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
......
......@@ -32,6 +32,7 @@
| "clock"
| "coins/bitcoin"
| "collection"
| "contracts/proxy"
| "contracts/regular_many"
| "contracts/regular"
| "contracts/verified_many"
......
......@@ -17,7 +17,6 @@ const sizes = {
xl: defineStyle({
fontSize: '40px',
lineHeight: '48px',
letterSpacing: '-1px',
}),
lg: defineStyle({
fontSize: '32px',
......
......@@ -19,6 +19,10 @@ const semanticTokens = {
link_hovered: {
'default': 'blue.400',
},
icon_info: {
'default': 'gray.400',
_dark: 'gray.500',
},
error: {
'default': 'red.500',
_dark: 'red.500',
......
......@@ -24,9 +24,7 @@ export interface UserTags {
export type AddressParamBasic = {
hash: string;
// API doesn't return hash in this model yet
// will be fixed in the future releases
implementations: Array<Omit<AddressImplementation, 'address'>> | null;
implementations: Array<AddressImplementation> | null;
name: string | null;
is_contract: boolean;
is_verified: boolean | null;
......
......@@ -77,6 +77,10 @@ export type Transaction = {
validator_address: AddressParam;
validator_fee: string;
};
// Celo fields
celo?: {
gas_token: TokenInfo<'ERC-20'> | null;
};
// zkEvm fields
zkevm_verify_hash?: string;
zkevm_batch_number?: number;
......
import type { ArrayElement } from 'types/utils';
export const SUPPORTED_AD_BANNER_PROVIDERS = [ 'slise', 'adbutler', 'coinzilla', 'hype', 'getit', 'none' ] as const;
export const SUPPORTED_AD_BANNER_PROVIDERS = [
'slise',
'adbutler',
'coinzilla',
'hype',
// 'getit', // temporary disabled
'none',
] as const;
export type AdBannerProviders = ArrayElement<typeof SUPPORTED_AD_BANNER_PROVIDERS>;
export const SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS = [ 'adbutler' ] as const;
......
import { Box, Text, Icon, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import { Box, Text, Icon, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import React from 'react';
// This icon doesn't work properly when it is in the sprite
......@@ -7,6 +7,7 @@ import React from 'react';
import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { SOLIDITYSCAN_REPORT } from 'stubs/contract';
import Popover from 'ui/shared/chakra/Popover';
import LinkExternal from 'ui/shared/links/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
......
......@@ -2,7 +2,6 @@ import {
Flex,
Button,
chakra,
Popover,
PopoverTrigger,
PopoverBody,
PopoverContent,
......@@ -14,6 +13,7 @@ import {
import React from 'react';
import config from 'configs/app';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
......
......@@ -7,7 +7,6 @@ import {
Modal,
ModalCloseButton,
ModalContent,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
......@@ -22,6 +21,7 @@ import type { SmartContractExternalLibrary } from 'types/api/contract';
import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities';
import Popover from 'ui/shared/chakra/Popover';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import IconSvg from 'ui/shared/IconSvg';
......
import {
chakra,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
......@@ -14,6 +13,7 @@ import {
import React from 'react';
import { times } from 'lib/html-entities';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
......
......@@ -4,7 +4,6 @@ import {
Flex,
Grid,
Hide,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
......@@ -23,6 +22,7 @@ import { route } from 'nextjs-routes';
import type { ResourceError } from 'lib/api/resources';
import dayjs from 'lib/date/dayjs';
import Popover from 'ui/shared/chakra/Popover';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import { PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { FormattedData } from './types';
import Popover from 'ui/shared/chakra/Popover';
import TokenSelectButton from './TokenSelectButton';
import TokenSelectMenu from './TokenSelectMenu';
import useTokenSelect from './useTokenSelect';
......
......@@ -53,7 +53,7 @@ const TokenBalances = () => {
name="Net Worth"
value={ addressData?.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) }` : 'N/A' }
isLoading={ addressQuery.isPending || tokenQuery.isPending }
icon={ <IconSvg name="wallet" boxSize="24px" flexShrink={ 0 } color="text_secondary"/> }
icon={ <IconSvg name="wallet" boxSize="20px" flexShrink={ 0 } color="text_secondary"/> }
/>
<TokenBalancesItem
name={ `${ currencyUnits.ether } Balance` }
......
......@@ -86,7 +86,6 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Skeleton>
) : data.tx_count }
</Td>
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
<Flex mt={ 2 }>
......@@ -107,12 +106,13 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
) }
</Flex>
</Td>
) }
{ !isRollup && !config.UI.views.block.hiddenFields?.total_reward && (
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ totalReward.toFixed(8) }
</Skeleton>
</Td>
) }
{ !isRollup && !config.UI.views.block.hiddenFields?.burnt_fees && (
<Td fontSize="sm">
<Flex alignItems="center" columnGap={ 2 }>
......
import {
Link,
chakra,
Popover,
PopoverTrigger,
Portal,
PopoverContent,
......@@ -21,6 +20,7 @@ import type { FormFields } from '../types';
import type { SmartContractVerificationConfig, SmartContractVerificationMethod } from 'types/api/contract';
import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import IconSvg from 'ui/shared/IconSvg';
......@@ -105,7 +105,7 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
<Popover trigger="hover" isLazy placement={ isMobile ? 'bottom-end' : 'right-start' } offset={ [ -8, 8 ] }>
<PopoverTrigger>
<chakra.span display="inline-block" ml={ 1 } cursor="pointer" verticalAlign="middle" h="22px">
<IconSvg name="info" boxSize={ 5 } color="link" _hover={{ color: 'link_hovered' }}/>
<IconSvg name="info" boxSize={ 5 } color="icon_info" _hover={{ color: 'link_hovered' }}/>
</chakra.span>
</PopoverTrigger>
<Portal>
......
......@@ -67,7 +67,7 @@ const OptimisticDepositsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntityL1
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, ens_domain_name: null }}
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, ens_domain_name: null, implementations: null }}
isLoading={ isLoading }
noCopy
truncation="constant"
......
......@@ -59,7 +59,7 @@ const OptimisticDepositsTableItem = ({ item, isLoading }: Props) => {
</Td>
<Td verticalAlign="middle">
<AddressEntityL1
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, ens_domain_name: null }}
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, ens_domain_name: null, implementations: null }}
isLoading={ isLoading }
truncation="constant"
noCopy
......
......@@ -17,7 +17,7 @@ import { currencyUnits } from 'lib/units';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxFee from 'ui/shared/tx/TxFee';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType';
......@@ -96,13 +96,7 @@ const LatestTxsItem = ({ tx, isLoading }: Props) => {
{ !config.UI.views.tx.hiddenFields?.tx_fee && (
<Skeleton isLoaded={ !isLoading } display="flex" whiteSpace="pre" my="3px">
<Text as="span">Fee </Text>
{ tx.stability_fee ? (
<TxFeeStability data={ tx.stability_fee } accuracy={ 5 } color="text_secondary" hideUsd/>
) : (
<Text as="span" variant="secondary">
{ tx.fee.value ? `${ getValueWithUnit(tx.fee.value).dp(5).toFormat() } ${ currencyUnits.ether }` : '-' }
</Text>
) }
<TxFee tx={ tx } accuracy={ 5 } color="text_secondary"/>
</Skeleton>
) }
</Flex>
......
......@@ -16,7 +16,7 @@ import { currencyUnits } from 'lib/units';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxFee from 'ui/shared/tx/TxFee';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType';
......@@ -76,18 +76,14 @@ const LatestTxsItem = ({ tx, isLoading }: Props) => {
/>
{ !config.UI.views.tx.hiddenFields?.value && (
<Skeleton isLoaded={ !isLoading } mb={ 2 } fontSize="sm" w="fit-content">
<Text as="span">Value { currencyUnits.ether } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() }</Text>
<Text as="span">Value </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() } { currencyUnits.ether }</Text>
</Skeleton>
) }
{ !config.UI.views.tx.hiddenFields?.tx_fee && (
<Skeleton isLoaded={ !isLoading } fontSize="sm" w="fit-content" display="flex" whiteSpace="pre">
<Text as="span">Fee { !config.UI.views.tx.hiddenFields?.fee_currency ? `${ currencyUnits.ether } ` : '' }</Text>
{ tx.stability_fee ? (
<TxFeeStability data={ tx.stability_fee } accuracy={ 5 } color="text_secondary" hideUsd/>
) : (
<Text as="span" variant="secondary">{ tx.fee.value ? getValueWithUnit(tx.fee.value).dp(5).toFormat() : '-' }</Text>
) }
<Text as="span">Fee </Text>
<TxFee tx={ tx } accuracy={ 5 } color="text_secondary"/>
</Skeleton>
) }
</Box>
......
......@@ -3,7 +3,6 @@ import React from 'react';
import * as statsMock from 'mocks/stats/index';
import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import Stats from './Stats';
......@@ -18,14 +17,6 @@ test.describe('all items', () => {
test('+@mobile +@dark-mode', async() => {
await expect(component).toHaveScreenshot();
});
test.describe('screen xl', () => {
test.use({ viewport: pwConfig.viewport.xl });
test('', async() => {
await expect(component).toHaveScreenshot();
});
});
});
test('4 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => {
......
......@@ -2,8 +2,6 @@ import { Grid } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts';
......@@ -11,8 +9,7 @@ import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice';
import IconSvg from 'ui/shared/IconSvg';
import StatsItem from './StatsItem';
import StatsWidget from 'ui/shared/stats/StatsWidget';
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
const rollupFeature = config.features.rollup;
......@@ -58,7 +55,7 @@ const Stats = () => {
let content;
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
const lastItemStyle = { gridColumn: 'span 2' };
let itemsCount = 5;
!hasGasTracker && itemsCount--;
......@@ -75,12 +72,10 @@ const Stats = () => {
isLoading={ isLoading }
name="info"
boxSize={ 5 }
display="block"
flexShrink={ 0 }
cursor="pointer"
color="icon_info"
_hover={{ color: 'link_hovered' }}
position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
right="10px"
/>
</GasInfoTooltip>
) : null;
......@@ -88,80 +83,80 @@ const Stats = () => {
content = (
<>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && (
<StatsItem
icon="txn_batches"
title="Latest batch"
<StatsWidget
icon="txn_batches_slim"
label="Latest batch"
value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() }
url={ route({ pathname: '/batches' }) }
href={{ pathname: '/batches' }}
isLoading={ isLoading }
/>
) }
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && (
<StatsItem
icon="txn_batches"
title="Latest batch"
<StatsWidget
icon="txn_batches_slim"
label="Latest batch"
value={ (zkSyncLatestBatchQuery.data || 0).toLocaleString() }
url={ route({ pathname: '/batches' }) }
href={{ pathname: '/batches' }}
isLoading={ isLoading }
/>
) }
{ !(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync')) && (
<StatsItem
icon="block"
title="Total blocks"
<StatsWidget
icon="block_slim"
label="Total blocks"
value={ Number(data.total_blocks).toLocaleString() }
url={ route({ pathname: '/blocks' }) }
href={{ pathname: '/blocks' }}
isLoading={ isLoading }
/>
) }
{ hasAvgBlockTime && (
<StatsItem
icon="clock-light"
title="Average block time"
<StatsWidget
icon="clock"
label="Average block time"
value={ `${ (data.average_block_time / 1000).toFixed(1) }s` }
isLoading={ isLoading }
/>
) }
<StatsItem
icon="transactions"
title="Total transactions"
<StatsWidget
icon="transactions_slim"
label="Total transactions"
value={ Number(data.total_transactions).toLocaleString() }
url={ route({ pathname: '/txs' }) }
href={{ pathname: '/txs' }}
isLoading={ isLoading }
/>
{ rollupFeature.isEnabled && data.last_output_root_size && (
<StatsItem
icon="txn_batches"
title="Latest L1 state batch"
<StatsWidget
icon="txn_batches_slim"
label="Latest L1 state batch"
value={ data.last_output_root_size }
url={ route({ pathname: '/batches' }) }
href={{ pathname: '/batches' }}
isLoading={ isLoading }
/>
) }
<StatsItem
<StatsWidget
icon="wallet"
title="Wallet addresses"
label="Wallet addresses"
value={ Number(data.total_addresses).toLocaleString() }
_last={ isOdd ? lastItemTouchStyle : undefined }
isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/>
{ hasGasTracker && data.gas_prices && (
<StatsItem
<StatsWidget
icon="gas"
title="Gas tracker"
label="Gas tracker"
value={ data.gas_prices.average ? <GasPrice data={ data.gas_prices.average }/> : 'N/A' }
_last={ isOdd ? lastItemTouchStyle : undefined }
tooltip={ gasInfoTooltip }
hint={ gasInfoTooltip }
isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/>
) }
{ data.rootstock_locked_btc && (
<StatsItem
<StatsWidget
icon="coins/bitcoin"
title="BTC Locked in 2WP"
label="BTC Locked in 2WP"
value={ `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC` }
_last={ isOdd ? lastItemTouchStyle : undefined }
isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/>
) }
</>
......@@ -170,10 +165,10 @@ const Stats = () => {
return (
<Grid
gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: '1fr 1fr' }}
gridTemplateRows={{ lg: 'none', base: undefined }}
gridTemplateColumns="1fr 1fr"
gridGap={{ base: 1, lg: 2 }}
marginTop={ 3 }
flexBasis="50%"
flexGrow={ 1 }
>
{ content }
</Grid>
......
import type { SystemStyleObject } from '@chakra-ui/react';
import { Skeleton, Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
import breakpoints from 'theme/foundations/breakpoints';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
type Props = {
icon: IconName;
title: string;
value: string | React.ReactNode;
className?: string;
tooltip?: React.ReactNode;
url?: string;
isLoading?: boolean;
}
const LARGEST_BREAKPOINT = '1240px';
const StatsItem = ({ icon, title, value, className, tooltip, url, isLoading }: Props) => {
const sxContainer: SystemStyleObject = {
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { flexDirection: 'column' },
};
const sxText: SystemStyleObject = {
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { alignItems: 'center' },
};
const bgColor = useColorModeValue('blue.50', 'whiteAlpha.100');
const loadingBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return (
<Flex
backgroundColor={ isLoading ? loadingBgColor : bgColor }
padding={ 3 }
borderRadius="md"
flexDirection="row"
sx={ sxContainer }
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
className={ className }
color={ useColorModeValue('black', 'white') }
position="relative"
{ ...(url && !isLoading ? {
as: 'a',
href: url,
} : {}) }
>
<IconSvg name={ icon } boxSize={ 7 } isLoading={ isLoading } borderRadius="base"/>
<Flex
flexDirection="column"
alignItems="start"
sx={ sxText }
>
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontSize="xs" lineHeight="16px" borderRadius="base">
<span>{ title }</span>
</Skeleton>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') } borderRadius="base">
{ typeof value === 'string' ? <span>{ value }</span> : value }
</Skeleton>
</Flex>
{ tooltip }
</Flex>
);
};
export default chakra(StatsItem);
import { Flex } from '@chakra-ui/react';
import { chakra, Flex, Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -15,21 +15,25 @@ const ChainIndicatorChartContainer = ({ data, isError, isPending }: Props) => {
const content = (() => {
if (isPending) {
return <ContentLoader mt="auto"/>;
return <ContentLoader mt="auto" fontSize="xs"/>;
}
if (isError) {
return <DataFetchAlert/>;
return <DataFetchAlert fontSize="xs" p={ 3 }/>;
}
if (data[0].items.length === 0) {
return <span>no data</span>;
return <chakra.span fontSize="xs">no data</chakra.span>;
}
return <ChainIndicatorChart data={ data }/>;
return (
<Box mx="-10px" my="-5px" h="calc(100% + 10px)" w="calc(100% + 20px)">
<ChainIndicatorChart data={ data }/>
</Box>
);
})();
return <Flex h={{ base: '150px', lg: 'auto' }} minH="150px" alignItems="flex-start" flexGrow={ 1 }>{ content }</Flex>;
return <Flex h={{ base: '80px', lg: '110px' }} alignItems="flex-start" flexGrow={ 1 }>{ content }</Flex>;
};
export default React.memo(ChainIndicatorChartContainer);
......@@ -6,8 +6,6 @@ import type { HomeStats } from 'types/api/stats';
import type { ChainIndicatorId } from 'types/homepage';
import type { ResourceError } from 'lib/api/resources';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
id: ChainIndicatorId;
......@@ -21,42 +19,27 @@ interface Props {
}
const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onClick, stats }: Props) => {
const isMobile = useIsMobile();
const activeBgColorDesktop = useColorModeValue('white', 'gray.900');
const activeBgColorMobile = useColorModeValue('white', 'black');
const activeBgColor = isMobile ? activeBgColorMobile : activeBgColorDesktop;
const activeColor = useColorModeValue('gray.500', 'gray.400');
const activeBgColor = useColorModeValue('white', 'black');
const handleClick = React.useCallback(() => {
onClick(id);
}, [ id, onClick ]);
const valueContent = (() => {
if (isMobile) {
return null;
}
if (stats.isPlaceholderData) {
return (
<Skeleton
h={ 3 }
w="70px"
my={ 1.5 }
// ssr: isMobile = undefined, isLoading = true
display={{ base: 'none', lg: 'block' }}
/>
);
}
if (!stats.data) {
return <Text variant="secondary" fontWeight={ 400 }>no data</Text>;
}
return <Text variant="secondary" fontWeight={ 600 }>{ value(stats.data) }</Text>;
return (
<Skeleton isLoaded={ !stats.isPlaceholderData } variant="secondary" fontWeight={ 600 } minW="30px">
{ value(stats.data) }
</Skeleton>
);
})();
const valueDiffContent = (() => {
if (isMobile || !valueDiff) {
if (!valueDiff) {
return null;
}
const diff = valueDiff(stats.data);
......@@ -67,9 +50,9 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC
const diffColor = diff >= 0 ? 'green.500' : 'red.500';
return (
<Skeleton isLoaded={ !stats.isPlaceholderData } ml={ 3 } display="flex" alignItems="center" color={ diffColor }>
<IconSvg name="arrows/up-head" boxSize={ 5 } mr={ 1 } transform={ diff < 0 ? 'rotate(180deg)' : 'rotate(0)' }/>
<Text color={ diffColor } fontWeight={ 600 }>{ diff }%</Text>
<Skeleton isLoaded={ !stats.isPlaceholderData } ml={ 1 } display="flex" alignItems="center" color={ diffColor }>
<span>{ diff >= 0 ? '+' : '-' }</span>
<Text color={ diffColor } fontWeight={ 600 }>{ Math.abs(diff) }%</Text>
</Skeleton>
);
})();
......@@ -77,25 +60,28 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC
return (
<Flex
alignItems="center"
columnGap={ 3 }
px={ 4 }
py={ 2 }
columnGap={ 2 }
flexGrow={{ base: 0, lg: 1 }}
px={{ base: '6px', lg: 2 }}
py="6px"
as="li"
borderRadius="md"
borderRadius="base"
cursor="pointer"
color={ isSelected ? activeColor : 'link' }
bgColor={ isSelected ? activeBgColor : undefined }
onClick={ handleClick }
bgColor={ isSelected ? activeBgColor : 'inherit' }
boxShadow={ isSelected ? 'lg' : 'none' }
zIndex={ isSelected ? 1 : 'initial' }
fontSize="xs"
fontWeight={ 500 }
_hover={{
activeBgColor,
bgColor: activeBgColor,
color: isSelected ? activeColor : 'link_hovered',
zIndex: 1,
}}
>
{ icon }
<Box>
<Text fontFamily="heading" fontWeight={ 500 }>{ title }</Text>
<Flex alignItems="center">
<Box display={{ base: 'none', lg: 'block' }}>
<span>{ title }</span>
<Flex alignItems="center" color="text">
{ valueContent }
{ valueDiffContent }
</Flex>
......
......@@ -22,7 +22,10 @@ test.describe('daily txs chart', () => {
await mockApiResponse('stats_charts_txs', dailyTxsMock.base);
await mockAssetResponse(statsMock.withSecondaryCoin.coin_image as string, './playwright/mocks/image_svg.svg');
component = await render(<ChainIndicators/>);
await page.hover('.ChartOverlay', { position: { x: 100, y: 100 } });
await page.waitForFunction(() => {
return document.querySelector('path[data-name="gradient-chart-area"]')?.getAttribute('opacity') === '1';
});
await page.hover('.ChartOverlay', { position: { x: 50, y: 50 } });
});
test('+@mobile', async() => {
......@@ -38,9 +41,11 @@ test.describe('daily txs chart', () => {
});
});
test('partial data', async({ page, mockApiResponse, render }) => {
test('partial data', async({ page, mockApiResponse, mockAssetResponse, render }) => {
await mockApiResponse('stats', statsMock.base);
await mockApiResponse('stats_charts_txs', dailyTxsMock.partialData);
await mockAssetResponse(statsMock.base.coin_image as string, './playwright/mocks/image_s.jpg');
const component = await render(<ChainIndicators/>);
await page.waitForFunction(() => {
return document.querySelector('path[data-name="gradient-chart-area"]')?.getAttribute('opacity') === '1';
......@@ -48,9 +53,11 @@ test('partial data', async({ page, mockApiResponse, render }) => {
await expect(component).toHaveScreenshot();
});
test('no data', async({ mockApiResponse, render }) => {
test('no data', async({ mockApiResponse, mockAssetResponse, render }) => {
await mockApiResponse('stats', statsMock.noChartData);
await mockApiResponse('stats_charts_txs', dailyTxsMock.noData);
await mockAssetResponse(statsMock.base.coin_image as string, './playwright/mocks/image_s.jpg');
const component = await render(<ChainIndicators/>);
await expect(component).toHaveScreenshot();
});
......@@ -38,10 +38,7 @@ const ChainIndicators = () => {
},
});
const bgColorDesktop = useColorModeValue('white', 'gray.900');
const bgColorMobile = useColorModeValue('white', 'black');
const listBgColorDesktop = useColorModeValue('gray.50', 'black');
const listBgColorMobile = useColorModeValue('gray.50', 'gray.900');
const bgColor = useColorModeValue('gray.50', 'whiteAlpha.100');
if (indicators.length === 0) {
return null;
......@@ -49,15 +46,15 @@ const ChainIndicators = () => {
const valueTitle = (() => {
if (statsQueryResult.isPlaceholderData) {
return <Skeleton h="48px" w="215px" mt={ 3 } mb={ 4 }/>;
return <Skeleton h="36px" w="215px"/>;
}
if (!statsQueryResult.data) {
return <Text mt={ 3 } mb={ 4 }>There is no data</Text>;
return <Text fontSize="xs">There is no data</Text>;
}
return (
<Text fontWeight={ 600 } fontFamily="heading" fontSize="48px" lineHeight="48px" mt={ 3 }>
<Text fontWeight={ 700 } fontSize="30px" lineHeight="36px">
{ indicator?.value(statsQueryResult.data) }
</Text>
);
......@@ -85,23 +82,22 @@ const ChainIndicators = () => {
return (
<Flex
p={{ base: 0, lg: 8 }}
borderRadius={{ base: 'none', lg: 'lg' }}
boxShadow={{ base: 'none', lg: 'xl' }}
bgColor={{ base: bgColorMobile, lg: bgColorDesktop }}
columnGap={ 6 }
px={{ base: 3, lg: 4 }}
py={ 3 }
borderRadius="base"
bgColor={ bgColor }
columnGap={{ base: 3, lg: 4 }}
rowGap={ 0 }
flexDir={{ base: 'column', lg: 'row' }}
w="100%"
flexBasis="50%"
flexGrow={ 1 }
alignItems="stretch"
mt={{ base: 1, lg: 3 }}
>
<Flex flexGrow={ 1 } flexDir="column" order={{ base: 2, lg: 1 }} p={{ base: 6, lg: 0 }}>
<Flex flexGrow={ 1 } flexDir="column">
<Flex alignItems="center">
<Text fontWeight={ 500 } fontFamily="heading" fontSize="lg">{ indicator?.title }</Text>
<Text fontWeight={ 500 }>{ indicator?.title }</Text>
{ indicator?.hint && <Hint label={ indicator.hint } ml={ 1 }/> }
</Flex>
<Flex mb={ 4 } alignItems="end">
<Flex mb={{ base: 0, lg: 2 }} mt={ 1 } alignItems="end">
{ valueTitle }
{ valueDiff }
</Flex>
......@@ -112,11 +108,9 @@ const ChainIndicators = () => {
flexShrink={ 0 }
flexDir="column"
as="ul"
p={ 3 }
borderRadius="lg"
bgColor={{ base: listBgColorMobile, lg: listBgColorDesktop }}
rowGap={ 3 }
order={{ base: 1, lg: 2 }}
rowGap="6px"
m={{ base: 'auto 0', lg: 0 }}
>
{ indicators.map((indicator) => (
<ChainIndicatorItem
......
......@@ -7,6 +7,7 @@ import config from 'configs/app';
import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import IconSvg from 'ui/shared/IconSvg';
import NativeTokenIcon from 'ui/shared/NativeTokenIcon';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
const nonNullTailReducer = (result: Array<TimeChartItemRaw>, item: TimeChartItemRaw) => {
if (item.value === null && result.length === 0) {
......@@ -70,7 +71,7 @@ const secondaryCoinPriceIndicator: TChainIndicator<'stats_charts_secondary_coin_
'$N/A' :
'$' + Number(stats.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
valueDiff: () => null,
icon: <NativeTokenIcon boxSize={ 6 }/>,
icon: <TokenLogoPlaceholder boxSize={ 6 }/>,
hint: `${ config.chain.secondaryCoin.symbol } token daily price in USD.`,
api: {
resourceName: 'stats_charts_secondary_coin_price',
......
import { Box, Text, Link, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, chakra, Flex, Divider, Icon } from '@chakra-ui/react';
import { Box, Text, Link, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, chakra, Flex, Divider, Icon } from '@chakra-ui/react';
import React from 'react';
import type { MarketplaceAppSecurityReport } from 'types/client/marketplace';
......@@ -11,6 +11,7 @@ import config from 'configs/app';
import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import { apos } from 'lib/html-entities';
import * as mixpanel from 'lib/mixpanel/index';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
......
import { Box, Text, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, Icon } from '@chakra-ui/react';
import { Box, Text, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, Icon } from '@chakra-ui/react';
import React from 'react';
import type { SolidityscanReport } from 'types/api/contract';
......@@ -9,6 +9,7 @@ import config from 'configs/app';
// eslint-disable-next-line no-restricted-imports
import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import * as mixpanel from 'lib/mixpanel/index';
import Popover from 'ui/shared/chakra/Popover';
import LinkExternal from 'ui/shared/links/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
......
......@@ -208,7 +208,7 @@ const MarketplaceAppCard = ({
showContractList={ showContractList }
isLoading={ isLoading }
source="Discovery view"
popoverPlacement={ isMobile ? 'bottom-end' : 'bottom-start' }
popoverPlacement={ isMobile ? 'bottom-end' : 'left' }
position="absolute"
right={{ base: 3, md: 5 }}
top={{ base: '10px', md: 5 }}
......
import {
Popover, PopoverTrigger, PopoverContent, PopoverBody,
PopoverTrigger, PopoverContent, PopoverBody,
Modal, ModalContent, ModalCloseButton, useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
......@@ -7,6 +7,7 @@ import React from 'react';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover';
import Content from './MarketplaceAppInfo/Content';
import TriggerButton from './MarketplaceAppInfo/TriggerButton';
......
......@@ -53,7 +53,7 @@ const NameDomainsActionBar = ({
minW={{ base: 'auto', lg: '250px' }}
size="xs"
onChange={ onSearchChange }
placeholder="Search by name"
placeholder="Search by name or address"
initialValue={ searchTerm }
isLoading={ isInitialLoading }
/>
......
......@@ -272,7 +272,7 @@ const AddressPageContent = () => {
/>
) }
<AddressEntity
address={{ ...addressQuery.data, hash, name: '', ens_domain_name: '' }}
address={{ ...addressQuery.data, hash, name: '', ens_domain_name: '', implementations: null }}
isLoading={ isLoading }
fontFamily="heading"
fontSize="lg"
......
......@@ -64,21 +64,12 @@ const GasTracker = () => {
</Flex>
);
const content = (() => {
const snippets = (() => {
if (!isPlaceholderData && data?.gas_prices?.slow === null && data?.gas_prices.average === null && data.gas_prices.fast === null) {
return <Alert status="warning">No data available yet</Alert>;
return <Alert status="warning">No recent data available</Alert>;
}
return (
<>
{ data?.gas_prices && <GasTrackerPrices prices={ data.gas_prices } isLoading={ isLoading }/> }
{ config.features.stats.isEnabled && (
<Box mt={ 12 }>
<GasTrackerChart/>
</Box>
) }
</>
);
return data?.gas_prices ? <GasTrackerPrices prices={ data.gas_prices } isLoading={ isLoading }/> : null;
})();
return (
......@@ -88,7 +79,12 @@ const GasTracker = () => {
secondRow={ titleSecondRow }
withTextAd
/>
{ content }
{ snippets }
{ config.features.stats.isEnabled && (
<Box mt={ 12 }>
<GasTrackerChart/>
</Box>
) }
</>
);
};
......
......@@ -2,6 +2,7 @@ import { Box, Flex, Heading } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestBlocks from 'ui/home/LatestBlocks';
import LatestZkEvmL2Batches from 'ui/home/LatestZkEvmL2Batches';
......@@ -15,23 +16,26 @@ import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
const rollupFeature = config.features.rollup;
const Home = () => {
const isMobile = useIsMobile();
return (
<Box as="main">
<Box
<Flex
w="100%"
background={ config.UI.homepage.plate.background }
borderRadius={{ base: 'md', lg: 'xl' }}
px={{ base: 4, lg: 10 }}
py={{ base: 3, lg: 8 }}
minW={{ base: 'unset', lg: '900px' }}
borderRadius="md"
p={{ base: 4, lg: 8 }}
columnGap={ 8 }
alignItems="center"
data-label="hero plate"
>
<Flex mb={{ base: 2, lg: 6 }} justifyContent="space-between" alignItems="center">
<Box flexGrow={ 1 }>
<Flex mb={{ base: 2, lg: 3 }} justifyContent="space-between" alignItems="center" columnGap={ 2 }>
<Heading
as="h1"
fontSize={{ base: '18px', lg: '40px' }}
lineHeight={{ base: '24px', lg: '48px' }}
fontWeight={ 600 }
fontSize={{ base: '18px', lg: '30px' }}
lineHeight={{ base: '24px', lg: '36px' }}
fontWeight={{ base: 500, lg: 700 }}
color={ config.UI.homepage.plate.textColor }
>
{
......@@ -49,10 +53,14 @@ const Home = () => {
</Flex>
<SearchBar isHomepage/>
</Box>
{ !isMobile && <AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden"/> }
</Flex>
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 1 } mt={ 3 } _empty={{ mt: 0 }}>
<Stats/>
<ChainIndicators/>
<AdBanner mt={ 6 } mx="auto" display="flex" justifyContent="center"/>
<Flex mt={ 6 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }>
</Flex>
{ isMobile && <AdBanner mt={ 6 } mx="auto" display="flex" justifyContent="center"/> }
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' ? <LatestZkEvmL2Batches/> : <LatestBlocks/> }
<Box flexGrow={ 1 }>
<Transactions/>
......
import { Box, Menu, MenuButton, MenuItem, MenuList, Flex, IconButton } from '@chakra-ui/react';
import { Box, MenuButton, MenuItem, MenuList, Flex, IconButton } from '@chakra-ui/react';
import React from 'react';
import type { MouseEvent } from 'react';
......@@ -14,6 +14,7 @@ import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList';
import { SORT_OPTIONS } from 'ui/marketplace/utils';
import Menu from 'ui/shared/chakra/Menu';
import FilterInput from 'ui/shared/filters/FilterInput';
import IconSvg from 'ui/shared/IconSvg';
import type { IconName } from 'ui/shared/IconSvg';
......
......@@ -33,9 +33,9 @@ const testFn: Parameters<typeof test>[1] = async({ render, mockConfigResponse, m
await expect(component).toHaveScreenshot();
};
test('base view +@dark-mode', testFn);
test.fixme('base view +@dark-mode', testFn);
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', testFn);
test.fixme('base view', testFn);
});
import { inRange } from 'lodash';
import _inRange from 'lodash/inRange';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -43,7 +43,11 @@ const UserOp = () => {
if (!userOpQuery.data) {
return true;
} else {
if (inRange(Number(tt.log_index), userOpQuery.data?.user_logs_start_index, userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count)) {
if (_inRange(
Number(tt.log_index),
userOpQuery.data?.user_logs_start_index,
userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count,
)) {
return true;
}
return false;
......@@ -54,7 +58,7 @@ const UserOp = () => {
if (!userOpQuery.data) {
return true;
} else {
if (inRange(log.index, userOpQuery.data?.user_logs_start_index, userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count)) {
if (_inRange(log.index, userOpQuery.data?.user_logs_start_index, userOpQuery.data?.user_logs_start_index + userOpQuery.data?.user_logs_count)) {
return true;
}
return false;
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import { PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import _debounce from 'lodash/debounce';
import type { FormEvent, FocusEvent } from 'react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import { getRecentSearchKeywords } from 'lib/recentSearchKeywords';
import Popover from 'ui/shared/chakra/Popover';
import SearchBarBackdrop from 'ui/snippets/searchBar/SearchBarBackdrop';
import SearchBarInput from 'ui/snippets/searchBar/SearchBarInput';
import SearchBarRecentKeywords from 'ui/snippets/searchBar/SearchBarRecentKeywords';
......@@ -76,7 +77,7 @@ const SearchResultsInput = ({ searchTerm, handleSubmit, handleSearchTermChange }
autoFocus={ false }
onClose={ onClose }
placement="bottom-start"
offset={ isMobile ? [ 16, -12 ] : undefined }
offset={ isMobile ? [ 16, -12 ] : [ 0, 8 ] }
isLazy
>
<PopoverTrigger>
......
import { Box, IconButton, Menu, MenuButton, MenuList, Skeleton, chakra } from '@chakra-ui/react';
import { Box, IconButton, MenuButton, MenuList, Skeleton, chakra } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -9,6 +9,7 @@ import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString';
import Menu from 'ui/shared/chakra/Menu';
import IconSvg from 'ui/shared/IconSvg';
import MetadataUpdateMenuItem from './items/MetadataUpdateMenuItem';
......
......@@ -42,7 +42,7 @@ const AdditionalInfoButton = ({ isOpen, onClick, className, isLoading }: Props,
<IconSvg
name="info"
boxSize={ 5 }
color="link"
color={ isOpen ? 'link_hovered' : 'icon_info' }
_hover={{ color: 'link_hovered' }}
/>
</Button>
......
import { Box, Text, chakra, Skeleton } from '@chakra-ui/react';
import { chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import getCurrencyValue from 'lib/getCurrencyValue';
......@@ -23,20 +23,20 @@ const CurrencyValue = ({ value, currency = '', decimals, exchangeRate, className
if (value === undefined || value === null) {
return (
<Box as="span" className={ className }>
<Text>N/A</Text>
</Box>
<chakra.span className={ className }>
-
</chakra.span>
);
}
const { valueStr: valueResult, usd: usdResult } = getCurrencyValue({ value, accuracy, accuracyUsd, exchangeRate, decimals });
return (
<Box as="span" className={ className } display="inline-flex" rowGap={ 3 } columnGap={ 1 }>
<Text display="inline-block">
<chakra.span className={ className } display="inline-flex" rowGap={ 3 } columnGap={ 1 }>
<chakra.span display="inline-block">
{ valueResult }{ currency ? ` ${ currency }` : '' }
</Text>
{ usdResult && <Text as="span" variant="secondary" fontWeight={ 400 }>(${ usdResult })</Text> }
</Box>
</chakra.span>
{ usdResult && <chakra.span color="text_secondary" fontWeight={ 400 }>(${ usdResult })</chakra.span> }
</chakra.span>
);
};
......
import { chakra, Image, Flex, Popover, PopoverArrow, PopoverBody, PopoverContent, PopoverTrigger, useColorModeValue, DarkMode } from '@chakra-ui/react';
import { chakra, Image, Flex, PopoverArrow, PopoverBody, PopoverContent, PopoverTrigger, useColorModeValue, DarkMode } from '@chakra-ui/react';
import React from 'react';
import type { EntityTag } from './types';
import makePrettyLink from 'lib/makePrettyLink';
import * as mixpanel from 'lib/mixpanel/index';
import Popover from 'ui/shared/chakra/Popover';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props {
......@@ -35,7 +36,7 @@ const EntityTagPopover = ({ data, children }: Props) => {
}
return (
<Popover trigger="hover" isLazy>
<Popover trigger="hover" isLazy gutter={ 8 }>
<PopoverTrigger>
{ children }
</PopoverTrigger>
......
import { Box, Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, chakra } from '@chakra-ui/react';
import { Box, Flex, PopoverBody, PopoverContent, PopoverTrigger, chakra } from '@chakra-ui/react';
import React from 'react';
import type { EntityTag as TEntityTag } from './types';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover';
import Tag from 'ui/shared/chakra/Tag';
import EntityTag from './EntityTag';
......
......@@ -35,7 +35,7 @@ const Hint = ({ label, className, tooltipProps, isLoading }: Props) => {
<IconButton
colorScheme="none"
aria-label="hint"
icon={ <IconSvg name="info" w="100%" h="100%"/> }
icon={ <IconSvg name="info" w="100%" h="100%" color="icon_info" _hover={{ color: 'link_hovered' }}/> }
boxSize={ 5 }
variant="simple"
display="inline-block"
......
import {
Image,
Button,
Popover,
PopoverTrigger,
PopoverBody,
PopoverContent,
......@@ -18,6 +17,7 @@ import type { NetworkExplorer as TNetworkExplorer } from 'types/networks';
import config from 'configs/app';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip';
......
import type {
ButtonProps } from '@chakra-ui/react';
import { Popover,
import {
PopoverTrigger,
PopoverContent,
PopoverBody,
......@@ -12,6 +12,8 @@ import React from 'react';
import type { MenuButton, TabItem } from './types';
import Popover from 'ui/shared/chakra/Popover';
import TabCounter from './TabCounter';
import { menuButton } from './utils';
......
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { BannerPlatform } from './types';
import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies';
......@@ -9,7 +11,13 @@ import AdBannerContent from './AdBannerContent';
const feature = config.features.adsBanner;
const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: boolean }) => {
interface Props {
className?: string;
isLoading?: boolean;
platform?: BannerPlatform;
}
const AdBanner = ({ className, isLoading, platform }: Props) => {
const provider = useAppContext().adBannerProvider;
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
......@@ -23,6 +31,7 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo
className={ className }
isLoading={ isLoading }
provider={ provider }
platform={ platform }
/>
);
};
......
import { chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { BannerPlatform } from './types';
import type { AdBannerProviders } from 'types/client/adProviders';
import config from 'configs/app';
import AdbutlerBanner from './AdbutlerBanner';
import CoinzillaBanner from './CoinzillaBanner';
import GetitBanner from './GetitBanner';
// import GetitBanner from './GetitBanner';
import HypeBanner from './HypeBanner';
import SliseBanner from './SliseBanner';
const feature = config.features.adsBanner;
const AdBannerContent = ({ className, isLoading, provider }: { className?: string; isLoading?: boolean; provider: AdBannerProviders }) => {
interface Props {
className?: string;
isLoading?: boolean;
platform?: BannerPlatform;
provider: AdBannerProviders;
}
const AdBannerContent = ({ className, isLoading, provider, platform }: Props) => {
const content = (() => {
switch (provider) {
case 'adbutler':
return <AdbutlerBanner/>;
return <AdbutlerBanner platform={ platform }/>;
case 'coinzilla':
return <CoinzillaBanner/>;
case 'getit':
return <GetitBanner/>;
return <CoinzillaBanner platform={ platform }/>;
// case 'getit':
// return <GetitBanner platform={ platform }/>;
case 'hype':
return <HypeBanner/>;
return <HypeBanner platform={ platform }/>;
case 'slise':
return <SliseBanner/>;
return <SliseBanner platform={ platform }/>;
}
})();
......
......@@ -3,6 +3,8 @@ import { useRouter } from 'next/navigation';
import Script from 'next/script';
import React from 'react';
import type { BannerProps } from './types';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import isBrowser from 'lib/isBrowser';
......@@ -10,9 +12,10 @@ import { connectAdbutler, placeAd, ADBUTLER_ACCOUNT } from 'ui/shared/ad/adbutle
const feature = config.features.adsBanner;
const AdbutlerBanner = ({ className }: { className?: string }) => {
const AdbutlerBanner = ({ className, platform }: BannerProps) => {
const router = useRouter();
const isMobile = useIsMobile();
const isMobileViewport = useIsMobile();
const isMobile = platform === 'mobile' || isMobileViewport;
React.useEffect(() => {
if (!('adButler' in feature)) {
......@@ -24,10 +27,10 @@ const AdbutlerBanner = ({ className }: { className?: string }) => {
if (!window.AdButler.ads) {
window.AdButler.ads = [];
}
const adButlerConfig = isMobile ? feature.adButler.config.mobile : feature.adButler.config.desktop;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
let plc = window[`plc${ feature.adButler.config.mobile.id }`] || 0;
const adButlerConfig = isMobile ? feature.adButler.config.mobile : feature.adButler.config.desktop;
let plc = window[`plc${ adButlerConfig.id }`] || 0;
const banner = document.getElementById('ad-banner');
if (banner) {
banner.innerHTML = '<' + 'div id="placement_' + adButlerConfig?.id + '_' + plc + '"></' + 'div>';
......@@ -46,10 +49,34 @@ const AdbutlerBanner = ({ className }: { className?: string }) => {
}
}, [ router, isMobile ]);
if (!('adButler' in feature)) {
return null;
}
const { width, height } = (() => {
switch (platform) {
case 'desktop':
return { width: `${ feature.adButler.config.desktop.width }px`, height: `${ feature.adButler.config.desktop.height }px` };
case 'mobile':
return { width: `${ feature.adButler.config.mobile.width }px`, height: `${ feature.adButler.config.mobile.height }px` };
default:
return {
width: {
base: `${ feature.adButler.config.mobile.width }px`,
lg: `${ feature.adButler.config.desktop.width }px`,
},
height: {
base: `${ feature.adButler.config.mobile.height }px`,
lg: `${ feature.adButler.config.desktop.height }px`,
},
};
}
})() ?? { width: '0', height: '0' };
return (
<Flex className={ className } id="adBanner" h={{ base: '100px', lg: '90px' }}>
<Flex className={ className } id="adBanner" h={ height } w={ width }>
<Script strategy="lazyOnload" id="ad-butler-1">{ connectAdbutler }</Script>
<Script strategy="lazyOnload" id="ad-butler-2">{ placeAd }</Script>
<Script strategy="lazyOnload" id="ad-butler-2">{ placeAd(platform) }</Script>
<div id="ad-banner"></div>
</Flex>
);
......
......@@ -2,25 +2,44 @@ import { Flex, chakra } from '@chakra-ui/react';
import Script from 'next/script';
import React from 'react';
import type { BannerProps } from './types';
import isBrowser from 'lib/isBrowser';
const CoinzillaBanner = ({ className }: { className?: string }) => {
const CoinzillaBanner = ({ className, platform }: BannerProps) => {
const isInBrowser = isBrowser();
const { width, height } = (() => {
switch (platform) {
case 'desktop':
return { width: 728, height: 90 };
case 'mobile':
return { width: 320, height: 100 };
default:
return { width: undefined, height: undefined };
}
})();
React.useEffect(() => {
if (isInBrowser) {
window.coinzilla_display = window.coinzilla_display || [];
const cDisplayPreferences = {
zone: '26660bf627543e46851',
width: '728',
height: '90',
width: width ? String(width) : '728',
height: height ? String(height) : '90',
};
window.coinzilla_display.push(cDisplayPreferences);
}
}, [ isInBrowser ]);
}, [ height, isInBrowser, width ]);
return (
<Flex className={ className } id="adBanner" h={{ base: '100px', lg: '90px' }}>
<Flex
className={ className }
id={ 'adBanner' + (platform ? `_${ platform }` : '') }
h={ height ? `${ height }px` : { base: '100px', lg: '90px' } }
w={ width ? `${ width }px` : undefined }
>
<Script strategy="lazyOnload" src="https://coinzillatag.com/lib/display.js"/>
<div className="coinzilla" data-zone="C-26660bf627543e46851"></div>
</Flex>
......
......@@ -2,6 +2,8 @@ import { Flex, chakra } from '@chakra-ui/react';
import dynamic from 'next/dynamic';
import React from 'react';
import type { BannerProps } from './types';
import useIsMobile from 'lib/hooks/useIsMobile';
import useAccount from 'lib/web3/useAccount';
......@@ -9,17 +11,17 @@ const GetitAdPlugin = dynamic(() => import('getit-sdk').then(module => module.Ge
const GETIT_API_KEY = 'ZmGXVvwYUAW4yXL8RzWQHNKmpSyQmt3TDXsXUxqFqXPdoaiSSFyca3BOyunDcWdyOwTkX3UVVQel28qbjoOoWPxYVpPdNzbUNkAHyFyJX7Lk9TVcPDZKTQmwHlSMzO3a';
const GetitBanner = ({ className }: { className?: string }) => {
const GetitBanner = ({ className, platform }: BannerProps) => {
const isMobile = Boolean(useIsMobile());
const { address } = useAccount();
return (
<Flex className={ className } h="90px">
<Flex className={ className } h="90px" w={{ base: '270px', lg: platform === 'mobile' ? '270px' : undefined }}>
<GetitAdPlugin
key={ isMobile.toString() }
apiKey={ GETIT_API_KEY }
walletConnected={ address ? address : '' }
isMobile={ isMobile }
isMobile={ platform === 'mobile' || isMobile }
slotId="0"
/>
</Flex>
......
......@@ -3,6 +3,8 @@ import { Banner, setWalletAddresses } from '@hypelab/sdk-react';
import Script from 'next/script';
import React from 'react';
import type { BannerProps } from './types';
import useAccount from 'lib/web3/useAccount';
import { hypeInit } from './hypeBannerScript';
......@@ -10,7 +12,7 @@ import { hypeInit } from './hypeBannerScript';
const DESKTOP_BANNER_SLUG = 'b1559fc3e7';
const MOBILE_BANNER_SLUG = '668ed80a9e';
const HypeBanner = ({ className }: { className?: string }) => {
const HypeBanner = ({ className, platform }: BannerProps) => {
const { address } = useAccount();
React.useEffect(() => {
......@@ -19,20 +21,48 @@ const HypeBanner = ({ className }: { className?: string }) => {
}
}, [ address ]);
const banner = (() => {
switch (platform) {
case 'desktop': {
return (
<Flex className={ className } w="728px" h="90px">
<Banner placement={ DESKTOP_BANNER_SLUG }/>
</Flex>
);
}
case 'mobile': {
return (
<Flex className={ className } w="320px" h="50px">
<Banner placement={ MOBILE_BANNER_SLUG }/>
</Flex>
);
}
default: {
return (
<>
<Script
id="hypelab"
strategy="afterInteractive"
>{ hypeInit }</Script>
<Flex className={ className } h="90px" display={{ base: 'none', lg: 'flex' }}>
<Flex className={ className } w="728px" h="90px" display={{ base: 'none', lg: 'flex' }}>
<Banner placement={ DESKTOP_BANNER_SLUG }/>
</Flex>
<Flex className={ className } h="50px" display={{ base: 'flex', lg: 'none' }}>
<Flex className={ className } w="320px" h="50px" display={{ base: 'flex', lg: 'none' }}>
<Banner placement={ MOBILE_BANNER_SLUG }/>
</Flex>
</>
);
}
}
})();
return (
<>
<Script
id="hypelab"
strategy="afterInteractive"
>
{ hypeInit }
</Script>
{ banner }
</>
);
};
export default chakra(HypeBanner);
......@@ -2,9 +2,35 @@ import { Flex, chakra } from '@chakra-ui/react';
import { SliseAd } from '@slise/embed-react';
import React from 'react';
import type { BannerProps } from './types';
import config from 'configs/app';
const SliseBanner = ({ className }: { className?: string }) => {
const SliseBanner = ({ className, platform }: BannerProps) => {
if (platform === 'desktop') {
return (
<Flex className={ className } h="90px">
<SliseAd
slotId={ config.chain.name || '' }
pub="pub-10"
format="728x90"
style={{ width: '728px', height: '90px' }}/>
</Flex>
);
}
if (platform === 'mobile') {
return (
<Flex className={ className } h="90px">
<SliseAd
slotId={ config.chain.name || '' }
pub="pub-10"
format="270x90"
style={{ width: '270px', height: '90px' }}/>
</Flex>
);
}
return (
<>
......
/* eslint-disable max-len */
import type { BannerPlatform } from './types';
import config from 'configs/app';
export const ADBUTLER_ACCOUNT = 182226;
export const connectAdbutler = `if (!window.AdButler){(function(){var s = document.createElement("script"); s.async = true; s.type = "text/javascript";s.src = 'https://servedbyadbutler.com/app.js';var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(s, n);}());}`;
export const placeAd = (() => {
export const placeAd = ((platform: BannerPlatform | undefined) => {
const feature = config.features.adsBanner;
if (!('adButler' in feature)) {
return;
}
if (platform === 'mobile') {
return `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || '';
var plc${ feature.adButler.config.mobile.id } = window.plc${ feature.adButler.config.mobile.id } || 0;
document.getElementById('ad-banner').innerHTML = '<'+'div id="placement_${ feature.adButler.config.mobile.id }_'+plc${ feature.adButler.config.mobile.id }+'"></'+'div>';
AdButler.ads.push({handler: function(opt){ AdButler.register(${ ADBUTLER_ACCOUNT }, ${ feature.adButler.config.mobile.id }, [${ feature.adButler.config.mobile.width },${ feature.adButler.config.mobile.height }], 'placement_${ feature.adButler.config.mobile.id }_'+opt.place, opt); }, opt: { place: plc${ feature.adButler.config.mobile.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
`;
}
return `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || '';
......@@ -26,4 +38,4 @@ export const placeAd = (() => {
AdButler.ads.push({handler: function(opt){ AdButler.register(${ ADBUTLER_ACCOUNT }, ${ feature.adButler.config.desktop.id }, [${ feature.adButler.config.desktop.width },${ feature.adButler.config.desktop.height }], 'placement_${ feature.adButler.config.desktop.id }_'+opt.place, opt); }, opt: { place: plc${ feature.adButler.config.desktop.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
}
`;
})();
});
export type BannerPlatform = 'mobile' | 'desktop';
export interface BannerProps {
className?: string;
platform?: BannerPlatform;
}
import type { MenuProps } from '@chakra-ui/react';
// eslint-disable-next-line no-restricted-imports
import { Menu as MenuBase } from '@chakra-ui/react';
import React from 'react';
const Menu = (props: MenuProps) => {
return <MenuBase gutter={ 4 } { ...props }/>;
};
export default React.memo(Menu);
import type { PopoverProps } from '@chakra-ui/react';
// eslint-disable-next-line no-restricted-imports
import { Popover as PopoverBase } from '@chakra-ui/react';
import React from 'react';
const Popover = (props: PopoverProps) => {
return <PopoverBase gutter={ 4 } { ...props }/>;
};
export default React.memo(Popover);
......@@ -20,7 +20,7 @@ function getBgColor(status?: AlertStatus) {
}
}
const Toast = ({ onClose, title, description, id, isClosable, status }: ToastProps) => {
const Toast = ({ onClose, title, description, id, isClosable, status, icon }: ToastProps) => {
const ids = id ?
{
......@@ -48,7 +48,7 @@ const Toast = ({ onClose, title, description, id, isClosable, status }: ToastPro
maxWidth="400px"
>
<chakra.div flex="1" maxWidth="100%">
{ title && <AlertTitle id={ ids?.title }>{ title }</AlertTitle> }
{ title && <AlertTitle id={ ids?.title } display="flex" alignItems="center">{ icon }{ title }</AlertTitle> }
{ description && (
<AlertDescription id={ ids?.description } display="block">
{ description }
......
......@@ -4,7 +4,6 @@ import {
chakra,
Flex,
IconButton, Link,
Menu,
MenuButton,
MenuItem,
MenuList,
......@@ -22,6 +21,7 @@ import type { TimeChartItem } from './types';
import dayjs from 'lib/date/dayjs';
import { apos } from 'lib/html-entities';
import saveAsCSV from 'lib/saveAsCSV';
import Menu from 'ui/shared/chakra/Menu';
import IconSvg from 'ui/shared/IconSvg';
import ChartWidgetGraph from './ChartWidgetGraph';
......
......@@ -3,6 +3,7 @@ import React from 'react';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import * as addressMock from 'mocks/address/address';
import * as implementationsMock from 'mocks/address/implementations';
import { test, expect } from 'playwright/lib';
import AddressEntity from './AddressEntity';
......@@ -30,7 +31,7 @@ test.describe('contract', () => {
test('unverified', async({ render, page }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, is_verified: false }}
address={{ ...addressMock.contract, is_verified: false, implementations: null }}
/>,
);
......@@ -41,7 +42,7 @@ test.describe('contract', () => {
test('verified', async({ render }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, is_verified: true }}
address={{ ...addressMock.contract, is_verified: true, implementations: null }}
/>,
);
......@@ -49,6 +50,58 @@ test.describe('contract', () => {
});
});
test.describe('proxy contract', () => {
test.use({ viewport: { width: 500, height: 300 } });
test('with implementation name', async({ render, page }) => {
const component = await render(
<AddressEntity
address={ addressMock.contract }
/>,
);
await component.getByText(/home/i).hover();
await expect(page.getByText('Proxy contract')).toBeVisible();
await expect(page).toHaveScreenshot();
});
test('without implementation name', async({ render, page }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, implementations: [ { address: addressMock.contract.implementations?.[0].address } ] }}
/>,
);
await component.getByText(/eternal/i).hover();
await expect(page.getByText('Proxy contract')).toBeVisible();
await expect(page).toHaveScreenshot();
});
test('without any name', async({ render, page }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, name: undefined, implementations: [ { address: addressMock.contract.implementations?.[0].address } ] }}
/>,
);
await component.getByText(addressMock.contract.hash.slice(0, 4)).hover();
await expect(page.getByText('Proxy contract')).toBeVisible();
await expect(page).toHaveScreenshot();
});
test('with multiple implementations', async({ render, page }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, implementations: implementationsMock.multiple }}
/>,
);
await component.getByText(/eternal/i).hover();
await expect(page.getByText('Proxy contract')).toBeVisible();
await expect(page).toHaveScreenshot();
});
});
test.describe('loading', () => {
test('without alias', async({ render }) => {
const component = await render(
......
......@@ -11,6 +11,7 @@ import { useAddressHighlightContext } from 'lib/contexts/addressHighlight';
import * as EntityBase from 'ui/shared/entities/base/components';
import { getIconProps } from '../base/utils';
import AddressEntityContentProxy from './AddressEntityContentProxy';
import AddressIdenticon from './AddressIdenticon';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>;
......@@ -57,27 +58,18 @@ const Icon = (props: IconProps) => {
);
}
if (props.address.is_verified) {
return (
<Tooltip label="Verified contract">
<span>
<EntityBase.Icon
{ ...props }
name="contracts/verified"
color="green.500"
borderRadius={ 0 }
/>
</span>
</Tooltip>
);
}
const isProxy = Boolean(props.address.implementations?.length);
const isVerified = isProxy ? props.address.is_verified && props.address.implementations?.every(({ name }) => Boolean(name)) : props.address.is_verified;
const contractIconName: EntityBase.IconBaseProps['name'] = props.address.is_verified ? 'contracts/verified' : 'contracts/regular';
const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract');
return (
<Tooltip label="Contract">
<Tooltip label={ label.slice(0, 1).toUpperCase() + label.slice(1) }>
<span>
<EntityBase.Icon
{ ...props }
name="contracts/regular"
name={ isProxy ? 'contracts/proxy' : contractIconName }
color={ isVerified ? 'green.500' : undefined }
borderRadius={ 0 }
/>
</span>
......@@ -95,12 +87,18 @@ const Icon = (props: IconProps) => {
);
};
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'>;
export type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'>;
const Content = chakra((props: ContentProps) => {
const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name;
const nameText = nameTag || props.address.ens_domain_name || props.address.name;
const isProxy = props.address.implementations && props.address.implementations.length > 0;
if (isProxy) {
return <AddressEntityContentProxy { ...props }/>;
}
if (nameText) {
const label = (
<VStack gap={ 0 } py={ 1 } color="inherit">
......@@ -140,15 +138,18 @@ const Copy = (props: CopyProps) => {
const Container = EntityBase.Container;
export interface EntityProps extends EntityBase.EntityBaseProps {
address: Pick<AddressParam, 'hash' | 'name' | 'is_contract' | 'is_verified' | 'ens_domain_name' | 'metadata'>;
address: Pick<AddressParam,
'hash' | 'name' | 'is_contract' | 'is_verified' | 'implementations' | 'ens_domain_name' | 'metadata'
>;
isSafeAddress?: boolean;
noHighlight?: boolean;
}
const AddressEntry = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const context = useAddressHighlightContext();
const context = useAddressHighlightContext(props.noHighlight);
return (
<Container
......
import { Box, DarkMode, PopoverBody, PopoverContent, PopoverTrigger, Portal, useColorModeValue, Flex, PopoverArrow } from '@chakra-ui/react';
import React from 'react';
import Popover from 'ui/shared/chakra/Popover';
import * as EntityBase from 'ui/shared/entities/base/components';
import type { ContentProps } from './AddressEntity';
import AddressEntity from './AddressEntity';
const AddressEntityContentProxy = (props: ContentProps) => {
const bgColor = useColorModeValue('gray.700', 'gray.900');
const implementations = props.address.implementations;
const handleClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
}, []);
if (!implementations || implementations.length === 0) {
return null;
}
const colNum = Math.min(implementations.length, 3);
const implementationName = implementations.length === 1 && implementations[0].name ? implementations[0].name : undefined;
return (
<Popover trigger="hover" isLazy gutter={ 8 }>
<PopoverTrigger>
<Box display="inline-flex" w="100%">
<EntityBase.Content
{ ...props }
truncation={ implementationName || props.address.name ? 'tail' : 'dynamic' }
text={ implementationName || props.address.name || props.address.hash }
/>
</Box>
</PopoverTrigger>
<Portal>
<DarkMode>
<PopoverContent bgColor={ bgColor } w="fit-content" borderRadius="sm" maxW={{ base: '100vw', lg: '410px' }} onClick={ handleClick }>
<PopoverArrow bgColor={ bgColor }/>
<PopoverBody color="white" p={ 2 } fontSize="sm" lineHeight={ 5 } textAlign="center">
<Box fontWeight={ 600 }>
Proxy contract
{ props.address.name ? ` (${ props.address.name })` : '' }
</Box>
<AddressEntity address={{ hash: props.address.hash }} noLink noIcon noHighlight justifyContent="center"/>
<Box fontWeight={ 600 } mt={ 2 }>
Implementation{ implementations.length > 1 ? 's' : '' }
{ implementationName ? ` (${ implementationName })` : '' }
</Box>
<Flex flexWrap="wrap" columnGap={ 3 }>
{ implementations.map((item) => (
<AddressEntity
key={ item.address }
address={{ hash: item.address }}
noLink
noIcon
noHighlight
minW={ `calc((100% - ${ colNum - 1 } * 12px) / ${ colNum })` }
flex={ 1 }
justifyContent={ colNum === 1 ? 'center' : undefined }
/>
)) }
</Flex>
</PopoverBody>
</PopoverContent>
</DarkMode>
</Portal>
</Popover>
);
};
export default React.memo(AddressEntityContentProxy);
......@@ -54,7 +54,7 @@ const Icon = dynamic(
// eslint-disable-next-line react/display-name
return (props: IconProps) => {
const svg = GradientAvatar(props.hash, props.size);
const svg = GradientAvatar(props.hash, props.size, 'circle');
return <div dangerouslySetInnerHTML={{ __html: svg }}/>;
};
}
......
......@@ -142,6 +142,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
tailLength={ tailLength }
/>
);
case 'tail':
case 'none':
return <chakra.span as={ asProp }>{ text }</chakra.span>;
}
......@@ -153,6 +154,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
isLoaded={ !isLoading }
overflow="hidden"
whiteSpace="nowrap"
textOverflow={ truncation === 'tail' ? 'ellipsis' : undefined }
>
{ children }
</Skeleton>
......
import { Box, chakra, Flex, Image, Popover, PopoverBody, PopoverContent, PopoverTrigger, Portal, Skeleton, Text } from '@chakra-ui/react';
import { Box, chakra, Flex, Image, PopoverBody, PopoverContent, PopoverTrigger, Portal, Skeleton, Text } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
......@@ -6,6 +6,7 @@ import type * as bens from '@blockscout/bens-types';
import { route } from 'nextjs-routes';
import Popover from 'ui/shared/chakra/Popover';
import * as EntityBase from 'ui/shared/entities/base/components';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
......
import type { PopoverContentProps } from '@chakra-ui/react';
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
......@@ -8,6 +7,7 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import Popover from 'ui/shared/chakra/Popover';
import FilterButton from 'ui/shared/filters/FilterButton';
interface Props {
......
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
......@@ -11,6 +10,7 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import Popover from 'ui/shared/chakra/Popover';
import FilterButton from 'ui/shared/filters/FilterButton';
import IconSvg from 'ui/shared/IconSvg';
......
......@@ -5,7 +5,6 @@ import {
DarkMode,
Flex,
Grid,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
......@@ -20,6 +19,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import Popover from 'ui/shared/chakra/Popover';
import LinkInternal from 'ui/shared/links/LinkInternal';
import GasInfoTooltipRow from './GasInfoTooltipRow';
......@@ -33,7 +33,6 @@ interface Props {
placement?: PlacementWithLogical;
}
const POPOVER_OFFSET: [ number, number ] = [ 0, 10 ];
const feature = config.features.gasTracker;
const GasInfoTooltip = ({ children, data, dataUpdatedAt, isOpen, placement }: Props) => {
......@@ -50,7 +49,7 @@ const GasInfoTooltip = ({ children, data, dataUpdatedAt, isOpen, placement }: Pr
3 : 2;
return (
<Popover trigger="hover" isLazy offset={ POPOVER_OFFSET } isOpen={ isOpen } placement={ placement }>
<Popover trigger="hover" isLazy isOpen={ isOpen } placement={ placement }>
<PopoverTrigger>
{ children }
</PopoverTrigger>
......
......@@ -34,7 +34,7 @@ if (config.features.dataAvailability.isEnabled) {
}
if (config.features.nameService.isEnabled) {
searchCategories.push({ id: 'domain', title: 'Names' });
searchCategories.unshift({ id: 'domain', title: 'Names' });
}
export const searchItemTitles: Record<Category, { itemTitle: string; itemTitleShort: string }> = {
......
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
......@@ -10,6 +9,7 @@ import {
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover';
import SortButtonDesktop from './ButtonDesktop';
import SortButtonMobile from './ButtonMobile';
......
......@@ -5,20 +5,23 @@ import React from 'react';
import type { Route } from 'nextjs-routes';
import Hint from 'ui/shared/Hint';
import IconSvg, { type IconName } from 'ui/shared/IconSvg';
import TruncatedValue from 'ui/shared/TruncatedValue';
type Props = {
className?: string;
label: string;
value: string;
value: string | React.ReactNode;
valuePrefix?: string;
valuePostfix?: string;
hint?: string;
hint?: string | React.ReactNode;
isLoading?: boolean;
diff?: string | number;
diffFormatted?: string;
diffPeriod?: '24h';
period?: '1h' | '24h';
href?: Route;
icon?: IconName;
}
const Container = ({ href, children }: { href?: Route; children: JSX.Element }) => {
......@@ -33,31 +36,57 @@ const Container = ({ href, children }: { href?: Route; children: JSX.Element })
return children;
};
const StatsWidget = ({ label, value, valuePrefix, valuePostfix, isLoading, hint, diff, diffPeriod = '24h', diffFormatted, period, href }: Props) => {
const bgColor = useColorModeValue('blue.50', 'whiteAlpha.100');
const StatsWidget = ({
className,
icon,
label,
value,
valuePrefix,
valuePostfix,
isLoading,
hint,
diff,
diffPeriod = '24h',
diffFormatted,
period,
href,
}: Props) => {
const bgColor = useColorModeValue('gray.50', 'whiteAlpha.100');
const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const hintColor = useColorModeValue('gray.600', 'gray.400');
return (
<Container href={ !isLoading ? href : undefined }>
<Flex
alignItems="flex-start"
className={ className }
alignItems="center"
bgColor={ isLoading ? skeletonBgColor : bgColor }
px={ 3 }
py={{ base: 2, lg: 3 }}
borderRadius="md"
p={ 3 }
borderRadius="base"
justifyContent="space-between"
columnGap={ 3 }
columnGap={ 2 }
{ ...(href && !isLoading ? {
as: 'a',
href,
} : {}) }
>
{ icon && (
<IconSvg
name={ icon }
p={ 2 }
boxSize="40px"
isLoading={ isLoading }
borderRadius="base"
display={{ base: 'none', lg: 'block' }}
flexShrink={ 0 }
/>
) }
<Box w="100%">
<Skeleton
isLoaded={ !isLoading }
color="text_secondary"
fontSize="xs"
lineHeight="16px"
w="fit-content"
>
<span>{ label }</span>
......@@ -66,30 +95,36 @@ const StatsWidget = ({ label, value, valuePrefix, valuePostfix, isLoading, hint,
isLoaded={ !isLoading }
display="flex"
alignItems="baseline"
mt={ 1 }
fontWeight={ 500 }
fontSize="lg"
lineHeight={ 6 }
>
{ valuePrefix && <chakra.span fontWeight={ 500 } fontSize="lg" lineHeight={ 6 } whiteSpace="pre">{ valuePrefix }</chakra.span> }
<TruncatedValue isLoading={ isLoading } fontWeight={ 500 } fontSize="lg" lineHeight={ 6 } value={ value }/>
{ valuePostfix && <chakra.span fontWeight={ 500 } fontSize="lg" lineHeight={ 6 } whiteSpace="pre">{ valuePostfix }</chakra.span> }
{ valuePrefix && <chakra.span whiteSpace="pre">{ valuePrefix }</chakra.span> }
{ typeof value === 'string' ? (
<TruncatedValue isLoading={ isLoading } value={ value }/>
) : (
value
) }
{ valuePostfix && <chakra.span whiteSpace="pre">{ valuePostfix }</chakra.span> }
{ diff && Number(diff) > 0 && (
<>
<Text fontWeight={ 500 } ml={ 2 } mr={ 1 } fontSize="lg" lineHeight={ 6 } color="green.500">
<Text ml={ 2 } mr={ 1 } color="green.500">
+{ diffFormatted || Number(diff).toLocaleString() }
</Text>
<Text variant="secondary" fontSize="sm">({ diffPeriod })</Text>
</>
) }
{ period && <Text variant="secondary" fontSize="xs" ml={ 1 }>({ period })</Text> }
{ period && <Text variant="secondary" fontSize="xs" fontWeight={ 400 } ml={ 1 }>({ period })</Text> }
</Skeleton>
</Box>
{ hint && (
{ typeof hint === 'string' ? (
<Skeleton isLoaded={ !isLoading } alignSelf="center" borderRadius="base">
<Hint label={ hint } boxSize={ 6 } color={ hintColor }/>
</Skeleton>
) }
) : hint }
</Flex>
</Container>
);
};
export default StatsWidget;
export default chakra(StatsWidget);
import React from 'react';
import * as txMock from 'mocks/txs/tx';
import { test, expect } from 'playwright/lib';
import TxFee from './TxFee';
test.use({ viewport: { width: 300, height: 100 } });
test('base view', async({ render }) => {
const component = await render(<TxFee tx={ txMock.base } withUsd/>);
await expect(component).toHaveScreenshot();
});
test('no usd value', async({ render }) => {
const component = await render(<TxFee tx={ txMock.base } accuracy={ 3 }/>);
await expect(component).toHaveScreenshot();
});
test('celo gas token', async({ render, mockAssetResponse }) => {
await mockAssetResponse(txMock.celoTxn.celo?.gas_token?.icon_url as string, './playwright/mocks/image_svg.svg');
const component = await render(<TxFee tx={ txMock.celoTxn } withUsd accuracyUsd={ 3 }/>);
await expect(component).toHaveScreenshot();
});
test('stability token', async({ render, mockAssetResponse }) => {
await mockAssetResponse(txMock.stabilityTx.stability_fee?.token.icon_url as string, './playwright/mocks/image_svg.svg');
const component = await render(<TxFee tx={ txMock.stabilityTx } withUsd accuracyUsd={ 3 }/>);
await expect(component).toHaveScreenshot();
});
import { chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
import { currencyUnits } from 'lib/units';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
interface Props {
className?: string;
isLoading?: boolean;
tx: Transaction;
withCurrency?: boolean;
withUsd?: boolean;
accuracy?: number;
accuracyUsd?: number;
}
const TxFee = ({ className, tx, accuracy, accuracyUsd, isLoading, withCurrency = true, withUsd }: Props) => {
if (tx.celo?.gas_token) {
const token = tx.celo.gas_token;
const { valueStr, usd } = getCurrencyValue({
value: tx.fee.value || '0',
exchangeRate: token.exchange_rate,
decimals: token.decimals,
accuracy,
accuracyUsd,
});
return (
<Skeleton whiteSpace="pre-wrap" wordBreak="break-word" isLoaded={ !isLoading } display="flex" flexWrap="wrap" className={ className }>
<span>{ valueStr } </span>
<TokenEntity token={ token } noCopy onlySymbol w="auto" ml={ 1 }/>
{ usd && withUsd && <chakra.span color="text_secondary"> (${ usd })</chakra.span> }
</Skeleton>
);
}
if (tx.stability_fee) {
const token = tx.stability_fee.token;
const { valueStr, usd } = getCurrencyValue({
value: tx.stability_fee.total_fee,
exchangeRate: token.exchange_rate,
decimals: token.decimals,
accuracy,
accuracyUsd,
});
return (
<Skeleton whiteSpace="pre" isLoaded={ !isLoading } display="flex" className={ className }>
<span>{ valueStr } </span>
{ valueStr !== '0' && <TokenEntity token={ token } noCopy onlySymbol w="auto" ml={ 1 }/> }
{ usd && withUsd && <chakra.span color="text_secondary"> (${ usd })</chakra.span> }
</Skeleton>
);
}
const showCurrency = withCurrency && !config.UI.views.tx.hiddenFields?.fee_currency;
return (
<CurrencyValue
value={ tx.fee.value }
currency={ showCurrency ? currencyUnits.ether : '' }
exchangeRate={ withUsd ? tx.exchange_rate : null }
accuracy={ accuracy }
accuracyUsd={ accuracyUsd }
flexWrap="wrap"
className={ className }
isLoading={ isLoading }
/>
);
};
export default React.memo(chakra(TxFee));
import { Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import type { ExcludeUndefined } from 'types/utils';
import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
interface Props {
data: ExcludeUndefined<Transaction['stability_fee']>;
isLoading?: boolean;
hideUsd?: boolean;
accuracy?: number;
className?: string;
}
const TxFeeStability = ({ data, isLoading, hideUsd, accuracy, className }: Props) => {
const { valueStr, usd } = getCurrencyValue({
value: data.total_fee,
exchangeRate: data.token.exchange_rate,
decimals: data.token.decimals,
accuracy,
});
return (
<Skeleton whiteSpace="pre" isLoaded={ !isLoading } display="flex" className={ className }>
<span>{ valueStr } </span>
{ valueStr !== '0' && <TokenEntity token={ data.token } noCopy onlySymbol w="auto" ml={ 1 }/> }
{ usd && !hideUsd && <chakra.span color="text_secondary"> (${ usd })</chakra.span> }
</Skeleton>
);
};
export default React.memo(chakra(TxFeeStability));
import { IconButton, Popover, PopoverTrigger, PopoverContent, PopoverBody, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import { IconButton, PopoverTrigger, PopoverContent, PopoverBody, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
......@@ -9,6 +9,7 @@ import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { apos, nbsp, ndash } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
const IntTxsIndexingStatus = () => {
......
import { HStack, Popover, PopoverBody, PopoverContent, PopoverTrigger, chakra, StackDivider } from '@chakra-ui/react';
import { HStack, PopoverBody, PopoverContent, PopoverTrigger, chakra, StackDivider } from '@chakra-ui/react';
import React from 'react';
import type { NavGroupItem } from 'types/client/navigation';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import LightningLabel from '../LightningLabel';
......@@ -27,6 +28,7 @@ const NavLinkGroup = ({ item }: Props) => {
trigger="hover"
placement="bottom-start"
isLazy
gutter={ 8 }
>
{ ({ isOpen }) => (
<>
......
......@@ -3,7 +3,6 @@ import {
HStack,
Box,
Link,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
......@@ -13,6 +12,7 @@ import React from 'react';
import type { NavGroupItem } from 'types/client/navigation';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import LightningLabel from '../LightningLabel';
......@@ -39,6 +39,7 @@ const NavLinkGroup = ({ item, isCollapsed }: Props) => {
trigger="hover"
placement="right-start"
isLazy
gutter={ 8 }
>
<PopoverTrigger>
<Link
......
import { Popover, PopoverTrigger } from '@chakra-ui/react';
import { PopoverTrigger } from '@chakra-ui/react';
import React from 'react';
import Popover from 'ui/shared/chakra/Popover';
import NetworkMenuButton from './NetworkMenuButton';
import NetworkMenuContentDesktop from './NetworkMenuContentDesktop';
import useNetworkMenu from './useNetworkMenu';
......@@ -13,7 +15,7 @@ const NetworkMenu = ({ isCollapsed }: Props) => {
const menu = useNetworkMenu();
return (
<Popover openDelay={ 300 } placement="right-start" gutter={ 8 } isLazy isOpen={ menu.isOpen } onClose={ menu.onClose }>
<Popover openDelay={ 300 } placement="right-start" isLazy isOpen={ menu.isOpen } onClose={ menu.onClose }>
<PopoverTrigger>
<NetworkMenuButton
marginLeft="auto"
......
import type { IconButtonProps } from '@chakra-ui/react';
import { Popover, PopoverContent, PopoverBody, PopoverTrigger, IconButton, Tooltip, Box, chakra } from '@chakra-ui/react';
import { PopoverContent, PopoverBody, PopoverTrigger, IconButton, Tooltip, Box, chakra } from '@chakra-ui/react';
import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useLoginUrl from 'lib/hooks/useLoginUrl';
import * as mixpanel from 'lib/mixpanel/index';
import Popover from 'ui/shared/chakra/Popover';
import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
......
import {
Box,
Portal,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
......@@ -20,6 +19,7 @@ import { route } from 'nextjs-routes';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import { getRecentSearchKeywords, saveToRecentKeywords } from 'lib/recentSearchKeywords';
import Popover from 'ui/shared/chakra/Popover';
import LinkInternal from 'ui/shared/links/LinkInternal';
import SearchBarBackdrop from './SearchBarBackdrop';
......@@ -123,7 +123,7 @@ const SearchBar = ({ isHomepage }: Props) => {
autoFocus={ false }
onClose={ onClose }
placement="bottom-start"
offset={ isMobile && !isHomepage ? [ 12, -4 ] : undefined }
offset={ isMobile && !isHomepage ? [ 12, -4 ] : [ 0, 8 ] }
isLazy
>
<PopoverTrigger>
......
......@@ -27,6 +27,7 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => {
name: '',
is_verified: data.is_smart_contract_verified,
ens_domain_name: null,
implementations: null,
}}
/>
);
......
import { Button, Box, Flex, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure, chakra } from '@chakra-ui/react';
import { Button, Box, Flex, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure, chakra } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -7,6 +7,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import getPageType from 'lib/mixpanel/getPageType';
import * as mixpanel from 'lib/mixpanel/index';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import DeFiDropdownItem from './DeFiDropdownItem';
......
import { IconButton, Popover, PopoverTrigger } from '@chakra-ui/react';
import { IconButton, PopoverTrigger } from '@chakra-ui/react';
import React from 'react';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import NetworkMenuContentDesktop from 'ui/snippets/networkMenu/NetworkMenuContentDesktop';
import useNetworkMenu from 'ui/snippets/networkMenu/useNetworkMenu';
......
import { Box, IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger, useDisclosure } from '@chakra-ui/react';
import { Box, IconButton, PopoverBody, PopoverContent, PopoverTrigger, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import SettingsColorTheme from './SettingsColorTheme';
......
import type { ButtonProps } from '@chakra-ui/react';
import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean, chakra, useColorModeValue } from '@chakra-ui/react';
import { PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import { useMarketplaceContext } from 'lib/contexts/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import Popover from 'ui/shared/chakra/Popover';
import HashStringShorten from 'ui/shared/HashStringShorten';
import IconSvg from 'ui/shared/IconSvg';
import useWallet from 'ui/snippets/walletMenu/useWallet';
import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent';
......@@ -65,7 +67,6 @@ const WalletMenuDesktop = ({ isHomePage, className, size = 'md' }: Props) => {
<Popover
openDelay={ 300 }
placement="bottom-end"
gutter={ 10 }
isLazy
isOpen={ isPopoverOpen }
onClose={ setIsPopoverOpen.off }
......@@ -87,6 +88,7 @@ const WalletMenuDesktop = ({ isHomePage, className, size = 'md' }: Props) => {
onClick={ isWalletConnected ? openPopover : connect }
fontSize="sm"
size={ size }
px={{ lg: isHomePage ? 2 : 4, xl: 4 }}
{ ...buttonStyles }
>
{ isWalletConnected ? (
......@@ -94,7 +96,12 @@ const WalletMenuDesktop = ({ isHomePage, className, size = 'md' }: Props) => {
<WalletIdenticon address={ address } isAutoConnectDisabled={ isAutoConnectDisabled } mr={ 2 }/>
<HashStringShorten hash={ address } isTooltipDisabled/>
</>
) : 'Connect wallet' }
) : (
<>
<IconSvg display={{ base: isHomePage ? 'inline' : 'none', xl: 'none' }} name="wallet" boxSize={ 6 } p={ 0.5 }/>
<chakra.span display={{ base: isHomePage ? 'none' : 'inline', xl: 'inline' }}>Connect wallet</chakra.span>
</>
) }
</Button>
</WalletTooltip>
</PopoverTrigger>
......
......@@ -38,7 +38,7 @@ const WalletMenuMobile = () => {
aria-label="wallet menu"
icon={ isWalletConnected ?
<WalletIdenticon address={ address } isAutoConnectDisabled={ isAutoConnectDisabled }/> :
<IconSvg name="wallet" boxSize={ 6 }/>
<IconSvg name="wallet" boxSize={ 6 } p={ 0.5 }/>
}
variant={ isWalletConnected ? 'subtle' : 'outline' }
colorScheme="gray"
......
......@@ -76,7 +76,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
</Heading>
{ section.id === 'gas' && homeStatsQuery.data && homeStatsQuery.data.gas_prices && (
<GasInfoTooltip data={ homeStatsQuery.data } dataUpdatedAt={ homeStatsQuery.dataUpdatedAt }>
<IconSvg name="info" boxSize={ 5 } display="block" cursor="pointer" _hover={{ color: 'link_hovered' }}/>
<IconSvg name="info" boxSize={ 5 } display="block" cursor="pointer" color="icon_info" _hover={{ color: 'link_hovered' }}/>
</GasInfoTooltip>
) }
</Skeleton>
......
import { Box, Button, Menu, MenuButton, MenuItemOption, MenuList, MenuOptionGroup, chakra } from '@chakra-ui/react';
import { Box, Button, MenuButton, MenuItemOption, MenuList, MenuOptionGroup, chakra } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import Menu from 'ui/shared/chakra/Menu';
import IconSvg from 'ui/shared/IconSvg';
type Props<T extends string> = {
......
import {
Popover, PopoverTrigger, PopoverContent, PopoverBody,
PopoverTrigger, PopoverContent, PopoverBody,
Modal, ModalContent, ModalCloseButton,
useDisclosure,
} from '@chakra-ui/react';
......@@ -8,6 +8,7 @@ import React from 'react';
import type { TokenVerifiedInfo } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover';
import Content, { hasContent } from './TokenProjectInfo/Content';
import TriggerButton from './TokenProjectInfo/TriggerButton';
......
import { chakra, Alert, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
import type { ToastId } from '@chakra-ui/react';
import { chakra, Alert, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Spinner } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import ReCaptcha from 'react-google-recaptcha';
......@@ -9,7 +10,7 @@ import type { TokenInstance } from 'types/api/token';
import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery';
import { MINUTE } from 'lib/consts';
import { MINUTE, SECOND } from 'lib/consts';
import useToast from 'lib/hooks/useToast';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -23,6 +24,7 @@ interface Props {
const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
const timeoutId = React.useRef<number>();
const toastId = React.useRef<ToastId>();
const { status, setStatus } = useMetadataUpdateContext() || {};
const apiFetch = useApiFetch();
......@@ -31,12 +33,12 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
const handleRefreshError = React.useCallback(() => {
setStatus?.('ERROR');
toast.closeAll();
toast({
toastId.current && toast.update(toastId.current, {
title: 'Error',
description: 'The refreshing process has failed. Please try again.',
status: 'warning',
variant: 'subtle',
duration: 5 * SECOND,
isClosable: true,
});
}, [ setStatus, toast ]);
......@@ -49,13 +51,15 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
},
})
.then(() => {
toast({
setStatus?.('WAITING_FOR_RESPONSE');
toastId.current = toast({
title: 'Please wait',
description: 'Refetching metadata request sent',
icon: <Spinner size="sm" mr={ 2 }/>,
status: 'warning',
variant: 'subtle',
duration: null,
isClosable: false,
});
setStatus?.('WAITING_FOR_RESPONSE');
timeoutId.current = window.setTimeout(handleRefreshError, 2 * MINUTE);
})
.catch(() => {
......@@ -63,7 +67,6 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
title: 'Error',
description: 'Unable to initialize metadata update',
status: 'warning',
variant: 'subtle',
});
setStatus?.('ERROR');
});
......@@ -112,12 +115,12 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
};
});
toast.closeAll();
toast({
toastId.current && toast.update(toastId.current, {
title: 'Success!',
description: 'Metadata has been refreshed',
status: 'success',
variant: 'subtle',
duration: 5 * SECOND,
isClosable: true,
});
setStatus?.('SUCCESS');
......@@ -138,6 +141,15 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
handler: handleSocketMessage,
});
React.useEffect(() => {
return () => {
timeoutId.current && window.clearTimeout(timeoutId.current);
toastId.current && toast.close(toastId.current);
};
// run only on mount/unmount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Modal isOpen={ status === 'MODAL_OPENED' } onClose={ handleModalClose } size={{ base: 'full', lg: 'sm' }}>
<ModalOverlay/>
......
......@@ -49,6 +49,7 @@ const TokensTableItem = ({
is_contract: true,
is_verified: false,
ens_domain_name: null,
implementations: null,
};
return (
......
import { Table, Tbody, Tr, Th, Box, Skeleton, Text, Show, Hide } from '@chakra-ui/react';
import _ from 'lodash';
import _chunk from 'lodash/chunk';
import React, { useMemo, useState } from 'react';
import type { PaginationParams } from 'ui/shared/pagination/types';
......@@ -33,7 +33,7 @@ export default function TxAssetFlows(props: FlowViewProps) {
const [ page, setPage ] = useState<number>(1);
const ViewData = useMemo(() => (queryData ? generateFlowViewData(queryData) : []), [ queryData ]);
const chunkedViewData = _.chunk(ViewData, 50);
const chunkedViewData = _chunk(ViewData, 50);
const paginationProps: PaginationParams = useMemo(() => ({
onNextPageClick: () => setPage(page + 1),
......
......@@ -14,6 +14,7 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
import RawInputData from 'ui/shared/RawInputData';
import TxFee from 'ui/shared/tx/TxFee';
import TxDetailsGasPrice from 'ui/tx/details/TxDetailsGasPrice';
import TxDetailsOther from 'ui/tx/details/TxDetailsOther';
......@@ -80,11 +81,7 @@ const TxDetailsWrapped = ({ data }: Props) => {
Transaction fee
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<CurrencyValue
value={ data.fee.value }
currency={ currencyUnits.ether }
flexWrap="wrap"
/>
<TxFee tx={ data } withUsd/>
</DetailsInfoItem.Value>
</>
) }
......
import { Box, Hide, Popover, PopoverArrow, PopoverContent, PopoverTrigger, Show, Skeleton, Text, useColorModeValue } from '@chakra-ui/react';
import { Box, Hide, PopoverArrow, PopoverContent, PopoverTrigger, Show, Skeleton, Text, useColorModeValue } from '@chakra-ui/react';
import type { FC } from 'react';
import React from 'react';
import { HEX_REGEXP } from 'lib/regexp';
import Popover from 'ui/shared/chakra/Popover';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import IconSvg from 'ui/shared/IconSvg';
......
import _ from 'lodash';
import _findIndex from 'lodash/findIndex';
import type { NovesNft, NovesResponseData, NovesSentReceived, NovesToken } from 'types/api/noves';
......@@ -27,7 +27,7 @@ export function generateFlowViewData(data: NovesResponseData): Array<NovesFlowVi
const txItems = [ ...sent, ...received ];
const paidGasIndex = _.findIndex(txItems, (item) => item.action === 'paidGas');
const paidGasIndex = _findIndex(txItems, (item) => item.action === 'paidGas');
if (paidGasIndex >= 0) {
const element = txItems.splice(paidGasIndex, 1)[0];
element.to.name = 'Validators';
......
import _ from 'lodash';
import _groupBy from 'lodash/groupBy';
import _keysIn from 'lodash/keysIn';
import _mapValues from 'lodash/mapValues';
import type { NovesResponseData } from 'types/api/noves';
import type { TokenInfo } from 'types/api/token';
......@@ -47,28 +49,28 @@ export function getTokensData(data: NovesResponseData): TokensData {
});
// Group tokens by property into arrays
const tokensGroupByname = _.groupBy(tokens, 'name');
const tokensGroupBySymbol = _.groupBy(tokens, 'symbol');
const tokensGroupById = _.groupBy(tokens, 'id');
const tokensGroupByname = _groupBy(tokens, 'name');
const tokensGroupBySymbol = _groupBy(tokens, 'symbol');
const tokensGroupById = _groupBy(tokens, 'id');
// Map properties to an object and remove duplicates
const mappedNames = _.mapValues(tokensGroupByname, (i) => {
const mappedNames = _mapValues(tokensGroupByname, (i) => {
return i[0];
});
const mappedSymbols = _.mapValues(tokensGroupBySymbol, (i) => {
const mappedSymbols = _mapValues(tokensGroupBySymbol, (i) => {
return i[0];
});
const mappedIds = _.mapValues(tokensGroupById, (i) => {
const mappedIds = _mapValues(tokensGroupById, (i) => {
return i[0];
});
const filters = [ 'undefined', 'null' ];
// Array of keys to match in string
const nameList = _.keysIn(mappedNames).filter(i => !filters.includes(i));
const symbolList = _.keysIn(mappedSymbols).filter(i => !filters.includes(i));
const idList = _.keysIn(mappedIds).filter(i => !filters.includes(i));
const nameList = _keysIn(mappedNames).filter(i => !filters.includes(i));
const symbolList = _keysIn(mappedSymbols).filter(i => !filters.includes(i));
const idList = _keysIn(mappedIds).filter(i => !filters.includes(i));
return {
nameList,
......
......@@ -2,21 +2,47 @@ import { Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
import config from 'configs/app';
import { WEI, WEI_IN_GWEI } from 'lib/consts';
import { currencyUnits } from 'lib/units';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
interface Props {
gasToken?: TokenInfo<'ERC-20'> | null;
gasPrice: string | null;
isLoading?: boolean;
}
const TxDetailsGasPrice = ({ gasPrice, isLoading }: Props) => {
const TxDetailsGasPrice = ({ gasPrice, gasToken, isLoading }: Props) => {
if (config.UI.views.tx.hiddenFields?.gas_price || !gasPrice) {
return null;
}
const content = (() => {
if (gasToken) {
return (
<Skeleton isLoaded={ !isLoading } display="flex">
<span>{ BigNumber(gasPrice).dividedBy(WEI).toFixed() }</span>
<TokenEntity token={ gasToken } noCopy onlySymbol w="auto" ml={ 1 }/>
</Skeleton>
);
}
return (
<>
<Skeleton isLoaded={ !isLoading } mr={ 1 }>
{ BigNumber(gasPrice).dividedBy(WEI).toFixed() } { currencyUnits.ether }
</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>({ BigNumber(gasPrice).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei })</span>
</Skeleton>
</>
);
})();
return (
<>
<DetailsInfoItem.Label
......@@ -26,12 +52,7 @@ const TxDetailsGasPrice = ({ gasPrice, isLoading }: Props) => {
Gas price
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isLoading } mr={ 1 }>
{ BigNumber(gasPrice).dividedBy(WEI).toFixed() } { currencyUnits.ether }
</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>({ BigNumber(gasPrice).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei })</span>
</Skeleton>
{ content }
</DetailsInfoItem.Value>
</>
);
......
......@@ -46,7 +46,7 @@ import RawInputData from 'ui/shared/RawInputData';
import StatusTag from 'ui/shared/statusTag/StatusTag';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TextSeparator from 'ui/shared/TextSeparator';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxFee from 'ui/shared/tx/TxFee';
import Utilization from 'ui/shared/Utilization/Utilization';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
import TxDetailsActions from 'ui/tx/details/txDetailsActions/TxDetailsActions';
......@@ -555,17 +555,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
Transaction fee
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.stability_fee ? (
<TxFeeStability data={ data.stability_fee } isLoading={ isLoading }/>
) : (
<CurrencyValue
value={ data.fee.value }
currency={ config.UI.views.tx.hiddenFields?.fee_currency ? '' : currencyUnits.ether }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
isLoading={ isLoading }
/>
) }
<TxFee tx={ data } isLoading={ isLoading } withUsd/>
</DetailsInfoItem.Value>
</>
) }
......@@ -606,7 +596,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
</>
) }
<TxDetailsGasPrice gasPrice={ data.gas_price } isLoading={ isLoading }/>
<TxDetailsGasPrice gasPrice={ data.gas_price } gasToken={ data.celo?.gas_token } isLoading={ isLoading }/>
<TxDetailsFeePerGas txFee={ data.fee.value } gasUsed={ data.gas_used } isLoading={ isLoading }/>
......
......@@ -3,7 +3,6 @@ import {
Modal,
ModalContent,
ModalCloseButton,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
......@@ -14,6 +13,7 @@ import React from 'react';
import type { Transaction } from 'types/api/transaction';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Popover from 'ui/shared/chakra/Popover';
import TxAdditionalInfoContainer from './TxAdditionalInfoContainer';
import TxAdditionalInfoContent from './TxAdditionalInfoContent';
......
......@@ -9,11 +9,10 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import getValueWithUnit from 'lib/getValueWithUnit';
import { currencyUnits } from 'lib/units';
import CurrencyValue from 'ui/shared/CurrencyValue';
import BlobEntity from 'ui/shared/entities/blob/BlobEntity';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxFee from 'ui/shared/tx/TxFee';
import Utilization from 'ui/shared/Utilization/Utilization';
const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => {
......@@ -60,20 +59,7 @@ const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => {
{ (tx.stability_fee !== undefined || tx.fee.value !== null) && (
<>
<Text { ...sectionTitleProps }>Transaction fee</Text>
{ tx.stability_fee ? (
<TxFeeStability data={ tx.stability_fee }/>
) : (
<Flex>
<CurrencyValue
value={ tx.fee.value }
currency={ config.UI.views.tx.hiddenFields?.fee_currency ? '' : currencyUnits.ether }
exchangeRate={ tx.exchange_rate }
accuracyUsd={ 2 }
flexWrap="wrap"
rowGap={ 0 }
/>
</Flex>
) }
<TxFee tx={ tx } withUsd accuracyUsd={ 2 } rowGap={ 0 }/>
</>
) }
</Box>
......
......@@ -17,7 +17,7 @@ import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxFee from 'ui/shared/tx/TxFee';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType';
......@@ -111,14 +111,7 @@ const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeI
{ (tx.stability_fee !== undefined || tx.fee.value !== null) && (
<>
<Skeleton isLoaded={ !isLoading } display="inline-block" whiteSpace="pre">Fee</Skeleton>
{ tx.stability_fee ? (
<TxFeeStability data={ tx.stability_fee } isLoading={ isLoading } hideUsd/>
) : (
<Skeleton isLoaded={ !isLoading } display="inline-block" variant="text_secondary" whiteSpace="pre">
{ getValueWithUnit(tx.fee.value || 0).toFormat() }
{ config.UI.views.tx.hiddenFields?.fee_currency ? '' : ` ${ currencyUnits.ether }` }
</Skeleton>
) }
<TxFee tx={ tx } isLoading={ isLoading }/>
</>
) }
</Flex>
......
......@@ -43,6 +43,10 @@ const TxsTable = ({
}: Props) => {
const { cutRef, renderedItemsNum } = useLazyRenderedList(txs, !isLoading);
const feeCurrency = config.UI.views.tx.hiddenFields?.fee_currency || config.chain.hasMultipleGasCurrencies ?
'' :
' ' + currencyUnits.ether;
return (
<AddressHighlightProvider>
<Table variant="simple" minWidth="950px" size="xs">
......@@ -68,7 +72,7 @@ const TxsTable = ({
<Link onClick={ sort('fee') } display="flex" justifyContent="end">
{ sorting === 'fee-asc' && <IconSvg boxSize={ 5 } name="arrows/east" transform="rotate(-90deg)"/> }
{ sorting === 'fee-desc' && <IconSvg boxSize={ 5 } name="arrows/east" transform="rotate(90deg)"/> }
{ `Fee${ config.UI.views.tx.hiddenFields?.fee_currency ? '' : ` ${ currencyUnits.ether }` }` }
{ `Fee${ feeCurrency }` }
</Link>
</Th>
) }
......
......@@ -17,7 +17,7 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxStatus from 'ui/shared/statusTag/TxStatus';
import TxFeeStability from 'ui/shared/tx/TxFeeStability';
import TxFee from 'ui/shared/tx/TxFee';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
......@@ -109,12 +109,13 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement,
) }
{ !config.UI.views.tx.hiddenFields?.tx_fee && (
<Td isNumeric>
{ /* eslint-disable-next-line no-nested-ternary */ }
{ tx.stability_fee ? (
<TxFeeStability data={ tx.stability_fee } isLoading={ isLoading } accuracy={ 8 } justifyContent="end" hideUsd/>
) : (
tx.fee.value ? <CurrencyValue value={ tx.fee.value } accuracy={ 8 } isLoading={ isLoading }/> : '-'
) }
<TxFee
tx={ tx }
accuracy={ 8 }
isLoading={ isLoading }
withCurrency={ Boolean(tx.celo || tx.stability_fee) }
justifyContent="end"
/>
</Td>
) }
</Tr>
......
import { useQuery } from '@tanstack/react-query';
import _ from 'lodash';
import _chunk from 'lodash/chunk';
import _uniq from 'lodash/uniq';
import React from 'react';
import type { NovesDescribeTxsResponse } from 'types/api/noves';
......@@ -15,8 +16,8 @@ const translateEnabled = feature.isEnabled && feature.provider === 'noves';
export default function useDescribeTxs(items: Array<Transaction> | undefined, viewAsAccountAddress: string | undefined, isPlaceholderData: boolean) {
const apiFetch = useApiFetch();
const txsHash = _.uniq(items?.map(i => i.hash));
const txChunks = _.chunk(txsHash, 10);
const txsHash = _uniq(items?.map(i => i.hash));
const txChunks = _chunk(txsHash, 10);
const queryKey = {
viewAsAccountAddress,
......
......@@ -2552,21 +2552,21 @@
dom-mutator "^0.6.0"
"@grpc/grpc-js@^1.7.1":
version "1.9.9"
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.9.tgz#ce3a05439b1c957ec64c2ecdc6f1e4f54e8af797"
integrity sha512-vQ1qwi/Kiyprt+uhb1+rHMpyk4CVRMTGNUGGPRGS7pLNfWkdCHrGEnT6T3/JyC2VZgoOX/X1KwdoU0WYQAeYcQ==
version "1.10.10"
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.10.10.tgz#476d315feeb9dbb0f2d6560008c92688c30f13e0"
integrity sha512-HPa/K5NX6ahMoeBv15njAc/sfF4/jmiXLar9UlC2UfHFKZzsCVLc3wbe7+7qua7w9VPh2/L6EBxyAV7/E8Wftg==
dependencies:
"@grpc/proto-loader" "^0.7.8"
"@types/node" ">=12.12.47"
"@grpc/proto-loader" "^0.7.13"
"@js-sdsl/ordered-map" "^4.4.2"
"@grpc/proto-loader@^0.7.8":
version "0.7.10"
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.10.tgz#6bf26742b1b54d0a473067743da5d3189d06d720"
integrity sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==
"@grpc/proto-loader@^0.7.13":
version "0.7.13"
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf"
integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==
dependencies:
lodash.camelcase "^4.3.0"
long "^5.0.0"
protobufjs "^7.2.4"
protobufjs "^7.2.5"
yargs "^17.7.2"
"@hapi/b64@5.x.x":
......@@ -2945,6 +2945,11 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@js-sdsl/ordered-map@^4.4.2":
version "4.4.2"
resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c"
integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==
"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9"
......@@ -6217,7 +6222,7 @@
dependencies:
undici-types "~5.26.4"
"@types/node@>=12.12.47", "@types/node@>=13.7.0":
"@types/node@>=13.7.0":
version "20.9.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.0.tgz#bfcdc230583aeb891cf51e73cfdaacdd8deae298"
integrity sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==
......@@ -8626,10 +8631,10 @@ damerau-levenshtein@^1.0.8:
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
dappscout-iframe@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/dappscout-iframe/-/dappscout-iframe-0.2.1.tgz#b4718515ee4f00022af3912fac6ca1a321c156f9"
integrity sha512-EsiAAEk2I6hN+/E8o45WUn4BFd7aN8UvBwsIcOH79WOly0GOOHkPEO/puPkBCV0EcdxBsZIfssx3X0fSWVz5Bw==
dappscout-iframe@0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dappscout-iframe/-/dappscout-iframe-0.2.2.tgz#de3df6abccad68a27c9304300b92d86ec0ab1c59"
integrity sha512-ASOimgBRG61pSYQLdYGWePdiO3IsfTEgWZ6CHpZ4XQjJRmj1+WiWF56vFTeLIo5aucp+2+6oRCJ8KgKHGVDj0A==
dependencies:
react "^18.2.0"
react-dom "^18.2.0"
......@@ -10470,10 +10475,9 @@ graceful-fs@^4.2.4, graceful-fs@^4.2.9:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
gradient-avatar@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/gradient-avatar/-/gradient-avatar-1.0.2.tgz#42bb408e402b1f21aafba3878858721055515224"
integrity sha512-Od9KI2YImV60wnsvU/u6GEyBm2fiHUUHgiLySE243GYl/T/tiJMJ5QYey8o7tepugmlnUGQRaCItHv19UnUjUg==
"gradient-avatar@git+https://github.com/blockscout/gradient-avatar.git":
version "1.0.3"
resolved "git+https://github.com/blockscout/gradient-avatar.git#86810368c2581d2dc3aca089dc648336c8e17045"
dependencies:
hsl-rgb "^1.0.0"
hsl-triad "^1.0.0"
......@@ -13627,7 +13631,7 @@ property-information@^5.0.0:
dependencies:
xtend "^4.0.0"
protobufjs@^7.2.3, protobufjs@^7.2.4:
protobufjs@^7.2.3:
version "7.2.5"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d"
integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==
......@@ -13645,6 +13649,24 @@ protobufjs@^7.2.3, protobufjs@^7.2.4:
"@types/node" ">=13.7.0"
long "^5.0.0"
protobufjs@^7.2.5:
version "7.3.2"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.3.2.tgz#60f3b7624968868f6f739430cfbc8c9370e26df4"
integrity sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
"@protobufjs/codegen" "^2.0.4"
"@protobufjs/eventemitter" "^1.1.0"
"@protobufjs/fetch" "^1.1.0"
"@protobufjs/float" "^1.0.2"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/path" "^1.1.2"
"@protobufjs/pool" "^1.1.0"
"@protobufjs/utf8" "^1.1.0"
"@types/node" ">=13.7.0"
long "^5.0.0"
proxy-compare@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/proxy-compare/-/proxy-compare-2.5.1.tgz#17818e33d1653fbac8c2ec31406bce8a2966f600"
......@@ -14877,7 +14899,16 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
......@@ -15005,7 +15036,14 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
......@@ -15952,9 +15990,9 @@ vite-tsconfig-paths@^3.5.2:
tsconfig-paths "^4.0.0"
vite@^4.4.12:
version "4.5.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.2.tgz#d6ea8610e099851dad8c7371599969e0f8b97e82"
integrity sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==
version "4.5.3"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.3.tgz#d88a4529ea58bae97294c7e2e6f0eab39a50fb1a"
integrity sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==
dependencies:
esbuild "^0.18.10"
postcss "^8.4.27"
......@@ -16165,7 +16203,7 @@ word-wrap@^1.2.5, word-wrap@~1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
......@@ -16183,6 +16221,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
......@@ -16216,14 +16263,14 @@ ws@8.13.0:
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
ws@^7.3.1, ws@^7.5.1:
version "7.5.9"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
version "7.5.10"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
ws@^8.17.1, ws@^8.9.0:
version "8.17.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
ws@~8.11.0:
version "8.11.0"
......
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