Commit 096c0791 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Stats Update (#1796)

* [WIP] chart for secondary coin price

* latest L1 state batch snippet

* txs stats snipper

* Merge main branch

* optimism preset

* rename governance token into secondary coin

* add secondary coin price to top bar

* calculate usd value for avg tx fee

* tests

* tweaks

* change envs for demo

* fix tests

* fixes

* fix flaky test

* change scroll to filter for stats link behaviour
parent 57b54a6c
...@@ -341,6 +341,7 @@ ...@@ -341,6 +341,7 @@
"eth", "eth",
"eth_goerli", "eth_goerli",
"sepolia", "sepolia",
"optimism",
"polygon", "polygon",
"zkevm", "zkevm",
"zksync", "zksync",
......
...@@ -12,8 +12,8 @@ const chain = Object.freeze({ ...@@ -12,8 +12,8 @@ const chain = Object.freeze({
symbol: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL'), symbol: getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL'),
decimals: Number(getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS')) || DEFAULT_CURRENCY_DECIMALS, decimals: Number(getEnvValue('NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS')) || DEFAULT_CURRENCY_DECIMALS,
}, },
governanceToken: { secondaryCoin: {
symbol: getEnvValue('NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL'), symbol: getEnvValue('NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL'),
}, },
rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'), rpcUrl: getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL'),
isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true', isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true',
......
...@@ -13,6 +13,7 @@ NEXT_PUBLIC_NETWORK_ID=100 ...@@ -13,6 +13,7 @@ NEXT_PUBLIC_NETWORK_ID=100
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=xDAI NEXT_PUBLIC_NETWORK_CURRENCY_NAME=xDAI
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=xDAI NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=xDAI
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.gnosischain.com NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.gnosischain.com
...@@ -22,7 +23,7 @@ NEXT_PUBLIC_API_BASE_PATH=/ ...@@ -22,7 +23,7 @@ NEXT_PUBLIC_API_BASE_PATH=/
# ui config # ui config
## homepage ## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','secondary_coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgb(46, 74, 60)" NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgb(46, 74, 60)"
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR="rgb(255, 255, 255)" NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR="rgb(255, 255, 255)"
## sidebar ## sidebar
...@@ -42,6 +43,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true ...@@ -42,6 +43,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace/gnosis-chain.json NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace/gnosis-chain.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrmiO9mDGJoPNmJe NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrmiO9mDGJoPNmJe
NEXT_PUBLIC_STATS_API_HOST="https://stats-gnosis-mainnet.k8s-prod-1.blockscout.com"
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
......
...@@ -13,7 +13,6 @@ NEXT_PUBLIC_NETWORK_ID=5 ...@@ -13,7 +13,6 @@ NEXT_PUBLIC_NETWORK_ID=5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
......
# Set of ENVs for Optimism (dev only)
# https://optimism.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='OP Mainnet'
NEXT_PUBLIC_NETWORK_SHORT_NAME='OP'
NEXT_PUBLIC_NETWORK_ID=10
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=OP
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.optimism.io
# api configuration
NEXT_PUBLIC_API_HOST=optimism.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap','secondary_coin_price']
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='rgb(255,255,255)'
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='linear-gradient(90deg,rgb(232,52,53)0%,rgb(139,28,232)100%)'
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-mainnet.json
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://optimism.drpc.org?ref=559183','text':'Public RPC'}]
## footer
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json
## views
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
## misc
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/optimism-mainnet.png
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic'}},{'title':'3xpl','baseUrl':'https://3xpl.com/','paths':{'tx':'/optimism/transaction','address':'/optimism/address'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST=https://stats-optimism-mainnet.k8s-prod-1.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/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_SWAP_BUTTON_URL=uniswap
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
# rollup
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/
\ No newline at end of file
# Set of ENVs for zkevm (dev only)
# https://eth.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='OP Goerli'
NEXT_PUBLIC_NETWORK_SHORT_NAME='OP Goerli'
NEXT_PUBLIC_NETWORK_ID=420
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://goerli.optimism.io
# api configuration
NEXT_PUBLIC_API_HOST=optimism-goerli.blockscout.com
NEXT_PUBLIC_API_PORT=80
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/polygon-mainnet.json
## footer
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
# NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
# rollup
NEXT_PUBLIC_ROLLUP_TYPE='optimistic'
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-goerli.blockscout.com/
\ No newline at end of file
...@@ -25,6 +25,7 @@ import type { ValidatorsChainType } from '../../../types/client/validators'; ...@@ -25,6 +25,7 @@ import type { ValidatorsChainType } from '../../../types/client/validators';
import type { WalletType } from '../../../types/client/wallets'; import type { WalletType } from '../../../types/client/wallets';
import { SUPPORTED_WALLETS } from '../../../types/client/wallets'; import { SUPPORTED_WALLETS } from '../../../types/client/wallets';
import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks'; import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
import { CHAIN_INDICATOR_IDS } from '../../../types/homepage';
import type { ChainIndicatorId } from '../../../types/homepage'; import type { ChainIndicatorId } from '../../../types/homepage';
import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks'; import { type NetworkVerificationType, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
import type { AddressViewId } from '../../../types/views/address'; import type { AddressViewId } from '../../../types/views/address';
...@@ -481,7 +482,7 @@ const schema = yup ...@@ -481,7 +482,7 @@ const schema = yup
NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(), NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: yup.string(),
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: yup.number().integer().positive(), NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: yup.number().integer().positive(),
NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL: yup.string(), NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL: yup.string(),
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: yup.string<NetworkVerificationType>().oneOf([ 'validation', 'mining' ]), NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: yup.string<NetworkVerificationType>().oneOf([ 'validation', 'mining' ]),
NEXT_PUBLIC_IS_TESTNET: yup.boolean(), NEXT_PUBLIC_IS_TESTNET: yup.boolean(),
...@@ -498,7 +499,7 @@ const schema = yup ...@@ -498,7 +499,7 @@ const schema = yup
.array() .array()
.transform(replaceQuotes) .transform(replaceQuotes)
.json() .json()
.of(yup.string<ChainIndicatorId>().oneOf([ 'daily_txs', 'coin_price', 'market_cap', 'tvl' ])), .of(yup.string<ChainIndicatorId>().oneOf(CHAIN_INDICATOR_IDS)),
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(), NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(),
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(), NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(),
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(), NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(),
......
...@@ -30,7 +30,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 ...@@ -30,7 +30,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Explorer','baseUrl':'https://example.com/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Explorer','baseUrl':'https://example.com/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL=gETH NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=GNO
NEXT_PUBLIC_NETWORK_ICON=https://example.com/icon.png NEXT_PUBLIC_NETWORK_ICON=https://example.com/icon.png
NEXT_PUBLIC_NETWORK_ICON_DARK=https://example.com/icon.png NEXT_PUBLIC_NETWORK_ICON_DARK=https://example.com/icon.png
NEXT_PUBLIC_NETWORK_LOGO=https://example.com/logo.png NEXT_PUBLIC_NETWORK_LOGO=https://example.com/logo.png
......
...@@ -7,3 +7,4 @@ ...@@ -7,3 +7,4 @@
| NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | | NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL |
| NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L1_BASE_URL | | NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L1_BASE_URL |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED | | NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED |
| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL |
\ No newline at end of file
...@@ -73,6 +73,8 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -73,6 +73,8 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
## Blockchain parameters ## Blockchain parameters
*Note!* The `NEXT_PUBLIC_NETWORK_CURRENCY` variables represent the blockchain's native token used for paying transaction fees. `NEXT_PUBLIC_NETWORK_SECONDARY_COIN` variables refer to tokens like protocol-specific tokens (e.g., OP token on Optimism chain) or governance tokens (e.g., GNO on Gnosis chain).
| Variable | Type| Description | Compulsoriness | Default value | Example value | | Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | | NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` |
...@@ -83,7 +85,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -83,7 +85,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of network currency subdenomination | - | `wei` | `duck` | | NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of network currency subdenomination | - | `wei` | `duck` |
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` | | NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | - | - | `ETH` |
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` | | NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | - | `18` | `6` |
| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | | NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | `string` | Network secondary coin symbol. | - | - | `GNO` |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` | Verification type in the network | - | `mining` | `validation` | | NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` | Verification type in the network | - | `mining` | `validation` |
| NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` | | NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` |
...@@ -107,7 +109,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -107,7 +109,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| Variable | Type| Description | Compulsoriness | Default value | Example value | | Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | | NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'secondary_coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `white` | `\#DCFE76` | | NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `white` | `\#DCFE76` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | | NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` |
......
<svg viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 4.167c0-.92.746-1.667 1.667-1.667H8c.92 0 1.667.746 1.667 1.667V7.5c0 .92-.747 1.667-1.667 1.667H4.667C3.747 9.167 3 8.42 3 7.5V4.167Zm1.667.5a.5.5 0 0 1 .5-.5H7.5a.5.5 0 0 1 .5.5V7a.5.5 0 0 1-.5.5H5.167a.5.5 0 0 1-.5-.5V4.667ZM3 12.5c0-.92.746-1.667 1.667-1.667H8c.92 0 1.667.746 1.667 1.667v3.333c0 .92-.747 1.667-1.667 1.667H4.667C3.747 17.5 3 16.754 3 15.833V12.5Zm1.667.5a.5.5 0 0 1 .5-.5H7.5a.5.5 0 0 1 .5.5v2.333a.5.5 0 0 1-.5.5H5.167a.5.5 0 0 1-.5-.5V13ZM13 2.5c-.92 0-1.667.746-1.667 1.667V7.5c0 .92.746 1.667 1.667 1.667h3.333C17.253 9.167 18 8.42 18 7.5V4.167c0-.92-.746-1.667-1.667-1.667H13Zm3.333 2.167a.5.5 0 0 0-.5-.5H13.5a.5.5 0 0 0-.5.5V7a.5.5 0 0 0 .5.5h2.333a.5.5 0 0 0 .5-.5V4.667ZM11.333 12.5c0-.92.746-1.667 1.667-1.667h3.333c.92 0 1.667.746 1.667 1.667v3.333c0 .92-.746 1.667-1.667 1.667H13c-.92 0-1.667-.746-1.667-1.667V12.5ZM13 13a.5.5 0 0 1 .5-.5h2.333a.5.5 0 0 1 .5.5v2.333a.5.5 0 0 1-.5.5H13.5a.5.5 0 0 1-.5-.5V13Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M3 4.167c0-.92.746-1.667 1.667-1.667H8c.92 0 1.667.746 1.667 1.667V7.5c0 .92-.747 1.667-1.667 1.667H4.667C3.747 9.167 3 8.42 3 7.5V4.167Zm1.667.5a.5.5 0 0 1 .5-.5H7.5a.5.5 0 0 1 .5.5V7a.5.5 0 0 1-.5.5H5.167a.5.5 0 0 1-.5-.5V4.667ZM3 12.5c0-.92.746-1.667 1.667-1.667H8c.92 0 1.667.746 1.667 1.667v3.333c0 .92-.747 1.667-1.667 1.667H4.667C3.747 17.5 3 16.754 3 15.833V12.5Zm1.667.5a.5.5 0 0 1 .5-.5H7.5a.5.5 0 0 1 .5.5v2.333a.5.5 0 0 1-.5.5H5.167a.5.5 0 0 1-.5-.5V13ZM13 2.5c-.92 0-1.667.746-1.667 1.667V7.5c0 .92.746 1.667 1.667 1.667h3.333C17.253 9.167 18 8.42 18 7.5V4.167c0-.92-.746-1.667-1.667-1.667H13Zm3.333 2.167a.5.5 0 0 0-.5-.5H13.5a.5.5 0 0 0-.5.5V7a.5.5 0 0 0 .5.5h2.333a.5.5 0 0 0 .5-.5V4.667Zm-5 7.833c0-.92.746-1.667 1.667-1.667h3.333c.92 0 1.667.746 1.667 1.667v3.333c0 .92-.746 1.667-1.667 1.667H13c-.92 0-1.667-.746-1.667-1.667V12.5ZM13 13a.5.5 0 0 1 .5-.5h2.333a.5.5 0 0 1 .5.5v2.333a.5.5 0 0 1-.5.5H13.5a.5.5 0 0 1-.5-.5V13Z" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 0 26 12" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 26 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="26" height="12" rx="2" fill="#F56565"/> <rect width="26" height="12" rx="2" fill="#F56565"/>
<path d="M3.028 9V1.727h1.061V4.43h.064c.062-.114.15-.245.267-.394.116-.15.277-.28.483-.391.205-.114.478-.17.816-.17.44 0 .834.11 1.18.333.345.223.616.544.812.963.2.419.299.923.299 1.512 0 .59-.098 1.095-.295 1.517a2.3 2.3 0 0 1-.81.97 2.097 2.097 0 0 1-1.175.337c-.332 0-.603-.056-.813-.167a1.54 1.54 0 0 1-.49-.391 2.957 2.957 0 0 1-.274-.398h-.089V9H3.028Zm1.04-2.727c0 .383.056.72.167 1.008.111.29.272.515.483.679.21.16.469.241.774.241.317 0 .582-.084.795-.252.214-.17.375-.401.483-.693.112-.29.167-.619.167-.983 0-.36-.054-.683-.163-.97a1.484 1.484 0 0 0-.483-.678c-.213-.166-.48-.249-.799-.249-.308 0-.568.08-.781.238-.21.159-.37.38-.48.664a2.77 2.77 0 0 0-.163.995Zm7.485 2.837c-.538 0-1-.115-1.389-.344a2.336 2.336 0 0 1-.894-.977c-.209-.421-.313-.915-.313-1.48 0-.56.104-1.052.313-1.478.21-.426.504-.759.88-.998.379-.239.822-.359 1.328-.359.308 0 .606.051.895.153.29.102.548.262.778.48.23.217.41.5.543.848.133.346.2.766.2 1.26v.377H9.556v-.795h3.296c0-.28-.057-.527-.17-.742a1.29 1.29 0 0 0-1.198-.703c-.298 0-.558.073-.78.22-.221.144-.391.334-.512.568a1.641 1.641 0 0 0-.178.756v.622c0 .364.064.674.192.93.13.256.311.451.543.586.232.133.503.199.814.199.2 0 .384-.028.55-.085a1.143 1.143 0 0 0 .707-.692l1.005.18a1.82 1.82 0 0 1-.434.778 2.1 2.1 0 0 1-.777.515 2.91 2.91 0 0 1-1.062.181Zm6.064-5.565v.853h-2.979v-.853h2.98Zm-2.18-1.306h1.062v5.16c0 .205.03.36.092.465.062.101.14.171.238.21.1.035.207.052.323.052.085 0 .16-.005.224-.017l.149-.029.192.877a2.08 2.08 0 0 1-.689.114 1.87 1.87 0 0 1-.781-.15 1.34 1.34 0 0 1-.586-.482c-.15-.218-.224-.491-.224-.82v-5.38Zm4.942 6.882c-.345 0-.658-.064-.937-.192a1.58 1.58 0 0 1-.664-.565c-.161-.246-.242-.548-.242-.905 0-.308.06-.561.178-.76a1.31 1.31 0 0 1 .48-.472c.2-.116.425-.204.674-.263.248-.06.502-.104.76-.135l.795-.092c.204-.027.352-.068.444-.125.092-.057.139-.149.139-.277V5.31c0-.31-.088-.55-.263-.72-.173-.171-.431-.256-.774-.256-.358 0-.64.08-.845.238-.204.156-.345.33-.423.522l-.998-.228a1.92 1.92 0 0 1 .519-.802c.23-.206.493-.355.791-.448.299-.094.612-.142.941-.142.218 0 .45.026.693.079.246.05.476.142.689.277.215.134.392.327.53.578.136.249.205.572.205.97V9h-1.037v-.746h-.043a1.512 1.512 0 0 1-.308.405 1.642 1.642 0 0 1-.53.33 2.053 2.053 0 0 1-.774.132Zm.231-.853c.294 0 .545-.058.753-.174.21-.116.37-.267.48-.454.11-.19.166-.392.166-.607V6.33a.552.552 0 0 1-.22.106 3.43 3.43 0 0 1-.366.082l-.401.06c-.13.017-.24.03-.327.043a2.63 2.63 0 0 0-.564.131.97.97 0 0 0-.405.266.665.665 0 0 0-.15.455c0 .263.098.462.292.597.194.132.441.198.742.198Z" fill="#fff"/> <path d="M3.028 9V1.727h1.061V4.43h.064c.062-.114.15-.245.267-.394.116-.15.277-.28.483-.391.205-.114.478-.17.816-.17.44 0 .834.11 1.18.333.345.223.616.544.812.963.2.419.299.923.299 1.512 0 .59-.098 1.095-.295 1.517a2.3 2.3 0 0 1-.81.97 2.097 2.097 0 0 1-1.175.337c-.332 0-.603-.056-.813-.167a1.54 1.54 0 0 1-.49-.391 2.957 2.957 0 0 1-.274-.398h-.089V9H3.028Zm1.04-2.727c0 .383.056.72.167 1.008.111.29.272.515.483.679.21.16.469.241.774.241.317 0 .582-.084.795-.252.214-.17.375-.401.483-.693.112-.29.167-.619.167-.983 0-.36-.054-.683-.163-.97a1.484 1.484 0 0 0-.483-.678c-.213-.166-.48-.249-.799-.249-.308 0-.568.08-.781.238-.21.159-.37.38-.48.664a2.77 2.77 0 0 0-.163.995Zm7.485 2.837c-.538 0-1-.115-1.389-.344a2.336 2.336 0 0 1-.894-.977c-.209-.421-.313-.915-.313-1.48 0-.56.104-1.052.313-1.478.21-.426.504-.759.88-.998.379-.239.822-.359 1.328-.359.308 0 .606.051.895.153.29.102.548.262.778.48.23.217.41.5.543.848.133.346.2.766.2 1.26v.377H9.556v-.795h3.296c0-.28-.057-.527-.17-.742a1.29 1.29 0 0 0-1.198-.703 1.38 1.38 0 0 0-.78.22 1.474 1.474 0 0 0-.512.568 1.641 1.641 0 0 0-.178.756v.622c0 .364.064.674.192.93.13.256.311.451.543.586.232.133.503.199.814.199.2 0 .384-.028.55-.085a1.143 1.143 0 0 0 .707-.692l1.005.18a1.82 1.82 0 0 1-.434.778 2.1 2.1 0 0 1-.777.515 2.91 2.91 0 0 1-1.062.181Zm6.064-5.565v.853h-2.979v-.853h2.98Zm-2.18-1.306h1.062v5.16c0 .205.03.36.092.465a.49.49 0 0 0 .238.21c.1.035.207.052.323.052.085 0 .16-.005.224-.017l.149-.029.192.877a2.08 2.08 0 0 1-.689.114 1.87 1.87 0 0 1-.781-.15 1.34 1.34 0 0 1-.586-.482c-.15-.218-.224-.491-.224-.82v-5.38Zm4.942 6.882c-.345 0-.658-.064-.937-.192a1.58 1.58 0 0 1-.664-.565c-.161-.246-.242-.548-.242-.905 0-.308.06-.561.178-.76a1.31 1.31 0 0 1 .48-.472c.2-.116.425-.204.674-.263a6.56 6.56 0 0 1 .76-.135l.795-.092c.204-.027.352-.068.444-.125.092-.057.139-.149.139-.277V5.31c0-.31-.088-.55-.263-.72-.173-.171-.431-.256-.774-.256-.358 0-.64.08-.845.238-.204.156-.345.33-.423.522l-.998-.228a1.92 1.92 0 0 1 .519-.802c.23-.206.493-.355.791-.448a3.116 3.116 0 0 1 1.634-.063c.246.05.476.142.689.277.215.134.392.327.53.578.136.249.205.572.205.97V9h-1.037v-.746h-.043a1.512 1.512 0 0 1-.308.405 1.642 1.642 0 0 1-.53.33 2.053 2.053 0 0 1-.774.132Zm.231-.853c.294 0 .545-.058.753-.174a1.192 1.192 0 0 0 .646-1.061V6.33a.552.552 0 0 1-.22.106 3.43 3.43 0 0 1-.366.082l-.401.06c-.13.017-.24.03-.327.043a2.63 2.63 0 0 0-.564.131.97.97 0 0 0-.405.266.665.665 0 0 0-.15.455c0 .263.098.462.292.597.194.132.441.198.742.198Z" fill="#fff"/>
</svg> </svg>
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="12" height="12" rx="2" fill="#F56565"/> <rect width="12" height="12" rx="2" fill="#F56565"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.42 2.494c-.373-.313-.896-.49-1.5-.49a2.028 2.028 0 0 0-1.52.567 1.948 1.948 0 0 0-.453.685c-.1.257-.143.531-.127.805V10h.79V8.508a2.46 2.46 0 0 0 1.59.549c.3.01.6-.04.88-.147a2.21 2.21 0 0 0 .751-.48c.215-.208.383-.457.495-.733.112-.274.165-.568.157-.863a1.925 1.925 0 0 0-.309-1.09 1.978 1.978 0 0 0-.717-.664c.157-.147.287-.32.381-.514.12-.245.18-.515.176-.787 0-.523-.22-.97-.594-1.285Zm-2.023.36c.163-.06.338-.085.512-.074h.008c.413 0 .741.105.964.28.22.171.343.417.343.72v.004a.944.944 0 0 1-.288.713.99.99 0 0 1-.732.283l-.115-.004v.781h.112c.467 0 .84.13 1.094.351.253.219.398.534.398.927v.004a1.357 1.357 0 0 1-.418 1.04 1.422 1.422 0 0 1-1.069.4h-.012a1.532 1.532 0 0 1-1.109-.393 1.437 1.437 0 0 1-.475-1.052V4.05a1.165 1.165 0 0 1 .352-.923c.123-.12.272-.214.435-.274Z" fill="#fff"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M7.42 2.494c-.373-.313-.896-.49-1.5-.49a2.028 2.028 0 0 0-1.52.567 1.948 1.948 0 0 0-.453.685 1.91 1.91 0 0 0-.127.805V10h.79V8.508a2.46 2.46 0 0 0 1.59.549c.3.01.6-.04.88-.147a2.21 2.21 0 0 0 .751-.48 2.136 2.136 0 0 0 .652-1.596 1.925 1.925 0 0 0-.309-1.09 1.978 1.978 0 0 0-.717-.664 1.75 1.75 0 0 0 .557-1.301c0-.523-.22-.97-.594-1.285Zm-2.023.36c.163-.06.338-.085.512-.074h.008c.413 0 .741.105.964.28.22.171.343.417.343.72v.004a.944.944 0 0 1-.288.713.99.99 0 0 1-.732.283l-.115-.004v.781h.112c.467 0 .84.13 1.094.351.253.219.398.534.398.927v.004a1.357 1.357 0 0 1-.418 1.04 1.422 1.422 0 0 1-1.069.4h-.012a1.532 1.532 0 0 1-1.109-.393 1.437 1.437 0 0 1-.475-1.052V4.05a1.165 1.165 0 0 1 .352-.923c.123-.12.272-.214.435-.274Z" fill="#fff"/>
</svg> </svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/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.4 2a1.8 1.8 0 0 0-1.8 1.8v14.8A1.4 1.4 0 0 0 3 20h1.6v-1.326H3V3.516h1.6V2H3.4Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M3.4 2a1.8 1.8 0 0 0-1.8 1.8v14.8A1.4 1.4 0 0 0 3 20h1.6v-1.326H3V3.516h1.6V2H3.4Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.992.45C4.244.163 4.585 0 4.94 0h8.038a.63.63 0 0 1 .474.225L18.14 5.61a.83.83 0 0 1 .196.544v12.308c0 .408-.141.799-.392 1.087-.252.289-.593.451-.948.451H4.94c-.356 0-.696-.162-.948-.45a1.661 1.661 0 0 1-.392-1.088V1.538c0-.408.141-.799.392-1.087Zm.948 1.088h6.87v4.497c0 .388.315.702.702.702h4.485v11.725H4.94V1.538Zm8.274.59 2.791 3.205h-2.79V2.128Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M3.992.45C4.244.163 4.585 0 4.94 0h8.038a.63.63 0 0 1 .474.225L18.14 5.61a.83.83 0 0 1 .196.544v12.308c0 .408-.141.799-.392 1.087a1.26 1.26 0 0 1-.948.451H4.94c-.356 0-.696-.162-.948-.45a1.661 1.661 0 0 1-.392-1.088V1.538c0-.408.141-.799.392-1.087Zm.948 1.088h6.87v4.497c0 .388.315.702.702.702h4.485v11.725H4.94V1.538Zm8.274.59 2.791 3.205h-2.79V2.128Z" fill="currentColor"/>
<rect x="7.2" y="14.3" width="7.8" height="1.2" rx=".6" fill="currentColor"/> <rect x="7.2" y="14.3" width="7.8" height="1.2" rx=".6" fill="currentColor"/>
<rect x="7.2" y="12" width="7.8" height="1.2" rx=".6" fill="currentColor"/> <rect x="7.2" y="12" width="7.8" height="1.2" rx=".6" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/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.4 2a1.8 1.8 0 0 0-1.8 1.8v14.8A1.4 1.4 0 0 0 3 20h1.6v-1.326H3V3.516h1.6V2H3.4Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M3.4 2a1.8 1.8 0 0 0-1.8 1.8v14.8A1.4 1.4 0 0 0 3 20h1.6v-1.326H3V3.516h1.6V2H3.4Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.992.45C4.244.163 4.585 0 4.94 0h8.038a.63.63 0 0 1 .474.225L18.14 5.61a.83.83 0 0 1 .196.544v12.308c0 .408-.141.799-.392 1.087-.252.289-.593.451-.948.451H4.94c-.356 0-.696-.162-.948-.45a1.661 1.661 0 0 1-.392-1.088V1.538c0-.408.141-.799.392-1.087Zm8.709 1.088H4.94v16.924h12.057V6.472l-4.296-4.934Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M3.992.45C4.244.163 4.585 0 4.94 0h8.038a.63.63 0 0 1 .474.225L18.14 5.61a.83.83 0 0 1 .196.544v12.308c0 .408-.141.799-.392 1.087a1.26 1.26 0 0 1-.948.451H4.94c-.356 0-.696-.162-.948-.45a1.661 1.661 0 0 1-.392-1.088V1.538c0-.408.141-.799.392-1.087Zm8.709 1.088H4.94v16.924h12.057V6.472l-4.296-4.934Z" fill="currentColor"/>
<path d="m7.9 13.357 2.2 2.357L14.5 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="m7.9 13.357 2.2 2.357L14.5 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.512.42c.388 0 .702.315.702.703v4.21h4.21a.702.702 0 1 1 0 1.404h-4.912a.702.702 0 0 1-.702-.702V1.123c0-.388.315-.702.702-.702Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M12.512.42c.388 0 .702.315.702.703v4.21h4.21a.702.702 0 1 1 0 1.404h-4.912a.702.702 0 0 1-.702-.702V1.123c0-.388.315-.702.702-.702Z" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.76 18.333a.603.603 0 0 1-.294-.075L10 15.798l-4.467 2.46a.607.607 0 0 1-.663-.052.657.657 0 0 1-.213-.285.69.69 0 0 1-.038-.36l.853-5.21-3.615-3.69a.69.69 0 0 1-.16-.677.663.663 0 0 1 .194-.3c.09-.08.199-.131.315-.149l4.995-.76 2.233-4.74a.65.65 0 0 1 .233-.269.61.61 0 0 1 .666 0c.1.065.18.158.232.269l2.234 4.74 4.994.76c.116.018.226.07.316.149.09.079.157.183.193.3a.69.69 0 0 1-.16.678l-3.615 3.69.854 5.21a.692.692 0 0 1-.14.537.636.636 0 0 1-.216.173.607.607 0 0 1-.266.061h.001Z" fill="currentColor"/> <path d="M14.76 18.333a.603.603 0 0 1-.294-.075L10 15.798l-4.467 2.46a.607.607 0 0 1-.663-.052.657.657 0 0 1-.213-.285.69.69 0 0 1-.038-.36l.853-5.21-3.615-3.69a.69.69 0 0 1-.16-.677.663.663 0 0 1 .194-.3.615.615 0 0 1 .315-.149l4.995-.76 2.233-4.74a.65.65 0 0 1 .233-.269.61.61 0 0 1 .666 0c.1.065.18.158.232.269l2.234 4.74 4.994.76c.116.018.226.07.316.149.09.079.157.183.193.3a.69.69 0 0 1-.16.678l-3.615 3.69.854 5.21a.692.692 0 0 1-.14.537.636.636 0 0 1-.216.173.607.607 0 0 1-.266.061h.001Z" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.76 18.333a.603.603 0 0 1-.294-.075l.293.075Zm.003 0c.09 0 .18-.021.262-.061.083-.04.157-.1.216-.173a.674.674 0 0 0 .14-.538l-.854-5.21 3.616-3.69a.69.69 0 0 0 .16-.677.662.662 0 0 0-.194-.3.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.158-.117l-2.186-4.64a.651.651 0 0 0-.232-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.233.269l-2.186 4.64a.208.208 0 0 1-.157.117l-4.885.743a.617.617 0 0 0-.315.149.663.663 0 0 0-.194.3.69.69 0 0 0 .16.678L5.4 12.276a.208.208 0 0 1 .056.18L4.62 17.56a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.285.613.613 0 0 0 .663.052l4.366-2.405a.208.208 0 0 1 .201 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.306.216L9.9 13.95a.208.208 0 0 1 .201 0l2.922 1.61a.208.208 0 0 0 .306-.216l-.565-3.452a.208.208 0 0 1 .057-.18l2.485-2.536a.208.208 0 0 0-.117-.351l-3.409-.52a.208.208 0 0 1-.157-.116l-1.434-3.044a.208.208 0 0 0-.377 0L8.377 8.189a.208.208 0 0 1-.157.117l-3.408.518a.208.208 0 0 0-.118.352l2.486 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.092 2.99h-.003.003Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M14.76 18.333a.603.603 0 0 1-.294-.075l.293.075Zm.003 0a.6.6 0 0 0 .262-.061c.083-.04.157-.1.216-.173a.674.674 0 0 0 .14-.538l-.854-5.21 3.616-3.69a.69.69 0 0 0 .16-.677.662.662 0 0 0-.194-.3.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.158-.117l-2.186-4.64a.651.651 0 0 0-.232-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.233.269l-2.186 4.64a.208.208 0 0 1-.157.117l-4.885.743a.617.617 0 0 0-.315.149.663.663 0 0 0-.194.3.69.69 0 0 0 .16.678L5.4 12.276a.208.208 0 0 1 .056.18L4.62 17.56a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.285.613.613 0 0 0 .663.052L9.9 15.852a.208.208 0 0 1 .201 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.306.216L9.9 13.95a.208.208 0 0 1 .201 0l2.922 1.61a.208.208 0 0 0 .306-.216l-.565-3.452a.208.208 0 0 1 .057-.18l2.485-2.536a.208.208 0 0 0-.117-.351l-3.409-.52a.208.208 0 0 1-.157-.116l-1.434-3.044a.208.208 0 0 0-.377 0L8.377 8.189a.208.208 0 0 1-.157.117l-3.408.518a.208.208 0 0 0-.118.352l2.486 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.092 2.99h-.003.003Z" fill="currentColor"/>
</svg> </svg>
...@@ -34,7 +34,7 @@ import type { ...@@ -34,7 +34,7 @@ import type {
import type { AddressesResponse } from 'types/api/addresses'; import type { AddressesResponse } from 'types/api/addresses';
import type { TxBlobs, Blob } from 'types/api/blobs'; import type { TxBlobs, Blob } from 'types/api/blobs';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
import type { BackendVersionConfig } from 'types/api/configs'; import type { BackendVersionConfig } from 'types/api/configs';
import type { import type {
SmartContract, SmartContract,
...@@ -88,6 +88,7 @@ import type { ...@@ -88,6 +88,7 @@ import type {
TransactionsResponseWatchlist, TransactionsResponseWatchlist,
TransactionsSorting, TransactionsSorting,
TransactionsResponseWithBlobs, TransactionsResponseWithBlobs,
TransactionsStats,
} from 'types/api/transaction'; } from 'types/api/transaction';
import type { TxInterpretationResponse } from 'types/api/txInterpretation'; import type { TxInterpretationResponse } from 'types/api/txInterpretation';
import type { TTxsFilters, TTxsWithBlobsFilters } from 'types/api/txsFilters'; import type { TTxsFilters, TTxsWithBlobsFilters } from 'types/api/txsFilters';
...@@ -276,6 +277,9 @@ export const RESOURCES = { ...@@ -276,6 +277,9 @@ export const RESOURCES = {
pathParams: [ 'height_or_hash' as const ], pathParams: [ 'height_or_hash' as const ],
filterFields: [], filterFields: [],
}, },
txs_stats: {
path: '/api/v2/transactions/stats',
},
txs_validated: { txs_validated: {
path: '/api/v2/transactions', path: '/api/v2/transactions',
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
...@@ -540,6 +544,9 @@ export const RESOURCES = { ...@@ -540,6 +544,9 @@ export const RESOURCES = {
stats_charts_market: { stats_charts_market: {
path: '/api/v2/stats/charts/market', path: '/api/v2/stats/charts/market',
}, },
stats_charts_secondary_coin_price: {
path: '/api/v2/stats/charts/secondary-coin-market',
},
// HOMEPAGE // HOMEPAGE
homepage_blocks: { homepage_blocks: {
...@@ -823,6 +830,7 @@ Q extends 'token_info_applications' ? TokenInfoApplications : ...@@ -823,6 +830,7 @@ Q extends 'token_info_applications' ? TokenInfoApplications :
Q extends 'stats' ? HomeStats : Q extends 'stats' ? HomeStats :
Q extends 'stats_charts_txs' ? ChartTransactionResponse : Q extends 'stats_charts_txs' ? ChartTransactionResponse :
Q extends 'stats_charts_market' ? ChartMarketResponse : Q extends 'stats_charts_market' ? ChartMarketResponse :
Q extends 'stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse :
Q extends 'homepage_blocks' ? Array<Block> : Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> : Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> : Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
...@@ -838,6 +846,7 @@ Q extends 'blocks' ? BlocksResponse : ...@@ -838,6 +846,7 @@ Q extends 'blocks' ? BlocksResponse :
Q extends 'block' ? Block : Q extends 'block' ? Block :
Q extends 'block_txs' ? BlockTransactionsResponse : Q extends 'block_txs' ? BlockTransactionsResponse :
Q extends 'block_withdrawals' ? BlockWithdrawalsResponse : Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
Q extends 'txs_stats' ? TransactionsStats :
Q extends 'txs_validated' ? TransactionsResponseValidated : Q extends 'txs_validated' ? TransactionsResponseValidated :
Q extends 'txs_pending' ? TransactionsResponsePending : Q extends 'txs_pending' ? TransactionsResponsePending :
Q extends 'txs_with_blobs' ? TransactionsResponseWithBlobs : Q extends 'txs_with_blobs' ? TransactionsResponseWithBlobs :
......
...@@ -61,6 +61,11 @@ export const withoutBothPrices = { ...@@ -61,6 +61,11 @@ export const withoutBothPrices = {
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null), gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null),
}; };
export const withSecondaryCoin = {
...base,
secondary_coin_price: '3.398',
};
export const noChartData = { export const noChartData = {
...base, ...base,
transactions_today: null, transactions_today: null,
......
import type { TransactionsStats } from 'types/api/transaction';
export const base: TransactionsStats = {
pending_transactions_count: '4200',
transaction_fees_avg_24h: '22342870314428',
transaction_fees_sum_24h: '22184012506492688277',
transactions_count_24h: '992890',
};
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { Transaction } from 'types/api/transaction'; import type { Transaction, TransactionsStats } from 'types/api/transaction';
import { ADDRESS_PARAMS } from './addressParams'; import { ADDRESS_PARAMS } from './addressParams';
...@@ -59,3 +59,10 @@ export const TX_ZKEVM_L2: Transaction = { ...@@ -59,3 +59,10 @@ export const TX_ZKEVM_L2: Transaction = {
}; };
export const TX_RAW_TRACE: RawTracesResponse = []; export const TX_RAW_TRACE: RawTracesResponse = [];
export const TXS_STATS: TransactionsStats = {
pending_transactions_count: '4200',
transaction_fees_avg_24h: '22342870314428',
transaction_fees_sum_24h: '22184012506492688277',
transactions_count_24h: '992890',
};
...@@ -18,3 +18,8 @@ export interface ChartMarketResponse { ...@@ -18,3 +18,8 @@ export interface ChartMarketResponse {
available_supply: string; available_supply: string;
chart_data: Array<ChartMarketItem>; chart_data: Array<ChartMarketItem>;
} }
export interface ChartSecondaryCoinPriceResponse {
available_supply: string;
chart_data: Array<ChartMarketItem>;
}
...@@ -16,6 +16,8 @@ export type HomeStats = { ...@@ -16,6 +16,8 @@ export type HomeStats = {
network_utilization_percentage: number; network_utilization_percentage: number;
tvl: string | null; tvl: string | null;
rootstock_locked_btc?: string | null; rootstock_locked_btc?: string | null;
last_output_root_size?: string | null;
secondary_coin_price?: string | null;
} }
export type GasPrices = { export type GasPrices = {
......
...@@ -97,6 +97,13 @@ export type Transaction = { ...@@ -97,6 +97,13 @@ export type Transaction = {
export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ]; export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ];
export interface TransactionsStats {
pending_transactions_count: string;
transaction_fees_avg_24h: string;
transaction_fees_sum_24h: string;
transactions_count_24h: string;
}
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
export interface TransactionsResponseValidated { export interface TransactionsResponseValidated {
......
export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cap' | 'tvl'; export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const;
export type ChainIndicatorId = typeof CHAIN_INDICATOR_IDS[number];
...@@ -59,6 +59,7 @@ const Stats = () => { ...@@ -59,6 +59,7 @@ const Stats = () => {
if (data) { if (data) {
!data.gas_prices && itemsCount--; !data.gas_prices && itemsCount--;
data.rootstock_locked_btc && itemsCount++; data.rootstock_locked_btc && itemsCount++;
rollupFeature.isEnabled && data.last_output_root_size && itemsCount++;
const isOdd = Boolean(itemsCount % 2); const isOdd = Boolean(itemsCount % 2);
const gasInfoTooltip = hasGasTracker && data.gas_prices ? ( const gasInfoTooltip = hasGasTracker && data.gas_prices ? (
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt }> <GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt }>
...@@ -120,6 +121,15 @@ const Stats = () => { ...@@ -120,6 +121,15 @@ const Stats = () => {
url={ route({ pathname: '/txs' }) } url={ route({ pathname: '/txs' }) }
isLoading={ isLoading } isLoading={ isLoading }
/> />
{ rollupFeature.isEnabled && data.last_output_root_size && (
<StatsItem
icon="txn_batches"
title="Latest L1 state batch"
value={ data.last_output_root_size }
url={ route({ pathname: '/batches' }) }
isLoading={ isLoading }
/>
) }
<StatsItem <StatsItem
icon="wallet" icon="wallet"
title="Wallet addresses" title="Wallet addresses"
......
...@@ -78,7 +78,8 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC ...@@ -78,7 +78,8 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC
<Flex <Flex
alignItems="center" alignItems="center"
columnGap={ 3 } columnGap={ 3 }
p={ 4 } px={ 4 }
py={ 2 }
as="li" as="li"
borderRadius="md" borderRadius="md"
cursor="pointer" cursor="pointer"
......
...@@ -15,7 +15,8 @@ const TX_CHART_API_URL = buildApiUrl('stats_charts_txs'); ...@@ -15,7 +15,8 @@ const TX_CHART_API_URL = buildApiUrl('stats_charts_txs');
const test = base.extend({ const test = base.extend({
context: contextWithEnvs([ context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_HOMEPAGE_CHARTS', value: '["daily_txs","coin_price","market_cap","tvl"]' }, { name: 'NEXT_PUBLIC_HOMEPAGE_CHARTS', value: '["daily_txs","coin_price","secondary_coin_price","market_cap","tvl"]' },
{ name: 'NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL', value: 'DUCK' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any, ]) as any,
}); });
...@@ -26,7 +27,7 @@ test.describe('daily txs chart', () => { ...@@ -26,7 +27,7 @@ test.describe('daily txs chart', () => {
test.beforeEach(async({ page, mount }) => { test.beforeEach(async({ page, mount }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({ await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(statsMock.base), body: JSON.stringify(statsMock.withSecondaryCoin),
})); }));
await page.route(TX_CHART_API_URL, (route) => route.fulfill({ await page.route(TX_CHART_API_URL, (route) => route.fulfill({
status: 200, status: 200,
......
...@@ -4,7 +4,7 @@ import type { TimeChartData } from 'ui/shared/chart/types'; ...@@ -4,7 +4,7 @@ import type { TimeChartData } from 'ui/shared/chart/types';
import type { ResourcePayload } from 'lib/api/resources'; import type { ResourcePayload } from 'lib/api/resources';
export type ChartsResources = 'stats_charts_txs' | 'stats_charts_market'; export type ChartsResources = 'stats_charts_txs' | 'stats_charts_market' | 'stats_charts_secondary_coin_price';
export interface TChainIndicator<R extends ChartsResources> { export interface TChainIndicator<R extends ChartsResources> {
id: ChainIndicatorId; id: ChainIndicatorId;
......
...@@ -50,13 +50,13 @@ const nativeTokenData = { ...@@ -50,13 +50,13 @@ const nativeTokenData = {
const coinPriceIndicator: TChainIndicator<'stats_charts_market'> = { const coinPriceIndicator: TChainIndicator<'stats_charts_market'> = {
id: 'coin_price', id: 'coin_price',
title: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } price`, title: `${ config.chain.currency.symbol } price`,
value: (stats) => stats.coin_price === null ? value: (stats) => stats.coin_price === null ?
'$N/A' : '$N/A' :
'$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
valueDiff: (stats) => stats?.coin_price !== null ? stats?.coin_price_change_percentage : null, valueDiff: (stats) => stats?.coin_price !== null ? stats?.coin_price_change_percentage : null,
icon: <TokenEntity.Icon token={ nativeTokenData } boxSize={ 6 } marginRight={ 0 }/>, icon: <TokenEntity.Icon token={ nativeTokenData } boxSize={ 6 } marginRight={ 0 }/>,
hint: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } token daily price in USD.`, hint: `${ config.chain.currency.symbol } token daily price in USD.`,
api: { api: {
resourceName: 'stats_charts_market', resourceName: 'stats_charts_market',
dataFn: (response) => ([ { dataFn: (response) => ([ {
...@@ -65,7 +65,30 @@ const coinPriceIndicator: TChainIndicator<'stats_charts_market'> = { ...@@ -65,7 +65,30 @@ const coinPriceIndicator: TChainIndicator<'stats_charts_market'> = {
.sort(sortByDateDesc) .sort(sortByDateDesc)
.reduceRight(nonNullTailReducer, [] as Array<TimeChartItemRaw>) .reduceRight(nonNullTailReducer, [] as Array<TimeChartItemRaw>)
.map(mapNullToZero), .map(mapNullToZero),
name: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } price`, name: `${ config.chain.currency.symbol } price`,
valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
} ]),
},
};
const secondaryCoinPriceIndicator: TChainIndicator<'stats_charts_secondary_coin_price'> = {
id: 'secondary_coin_price',
title: `${ config.chain.secondaryCoin.symbol } price`,
value: (stats) => !stats.secondary_coin_price || stats.secondary_coin_price === null ?
'$N/A' :
'$' + Number(stats.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
valueDiff: () => null,
icon: <TokenEntity.Icon token={ nativeTokenData } boxSize={ 6 } marginRight={ 0 }/>,
hint: `${ config.chain.secondaryCoin.symbol } token daily price in USD.`,
api: {
resourceName: 'stats_charts_secondary_coin_price',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: item.closing_price }))
.sort(sortByDateDesc)
.reduceRight(nonNullTailReducer, [] as Array<TimeChartItemRaw>)
.map(mapNullToZero),
name: `${ config.chain.secondaryCoin.symbol } price`,
valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
} ]), } ]),
}, },
...@@ -138,6 +161,7 @@ const tvlIndicator: TChainIndicator<'stats_charts_market'> = { ...@@ -138,6 +161,7 @@ const tvlIndicator: TChainIndicator<'stats_charts_market'> = {
const INDICATORS = [ const INDICATORS = [
dailyTxsIndicator, dailyTxsIndicator,
coinPriceIndicator, coinPriceIndicator,
secondaryCoinPriceIndicator,
marketPriceIndicator, marketPriceIndicator,
tvlIndicator, tvlIndicator,
]; ];
......
...@@ -21,6 +21,7 @@ const Stats = () => { ...@@ -21,6 +21,7 @@ const Stats = () => {
handleFilterChange, handleFilterChange,
displayedCharts, displayedCharts,
filterQuery, filterQuery,
initialFilterQuery,
} = useStats(); } = useStats();
return ( return (
...@@ -33,6 +34,8 @@ const Stats = () => { ...@@ -33,6 +34,8 @@ const Stats = () => {
<Box mb={{ base: 6, sm: 8 }}> <Box mb={{ base: 6, sm: 8 }}>
<StatsFilters <StatsFilters
isLoading={ isPlaceholderData }
initialFilterValue={ initialFilterQuery }
sections={ sections } sections={ sections }
currentSection={ currentSection } currentSection={ currentSection }
onSectionChange={ handleSectionChange } onSectionChange={ handleSectionChange }
...@@ -44,6 +47,7 @@ const Stats = () => { ...@@ -44,6 +47,7 @@ const Stats = () => {
<ChartsWidgetsList <ChartsWidgetsList
filterQuery={ filterQuery } filterQuery={ filterQuery }
initialFilterQuery={ initialFilterQuery }
isError={ isError } isError={ isError }
isPlaceholderData={ isPlaceholderData } isPlaceholderData={ isPlaceholderData }
charts={ displayedCharts } charts={ displayedCharts }
......
...@@ -14,6 +14,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -14,6 +14,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TxsStats from 'ui/txs/TxsStats';
import TxsWatchlist from 'ui/txs/TxsWatchlist'; import TxsWatchlist from 'ui/txs/TxsWatchlist';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting'; import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
...@@ -140,6 +141,7 @@ const Transactions = () => { ...@@ -140,6 +141,7 @@ const Transactions = () => {
return ( return (
<> <>
<PageTitle title="Transactions" withTextAd/> <PageTitle title="Transactions" withTextAd/>
<TxsStats/>
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
......
import React from 'react';
import { test, expect } from 'playwright/lib';
import StatsWidget from './StatsWidget';
test.use({ viewport: { width: 300, height: 100 } });
test('with positive diff +@dark-mode', async({ render }) => {
const component = await render(
<StatsWidget
label="Verified contracts"
hint="Contracts that have been verified"
value="1 000 000"
diff={ 4200 }
diffFormatted="4 200"
/>,
);
await expect(component).toHaveScreenshot();
});
// according to current logic we don't show diff if it's negative
test('with negative diff', async({ render }) => {
const component = await render(
<StatsWidget
label="Verified contracts"
hint="Contracts that have been verified"
value="1,000,000"
diff={ -4200 }
/>,
);
await expect(component).toHaveScreenshot();
});
test('loading state', async({ render }) => {
const component = await render(
<StatsWidget
label="Verified contracts"
hint="Contracts that have been verified"
value="1,000,000"
isLoading
/>,
);
await expect(component).toHaveScreenshot();
});
test('with period only', async({ render }) => {
const component = await render(
<StatsWidget
label="Verified contracts"
hint="Contracts that have been verified"
value="1,000,000"
period="1h"
/>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Flex, Text, Skeleton, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, Text, Skeleton, useColorModeValue, chakra } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import type { Route } from 'nextjs-routes';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
import TruncatedValue from 'ui/shared/TruncatedValue';
type Props = { type Props = {
label: string; label: string;
value: string; value: string;
valuePrefix?: string;
valuePostfix?: string;
hint?: string; hint?: string;
isLoading?: boolean; isLoading?: boolean;
diff?: string | number; diff?: string | number;
diffFormatted?: string; diffFormatted?: string;
diffPeriod?: '24h'; diffPeriod?: '24h';
period?: '1h' | '24h';
href?: Route;
} }
const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h', diffFormatted }: Props) => { const Container = ({ href, children }: { href?: Route; children: JSX.Element }) => {
if (href) {
return (
<NextLink href={ href } passHref legacyBehavior>
{ children }
</NextLink>
);
}
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 bgColor = useColorModeValue('blue.50', 'whiteAlpha.100');
const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const hintColor = useColorModeValue('gray.600', 'gray.400'); const hintColor = useColorModeValue('gray.600', 'gray.400');
return ( return (
<Container href={ !isLoading ? href : undefined }>
<Flex <Flex
alignItems="flex-start" alignItems="flex-start"
bgColor={ isLoading ? skeletonBgColor : bgColor } bgColor={ isLoading ? skeletonBgColor : bgColor }
...@@ -27,8 +48,12 @@ const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h', ...@@ -27,8 +48,12 @@ const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h',
borderRadius="md" borderRadius="md"
justifyContent="space-between" justifyContent="space-between"
columnGap={ 3 } columnGap={ 3 }
{ ...(href && !isLoading ? {
as: 'a',
href,
} : {}) }
> >
<Box> <Box w="100%">
<Skeleton <Skeleton
isLoaded={ !isLoading } isLoaded={ !isLoading }
color="text_secondary" color="text_secondary"
...@@ -39,12 +64,13 @@ const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h', ...@@ -39,12 +64,13 @@ const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h',
</Skeleton> </Skeleton>
<Skeleton <Skeleton
isLoaded={ !isLoading } isLoaded={ !isLoading }
w="fit-content"
display="flex" display="flex"
alignItems="baseline" alignItems="baseline"
mt={ 1 } mt={ 1 }
> >
<Text fontWeight={ 500 } fontSize="lg" lineHeight={ 6 }>{ value }</Text> { 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> }
{ diff && Number(diff) > 0 && ( { diff && Number(diff) > 0 && (
<> <>
<Text fontWeight={ 500 } ml={ 2 } mr={ 1 } fontSize="lg" lineHeight={ 6 } color="green.500"> <Text fontWeight={ 500 } ml={ 2 } mr={ 1 } fontSize="lg" lineHeight={ 6 } color="green.500">
...@@ -53,6 +79,7 @@ const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h', ...@@ -53,6 +79,7 @@ const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h',
<Text variant="secondary" fontSize="sm">({ diffPeriod })</Text> <Text variant="secondary" fontSize="sm">({ diffPeriod })</Text>
</> </>
) } ) }
{ period && <Text variant="secondary" fontSize="xs" ml={ 1 }>({ period })</Text> }
</Skeleton> </Skeleton>
</Box> </Box>
{ hint && ( { hint && (
...@@ -61,6 +88,7 @@ const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h', ...@@ -61,6 +88,7 @@ const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h',
</Skeleton> </Skeleton>
) } ) }
</Flex> </Flex>
</Container>
); );
}; };
......
...@@ -61,7 +61,7 @@ const NavLink = ({ item, isCollapsed, px, className, onClick }: Props) => { ...@@ -61,7 +61,7 @@ const NavLink = ({ item, isCollapsed, px, className, onClick }: Props) => {
> >
<HStack spacing={ 3 } overflow="hidden"> <HStack spacing={ 3 } overflow="hidden">
<NavLinkIcon item={ item }/> <NavLinkIcon item={ item }/>
<Text { ...styleProps.textProps }> <Text { ...styleProps.textProps } as="span">
<span>{ item.text }</span> <span>{ item.text }</span>
{ !isInternalLink && <IconSvg name="arrows/north-east" boxSize={ 4 } color="text_secondary" verticalAlign="middle"/> } { !isInternalLink && <IconSvg name="arrows/north-east" boxSize={ 4 } color="text_secondary" verticalAlign="middle"/> }
</Text> </Text>
......
...@@ -11,6 +11,7 @@ import TopBar from './TopBar'; ...@@ -11,6 +11,7 @@ import TopBar from './TopBar';
const test = base.extend({ const test = base.extend({
context: contextWithEnvs([ context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_SWAP_BUTTON_URL', value: 'uniswap' }, { name: 'NEXT_PUBLIC_SWAP_BUTTON_URL', value: 'uniswap' },
{ name: 'NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL', value: 'DUCK' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any, ]) as any,
}); });
...@@ -28,8 +29,24 @@ test('default view +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -28,8 +29,24 @@ test('default view +@dark-mode +@mobile', async({ mount, page }) => {
); );
await component.getByText(/\$1\.39/).click(); await component.getByText(/\$1\.39/).click();
await expect(page.getByText(/last update/i)).toBeVisible();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } });
await component.getByLabel('User settings').click(); await component.getByLabel('User settings').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 400 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 400 } });
}); });
test('with secondary coin price +@mobile', async({ mount, page }) => {
await page.route(buildApiUrl('stats'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.withSecondaryCoin),
}));
const component = await mount(
<TestApp>
<TopBar/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
...@@ -4,12 +4,15 @@ import React from 'react'; ...@@ -4,12 +4,15 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile';
import { HOMEPAGE_STATS } from 'stubs/stats'; import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip'; import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice'; import GasPrice from 'ui/shared/gas/GasPrice';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
const TopBarStats = () => { const TopBarStats = () => {
const isMobile = useIsMobile();
const { data, isPlaceholderData, isError, refetch, dataUpdatedAt } = useApiQuery('stats', { const { data, isPlaceholderData, isError, refetch, dataUpdatedAt } = useApiQuery('stats', {
queryOptions: { queryOptions: {
placeholderData: HOMEPAGE_STATS, placeholderData: HOMEPAGE_STATS,
...@@ -51,7 +54,7 @@ const TopBarStats = () => { ...@@ -51,7 +54,7 @@ const TopBarStats = () => {
{ data?.coin_price && ( { data?.coin_price && (
<Flex columnGap={ 1 }> <Flex columnGap={ 1 }>
<Skeleton isLoaded={ !isPlaceholderData }> <Skeleton isLoaded={ !isPlaceholderData }>
<chakra.span color="text_secondary">{ config.chain.governanceToken.symbol || config.chain.currency.symbol } </chakra.span> <chakra.span color="text_secondary">{ config.chain.currency.symbol } </chakra.span>
<span>${ Number(data.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span> <span>${ Number(data.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span>
</Skeleton> </Skeleton>
{ data.coin_price_change_percentage && ( { data.coin_price_change_percentage && (
...@@ -63,6 +66,14 @@ const TopBarStats = () => { ...@@ -63,6 +66,14 @@ const TopBarStats = () => {
) } ) }
</Flex> </Flex>
) } ) }
{ !isMobile && data?.secondary_coin_price && config.chain.secondaryCoin.symbol && (
<Flex columnGap={ 1 } ml={ data?.coin_price ? 3 : 0 }>
<Skeleton isLoaded={ !isPlaceholderData }>
<chakra.span color="text_secondary">{ config.chain.secondaryCoin.symbol } </chakra.span>
<span>${ Number(data.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span>
</Skeleton>
</Flex>
) }
{ data?.coin_price && config.features.gasTracker.isEnabled && <TextSeparator color="divider"/> } { data?.coin_price && config.features.gasTracker.isEnabled && <TextSeparator color="divider"/> }
{ data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled && ( { data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled && (
<Skeleton isLoaded={ !isPlaceholderData }> <Skeleton isLoaded={ !isPlaceholderData }>
......
...@@ -15,16 +15,26 @@ import ChartWidgetContainer from './ChartWidgetContainer'; ...@@ -15,16 +15,26 @@ import ChartWidgetContainer from './ChartWidgetContainer';
type Props = { type Props = {
filterQuery: string; filterQuery: string;
initialFilterQuery: string;
isError: boolean; isError: boolean;
isPlaceholderData: boolean; isPlaceholderData: boolean;
charts?: Array<StatsChartsSection>; charts?: Array<StatsChartsSection>;
interval: StatsIntervalIds; interval: StatsIntervalIds;
} }
const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, interval }: Props) => { const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, interval, initialFilterQuery }: Props) => {
const [ isSomeChartLoadingError, setIsSomeChartLoadingError ] = useState(false); const [ isSomeChartLoadingError, setIsSomeChartLoadingError ] = useState(false);
const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0); const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0);
const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed; const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed;
const sectionRef = React.useRef<HTMLUListElement | null>(null);
const shouldScrollToSection = Boolean(initialFilterQuery);
React.useEffect(() => {
if (shouldScrollToSection) {
sectionRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [ shouldScrollToSection ]);
const homeStatsQuery = useApiQuery('stats', { const homeStatsQuery = useApiQuery('stats', {
queryOptions: { queryOptions: {
...@@ -50,7 +60,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in ...@@ -50,7 +60,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
<ChartsLoadingErrorAlert/> <ChartsLoadingErrorAlert/>
) } ) }
<List> <List ref={ sectionRef }>
{ {
charts?.map((section) => ( charts?.map((section) => (
<ListItem <ListItem
...@@ -61,7 +71,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in ...@@ -61,7 +71,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
}} }}
> >
<Skeleton isLoaded={ !isPlaceholderData } mb={ 4 } display="inline-flex" alignItems="center" columnGap={ 2 } id={ section.id }> <Skeleton isLoaded={ !isPlaceholderData } mb={ 4 } display="inline-flex" alignItems="center" columnGap={ 2 } id={ section.id }>
<Heading size="md" > <Heading size="md" id={ section.id }>
{ section.title } { section.title }
</Heading> </Heading>
{ section.id === 'gas' && homeStatsQuery.data && homeStatsQuery.data.gas_prices && ( { section.id === 'gas' && homeStatsQuery.data && homeStatsQuery.data.gas_prices && (
......
import { Grid, GridItem } from '@chakra-ui/react'; import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { StatsChartsSection } from 'types/api/stats'; import type { StatsChartsSection } from 'types/api/stats';
...@@ -21,6 +21,8 @@ type Props = { ...@@ -21,6 +21,8 @@ type Props = {
interval: StatsIntervalIds; interval: StatsIntervalIds;
onIntervalChange: (newInterval: StatsIntervalIds) => void; onIntervalChange: (newInterval: StatsIntervalIds) => void;
onFilterInputChange: (q: string) => void; onFilterInputChange: (q: string) => void;
isLoading: boolean;
initialFilterValue: string;
} }
const StatsFilters = ({ const StatsFilters = ({
...@@ -30,8 +32,9 @@ const StatsFilters = ({ ...@@ -30,8 +32,9 @@ const StatsFilters = ({
interval, interval,
onIntervalChange, onIntervalChange,
onFilterInputChange, onFilterInputChange,
isLoading,
initialFilterValue,
}: Props) => { }: Props) => {
const sectionsList = [ { const sectionsList = [ {
id: 'all', id: 'all',
title: 'All', title: 'All',
...@@ -51,22 +54,26 @@ const StatsFilters = ({ ...@@ -51,22 +54,26 @@ const StatsFilters = ({
w={{ base: '100%', lg: 'auto' }} w={{ base: '100%', lg: 'auto' }}
area="section" area="section"
> >
{ isLoading ? <Skeleton w={{ base: '100%', lg: '76px' }} h="40px" borderRadius="base"/> : (
<StatsDropdownMenu <StatsDropdownMenu
items={ sectionsList } items={ sectionsList }
selectedId={ currentSection } selectedId={ currentSection }
onSelect={ onSectionChange } onSelect={ onSectionChange }
/> />
) }
</GridItem> </GridItem>
<GridItem <GridItem
w={{ base: '100%', lg: 'auto' }} w={{ base: '100%', lg: 'auto' }}
area="interval" area="interval"
> >
{ isLoading ? <Skeleton w={{ base: '100%', lg: '118px' }} h="40px" borderRadius="base"/> : (
<StatsDropdownMenu <StatsDropdownMenu
items={ intervalList } items={ intervalList }
selectedId={ interval } selectedId={ interval }
onSelect={ onIntervalChange } onSelect={ onIntervalChange }
/> />
) }
</GridItem> </GridItem>
<GridItem <GridItem
...@@ -74,8 +81,12 @@ const StatsFilters = ({ ...@@ -74,8 +81,12 @@ const StatsFilters = ({
area="input" area="input"
> >
<FilterInput <FilterInput
key={ initialFilterValue }
isLoading={ isLoading }
onChange={ onFilterInputChange } onChange={ onFilterInputChange }
placeholder="Find chart, metric..."/> placeholder="Find chart, metric..."
initialValue={ initialFilterValue }
/>
</GridItem> </GridItem>
</Grid> </Grid>
); );
......
import { useRouter } from 'next/router';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import type { StatsChartInfo, StatsChartsSection } from 'types/api/stats'; import type { StatsChartInfo, StatsChartsSection } from 'types/api/stats';
import type { StatsIntervalIds } from 'types/client/stats'; import type { StatsIntervalIds } from 'types/client/stats';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce'; import getQueryParamString from 'lib/router/getQueryParamString';
import { STATS_CHARTS } from 'stubs/stats'; import { STATS_CHARTS } from 'stubs/stats';
function isSectionMatches(section: StatsChartsSection, currentSection: string): boolean { function isSectionMatches(section: StatsChartsSection, currentSection: string): boolean {
...@@ -16,6 +17,8 @@ function isChartNameMatches(q: string, chart: StatsChartInfo) { ...@@ -16,6 +17,8 @@ function isChartNameMatches(q: string, chart: StatsChartInfo) {
} }
export default function useStats() { export default function useStats() {
const router = useRouter();
const { data, isPlaceholderData, isError } = useApiQuery('stats_lines', { const { data, isPlaceholderData, isError } = useApiQuery('stats_lines', {
queryOptions: { queryOptions: {
placeholderData: STATS_CHARTS, placeholderData: STATS_CHARTS,
...@@ -24,22 +27,35 @@ export default function useStats() { ...@@ -24,22 +27,35 @@ export default function useStats() {
const [ currentSection, setCurrentSection ] = useState('all'); const [ currentSection, setCurrentSection ] = useState('all');
const [ filterQuery, setFilterQuery ] = useState(''); const [ filterQuery, setFilterQuery ] = useState('');
const [ initialFilterQuery, setInitialFilterQuery ] = React.useState('');
const [ interval, setInterval ] = useState<StatsIntervalIds>('oneMonth'); const [ interval, setInterval ] = useState<StatsIntervalIds>('oneMonth');
const sectionIds = useMemo(() => data?.sections?.map(({ id }) => id), [ data ]); const sectionIds = useMemo(() => data?.sections?.map(({ id }) => id), [ data ]);
const debouncedFilterQuery = useDebounce(filterQuery, 500); React.useEffect(() => {
if (!isPlaceholderData && !isError) {
const chartId = getQueryParamString(router.query.chartId);
const chartName = data?.sections.map((section) => section.charts.find((chart) => chart.id === chartId)).filter(Boolean)[0]?.title;
if (chartName) {
setInitialFilterQuery(chartName);
setFilterQuery(chartName);
router.replace({ pathname: '/stats' }, undefined, { scroll: false });
}
}
// run only when data is loaded
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isPlaceholderData ]);
const displayedCharts = React.useMemo(() => { const displayedCharts = React.useMemo(() => {
return data?.sections return data?.sections
?.map((section) => { ?.map((section) => {
const charts = section.charts.filter((chart) => isSectionMatches(section, currentSection) && isChartNameMatches(debouncedFilterQuery, chart)); const charts = section.charts.filter((chart) => isSectionMatches(section, currentSection) && isChartNameMatches(filterQuery, chart));
return { return {
...section, ...section,
charts, charts,
}; };
}).filter((section) => section.charts.length > 0); }).filter((section) => section.charts.length > 0);
}, [ currentSection, data?.sections, debouncedFilterQuery ]); }, [ currentSection, data?.sections, filterQuery ]);
const handleSectionChange = useCallback((newSection: string) => { const handleSectionChange = useCallback((newSection: string) => {
setCurrentSection(newSection); setCurrentSection(newSection);
...@@ -58,6 +74,7 @@ export default function useStats() { ...@@ -58,6 +74,7 @@ export default function useStats() {
sectionIds, sectionIds,
isPlaceholderData, isPlaceholderData,
isError, isError,
initialFilterQuery,
filterQuery, filterQuery,
currentSection, currentSection,
handleSectionChange, handleSectionChange,
...@@ -70,6 +87,7 @@ export default function useStats() { ...@@ -70,6 +87,7 @@ export default function useStats() {
sectionIds, sectionIds,
isPlaceholderData, isPlaceholderData,
isError, isError,
initialFilterQuery,
filterQuery, filterQuery,
currentSection, currentSection,
handleSectionChange, handleSectionChange,
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as statsMock from 'mocks/stats';
import * as txsStatsMock from 'mocks/txs/stats';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxsStats from './TxsStats';
const TXS_STATS_API_URL = buildApiUrl('txs_stats');
const STATS_API_URL = buildApiUrl('stats');
test('base view +@mobile', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(TXS_STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txsStatsMock.base),
}));
const component = await mount(
<TestApp>
<TxsStats/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue';
import { thinsp } from 'lib/html-entities';
import { HOMEPAGE_STATS } from 'stubs/stats';
import { TXS_STATS } from 'stubs/tx';
import StatsWidget from 'ui/shared/stats/StatsWidget';
const TxsStats = () => {
const txsStatsQuery = useApiQuery('txs_stats', {
queryOptions: {
placeholderData: TXS_STATS,
},
});
const statsQuery = useApiQuery('stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
});
if (!txsStatsQuery.data) {
return null;
}
const txFeeAvg = getCurrencyValue({
value: txsStatsQuery.data.transaction_fees_avg_24h,
exchangeRate: statsQuery.data?.coin_price,
decimals: String(config.chain.currency.decimals),
accuracyUsd: 2,
});
return (
<Box
display="grid"
gridTemplateColumns={{ base: '1fr', lg: 'repeat(4, calc(25% - 9px))' }}
rowGap={ 3 }
columnGap={ 3 }
mb={ 6 }
>
<StatsWidget
label="Transactions"
value={ Number(txsStatsQuery.data?.transactions_count_24h).toLocaleString() }
period="24h"
isLoading={ txsStatsQuery.isPlaceholderData }
href={{ pathname: '/stats', query: { chartId: 'newTxns' } }}
/>
<StatsWidget
label="Pending transactions"
value={ Number(txsStatsQuery.data?.pending_transactions_count).toLocaleString() }
period="1h"
isLoading={ txsStatsQuery.isPlaceholderData }
/>
<StatsWidget
label="Transactions fees"
value={
(Number(txsStatsQuery.data?.transaction_fees_sum_24h) / (10 ** config.chain.currency.decimals))
.toLocaleString(undefined, { maximumFractionDigits: 2 })
}
valuePostfix={ thinsp + config.chain.currency.symbol }
period="24h"
isLoading={ txsStatsQuery.isPlaceholderData }
href={{ pathname: '/stats', query: { chartId: 'txnsFee' } }}
/>
<StatsWidget
label="Avg. transaction fee"
value={ txFeeAvg.usd ? txFeeAvg.usd : txFeeAvg.valueStr }
valuePrefix={ txFeeAvg.usd ? '$' : undefined }
valuePostfix={ txFeeAvg.usd ? undefined : thinsp + config.chain.currency.symbol }
period="24h"
isLoading={ txsStatsQuery.isPlaceholderData }
href={{ pathname: '/stats', query: { chartId: 'averageTxnFee' } }}
/>
</Box>
);
};
export default React.memo(TxsStats);
...@@ -24,6 +24,7 @@ const VerifiedContractsCounters = () => { ...@@ -24,6 +24,7 @@ const VerifiedContractsCounters = () => {
diff={ countersQuery.data.new_smart_contracts_24h } diff={ countersQuery.data.new_smart_contracts_24h }
diffFormatted={ Number(countersQuery.data.new_smart_contracts_24h).toLocaleString() } diffFormatted={ Number(countersQuery.data.new_smart_contracts_24h).toLocaleString() }
isLoading={ countersQuery.isPlaceholderData } isLoading={ countersQuery.isPlaceholderData }
href={{ pathname: '/stats', query: { chartId: 'contractsGrowth' } }}
/> />
<StatsWidget <StatsWidget
label="Verified contracts" label="Verified contracts"
...@@ -31,6 +32,7 @@ const VerifiedContractsCounters = () => { ...@@ -31,6 +32,7 @@ const VerifiedContractsCounters = () => {
diff={ countersQuery.data.new_verified_smart_contracts_24h } diff={ countersQuery.data.new_verified_smart_contracts_24h }
diffFormatted={ Number(countersQuery.data.new_verified_smart_contracts_24h).toLocaleString() } diffFormatted={ Number(countersQuery.data.new_verified_smart_contracts_24h).toLocaleString() }
isLoading={ countersQuery.isPlaceholderData } isLoading={ countersQuery.isPlaceholderData }
href={{ pathname: '/stats', query: { chartId: 'verifiedContractsGrowth' } }}
/> />
</Box> </Box>
); );
......
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