Commit 74561d60 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Validators list (#1621)

* new ENV and page placeholder

* display list data

* comment out SearchInput

* add counters

* add tag to address page

* tests

* refactoring

* fix ts

* change ENVs for demo

* fix ENVs validator

* [skip ci] remove hash

* review fixes
parent b7b891d9
...@@ -22,5 +22,6 @@ export { default as suave } from './suave'; ...@@ -22,5 +22,6 @@ export { default as suave } from './suave';
export { default as swapButton } from './swapButton'; export { default as swapButton } from './swapButton';
export { default as txInterpretation } from './txInterpretation'; export { default as txInterpretation } from './txInterpretation';
export { default as userOps } from './userOps'; export { default as userOps } from './userOps';
export { default as validators } from './validators';
export { default as verifiedTokens } from './verifiedTokens'; export { default as verifiedTokens } from './verifiedTokens';
export { default as web3Wallet } from './web3Wallet'; export { default as web3Wallet } from './web3Wallet';
import type { Feature } from './types';
import { VALIDATORS_CHAIN_TYPE } from 'types/client/validators';
import type { ValidatorsChainType } from 'types/client/validators';
import { getEnvValue } from '../utils';
const chainType = ((): ValidatorsChainType | undefined => {
const envValue = getEnvValue('NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE') as ValidatorsChainType | undefined;
return envValue && VALIDATORS_CHAIN_TYPE.includes(envValue) ? envValue : undefined;
})();
const title = 'Validators list';
const config: Feature<{ chainType: ValidatorsChainType }> = (() => {
if (chainType) {
return Object.freeze({
title,
isEnabled: true,
chainType,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -18,6 +18,8 @@ import type { NavItemExternal, NavigationLinkId } from '../../../types/client/na ...@@ -18,6 +18,8 @@ import type { NavItemExternal, NavigationLinkId } from '../../../types/client/na
import { ROLLUP_TYPES } from '../../../types/client/rollup'; import { ROLLUP_TYPES } from '../../../types/client/rollup';
import type { BridgedTokenChain, TokenBridge } from '../../../types/client/token'; import type { BridgedTokenChain, TokenBridge } from '../../../types/client/token';
import { PROVIDERS as TX_INTERPRETATION_PROVIDERS } from '../../../types/client/txInterpretation'; import { PROVIDERS as TX_INTERPRETATION_PROVIDERS } from '../../../types/client/txInterpretation';
import { VALIDATORS_CHAIN_TYPE } from '../../../types/client/validators';
import type { ValidatorsChainType } from '../../../types/client/validators';
import type { WalletType } from '../../../types/client/wallets'; import type { WalletType } from '../../../types/client/wallets';
import { SUPPORTED_WALLETS } from '../../../types/client/wallets'; import { SUPPORTED_WALLETS } from '../../../types/client/wallets';
import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks'; import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
...@@ -489,6 +491,7 @@ const schema = yup ...@@ -489,6 +491,7 @@ const schema = yup
NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(), NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(),
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(), NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
NEXT_PUBLIC_SWAP_BUTTON_URL: yup.string(), NEXT_PUBLIC_SWAP_BUTTON_URL: yup.string(),
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string<ValidatorsChainType>().oneOf(VALIDATORS_CHAIN_TYPE),
// 6. External services envs // 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
......
...@@ -51,3 +51,4 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://example.com ...@@ -51,3 +51,4 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://example.com
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=false NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=false
NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket'] NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket']
NEXT_PUBLIC_SWAP_BUTTON_URL=uniswap NEXT_PUBLIC_SWAP_BUTTON_URL=uniswap
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability
\ No newline at end of file
...@@ -573,6 +573,16 @@ For blockchains that implement SUAVE architecture additional fields will be show ...@@ -573,6 +573,16 @@ For blockchains that implement SUAVE architecture additional fields will be show
&nbsp; &nbsp;
### Validators list
The feature enables the Validators page which provides detailed information about the validators of the PoS chains.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE | `'stability'` | Chain type | Required | - | `'stability'` |
&nbsp;
### Sentry error monitoring ### Sentry error monitoring
| Variable | Type| Description | Compulsoriness | Default value | Example value | | Variable | Type| Description | Compulsoriness | Default value | Example value |
......
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.671 22.827a4.363 4.363 0 0 0 1.53-2.195 4.338 4.338 0 0 0-.042-2.67 4.366 4.366 0 0 0-1.6-2.145 4.41 4.41 0 0 0-5.117 0 4.366 4.366 0 0 0-1.6 2.145 4.338 4.338 0 0 0-.044 2.67c.267.873.802 1.64 1.53 2.195a6.578 6.578 0 0 0-3.256 3.127.705.705 0 0 0 .046.7.712.712 0 0 0 .613.346h1.246c.461-1.593 2.088-2.77 4.023-2.77 1.936 0 3.562 1.177 4.023 2.77h1.247a.716.716 0 0 0 .612-.346.705.705 0 0 0 .046-.7 6.578 6.578 0 0 0-3.257-3.127Zm-.179-3.489a2.492 2.492 0 1 1-4.984 0 2.492 2.492 0 0 1 4.984 0ZM15.002 3a1 1 0 0 1 .832.445l2.063 3.094 2.482-1.986a1 1 0 0 1 1.605.977l-1.333 6.669a1 1 0 0 1-.98.804h-9.337a1 1 0 0 1-.98-.804l-1.335-6.67a1 1 0 0 1 1.606-.976l2.482 1.986 2.063-3.094A1 1 0 0 1 15.002 3Zm0 2.803-1.836 2.753a1 1 0 0 1-1.456.226l-1.191-.952.635 3.173h7.696l.635-3.173-1.19.952a1 1 0 0 1-1.458-.226l-1.835-2.753Z" fill="currentColor"/>
</svg>
...@@ -81,6 +81,7 @@ import type { TxInterpretationResponse } from 'types/api/txInterpretation'; ...@@ -81,6 +81,7 @@ import type { TxInterpretationResponse } from 'types/api/txInterpretation';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges'; import type { TxStateChanges } from 'types/api/txStateChanges';
import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps'; import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps';
import type { ValidatorsCountersResponse, ValidatorsFilters, ValidatorsResponse, ValidatorsSorting } from 'types/api/validators';
import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
import type { VisualizedContract } from 'types/api/visualization'; import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
...@@ -615,6 +616,17 @@ export const RESOURCES = { ...@@ -615,6 +616,17 @@ export const RESOURCES = {
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
}, },
// VALIDATORS
validators: {
path: '/api/v2/validators/:chainType',
pathParams: [ 'chainType' as const ],
filterFields: [ 'address_hash' as const, 'state' as const ],
},
validators_counters: {
path: '/api/v2/validators/:chainType/counters',
pathParams: [ 'chainType' as const ],
},
// CONFIGS // CONFIGS
config_backend_version: { config_backend_version: {
path: '/api/v2/config/backend-version', path: '/api/v2/config/backend-version',
...@@ -687,7 +699,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -687,7 +699,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' |
'domains_lookup' | 'addresses_lookup' | 'user_ops'; 'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -805,6 +817,8 @@ never; ...@@ -805,6 +817,8 @@ never;
export type ResourcePayloadB<Q extends ResourceName> = export type ResourcePayloadB<Q extends ResourceName> =
Q extends 'marketplace_dapps' ? Array<MarketplaceAppOverview> : Q extends 'marketplace_dapps' ? Array<MarketplaceAppOverview> :
Q extends 'marketplace_dapp' ? MarketplaceAppOverview : Q extends 'marketplace_dapp' ? MarketplaceAppOverview :
Q extends 'validators' ? ValidatorsResponse :
Q extends 'validators_counters' ? ValidatorsCountersResponse :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -812,8 +826,10 @@ export type ResourcePayload<Q extends ResourceName> = ResourcePayloadA<Q> | Reso ...@@ -812,8 +826,10 @@ export type ResourcePayload<Q extends ResourceName> = ResourcePayloadA<Q> | Reso
// Right now there is no paginated resources in B-part // Right now there is no paginated resources in B-part
// Add "| ResourcePayloadB<Q>[...]" if it is not true anymore // Add "| ResourcePayloadB<Q>[...]" if it is not true anymore
export type PaginatedResponseItems<Q extends ResourceName> = Q extends PaginatedResources ? ResourcePayloadA<Q>['items'] : never; export type PaginatedResponseItems<Q extends ResourceName> = Q extends PaginatedResources ? ResourcePayloadA<Q>['items'] | ResourcePayloadB<Q>['items'] : never;
export type PaginatedResponseNextPageParams<Q extends ResourceName> = Q extends PaginatedResources ? ResourcePayloadA<Q>['next_page_params'] : never; export type PaginatedResponseNextPageParams<Q extends ResourceName> = Q extends PaginatedResources ?
ResourcePayloadA<Q>['next_page_params'] | ResourcePayloadB<Q>['next_page_params'] :
never;
/* eslint-disable @typescript-eslint/indent */ /* eslint-disable @typescript-eslint/indent */
export type PaginationFilters<Q extends PaginatedResources> = export type PaginationFilters<Q extends PaginatedResources> =
...@@ -834,6 +850,7 @@ Q extends 'verified_contracts' ? VerifiedContractsFilters : ...@@ -834,6 +850,7 @@ Q extends 'verified_contracts' ? VerifiedContractsFilters :
Q extends 'addresses_lookup' ? EnsAddressLookupFilters : Q extends 'addresses_lookup' ? EnsAddressLookupFilters :
Q extends 'domains_lookup' ? EnsDomainLookupFilters : Q extends 'domains_lookup' ? EnsDomainLookupFilters :
Q extends 'user_ops' ? UserOpsFilters : Q extends 'user_ops' ? UserOpsFilters :
Q extends 'validators' ? ValidatorsFilters :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -845,5 +862,6 @@ Q extends 'verified_contracts' ? VerifiedContractsSorting : ...@@ -845,5 +862,6 @@ Q extends 'verified_contracts' ? VerifiedContractsSorting :
Q extends 'address_txs' ? TransactionsSorting : Q extends 'address_txs' ? TransactionsSorting :
Q extends 'addresses_lookup' ? EnsLookupSorting : Q extends 'addresses_lookup' ? EnsLookupSorting :
Q extends 'domains_lookup' ? EnsLookupSorting : Q extends 'domains_lookup' ? EnsLookupSorting :
Q extends 'validators' ? ValidatorsSorting :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -66,6 +66,12 @@ export default function useNavItems(): ReturnType { ...@@ -66,6 +66,12 @@ export default function useNavItems(): ReturnType {
icon: 'ENS', icon: 'ENS',
isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]', isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]',
} : null; } : null;
const validators = config.features.validators.isEnabled ? {
text: 'Top validators',
nextRoute: { pathname: '/validators' as const },
icon: 'validator',
isActive: pathname === '/validators',
} : null;
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
...@@ -84,6 +90,7 @@ export default function useNavItems(): ReturnType { ...@@ -84,6 +90,7 @@ export default function useNavItems(): ReturnType {
].filter(Boolean), ].filter(Boolean),
[ [
topAccounts, topAccounts,
validators,
verifiedContracts, verifiedContracts,
ensLookup, ensLookup,
].filter(Boolean), ].filter(Boolean),
...@@ -107,6 +114,7 @@ export default function useNavItems(): ReturnType { ...@@ -107,6 +114,7 @@ export default function useNavItems(): ReturnType {
[ [
userOps, userOps,
topAccounts, topAccounts,
validators,
verifiedContracts, verifiedContracts,
ensLookup, ensLookup,
].filter(Boolean), ].filter(Boolean),
...@@ -117,6 +125,7 @@ export default function useNavItems(): ReturnType { ...@@ -117,6 +125,7 @@ export default function useNavItems(): ReturnType {
userOps, userOps,
blocks, blocks,
topAccounts, topAccounts,
validators,
verifiedContracts, verifiedContracts,
ensLookup, ensLookup,
config.features.beaconChain.isEnabled && { config.features.beaconChain.isEnabled && {
......
...@@ -42,6 +42,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -42,6 +42,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/404': 'Regular page', '/404': 'Regular page',
'/name-domains': 'Root page', '/name-domains': 'Root page',
'/name-domains/[name]': 'Regular page', '/name-domains/[name]': 'Regular page',
'/validators': 'Root page',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Regular page', '/login': 'Regular page',
......
...@@ -45,6 +45,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -45,6 +45,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/404': DEFAULT_TEMPLATE, '/404': DEFAULT_TEMPLATE,
'/name-domains': DEFAULT_TEMPLATE, '/name-domains': DEFAULT_TEMPLATE,
'/name-domains/[name]': DEFAULT_TEMPLATE, '/name-domains/[name]': DEFAULT_TEMPLATE,
'/validators': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': DEFAULT_TEMPLATE, '/login': DEFAULT_TEMPLATE,
......
...@@ -40,6 +40,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -40,6 +40,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/404': 'error - page not found', '/404': 'error - page not found',
'/name-domains': 'domains search and resolve', '/name-domains': 'domains search and resolve',
'/name-domains/[name]': '%name% domain details', '/name-domains/[name]': '%name% domain details',
'/validators': 'validators list',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'login', '/login': 'login',
......
...@@ -40,6 +40,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -40,6 +40,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/404': '404', '/404': '404',
'/name-domains': 'Domains search and resolve', '/name-domains': 'Domains search and resolve',
'/name-domains/[name]': 'Domain details', '/name-domains/[name]': 'Domain details',
'/validators': 'Validators list',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Login', '/login': 'Login',
......
import type { Validator, ValidatorsCountersResponse, ValidatorsResponse } from 'types/api/validators';
import * as addressMock from '../address/address';
export const validator1: Validator = {
address: addressMock.withName,
blocks_validated_count: 7334224,
state: 'active',
};
export const validator2: Validator = {
address: addressMock.withEns,
blocks_validated_count: 8937453,
state: 'probation',
};
export const validator3: Validator = {
address: addressMock.withoutName,
blocks_validated_count: 1234,
state: 'inactive',
};
export const validatorsResponse: ValidatorsResponse = {
items: [ validator1, validator2, validator3 ],
next_page_params: null,
};
export const validatorsCountersResponse: ValidatorsCountersResponse = {
active_validators_counter: '42',
active_validators_percentage: 7.14,
new_validators_counter_24h: '11',
validators_counter: '140',
};
...@@ -171,3 +171,13 @@ export const userOps: GetServerSideProps<Props> = async(context) => { ...@@ -171,3 +171,13 @@ export const userOps: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
export const validators: GetServerSideProps<Props> = async(context) => {
if (!config.features.validators.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
...@@ -49,6 +49,7 @@ declare module "nextjs-routes" { ...@@ -49,6 +49,7 @@ declare module "nextjs-routes" {
| DynamicRoute<"/tx/[hash]", { "hash": string }> | DynamicRoute<"/tx/[hash]", { "hash": string }>
| StaticRoute<"/txs"> | StaticRoute<"/txs">
| DynamicRoute<"/txs/kettle/[hash]", { "hash": string }> | DynamicRoute<"/txs/kettle/[hash]", { "hash": string }>
| StaticRoute<"/validators">
| StaticRoute<"/verified-contracts"> | StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml"> | StaticRoute<"/visualize/sol2uml">
| StaticRoute<"/withdrawals">; | StaticRoute<"/withdrawals">;
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const Validators = dynamic(() => import('ui/pages/Validators'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/validators">
<Validators/>
</PageNextJs>
);
};
export default Page;
export { validators as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -39,6 +39,9 @@ export const featureEnvs = { ...@@ -39,6 +39,9 @@ export const featureEnvs = {
userOps: [ userOps: [
{ name: 'NEXT_PUBLIC_HAS_USER_OPS', value: 'true' }, { name: 'NEXT_PUBLIC_HAS_USER_OPS', value: 'true' },
], ],
validators: [
{ name: 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', value: 'stability' },
],
}; };
export const viewsEnvs = { export const viewsEnvs = {
......
...@@ -130,6 +130,7 @@ ...@@ -130,6 +130,7 @@
| "uniswap" | "uniswap"
| "user_op_slim" | "user_op_slim"
| "user_op" | "user_op"
| "validator"
| "verified_token" | "verified_token"
| "verified" | "verified"
| "verify-contract" | "verify-contract"
......
import type { SmartContract, SolidityscanReport } from 'types/api/contract'; import type { SmartContract, SolidityscanReport } from 'types/api/contract';
import type { VerifiedContract } from 'types/api/contracts'; import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts';
import { ADDRESS_PARAMS } from './addressParams'; import { ADDRESS_PARAMS } from './addressParams';
...@@ -54,6 +54,13 @@ export const VERIFIED_CONTRACT_INFO: VerifiedContract = { ...@@ -54,6 +54,13 @@ export const VERIFIED_CONTRACT_INFO: VerifiedContract = {
verified_at: '2023-04-10T13:16:33.884921Z', verified_at: '2023-04-10T13:16:33.884921Z',
}; };
export const VERIFIED_CONTRACTS_COUNTERS: VerifiedContractsCounters = {
smart_contracts: '123456789',
new_smart_contracts_24h: '12345',
verified_smart_contracts: '654321',
new_verified_smart_contracts_24h: '1234',
};
export const SOLIDITYSCAN_REPORT: SolidityscanReport = { export const SOLIDITYSCAN_REPORT: SolidityscanReport = {
scan_report: { scan_report: {
scan_status: 'scan_done', scan_status: 'scan_done',
......
import type { Validator, ValidatorsCountersResponse } from 'types/api/validators';
import { ADDRESS_PARAMS } from './addressParams';
export const VALIDATOR: Validator = {
address: ADDRESS_PARAMS,
blocks_validated_count: 25987,
state: 'active',
};
export const VALIDATORS_COUNTERS: ValidatorsCountersResponse = {
active_validators_counter: '42',
active_validators_percentage: 7.14,
new_validators_counter_24h: '11',
validators_counter: '140',
};
import type { AddressParam } from './addressParams';
export interface Validator {
address: AddressParam;
blocks_validated_count: number;
state: 'active' | 'probation' | 'inactive';
}
export interface ValidatorsResponse {
items: Array<Validator>;
next_page_params: {
'address_hash': string;
'blocks_validated': string;
'items_count': string;
'state': Validator['state'];
} | null;
}
export interface ValidatorsCountersResponse {
active_validators_counter: string;
active_validators_percentage: number;
new_validators_counter_24h: string;
validators_counter: string;
}
export interface ValidatorsFilters {
// address_hash: string | undefined; // right now API doesn't support filtering by address_hash
state_filter: Validator['state'] | undefined;
}
export interface ValidatorsSorting {
sort: 'state' | 'blocks_validated';
order: 'asc' | 'desc';
}
export type ValidatorsSortingField = ValidatorsSorting['sort'];
export type ValidatorsSortingValue = `${ ValidatorsSortingField }-${ ValidatorsSorting['order'] }`;
import type { ArrayElement } from 'types/utils';
export const VALIDATORS_CHAIN_TYPE = [
'stability',
] as const;
export type ValidatorsChainType = ArrayElement<typeof VALIDATORS_CHAIN_TYPE>;
...@@ -43,11 +43,13 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -43,11 +43,13 @@ const AddressBlocksValidatedListItem = (props: Props) => {
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Skeleton> <Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Skeleton>
<Utilization { props.gas_used && props.gas_used !== '0' && (
colorScheme="gray" <Utilization
value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() } colorScheme="gray"
isLoading={ props.isLoading } value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }
/> isLoading={ props.isLoading }
/>
) }
</Flex> </Flex>
{ !config.UI.views.block.hiddenFields?.total_reward && ( { !config.UI.views.block.hiddenFields?.total_reward && (
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
......
...@@ -46,11 +46,13 @@ const AddressBlocksValidatedTableItem = (props: Props) => { ...@@ -46,11 +46,13 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
<Skeleton isLoaded={ !props.isLoading } flexBasis="80px"> <Skeleton isLoaded={ !props.isLoading } flexBasis="80px">
{ BigNumber(props.gas_used || 0).toFormat() } { BigNumber(props.gas_used || 0).toFormat() }
</Skeleton> </Skeleton>
<Utilization { props.gas_used && props.gas_used !== '0' && (
colorScheme="gray" <Utilization
value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() } colorScheme="gray"
isLoading={ props.isLoading } value={ BigNumber(props.gas_used).dividedBy(BigNumber(props.gas_limit)).toNumber() }
/> isLoading={ props.isLoading }
/>
) }
</Flex> </Flex>
</Td> </Td>
{ !config.UI.views.block.hiddenFields?.total_reward && ( { !config.UI.views.block.hiddenFields?.total_reward && (
......
...@@ -163,6 +163,7 @@ const AddressPageContent = () => { ...@@ -163,6 +163,7 @@ const AddressPageContent = () => {
isLoading={ isLoading } isLoading={ isLoading }
tagsBefore={ [ tagsBefore={ [
!addressQuery.data?.is_contract ? { label: 'eoa', display_name: 'EOA' } : undefined, !addressQuery.data?.is_contract ? { label: 'eoa', display_name: 'EOA' } : undefined,
config.features.validators.isEnabled && addressQuery.data?.has_validated_blocks ? { label: 'validator', display_name: 'Validator' } : undefined,
addressQuery.data?.implementation_address ? { label: 'proxy', display_name: 'Proxy' } : undefined, addressQuery.data?.implementation_address ? { label: 'proxy', display_name: 'Proxy' } : undefined,
addressQuery.data?.token ? { label: 'token', display_name: 'Token' } : undefined, addressQuery.data?.token ? { label: 'token', display_name: 'Token' } : undefined,
isSafeAddress ? { label: 'safe', display_name: 'Multisig: Safe' } : undefined, isSafeAddress ? { label: 'safe', display_name: 'Multisig: Safe' } : undefined,
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as textAdMock from 'mocks/ad/textAd';
import * as validatorsMock from 'mocks/validators/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import Validators from './Validators';
const VALIDATORS_API_URL = buildApiUrl('validators', { chainType: 'stability' });
const VALIDATORS_COUNTERS_API_URL = buildApiUrl('validators_counters', { chainType: 'stability' });
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.validators) as any,
});
test.beforeEach(async({ page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: JSON.stringify(textAdMock.duck),
}));
await page.route(textAdMock.duck.ad.thumbnail, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
});
});
});
test('base view +@mobile', async({ mount, page }) => {
await page.route(VALIDATORS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(validatorsMock.validatorsResponse),
}));
await page.route(VALIDATORS_COUNTERS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(validatorsMock.validatorsCountersResponse),
}));
const component = await mount(
<TestApp>
<Validators/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Hide, HStack, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { getFeaturePayload } from 'configs/app/features/types';
import type { ValidatorsFilters, ValidatorsSorting, ValidatorsSortingField, ValidatorsSortingValue } from 'types/api/validators';
import config from 'configs/app';
// import useDebounce from 'lib/hooks/useDebounce';
import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import { generateListStub } from 'stubs/utils';
import { VALIDATOR } from 'stubs/validators';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
// import FilterInput from 'ui/shared/filters/FilterInput';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import Sort from 'ui/shared/sort/Sort';
import { SORT_OPTIONS } from 'ui/validators/utils';
import ValidatorsCounters from 'ui/validators/ValidatorsCounters';
import ValidatorsFilter from 'ui/validators/ValidatorsFilter';
import ValidatorsList from 'ui/validators/ValidatorsList';
import ValidatorsTable from 'ui/validators/ValidatorsTable';
const Validators = () => {
const router = useRouter();
// const [ searchTerm, setSearchTerm ] = React.useState(getQueryParamString(router.query.address_hash) || undefined);
const [ statusFilter, setStatusFilter ] = React.useState(getQueryParamString(router.query.state_filter) as ValidatorsFilters['state_filter'] || undefined);
const [ sort, setSort ] =
React.useState<ValidatorsSortingValue | undefined>(getSortValueFromQuery<ValidatorsSortingValue>(router.query, SORT_OPTIONS));
// const debouncedSearchTerm = useDebounce(searchTerm || '', 300);
const isMobile = useIsMobile();
const { isError, isPlaceholderData, data, pagination, onFilterChange, onSortingChange } = useQueryWithPages({
resourceName: 'validators',
pathParams: { chainType: getFeaturePayload(config.features.validators)?.chainType },
filters: {
// address_hash: debouncedSearchTerm,
state_filter: statusFilter,
},
sorting: getSortParamsFromValue<ValidatorsSortingValue, ValidatorsSortingField, ValidatorsSorting['order']>(sort),
options: {
enabled: config.features.validators.isEnabled,
placeholderData: generateListStub<'validators'>(
VALIDATOR,
50,
{ next_page_params: null },
),
},
});
// const handleSearchTermChange = React.useCallback((value: string) => {
// onFilterChange({
// address_hash: value,
// state_filter: statusFilter
// });
// setSearchTerm(value);
// }, [ statusFilter, onFilterChange ]);
const handleStateFilterChange = React.useCallback((value: string | Array<string>) => {
if (Array.isArray(value)) {
return;
}
const state = value === 'all' ? undefined : value as ValidatorsFilters['state_filter'];
onFilterChange({
// address_hash: debouncedSearchTerm,
state_filter: state,
});
setStatusFilter(state);
}, [ onFilterChange ]);
const handleSortChange = React.useCallback((value?: ValidatorsSortingValue) => {
setSort(value);
onSortingChange(getSortParamsFromValue(value));
}, [ onSortingChange ]);
const filterMenu = <ValidatorsFilter onChange={ handleStateFilterChange } defaultValue={ statusFilter } isActive={ Boolean(statusFilter) }/>;
// const filterInput = (
// <FilterInput
// w={{ base: '100%', lg: '350px' }}
// size="xs"
// onChange={ handleSearchTermChange }
// placeholder="Search by validator's address hash"
// initialValue={ searchTerm }
// />
// );
const sortButton = (
<Sort
options={ SORT_OPTIONS }
sort={ sort }
setSort={ handleSortChange }
/>
);
const actionBar = (
<>
<HStack spacing={ 3 } mb={ 6 } display={{ base: 'flex', lg: 'none' }}>
{ filterMenu }
{ sortButton }
{ /* { filterInput } */ }
</HStack>
{ (!isMobile || pagination.isVisible) && (
<ActionBar mt={ -6 }>
<HStack spacing={ 3 } display={{ base: 'none', lg: 'flex' }}>
{ filterMenu }
{ /* { filterInput } */ }
</HStack>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
</>
);
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
<ValidatorsList data={ data.items } isLoading={ isPlaceholderData }/>
</Show>
<Hide below="lg" ssr={ false }>
<ValidatorsTable data={ data.items } sort={ sort } setSorting={ handleSortChange } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
return (
<Box>
<PageTitle title="Validators" withTextAd/>
<ValidatorsCounters/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no verified contracts."
filterProps={{
emptyFilteredText: `Couldn${ apos }t find any validator that matches your query.`,
hasActiveFilters: Boolean(
// searchTerm ||
statusFilter,
),
}}
content={ content }
actionBar={ actionBar }
/>
</Box>
);
};
export default Validators;
import { Box, Flex, Skeleton, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, Text, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
type Props = { type Props = {
label: string; label: string;
description?: string;
value: string; value: string;
hint?: string;
isLoading?: boolean; isLoading?: boolean;
diff?: string | number;
diffFormatted?: string;
diffPeriod?: '24h';
} }
const NumberWidget = ({ label, value, isLoading, description }: Props) => { const StatsWidget = ({ label, value, isLoading, hint, diff, diffPeriod = '24h', diffFormatted }: Props) => {
const bgColor = useColorModeValue('blue.50', 'blue.800'); const bgColor = useColorModeValue('blue.50', 'blue.800');
const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const hintColor = useColorModeValue('gray.600', 'gray.400'); const hintColor = useColorModeValue('gray.600', 'gray.400');
...@@ -18,15 +21,14 @@ const NumberWidget = ({ label, value, isLoading, description }: Props) => { ...@@ -18,15 +21,14 @@ const NumberWidget = ({ label, value, isLoading, description }: Props) => {
return ( return (
<Flex <Flex
alignItems="flex-start" alignItems="flex-start"
bg={ isLoading ? skeletonBgColor : bgColor } bgColor={ isLoading ? skeletonBgColor : bgColor }
px={ 3 } px={ 3 }
py={{ base: 2, lg: 3 }} py={{ base: 2, lg: 3 }}
borderRadius={ 12 } borderRadius="md"
justifyContent="space-between" justifyContent="space-between"
columnGap={ 3 } columnGap={ 3 }
> >
<Box <Box>
>
<Skeleton <Skeleton
isLoaded={ !isLoading } isLoaded={ !isLoading }
color="text_secondary" color="text_secondary"
...@@ -35,26 +37,31 @@ const NumberWidget = ({ label, value, isLoading, description }: Props) => { ...@@ -35,26 +37,31 @@ const NumberWidget = ({ label, value, isLoading, description }: Props) => {
> >
<span>{ label }</span> <span>{ label }</span>
</Skeleton> </Skeleton>
<Skeleton <Skeleton
isLoaded={ !isLoading } isLoaded={ !isLoading }
fontWeight={ 500 }
fontSize="lg"
w="fit-content" w="fit-content"
display="flex"
alignItems="baseline"
mt={ 1 }
> >
{ value } <Text fontWeight={ 500 } fontSize="lg" lineHeight={ 6 }>{ value }</Text>
{ diff && Number(diff) > 0 && (
<>
<Text fontWeight={ 500 } ml={ 2 } mr={ 1 } fontSize="lg" lineHeight={ 6 } color="green.500">
+{ diffFormatted || Number(diff).toLocaleString() }
</Text>
<Text variant="secondary" fontSize="sm">({ diffPeriod })</Text>
</>
) }
</Skeleton> </Skeleton>
</Box> </Box>
<Skeleton isLoaded={ !isLoading } alignSelf="center" borderRadius="base"> { hint && (
<Hint <Skeleton isLoaded={ !isLoading } alignSelf="center" borderRadius="base">
label={ description } <Hint label={ hint } boxSize={ 6 } color={ hintColor }/>
boxSize={ 6 } </Skeleton>
color={ hintColor } ) }
/>
</Skeleton>
</Flex> </Flex>
); );
}; };
export default NumberWidget; export default StatsWidget;
import React from 'react';
import type { Validator } from 'types/api/validators';
import StatusTag from './StatusTag';
interface Props {
state: Validator['state'];
isLoading?: boolean;
}
const ValidatorStatus = ({ state, isLoading }: Props) => {
switch (state) {
case 'active':
return <StatusTag type="ok" text="Active" isLoading={ isLoading }/>;
case 'probation':
return <StatusTag type="pending" text="Probation" isLoading={ isLoading }/>;
case 'inactive':
return <StatusTag type="error" text="Failed" isLoading={ isLoading }/>;
}
};
export default React.memo(ValidatorStatus);
...@@ -3,9 +3,9 @@ import React from 'react'; ...@@ -3,9 +3,9 @@ import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { STATS_COUNTER } from 'stubs/stats'; import { STATS_COUNTER } from 'stubs/stats';
import StatsWidget from 'ui/shared/stats/StatsWidget';
import DataFetchAlert from '../shared/DataFetchAlert'; import DataFetchAlert from '../shared/DataFetchAlert';
import NumberWidget from './NumberWidget';
const UNITS_WITHOUT_SPACE = [ 's' ]; const UNITS_WITHOUT_SPACE = [ 's' ];
...@@ -36,12 +36,12 @@ const NumberWidgetsList = () => { ...@@ -36,12 +36,12 @@ const NumberWidgetsList = () => {
} }
return ( return (
<NumberWidget <StatsWidget
key={ id + (isPlaceholderData ? index : '') } key={ id + (isPlaceholderData ? index : '') }
label={ title } label={ title }
value={ `${ Number(value).toLocaleString(undefined, { maximumFractionDigits: 3, notation: 'compact' }) }${ unitsStr }` } value={ `${ Number(value).toLocaleString(undefined, { maximumFractionDigits: 3, notation: 'compact' }) }${ unitsStr }` }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
description={ description } hint={ description }
/> />
); );
}) })
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { getFeaturePayload } from 'configs/app/features/types';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { VALIDATORS_COUNTERS } from 'stubs/validators';
import StatsWidget from 'ui/shared/stats/StatsWidget';
const ValidatorsCounters = () => {
const countersQuery = useApiQuery('validators_counters', {
pathParams: { chainType: getFeaturePayload(config.features.validators)?.chainType },
queryOptions: {
enabled: config.features.validators.isEnabled,
placeholderData: VALIDATORS_COUNTERS,
},
});
if (!countersQuery.data) {
return null;
}
return (
<Box columnGap={ 3 } rowGap={ 3 } mb={ 6 } display="grid" gridTemplateColumns={{ base: '1fr', lg: 'repeat(2, 1fr)' }}>
<StatsWidget
label="Total validators"
value={ Number(countersQuery.data.validators_counter).toLocaleString() }
diff={ Number(countersQuery.data.new_validators_counter_24h).toLocaleString() }
isLoading={ countersQuery.isPlaceholderData }
/>
<StatsWidget
label="Active validators"
value={ `${ Number(countersQuery.data.active_validators_percentage).toLocaleString() }%` }
isLoading={ countersQuery.isPlaceholderData }
/>
</Box>
);
};
export default React.memo(ValidatorsCounters);
import {
Menu,
MenuButton,
MenuList,
MenuOptionGroup,
MenuItemOption,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import type { ValidatorsFilters } from 'types/api/validators';
import FilterButton from 'ui/shared/filters/FilterButton';
interface Props {
isActive: boolean;
defaultValue: ValidatorsFilters['state_filter'] | undefined;
onChange: (nextValue: string | Array<string>) => void;
}
const ValidatorsFilter = ({ onChange, defaultValue, isActive }: Props) => {
const { isOpen, onToggle } = useDisclosure();
return (
<Menu>
<MenuButton>
<FilterButton
isActive={ isOpen || isActive }
appliedFiltersNum={ isActive ? 1 : 0 }
onClick={ onToggle }
as="div"
/>
</MenuButton>
<MenuList zIndex="popover">
<MenuOptionGroup defaultValue={ defaultValue || 'all' } title="Status" type="radio" onChange={ onChange }>
<MenuItemOption value="all">All</MenuItemOption>
<MenuItemOption value="active">Active</MenuItemOption>
<MenuItemOption value="probation">Probation</MenuItemOption>
<MenuItemOption value="inactive">Failed</MenuItemOption>
</MenuOptionGroup>
</MenuList>
</Menu>
);
};
export default React.memo(ValidatorsFilter);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { Validator } from 'types/api/validators';
import ValidatorsListItem from './ValidatorsListItem';
const ValidatorsList = ({ data, isLoading }: { data: Array<Validator>; isLoading: boolean }) => {
return (
<Box>
{ data.map((item, index) => (
<ValidatorsListItem
key={ item.address.hash + (isLoading ? index : '') }
data={ item }
isLoading={ isLoading }
/>
)) }
</Box>
);
};
export default React.memo(ValidatorsList);
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { Validator } from 'types/api/validators';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ValidatorStatus from 'ui/shared/statusTag/ValidatorStatus';
interface Props {
data: Validator;
isLoading?: boolean;
}
const ValidatorsListItem = ({ data, isLoading }: Props) => {
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>Address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity
isLoading={ isLoading }
address={ data.address }
truncation="constant"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ValidatorStatus state={ data.state } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Blocks</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.blocks_validated_count.toLocaleString() }
</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default React.memo(ValidatorsListItem);
import { Table, Tbody, Tr, Th, Link } from '@chakra-ui/react';
import React from 'react';
import type { Validator, ValidatorsSorting, ValidatorsSortingField, ValidatorsSortingValue } from 'types/api/validators';
import IconSvg from 'ui/shared/IconSvg';
import getNextSortValue from 'ui/shared/sort/getNextSortValue';
import { default as Thead } from 'ui/shared/TheadSticky';
import { SORT_SEQUENCE } from './utils';
import ValidatorsTableItem from './ValidatorsTableItem';
interface Props {
data: Array<Validator>;
sort: ValidatorsSortingValue | undefined;
setSorting: (val: ValidatorsSortingValue | undefined) => void;
isLoading?: boolean;
}
const ValidatorsTable = ({ data, sort, setSorting, isLoading }: Props) => {
const sortIconTransform = sort?.includes('asc' as ValidatorsSorting['order']) ? 'rotate(-90deg)' : 'rotate(90deg)';
const onSortToggle = React.useCallback((field: ValidatorsSortingField) => () => {
const value = getNextSortValue<ValidatorsSortingField, ValidatorsSortingValue>(SORT_SEQUENCE, field)(sort);
setSorting(value);
}, [ sort, setSorting ]);
return (
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th width="50%">Validator’s address</Th>
<Th width="25%">
<Link
display="flex"
alignItems="center"
onClick={ isLoading ? undefined : onSortToggle('state') }
columnGap={ 1 }
>
{ sort?.includes('state') && <IconSvg name="arrows/east" boxSize={ 4 } transform={ sortIconTransform }/> }
Status
</Link>
</Th>
<Th width="25%" isNumeric>
<Link
display="flex"
alignItems="center"
justifyContent="flex-end"
onClick={ isLoading ? undefined : onSortToggle('blocks_validated') }
columnGap={ 1 }
>
{ sort?.includes('blocks_validated') && <IconSvg name="arrows/east" boxSize={ 4 } transform={ sortIconTransform }/> }
Blocks
</Link>
</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => (
<ValidatorsTableItem
key={ item.address.hash + (isLoading ? index : '') }
data={ item }
isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(ValidatorsTable);
import { Tr, Td, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { Validator } from 'types/api/validators';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import ValidatorStatus from 'ui/shared/statusTag/ValidatorStatus';
interface Props {
data: Validator;
isLoading?: boolean;
}
const ValidatorsTableItem = ({ data, isLoading }: Props) => {
return (
<Tr>
<Td verticalAlign="middle">
<AddressEntity
address={ data.address }
isLoading={ isLoading }
truncation="constant"
/>
</Td>
<Td verticalAlign="middle">
<ValidatorStatus state={ data.state } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.blocks_validated_count.toLocaleString() }
</Skeleton>
</Td>
</Tr>
);
};
export default React.memo(ValidatorsTableItem);
import type { ValidatorsSortingValue, ValidatorsSortingField } from 'types/api/validators';
import type { Option } from 'ui/shared/sort/Sort';
export const SORT_OPTIONS: Array<Option<ValidatorsSortingValue>> = [
{ title: 'Default', id: undefined },
{ title: 'Status descending', id: 'state-desc' },
{ title: 'Status ascending', id: 'state-asc' },
{ title: 'Blocks validated descending', id: 'blocks_validated-desc' },
{ title: 'Blocks validated ascending', id: 'blocks_validated-asc' },
];
export const SORT_SEQUENCE: Record<ValidatorsSortingField, Array<ValidatorsSortingValue | undefined>> = {
state: [ 'state-desc', 'state-asc', undefined ],
blocks_validated: [ 'blocks_validated-desc', 'blocks_validated-asc', undefined ],
};
import { Flex, Skeleton } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { VERIFIED_CONTRACTS_COUNTERS } from 'stubs/contract';
import VerifiedContractsCountersItem from './VerifiedContractsCountersItem'; import StatsWidget from 'ui/shared/stats/StatsWidget';
const VerifiedContractsCounters = () => { const VerifiedContractsCounters = () => {
const countersQuery = useApiQuery('verified_contracts_counters'); const countersQuery = useApiQuery('verified_contracts_counters', {
queryOptions: {
placeholderData: VERIFIED_CONTRACTS_COUNTERS,
},
});
if (countersQuery.isError) { if (!countersQuery.data) {
return null; return null;
} }
const content = (() => {
if (countersQuery.isPending) {
const item = <Skeleton w={{ base: '100%', lg: 'calc((100% - 12px)/2)' }} h="69px" borderRadius="12px"/>;
return (
<>
{ item }
{ item }
</>
);
}
return (
<>
<VerifiedContractsCountersItem
name="Total contracts"
total={ countersQuery.data.smart_contracts }
new24={ countersQuery.data.new_smart_contracts_24h }
/>
<VerifiedContractsCountersItem
name="Verified contracts"
total={ countersQuery.data.verified_smart_contracts }
new24={ countersQuery.data.new_verified_smart_contracts_24h }
/>
</>
);
})();
return ( return (
<Flex columnGap={ 3 } rowGap={ 3 } flexDirection={{ base: 'column', lg: 'row' }} mb={ 6 }> <Box columnGap={ 3 } rowGap={ 3 } mb={ 6 } display="grid" gridTemplateColumns={{ base: '1fr', lg: 'repeat(2, 1fr)' }}>
{ content } <StatsWidget
</Flex> label="Total contracts"
value={ Number(countersQuery.data.smart_contracts).toLocaleString() }
diff={ countersQuery.data.new_smart_contracts_24h }
diffFormatted={ Number(countersQuery.data.new_smart_contracts_24h).toLocaleString() }
isLoading={ countersQuery.isPlaceholderData }
/>
<StatsWidget
label="Verified contracts"
value={ Number(countersQuery.data.verified_smart_contracts).toLocaleString() }
diff={ countersQuery.data.new_verified_smart_contracts_24h }
diffFormatted={ Number(countersQuery.data.new_verified_smart_contracts_24h).toLocaleString() }
isLoading={ countersQuery.isPlaceholderData }
/>
</Box>
); );
}; };
......
import { Box, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
type Props = {
name: string;
total: string;
new24: string;
}
const VerifiedContractsCountersItem = ({ name, total, new24 }: Props) => {
const itemBgColor = useColorModeValue('blue.50', 'blue.800');
return (
<Box
w={{ base: '100%', lg: 'calc((100% - 12px)/2)' }}
borderRadius="12px"
backgroundColor={ itemBgColor }
p={ 3 }
>
<Text variant="secondary" fontSize="xs">{ name }</Text>
<Flex alignItems="baseline">
<Text fontWeight={ 600 } mr={ 2 } fontSize="lg">{ Number(total).toLocaleString() }</Text>
{ Number(new24) > 0 && (
<>
<Text fontWeight={ 600 } mr={ 1 } fontSize="lg" color="green.500">+{ Number(new24).toLocaleString() }</Text>
<Text variant="secondary" fontSize="sm">(24h)</Text>
</>
) }
</Flex>
</Box>
);
};
export default VerifiedContractsCountersItem;
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