Commit a135c4da authored by tom goriunov's avatar tom goriunov Committed by GitHub

Zilliqa: Validator views (#2557)

* list of validators

* validator details page

* clean up

* review fixes
parent 2747e02d
...@@ -9,14 +9,12 @@ NEXT_PUBLIC_APP_PORT=3000 ...@@ -9,14 +9,12 @@ NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS=['solidity','vyper','yul','scilla']
# Instance ENVs # Instance ENVs
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=zilliqa-prototestnet.blockscout.com NEXT_PUBLIC_API_HOST=zilliqa-prototestnet.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x3d1ded3a7924cd3256a4b1a447c9bfb194f54b9a8ceb441edb8bb01563b516db NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x3d1ded3a7924cd3256a4b1a447c9bfb194f54b9a8ceb441edb8bb01563b516db
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(90deg, rgba(0, 208, 198, 1) 0.06%, rgba(43, 146, 151, 1) 99.97%)','linear-gradient(90deg, rgba(0, 208, 198, 1) 0.06%, rgba(43, 146, 151, 1) 50.02%, rgba(0, 0, 0, 1) 99.97%)'],'text_color':['rgba(255, 255, 255, 1)','rgba(255, 255, 255, 1)'],'button':{'_default':{'background':['rgba(38, 6, 124, 1)']},'_hover':{'background':['rgba(17, 4, 87, 1)']}}} NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(90deg, rgba(0, 208, 198, 1) 0.06%, rgba(43, 146, 151, 1) 99.97%)','linear-gradient(90deg, rgba(0, 208, 198, 1) 0.06%, rgba(43, 146, 151, 1) 50.02%, rgba(0, 0, 0, 1) 99.97%)'],'text_color':['rgba(255, 255, 255, 1)','rgba(255, 255, 255, 1)'],'button':{'_default':{'background':['rgba(38, 6, 124, 1)']},'_hover':{'background':['rgba(17, 4, 87, 1)']}}}
...@@ -38,4 +36,6 @@ NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-c ...@@ -38,4 +36,6 @@ NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-c
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=zil NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=zil
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=["base16", "bech32"] NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=["base16", "bech32"]
NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS=['solidity','vyper','yul','scilla']
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=zilliqa
\ No newline at end of file
...@@ -750,7 +750,7 @@ The feature enables the Validators page which provides detailed information abou ...@@ -750,7 +750,7 @@ The feature enables the Validators page which provides detailed information abou
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE | `'stability' \| 'blackfort'` | Chain type | Required | - | `'stability'` | v1.25.0+ | | NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE | `'stability' \| 'blackfort' \| 'zilliqa'` | Chain type | Required | - | `'stability'` | v1.25.0+ |
   
