Commit 7c693149 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Gas tracker page (#1524)

* add custom headers to API resource

* refactoring

* page title

* chart

* gas prices snippet

* GasPrice component

* add ENV for gas units

* dark theme and other tweaks

* tests

* add base and priority fee

* [skip ci] small fiat values

* change behavior of the variable

* update tooltip layout

* refactor gas info tooltip

* refactor GasPrice component

* fix logic and deploy demo for eth mainnet

* tests

* remove link from anchor

* [skip ci] rollback ENVs for demo

* review fixes

* fix tests
parent 5c2d06fc
import type { Feature } from './types';
import { GAS_UNITS } from 'types/client/gasTracker';
import type { GasUnit } from 'types/client/gasTracker';
import { getEnvValue, parseEnvJson } from '../utils';
const isDisabled = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_ENABLED') === 'false';
const units = ((): Array<GasUnit> => {
const envValue = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_UNITS');
if (!envValue) {
return [ 'usd', 'gwei' ];
}
const units = parseEnvJson<Array<GasUnit>>(envValue)?.filter((type) => GAS_UNITS.includes(type)) || [];
return units;
})();
const title = 'Gas tracker';
const config: Feature<{ units: Array<GasUnit> }> = (() => {
if (!isDisabled && units.length > 0) {
return Object.freeze({
title,
isEnabled: true,
units,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
......@@ -6,6 +6,7 @@ export { default as beaconChain } from './beaconChain';
export { default as bridgedTokens } from './bridgedTokens';
export { default as blockchainInteraction } from './blockchainInteraction';
export { default as csvExport } from './csvExport';
export { default as gasTracker } from './gasTracker';
export { default as googleAnalytics } from './googleAnalytics';
export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as growthBook } from './growthBook';
......
......@@ -49,7 +49,6 @@ const UI = Object.freeze({
background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND') || HOMEPAGE_PLATE_BACKGROUND_DEFAULT,
textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR') || 'white',
},
showGasTracker: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER') === 'false' ? false : true,
showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true,
},
views,
......
......@@ -25,7 +25,6 @@ NEXT_PUBLIC_API_BASE_PATH=/
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=
## sidebar
NEXT_PUBLIC_NETWORK_LOGO=
......
......@@ -25,7 +25,6 @@ NEXT_PUBLIC_API_BASE_PATH=/
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
## sidebar
## footer
NEXT_PUBLIC_GIT_TAG=v1.0.11
......
......@@ -12,6 +12,8 @@ import type { AdButlerConfig } from '../../../types/client/adButlerConfig';
import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS } from '../../../types/client/adProviders';
import type { AdTextProviders, AdBannerProviders } from '../../../types/client/adProviders';
import type { ContractCodeIde } from '../../../types/client/contract';
import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview } from '../../../types/client/marketplace';
import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items';
import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items';
......@@ -390,7 +392,6 @@ const schema = yup
.of(yup.string<ChainIndicatorId>().oneOf([ 'daily_txs', 'coin_price', 'market_cap', 'tvl' ])),
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(),
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(),
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER: yup.boolean(),
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(),
// b. sidebar
......@@ -493,6 +494,8 @@ const schema = yup
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
NEXT_PUBLIC_SWAP_BUTTON_URL: yup.string(),
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string<ValidatorsChainType>().oneOf(VALIDATORS_CHAIN_TYPE),
NEXT_PUBLIC_GAS_TRACKER_ENABLED: yup.boolean(),
NEXT_PUBLIC_GAS_TRACKER_UNITS: yup.array().transform(replaceQuotes).json().of(yup.string<GasUnit>().oneOf(GAS_UNITS)),
// 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
......
......@@ -19,8 +19,9 @@ NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff'
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)'
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_GAS_TRACKER_ENABLED=true
NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei']
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='<a href="#">Hello</a>'
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
......@@ -6,3 +6,4 @@
| NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK | `boolean` | Set to true for zkevm L2 solutions | Required | - | `true` | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE |
| 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_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 |
\ No newline at end of file
......@@ -28,6 +28,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Misc](ENVS.md#misc)
- [App features](ENVS.md#app-features)
- [My account](ENVS.md#my-account)
- [Gas tracker](ENVS.md#gas-tracker)
- [Address verification](ENVS.md#address-verification-in-my-account) in "My account"
- [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) (writing to contract, etc.)
- [Banner ads](ENVS.md#banner-ads)
......@@ -108,7 +109,6 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| 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_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_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` |
&nbsp;
......@@ -302,6 +302,17 @@ Settings for meta tags and OG tags
&nbsp;
### Gas tracker
This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_GAS_TRACKER_ENABLED=false`.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_GAS_TRACKER_ENABLED | `boolean` | Set to true to enable "Gas tracker" in the app | Required | `true` | `false` |
| NEXT_PUBLIC_GAS_TRACKER_UNITS | Array<`usd` \| `gwei`> | Array of units for displaying gas prices on the Gas Tracker page, in the stats snippet on the Home page, and in the top bar. The first value in the array will take priority over the second one in all mentioned views. If only one value is provided, gas prices will be displayed only in that unit. | - | `[ 'usd', 'gwei' ]` | `[ 'gwei' ]` |
&nbsp;
### Address verification in "My account"
*Note* all ENV variables required for [My account](ENVS.md#my-account) feature should be passed alongside the following ones:
......
<svg viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.121 1C3.131 1 .667 3.464.667 6.455V41H26.12V28.273h3.637v7.272c0 2.99 2.464 5.455 5.454 5.455 2.99 0 5.455-2.465 5.455-5.455V17.023a5.33 5.33 0 0 0-1.591-3.807l-8.58-8.58-2.557 2.557 5.17 5.17c-1.952.832-3.352 2.756-3.352 5 0 2.99 2.465 5.455 5.455 5.455.64 0 1.243-.135 1.818-.34v13.067c0 1.03-.788 1.819-1.818 1.819a1.786 1.786 0 0 1-1.818-1.819v-7.272c0-1.989-1.648-3.637-3.636-3.637H26.12V6.455c0-2.99-2.464-5.455-5.454-5.455H6.12Zm0 3.636h14.546c1.03 0 1.818.789 1.818 1.819v7.272H4.303V6.455c0-1.03.788-1.819 1.818-1.819Zm29.091 10.91c1.023 0 1.818.795 1.818 1.818a1.795 1.795 0 0 1-1.818 1.818 1.795 1.795 0 0 1-1.818-1.818c0-1.023.795-1.819 1.818-1.819ZM4.303 17.364h18.182v20H4.303v-20Z" fill="currentColor" stroke="currentColor"/>
</svg>
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.05 8.552c3.799-3.798 7.474-4.099 9.352-3.954.145 1.878-.156 5.553-3.954 9.352l-11.32 11.32-5.398-5.398 9.163-9.162.008-.01 2.15-2.148ZM22.817 5.32l-1.484 1.483H9.94a3.81 3.81 0 0 0-2.656 1.132l-6.166 6.147-.003.003a3.81 3.81 0 0 0 1.95 6.43h.002l7.302 1.464 7.653 7.653 1.463 7.301v.003a3.812 3.812 0 0 0 6.43 1.95l6.151-6.169a3.81 3.81 0 0 0 1.132-2.656V18.667l1.483-1.484C40.216 11.65 40.24 5.961 39.863 3.36A3.773 3.773 0 0 0 36.641.137c-2.602-.376-8.29-.353-13.824 5.182ZM5.358 16.31l5.388 1.08 6.015-6.015h-6.452l-4.95 4.935ZM22.61 29.255l1.08 5.387 4.935-4.951v-6.452l-6.015 6.015Zm-13.608-.278a2.286 2.286 0 1 0-2.29-3.958c-3.224 1.866-4.709 5.062-5.404 7.487a19.116 19.116 0 0 0-.697 4.184 11.72 11.72 0 0 0-.012.38v.044l2.286.001H.598a2.286 2.286 0 0 0 2.287 2.287v-2.287l.001 2.287h.045a9.419 9.419 0 0 0 .38-.013 19.11 19.11 0 0 0 4.184-.698c2.424-.694 5.62-2.179 7.486-5.404a2.286 2.286 0 0 0-3.958-2.29c-1.012 1.749-2.874 2.75-4.788 3.299-.244.07-.483.13-.714.183.053-.23.113-.47.183-.714.549-1.914 1.55-3.776 3.298-4.788Z" fill="currentColor"/>
</svg>
......@@ -108,6 +108,7 @@ export interface ApiResource {
basePath?: string;
pathParams?: Array<string>;
needAuth?: boolean; // for external APIs which require authentication
headers?: RequestInit['headers'];
}
export const SORTING_FIELDS = [ 'sort', 'order' ];
......@@ -184,7 +185,7 @@ export const RESOURCES = {
needAuth: true,
},
// STATS
// STATS MICROSERVICE API
stats_counters: {
path: '/api/v1/counters',
endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
......@@ -513,16 +514,21 @@ export const RESOURCES = {
filterFields: [],
},
// HOMEPAGE
homepage_stats: {
// APP STATS
stats: {
path: '/api/v2/stats',
headers: {
'updated-gas-oracle': 'true',
},
},
homepage_chart_txs: {
stats_charts_txs: {
path: '/api/v2/stats/charts/transactions',
},
homepage_chart_market: {
stats_charts_market: {
path: '/api/v2/stats/charts/market',
},
// HOMEPAGE
homepage_blocks: {
path: '/api/v2/main-page/blocks',
},
......@@ -750,9 +756,9 @@ Q extends 'watchlist' ? WatchlistResponse :
Q extends 'verified_addresses' ? VerifiedAddressResponse :
Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig :
Q extends 'token_info_applications' ? TokenInfoApplications :
Q extends 'homepage_stats' ? HomeStats :
Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse :
Q extends 'stats' ? HomeStats :
Q extends 'stats_charts_txs' ? ChartTransactionResponse :
Q extends 'stats_charts_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
......
......@@ -41,6 +41,7 @@ export default function useApiFetch() {
'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined,
Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined,
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
...resource.headers,
...fetchParams?.headers,
}, Boolean) as HeadersInit;
......
......@@ -219,8 +219,13 @@ export default function useNavItems(): ReturnType {
nextRoute: { pathname: '/contract-verification' as const },
isActive: pathname.startsWith('/contract-verification'),
},
config.features.gasTracker.isEnabled && {
text: 'Gas tracker',
nextRoute: { pathname: '/gas-tracker' as const },
isActive: pathname.startsWith('/gas-tracker'),
},
...config.UI.sidebar.otherLinks,
],
].filter(Boolean),
},
].filter(Boolean);
......
......@@ -43,6 +43,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/name-domains': 'Root page',
'/name-domains/[name]': 'Regular page',
'/validators': 'Root page',
'/gas-tracker': 'Root page',
// service routes, added only to make typescript happy
'/login': 'Regular page',
......
......@@ -46,6 +46,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/name-domains': DEFAULT_TEMPLATE,
'/name-domains/[name]': DEFAULT_TEMPLATE,
'/validators': DEFAULT_TEMPLATE,
'/gas-tracker': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy
'/login': DEFAULT_TEMPLATE,
......
......@@ -41,6 +41,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/name-domains': 'domains search and resolve',
'/name-domains/[name]': '%name% domain details',
'/validators': 'validators list',
'/gas-tracker': 'gas tracker',
// service routes, added only to make typescript happy
'/login': 'login',
......
......@@ -41,6 +41,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/name-domains': 'Domains search and resolve',
'/name-domains/[name]': 'Domain details',
'/validators': 'Validators list',
'/gas-tracker': 'Gas tracker',
// service routes, added only to make typescript happy
'/login': 'Login',
......
import type { HomeStats } from 'types/api/stats';
import _mapValues from 'lodash/mapValues';
export const base: HomeStats = {
export const base = {
average_block_time: 6212.0,
coin_price: '0.00199678',
coin_price_change_percentage: -7.42,
gas_prices: {
average: {
fiat_price: '1.01',
price: 20.41,
time: 12283,
fiat_price: '1.39',
price: 23.75,
time: 12030.25,
base_fee: 2.22222,
priority_fee: 12.424242,
},
fast: {
fiat_price: '1.26',
price: 25.47,
time: 9321,
fiat_price: '1.74',
price: 29.72,
time: 8763.25,
base_fee: 4.44444,
priority_fee: 22.242424,
},
slow: {
fiat_price: '0.97',
price: 19.55,
time: 24543,
fiat_price: '1.35',
price: 23.04,
time: 20100.25,
base_fee: 1.11111,
priority_fee: 7.8909,
},
},
gas_price_updated_at: '2022-11-11T11:09:49.051171Z',
......@@ -35,7 +41,22 @@ export const base: HomeStats = {
tvl: '1767425.102766552',
};
export const withBtcLocked: HomeStats = {
export const withBtcLocked = {
...base,
rootstock_locked_btc: '3337493406696977561374',
};
export const withoutFiatPrices = {
...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null),
};
export const withoutGweiPrices = {
...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null),
};
export const withoutBothPrices = {
...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null),
};
export const averageGasPrice = {
chart: [
{
date: '2023-12-22',
value: '37.7804422597599',
},
{
date: '2023-12-23',
value: '25.84889883009387',
},
{
date: '2023-12-24',
value: '25.818463227198574',
},
{
date: '2023-12-25',
value: '26.045513050051298',
},
{
date: '2023-12-26',
value: '21.42600692652399',
},
{
date: '2023-12-27',
value: '31.066730409846656',
},
{
date: '2023-12-28',
value: '33.63955781902089',
},
{
date: '2023-12-29',
value: '28.064736756058384',
},
{
date: '2023-12-30',
value: '23.074500869678175',
},
{
date: '2023-12-31',
value: '17.651005734615133',
},
{
date: '2024-01-01',
value: '14.906085174476441',
},
{
date: '2024-01-02',
value: '22.28459059038656',
},
{
date: '2024-01-03',
value: '39.8311646806592',
},
{
date: '2024-01-04',
value: '26.09989322256083',
},
{
date: '2024-01-05',
value: '22.821996688111998',
},
{
date: '2024-01-06',
value: '20.32680041262083',
},
{
date: '2024-01-07',
value: '32.535045831809704',
},
{
date: '2024-01-08',
value: '27.443477102139482',
},
{
date: '2024-01-09',
value: '20.7911332558055',
},
{
date: '2024-01-10',
value: '42.10740192523919',
},
{
date: '2024-01-11',
value: '35.75215680343582',
},
{
date: '2024-01-12',
value: '27.430414798093253',
},
{
date: '2024-01-13',
value: '20.170934096589875',
},
{
date: '2024-01-14',
value: '38.79660984371034',
},
{
date: '2024-01-15',
value: '26.140740484554204',
},
{
date: '2024-01-16',
value: '36.708543184194156',
},
{
date: '2024-01-17',
value: '40.325438794298876',
},
{
date: '2024-01-18',
value: '37.55145309930694',
},
{
date: '2024-01-19',
value: '33.271450114434664',
},
{
date: '2024-01-20',
value: '19.303304377685638',
},
{
date: '2024-01-21',
value: '14.375908594704976',
},
],
};
export const base = {
sections: [
{
id: 'accounts',
title: 'Accounts',
charts: [
{
id: 'accountsGrowth',
title: 'Accounts growth',
description: 'Cumulative accounts number per period',
units: null,
},
{
id: 'activeAccounts',
title: 'Active accounts',
description: 'Active accounts number per period',
units: null,
},
{
id: 'newAccounts',
title: 'New accounts',
description: 'New accounts number per day',
units: null,
},
],
},
{
id: 'transactions',
title: 'Transactions',
charts: [
{
id: 'averageTxnFee',
title: 'Average transaction fee',
description: 'The average amount in ETH spent per transaction',
units: 'ETH',
},
{
id: 'newTxns',
title: 'New transactions',
description: 'New transactions number',
units: null,
},
{
id: 'txnsFee',
title: 'Transactions fees',
description: 'Amount of tokens paid as fees',
units: 'ETH',
},
{
id: 'txnsGrowth',
title: 'Transactions growth',
description: 'Cumulative transactions number',
units: null,
},
{
id: 'txnsSuccessRate',
title: 'Transactions success rate',
description: 'Successful transactions rate per day',
units: null,
},
],
},
{
id: 'blocks',
title: 'Blocks',
charts: [
{
id: 'averageBlockRewards',
title: 'Average block rewards',
description: 'Average amount of distributed reward in tokens per day',
units: 'ETH',
},
{
id: 'averageBlockSize',
title: 'Average block size',
description: 'Average size of blocks in bytes',
units: 'Bytes',
},
{
id: 'newBlocks',
title: 'New blocks',
description: 'New blocks number',
units: null,
},
],
},
{
id: 'tokens',
title: 'Tokens',
charts: [
{
id: 'newNativeCoinTransfers',
title: 'New ETH transfers',
description: 'New token transfers number for the period',
units: null,
},
],
},
{
id: 'gas',
title: 'Gas',
charts: [
{
id: 'averageGasLimit',
title: 'Average gas limit',
description: 'Average gas limit per block for the period',
units: null,
},
{
id: 'averageGasPrice',
title: 'Average gas price',
description: 'Average gas price for the period (Gwei)',
units: 'Gwei',
},
{
id: 'gasUsedGrowth',
title: 'Gas used growth',
description: 'Cumulative gas used for the period',
units: null,
},
],
},
{
id: 'contracts',
title: 'Contracts',
charts: [
{
id: 'newVerifiedContracts',
title: 'New verified contracts',
description: 'New verified contracts number for the period',
units: null,
},
{
id: 'verifiedContractsGrowth',
title: 'Verified contracts growth',
description: 'Cumulative number verified contracts for the period',
units: null,
},
],
},
],
};
......@@ -191,3 +191,13 @@ export const validators: GetServerSideProps<Props> = async(context) => {
return base(context);
};
export const gasTracker: GetServerSideProps<Props> = async(context) => {
if (!config.features.gasTracker.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
......@@ -33,6 +33,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/contract-verification">
| StaticRoute<"/csv-export">
| StaticRoute<"/deposits">
| StaticRoute<"/gas-tracker">
| StaticRoute<"/graphiql">
| StaticRoute<"/">
| StaticRoute<"/login">
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const GasTracker = dynamic(() => import('ui/pages/GasTracker'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/gas-tracker">
<GasTracker/>
</PageNextJs>
);
};
export default Page;
export { gasTracker as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -3,6 +3,7 @@ import { devices } from '@playwright/test';
export const viewport = {
mobile: devices['iPhone 13 Pro'].viewport,
md: { width: 1001, height: 800 },
xl: { width: 1600, height: 1000 },
};
......
......@@ -49,6 +49,7 @@
| "filter"
| "finalized"
| "flame"
| "gas_xl"
| "gas"
| "gear"
| "globe-b"
......@@ -82,6 +83,7 @@
| "qr_code"
| "repeat_arrow"
| "restAPI"
| "rocket_xl"
| "rocket"
| "RPC"
| "scope"
......
......@@ -9,16 +9,22 @@ export const HOMEPAGE_STATS: HomeStats = {
fiat_price: '1.01',
price: 20.41,
time: 12283,
base_fee: 2.22222,
priority_fee: 12.424242,
},
fast: {
fiat_price: '1.26',
price: 25.47,
time: 9321,
base_fee: 4.44444,
priority_fee: 22.242424,
},
slow: {
fiat_price: '0.97',
price: 19.55,
time: 24543,
base_fee: 1.11111,
priority_fee: 7.8909,
},
},
gas_price_updated_at: '2022-11-11T11:09:49.051171Z',
......
......@@ -28,6 +28,8 @@ export interface GasPriceInfo {
fiat_price: string | null;
price: number | null;
time: number | null;
base_fee: number | null;
priority_fee: number | null;
}
export type Counters = {
......
export const GAS_UNITS = [
'usd',
'gwei',
] as const;
export type GasUnit = typeof GAS_UNITS[number];
......@@ -13,12 +13,7 @@ interface Props {
}
const BlocksTabSlot = ({ pagination }: Props) => {
const statsQuery = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
const statsQuery = useApiQuery('stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
......
import { Box, Flex, chakra, useBoolean } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery';
import { STATS_CHARTS } from 'stubs/stats';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import LinkInternal from 'ui/shared/LinkInternal';
import ChartWidgetContainer from 'ui/stats/ChartWidgetContainer';
const GAS_PRICE_CHART_ID = 'averageGasPrice';
const GasTrackerChart = () => {
const [ isChartLoadingError, setChartLoadingError ] = useBoolean(false);
const { data, isPlaceholderData, isError } = useApiQuery('stats_lines', {
queryOptions: {
placeholderData: STATS_CHARTS,
},
});
const content = (() => {
if (isPlaceholderData) {
return <ContentLoader/>;
}
if (isChartLoadingError || isError) {
return <DataFetchAlert/>;
}
const chart = data?.sections.map((section) => section.charts.find((chart) => chart.id === GAS_PRICE_CHART_ID)).filter(Boolean)?.[0];
if (!chart) {
return <DataFetchAlert/>;
}
return (
<ChartWidgetContainer
id={ GAS_PRICE_CHART_ID }
title={ chart.title }
description={ chart.description }
interval="oneMonth"
units={ chart.units || undefined }
isPlaceholderData={ isPlaceholderData }
onLoadingError={ setChartLoadingError.on }
h="320px"
/>
);
})();
return (
<Box>
<Flex justifyContent="space-between" alignItems="center" mb={ 6 }>
<chakra.h3 textStyle="h3">Gas price history</chakra.h3>
<LinkInternal href={ route({ pathname: '/stats', hash: 'gas' }) }>Charts & stats</LinkInternal>
</Flex>
{ content }
</Box>
);
};
export default React.memo(GasTrackerChart);
import { Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import { mdash } from 'lib/html-entities';
interface Props {
percentage: number;
isLoading: boolean;
}
const GasTrackerNetworkUtilization = ({ percentage, isLoading }: Props) => {
const load = (() => {
if (percentage > 80) {
return 'high';
}
if (percentage > 50) {
return 'medium';
}
return 'low';
})();
const colors = {
high: 'red.600',
medium: 'orange.600',
low: 'green.600',
};
const color = colors[load];
return (
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">
<span>Network utilization </span>
<chakra.span color={ color }>{ percentage.toFixed(2) }% { mdash } { load } load</chakra.span>
</Skeleton>
);
};
export default React.memo(GasTrackerNetworkUtilization);
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import * as configs from 'playwright/utils/configs';
import GasTrackerPriceSnippet from './GasTrackerPriceSnippet';
test.use({ viewport: configs.viewport.md });
const data = statsMock.base.gas_prices.fast;
const clip = { x: 0, y: 0, width: 334, height: 204 };
test('with usd as primary unit +@dark-mode', async({ mount, page }) => {
await mount(
<TestApp>
<GasTrackerPriceSnippet
data={ data }
type="fast"
isLoading={ false }
/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip });
});
test('loading state', async({ mount, page }) => {
await mount(
<TestApp>
<GasTrackerPriceSnippet
data={ data }
type="fast"
isLoading={ true }
/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip });
});
const gweiUnitsTest = test.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_GAS_TRACKER_UNITS', value: '["gwei","usd"]' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
gweiUnitsTest('with gwei as primary unit +@dark-mode', async({ mount, page }) => {
await mount(
<TestApp>
<GasTrackerPriceSnippet
data={ data }
type="slow"
isLoading={ false }
/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip });
});
import { Box, Flex, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { GasPriceInfo, GasPrices } from 'types/api/stats';
import { SECOND } from 'lib/consts';
import { asymp } from 'lib/html-entities';
import GasPrice from 'ui/shared/gas/GasPrice';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
type: keyof GasPrices;
data: GasPriceInfo;
isLoading: boolean;
}
const TITLES: Record<keyof GasPrices, string> = {
fast: 'Fast',
average: 'Normal',
slow: 'Slow',
};
const ICONS: Record<keyof GasPrices, IconName> = {
fast: 'rocket_xl',
average: 'gas_xl',
slow: 'gas_xl',
};
const GasTrackerPriceSnippet = ({ data, type, isLoading }: Props) => {
const bgColors = {
fast: 'transparent',
average: useColorModeValue('gray.50', 'whiteAlpha.200'),
slow: useColorModeValue('gray.50', 'whiteAlpha.200'),
};
return (
<Box
as="li"
listStyleType="none"
px={ 9 }
py={ 6 }
w={{ lg: 'calc(100% / 3)' }}
bgColor={ bgColors[type] }
>
<Skeleton textStyle="h3" display="inline-block" isLoaded={ !isLoading }>{ TITLES[type] }</Skeleton>
<Flex columnGap={ 3 } alignItems="center" mt={ 3 }>
<IconSvg name={ ICONS[type] } boxSize={{ base: '30px', xl: 10 }} isLoading={ isLoading } flexShrink={ 0 }/>
<Skeleton isLoaded={ !isLoading }>
<GasPrice data={ data } fontSize={{ base: '36px', xl: '48px' }} lineHeight="48px" fontWeight={ 600 } letterSpacing="-1px" fontFamily="heading"/>
</Skeleton>
</Flex>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" mt={ 3 } display="inline-block">
{ data.price && data.fiat_price && <GasPrice data={ data } prefix={ `${ asymp } ` } unitMode="secondary"/> }
<span> per transaction</span>
{ data.time && <span> / { (data.time / SECOND).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s</span> }
</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" mt={ 2 } display="inline-block" whiteSpace="pre">
{ data.base_fee && <span>Base { data.base_fee.toLocaleString(undefined, { maximumFractionDigits: 0 }) }</span> }
{ data.base_fee && data.priority_fee && <span> / </span> }
{ data.priority_fee && <span>Priority { data.priority_fee.toLocaleString(undefined, { maximumFractionDigits: 0 }) }</span> }
</Skeleton>
</Box>
);
};
export default React.memo(GasTrackerPriceSnippet);
import { Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { GasPrices } from 'types/api/stats';
import GasTrackerPriceSnippet from './GasTrackerPriceSnippet';
interface Props {
prices: GasPrices;
isLoading: boolean;
}
const GasTrackerPrices = ({ prices, isLoading }: Props) => {
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.300');
return (
<Flex
as="ul"
flexDir={{ base: 'column', lg: 'row' }}
borderColor={ borderColor }
borderWidth="2px"
borderRadius="xl"
overflow="hidden"
sx={{
'li:not(:last-child)': {
borderColor: borderColor,
borderRightWidth: { lg: '2px' },
borderBottomWidth: { base: '2px', lg: '0' },
},
}}
>
{ prices.fast && <GasTrackerPriceSnippet type="fast" data={ prices.fast } isLoading={ isLoading }/> }
{ prices.average && <GasTrackerPriceSnippet type="average" data={ prices.average } isLoading={ isLoading }/> }
{ prices.slow && <GasTrackerPriceSnippet type="slow" data={ prices.slow } isLoading={ isLoading }/> }
</Flex>
);
};
export default React.memo(GasTrackerPrices);
......@@ -11,7 +11,7 @@ import * as configs from 'playwright/utils/configs';
import LatestBlocks from './LatestBlocks';
const STATS_API_URL = buildApiUrl('homepage_stats');
const STATS_API_URL = buildApiUrl('stats');
const BLOCKS_API_URL = buildApiUrl('homepage_blocks');
export const test = base.extend<socketServer.SocketServerFixture>({
......
......@@ -36,12 +36,7 @@ const LatestBlocks = () => {
});
const queryClient = useQueryClient();
const statsQueryResult = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
const statsQueryResult = useApiQuery('stats', {
queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS,
......
......@@ -10,7 +10,7 @@ import * as configs from 'playwright/utils/configs';
import Stats from './Stats';
const API_URL = buildApiUrl('homepage_stats');
const API_URL = buildApiUrl('stats');
test.describe('all items', () => {
let component: Locator;
......@@ -69,7 +69,7 @@ test.describe('3 items', () => {
const extendedTest = test.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME', value: 'false' },
{ name: 'NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER', value: 'false' },
{ name: 'NEXT_PUBLIC_GAS_TRACKER_ENABLED', value: 'false' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
......
......@@ -7,23 +7,19 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts';
import { currencyUnits } from 'lib/units';
import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltipContent from 'ui/shared/GasInfoTooltipContent/GasInfoTooltipContent';
import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice';
import IconSvg from 'ui/shared/IconSvg';
import StatsItem from './StatsItem';
const hasGasTracker = config.UI.homepage.showGasTracker;
const hasGasTracker = config.features.gasTracker.isEnabled;
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
const rollupFeature = config.features.rollup;
const Stats = () => {
const { data, isPlaceholderData, isError, dataUpdatedAt } = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
const { data, isPlaceholderData, isError, dataUpdatedAt } = useApiQuery('stats', {
queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS,
......@@ -53,19 +49,21 @@ const Stats = () => {
!data.gas_prices && itemsCount--;
data.rootstock_locked_btc && itemsCount++;
const isOdd = Boolean(itemsCount % 2);
const gasLabel = hasGasTracker && data.gas_prices ? <GasInfoTooltipContent data={ data } dataUpdatedAt={ dataUpdatedAt }/> : null;
const gasPriceText = (() => {
if (data.gas_prices?.average?.fiat_price) {
return `$${ data.gas_prices.average.fiat_price }`;
}
if (data.gas_prices?.average?.price) {
return `${ data.gas_prices.average.price.toLocaleString() } ${ currencyUnits.gwei }`;
}
return 'N/A';
})();
const gasInfoTooltip = hasGasTracker && data.gas_prices ? (
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt }>
<IconSvg
isLoading={ isPlaceholderData }
name="info"
boxSize={ 5 }
display="block"
cursor="pointer"
_hover={{ color: 'link_hovered' }}
position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
right="10px"
/>
</GasInfoTooltip>
) : null;
content = (
<>
......@@ -112,9 +110,9 @@ const Stats = () => {
<StatsItem
icon="gas"
title="Gas tracker"
value={ gasPriceText }
value={ <GasPrice data={ data.gas_prices.average }/> }
_last={ isOdd ? lastItemTouchStyle : undefined }
tooltipLabel={ gasLabel }
tooltip={ gasInfoTooltip }
isLoading={ isPlaceholderData }
/>
) }
......
import type { SystemStyleObject, TooltipProps } from '@chakra-ui/react';
import { Skeleton, Flex, useColorModeValue, chakra, LightMode } from '@chakra-ui/react';
import type { SystemStyleObject } from '@chakra-ui/react';
import { Skeleton, Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
import breakpoints from 'theme/foundations/breakpoints';
import Hint from 'ui/shared/Hint';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
type Props = {
icon: IconName;
title: string;
value: string;
value: string | React.ReactNode;
className?: string;
tooltipLabel?: React.ReactNode;
tooltip?: React.ReactNode;
url?: string;
isLoading?: boolean;
}
const LARGEST_BREAKPOINT = '1240px';
const TOOLTIP_PROPS: Partial<TooltipProps> = {
hasArrow: false,
borderRadius: 'md',
placement: 'bottom-end',
offset: [ 0, 0 ],
bgColor: 'blackAlpha.900',
};
const StatsItem = ({ icon, title, value, className, tooltipLabel, url, isLoading }: Props) => {
const StatsItem = ({ icon, title, value, className, tooltip, url, isLoading }: Props) => {
const sxContainer: SystemStyleObject = {
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { flexDirection: 'column' },
};
......@@ -38,7 +29,6 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url, isLoading
const bgColor = useColorModeValue('blue.50', 'blue.800');
const loadingBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const infoColor = useColorModeValue('gray.600', 'gray.400');
return (
<Flex
......@@ -68,22 +58,10 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url, isLoading
<span>{ title }</span>
</Skeleton>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') } borderRadius="base">
<span>{ value }</span>
{ typeof value === 'string' ? <span>{ value }</span> : value }
</Skeleton>
</Flex>
{ tooltipLabel && !isLoading && (
<LightMode>
<Hint
label={ tooltipLabel }
tooltipProps={ TOOLTIP_PROPS }
boxSize={ 6 }
color={ infoColor }
position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
right="10px"
/>
</LightMode>
) }
{ tooltip }
</Flex>
);
};
......
......@@ -10,8 +10,8 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import ChainIndicators from './ChainIndicators';
const STATS_API_URL = buildApiUrl('homepage_stats');
const TX_CHART_API_URL = buildApiUrl('homepage_chart_txs');
const STATS_API_URL = buildApiUrl('stats');
const TX_CHART_API_URL = buildApiUrl('stats_charts_txs');
const test = base.extend({
context: contextWithEnvs([
......
......@@ -30,12 +30,7 @@ const ChainIndicators = () => {
const indicator = indicators.find(({ id }) => id === selectedIndicator);
const queryResult = useFetchChartData(indicator);
const statsQueryResult = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
const statsQueryResult = useApiQuery('stats', {
queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS,
......
......@@ -4,7 +4,7 @@ import type { TimeChartData } from 'ui/shared/chart/types';
import type { ResourcePayload } from 'lib/api/resources';
export type ChartsResources = 'homepage_chart_txs' | 'homepage_chart_market';
export type ChartsResources = 'stats_charts_txs' | 'stats_charts_market';
export interface TChainIndicator<R extends ChartsResources> {
id: ChainIndicatorId;
......
......@@ -8,7 +8,7 @@ import type { ResourcePayload } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
export default function useFetchChartData<R extends ChartsResources>(indicator: TChainIndicator<R> | undefined): UseQueryResult<TimeChartData> {
const queryResult = useApiQuery(indicator?.api.resourceName || 'homepage_chart_txs', {
const queryResult = useApiQuery(indicator?.api.resourceName || 'stats_charts_txs', {
queryOptions: { enabled: Boolean(indicator) },
});
......
......@@ -7,14 +7,14 @@ import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import IconSvg from 'ui/shared/IconSvg';
const dailyTxsIndicator: TChainIndicator<'homepage_chart_txs'> = {
const dailyTxsIndicator: TChainIndicator<'stats_charts_txs'> = {
id: 'daily_txs',
title: 'Daily transactions',
value: (stats) => Number(stats.transactions_today).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }),
icon: <IconSvg name="transactions" boxSize={ 6 } bgColor="#56ACD1" borderRadius="base" color="white"/>,
hint: `Number of transactions yesterday (0:00 - 23:59 UTC). The chart displays daily transactions for the past 30 days.`,
api: {
resourceName: 'homepage_chart_txs',
resourceName: 'stats_charts_txs',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: item.tx_count }))
......@@ -33,14 +33,14 @@ const nativeTokenData = {
type: 'ERC-20' as const,
};
const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
const coinPriceIndicator: TChainIndicator<'stats_charts_market'> = {
id: 'coin_price',
title: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } price`,
value: (stats) => '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
icon: <TokenEntity.Icon token={ nativeTokenData } boxSize={ 6 } marginRight={ 0 }/>,
hint: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } token daily price in USD.`,
api: {
resourceName: 'homepage_chart_market',
resourceName: 'stats_charts_market',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
......@@ -51,7 +51,7 @@ const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
},
};
const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
const marketPriceIndicator: TChainIndicator<'stats_charts_market'> = {
id: 'market_cap',
title: 'Market cap',
value: (stats) => '$' + Number(stats.market_cap).toLocaleString(undefined, { maximumFractionDigits: 0, notation: 'compact' }),
......@@ -59,7 +59,7 @@ const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
// 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: 'homepage_chart_market',
resourceName: 'stats_charts_market',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => (
......@@ -74,7 +74,7 @@ const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
},
};
const tvlIndicator: TChainIndicator<'homepage_chart_market'> = {
const tvlIndicator: TChainIndicator<'stats_charts_market'> = {
id: 'tvl',
title: 'Total value locked',
value: (stats) => '$' + Number(stats.tvl).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }),
......@@ -82,7 +82,7 @@ const tvlIndicator: TChainIndicator<'homepage_chart_market'> = {
// eslint-disable-next-line max-len
hint: 'Total value of digital assets locked or staked in a chain',
api: {
resourceName: 'homepage_chart_market',
resourceName: 'stats_charts_market',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => (
......
......@@ -13,7 +13,7 @@ import * as configs from 'playwright/utils/configs';
import Blocks from './Blocks';
const BLOCKS_API_URL = buildApiUrl('blocks') + '?type=block';
const STATS_API_URL = buildApiUrl('homepage_stats');
const STATS_API_URL = buildApiUrl('stats');
const hooksConfig = {
router: {
query: { tab: 'blocks' },
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as statsMock from 'mocks/stats/index';
import * as statsLineMock from 'mocks/stats/line';
import * as statsLinesMock from 'mocks/stats/lines';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import GasTracker from './GasTracker';
const STATS_LINES_API_URL = buildApiUrl('stats_lines');
const GAS_PRICE_CHART_API_URL = buildApiUrl('stats_line', { id: 'averageGasPrice' }) + '?**';
const STATS_API_URL = buildApiUrl('stats');
test('base view +@dark-mode +@mobile', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ ...statsMock.base, coin_price: '2442.789' }),
}));
await page.route(STATS_LINES_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsLinesMock.base),
}));
await page.route(GAS_PRICE_CHART_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsLineMock.averageGasPrice),
}));
const component = await mount(
<TestApp>
<GasTracker/>
</TestApp>,
);
await page.waitForResponse(GAS_PRICE_CHART_API_URL);
await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Averagegasprice-small"]')?.getAttribute('opacity') === '1';
});
await expect(component).toHaveScreenshot();
});
import { Box, Flex, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import { HOMEPAGE_STATS } from 'stubs/stats';
import GasTrackerChart from 'ui/gasTracker/GasTrackerChart';
import GasTrackerNetworkUtilization from 'ui/gasTracker/GasTrackerNetworkUtilization';
import GasTrackerPrices from 'ui/gasTracker/GasTrackerPrices';
import GasInfoUpdateTimer from 'ui/shared/gas/GasInfoUpdateTimer';
import PageTitle from 'ui/shared/Page/PageTitle';
const GasTracker = () => {
const { data, isPlaceholderData, isError, error, dataUpdatedAt } = useApiQuery('stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
refetchOnMount: false,
},
});
if (isError) {
throw new Error(undefined, { cause: error });
}
const isLoading = isPlaceholderData;
const titleSecondRow = (
<Flex
alignItems={{ base: 'flex-start', lg: 'center' }}
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
w="100%"
columnGap={ 3 }
rowGap={ 1 }
flexDir={{ base: 'column', lg: 'row' }}
>
{ data?.network_utilization_percentage && <GasTrackerNetworkUtilization percentage={ data.network_utilization_percentage } isLoading={ isLoading }/> }
{ data?.gas_price_updated_at && (
<Skeleton isLoaded={ !isLoading } whiteSpace="pre" display="flex" alignItems="center">
<span>Last updated </span>
<chakra.span color="text_secondary">{ dayjs(data.gas_price_updated_at).format('DD MMM, HH:mm:ss') }</chakra.span>
{ data.gas_prices_update_in !== 0 && (
<GasInfoUpdateTimer
key={ dataUpdatedAt }
startTime={ dataUpdatedAt }
duration={ data.gas_prices_update_in }
size={ 5 }
ml={ 2 }
/>
) }
</Skeleton>
) }
{ data?.coin_price && (
<Skeleton isLoaded={ !isLoading } ml={{ base: 0, lg: 'auto' }} whiteSpace="pre">
<chakra.span color="text_secondary">{ config.chain.currency.symbol }</chakra.span>
<span> ${ Number(data.coin_price).toLocaleString(undefined, { maximumFractionDigits: 2 }) }</span>
</Skeleton>
) }
</Flex>
);
return (
<>
<PageTitle
title="Gas tracker"
secondRow={ titleSecondRow }
withTextAd
/>
{ data?.gas_prices && <GasTrackerPrices prices={ data.gas_prices } isLoading={ isLoading }/> }
<Box mt={ 12 }>
<GasTrackerChart/>
</Box>
</>
);
};
export default GasTracker;
......@@ -17,7 +17,7 @@ test.describe('default view', () => {
let component: Locator;
test.beforeEach(async({ page, mount }) => {
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
await page.route(buildApiUrl('stats'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
......@@ -36,7 +36,7 @@ test.describe('default view', () => {
txMock.withTokenTransfer,
]),
}));
await page.route(buildApiUrl('homepage_chart_txs'), (route) => route.fulfill({
await page.route(buildApiUrl('stats_charts_txs'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(dailyTxsMock.base),
}));
......@@ -104,7 +104,7 @@ test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ mount, page }) => {
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
await page.route(buildApiUrl('stats'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
......@@ -123,7 +123,7 @@ test.describe('mobile', () => {
txMock.withTokenTransfer,
]),
}));
await page.route(buildApiUrl('homepage_chart_txs'), (route) => route.fulfill({
await page.route(buildApiUrl('stats_charts_txs'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(dailyTxsMock.base),
}));
......
import { GridItem, chakra } from '@chakra-ui/react';
import React from 'react';
import type { GasPriceInfo } from 'types/api/stats';
import { asymp, space } from 'lib/html-entities';
import { currencyUnits } from 'lib/units';
interface Props {
name: string;
info: GasPriceInfo | null;
}
const GasInfoRow = ({ name, info }: Props) => {
const content = (() => {
if (!info || info.price === null) {
return 'N/A';
}
return (
<>
<span>{ info.fiat_price ? `$${ info.fiat_price }` : `${ info.price } ${ currencyUnits.gwei }` }</span>
{ info.time && (
<chakra.span color="text_secondary">
{ space }per tx { asymp } { (info.time / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s
</chakra.span>
) }
</>
);
})();
return (
<>
<GridItem color="blue.100">{ name }</GridItem>
<GridItem color="text" textAlign="right">{ content }</GridItem>
</>
);
};
export default React.memo(GasInfoRow);
import { DarkMode, Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import type { HomeStats } from 'types/api/stats';
import dayjs from 'lib/date/dayjs';
import GasInfoRow from './GasInfoRow';
import GasInfoUpdateTimer from './GasInfoUpdateTimer';
interface Props {
data: HomeStats;
dataUpdatedAt: number;
}
const GasInfoTooltipContent = ({ data, dataUpdatedAt }: Props) => {
if (!data.gas_prices) {
return null;
}
return (
<DarkMode>
<Grid templateColumns="repeat(2, max-content)" rowGap={ 2 } columnGap={ 4 } padding={ 4 } fontSize="xs" lineHeight={ 4 }>
{ data.gas_price_updated_at && (
<>
<GridItem color="text_secondary">Last update</GridItem>
<GridItem color="text_secondary" display="flex" justifyContent="flex-end" columnGap={ 2 }>
{ dayjs(data.gas_price_updated_at).format('MMM DD, HH:mm:ss') }
{ data.gas_prices_update_in !== 0 &&
<GasInfoUpdateTimer key={ dataUpdatedAt } startTime={ dataUpdatedAt } duration={ data.gas_prices_update_in }/> }
</GridItem>
</>
) }
<GasInfoRow name="Slow" info={ data.gas_prices.slow }/>
<GasInfoRow name="Normal" info={ data.gas_prices.average }/>
<GasInfoRow name="Fast" info={ data.gas_prices.fast }/>
</Grid>
</DarkMode>
);
};
export default React.memo(GasInfoTooltipContent);
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { SECOND } from 'lib/consts';
import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import GasInfoTooltip from './GasInfoTooltip';
import GasPrice from './GasPrice';
const dataUpdatedAt = Date.now() - 30 * SECOND;
test.use({ viewport: { width: 300, height: 300 } });
test('all data', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.base } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas <GasPrice data={ statsMock.base.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
test('without primary unit price', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.withoutFiatPrices } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas: <GasPrice data={ statsMock.withoutFiatPrices.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
test('without secondary unit price', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.withoutGweiPrices } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas: <GasPrice data={ statsMock.withoutGweiPrices.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
test('no data', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.withoutBothPrices } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas: <GasPrice data={ statsMock.withoutBothPrices.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
const oneUnitTest = test.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_GAS_TRACKER_UNITS', value: '["gwei"]' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
oneUnitTest.describe('one unit', () => {
oneUnitTest('with data', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.withoutFiatPrices } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas: <GasPrice data={ statsMock.withoutFiatPrices.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
oneUnitTest('without data', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.withoutGweiPrices } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas: <GasPrice data={ statsMock.withoutGweiPrices.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
});
import { Box, DarkMode, Flex, Grid, Popover, PopoverBody, PopoverContent, PopoverTrigger, Portal, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { HomeStats } from 'types/api/stats';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import LinkInternal from 'ui/shared/LinkInternal';
import GasInfoTooltipRow from './GasInfoTooltipRow';
import GasInfoUpdateTimer from './GasInfoUpdateTimer';
interface Props {
children: React.ReactNode;
data: HomeStats;
dataUpdatedAt: number;
isOpen?: boolean; // for testing purposes only; the tests were flaky, i couldn't find a better way
}
const POPOVER_OFFSET: [ number, number ] = [ 0, 10 ];
const feature = config.features.gasTracker;
const GasInfoTooltip = ({ children, data, dataUpdatedAt, isOpen }: Props) => {
const tooltipBg = useColorModeValue('gray.700', 'gray.900');
if (!data.gas_prices) {
return null;
}
const columnNum =
Object.values(data.gas_prices).some((price) => price?.fiat_price) &&
Object.values(data.gas_prices).some((price) => price?.price) &&
feature.isEnabled && feature.units.length === 2 ?
3 : 2;
return (
<Popover trigger="hover" isLazy offset={ POPOVER_OFFSET } isOpen={ isOpen }>
<PopoverTrigger>
{ children }
</PopoverTrigger>
<Portal>
<PopoverContent bgColor={ tooltipBg } w="auto">
<PopoverBody color="white">
<DarkMode>
<Flex flexDir="column" fontSize="xs" lineHeight={ 4 } rowGap={ 3 }>
{ data.gas_price_updated_at && (
<Flex justifyContent="space-between">
<Box color="text_secondary">Last update</Box>
<Flex color="text_secondary" justifyContent="flex-end" columnGap={ 2 } ml={ 3 }>
{ dayjs(data.gas_price_updated_at).format('MMM DD, HH:mm:ss') }
{ data.gas_prices_update_in !== 0 &&
<GasInfoUpdateTimer key={ dataUpdatedAt } startTime={ dataUpdatedAt } duration={ data.gas_prices_update_in }/> }
</Flex>
</Flex>
) }
<Grid rowGap={ 2 } columnGap="10px" gridTemplateColumns={ `repeat(${ columnNum }, minmax(min-content, auto))` }>
<GasInfoTooltipRow name="Fast" info={ data.gas_prices.fast }/>
<GasInfoTooltipRow name="Normal" info={ data.gas_prices.average }/>
<GasInfoTooltipRow name="Slow" info={ data.gas_prices.slow }/>
</Grid>
<LinkInternal href={ route({ pathname: '/gas-tracker' }) }>
Gas tracker overview
</LinkInternal>
</Flex>
</DarkMode>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
};
export default React.memo(GasInfoTooltip);
import { Box, chakra } from '@chakra-ui/react';
import React from 'react';
import type { GasPriceInfo } from 'types/api/stats';
import { space } from 'lib/html-entities';
import GasPrice from 'ui/shared/gas/GasPrice';
interface Props {
name: string;
info: GasPriceInfo | null;
}
const GasInfoTooltipRow = ({ name, info }: Props) => {
return (
<>
<Box>
<chakra.span>{ name }</chakra.span>
{ info && info.time && (
<chakra.span color="text_secondary">
{ space }{ (info.time / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s
</chakra.span>
) }
</Box>
<GasPrice data={ info } textAlign="right"/>
<GasPrice data={ info } unitMode="secondary" color="text_secondary" textAlign="right"/>
</>
);
};
export default React.memo(GasInfoTooltipRow);
import { CircularProgress } from '@chakra-ui/react';
import { CircularProgress, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import dayjs from 'lib/date/dayjs';
......@@ -6,6 +6,8 @@ import dayjs from 'lib/date/dayjs';
interface Props {
startTime: number;
duration: number;
className?: string;
size?: number;
}
const getValue = (startDate: dayjs.Dayjs, duration: number) => {
......@@ -20,8 +22,9 @@ const getValue = (startDate: dayjs.Dayjs, duration: number) => {
return value;
};
const GasInfoUpdateTimer = ({ startTime, duration }: Props) => {
const GasInfoUpdateTimer = ({ startTime, duration, className, size = 4 }: Props) => {
const [ value, setValue ] = React.useState(getValue(dayjs(startTime), duration));
const trackColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.100');
React.useEffect(() => {
const startDate = dayjs(startTime);
......@@ -39,7 +42,7 @@ const GasInfoUpdateTimer = ({ startTime, duration }: Props) => {
};
}, [ startTime, duration ]);
return <CircularProgress value={ value } trackColor="whiteAlpha.100" size={ 4 }/>;
return <CircularProgress className={ className } value={ value } trackColor={ trackColor } size={ size }/>;
};
export default React.memo(GasInfoUpdateTimer);
export default React.memo(chakra(GasInfoUpdateTimer));
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { GasPriceInfo } from 'types/api/stats';
import type { GasUnit } from 'types/client/gasTracker';
import config from 'configs/app';
import formatGasValue from './formatGasValue';
const feature = config.features.gasTracker;
const UNITS_TO_API_FIELD_MAP: Record<GasUnit, 'price' | 'fiat_price'> = {
gwei: 'price',
usd: 'fiat_price',
};
interface Props {
data: GasPriceInfo | null;
className?: string;
unitMode?: 'primary' | 'secondary';
prefix?: string;
}
const GasPrice = ({ data, prefix, className, unitMode = 'primary' }: Props) => {
if (!data || !feature.isEnabled) {
return null;
}
switch (unitMode) {
case 'secondary': {
const primaryUnits = feature.units[0];
const secondaryUnits = feature.units[1];
if (!secondaryUnits) {
return null;
}
const primaryUnitsValue = data[UNITS_TO_API_FIELD_MAP[primaryUnits]];
if (!primaryUnitsValue) {
// in this case we display values in secondary untis in primary mode as fallback
return null;
}
const secondaryUnitsValue = data[UNITS_TO_API_FIELD_MAP[secondaryUnits]];
if (!secondaryUnitsValue) {
return null;
}
const formattedValue = formatGasValue(data, secondaryUnits);
return <span className={ className }>{ prefix }{ formattedValue }</span>;
}
case 'primary': {
const primaryUnits = feature.units[0];
const secondaryUnits = feature.units[1];
if (!primaryUnits) {
// this should never happen since feature will be disabled if there are no units at all
return null;
}
const value = data[UNITS_TO_API_FIELD_MAP[primaryUnits]];
if (!value) {
// in primary mode we want to fallback to secondary units if value in primary units are not available
// unless there are no secondary units
const valueInSecondaryUnits = data[UNITS_TO_API_FIELD_MAP[secondaryUnits]];
if (!secondaryUnits || !valueInSecondaryUnits) {
// in primary mode we always want to show something
// this will return "N/A <units>"
return <span className={ className }>{ formatGasValue(data, primaryUnits) }</span>;
} else {
return <span className={ className }>{ prefix }{ formatGasValue(data, secondaryUnits) }</span>;
}
}
return <span className={ className }>{ prefix }{ formatGasValue(data, primaryUnits) }</span>;
}
}
};
export default chakra(GasPrice);
import type { GasPriceInfo } from 'types/api/stats';
import type { GasUnit } from 'types/client/gasTracker';
import { currencyUnits } from 'lib/units';
export default function formatGasValue(data: GasPriceInfo, unit: GasUnit) {
switch (unit) {
case 'gwei': {
if (!data.price) {
return `N/A ${ currencyUnits.gwei }`;
}
return `${ Number(data.price).toLocaleString(undefined, { maximumFractionDigits: 1 }) } ${ currencyUnits.gwei }`;
}
case 'usd': {
if (!data.fiat_price) {
return `$N/A`;
}
if (Number(data.fiat_price) < 0.01) {
return `< $0.01`;
}
return `$${ Number(data.fiat_price).toLocaleString(undefined, { maximumFractionDigits: 2 }) }`;
}
}
}
......@@ -16,7 +16,7 @@ const test = base.extend({
});
test('default view +@dark-mode +@mobile', async({ mount, page }) => {
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
await page.route(buildApiUrl('stats'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
......@@ -27,7 +27,7 @@ test('default view +@dark-mode +@mobile', async({ mount, page }) => {
</TestApp>,
);
await component.getByText(/\$1\.01/).hover();
await component.getByText(/\$1\.39/).click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } });
await component.getByLabel('color mode switch').click();
......
import { Flex, Link, Skeleton, Tooltip, chakra, useDisclosure } from '@chakra-ui/react';
import { Flex, Link, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import { currencyUnits } from 'lib/units';
import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltipContent from 'ui/shared/GasInfoTooltipContent/GasInfoTooltipContent';
import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice';
import TextSeparator from 'ui/shared/TextSeparator';
const TopBarStats = () => {
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
const handleClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
onToggle();
}, [ onToggle ]);
const { data, isPlaceholderData, isError, refetch, dataUpdatedAt } = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
const { data, isPlaceholderData, isError, refetch, dataUpdatedAt } = useApiQuery('stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
refetchOnMount: false,
......@@ -76,28 +63,15 @@ const TopBarStats = () => {
) }
</Flex>
) }
{ data?.coin_price && config.UI.homepage.showGasTracker && <TextSeparator color="divider"/> }
{ data?.gas_prices && data.gas_prices.average !== null && config.UI.homepage.showGasTracker && (
{ data?.coin_price && config.features.gasTracker.isEnabled && <TextSeparator color="divider"/> }
{ data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled && (
<Skeleton isLoaded={ !isPlaceholderData }>
<chakra.span color="text_secondary">Gas </chakra.span>
<Tooltip
label={ <GasInfoTooltipContent data={ data } dataUpdatedAt={ dataUpdatedAt }/> }
hasArrow={ false }
borderRadius="md"
offset={ [ 0, 16 ] }
bgColor="blackAlpha.900"
p={ 0 }
isOpen={ isOpen }
>
<Link
_hover={{ textDecoration: 'none', color: 'link_hovered' }}
onClick={ handleClick }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
>
{ data.gas_prices.average.fiat_price ? `$${ data.gas_prices.average.fiat_price }` : `${ data.gas_prices.average.price } ${ currencyUnits.gwei }` }
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt } >
<Link>
<GasPrice data={ data.gas_prices.average }/>
</Link>
</Tooltip>
</GasInfoTooltip>
</Skeleton>
) }
</Flex>
......
import { chakra } from '@chakra-ui/react';
import React, { useEffect, useMemo } from 'react';
import type { StatsIntervalIds } from 'types/client/stats';
......@@ -15,13 +16,14 @@ type Props = {
interval: StatsIntervalIds;
onLoadingError: () => void;
isPlaceholderData: boolean;
className?: string;
}
function formatDate(date: Date) {
return date.toISOString().substring(0, 10);
}
const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units, isPlaceholderData }: Props) => {
const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units, isPlaceholderData, className }: Props) => {
const selectedInterval = STATS_INTERVALS[interval];
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
......@@ -58,8 +60,9 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
description={ description }
isLoading={ isPending }
minH="230px"
className={ className }
/>
);
};
export default ChartWidgetContainer;
export default chakra(ChartWidgetContainer);
import type { TooltipProps } from '@chakra-ui/react';
import { Box, Grid, Heading, List, ListItem, Skeleton } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
......@@ -8,18 +7,12 @@ import type { StatsIntervalIds } from 'types/client/stats';
import useApiQuery from 'lib/api/useApiQuery';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
import GasInfoTooltipContent from 'ui/shared/GasInfoTooltipContent/GasInfoTooltipContent';
import Hint from 'ui/shared/Hint';
import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import IconSvg from 'ui/shared/IconSvg';
import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert';
import ChartWidgetContainer from './ChartWidgetContainer';
const GAS_TOOLTIP_PROPS: Partial<TooltipProps> = {
borderRadius: 'md',
hasArrow: false,
padding: 0,
};
type Props = {
filterQuery: string;
isError: boolean;
......@@ -33,12 +26,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0);
const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed;
const homeStatsQuery = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
const homeStatsQuery = useApiQuery('stats', {
queryOptions: {
refetchOnMount: false,
},
......@@ -72,15 +60,14 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
marginBottom: 0,
}}
>
<Skeleton isLoaded={ !isPlaceholderData } mb={ 4 } display="inline-flex" alignItems="center" columnGap={ 2 }>
<Skeleton isLoaded={ !isPlaceholderData } mb={ 4 } display="inline-flex" alignItems="center" columnGap={ 2 } id={ section.id }>
<Heading size="md" >
{ section.title }
</Heading>
{ section.id === 'gas' && homeStatsQuery.data && homeStatsQuery.data.gas_prices && (
<Hint
label={ <GasInfoTooltipContent data={ homeStatsQuery.data } dataUpdatedAt={ homeStatsQuery.dataUpdatedAt }/> }
tooltipProps={ GAS_TOOLTIP_PROPS }
/>
<GasInfoTooltip data={ homeStatsQuery.data } dataUpdatedAt={ homeStatsQuery.dataUpdatedAt }>
<IconSvg name="info" boxSize={ 5 } display="block" cursor="pointer" _hover={{ color: 'link_hovered' }}/>
</GasInfoTooltip>
) }
</Skeleton>
......
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