From c16a4e3175975ba3b4a6492730c5f146922e98ab Mon Sep 17 00:00:00 2001 From: isstuev Date: Thu, 30 Jan 2025 17:43:50 +0100 Subject: [PATCH 1/7] Unify stats across relevant pages --- configs/envs/.env.eth_sepolia | 2 +- deploy/tools/envs-validator/schema.ts | 28 ++- docs/ENVS.md | 4 +- lib/api/resources.ts | 18 ++ package.json | 2 +- stubs/contract.ts | 9 + stubs/stats.ts | 34 ++- stubs/tx.ts | 9 + types/homepage.ts | 3 +- ui/home/Stats.tsx | 68 ++++-- .../ChainIndicatorChartContainer.tsx | 43 ++-- ...art.tsx => ChainIndicatorChartContent.tsx} | 4 +- ui/home/indicators/ChainIndicatorItem.tsx | 32 ++- ui/home/indicators/ChainIndicators.tsx | 68 ++++-- ui/home/indicators/types.ts | 13 +- ui/home/indicators/useChartDataQuery.tsx | 172 +++++++++++++++ ui/home/indicators/useFetchChartData.tsx | 21 -- .../indicators/utils/getIndicatorValues.ts | 28 +++ ui/home/indicators/utils/indicators.tsx | 201 +++++------------- ui/home/indicators/utils/prepareChartItems.ts | 20 ++ ui/shared/stats/StatsWidget.tsx | 2 +- ui/txs/TxsStats.tsx | 109 ++++++---- .../VerifiedContractsCounters.tsx | 44 +++- yarn.lock | 8 +- 24 files changed, 608 insertions(+), 334 deletions(-) rename ui/home/indicators/{ChainIndicatorChart.tsx => ChainIndicatorChartContent.tsx} (94%) create mode 100644 ui/home/indicators/useChartDataQuery.tsx delete mode 100644 ui/home/indicators/useFetchChartData.tsx create mode 100644 ui/home/indicators/utils/getIndicatorValues.ts create mode 100644 ui/home/indicators/utils/prepareChartItems.ts diff --git a/configs/envs/.env.eth_sepolia b/configs/envs/.env.eth_sepolia index 6de7b51a9a..5809f0719a 100644 --- a/configs/envs/.env.eth_sepolia +++ b/configs/envs/.env.eth_sepolia @@ -64,7 +64,7 @@ NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-c NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}] NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://points.k8s-dev.blockscout.com NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global -NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s.blockscout.com +NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index bed33149c7..3d1f697ade 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -635,12 +635,34 @@ const schema = yup .array() .transform(replaceQuotes) .json() - .of(yup.string().oneOf(CHAIN_INDICATOR_IDS)), + .of(yup.string().oneOf(CHAIN_INDICATOR_IDS)) + .test( + 'stats-api-required', + 'NEXT_PUBLIC_STATS_API_HOST is required when daily_operational_txs is enabled in NEXT_PUBLIC_HOMEPAGE_CHARTS', + function(value) { + // daily_operational_txs is presented only in stats microservice + if (value?.includes('daily_operational_txs')) { + return Boolean(this.parent.NEXT_PUBLIC_STATS_API_HOST); + } + return true; + } + ), NEXT_PUBLIC_HOMEPAGE_STATS: yup .array() .transform(replaceQuotes) .json() - .of(yup.string().oneOf(HOME_STATS_WIDGET_IDS)), + .of(yup.string().oneOf(HOME_STATS_WIDGET_IDS)) + .test( + 'stats-api-required', + 'NEXT_PUBLIC_STATS_API_HOST is required when total_operational_txs is enabled in NEXT_PUBLIC_HOMEPAGE_STATS', + function(value) { + // total_operational_txs is presented only in stats microservice + if (value?.includes('total_operational_txs')) { + return Boolean(this.parent.NEXT_PUBLIC_STATS_API_HOST); + } + return true; + } + ), NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(), NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(), NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG: yup @@ -750,7 +772,7 @@ const schema = yup .transform(replaceQuotes) .json() .of(yup.string().oneOf(TX_ADDITIONAL_FIELDS_IDS)), - NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: yup + NEXT_PUBLIC_VIEWS_NFT_MARKPLACES: yup .array() .transform(replaceQuotes) .json() diff --git a/docs/ENVS.md b/docs/ENVS.md index a2272bbf2a..90f44f304a 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -126,8 +126,8 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will | Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | --- | --- | --- | --- | --- | --- | --- | -| 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']` | v1.0.x+ | -| NEXT_PUBLIC_HOMEPAGE_STATS | `Array<'latest_batch' \| 'total_blocks' \| 'average_block_time' \| 'total_txs' \| 'latest_l1_state_batch' \| 'wallet_addresses' \| 'gas_tracker' \| 'btc_locked' \| 'current_epoch'>` | List of stats widgets displayed on the home page | - | For zkSync, zkEvm and Arbitrum rollups: `['latest_batch','average_block_time','total_txs','wallet_addresses','gas_tracker']`, for other cases: `['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker']` | `['total_blocks','total_txs','wallet_addresses']` | v1.35.x+ | +| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'daily_operational_txs' \| 'coin_price' \| 'secondary_coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | v1.0.x+ | +| NEXT_PUBLIC_HOMEPAGE_STATS | `Array<'latest_batch' \| 'total_blocks' \| 'average_block_time' \| 'total_txs' \| 'total_operational_txs' \| 'latest_l1_state_batch' \| 'wallet_addresses' \| 'gas_tracker' \| 'btc_locked' \| 'current_epoch'>` | List of stats widgets displayed on the home page | - | For zkSync, zkEvm and Arbitrum rollups: `['latest_batch','average_block_time','total_txs','wallet_addresses','gas_tracker']`, for other cases: `['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker']` | `['total_blocks','total_txs','wallet_addresses']` | v1.35.x+ | | 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). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` instead_ | - | `white` | `\#DCFE76` | v1.0.x+ | | 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). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` 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)` | v1.1.0+ | | NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG | `HeroBannerConfig`, see details [below](#hero-banner-configuration-properties) | Configuration of hero banner appearance. | - | - | See [below](#hero-banner-configuration-properties) | v1.35.0+ | diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 2c9f4ca1ef..cb208ecef4 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -286,6 +286,21 @@ export const RESOURCES = { endpoint: getFeaturePayload(config.features.stats)?.api.endpoint, basePath: getFeaturePayload(config.features.stats)?.api.basePath, }, + stats_main: { + path: '/api/v1/pages/main', + endpoint: getFeaturePayload(config.features.stats)?.api.endpoint, + basePath: getFeaturePayload(config.features.stats)?.api.basePath, + }, + stats_transactions: { + path: '/api/v1/pages/transactions', + endpoint: getFeaturePayload(config.features.stats)?.api.endpoint, + basePath: getFeaturePayload(config.features.stats)?.api.basePath, + }, + stats_contracts: { + path: '/api/v1/pages/contracts', + endpoint: getFeaturePayload(config.features.stats)?.api.endpoint, + basePath: getFeaturePayload(config.features.stats)?.api.basePath, + }, // NAME SERVICE addresses_lookup: { @@ -1275,6 +1290,9 @@ Q extends 'homepage_arbitrum_latest_batch' ? number : Q extends 'stats_counters' ? stats.Counters : Q extends 'stats_lines' ? stats.LineCharts : Q extends 'stats_line' ? stats.LineChart : +Q extends 'stats_main' ? stats.MainPageStats : +Q extends 'stats_transactions' ? stats.TransactionsPageStats : +Q extends 'stats_contracts' ? stats.ContractsPageStats : Q extends 'blocks' ? BlocksResponse : Q extends 'block' ? Block : Q extends 'block_countdown' ? BlockCountdownResponse : diff --git a/package.json b/package.json index d9212fb6ed..53be65eb1c 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@blockscout/bens-types": "1.4.1", - "@blockscout/stats-types": "2.0.0", + "@blockscout/stats-types": "2.4.0", "@blockscout/visualizer-types": "0.2.0", "@chakra-ui/react": "2.7.1", "@chakra-ui/theme-tools": "^2.0.18", diff --git a/stubs/contract.ts b/stubs/contract.ts index cb9111cfdb..2b73224252 100644 --- a/stubs/contract.ts +++ b/stubs/contract.ts @@ -1,9 +1,11 @@ +import type * as stats from '@blockscout/stats-types'; import type { SmartContract, SmartContractMudSystemsResponse } from 'types/api/contract'; import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts'; import type { SolidityScanReport } from 'lib/solidityScan/schema'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; +import { STATS_COUNTER } from './stats'; export const CONTRACT_CODE_UNVERIFIED = { creation_bytecode: '0x60806040526e', @@ -81,6 +83,13 @@ export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = { new_verified_smart_contracts_24h: '1234', }; +export const VERIFIED_CONTRACTS_COUNTERS_MICROSERVICE: stats.ContractsPageStats = { + total_contracts: STATS_COUNTER, + new_contracts_24h: STATS_COUNTER, + total_verified_contracts: STATS_COUNTER, + new_verified_contracts_24h: STATS_COUNTER, +}; + export const SOLIDITY_SCAN_REPORT: SolidityScanReport = { scan_report: { contractname: 'BullRunners', diff --git a/stubs/stats.ts b/stubs/stats.ts index 3e7be4b565..976debc279 100644 --- a/stubs/stats.ts +++ b/stubs/stats.ts @@ -42,17 +42,19 @@ export const HOMEPAGE_STATS: HomeStats = { tvl: '1767425.102766552', }; +const STATS_CHART_INFO: stats.LineChartInfo = { + id: 'chart_0', + title: 'Average transaction fee', + description: 'The average amount in ETH spent per transaction', + units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], +}; + export const STATS_CHARTS_SECTION: stats.LineChartSection = { id: 'placeholder', title: 'Placeholder', charts: [ - { - id: 'chart_0', - title: 'Average transaction fee', - description: 'The average amount in ETH spent per transaction', - units: 'ETH', - resolutions: [ 'DAY', 'MONTH' ], - }, + STATS_CHART_INFO, { id: 'chart_1', title: 'Transactions fees', @@ -88,3 +90,21 @@ export const STATS_COUNTER: stats.Counter = { description: 'Placeholder description', units: '', }; + +export const HOMEPAGE_STATS_MICROSERVICE: stats.MainPageStats = { + average_block_time: STATS_COUNTER, + total_addresses: STATS_COUNTER, + total_blocks: STATS_COUNTER, + total_transactions: STATS_COUNTER, + yesterday_transactions: STATS_COUNTER, + total_operational_transactions: STATS_COUNTER, + yesterday_operational_transactions: STATS_COUNTER, + daily_new_transactions: { + chart: [], + info: STATS_CHART_INFO, + }, + daily_new_operational_transactions: { + chart: [], + info: STATS_CHART_INFO, + }, +}; diff --git a/stubs/tx.ts b/stubs/tx.ts index 211e73a026..343d7a8003 100644 --- a/stubs/tx.ts +++ b/stubs/tx.ts @@ -1,7 +1,9 @@ +import type * as stats from '@blockscout/stats-types'; import type { RawTracesResponse } from 'types/api/rawTrace'; import type { Transaction, TransactionsStats } from 'types/api/transaction'; import { ADDRESS_PARAMS } from './addressParams'; +import { STATS_COUNTER } from './stats'; export const TX_HASH = '0x3ed9d81e7c1001bdda1caa1dc62c0acbbe3d2c671cdc20dc1e65efdaa4186967'; @@ -66,3 +68,10 @@ export const TXS_STATS: TransactionsStats = { transaction_fees_sum_24h: '22184012506492688277', transactions_count_24h: '992890', }; + +export const TXS_STATS_MICROSERVICE: stats.TransactionsPageStats = { + pending_transactions_30m: STATS_COUNTER, + transactions_24h: STATS_COUNTER, + transactions_fee_24h: STATS_COUNTER, + average_transactions_fee_24h: STATS_COUNTER, +}; diff --git a/types/homepage.ts b/types/homepage.ts index d8d06647c9..4213625dd9 100644 --- a/types/homepage.ts +++ b/types/homepage.ts @@ -1,4 +1,4 @@ -export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const; +export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'daily_operational_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const; export type ChainIndicatorId = typeof CHAIN_INDICATOR_IDS[number]; export const HOME_STATS_WIDGET_IDS = [ @@ -6,6 +6,7 @@ export const HOME_STATS_WIDGET_IDS = [ 'total_blocks', 'average_block_time', 'total_txs', + 'total_operational_txs', 'latest_l1_state_batch', 'wallet_addresses', 'gas_tracker', diff --git a/ui/home/Stats.tsx b/ui/home/Stats.tsx index ab00e80cf1..fa15b2f0ab 100644 --- a/ui/home/Stats.tsx +++ b/ui/home/Stats.tsx @@ -7,7 +7,7 @@ import type { HomeStatsWidgetId } from 'types/homepage'; import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; import { WEI } from 'lib/consts'; -import { HOMEPAGE_STATS } from 'stubs/stats'; +import { HOMEPAGE_STATS, HOMEPAGE_STATS_MICROSERVICE } from 'stubs/stats'; import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip'; import GasPrice from 'ui/shared/gas/GasPrice'; import IconSvg from 'ui/shared/IconSvg'; @@ -15,18 +15,31 @@ import type { Props as StatsWidgetProps } from 'ui/shared/stats/StatsWidget'; import StatsWidget from 'ui/shared/stats/StatsWidget'; const rollupFeature = config.features.rollup; +const statsFeature = config.features.stats; const Stats = () => { const [ hasGasTracker, setHasGasTracker ] = React.useState(config.features.gasTracker.isEnabled); - const { data, isPlaceholderData, isError, dataUpdatedAt } = useApiQuery('stats', { + + // data from stats microservice is prioritized over data from stats api + const statsQuery = useApiQuery('stats_main', { + queryOptions: { + refetchOnMount: false, + placeholderData: statsFeature.isEnabled ? HOMEPAGE_STATS_MICROSERVICE : undefined, + enabled: statsFeature.isEnabled, + }, + }); + + const apiQuery = useApiQuery('stats', { queryOptions: { refetchOnMount: false, placeholderData: HOMEPAGE_STATS, }, }); + const isPlaceholderData = statsQuery.isPlaceholderData || apiQuery.isPlaceholderData; + React.useEffect(() => { - if (!isPlaceholderData && !data?.gas_prices?.average) { + if (!isPlaceholderData && !apiQuery.data?.gas_prices?.average) { setHasGasTracker(false); } // should run only after initial fetch @@ -69,7 +82,7 @@ const Stats = () => { } })(); - if (isError || latestBatchQuery?.isError) { + if (apiQuery.isError || statsQuery.isError || latestBatchQuery?.isError) { return null; } @@ -79,13 +92,16 @@ const Stats = () => { id: HomeStatsWidgetId; } + const apiData = apiQuery.data; + const statsData = statsQuery.data; + const items: Array = (() => { - if (!data) { + if (!statsData && !apiData) { return []; } - const gasInfoTooltip = hasGasTracker && data.gas_prices && data.gas_prices.average ? ( - + const gasInfoTooltip = hasGasTracker && apiData?.gas_prices && apiData.gas_prices.average ? ( + { id: 'total_blocks' as const, icon: 'block_slim' as const, label: 'Total blocks', - value: Number(data.total_blocks).toLocaleString(), + value: Number(statsData?.total_blocks?.value || apiData?.total_blocks).toLocaleString(), href: { pathname: '/blocks' as const }, isLoading, }, - { + (statsData?.average_block_time?.value || apiData?.average_block_time) && { id: 'average_block_time' as const, icon: 'clock-light' as const, label: 'Average block time', - value: `${ (data.average_block_time / 1000).toFixed(1) }s`, + value: `${ Number(statsData?.average_block_time?.value).toFixed(1) || (apiData ? (apiData.average_block_time / 1000).toFixed(1) : 'N/A') }s`, isLoading, }, - { + (statsData?.total_transactions?.value || apiData?.total_transactions) && { id: 'total_txs' as const, icon: 'transactions_slim' as const, label: 'Total transactions', - value: Number(data.total_transactions).toLocaleString(), + value: Number(statsData?.total_transactions?.value || apiData?.total_transactions).toLocaleString(), href: { pathname: '/txs' as const }, isLoading, }, - data.last_output_root_size && { + statsData?.total_operational_transactions?.value && { + id: 'total_operational_txs' as const, + icon: 'transactions_slim' as const, + label: 'Total operational transactions', + value: Number(statsData?.total_operational_transactions?.value).toLocaleString(), + href: { pathname: '/txs' as const }, + isLoading, + }, + apiData?.last_output_root_size && { id: 'latest_l1_state_batch' as const, icon: 'txn_batches_slim' as const, label: 'Latest L1 state batch', - value: data.last_output_root_size, + value: apiData?.last_output_root_size, href: { pathname: '/batches' as const }, isLoading, }, - { + (statsData?.total_addresses?.value || apiData?.total_addresses) && { id: 'wallet_addresses' as const, icon: 'wallet' as const, label: 'Wallet addresses', - value: Number(data.total_addresses).toLocaleString(), + value: Number(statsData?.total_addresses?.value || apiData?.total_addresses).toLocaleString(), isLoading, }, - hasGasTracker && data.gas_prices && { + hasGasTracker && apiData?.gas_prices && { id: 'gas_tracker' as const, icon: 'gas' as const, label: 'Gas tracker', - value: data.gas_prices.average ? : 'N/A', + value: apiData.gas_prices.average ? : 'N/A', hint: gasInfoTooltip, isLoading, }, - data.rootstock_locked_btc && { + apiData?.rootstock_locked_btc && { id: 'btc_locked' as const, icon: 'coins/bitcoin' as const, label: 'BTC Locked in 2WP', - value: `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC`, + value: `${ BigNumber(apiData.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC`, isLoading, }, - data.celo && { + apiData?.celo && { id: 'current_epoch' as const, icon: 'hourglass' as const, label: 'Current epoch', - value: `#${ data.celo.epoch_number }`, + value: `#${ apiData.celo.epoch_number }`, isLoading, }, ] diff --git a/ui/home/indicators/ChainIndicatorChartContainer.tsx b/ui/home/indicators/ChainIndicatorChartContainer.tsx index a8bc2d2192..a435aa16f0 100644 --- a/ui/home/indicators/ChainIndicatorChartContainer.tsx +++ b/ui/home/indicators/ChainIndicatorChartContainer.tsx @@ -1,5 +1,4 @@ -import { chakra, Flex, Box } from '@chakra-ui/react'; -import type { UseQueryResult } from '@tanstack/react-query'; +import { chakra, Box } from '@chakra-ui/react'; import React from 'react'; import type { TimeChartData } from 'ui/shared/chart/types'; @@ -7,33 +6,33 @@ import type { TimeChartData } from 'ui/shared/chart/types'; import ContentLoader from 'ui/shared/ContentLoader'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; -import ChainIndicatorChart from './ChainIndicatorChart'; +import ChainIndicatorChartContent from './ChainIndicatorChartContent'; -type Props = UseQueryResult; +type Props = { + data: TimeChartData; + isError: boolean; + isPending: boolean; +}; const ChainIndicatorChartContainer = ({ data, isError, isPending }: Props) => { - const content = (() => { - if (isPending) { - return ; - } - - if (isError) { - return ; - } + if (isPending) { + return ; + } - if (data[0].items.length === 0) { - return no data; - } + if (isError) { + return ; + } - return ( - - - - ); - })(); + if (data[0].items.length === 0) { + return no data; + } - return { content }; + return ( + + + + ); }; export default React.memo(ChainIndicatorChartContainer); diff --git a/ui/home/indicators/ChainIndicatorChart.tsx b/ui/home/indicators/ChainIndicatorChartContent.tsx similarity index 94% rename from ui/home/indicators/ChainIndicatorChart.tsx rename to ui/home/indicators/ChainIndicatorChartContent.tsx index daf0251317..a167099170 100644 --- a/ui/home/indicators/ChainIndicatorChart.tsx +++ b/ui/home/indicators/ChainIndicatorChartContent.tsx @@ -16,7 +16,7 @@ interface Props { const CHART_MARGIN = { bottom: 5, left: 10, right: 10, top: 5 }; -const ChainIndicatorChart = ({ data }: Props) => { +const ChainIndicatorChartContent = ({ data }: Props) => { const overlayRef = React.useRef(null); const lineColor = useToken('colors', 'blue.500'); @@ -64,4 +64,4 @@ const ChainIndicatorChart = ({ data }: Props) => { ); }; -export default React.memo(ChainIndicatorChart); +export default React.memo(ChainIndicatorChartContent); diff --git a/ui/home/indicators/ChainIndicatorItem.tsx b/ui/home/indicators/ChainIndicatorItem.tsx index 7b9606d924..13e0da03e9 100644 --- a/ui/home/indicators/ChainIndicatorItem.tsx +++ b/ui/home/indicators/ChainIndicatorItem.tsx @@ -1,25 +1,23 @@ import { Text, Flex, Box, useColorModeValue } from '@chakra-ui/react'; -import type { UseQueryResult } from '@tanstack/react-query'; import React from 'react'; -import type { HomeStats } from 'types/api/stats'; import type { ChainIndicatorId } from 'types/homepage'; -import type { ResourceError } from 'lib/api/resources'; import Skeleton from 'ui/shared/chakra/Skeleton'; interface Props { id: ChainIndicatorId; title: string; - value: (stats: HomeStats) => string; - valueDiff?: (stats?: HomeStats) => number | null | undefined; + value?: string; + valueDiff?: number | null | undefined; icon: React.ReactNode; isSelected: boolean; onClick: (id: ChainIndicatorId) => void; - stats: UseQueryResult>; + isLoading: boolean; + hasData: boolean; } -const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onClick, stats }: Props) => { +const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onClick, isLoading, hasData }: Props) => { const activeColor = useColorModeValue('gray.500', 'gray.400'); const activeBgColor = useColorModeValue('white', 'black'); @@ -28,32 +26,28 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC }, [ id, onClick ]); const valueContent = (() => { - if (!stats.data) { + if (!hasData) { return no data; } return ( - - { value(stats.data) } + + { value } ); })(); const valueDiffContent = (() => { - if (!valueDiff) { - return null; - } - const diff = valueDiff(stats.data); - if (diff === undefined || diff === null) { + if (valueDiff === undefined || valueDiff === null) { return null; } - const diffColor = diff >= 0 ? 'green.500' : 'red.500'; + const diffColor = valueDiff >= 0 ? 'green.500' : 'red.500'; return ( - - { diff >= 0 ? '+' : '-' } - { Math.abs(diff) }% + + { valueDiff >= 0 ? '+' : '-' } + { Math.abs(valueDiff) }% ); })(); diff --git a/ui/home/indicators/ChainIndicators.tsx b/ui/home/indicators/ChainIndicators.tsx index 698b053555..bced0945d2 100644 --- a/ui/home/indicators/ChainIndicators.tsx +++ b/ui/home/indicators/ChainIndicators.tsx @@ -1,18 +1,24 @@ import { Flex, Text, useColorModeValue } from '@chakra-ui/react'; import React from 'react'; +import type { TChainIndicator } from './types'; +import type { ChainIndicatorId } from 'types/homepage'; + import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; -import { HOMEPAGE_STATS } from 'stubs/stats'; +import { HOMEPAGE_STATS, HOMEPAGE_STATS_MICROSERVICE } from 'stubs/stats'; import Skeleton from 'ui/shared/chakra/Skeleton'; import Hint from 'ui/shared/Hint'; import IconSvg from 'ui/shared/IconSvg'; import ChainIndicatorChartContainer from './ChainIndicatorChartContainer'; import ChainIndicatorItem from './ChainIndicatorItem'; -import useFetchChartData from './useFetchChartData'; +import useChartDataQuery from './useChartDataQuery'; +import getIndicatorValues from './utils/getIndicatorValues'; import INDICATORS from './utils/indicators'; +const isStatsFeatureEnabled = config.features.stats.isEnabled; + const indicators = INDICATORS .filter(({ id }) => config.UI.homepage.charts.includes(id)) .sort((a, b) => { @@ -29,10 +35,19 @@ const indicators = INDICATORS const ChainIndicators = () => { const [ selectedIndicator, selectIndicator ] = React.useState(indicators[0]?.id); - const indicator = indicators.find(({ id }) => id === selectedIndicator); + const selectedIndicatorData = indicators.find(({ id }) => id === selectedIndicator); + + const queryResult = useChartDataQuery(selectedIndicatorData?.id as ChainIndicatorId); + + const statsMicroserviceQueryResult = useApiQuery('stats_main', { + queryOptions: { + refetchOnMount: false, + enabled: isStatsFeatureEnabled, + placeholderData: HOMEPAGE_STATS_MICROSERVICE, + }, + }); - const queryResult = useFetchChartData(indicator); - const statsQueryResult = useApiQuery('stats', { + const statsApiQueryResult = useApiQuery('stats', { queryOptions: { refetchOnMount: false, placeholderData: HOMEPAGE_STATS, @@ -45,38 +60,39 @@ const ChainIndicators = () => { return null; } + const isPlaceholderData = (isStatsFeatureEnabled && statsMicroserviceQueryResult.isPlaceholderData) || statsApiQueryResult.isPlaceholderData; + const hasData = Boolean(statsApiQueryResult?.data || statsMicroserviceQueryResult?.data); + + const { value: indicatorValue, valueDiff: indicatorValueDiff } = + getIndicatorValues(selectedIndicatorData as TChainIndicator, statsMicroserviceQueryResult?.data, statsApiQueryResult?.data); + const valueTitle = (() => { - if (statsQueryResult.isPlaceholderData) { + if (isPlaceholderData) { return ; } - if (!statsQueryResult.data) { + if (!hasData) { return There is no data; } return ( - { indicator?.value(statsQueryResult.data) } + { indicatorValue } ); })(); const valueDiff = (() => { - if (!statsQueryResult.data || !indicator?.valueDiff) { + if (indicatorValueDiff === undefined || indicatorValueDiff === null) { return null; } - const diff = indicator.valueDiff(statsQueryResult.data); - if (diff === undefined || diff === null) { - return null; - } - - const diffColor = diff >= 0 ? 'green.500' : 'red.500'; + const diffColor = indicatorValueDiff >= 0 ? 'green.500' : 'red.500'; return ( - - - { diff }% + + + { indicatorValueDiff }% ); })(); @@ -95,14 +111,16 @@ const ChainIndicators = () => { > - { indicator?.title } - { indicator?.hint && } + { selectedIndicatorData?.title } + { selectedIndicatorData?.hint && } { valueTitle } { valueDiff } - + + + { indicators.length > 1 && ( { { indicators.map((indicator) => ( )) } diff --git a/ui/home/indicators/types.ts b/ui/home/indicators/types.ts index 5fcfcc86f2..7194344194 100644 --- a/ui/home/indicators/types.ts +++ b/ui/home/indicators/types.ts @@ -1,22 +1,15 @@ import type React from 'react'; +import type { MainPageStats } from '@blockscout/stats-types'; import type { HomeStats } from 'types/api/stats'; import type { ChainIndicatorId } from 'types/homepage'; -import type { TimeChartData } from 'ui/shared/chart/types'; -import type { ResourcePayload } from 'lib/api/resources'; - -export type ChartsResources = 'stats_charts_txs' | 'stats_charts_market' | 'stats_charts_secondary_coin_price'; - -export interface TChainIndicator { +export interface TChainIndicator { id: ChainIndicatorId; title: string; value: (stats: HomeStats) => string; + valueMicroservice?: (stats: MainPageStats) => string; valueDiff?: (stats?: HomeStats) => number | null | undefined; icon: React.ReactNode; hint?: string; - api: { - resourceName: R; - dataFn: (response: ResourcePayload) => TimeChartData; - }; } diff --git a/ui/home/indicators/useChartDataQuery.tsx b/ui/home/indicators/useChartDataQuery.tsx new file mode 100644 index 0000000000..974a15fe8d --- /dev/null +++ b/ui/home/indicators/useChartDataQuery.tsx @@ -0,0 +1,172 @@ +import type { ChainIndicatorId } from 'types/homepage'; +import type { TimeChartData, TimeChartDataItem, TimeChartItemRaw } from 'ui/shared/chart/types'; + +import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; + +import prepareChartItems from './utils/prepareChartItems'; + +const CHART_ITEMS: Record> = { + daily_txs: { + name: 'Tx/day', + valueFormatter: (x: number) => x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + }, + daily_operational_txs: { + name: 'Operational Tx/day', + valueFormatter: (x: number) => x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + }, + coin_price: { + name: `${ config.chain.currency.symbol } price`, + valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), + }, + secondary_coin_price: { + name: `${ config.chain.currency.symbol } price`, + valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), + }, + market_cap: { + name: 'Market cap', + valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { maximumFractionDigits: 2 }), + }, + tvl: { + name: 'Total value locked', + valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + }, +}; + +const isStatsFeatureEnabled = config.features.stats.isEnabled; + +type UseFetchChartDataResult = { + isError: boolean; + isPending: boolean; + data: TimeChartData; +}; + +function getChartData(indicatorId: ChainIndicatorId, data: Array): TimeChartData { + return [ { + items: prepareChartItems(data), + name: CHART_ITEMS[indicatorId].name, + valueFormatter: CHART_ITEMS[indicatorId].valueFormatter, + } ]; +} + +export default function useChartDataQuery(indicatorId: ChainIndicatorId): UseFetchChartDataResult { + const statsDailyTxsQuery = useApiQuery('stats_main', { + queryOptions: { + refetchOnMount: false, + enabled: isStatsFeatureEnabled && indicatorId === 'daily_txs', + select: (data) => data.daily_new_transactions?.chart.map((item) => ({ date: new Date(item.date), value: Number(item.value) })) || [], + }, + }); + + const statsDailyOperationalTxsQuery = useApiQuery('stats_main', { + queryOptions: { + refetchOnMount: false, + enabled: isStatsFeatureEnabled && indicatorId === 'daily_operational_txs', + select: (data) => data.daily_new_operational_transactions?.chart.map((item) => ({ date: new Date(item.date), value: Number(item.value) })) || [], + }, + }); + + const apiDailyTxsQuery = useApiQuery('stats_charts_txs', { + queryOptions: { + refetchOnMount: false, + enabled: !isStatsFeatureEnabled && indicatorId === 'daily_txs', + select: (data) => data.chart_data.map((item) => ({ date: new Date(item.date), value: item.transaction_count })), + }, + }); + + const coinPriceQuery = useApiQuery('stats_charts_market', { + queryOptions: { + refetchOnMount: false, + enabled: indicatorId === 'coin_price', + select: (data) => data.chart_data.map((item) => ({ date: new Date(item.date), value: item.closing_price })), + }, + }); + + const secondaryCoinPriceQuery = useApiQuery('stats_charts_secondary_coin_price', { + queryOptions: { + refetchOnMount: false, + enabled: indicatorId === 'secondary_coin_price', + select: (data) => data.chart_data.map((item) => ({ date: new Date(item.date), value: item.closing_price })), + }, + }); + + const marketCapQuery = useApiQuery('stats_charts_market', { + queryOptions: { + refetchOnMount: false, + enabled: indicatorId === 'market_cap', + select: (data) => data.chart_data.map((item) => ( + { + date: new Date(item.date), + value: (() => { + if (item.market_cap !== undefined) { + return item.market_cap; + } + + if (item.closing_price === null) { + return null; + } + + return Number(item.closing_price) * Number(data.available_supply); + })(), + })), + }, + }); + + const tvlQuery = useApiQuery('stats_charts_market', { + queryOptions: { + refetchOnMount: false, + enabled: indicatorId === 'tvl', + select: (data) => data.chart_data.map((item) => ( + { + date: new Date(item.date), + value: item.tvl !== undefined ? item.tvl : 0, + })), + }, + }); + + switch (indicatorId) { + case 'daily_txs': { + const query = isStatsFeatureEnabled ? statsDailyTxsQuery : apiDailyTxsQuery; + return { + data: getChartData(indicatorId, query.data || []), + isError: query.isError, + isPending: query.isPending, + }; + } + case 'daily_operational_txs': { + return { + data: getChartData(indicatorId, statsDailyOperationalTxsQuery.data || []), + isError: statsDailyOperationalTxsQuery.isError, + isPending: statsDailyOperationalTxsQuery.isPending, + }; + } + case 'coin_price': { + return { + data: getChartData(indicatorId, coinPriceQuery.data || []), + isError: coinPriceQuery.isError, + isPending: coinPriceQuery.isPending, + }; + } + case 'secondary_coin_price': { + return { + data: getChartData(indicatorId, secondaryCoinPriceQuery.data || []), + isError: secondaryCoinPriceQuery.isError, + isPending: secondaryCoinPriceQuery.isPending, + }; + } + case 'market_cap': { + return { + data: getChartData(indicatorId, marketCapQuery.data || []), + isError: marketCapQuery.isError, + isPending: marketCapQuery.isPending, + }; + } + case 'tvl': { + return { + data: getChartData(indicatorId, tvlQuery.data || []), + isError: tvlQuery.isError, + isPending: tvlQuery.isPending, + }; + } + } +} diff --git a/ui/home/indicators/useFetchChartData.tsx b/ui/home/indicators/useFetchChartData.tsx deleted file mode 100644 index ffebc2b685..0000000000 --- a/ui/home/indicators/useFetchChartData.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { UseQueryResult } from '@tanstack/react-query'; -import React from 'react'; - -import type { TChainIndicator, ChartsResources } from './types'; -import type { TimeChartData } from 'ui/shared/chart/types'; - -import type { ResourcePayload } from 'lib/api/resources'; -import useApiQuery from 'lib/api/useApiQuery'; - -export default function useFetchChartData(indicator: TChainIndicator | undefined): UseQueryResult { - const queryResult = useApiQuery(indicator?.api.resourceName || 'stats_charts_txs', { - queryOptions: { enabled: Boolean(indicator) }, - }); - - return React.useMemo(() => { - return { - ...queryResult, - data: queryResult.data && indicator ? indicator.api.dataFn(queryResult.data as ResourcePayload) : queryResult.data, - } as UseQueryResult; - }, [ indicator, queryResult ]); -} diff --git a/ui/home/indicators/utils/getIndicatorValues.ts b/ui/home/indicators/utils/getIndicatorValues.ts new file mode 100644 index 0000000000..685f91fafd --- /dev/null +++ b/ui/home/indicators/utils/getIndicatorValues.ts @@ -0,0 +1,28 @@ +import type { TChainIndicator } from '../types'; +import type * as stats from '@blockscout/stats-types'; +import type { HomeStats } from 'types/api/stats'; + +import config from 'configs/app'; + +export default function getIndicatorValues(indicator: TChainIndicator, statsData?: stats.MainPageStats, statsApiData?: HomeStats) { + const value = (() => { + if (config.features.stats.isEnabled && indicator?.valueMicroservice && statsData) { + return indicator.valueMicroservice(statsData); + } + + if (statsApiData) { + return indicator?.value(statsApiData); + } + + return 'N/A'; + })(); + + // we have diffs only for coin adn second coin price charts that get data from stats api + // so we don't check microservice data here, but may require to add it in the future + const valueDiff = indicator?.valueDiff ? indicator.valueDiff(statsApiData) : undefined; + + return { + value, + valueDiff, + }; +} diff --git a/ui/home/indicators/utils/indicators.tsx b/ui/home/indicators/utils/indicators.tsx index b1fbcd5cc9..6d6de11d45 100644 --- a/ui/home/indicators/utils/indicators.tsx +++ b/ui/home/indicators/utils/indicators.tsx @@ -1,160 +1,73 @@ import React from 'react'; import type { TChainIndicator } from '../types'; -import type { TimeChartItem, TimeChartItemRaw } from 'ui/shared/chart/types'; 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'; -const nonNullTailReducer = (result: Array, item: TimeChartItemRaw) => { - if (item.value === null && result.length === 0) { - return result; - } - result.unshift(item); - return result; -}; - -const mapNullToZero: (item: TimeChartItemRaw) => TimeChartItem = (item) => ({ ...item, value: Number(item.value) }); - -const dailyTxsIndicator: TChainIndicator<'stats_charts_txs'> = { - id: 'daily_txs', - title: 'Daily transactions', - value: (stats) => stats.transactions_today === null ? - 'N/A' : - Number(stats.transactions_today).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), - icon: , - hint: `Number of transactions yesterday (0:00 - 23:59 UTC). The chart displays daily transactions for the past 30 days.`, - api: { - resourceName: 'stats_charts_txs', - dataFn: (response) => ([ { - items: response.chart_data - .map((item) => ({ date: new Date(item.date), value: item.transaction_count })) - .sort(sortByDateDesc) - .reduceRight(nonNullTailReducer, [] as Array) - .map(mapNullToZero), - name: 'Tx/day', - valueFormatter: (x: number) => x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), - } ]), +const INDICATORS: Array = [ + { + id: 'daily_txs', + title: 'Daily transactions', + value: (stats) => stats.transactions_today === null ? + 'N/A' : + Number(stats.transactions_today).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + valueMicroservice: (stats) => stats.yesterday_transactions?.value === null ? + 'N/A' : + Number(stats.yesterday_transactions?.value).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + icon: , + hint: `Number of transactions yesterday (0:00 - 23:59 UTC). The chart displays daily transactions for the past 30 days.`, }, -}; - -const coinPriceIndicator: TChainIndicator<'stats_charts_market'> = { - id: 'coin_price', - title: `${ config.chain.currency.symbol } price`, - value: (stats) => stats.coin_price === null ? - '$N/A' : - '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), - valueDiff: (stats) => stats?.coin_price !== null ? stats?.coin_price_change_percentage : null, - icon: , - hint: `${ config.chain.currency.symbol } token daily price in USD.`, - api: { - resourceName: 'stats_charts_market', - dataFn: (response) => ([ { - items: response.chart_data - .map((item) => ({ date: new Date(item.date), value: item.closing_price })) - .sort(sortByDateDesc) - .reduceRight(nonNullTailReducer, [] as Array) - .map(mapNullToZero), - name: `${ config.chain.currency.symbol } price`, - valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), - } ]), + { + id: 'daily_operational_txs', + title: 'Operational txns', + value: () => 'N/A', + valueMicroservice: (stats) => stats.yesterday_operational_transactions?.value === null ? + 'N/A' : + Number(stats.yesterday_operational_transactions?.value).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + icon: , + hint: `Number of operational transactions yesterday (0:00 - 23:59 UTC). The chart displays daily operational transactions for the past 30 days.`, }, -}; - -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: , - 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) - .map(mapNullToZero), - name: `${ config.chain.secondaryCoin.symbol } price`, - valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), - } ]), + { + id: 'coin_price', + title: `${ config.chain.currency.symbol } price`, + value: (stats) => stats.coin_price === null ? + '$N/A' : + '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), + valueDiff: (stats) => stats?.coin_price !== null ? stats?.coin_price_change_percentage : null, + icon: , + hint: `${ config.chain.currency.symbol } token daily price in USD.`, }, -}; - -const marketPriceIndicator: TChainIndicator<'stats_charts_market'> = { - id: 'market_cap', - title: 'Market cap', - value: (stats) => stats.market_cap === null ? - '$N/A' : - '$' + Number(stats.market_cap).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), - icon: , - // eslint-disable-next-line max-len - hint: 'The total market value of a cryptocurrency\'s circulating supply. It is analogous to the free-float capitalization in the stock market. Market Cap = Current Price x Circulating Supply.', - api: { - resourceName: 'stats_charts_market', - dataFn: (response) => ([ { - items: response.chart_data - .map((item) => ( - { - date: new Date(item.date), - value: (() => { - if (item.market_cap !== undefined) { - return item.market_cap; - } - - if (item.closing_price === null) { - return null; - } - - return Number(item.closing_price) * Number(response.available_supply); - })(), - })) - .sort(sortByDateDesc) - .reduceRight(nonNullTailReducer, [] as Array) - .map(mapNullToZero), - name: 'Market cap', - valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { maximumFractionDigits: 2 }), - } ]), + { + 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: , + hint: `${ config.chain.secondaryCoin.symbol } token daily price in USD.`, }, -}; - -const tvlIndicator: TChainIndicator<'stats_charts_market'> = { - id: 'tvl', - title: 'Total value locked', - value: (stats) => stats.tvl === null ? - '$N/A' : - '$' + Number(stats.tvl).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), - icon: , - hint: 'Total value of digital assets locked or staked in a chain', - api: { - resourceName: 'stats_charts_market', - dataFn: (response) => ([ { - items: response.chart_data - .map((item) => ( - { - date: new Date(item.date), - value: item.tvl !== undefined ? item.tvl : 0, - })) - .sort(sortByDateDesc) - .reduceRight(nonNullTailReducer, [] as Array) - .map(mapNullToZero), - name: 'TVL', - valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), - } ]), + { + id: 'market_cap', + title: 'Market cap', + value: (stats) => stats.market_cap === null ? + '$N/A' : + '$' + Number(stats.market_cap).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + icon: , + // eslint-disable-next-line max-len + hint: 'The total market value of a cryptocurrency\'s circulating supply. It is analogous to the free-float capitalization in the stock market. Market Cap = Current Price x Circulating Supply.', + }, + { + id: 'tvl', + title: 'Total value locked', + value: (stats) => stats.tvl === null ? + '$N/A' : + '$' + Number(stats.tvl).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), + icon: , + hint: 'Total value of digital assets locked or staked in a chain', }, -}; - -const INDICATORS = [ - dailyTxsIndicator, - coinPriceIndicator, - secondaryCoinPriceIndicator, - marketPriceIndicator, - tvlIndicator, ]; export default INDICATORS; diff --git a/ui/home/indicators/utils/prepareChartItems.ts b/ui/home/indicators/utils/prepareChartItems.ts new file mode 100644 index 0000000000..9f5cb73aab --- /dev/null +++ b/ui/home/indicators/utils/prepareChartItems.ts @@ -0,0 +1,20 @@ +import type { TimeChartItem, TimeChartItemRaw } from 'ui/shared/chart/types'; + +import { sortByDateDesc } from 'ui/shared/chart/utils/sorts'; + +const nonNullTailReducer = (result: Array, item: TimeChartItemRaw) => { + if (item.value === null && result.length === 0) { + return result; + } + result.unshift(item); + return result; +}; + +const mapNullToZero: (item: TimeChartItemRaw) => TimeChartItem = (item) => ({ ...item, value: Number(item.value) }); + +export default function prepareChartItems(items: Array) { + return items + .sort(sortByDateDesc) + .reduceRight(nonNullTailReducer, [] as Array) + .map(mapNullToZero); +} diff --git a/ui/shared/stats/StatsWidget.tsx b/ui/shared/stats/StatsWidget.tsx index e6987e8c4f..7ebca78cf4 100644 --- a/ui/shared/stats/StatsWidget.tsx +++ b/ui/shared/stats/StatsWidget.tsx @@ -20,7 +20,7 @@ export type Props = { diff?: string | number; diffFormatted?: string; diffPeriod?: '24h'; - period?: '1h' | '24h'; + period?: '1h' | '24h' | '30min'; href?: Route; icon?: IconName; }; diff --git a/ui/txs/TxsStats.tsx b/ui/txs/TxsStats.tsx index 763d6eada7..c63e6cb769 100644 --- a/ui/txs/TxsStats.tsx +++ b/ui/txs/TxsStats.tsx @@ -6,12 +6,22 @@ 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 { TXS_STATS, TXS_STATS_MICROSERVICE } from 'stubs/tx'; import StatsWidget from 'ui/shared/stats/StatsWidget'; +const isStatsFeatureEnabled = config.features.stats.isEnabled; + const TxsStats = () => { - const txsStatsQuery = useApiQuery('txs_stats', { + const txsStatsQuery = useApiQuery('stats_transactions', { + queryOptions: { + enabled: isStatsFeatureEnabled, + placeholderData: TXS_STATS_MICROSERVICE, + }, + }); + + const txsStatsApiQuery = useApiQuery('txs_stats', { queryOptions: { + enabled: !isStatsFeatureEnabled, placeholderData: TXS_STATS, }, }); @@ -22,16 +32,30 @@ const TxsStats = () => { }, }); - if (!txsStatsQuery.data) { + if ((isStatsFeatureEnabled && !txsStatsQuery.data) || (!isStatsFeatureEnabled && !txsStatsApiQuery.data)) { return null; } - const txFeeAvg = getCurrencyValue({ - value: txsStatsQuery.data.transaction_fees_avg_24h, + const isLoading = isStatsFeatureEnabled ? txsStatsQuery.isPlaceholderData : txsStatsApiQuery.isPlaceholderData; + + const txCount24h = isStatsFeatureEnabled ? txsStatsQuery.data?.transactions_24h?.value : txsStatsApiQuery.data?.transactions_count_24h; + + const pendingTxns = isStatsFeatureEnabled ? txsStatsQuery.data?.pending_transactions_30m?.value : txsStatsApiQuery.data?.pending_transactions_count; + + // in microservice data, fee values are already divided by 10^decimals + const txFeeSum24h = isStatsFeatureEnabled ? + Number(txsStatsQuery.data?.transactions_fee_24h?.value) : + Number(txsStatsApiQuery.data?.transaction_fees_sum_24h) / (10 ** config.chain.currency.decimals); + + const avgFee = isStatsFeatureEnabled ? txsStatsQuery.data?.average_transactions_fee_24h?.value : txsStatsApiQuery.data?.transaction_fees_avg_24h; + + const txFeeAvg = avgFee ? getCurrencyValue({ + value: avgFee, exchangeRate: statsQuery.data?.coin_price, - decimals: String(config.chain.currency.decimals), + // in microservice data, fee values are already divided by 10^decimals + decimals: isStatsFeatureEnabled ? '0' : String(config.chain.currency.decimals), accuracyUsd: 2, - }); + }) : null; return ( { columnGap={ 3 } mb={ 6 } > - - - - + { txCount24h && ( + + ) } + { pendingTxns && ( + + ) } + { txFeeSum24h && ( + + ) } + { txFeeAvg && ( + + ) } ); }; diff --git a/ui/verifiedContracts/VerifiedContractsCounters.tsx b/ui/verifiedContracts/VerifiedContractsCounters.tsx index ba0c5e8a26..3db08b5a3d 100644 --- a/ui/verifiedContracts/VerifiedContractsCounters.tsx +++ b/ui/verifiedContracts/VerifiedContractsCounters.tsx @@ -3,37 +3,59 @@ import React from 'react'; import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; -import { VERIFIED_CONTRACTS_COUNTERS } from 'stubs/contract'; +import { VERIFIED_CONTRACTS_COUNTERS, VERIFIED_CONTRACTS_COUNTERS_MICROSERVICE } from 'stubs/contract'; import StatsWidget from 'ui/shared/stats/StatsWidget'; +const isStatsFeatureEnabled = config.features.stats.isEnabled; + const VerifiedContractsCounters = () => { - const countersQuery = useApiQuery('verified_contracts_counters', { + const countersStatsQuery = useApiQuery('stats_contracts', { + queryOptions: { + enabled: isStatsFeatureEnabled, + placeholderData: VERIFIED_CONTRACTS_COUNTERS_MICROSERVICE, + }, + }); + + const countersApiQuery = useApiQuery('verified_contracts_counters', { queryOptions: { + enabled: !isStatsFeatureEnabled, placeholderData: VERIFIED_CONTRACTS_COUNTERS, }, }); - if (!countersQuery.data) { + if (!(countersStatsQuery.data || countersApiQuery.data)) { return null; } + const isLoading = isStatsFeatureEnabled ? countersStatsQuery.isPlaceholderData : countersApiQuery.isPlaceholderData; + + const contractsCount = isStatsFeatureEnabled ? countersStatsQuery.data?.total_contracts?.value : countersApiQuery.data?.smart_contracts; + const newContractsCount = isStatsFeatureEnabled ? countersStatsQuery.data?.new_contracts_24h?.value : countersApiQuery.data?.new_smart_contracts_24h; + + const verifiedContractsCount = isStatsFeatureEnabled ? + countersStatsQuery.data?.total_verified_contracts?.value : + countersApiQuery.data?.verified_smart_contracts; + const newVerifiedContractsCount = isStatsFeatureEnabled ? + countersStatsQuery.data?.new_verified_contracts_24h?.value : + countersApiQuery.data?.new_verified_smart_contracts_24h; + return ( diff --git a/yarn.lock b/yarn.lock index eedbade03b..b1726d6c05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1508,10 +1508,10 @@ resolved "https://registry.yarnpkg.com/@blockscout/bens-types/-/bens-types-1.4.1.tgz#9182a79d9015b7fa2339edf0bfa3cd0c32045e66" integrity sha512-TlZ1HVdZ2Cswm/CcvNoxS+Ydiht/YGaLo//PJR/UmkmihlEFoY4HfVJvVcUnOQXi+Si7FwJ486DPii889nTJsQ== -"@blockscout/stats-types@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@blockscout/stats-types/-/stats-types-2.0.0.tgz#3805f8379b75377cde8a9ab76306af37bb735846" - integrity sha512-icYDsOHsDACjG/7VZhlV+1QRKSJOycblpswQ5Si0dqeWdOpbtmxSqolAS/z6C77d8p+uxZUCMjNa9otUCqn18A== +"@blockscout/stats-types@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@blockscout/stats-types/-/stats-types-2.4.0.tgz#3765449017136a800d04bdcdc16004661f93410b" + integrity sha512-UaY9PeNZpL61XDKJwwOTOIEtl5jpX1XxMcElxSvwidSPanKK2+Fs20nhm0aIWO0MXbTr67VUf9u4ZbQw8bl9rw== "@blockscout/visualizer-types@0.2.0": version "0.2.0" From 91de96474de7553e808d29e2b5ff0a7c5f546d2a Mon Sep 17 00:00:00 2001 From: isstuev Date: Fri, 7 Feb 2025 11:33:20 +0100 Subject: [PATCH 2/7] add op stats to tx page and change stats titles --- lib/api/resources.ts | 6 +++--- package.json | 2 +- stubs/tx.ts | 1 + ui/home/Stats.tsx | 12 ++++++------ ui/home/indicators/ChainIndicators.tsx | 22 ++++++++++++++++++++-- ui/home/indicators/types.ts | 4 +++- ui/home/indicators/utils/indicators.tsx | 6 +++++- ui/txs/TxsStats.tsx | 18 +++++++++++++----- yarn.lock | 8 ++++---- 9 files changed, 56 insertions(+), 23 deletions(-) diff --git a/lib/api/resources.ts b/lib/api/resources.ts index cb208ecef4..5c6940ed19 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -1290,9 +1290,6 @@ Q extends 'homepage_arbitrum_latest_batch' ? number : Q extends 'stats_counters' ? stats.Counters : Q extends 'stats_lines' ? stats.LineCharts : Q extends 'stats_line' ? stats.LineChart : -Q extends 'stats_main' ? stats.MainPageStats : -Q extends 'stats_transactions' ? stats.TransactionsPageStats : -Q extends 'stats_contracts' ? stats.ContractsPageStats : Q extends 'blocks' ? BlocksResponse : Q extends 'block' ? Block : Q extends 'block_countdown' ? BlockCountdownResponse : @@ -1453,6 +1450,9 @@ Q extends 'advanced_filter' ? AdvancedFilterResponse : Q extends 'advanced_filter_methods' ? AdvancedFilterMethodsResponse : Q extends 'pools' ? PoolsResponse : Q extends 'pool' ? Pool : +Q extends 'stats_main' ? stats.MainPageStats : +Q extends 'stats_transactions' ? stats.TransactionsPageStats : +Q extends 'stats_contracts' ? stats.ContractsPageStats : never; /* eslint-enable @stylistic/indent */ diff --git a/package.json b/package.json index 53be65eb1c..f75d2753ac 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@blockscout/bens-types": "1.4.1", - "@blockscout/stats-types": "2.4.0", + "@blockscout/stats-types": "2.5.0-alpha", "@blockscout/visualizer-types": "0.2.0", "@chakra-ui/react": "2.7.1", "@chakra-ui/theme-tools": "^2.0.18", diff --git a/stubs/tx.ts b/stubs/tx.ts index 343d7a8003..a1c7f728e3 100644 --- a/stubs/tx.ts +++ b/stubs/tx.ts @@ -72,6 +72,7 @@ export const TXS_STATS: TransactionsStats = { export const TXS_STATS_MICROSERVICE: stats.TransactionsPageStats = { pending_transactions_30m: STATS_COUNTER, transactions_24h: STATS_COUNTER, + operational_transactions_24h: STATS_COUNTER, transactions_fee_24h: STATS_COUNTER, average_transactions_fee_24h: STATS_COUNTER, }; diff --git a/ui/home/Stats.tsx b/ui/home/Stats.tsx index fa15b2f0ab..eaba967de0 100644 --- a/ui/home/Stats.tsx +++ b/ui/home/Stats.tsx @@ -123,10 +123,10 @@ const Stats = () => { href: { pathname: '/batches' as const }, isLoading, }, - { + (statsData?.total_blocks?.value || apiData?.total_blocks) && { id: 'total_blocks' as const, icon: 'block_slim' as const, - label: 'Total blocks', + label: statsData?.total_blocks?.title || 'Total blocks', value: Number(statsData?.total_blocks?.value || apiData?.total_blocks).toLocaleString(), href: { pathname: '/blocks' as const }, isLoading, @@ -134,14 +134,14 @@ const Stats = () => { (statsData?.average_block_time?.value || apiData?.average_block_time) && { id: 'average_block_time' as const, icon: 'clock-light' as const, - label: 'Average block time', + label: statsData?.average_block_time?.title || 'Average block time', value: `${ Number(statsData?.average_block_time?.value).toFixed(1) || (apiData ? (apiData.average_block_time / 1000).toFixed(1) : 'N/A') }s`, isLoading, }, (statsData?.total_transactions?.value || apiData?.total_transactions) && { id: 'total_txs' as const, icon: 'transactions_slim' as const, - label: 'Total transactions', + label: statsData?.total_transactions?.title || 'Total transactions', value: Number(statsData?.total_transactions?.value || apiData?.total_transactions).toLocaleString(), href: { pathname: '/txs' as const }, isLoading, @@ -149,7 +149,7 @@ const Stats = () => { statsData?.total_operational_transactions?.value && { id: 'total_operational_txs' as const, icon: 'transactions_slim' as const, - label: 'Total operational transactions', + label: statsData?.total_operational_transactions?.title || 'Total operational transactions', value: Number(statsData?.total_operational_transactions?.value).toLocaleString(), href: { pathname: '/txs' as const }, isLoading, @@ -165,7 +165,7 @@ const Stats = () => { (statsData?.total_addresses?.value || apiData?.total_addresses) && { id: 'wallet_addresses' as const, icon: 'wallet' as const, - label: 'Wallet addresses', + label: statsData?.total_addresses?.title || 'Wallet addresses', value: Number(statsData?.total_addresses?.value || apiData?.total_addresses).toLocaleString(), isLoading, }, diff --git a/ui/home/indicators/ChainIndicators.tsx b/ui/home/indicators/ChainIndicators.tsx index bced0945d2..eef09f3f69 100644 --- a/ui/home/indicators/ChainIndicators.tsx +++ b/ui/home/indicators/ChainIndicators.tsx @@ -66,6 +66,24 @@ const ChainIndicators = () => { const { value: indicatorValue, valueDiff: indicatorValueDiff } = getIndicatorValues(selectedIndicatorData as TChainIndicator, statsMicroserviceQueryResult?.data, statsApiQueryResult?.data); + const title = (() => { + let title: string | undefined; + if (selectedIndicatorData?.titleMicroservice && statsMicroserviceQueryResult?.data) { + title = selectedIndicatorData.titleMicroservice(statsMicroserviceQueryResult.data); + } + + return title || selectedIndicatorData?.title; + })(); + + const hint = (() => { + let hint: string | undefined; + if (selectedIndicatorData?.hintMicroservice && statsMicroserviceQueryResult?.data) { + hint = selectedIndicatorData.hintMicroservice(statsMicroserviceQueryResult.data); + } + + return hint || selectedIndicatorData?.hint; + })(); + const valueTitle = (() => { if (isPlaceholderData) { return ; @@ -111,8 +129,8 @@ const ChainIndicators = () => { > - { selectedIndicatorData?.title } - { selectedIndicatorData?.hint && } + { title } + { hint && } { valueTitle } diff --git a/ui/home/indicators/types.ts b/ui/home/indicators/types.ts index 7194344194..9bb8ad132c 100644 --- a/ui/home/indicators/types.ts +++ b/ui/home/indicators/types.ts @@ -6,10 +6,12 @@ import type { ChainIndicatorId } from 'types/homepage'; export interface TChainIndicator { id: ChainIndicatorId; + titleMicroservice?: (stats: MainPageStats) => string | undefined; title: string; value: (stats: HomeStats) => string; - valueMicroservice?: (stats: MainPageStats) => string; + valueMicroservice?: (stats: MainPageStats) => string | undefined; valueDiff?: (stats?: HomeStats) => number | null | undefined; icon: React.ReactNode; hint?: string; + hintMicroservice?: (stats: MainPageStats) => string | undefined; } diff --git a/ui/home/indicators/utils/indicators.tsx b/ui/home/indicators/utils/indicators.tsx index 6d6de11d45..08b91111cd 100644 --- a/ui/home/indicators/utils/indicators.tsx +++ b/ui/home/indicators/utils/indicators.tsx @@ -10,6 +10,7 @@ const INDICATORS: Array = [ { id: 'daily_txs', title: 'Daily transactions', + titleMicroservice: (stats) => stats.daily_new_transactions?.info?.title, value: (stats) => stats.transactions_today === null ? 'N/A' : Number(stats.transactions_today).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), @@ -18,16 +19,19 @@ const INDICATORS: Array = [ Number(stats.yesterday_transactions?.value).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), icon: , hint: `Number of transactions yesterday (0:00 - 23:59 UTC). The chart displays daily transactions for the past 30 days.`, + hintMicroservice: (stats) => stats.daily_new_transactions?.info?.description, }, { id: 'daily_operational_txs', - title: 'Operational txns', + title: 'Daily op txns', + titleMicroservice: (stats) => stats.daily_new_operational_transactions?.info?.title, value: () => 'N/A', valueMicroservice: (stats) => stats.yesterday_operational_transactions?.value === null ? 'N/A' : Number(stats.yesterday_operational_transactions?.value).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), icon: , hint: `Number of operational transactions yesterday (0:00 - 23:59 UTC). The chart displays daily operational transactions for the past 30 days.`, + hintMicroservice: (stats) => stats.daily_new_operational_transactions?.info?.description, }, { id: 'coin_price', diff --git a/ui/txs/TxsStats.tsx b/ui/txs/TxsStats.tsx index c63e6cb769..005eea9102 100644 --- a/ui/txs/TxsStats.tsx +++ b/ui/txs/TxsStats.tsx @@ -60,23 +60,31 @@ const TxsStats = () => { return ( { txCount24h && ( ) } + { isStatsFeatureEnabled && txsStatsQuery.data?.operational_transactions_24h?.value && ( + + ) } { pendingTxns && ( { ) } { txFeeSum24h && ( { ) } { txFeeAvg && ( Date: Fri, 7 Feb 2025 11:43:57 +0100 Subject: [PATCH 3/7] demo config --- configs/envs/.env.eth_sepolia | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/configs/envs/.env.eth_sepolia b/configs/envs/.env.eth_sepolia index 5809f0719a..dab30785ad 100644 --- a/configs/envs/.env.eth_sepolia +++ b/configs/envs/.env.eth_sepolia @@ -25,7 +25,8 @@ NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-c NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363 NEXT_PUBLIC_HAS_BEACON_CHAIN=true NEXT_PUBLIC_HAS_USER_OPS=true -NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_operational_txs','coin_price'] +NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks', 'average_block_time','total_operational_txs','wallet_addresses'] NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(51, 53, 67, 1)'],'text_color':['rgba(165, 252, 122, 1)']} NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_TESTNET=true From 07c7561b276708267004207beeeb151d95c72597 Mon Sep 17 00:00:00 2001 From: isstuev Date: Fri, 7 Feb 2025 14:01:03 +0100 Subject: [PATCH 4/7] aboba or cat fix --- deploy/tools/envs-validator/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 3d1f697ade..be5baae687 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -772,7 +772,7 @@ const schema = yup .transform(replaceQuotes) .json() .of(yup.string().oneOf(TX_ADDITIONAL_FIELDS_IDS)), - NEXT_PUBLIC_VIEWS_NFT_MARKPLACES: yup + NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: yup .array() .transform(replaceQuotes) .json() From eeaed64ce08d2fece1d4f864fe6938ce1aa979fb Mon Sep 17 00:00:00 2001 From: isstuev Date: Mon, 10 Feb 2025 11:51:06 +0100 Subject: [PATCH 5/7] chart tooltip fix --- ui/home/indicators/useChartDataQuery.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/home/indicators/useChartDataQuery.tsx b/ui/home/indicators/useChartDataQuery.tsx index 974a15fe8d..e30d01126e 100644 --- a/ui/home/indicators/useChartDataQuery.tsx +++ b/ui/home/indicators/useChartDataQuery.tsx @@ -12,7 +12,7 @@ const CHART_ITEMS: Record x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), }, daily_operational_txs: { - name: 'Operational Tx/day', + name: 'Tx/day', valueFormatter: (x: number) => x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), }, coin_price: { @@ -28,7 +28,7 @@ const CHART_ITEMS: Record '$' + x.toLocaleString(undefined, { maximumFractionDigits: 2 }), }, tvl: { - name: 'Total value locked', + name: 'TVL', valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), }, }; From f9675422501a75ef74863b35bb29a675e03aee1d Mon Sep 17 00:00:00 2001 From: isstuev Date: Mon, 10 Feb 2025 16:55:42 +0100 Subject: [PATCH 6/7] tests --- mocks/stats/main.tsx | 71 ++++++++++++++++++ ui/home/Stats.pw.tsx | 8 +- ui/home/Stats.tsx | 12 ++- ui/home/indicators/ChainIndicators.pw.tsx | 1 + ui/home/indicators/ChainIndicators.tsx | 4 +- ui/pages/Home.pw.tsx | 3 + ui/pages/VerifiedContracts.pw.tsx | 3 +- ...ode_default-view---default-dark-mode-1.png | Bin 177229 -> 178940 bytes ...ult_default-view-screen-xl-base-view-1.png | Bin 197625 -> 200247 bytes ...Home.pw.tsx_default_mobile-base-view-1.png | Bin 108153 -> 108973 bytes ui/txs/TxsStats.pw.tsx | 3 +- ui/txs/TxsStats.tsx | 19 +++-- .../VerifiedContractsCounters.tsx | 6 +- 13 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 mocks/stats/main.tsx diff --git a/mocks/stats/main.tsx b/mocks/stats/main.tsx new file mode 100644 index 0000000000..a3a6bbd016 --- /dev/null +++ b/mocks/stats/main.tsx @@ -0,0 +1,71 @@ +import type * as stats from '@blockscout/stats-types'; + +import { averageGasPrice } from './line'; + +export const base: stats.MainPageStats = { + average_block_time: { + id: 'averageBlockTime', + value: '14.909090909090908', + title: 'Average block time', + units: 's', + description: 'Average time taken in seconds for a block to be included in the blockchain', + }, + total_addresses: { + id: 'totalAddresses', + value: '113606435', + title: 'Total addresses', + description: 'Number of addresses that participated in the blockchain', + }, + total_blocks: { + id: 'totalBlocks', + value: '7660515', + title: 'Total blocks', + description: 'Number of blocks over all time', + }, + total_transactions: { + id: 'totalTxns', + value: '411264599', + title: 'Total txns', + description: 'All transactions including pending, dropped, replaced, failed transactions', + }, + yesterday_transactions: { + id: 'yesterdayTxns', + value: '213019', + title: 'Yesterday txns', + description: 'Number of transactions yesterday (0:00 - 23:59 UTC)', + }, + total_operational_transactions: { + id: 'totalOperationalTxns', + value: '403598877', + title: 'Total operational txns', + description: '\'Total txns\' without block creation transactions', + }, + yesterday_operational_transactions: { + id: 'yesterdayOperationalTxns', + value: '210852', + title: 'Yesterday operational txns', + description: 'Number of transactions yesterday (0:00 - 23:59 UTC) without block creation transactions', + }, + daily_new_transactions: { + chart: averageGasPrice.chart, + info: { + id: 'newTxnsWindow', + title: 'Daily transactions', + description: 'The chart displays daily transactions for the past 30 days', + resolutions: [ + 'DAY', + ], + }, + }, + daily_new_operational_transactions: { + chart: averageGasPrice.chart, + info: { + id: 'newOperationalTxnsWindow', + title: 'Daily operational transactions', + description: 'The chart displays daily transactions for the past 30 days (without block creation transactions)', + resolutions: [ + 'DAY', + ], + }, + }, +}; diff --git a/ui/home/Stats.pw.tsx b/ui/home/Stats.pw.tsx index 9add012cba..f5818f18ca 100644 --- a/ui/home/Stats.pw.tsx +++ b/ui/home/Stats.pw.tsx @@ -12,6 +12,7 @@ test.describe('all items', () => { test.beforeEach(async({ render, mockApiResponse, mockEnvs }) => { await mockEnvs([ [ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_blocks","average_block_time","total_txs","wallet_addresses","gas_tracker","btc_locked"]' ], + [ 'NEXT_PUBLIC_STATS_API_HOST', '' ], ]); await mockApiResponse('stats', statsMock.withBtcLocked); component = await render(); @@ -22,7 +23,10 @@ test.describe('all items', () => { }); }); -test('no gas info', async({ render, mockApiResponse }) => { +test('no gas info', async({ render, mockApiResponse, mockEnvs }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_STATS_API_HOST', '' ], + ]); await mockApiResponse('stats', statsMock.withoutGasInfo); const component = await render(); @@ -32,6 +36,7 @@ test('no gas info', async({ render, mockApiResponse }) => { test('4 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => { await mockEnvs([ [ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_txs","gas_tracker","wallet_addresses","total_blocks"]' ], + [ 'NEXT_PUBLIC_STATS_API_HOST', '' ], ]); await mockApiResponse('stats', statsMock.base); const component = await render(); @@ -41,6 +46,7 @@ test('4 items default view +@mobile -@default', async({ render, mockApiResponse, test('3 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => { await mockEnvs([ [ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_txs","wallet_addresses","total_blocks"]' ], + [ 'NEXT_PUBLIC_STATS_API_HOST', '' ], ]); await mockApiResponse('stats', statsMock.base); const component = await render(); diff --git a/ui/home/Stats.tsx b/ui/home/Stats.tsx index eaba967de0..5ce4e3cc81 100644 --- a/ui/home/Stats.tsx +++ b/ui/home/Stats.tsx @@ -15,7 +15,7 @@ import type { Props as StatsWidgetProps } from 'ui/shared/stats/StatsWidget'; import StatsWidget from 'ui/shared/stats/StatsWidget'; const rollupFeature = config.features.rollup; -const statsFeature = config.features.stats; +const isStatsFeatureEnabled = config.features.stats.isEnabled; const Stats = () => { const [ hasGasTracker, setHasGasTracker ] = React.useState(config.features.gasTracker.isEnabled); @@ -24,8 +24,8 @@ const Stats = () => { const statsQuery = useApiQuery('stats_main', { queryOptions: { refetchOnMount: false, - placeholderData: statsFeature.isEnabled ? HOMEPAGE_STATS_MICROSERVICE : undefined, - enabled: statsFeature.isEnabled, + placeholderData: isStatsFeatureEnabled ? HOMEPAGE_STATS_MICROSERVICE : undefined, + enabled: isStatsFeatureEnabled, }, }); @@ -135,7 +135,11 @@ const Stats = () => { id: 'average_block_time' as const, icon: 'clock-light' as const, label: statsData?.average_block_time?.title || 'Average block time', - value: `${ Number(statsData?.average_block_time?.value).toFixed(1) || (apiData ? (apiData.average_block_time / 1000).toFixed(1) : 'N/A') }s`, + value: `${ + statsData?.average_block_time?.value ? + Number(statsData.average_block_time.value).toFixed(1) : + (apiData!.average_block_time / 1000).toFixed(1) + }s`, isLoading, }, (statsData?.total_transactions?.value || apiData?.total_transactions) && { diff --git a/ui/home/indicators/ChainIndicators.pw.tsx b/ui/home/indicators/ChainIndicators.pw.tsx index a1454282b6..0ae0c70f85 100644 --- a/ui/home/indicators/ChainIndicators.pw.tsx +++ b/ui/home/indicators/ChainIndicators.pw.tsx @@ -11,6 +11,7 @@ test.beforeEach(async({ mockEnvs }) => { await mockEnvs([ [ 'NEXT_PUBLIC_HOMEPAGE_CHARTS', '["daily_txs","coin_price","secondary_coin_price","market_cap","tvl"]' ], [ 'NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL', 'DUCK' ], + [ 'NEXT_PUBLIC_STATS_API_HOST', '' ], ]); }); diff --git a/ui/home/indicators/ChainIndicators.tsx b/ui/home/indicators/ChainIndicators.tsx index eef09f3f69..c9be478dc7 100644 --- a/ui/home/indicators/ChainIndicators.tsx +++ b/ui/home/indicators/ChainIndicators.tsx @@ -68,7 +68,7 @@ const ChainIndicators = () => { const title = (() => { let title: string | undefined; - if (selectedIndicatorData?.titleMicroservice && statsMicroserviceQueryResult?.data) { + if (isStatsFeatureEnabled && selectedIndicatorData?.titleMicroservice && statsMicroserviceQueryResult?.data) { title = selectedIndicatorData.titleMicroservice(statsMicroserviceQueryResult.data); } @@ -77,7 +77,7 @@ const ChainIndicators = () => { const hint = (() => { let hint: string | undefined; - if (selectedIndicatorData?.hintMicroservice && statsMicroserviceQueryResult?.data) { + if (isStatsFeatureEnabled && selectedIndicatorData?.hintMicroservice && statsMicroserviceQueryResult?.data) { hint = selectedIndicatorData.hintMicroservice(statsMicroserviceQueryResult.data); } diff --git a/ui/pages/Home.pw.tsx b/ui/pages/Home.pw.tsx index 51630da274..d33ce54c0e 100644 --- a/ui/pages/Home.pw.tsx +++ b/ui/pages/Home.pw.tsx @@ -4,6 +4,7 @@ import React from 'react'; import * as blockMock from 'mocks/blocks/block'; import * as dailyTxsMock from 'mocks/stats/daily_txs'; import * as statsMock from 'mocks/stats/index'; +import * as statsMainMock from 'mocks/stats/main'; import * as txMock from 'mocks/txs/tx'; import { test, expect, devices } from 'playwright/lib'; import * as pwConfig from 'playwright/utils/config'; @@ -15,6 +16,7 @@ test.describe('default view', () => { test.beforeEach(async({ render, mockApiResponse, mockAssetResponse }) => { await mockAssetResponse(statsMock.base.coin_image as string, './playwright/mocks/image_s.jpg'); + await mockApiResponse('stats_main', statsMainMock.base); await mockApiResponse('stats', statsMock.base); await mockApiResponse('homepage_blocks', [ blockMock.base, @@ -55,6 +57,7 @@ test.describe('mobile', () => { test('base view', async({ render, page, mockAssetResponse, mockApiResponse }) => { await mockAssetResponse(statsMock.base.coin_image as string, './playwright/mocks/image_s.jpg'); + await mockApiResponse('stats_main', statsMainMock.base); await mockApiResponse('stats', statsMock.base); await mockApiResponse('homepage_blocks', [ blockMock.base, diff --git a/ui/pages/VerifiedContracts.pw.tsx b/ui/pages/VerifiedContracts.pw.tsx index 7a07731b5c..b5ea870350 100644 --- a/ui/pages/VerifiedContracts.pw.tsx +++ b/ui/pages/VerifiedContracts.pw.tsx @@ -6,7 +6,8 @@ import { test, expect } from 'playwright/lib'; import VerifiedContracts from './VerifiedContracts'; -test('base view +@mobile', async({ render, mockTextAd, mockApiResponse }) => { +test('base view +@mobile', async({ render, mockTextAd, mockApiResponse, mockEnvs }) => { + await mockEnvs([ [ 'NEXT_PUBLIC_STATS_API_HOST', '' ] ]); await mockTextAd(); await mockApiResponse('verified_contracts', verifiedContractsMock.baseResponse); await mockApiResponse('verified_contracts_counters', verifiedContractsCountersMock); diff --git a/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png b/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png index c0ef85b8aaba715b9840a76a61f31d786c6ce20b..7f2a7782cf5b7bacefaff9feede033c0240f50bb 100644 GIT binary patch delta 123935 zcmZs@bx;@I8#cT&NQZPPNP{%eARyh{-QB$)CDJV+A>G|62uOD~NOw0pAHKi$oq7Iv zW*BB?*>iU9ICos^Qo+YKO<3B!VQtT;ZWDan@QCnZd`rn5yaJQ@y5j|9QfvQ2vaKp0@K3y4?anf5?A@IGZ z(4h+Yyv!L_%Qf%+QB7Bu5%}n6qR%9R!*AQQ#+LNBfFzVWNx+c>;^TGhxj;n^!mVAC zusn~Lfd_pkpvD3<3OSG8hR1@Pr2~doR=v-Jr{ZVx)|gc8F(Nx&=>aMEf4=QiHVIA} zV~Bu1R2Xd$8+<4T)kvqqc{3fFm--jEiZ6q;wAERcZB!lEV>{g zHN#<*yZ=TeVO3xIDhq^7i_xn-# zGN}JEvj&=1UsMTA#g7E9FQ^T(Fq7Nud|fAHD9`XkCEOKYW2nn7nk3}b!gcv2wk%G2 z2UoXqjq9?qXLZ@JuTRFpvzb=m{apYp4l49F+!EGlR?NLhd?!?K`Y|QbW+xZ65-Nh4 z@d0Q?+}W~Ek#EB-zt*M56!_H4NAUdYM*|{yvL^QQhYD;evVVm{E) zq)AqAX%R?)lUpD>m;n!(p>eHyWm7h+sY57KaV2c}*vbN*v}XIW(rKQDxd=rOv(q#~hcQAzLY|Em zYCk!f#Z!8oP2`~qPT9;?o6c1l1_uW(*4w7>x*ubM@_f%Di}v(b*vSmOFP#A}B-3_` z>h0?_jLKO0nSzYaOm9gvD)<#z#HT%d`jY5J+xP_Sq_3+_>ig)iSp|?v_>?P0-KaX- zf&oBoG5hPY@`PYmFxEqR9T8~N*vPJ+-M*Js4|7l_*3O2Q>~6b&|02?L(Vgt4a=Mdb z+am>P-cu{;>?CgJrp@3%iI5oHU+(SC>cRNGrB~aXEh{N4U1prD`GO8KdhAV{bi6#T z{1Qtw9n6>U+}KJ;PG-`qKFl$-o+-7}cJ>utCQx&61xY#y!t+dK{(9Jr|9&@!G|#?L z{@5Jm^`B7HmKWRd;p=J{wD%_kaL-ASV9T3F)QRFic->W6Bm!ittWuA`=U?9`e$UR& z8{b)l3v^hy2!nsHm&lNtnLZt&0Z@`q-1yDd#>TB)hXMDuo^w`p#Jq{2 zm{FC+scbokKTa6L6$pk=<%m%YACgtaFFlBu^E3MRqk=5+8+Wj*7bjb4G`=V@UrR+-vs|KVO*FB7DoX5f47?y z=`djKF>DJxT@q$XV8xb-#n+Gh1AxDY?I~qA)cv6;b-`o#a`1t;D_iA`&u0(o2FT<= zW057s&|Qw3y>0gc^f2(36-ACEBM~^U`0wkI-2ZR za-?{<#T`7&&R=f%kq-BX8E49Olc&vkb3#{^tx1lkL&M_I&)35DKKsg**#-*mSK6eu z=b&?R*wjXVGI)^Bw7@}%{+HBy=)u<1bgS9*AUr%=Q0sbpDS0P@*WC}|x3(5%hwjKp>@rQ2 z@0Zgy7G9pao>29iey*@74W|GAIUp%2tWsyG8Lv3$0c4!9NkA1pFID@+-Vm*f<$ejB zk}ZCyxF}}AA8Z#Ze+G+-hW8c~et$Wce*3A#RJdOL^RNJPGgN|>?5YNo#BcI!e=Nj5 z@7@5Zg7SF_Hr167`w}Ood4I=|w)k4d8HE62Y?D;ts3=|bU{b$$$S_7J20eG=hHH@` z1qUDBW5sVaz`s3=UCF5>quiMq(c7%I;JhP0iu$Xe_S=U54gyft6^=wahf0zX_dBeD zg}pL$ASNm4*4Y|uI3fis2?oXld)u1pm9G0T91!aFbhYfh%IkG$@l;||HD^=)gfn4Q z!_RBn`{xU(V$pUK5trTJTvgMmPmR-7F}-?uZ4EkNw)XwCZhGCEJv$;0tEea&ez@51 zlF8hDd-nb9i5Y0Ny|c4;Bn;NB^fZ2^Zodt(u5oKAqdZJP5J#i=^gz~HivF~v2>r(2 zksA;q|5>8+%`y&1G9`8kfwS22ke#t%WrL`M;V;JRe!iZ*%)OkJ-o>M+rqNQz?9)>Ic4IMS&4gGSTJ@!w@M{T_D0R zGiiNK%!vZ9vf|p^m!GApOG<9W>()Q*2)6h=IzB&66)ETQT}6W`HPd81w}!kA{uo~C z!FYhd;0&W|&TlhfK9AGkOD;Tf#teSmyJ412}X zNd{(mXP^$f>ny$NL>U-yzEiR`iIywz#JQSuF5rhiWl6lrdOPU?^~TFsG<0>-Q0v&F z_c{p~+Jnz7x?mdNj3Q_ZwWuw0V2TXuV#!$&9pt}M$Wd>T7Wc)3%~qfkcs%Wn{Qf>@ zp;SIoAe#RJjDH2E&z4|l9}LvW{o@J9Ys0yi8-|_pIQ;HeOovvZ#cN#