......
...@@ -150,6 +150,8 @@ import type { ...@@ -150,6 +150,8 @@ import type {
ValidatorsBlackfortCountersResponse, ValidatorsBlackfortCountersResponse,
ValidatorsBlackfortResponse, ValidatorsBlackfortResponse,
ValidatorsBlackfortSorting, ValidatorsBlackfortSorting,
ValidatorsZilliqaResponse,
ValidatorZilliqa,
} from 'types/api/validators'; } from 'types/api/validators';
import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
...@@ -1097,6 +1099,15 @@ export const RESOURCES = { ...@@ -1097,6 +1099,15 @@ export const RESOURCES = {
validators_blackfort_counters: { validators_blackfort_counters: {
path: '/api/v2/validators/blackfort/counters', path: '/api/v2/validators/blackfort/counters',
}, },
validators_zilliqa: {
path: '/api/v2/validators/zilliqa',
filterFields: [],
},
validator_zilliqa: {
path: '/api/v2/validators/zilliqa/:bls_public_key',
pathParams: [ 'bls_public_key' as const ],
filterFields: [],
},
// BLOBS // BLOBS
blob: { blob: {
...@@ -1247,7 +1258,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward ...@@ -1247,7 +1258,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' | 'zksync_l2_txn_batches' | 'zksync_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' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history' | 'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'validators_zilliqa' | 'noves_address_history' |
'token_transfers_all' | 'scroll_l2_txn_batches' | 'scroll_l2_txn_batch_txs' | 'scroll_l2_txn_batch_blocks' | 'token_transfers_all' | 'scroll_l2_txn_batches' | 'scroll_l2_txn_batch_txs' | 'scroll_l2_txn_batch_blocks' |
'scroll_l2_deposits' | 'scroll_l2_withdrawals' | 'advanced_filter' | 'pools'; 'scroll_l2_deposits' | 'scroll_l2_withdrawals' | 'advanced_filter' | 'pools';
...@@ -1375,6 +1386,8 @@ Q extends 'validators_stability' ? ValidatorsStabilityResponse : ...@@ -1375,6 +1386,8 @@ Q extends 'validators_stability' ? ValidatorsStabilityResponse :
Q extends 'validators_stability_counters' ? ValidatorsStabilityCountersResponse : Q extends 'validators_stability_counters' ? ValidatorsStabilityCountersResponse :
Q extends 'validators_blackfort' ? ValidatorsBlackfortResponse : Q extends 'validators_blackfort' ? ValidatorsBlackfortResponse :
Q extends 'validators_blackfort_counters' ? ValidatorsBlackfortCountersResponse : Q extends 'validators_blackfort_counters' ? ValidatorsBlackfortCountersResponse :
Q extends 'validators_zilliqa' ? ValidatorsZilliqaResponse :
Q extends 'validator_zilliqa' ? ValidatorZilliqa :
Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse : Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse :
Q extends 'shibarium_deposits' ? ShibariumDepositsResponse : Q extends 'shibarium_deposits' ? ShibariumDepositsResponse :
Q extends 'shibarium_withdrawals_count' ? number : Q extends 'shibarium_withdrawals_count' ? number :
......
...@@ -68,7 +68,7 @@ export default function useNavItems(): ReturnType { ...@@ -68,7 +68,7 @@ export default function useNavItems(): ReturnType {
text: 'Top validators', text: 'Top validators',
nextRoute: { pathname: '/validators' as const }, nextRoute: { pathname: '/validators' as const },
icon: 'validator', icon: 'validator',
isActive: pathname === '/validators', isActive: pathname === '/validators' || pathname === '/validators/[id]',
} : null; } : null;
const rollupDeposits = { const rollupDeposits = {
text: `Deposits (L1${ rightLineArrow }L2)`, text: `Deposits (L1${ rightLineArrow }L2)`,
......
...@@ -51,6 +51,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -51,6 +51,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/name-domains': 'Root page', '/name-domains': 'Root page',
'/name-domains/[name]': 'Regular page', '/name-domains/[name]': 'Regular page',
'/validators': 'Root page', '/validators': 'Root page',
'/validators/[id]': 'Regular page',
'/gas-tracker': 'Root page', '/gas-tracker': 'Root page',
'/mud-worlds': 'Root page', '/mud-worlds': 'Root page',
'/token-transfers': 'Root page', '/token-transfers': 'Root page',
......
...@@ -54,6 +54,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -54,6 +54,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/name-domains': DEFAULT_TEMPLATE, '/name-domains': DEFAULT_TEMPLATE,
'/name-domains/[name]': DEFAULT_TEMPLATE, '/name-domains/[name]': DEFAULT_TEMPLATE,
'/validators': DEFAULT_TEMPLATE, '/validators': DEFAULT_TEMPLATE,
'/validators/[id]': DEFAULT_TEMPLATE,
'/gas-tracker': 'Explore real-time %network_title% gas fees with Blockscout\'s advanced gas fee tracker. Get accurate %network_gwei% estimates and track transaction costs live.', '/gas-tracker': 'Explore real-time %network_title% gas fees with Blockscout\'s advanced gas fee tracker. Get accurate %network_gwei% estimates and track transaction costs live.',
'/mud-worlds': DEFAULT_TEMPLATE, '/mud-worlds': DEFAULT_TEMPLATE,
'/token-transfers': DEFAULT_TEMPLATE, '/token-transfers': DEFAULT_TEMPLATE,
......
...@@ -51,6 +51,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -51,6 +51,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/name-domains': '%network_name% name domains - %network_name% explorer', '/name-domains': '%network_name% name domains - %network_name% explorer',
'/name-domains/[name]': '%network_name% %name% domain details', '/name-domains/[name]': '%network_name% %name% domain details',
'/validators': '%network_name% validators list', '/validators': '%network_name% validators list',
'/validators/[id]': '%network_name% validator %id% details',
'/gas-tracker': 'Track %network_name% gas fees in %network_gwei%', '/gas-tracker': 'Track %network_name% gas fees in %network_gwei%',
'/mud-worlds': '%network_name% MUD worlds list', '/mud-worlds': '%network_name% MUD worlds list',
'/token-transfers': '%network_name% token transfers', '/token-transfers': '%network_name% token transfers',
......
...@@ -49,6 +49,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -49,6 +49,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/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', '/validators': 'Validators list',
'/validators/[id]': 'Validator details',
'/gas-tracker': 'Gas tracker', '/gas-tracker': 'Gas tracker',
'/mud-worlds': 'MUD worlds', '/mud-worlds': 'MUD worlds',
'/token-transfers': 'Token transfers', '/token-transfers': 'Token transfers',
......
import type { ValidatorZilliqa, ValidatorsZilliqaItem, ValidatorsZilliqaResponse } from 'types/api/validators';
export const validator1: ValidatorsZilliqaItem = {
index: 420,
bls_public_key: '0x95125dca41be848801f9bd75254f1faf1ae3194b1da53e9a5684ed7f67b729542482bc521924603b9703c33bf831a100',
balance: '1000000000000000000',
};
export const validatorsResponse: ValidatorsZilliqaResponse = {
items: [ validator1 ],
next_page_params: null,
};
export const validatorDetails: ValidatorZilliqa = {
added_at_block_number: 7527600,
balance: '20000000000000000000000000',
bls_public_key: '0x95125dca41be848801f9bd75254f1faf1ae3194b1da53e9a5684ed7f67b729542482bc521924603b9703c33bf831a100',
control_address: {
ens_domain_name: null,
hash: '0xB4492C468Fe97CB73Ea70a9A712cdd5B5aB621c3',
implementations: [],
is_contract: false,
is_verified: null,
metadata: null,
name: null,
private_tags: [],
proxy_type: null,
public_tags: [],
watchlist_names: [],
},
index: 1,
peer_id: '0x002408011220a8ce8c9a146f3dc411cd72ba845b76722824c55824ac74b3362f070a332d85f2',
reward_address: {
ens_domain_name: null,
hash: '0x0000000000000000000000000000000000000000',
implementations: [],
is_contract: false,
is_verified: null,
metadata: null,
name: null,
private_tags: [],
proxy_type: null,
public_tags: [],
watchlist_names: [],
},
signing_address: {
ens_domain_name: null,
hash: '0x0000000000000000000000000000000000000026',
implementations: [],
is_contract: false,
is_verified: null,
metadata: null,
name: null,
private_tags: [],
proxy_type: null,
public_tags: [],
watchlist_names: [],
},
stake_updated_at_block_number: 7527642,
};
...@@ -244,6 +244,17 @@ export const validators: GetServerSideProps<Props> = async(context) => { ...@@ -244,6 +244,17 @@ export const validators: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
export const validatorDetails: GetServerSideProps<Props> = async(context) => {
const feature = config.features.validators;
if (!feature.isEnabled || feature.chainType !== 'zilliqa') {
return {
notFound: true,
};
}
return base(context);
};
export const gasTracker: GetServerSideProps<Props> = async(context) => { export const gasTracker: GetServerSideProps<Props> = async(context) => {
if (!config.features.gasTracker.isEnabled) { if (!config.features.gasTracker.isEnabled) {
return { return {
......
...@@ -67,6 +67,7 @@ declare module "nextjs-routes" { ...@@ -67,6 +67,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 }>
| DynamicRoute<"/validators/[id]", { "id": string }>
| StaticRoute<"/validators"> | StaticRoute<"/validators">
| StaticRoute<"/verified-contracts"> | StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml"> | StaticRoute<"/visualize/sol2uml">
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
import config from 'configs/app';
const validatorsFeature = config.features.validators;
const ValidatorDetails = dynamic(() => {
if (validatorsFeature.isEnabled && validatorsFeature.chainType === 'zilliqa') {
return import('ui/pages/ValidatorZilliqa');
}
throw new Error('Validators feature is not enabled.');
}, { ssr: false });
const Page: NextPage<Props> = (props) => {
return (
<PageNextJs pathname="/validators/[id]" query={ props.query }>
<ValidatorDetails/>
</PageNextJs>
);
};
export default Page;
export { validatorDetails as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -17,6 +17,10 @@ const Validators = dynamic(() => { ...@@ -17,6 +17,10 @@ const Validators = dynamic(() => {
return import('ui/pages/ValidatorsBlackfort'); return import('ui/pages/ValidatorsBlackfort');
} }
if (validatorsFeature.isEnabled && validatorsFeature.chainType === 'zilliqa') {
return import('ui/pages/ValidatorsZilliqa');
}
throw new Error('Validators feature is not enabled.'); throw new Error('Validators feature is not enabled.');
}, { ssr: false }); }, { ssr: false });
......
...@@ -3,6 +3,8 @@ import type { ...@@ -3,6 +3,8 @@ import type {
ValidatorsStabilityCountersResponse, ValidatorsStabilityCountersResponse,
ValidatorBlackfort, ValidatorBlackfort,
ValidatorsBlackfortCountersResponse, ValidatorsBlackfortCountersResponse,
ValidatorsZilliqaItem,
ValidatorZilliqa,
} from 'types/api/validators'; } from 'types/api/validators';
import { ADDRESS_PARAMS } from './addressParams'; import { ADDRESS_PARAMS } from './addressParams';
...@@ -32,3 +34,21 @@ export const VALIDATORS_BLACKFORT_COUNTERS: ValidatorsBlackfortCountersResponse ...@@ -32,3 +34,21 @@ export const VALIDATORS_BLACKFORT_COUNTERS: ValidatorsBlackfortCountersResponse
new_validators_counter_24h: '11', new_validators_counter_24h: '11',
validators_counter: '140', validators_counter: '140',
}; };
export const VALIDATORS_ZILLIQA_ITEM: ValidatorsZilliqaItem = {
index: 420,
bls_public_key: '0x95125dca41be848801f9bd75254f1faf1ae3194b1da53e9a5684ed7f67b729542482bc521924603b9703c33bf831a100',
balance: '1000000000000000000',
};
export const VALIDATOR_ZILLIQA: ValidatorZilliqa = {
index: 420,
bls_public_key: '0x95125dca41be848801f9bd75254f1faf1ae3194b1da53e9a5684ed7f67b729542482bc521924603b9703c33bf831a100',
balance: '1000000000000000000',
added_at_block_number: 1234567890,
control_address: ADDRESS_PARAMS,
peer_id: '1234567890',
reward_address: ADDRESS_PARAMS,
signing_address: ADDRESS_PARAMS,
stake_updated_at_block_number: 1234567890,
};
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
// Stability
export interface ValidatorStability { export interface ValidatorStability {
address: AddressParam; address: AddressParam;
blocks_validated_count: number; blocks_validated_count: number;
...@@ -37,6 +39,8 @@ export type ValidatorsStabilitySortingField = ValidatorsStabilitySorting['sort'] ...@@ -37,6 +39,8 @@ export type ValidatorsStabilitySortingField = ValidatorsStabilitySorting['sort']
export type ValidatorsStabilitySortingValue = `${ ValidatorsStabilitySortingField }-${ ValidatorsStabilitySorting['order'] }`; export type ValidatorsStabilitySortingValue = `${ ValidatorsStabilitySortingField }-${ ValidatorsStabilitySorting['order'] }`;
// Blackfort
export interface ValidatorBlackfort { export interface ValidatorBlackfort {
address: AddressParam; address: AddressParam;
name: string; name: string;
...@@ -65,3 +69,27 @@ export interface ValidatorsBlackfortSorting { ...@@ -65,3 +69,27 @@ export interface ValidatorsBlackfortSorting {
export type ValidatorsBlackfortSortingField = ValidatorsBlackfortSorting['sort']; export type ValidatorsBlackfortSortingField = ValidatorsBlackfortSorting['sort'];
export type ValidatorsBlackfortSortingValue = `${ ValidatorsBlackfortSortingField }-${ ValidatorsBlackfortSorting['order'] }`; export type ValidatorsBlackfortSortingValue = `${ ValidatorsBlackfortSortingField }-${ ValidatorsBlackfortSorting['order'] }`;
// Zilliqa
export interface ValidatorsZilliqaItem {
index: number;
bls_public_key: string;
balance: string;
}
export interface ValidatorsZilliqaResponse {
items: Array<ValidatorsZilliqaItem>;
next_page_params: null;
}
export interface ValidatorZilliqa {
added_at_block_number: number;
balance: string;
bls_public_key: string;
control_address: AddressParam;
index: number;
peer_id: string;
reward_address: AddressParam;
signing_address: AddressParam;
stake_updated_at_block_number: number;
}
...@@ -3,6 +3,7 @@ import type { ArrayElement } from 'types/utils'; ...@@ -3,6 +3,7 @@ import type { ArrayElement } from 'types/utils';
export const VALIDATORS_CHAIN_TYPE = [ export const VALIDATORS_CHAIN_TYPE = [
'stability', 'stability',
'blackfort', 'blackfort',
'zilliqa',
] as const; ] as const;
export type ValidatorsChainType = ArrayElement<typeof VALIDATORS_CHAIN_TYPE>; export type ValidatorsChainType = ArrayElement<typeof VALIDATORS_CHAIN_TYPE>;
import { Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString';
import { VALIDATOR_ZILLIQA } from 'stubs/validators';
import TextAd from 'ui/shared/ad/TextAd';
import ValidatorEntity from 'ui/shared/entities/validator/ValidatorEntity';
import PageTitle from 'ui/shared/Page/PageTitle';
import ValidatorDetails from 'ui/validators/zilliqa/ValidatorDetails';
const ValidatorZilliqa = () => {
const appProps = useAppContext();
const router = useRouter();
const blsPublicKey = getQueryParamString(router.query.id);
const query = useApiQuery('validator_zilliqa', {
pathParams: { bls_public_key: blsPublicKey },
queryOptions: {
placeholderData: VALIDATOR_ZILLIQA,
},
});
throwOnResourceLoadError(query);
const isLoading = query.isPlaceholderData;
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.endsWith('/validators');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to validators list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
const titleSecondRow = (
<Flex
columnGap={ 3 }
rowGap={ 3 }
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
alignItems="center"
w="100%"
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
>
<ValidatorEntity
id={ query.data?.bls_public_key ?? '' }
isLoading={ isLoading }
noLink
/>
</Flex>
);
return (
<>
<TextAd mb={ 6 }/>
<PageTitle title="Validator details" secondRow={ titleSecondRow } backLink={ backLink }/>
{ query.data && <ValidatorDetails data={ query.data } isLoading={ isLoading }/> }
</>
);
};
export default ValidatorZilliqa;
...@@ -19,10 +19,10 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; ...@@ -19,10 +19,10 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery'; import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import Sort from 'ui/shared/sort/Sort'; import Sort from 'ui/shared/sort/Sort';
import { VALIDATORS_BLACKFORT_SORT_OPTIONS } from 'ui/validatorsBlackfort/utils'; import { VALIDATORS_BLACKFORT_SORT_OPTIONS } from 'ui/validators/blackfort/utils';
import ValidatorsCounters from 'ui/validatorsBlackfort/ValidatorsCounters'; import ValidatorsCounters from 'ui/validators/blackfort/ValidatorsCounters';
import ValidatorsList from 'ui/validatorsBlackfort/ValidatorsList'; import ValidatorsList from 'ui/validators/blackfort/ValidatorsList';
import ValidatorsTable from 'ui/validatorsBlackfort/ValidatorsTable'; import ValidatorsTable from 'ui/validators/blackfort/ValidatorsTable';
const ValidatorsBlackfort = () => { const ValidatorsBlackfort = () => {
const router = useRouter(); const router = useRouter();
......
...@@ -25,11 +25,11 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; ...@@ -25,11 +25,11 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery'; import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import Sort from 'ui/shared/sort/Sort'; import Sort from 'ui/shared/sort/Sort';
import { VALIDATORS_STABILITY_SORT_OPTIONS } from 'ui/validatorsStability/utils'; import { VALIDATORS_STABILITY_SORT_OPTIONS } from 'ui/validators/stability/utils';
import ValidatorsCounters from 'ui/validatorsStability/ValidatorsCounters'; import ValidatorsCounters from 'ui/validators/stability/ValidatorsCounters';
import ValidatorsFilter from 'ui/validatorsStability/ValidatorsFilter'; import ValidatorsFilter from 'ui/validators/stability/ValidatorsFilter';
import ValidatorsList from 'ui/validatorsStability/ValidatorsList'; import ValidatorsList from 'ui/validators/stability/ValidatorsList';
import ValidatorsTable from 'ui/validatorsStability/ValidatorsTable'; import ValidatorsTable from 'ui/validators/stability/ValidatorsTable';
const ValidatorsStability = () => { const ValidatorsStability = () => {
const router = useRouter(); const router = useRouter();
......
import React from 'react';
import * as validatorsMock from 'mocks/validators/zilliqa';
import { test, expect } from 'playwright/lib';
import Validators from './ValidatorsZilliqa';
const chainType = 'zilliqa';
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', chainType ],
]);
await mockApiResponse('validators_zilliqa', validatorsMock.validatorsResponse);
await mockTextAd();
const component = await render(<Validators/>);
await expect(component).toHaveScreenshot();
});
import { Box, Hide, Show } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import { generateListStub } from 'stubs/utils';
import { VALIDATORS_ZILLIQA_ITEM } from 'stubs/validators';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import ValidatorsList from 'ui/validators/zilliqa/ValidatorsList';
import ValidatorsTable from 'ui/validators/zilliqa/ValidatorsTable';
const ValidatorsZilliqa = () => {
const isMobile = useIsMobile();
const { isError, isPlaceholderData, data, pagination } = useQueryWithPages({
resourceName: 'validators_zilliqa',
options: {
enabled: config.features.validators.isEnabled,
placeholderData: generateListStub<'validators_zilliqa'>(
VALIDATORS_ZILLIQA_ITEM,
50,
{ next_page_params: null },
),
},
});
const actionBar = (!isMobile || pagination.isVisible) ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
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 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
return (
<Box>
<PageTitle title="Validators" withTextAd/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no validators."
content={ content }
actionBar={ actionBar }
/>
</Box>
);
};
export default ValidatorsZilliqa;
import type { As } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps } from '../base/utils';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'id'>;
const Link = chakra((props: LinkProps) => {
const defaultHref = route({ pathname: '/validators/[id]', query: { id: props.id } });
return (
<EntityBase.Link
{ ...props }
href={ props.href ?? defaultHref }
>
{ props.children }
</EntityBase.Link>
);
});
const Icon = (props: EntityBase.IconBaseProps) => {
return (
<EntityBase.Icon
{ ...props }
name={ props.name ?? 'key' }
/>
);
};
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'id'>;
const Content = chakra((props: ContentProps) => {
return (
<EntityBase.Content
{ ...props }
text={ props.id }
/>
);
});
type CopyProps = Omit<EntityBase.CopyBaseProps, 'text'> & Pick<EntityProps, 'id'>;
const Copy = (props: CopyProps) => {
return (
<EntityBase.Copy
{ ...props }
text={ props.id }
/>
);
};
const Container = EntityBase.Container;
export interface EntityProps extends EntityBase.EntityBaseProps {
id: string;
}
const UserOpEntity = (props: EntityProps) => {
const partsProps = distributeEntityProps(props);
return (
<Container { ...partsProps.container }>
<Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }>
<Content { ...partsProps.content }/>
</Link>
<Copy { ...partsProps.copy }/>
</Container>
);
};
export default React.memo(chakra<As, EntityProps>(UserOpEntity));
export {
Container,
Link,
Icon,
Content,
Copy,
};
import React from 'react';
import * as validatorsMock from 'mocks/validators/zilliqa';
import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import ValidatorDetails from './ValidatorDetails';
test('base view +@mobile', async({ render, page }) => {
const component = await render(<ValidatorDetails data={ validatorsMock.validatorDetails } isLoading={ false }/>);
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
import { Flex, Grid } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { ValidatorZilliqa } from 'types/api/validators';
import config from 'configs/app';
import Skeleton from 'ui/shared/chakra/Skeleton';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import NativeTokenIcon from 'ui/shared/NativeTokenIcon';
interface Props {
data: ValidatorZilliqa;
isLoading: boolean;
}
const ValidatorDetails = ({ data, isLoading }: Props) => {
return (
<Grid columnGap={ 8 } rowGap={ 3 } templateColumns={{ base: 'minmax(0, 1fr)', lg: 'max-content minmax(728px, auto)' }}>
<DetailsInfoItem.Label
hint="Index of the staker in the committee"
isLoading={ isLoading }
>
Index
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isLoading } display="inline">
{ data.index }
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
hint="Staker's balance"
isLoading={ isLoading }
>
Staked
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<NativeTokenIcon isLoading={ isLoading } boxSize={ 5 } mr={ 2 }/>
<Skeleton isLoaded={ !isLoading } display="inline">
{ BigNumber(data.balance).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() } { config.chain.currency.symbol }
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
hint="libp2p peer ID, corresponding to the staker's BLS public key"
isLoading={ isLoading }
>
Peer ID
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Flex alignItems="center" w="100%" minWidth={ 0 }>
<Skeleton isLoaded={ !isLoading } maxW="calc(100% - 28px)" overflow="hidden">
<HashStringShortenDynamic hash={ data.peer_id }/>
</Skeleton>
<CopyToClipboard text={ data.peer_id } isLoading={ isLoading }/>
</Flex>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
hint="The address used for authenticating requests from this staker to the deposit contract"
isLoading={ isLoading }
>
Control address
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<AddressEntity address={ data.control_address } isLoading={ isLoading }/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
hint="The address which rewards for this staker will be sent to"
isLoading={ isLoading }
>
Reward address
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<AddressEntity address={ data.reward_address } isLoading={ isLoading }/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
hint="The address whose key the validator uses to sign cross-chain events"
isLoading={ isLoading }
>
Signing address
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<AddressEntity address={ data.signing_address } isLoading={ isLoading }/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
hint="Block number at which the staker was added"
isLoading={ isLoading }
>
Added at block
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<BlockEntity number={ data.added_at_block_number } isLoading={ isLoading }/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
hint="Block number at which the staker's stake was last updated"
isLoading={ isLoading }
>
Stake updated
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<BlockEntity number={ data.stake_updated_at_block_number } isLoading={ isLoading }/>
</DetailsInfoItem.Value>
<DetailsSponsoredItem isLoading={ isLoading }/>
</Grid>
);
};
export default React.memo(ValidatorDetails);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { ValidatorsZilliqaItem } from 'types/api/validators';
import ValidatorsListItem from './ValidatorsListItem';
const ValidatorsList = ({ data, isLoading }: { data: Array<ValidatorsZilliqaItem>; isLoading: boolean }) => {
return (
<Box>
{ data.map((item, index) => (
<ValidatorsListItem
key={ item.bls_public_key + (isLoading ? index : '') }
data={ item }
isLoading={ isLoading }
/>
)) }
</Box>
);
};
export default React.memo(ValidatorsList);
import BigNumber from 'bignumber.js';
import React from 'react';
import type { ValidatorsZilliqaItem } from 'types/api/validators';
import config from 'configs/app';
import Skeleton from 'ui/shared/chakra/Skeleton';
import ValidatorEntity from 'ui/shared/entities/validator/ValidatorEntity';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
interface Props {
data: ValidatorsZilliqaItem;
isLoading?: boolean;
}
const ValidatorsListItem = ({ data, isLoading }: Props) => {
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>BLS public key</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ValidatorEntity
isLoading={ isLoading }
id={ data.bls_public_key }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Index</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.index }
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Balance</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ BigNumber(data.balance).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() } { config.chain.currency.symbol }
</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default React.memo(ValidatorsListItem);
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { ValidatorsZilliqaItem } from 'types/api/validators';
import config from 'configs/app';
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import { default as Thead } from 'ui/shared/TheadSticky';
import ValidatorsTableItem from './ValidatorsTableItem';
interface Props {
data: Array<ValidatorsZilliqaItem>;
isLoading?: boolean;
}
const ValidatorsTable = ({ data, isLoading }: Props) => {
return (
<Table>
<Thead top={ ACTION_BAR_HEIGHT_DESKTOP }>
<Tr>
<Th width="50%">BLS public key</Th>
<Th width="25%">Index</Th>
<Th width="25%" isNumeric>
Staked { config.chain.currency.symbol }
</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => (
<ValidatorsTableItem
key={ item.bls_public_key + (isLoading ? index : '') }
data={ item }
isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(ValidatorsTable);
import { Tr, Td } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { ValidatorsZilliqaItem } from 'types/api/validators';
import config from 'configs/app';
import Skeleton from 'ui/shared/chakra/Skeleton';
import ValidatorEntity from 'ui/shared/entities/validator/ValidatorEntity';
interface Props {
data: ValidatorsZilliqaItem;
isLoading?: boolean;
}
const ValidatorsTableItem = ({ data, isLoading }: Props) => {
return (
<Tr>
<Td verticalAlign="middle">
<ValidatorEntity id={ data.bls_public_key } isLoading={ isLoading } fontWeight="700"/>
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.index }
</Skeleton>
</Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ BigNumber(data.balance).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() }
</Skeleton>
</Td>
</Tr>
);
};
export default React.memo(ValidatorsTableItem);
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