diff --git a/configs/envs/.env.blackfort_testnet b/configs/envs/.env.blackfort_testnet new file mode 100644 index 0000000000..eeaad00054 --- /dev/null +++ b/configs/envs/.env.blackfort_testnet @@ -0,0 +1,44 @@ +# Set of ENVs for BXN Testnet network explorer +# https://blackfort-testnet.blockscout.com +# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=blackfort_testnet" + +# Local ENVs +NEXT_PUBLIC_APP_PROTOCOL=http +NEXT_PUBLIC_APP_HOST=localhost +NEXT_PUBLIC_APP_PORT=3000 +NEXT_PUBLIC_APP_ENV=development +NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws + +# Instance ENVs +NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_API_BASE_PATH=/ +NEXT_PUBLIC_API_HOST=blackfort-testnet.blockscout.com +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_INFO_API_HOST=https://contracts-info.services.blockscout.com +NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/blackfort-testnet.json +NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/blackfort.json +NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xcb4140e22cde3412eb5aecdedf2403032c7a251f5c96b11122aca5b1b88ed953 +NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] +NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(92deg, rgb(3, 150, 254) 0.24%, rgb(36, 209, 245) 98.31%) +NEXT_PUBLIC_IS_TESTNET=true +NEXT_PUBLIC_MARKETPLACE_ENABLED=false +NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com +NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 +NEXT_PUBLIC_NETWORK_CURRENCY_NAME=TBXN +NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=TBXN +NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/blackfort.svg +NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/blackfort-dark.svg +NEXT_PUBLIC_NETWORK_ID=4777 +NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/blackfort.svg +NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/blackfort-dark.svg +NEXT_PUBLIC_NETWORK_NAME=BXN Testnet +NEXT_PUBLIC_NETWORK_RPC_URL=https://testnet.blackfort.network/rpc +NEXT_PUBLIC_NETWORK_SHORT_NAME=BXN Testnet +NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true +NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/blackfort.png +NEXT_PUBLIC_STATS_API_HOST=https://stats-blackfort-testnet.k8s.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout +NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true +NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com +NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=blackfort \ No newline at end of file diff --git a/docs/ENVS.md b/docs/ENVS.md index f9e34fa288..4b8d6c10fc 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -679,7 +679,7 @@ The feature enables the Validators page which provides detailed information abou | Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | --- | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE | `'stability'` | Chain type | Required | - | `'stability'` | v1.25.0+ | +| NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE | `'stability' \| 'blackfort'` | Chain type | Required | - | `'stability'` | v1.25.0+ |   diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 826046f9ac..cee68978d0 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -118,7 +118,15 @@ import type { TxInterpretationResponse } from 'types/api/txInterpretation'; import type { TTxsFilters, TTxsWithBlobsFilters } from 'types/api/txsFilters'; import type { TxStateChanges } from 'types/api/txStateChanges'; import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps'; -import type { ValidatorsCountersResponse, ValidatorsFilters, ValidatorsResponse, ValidatorsSorting } from 'types/api/validators'; +import type { + ValidatorsStabilityCountersResponse, + ValidatorsStabilityFilters, + ValidatorsStabilityResponse, + ValidatorsStabilitySorting, + ValidatorsBlackfortCountersResponse, + ValidatorsBlackfortResponse, + ValidatorsBlackfortSorting, +} from 'types/api/validators'; import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals'; import type { @@ -897,14 +905,19 @@ export const RESOURCES = { }, // VALIDATORS - validators: { - path: '/api/v2/validators/:chainType', - pathParams: [ 'chainType' as const ], + validators_stability: { + path: '/api/v2/validators/stability', filterFields: [ 'address_hash' as const, 'state_filter' as const ], }, - validators_counters: { - path: '/api/v2/validators/:chainType/counters', - pathParams: [ 'chainType' as const ], + validators_stability_counters: { + path: '/api/v2/validators/stability/counters', + }, + validators_blackfort: { + path: '/api/v2/validators/blackfort', + filterFields: [], + }, + validators_blackfort_counters: { + path: '/api/v2/validators/blackfort/counters', }, // BLOBS @@ -1002,7 +1015,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward 'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' | -'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators' | 'noves_address_history'; +'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history'; export type PaginatedResponse = ResourcePayload; @@ -1123,8 +1136,10 @@ Q extends 'address_metadata_tag_types' ? PublicTagTypesResponse : Q extends 'blob' ? Blob : Q extends 'marketplace_dapps' ? Array : Q extends 'marketplace_dapp' ? MarketplaceAppOverview : -Q extends 'validators' ? ValidatorsResponse : -Q extends 'validators_counters' ? ValidatorsCountersResponse : +Q extends 'validators_stability' ? ValidatorsStabilityResponse : +Q extends 'validators_stability_counters' ? ValidatorsStabilityCountersResponse : +Q extends 'validators_blackfort' ? ValidatorsBlackfortResponse : +Q extends 'validators_blackfort_counters' ? ValidatorsBlackfortCountersResponse : Q extends 'shibarium_withdrawals' ? ShibariumWithdrawalsResponse : Q extends 'shibarium_deposits' ? ShibariumDepositsResponse : Q extends 'shibarium_withdrawals_count' ? number : @@ -1200,7 +1215,7 @@ Q extends 'verified_contracts' ? VerifiedContractsFilters : Q extends 'addresses_lookup' ? EnsAddressLookupFilters : Q extends 'domains_lookup' ? EnsDomainLookupFilters : Q extends 'user_ops' ? UserOpsFilters : -Q extends 'validators' ? ValidatorsFilters : +Q extends 'validators_stability' ? ValidatorsStabilityFilters : Q extends 'address_mud_tables' ? AddressMudTablesFilter : Q extends 'address_mud_records' ? AddressMudRecordsFilter : never; @@ -1214,7 +1229,8 @@ Q extends 'verified_contracts' ? VerifiedContractsSorting : Q extends 'address_txs' ? TransactionsSorting : Q extends 'addresses_lookup' ? EnsLookupSorting : Q extends 'domains_lookup' ? EnsLookupSorting : -Q extends 'validators' ? ValidatorsSorting : +Q extends 'validators_stability' ? ValidatorsStabilitySorting : +Q extends 'validators_blackfort' ? ValidatorsBlackfortSorting : Q extends 'address_mud_records' ? AddressMudRecordsSorting : never; /* eslint-enable @typescript-eslint/indent */ diff --git a/mocks/validators/blackfort.ts b/mocks/validators/blackfort.ts new file mode 100644 index 0000000000..6a5232a321 --- /dev/null +++ b/mocks/validators/blackfort.ts @@ -0,0 +1,41 @@ +import type { + ValidatorBlackfort, + ValidatorsBlackfortCountersResponse, + ValidatorsBlackfortResponse, +} from 'types/api/validators'; + +import * as addressMock from '../address/address'; + +export const validator1: ValidatorBlackfort = { + address: addressMock.withName, + name: 'testnet-3', + commission: 10, + delegated_amount: '0', + self_bonded_amount: '10000', +}; + +export const validator2: ValidatorBlackfort = { + address: addressMock.withEns, + name: 'GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG', + commission: 5000, + delegated_amount: '10000', + self_bonded_amount: '100', +}; + +export const validator3: ValidatorBlackfort = { + address: addressMock.withoutName, + name: 'testnet-1', + commission: 0, + delegated_amount: '0', + self_bonded_amount: '10000', +}; + +export const validatorsResponse: ValidatorsBlackfortResponse = { + items: [ validator1, validator2, validator3 ], + next_page_params: null, +}; + +export const validatorsCountersResponse: ValidatorsBlackfortCountersResponse = { + new_validators_counter_24h: '11', + validators_counter: '140', +}; diff --git a/mocks/validators/index.ts b/mocks/validators/stability.ts similarity index 56% rename from mocks/validators/index.ts rename to mocks/validators/stability.ts index 22081cae8c..a85f596674 100644 --- a/mocks/validators/index.ts +++ b/mocks/validators/stability.ts @@ -1,31 +1,35 @@ -import type { Validator, ValidatorsCountersResponse, ValidatorsResponse } from 'types/api/validators'; +import type { + ValidatorStability, + ValidatorsStabilityCountersResponse, + ValidatorsStabilityResponse, +} from 'types/api/validators'; import * as addressMock from '../address/address'; -export const validator1: Validator = { +export const validator1: ValidatorStability = { address: addressMock.withName, blocks_validated_count: 7334224, state: 'active', }; -export const validator2: Validator = { +export const validator2: ValidatorStability = { address: addressMock.withEns, blocks_validated_count: 8937453, state: 'probation', }; -export const validator3: Validator = { +export const validator3: ValidatorStability = { address: addressMock.withoutName, blocks_validated_count: 1234, state: 'inactive', }; -export const validatorsResponse: ValidatorsResponse = { +export const validatorsResponse: ValidatorsStabilityResponse = { items: [ validator1, validator2, validator3 ], next_page_params: null, }; -export const validatorsCountersResponse: ValidatorsCountersResponse = { +export const validatorsCountersResponse: ValidatorsStabilityCountersResponse = { active_validators_counter: '42', active_validators_percentage: 7.14, new_validators_counter_24h: '11', diff --git a/pages/validators.tsx b/pages/validators.tsx index 42409f3fd3..a234316a58 100644 --- a/pages/validators.tsx +++ b/pages/validators.tsx @@ -4,7 +4,21 @@ import React from 'react'; import PageNextJs from 'nextjs/PageNextJs'; -const Validators = dynamic(() => import('ui/pages/Validators'), { ssr: false }); +import config from 'configs/app'; + +const validatorsFeature = config.features.validators; + +const Validators = dynamic(() => { + if (validatorsFeature.isEnabled && validatorsFeature.chainType === 'stability') { + return import('ui/pages/ValidatorsStability'); + } + + if (validatorsFeature.isEnabled && validatorsFeature.chainType === 'blackfort') { + return import('ui/pages/ValidatorsBlackfort'); + } + + throw new Error('Validators feature is not enabled.'); +}, { ssr: false }); const Page: NextPage = () => { return ( diff --git a/stubs/validators.ts b/stubs/validators.ts index 1717a59ec0..885818e115 100644 --- a/stubs/validators.ts +++ b/stubs/validators.ts @@ -1,16 +1,34 @@ -import type { Validator, ValidatorsCountersResponse } from 'types/api/validators'; +import type { + ValidatorStability, + ValidatorsStabilityCountersResponse, + ValidatorBlackfort, + ValidatorsBlackfortCountersResponse, +} from 'types/api/validators'; import { ADDRESS_PARAMS } from './addressParams'; -export const VALIDATOR: Validator = { +export const VALIDATOR_STABILITY: ValidatorStability = { address: ADDRESS_PARAMS, blocks_validated_count: 25987, state: 'active', }; -export const VALIDATORS_COUNTERS: ValidatorsCountersResponse = { +export const VALIDATORS_STABILITY_COUNTERS: ValidatorsStabilityCountersResponse = { active_validators_counter: '42', active_validators_percentage: 7.14, new_validators_counter_24h: '11', validators_counter: '140', }; + +export const VALIDATOR_BLACKFORT: ValidatorBlackfort = { + address: ADDRESS_PARAMS, + name: 'testnet-1', + commission: 10, + delegated_amount: '0', + self_bonded_amount: '10000', +}; + +export const VALIDATORS_BLACKFORT_COUNTERS: ValidatorsBlackfortCountersResponse = { + new_validators_counter_24h: '11', + validators_counter: '140', +}; diff --git a/tools/preset-sync/index.ts b/tools/preset-sync/index.ts index 5f83d060e1..5991d1cb7b 100755 --- a/tools/preset-sync/index.ts +++ b/tools/preset-sync/index.ts @@ -5,6 +5,7 @@ import path from 'path'; const PRESETS = { arbitrum: 'https://arbitrum.blockscout.com', base: 'https://base.blockscout.com', + blackfort_testnet: 'https://blackfort-testnet.blockscout.com', celo_alfajores: 'https://celo-alfajores.blockscout.com', eth: 'https://eth.blockscout.com', eth_goerli: 'https://eth-goerli.blockscout.com', diff --git a/types/api/validators.ts b/types/api/validators.ts index ad6e33de96..8a54458544 100644 --- a/types/api/validators.ts +++ b/types/api/validators.ts @@ -1,38 +1,68 @@ import type { AddressParam } from './addressParams'; -export interface Validator { +export interface ValidatorStability { address: AddressParam; blocks_validated_count: number; state: 'active' | 'probation' | 'inactive'; } -export interface ValidatorsResponse { - items: Array; +export interface ValidatorsStabilityResponse { + items: Array; next_page_params: { 'address_hash': string; 'blocks_validated': string; 'items_count': string; - 'state': Validator['state']; + 'state': ValidatorStability['state']; } | null; } -export interface ValidatorsCountersResponse { +export interface ValidatorsStabilityCountersResponse { active_validators_counter: string; active_validators_percentage: number; new_validators_counter_24h: string; validators_counter: string; } -export interface ValidatorsFilters { +export interface ValidatorsStabilityFilters { // address_hash: string | undefined; // right now API doesn't support filtering by address_hash - state_filter: Validator['state'] | undefined; + state_filter: ValidatorStability['state'] | undefined; } -export interface ValidatorsSorting { +export interface ValidatorsStabilitySorting { sort: 'state' | 'blocks_validated'; order: 'asc' | 'desc'; } -export type ValidatorsSortingField = ValidatorsSorting['sort']; +export type ValidatorsStabilitySortingField = ValidatorsStabilitySorting['sort']; -export type ValidatorsSortingValue = `${ ValidatorsSortingField }-${ ValidatorsSorting['order'] }`; +export type ValidatorsStabilitySortingValue = `${ ValidatorsStabilitySortingField }-${ ValidatorsStabilitySorting['order'] }`; + +export interface ValidatorBlackfort { + address: AddressParam; + name: string; + commission: number; + delegated_amount: string; + self_bonded_amount: string; +} + +export interface ValidatorsBlackfortResponse { + items: Array; + next_page_params: { + 'address_hash': string; + 'blocks_validated': string; + } | null; +} + +export interface ValidatorsBlackfortCountersResponse { + new_validators_counter_24h: string; + validators_counter: string; +} + +export interface ValidatorsBlackfortSorting { + sort: 'address_hash'; + order: 'asc' | 'desc'; +} + +export type ValidatorsBlackfortSortingField = ValidatorsBlackfortSorting['sort']; + +export type ValidatorsBlackfortSortingValue = `${ ValidatorsBlackfortSortingField }-${ ValidatorsBlackfortSorting['order'] }`; diff --git a/types/client/validators.ts b/types/client/validators.ts index 9fdc949d5d..3b806c4366 100644 --- a/types/client/validators.ts +++ b/types/client/validators.ts @@ -2,6 +2,7 @@ import type { ArrayElement } from 'types/utils'; export const VALIDATORS_CHAIN_TYPE = [ 'stability', + 'blackfort', ] as const; export type ValidatorsChainType = ArrayElement; diff --git a/ui/pages/ValidatorsBlackfort.pw.tsx b/ui/pages/ValidatorsBlackfort.pw.tsx new file mode 100644 index 0000000000..05df5a40a3 --- /dev/null +++ b/ui/pages/ValidatorsBlackfort.pw.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import * as validatorsMock from 'mocks/validators/blackfort'; +import { test, expect } from 'playwright/lib'; + +import Validators from './ValidatorsBlackfort'; + +const chainType = 'blackfort'; + +test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', chainType ], + ]); + await mockApiResponse('validators_blackfort', validatorsMock.validatorsResponse); + await mockApiResponse('validators_blackfort_counters', validatorsMock.validatorsCountersResponse); + await mockTextAd(); + + const component = await render(); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/ValidatorsBlackfort.tsx b/ui/pages/ValidatorsBlackfort.tsx new file mode 100644 index 0000000000..32b45aa872 --- /dev/null +++ b/ui/pages/ValidatorsBlackfort.tsx @@ -0,0 +1,106 @@ +import { Box, Hide, HStack, Show } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { + ValidatorsBlackfortSorting, + ValidatorsBlackfortSortingField, + ValidatorsBlackfortSortingValue, +} from 'types/api/validators'; + +import config from 'configs/app'; +import { generateListStub } from 'stubs/utils'; +import { VALIDATOR_BLACKFORT } from 'stubs/validators'; +import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } 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 getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; +import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery'; +import Sort from 'ui/shared/sort/Sort'; +import { VALIDATORS_BLACKFORT_SORT_OPTIONS } from 'ui/validatorsBlackfort/utils'; +import ValidatorsCounters from 'ui/validatorsBlackfort/ValidatorsCounters'; +import ValidatorsList from 'ui/validatorsBlackfort/ValidatorsList'; +import ValidatorsTable from 'ui/validatorsBlackfort/ValidatorsTable'; + +const ValidatorsBlackfort = () => { + const router = useRouter(); + const [ sort, setSort ] = + React.useState( + getSortValueFromQuery(router.query, VALIDATORS_BLACKFORT_SORT_OPTIONS), + ); + + const { isError, isPlaceholderData, data, pagination, onSortingChange } = useQueryWithPages({ + resourceName: 'validators_blackfort', + sorting: getSortParamsFromValue(sort), + options: { + enabled: config.features.validators.isEnabled, + placeholderData: generateListStub<'validators_blackfort'>( + VALIDATOR_BLACKFORT, + 50, + { next_page_params: null }, + ), + }, + }); + + const handleSortChange = React.useCallback((value?: ValidatorsBlackfortSortingValue) => { + setSort(value); + onSortingChange(getSortParamsFromValue(value)); + }, [ onSortingChange ]); + + const sortButton = ( + + ); + + const actionBar = ( + <> + + { sortButton } + + { pagination.isVisible && ( + + + + ) } + + ); + + const content = data?.items ? ( + <> + + + + + + + + ) : null; + + return ( + + + + + + ); +}; + +export default ValidatorsBlackfort; diff --git a/ui/pages/Validators.pw.tsx b/ui/pages/ValidatorsStability.pw.tsx similarity index 55% rename from ui/pages/Validators.pw.tsx rename to ui/pages/ValidatorsStability.pw.tsx index 605328b313..fcd3467754 100644 --- a/ui/pages/Validators.pw.tsx +++ b/ui/pages/ValidatorsStability.pw.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import * as validatorsMock from 'mocks/validators/index'; +import * as validatorsMock from 'mocks/validators/stability'; import { test, expect } from 'playwright/lib'; -import Validators from './Validators'; +import Validators from './ValidatorsStability'; const chainType = 'stability'; @@ -11,8 +11,8 @@ test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd await mockEnvs([ [ 'NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE', chainType ], ]); - await mockApiResponse('validators', validatorsMock.validatorsResponse, { pathParams: { chainType } }); - await mockApiResponse('validators_counters', validatorsMock.validatorsCountersResponse, { pathParams: { chainType } }); + await mockApiResponse('validators_stability', validatorsMock.validatorsResponse); + await mockApiResponse('validators_stability_counters', validatorsMock.validatorsCountersResponse); await mockTextAd(); const component = await render(); diff --git a/ui/pages/Validators.tsx b/ui/pages/ValidatorsStability.tsx similarity index 71% rename from ui/pages/Validators.tsx rename to ui/pages/ValidatorsStability.tsx index 8ef1b8b93f..8b518b4356 100644 --- a/ui/pages/Validators.tsx +++ b/ui/pages/ValidatorsStability.tsx @@ -2,8 +2,12 @@ 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 type { + ValidatorsStabilityFilters, + ValidatorsStabilitySorting, + ValidatorsStabilitySortingField, + ValidatorsStabilitySortingValue, +} from 'types/api/validators'; import config from 'configs/app'; // import useDebounce from 'lib/hooks/useDebounce'; @@ -11,7 +15,7 @@ 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 { VALIDATOR_STABILITY } from 'stubs/validators'; import ActionBar from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; // import FilterInput from 'ui/shared/filters/FilterInput'; @@ -21,35 +25,36 @@ 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'; +import { VALIDATORS_STABILITY_SORT_OPTIONS } from 'ui/validatorsStability/utils'; +import ValidatorsCounters from 'ui/validatorsStability/ValidatorsCounters'; +import ValidatorsFilter from 'ui/validatorsStability/ValidatorsFilter'; +import ValidatorsList from 'ui/validatorsStability/ValidatorsList'; +import ValidatorsTable from 'ui/validatorsStability/ValidatorsTable'; -const Validators = () => { +const ValidatorsStability = () => { 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(getSortValueFromQuery(router.query, SORT_OPTIONS)); + const [ statusFilter, setStatusFilter ] = + React.useState(getQueryParamString(router.query.state_filter) as ValidatorsStabilityFilters['state_filter'] || undefined); + const [ sort, setSort ] = React.useState( + getSortValueFromQuery(router.query, VALIDATORS_STABILITY_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 }, + resourceName: 'validators_stability', filters: { // address_hash: debouncedSearchTerm, state_filter: statusFilter, }, - sorting: getSortParamsFromValue(sort), + sorting: getSortParamsFromValue(sort), options: { enabled: config.features.validators.isEnabled, - placeholderData: generateListStub<'validators'>( - VALIDATOR, + placeholderData: generateListStub<'validators_stability'>( + VALIDATOR_STABILITY, 50, { next_page_params: null }, ), @@ -69,7 +74,7 @@ const Validators = () => { return; } - const state = value === 'all' ? undefined : value as ValidatorsFilters['state_filter']; + const state = value === 'all' ? undefined : value as ValidatorsStabilityFilters['state_filter']; onFilterChange({ // address_hash: debouncedSearchTerm, @@ -78,12 +83,13 @@ const Validators = () => { setStatusFilter(state); }, [ onFilterChange ]); - const handleSortChange = React.useCallback((value?: ValidatorsSortingValue) => { + const handleSortChange = React.useCallback((value?: ValidatorsStabilitySortingValue) => { setSort(value); onSortingChange(getSortParamsFromValue(value)); }, [ onSortingChange ]); - const filterMenu = ; + const filterMenu = + ; // const filterInput = ( // { ); @@ -141,7 +147,7 @@ const Validators = () => { { ); }; -export default Validators; +export default ValidatorsStability; diff --git a/ui/pages/__screenshots__/ValidatorsBlackfort.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ValidatorsBlackfort.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..381b041f88 Binary files /dev/null and b/ui/pages/__screenshots__/ValidatorsBlackfort.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/ValidatorsBlackfort.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ValidatorsBlackfort.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..4ead093d77 Binary files /dev/null and b/ui/pages/__screenshots__/ValidatorsBlackfort.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/Validators.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/ValidatorsStability.pw.tsx_default_base-view-mobile-1.png similarity index 100% rename from ui/pages/__screenshots__/Validators.pw.tsx_default_base-view-mobile-1.png rename to ui/pages/__screenshots__/ValidatorsStability.pw.tsx_default_base-view-mobile-1.png diff --git a/ui/pages/__screenshots__/Validators.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/ValidatorsStability.pw.tsx_mobile_base-view-mobile-1.png similarity index 100% rename from ui/pages/__screenshots__/Validators.pw.tsx_mobile_base-view-mobile-1.png rename to ui/pages/__screenshots__/ValidatorsStability.pw.tsx_mobile_base-view-mobile-1.png diff --git a/ui/shared/statusTag/ValidatorStatus.tsx b/ui/shared/statusTag/ValidatorStabilityStatus.tsx similarity index 66% rename from ui/shared/statusTag/ValidatorStatus.tsx rename to ui/shared/statusTag/ValidatorStabilityStatus.tsx index 4386b55af5..93b1bb771f 100644 --- a/ui/shared/statusTag/ValidatorStatus.tsx +++ b/ui/shared/statusTag/ValidatorStabilityStatus.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import type { Validator } from 'types/api/validators'; +import type { ValidatorStability } from 'types/api/validators'; import StatusTag from './StatusTag'; interface Props { - state: Validator['state']; + state: ValidatorStability['state']; isLoading?: boolean; } -const ValidatorStatus = ({ state, isLoading }: Props) => { +const ValidatorStabilityStatus = ({ state, isLoading }: Props) => { switch (state) { case 'active': return ; @@ -20,4 +20,4 @@ const ValidatorStatus = ({ state, isLoading }: Props) => { } }; -export default React.memo(ValidatorStatus); +export default React.memo(ValidatorStabilityStatus); diff --git a/ui/validatorsBlackfort/ValidatorsCounters.tsx b/ui/validatorsBlackfort/ValidatorsCounters.tsx new file mode 100644 index 0000000000..0901e57688 --- /dev/null +++ b/ui/validatorsBlackfort/ValidatorsCounters.tsx @@ -0,0 +1,33 @@ +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; +import { VALIDATORS_BLACKFORT_COUNTERS } from 'stubs/validators'; +import StatsWidget from 'ui/shared/stats/StatsWidget'; + +const ValidatorsCounters = () => { + const countersQuery = useApiQuery('validators_blackfort_counters', { + queryOptions: { + enabled: config.features.validators.isEnabled, + placeholderData: VALIDATORS_BLACKFORT_COUNTERS, + }, + }); + + if (!countersQuery.data) { + return null; + } + + return ( + + + + ); +}; + +export default React.memo(ValidatorsCounters); diff --git a/ui/validators/ValidatorsList.tsx b/ui/validatorsBlackfort/ValidatorsList.tsx similarity index 81% rename from ui/validators/ValidatorsList.tsx rename to ui/validatorsBlackfort/ValidatorsList.tsx index abb7305ca0..90c24fea7e 100644 --- a/ui/validators/ValidatorsList.tsx +++ b/ui/validatorsBlackfort/ValidatorsList.tsx @@ -1,11 +1,11 @@ import { Box } from '@chakra-ui/react'; import React from 'react'; -import type { Validator } from 'types/api/validators'; +import type { ValidatorBlackfort } from 'types/api/validators'; import ValidatorsListItem from './ValidatorsListItem'; -const ValidatorsList = ({ data, isLoading }: { data: Array; isLoading: boolean }) => { +const ValidatorsList = ({ data, isLoading }: { data: Array; isLoading: boolean }) => { return ( { data.map((item, index) => ( diff --git a/ui/validatorsBlackfort/ValidatorsListItem.tsx b/ui/validatorsBlackfort/ValidatorsListItem.tsx new file mode 100644 index 0000000000..a01a144378 --- /dev/null +++ b/ui/validatorsBlackfort/ValidatorsListItem.tsx @@ -0,0 +1,62 @@ +import { Flex, Skeleton } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { ValidatorBlackfort } from 'types/api/validators'; + +import config from 'configs/app'; +import { currencyUnits } from 'lib/units'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; +import TruncatedValue from 'ui/shared/TruncatedValue'; + +interface Props { + data: ValidatorBlackfort; + isLoading?: boolean; +} + +const ValidatorsListItem = ({ data, isLoading }: Props) => { + + return ( + + + Address + + + + + Name + + + + + Commission + + + { `${ data.commission / 100 }%` } + + + + Self bonded + + + { `${ BigNumber(data.self_bonded_amount).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() } ${ currencyUnits.ether }` } + + + + Delegated amount + + + { `${ BigNumber(data.delegated_amount).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() } ${ currencyUnits.ether }` } + + + + + ); +}; + +export default React.memo(ValidatorsListItem); diff --git a/ui/validatorsBlackfort/ValidatorsTable.tsx b/ui/validatorsBlackfort/ValidatorsTable.tsx new file mode 100644 index 0000000000..cf7b839b7f --- /dev/null +++ b/ui/validatorsBlackfort/ValidatorsTable.tsx @@ -0,0 +1,68 @@ +import { Table, Tbody, Tr, Th, Link } from '@chakra-ui/react'; +import React from 'react'; + +import type { + ValidatorBlackfort, + ValidatorsBlackfortSorting, + ValidatorsBlackfortSortingField, + ValidatorsBlackfortSortingValue, +} from 'types/api/validators'; + +import { currencyUnits } from 'lib/units'; +import IconSvg from 'ui/shared/IconSvg'; +import getNextSortValue from 'ui/shared/sort/getNextSortValue'; +import { default as Thead } from 'ui/shared/TheadSticky'; + +import { VALIDATORS_BLACKFORT_SORT_SEQUENCE } from './utils'; +import ValidatorsTableItem from './ValidatorsTableItem'; + +interface Props { + data: Array; + sort: ValidatorsBlackfortSortingValue | undefined; + setSorting: (val: ValidatorsBlackfortSortingValue | undefined) => void; + isLoading?: boolean; + top: number; +} + +const ValidatorsTable = ({ data, sort, setSorting, isLoading, top }: Props) => { + const sortIconTransform = sort?.includes('asc' as ValidatorsBlackfortSorting['order']) ? 'rotate(-90deg)' : 'rotate(90deg)'; + + const onSortToggle = React.useCallback((field: ValidatorsBlackfortSortingField) => () => { + const value = getNextSortValue(VALIDATORS_BLACKFORT_SORT_SEQUENCE, field)(sort); + setSorting(value); + }, [ sort, setSorting ]); + + return ( + + + + + + + + + + + + { data.map((item, index) => ( + + )) } + +
+ + { sort?.includes('address') && } + Validator’s address + + NameCommission{ `Self bonded ${ currencyUnits.ether }` }{ `Delegated amount ${ currencyUnits.ether }` }
+ ); +}; + +export default React.memo(ValidatorsTable); diff --git a/ui/validatorsBlackfort/ValidatorsTableItem.tsx b/ui/validatorsBlackfort/ValidatorsTableItem.tsx new file mode 100644 index 0000000000..35e7cd0cbc --- /dev/null +++ b/ui/validatorsBlackfort/ValidatorsTableItem.tsx @@ -0,0 +1,50 @@ +import { Tr, Td, Skeleton, Flex } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { ValidatorBlackfort } from 'types/api/validators'; + +import config from 'configs/app'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import TruncatedValue from 'ui/shared/TruncatedValue'; + +interface Props { + data: ValidatorBlackfort; + isLoading?: boolean; +} + +const ValidatorsTableItem = ({ data, isLoading }: Props) => { + return ( + + + + + + + + + + + + { `${ data.commission / 100 }%` } + + + + + { BigNumber(data.self_bonded_amount).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() } + + + + + { BigNumber(data.delegated_amount).div(BigNumber(10 ** config.chain.currency.decimals)).toFormat() } + + + + ); +}; + +export default React.memo(ValidatorsTableItem); diff --git a/ui/validatorsBlackfort/utils.ts b/ui/validatorsBlackfort/utils.ts new file mode 100644 index 0000000000..a146199674 --- /dev/null +++ b/ui/validatorsBlackfort/utils.ts @@ -0,0 +1,16 @@ +import type { + ValidatorsBlackfortSortingValue, + ValidatorsBlackfortSortingField, +} from 'types/api/validators'; + +import type { TOption } from 'ui/shared/sort/Option'; + +export const VALIDATORS_BLACKFORT_SORT_OPTIONS: Array> = [ + { title: 'Default', id: undefined }, + { title: 'Address descending', id: 'address_hash-desc' }, + { title: 'Address ascending', id: 'address_hash-asc' }, +]; + +export const VALIDATORS_BLACKFORT_SORT_SEQUENCE: Record> = { + address_hash: [ 'address_hash-desc', 'address_hash-asc', undefined ], +}; diff --git a/ui/validators/ValidatorsCounters.tsx b/ui/validatorsStability/ValidatorsCounters.tsx similarity index 77% rename from ui/validators/ValidatorsCounters.tsx rename to ui/validatorsStability/ValidatorsCounters.tsx index 2e056ff1aa..3e76e81312 100644 --- a/ui/validators/ValidatorsCounters.tsx +++ b/ui/validatorsStability/ValidatorsCounters.tsx @@ -1,19 +1,16 @@ 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 { VALIDATORS_STABILITY_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 }, + const countersQuery = useApiQuery('validators_stability_counters', { queryOptions: { enabled: config.features.validators.isEnabled, - placeholderData: VALIDATORS_COUNTERS, + placeholderData: VALIDATORS_STABILITY_COUNTERS, }, }); diff --git a/ui/validators/ValidatorsFilter.tsx b/ui/validatorsStability/ValidatorsFilter.tsx similarity index 83% rename from ui/validators/ValidatorsFilter.tsx rename to ui/validatorsStability/ValidatorsFilter.tsx index d0cf690c73..bcda1b5984 100644 --- a/ui/validators/ValidatorsFilter.tsx +++ b/ui/validatorsStability/ValidatorsFilter.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type { ValidatorsFilters } from 'types/api/validators'; +import type { ValidatorsStabilityFilters } from 'types/api/validators'; import PopoverFilterRadio from 'ui/shared/filters/PopoverFilterRadio'; @@ -13,7 +13,7 @@ const OPTIONS = [ interface Props { hasActiveFilter: boolean; - defaultValue: ValidatorsFilters['state_filter'] | undefined; + defaultValue: ValidatorsStabilityFilters['state_filter'] | undefined; onChange: (nextValue: string | Array) => void; } diff --git a/ui/validatorsStability/ValidatorsList.tsx b/ui/validatorsStability/ValidatorsList.tsx new file mode 100644 index 0000000000..1193a908c3 --- /dev/null +++ b/ui/validatorsStability/ValidatorsList.tsx @@ -0,0 +1,22 @@ +import { Box } from '@chakra-ui/react'; +import React from 'react'; + +import type { ValidatorStability } from 'types/api/validators'; + +import ValidatorsListItem from './ValidatorsListItem'; + +const ValidatorsList = ({ data, isLoading }: { data: Array; isLoading: boolean }) => { + return ( + + { data.map((item, index) => ( + + )) } + + ); +}; + +export default React.memo(ValidatorsList); diff --git a/ui/validators/ValidatorsListItem.tsx b/ui/validatorsStability/ValidatorsListItem.tsx similarity index 88% rename from ui/validators/ValidatorsListItem.tsx rename to ui/validatorsStability/ValidatorsListItem.tsx index 35c97654d6..668bad3e86 100644 --- a/ui/validators/ValidatorsListItem.tsx +++ b/ui/validatorsStability/ValidatorsListItem.tsx @@ -1,14 +1,14 @@ import { Skeleton } from '@chakra-ui/react'; import React from 'react'; -import type { Validator } from 'types/api/validators'; +import type { ValidatorStability } 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'; +import ValidatorStatus from 'ui/shared/statusTag/ValidatorStabilityStatus'; interface Props { - data: Validator; + data: ValidatorStability; isLoading?: boolean; } diff --git a/ui/validators/ValidatorsTable.tsx b/ui/validatorsStability/ValidatorsTable.tsx similarity index 71% rename from ui/validators/ValidatorsTable.tsx rename to ui/validatorsStability/ValidatorsTable.tsx index 3955fa01a8..dd68f96109 100644 --- a/ui/validators/ValidatorsTable.tsx +++ b/ui/validatorsStability/ValidatorsTable.tsx @@ -1,28 +1,33 @@ 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 type { + ValidatorStability, + ValidatorsStabilitySorting, + ValidatorsStabilitySortingField, + ValidatorsStabilitySortingValue, +} from 'types/api/validators'; import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; 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 { VALIDATORS_STABILITY_SORT_SEQUENCE } from './utils'; import ValidatorsTableItem from './ValidatorsTableItem'; interface Props { - data: Array; - sort: ValidatorsSortingValue | undefined; - setSorting: (val: ValidatorsSortingValue | undefined) => void; + data: Array; + sort: ValidatorsStabilitySortingValue | undefined; + setSorting: (val: ValidatorsStabilitySortingValue | undefined) => void; isLoading?: boolean; } const ValidatorsTable = ({ data, sort, setSorting, isLoading }: Props) => { - const sortIconTransform = sort?.includes('asc' as ValidatorsSorting['order']) ? 'rotate(-90deg)' : 'rotate(90deg)'; + const sortIconTransform = sort?.includes('asc' as ValidatorsStabilitySorting['order']) ? 'rotate(-90deg)' : 'rotate(90deg)'; - const onSortToggle = React.useCallback((field: ValidatorsSortingField) => () => { - const value = getNextSortValue(SORT_SEQUENCE, field)(sort); + const onSortToggle = React.useCallback((field: ValidatorsStabilitySortingField) => () => { + const value = getNextSortValue(VALIDATORS_STABILITY_SORT_SEQUENCE, field)(sort); setSorting(value); }, [ sort, setSorting ]); diff --git a/ui/validators/ValidatorsTableItem.tsx b/ui/validatorsStability/ValidatorsTableItem.tsx similarity index 83% rename from ui/validators/ValidatorsTableItem.tsx rename to ui/validatorsStability/ValidatorsTableItem.tsx index e4c600357c..7a1231b796 100644 --- a/ui/validators/ValidatorsTableItem.tsx +++ b/ui/validatorsStability/ValidatorsTableItem.tsx @@ -1,13 +1,13 @@ import { Tr, Td, Skeleton } from '@chakra-ui/react'; import React from 'react'; -import type { Validator } from 'types/api/validators'; +import type { ValidatorStability } from 'types/api/validators'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import ValidatorStatus from 'ui/shared/statusTag/ValidatorStatus'; +import ValidatorStatus from 'ui/shared/statusTag/ValidatorStabilityStatus'; interface Props { - data: Validator; + data: ValidatorStability; isLoading?: boolean; } diff --git a/ui/validators/utils.ts b/ui/validatorsStability/utils.ts similarity index 57% rename from ui/validators/utils.ts rename to ui/validatorsStability/utils.ts index 71558189c3..d3e37f8bf0 100644 --- a/ui/validators/utils.ts +++ b/ui/validatorsStability/utils.ts @@ -1,8 +1,11 @@ -import type { ValidatorsSortingValue, ValidatorsSortingField } from 'types/api/validators'; +import type { + ValidatorsStabilitySortingValue, + ValidatorsStabilitySortingField, +} from 'types/api/validators'; import type { TOption } from 'ui/shared/sort/Option'; -export const SORT_OPTIONS: Array> = [ +export const VALIDATORS_STABILITY_SORT_OPTIONS: Array> = [ { title: 'Default', id: undefined }, { title: 'Status descending', id: 'state-desc' }, { title: 'Status ascending', id: 'state-asc' }, @@ -10,7 +13,7 @@ export const SORT_OPTIONS: Array> = [ { title: 'Blocks validated ascending', id: 'blocks_validated-asc' }, ]; -export const SORT_SEQUENCE: Record> = { +export const VALIDATORS_STABILITY_SORT_SEQUENCE: Record> = { state: [ 'state-desc', 'state-asc', undefined ], blocks_validated: [ 'blocks_validated-desc', 'blocks_validated-asc', undefined ], };