feat(1263): split validator page into separate tables (#2250)
* frontend-monorepo-1263 validator status of pending now named 'candidate' in the ui * feat(1263): validator table updates, calculating penalties * feat(1263): bit of cleanup * feat(1263): tables built and values derived * feat(1263): tweaks * feat(1263): upping site max width to 1500px * feat(1263): tweak to normalised voting power column width * feat(1263): ensure validator rows when hovered have cursor:pointer * feat(1263): consensus validators now have a ranking field by voting power shown in the table * feat(1263): fixed linting errors * feat(1263): a couple of extra tests and correct import of schema to use enums * Update apps/token/src/routes/staking/home/validator-tables/standby-pending-validators-table.tsx Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * Update apps/token/src/routes/staking/home/validator-tables/validator-tables.tsx Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * Update apps/token/src/routes/staking/home/validator-tables/validator-tables.tsx Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * Update apps/token/src/routes/staking/home/validator-tables/validator-tables.tsx Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * feat(1263): tweaks from PR comments, initial push of util to remove 'node' from arrs * feat(1263): updated new util to remove 'node' from 'edges'. Implemented across the monorepo * feat(1263): cleaned up splitting validators into consensus, pending and ersatz to put into tables * feat(1263): used decimals from app state in validator-tables.tsx * feat(1263): removed redundant compact import Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
This commit is contained in:
parent
5bc861b5ec
commit
7c8a54b35a
@ -1,11 +1,14 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import {
|
import {
|
||||||
DealTicketManager,
|
DealTicketManager,
|
||||||
usePartyBalanceQuery,
|
usePartyBalanceQuery,
|
||||||
} from '@vegaprotocol/deal-ticket';
|
} from '@vegaprotocol/deal-ticket';
|
||||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
|
t,
|
||||||
|
useDataProvider,
|
||||||
|
removePaginationWrapper,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { DealTicketSteps } from './deal-ticket-steps';
|
import { DealTicketSteps } from './deal-ticket-steps';
|
||||||
import { DealTicketBalance } from './deal-ticket-balance';
|
import { DealTicketBalance } from './deal-ticket-balance';
|
||||||
@ -46,8 +49,8 @@ export const DealTicketContainer = () => {
|
|||||||
skip: !marketId,
|
skip: !marketId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const accounts = compact(partyData?.party?.accountsConnection?.edges).map(
|
const accounts = removePaginationWrapper(
|
||||||
(e) => e.node
|
partyData?.party?.accountsConnection?.edges
|
||||||
);
|
);
|
||||||
|
|
||||||
const loader = <Loader />;
|
const loader = <Loader />;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import { Stepper } from '../stepper';
|
import { Stepper } from '../stepper';
|
||||||
import {
|
import {
|
||||||
getDefaultOrder,
|
getDefaultOrder,
|
||||||
@ -25,6 +24,7 @@ import {
|
|||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
addDecimal,
|
addDecimal,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
removePaginationWrapper,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import {
|
import {
|
||||||
useOrderSubmit,
|
useOrderSubmit,
|
||||||
@ -82,8 +82,8 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
|||||||
skip: !pubKey,
|
skip: !pubKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const accounts = compact(partyBalance?.party?.accountsConnection?.edges).map(
|
const accounts = removePaginationWrapper(
|
||||||
(e) => e.node
|
partyBalance?.party?.accountsConnection?.edges
|
||||||
);
|
);
|
||||||
const maxTrade = useMaximumPositionSize({
|
const maxTrade = useMaximumPositionSize({
|
||||||
partyId: pubKey || '',
|
partyId: pubKey || '',
|
||||||
|
@ -7,14 +7,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
removePaginationWrapper,
|
||||||
|
suitableForSyntaxHighlighter,
|
||||||
t,
|
t,
|
||||||
|
useNetworkParamsQuery,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { suitableForSyntaxHighlighter } from '@vegaprotocol/react-helpers';
|
|
||||||
import { RouteTitle } from '../../components/route-title';
|
import { RouteTitle } from '../../components/route-title';
|
||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
import type { NetworkParamsQuery } from '@vegaprotocol/react-helpers';
|
import type { NetworkParamsQuery } from '@vegaprotocol/react-helpers';
|
||||||
import { useNetworkParamsQuery } from '@vegaprotocol/react-helpers';
|
|
||||||
import compact from 'lodash/compact';
|
|
||||||
|
|
||||||
const PERCENTAGE_PARAMS = [
|
const PERCENTAGE_PARAMS = [
|
||||||
'governance.proposal.asset.requiredMajority',
|
'governance.proposal.asset.requiredMajority',
|
||||||
@ -107,9 +107,7 @@ export const NetworkParametersTable = ({
|
|||||||
error={error}
|
error={error}
|
||||||
render={(data) => {
|
render={(data) => {
|
||||||
const ascParams = orderBy(
|
const ascParams = orderBy(
|
||||||
compact(data.networkParametersConnection.edges).map(
|
removePaginationWrapper(data.networkParametersConnection.edges),
|
||||||
({ node }) => node
|
|
||||||
) || [],
|
|
||||||
(param) => param.key,
|
(param) => param.key,
|
||||||
'asc'
|
'asc'
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_VEGA_ENV=DEVNET
|
NX_VEGA_ENV=DEVNET
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
||||||
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
|
NX_VEGA_URL=https://api.n00.devnet1.vega.xyz/graphql
|
||||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_VEGA_ENV=TESTNET
|
NX_VEGA_ENV=TESTNET
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
||||||
NX_VEGA_URL=https://api.n11.testnet.vega.xyz/graphql
|
NX_VEGA_URL=https://api.n10.testnet.vega.xyz/graphql
|
||||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
@ -88,7 +88,7 @@ const Web3Container = ({
|
|||||||
<AppLoader>
|
<AppLoader>
|
||||||
<BalanceManager>
|
<BalanceManager>
|
||||||
<>
|
<>
|
||||||
<div className="app w-full max-w-[1300px] mx-auto grid grid-rows-[1fr_min-content] min-h-full border-neutral-700 lg:border-l lg:border-r lg:text-body-large">
|
<div className="app w-full max-w-[1500px] mx-auto grid grid-rows-[1fr_min-content] min-h-full border-neutral-700 lg:border-l lg:border-r lg:text-body-large">
|
||||||
<TemplateSidebar sidebar={sideBar}>
|
<TemplateSidebar sidebar={sideBar}>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</TemplateSidebar>
|
</TemplateSidebar>
|
||||||
|
@ -4,7 +4,6 @@ import keyBy from 'lodash/keyBy';
|
|||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import compact from 'lodash/compact';
|
|
||||||
|
|
||||||
import noIcon from '../../images/token-no-icon.png';
|
import noIcon from '../../images/token-no-icon.png';
|
||||||
import vegaBlack from '../../images/vega_black.png';
|
import vegaBlack from '../../images/vega_black.png';
|
||||||
@ -15,7 +14,7 @@ import { useContracts } from '../../contexts/contracts/contracts-context';
|
|||||||
import type { ERC20Asset } from '@vegaprotocol/assets';
|
import type { ERC20Asset } from '@vegaprotocol/assets';
|
||||||
import { isAssetTypeERC20 } from '@vegaprotocol/assets';
|
import { isAssetTypeERC20 } from '@vegaprotocol/assets';
|
||||||
import { Schema } from '@vegaprotocol/types';
|
import { Schema } from '@vegaprotocol/types';
|
||||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
import { removePaginationWrapper, toBigNum } from '@vegaprotocol/react-helpers';
|
||||||
import { useAppState } from '../../contexts/app-state/app-state-context';
|
import { useAppState } from '../../contexts/app-state/app-state-context';
|
||||||
import { addDecimal } from '@vegaprotocol/react-helpers';
|
import { addDecimal } from '@vegaprotocol/react-helpers';
|
||||||
import type {
|
import type {
|
||||||
@ -66,9 +65,9 @@ export const usePollForDelegations = () => {
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
const canonisedDelegations = compact(
|
const canonisedDelegations = removePaginationWrapper(
|
||||||
res.data.party?.delegationsConnection?.edges
|
res.data.party?.delegationsConnection?.edges
|
||||||
).map(({ node }) => node);
|
);
|
||||||
const filter =
|
const filter =
|
||||||
canonisedDelegations.filter((d) => {
|
canonisedDelegations.filter((d) => {
|
||||||
return d.epoch.toString() === res.data.epoch.id;
|
return d.epoch.toString() === res.data.epoch.id;
|
||||||
@ -85,9 +84,9 @@ export const usePollForDelegations = () => {
|
|||||||
decimals
|
decimals
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const accounts = compact(
|
const accounts = removePaginationWrapper(
|
||||||
res.data.party?.accountsConnection?.edges
|
res.data.party?.accountsConnection?.edges
|
||||||
).map((e) => e.node);
|
);
|
||||||
setAccounts(
|
setAccounts(
|
||||||
accounts
|
accounts
|
||||||
.filter(
|
.filter(
|
||||||
|
@ -574,19 +574,25 @@
|
|||||||
"proposalTerms": "Proposal terms",
|
"proposalTerms": "Proposal terms",
|
||||||
"currentlySetTo": "Currently expected to ",
|
"currentlySetTo": "Currently expected to ",
|
||||||
"finalOutcomeMayDiffer": "Final outcome may differ",
|
"finalOutcomeMayDiffer": "Final outcome may differ",
|
||||||
"rankingScore": "Ranking score",
|
|
||||||
"stakeScore": "Stake score",
|
|
||||||
"performanceScore": "Performance",
|
|
||||||
"votingPower": "Voting power",
|
"votingPower": "Voting power",
|
||||||
|
"normalisedVotingPower": "Normalised voting power",
|
||||||
"noValidators": "No validators",
|
"noValidators": "No validators",
|
||||||
"validator": "Validator",
|
"validator": "Validator",
|
||||||
"totalStakeThisEpoch": "Total stake this epoch",
|
"stake": "Stake",
|
||||||
"share": "Share",
|
"stakeShare": "Stake share",
|
||||||
"validatorStake": "Validator stake",
|
"pendingStake": "Pending stake",
|
||||||
|
"totalPenalties": "Total penalties",
|
||||||
|
"noPenaltyDataFromLastEpoch": "No penalty data from last epoch",
|
||||||
|
"stakeNeededForPromotion": "Stake needed for promotion",
|
||||||
"Nodes": "Nodes",
|
"Nodes": "Nodes",
|
||||||
"status-tendermint": "Consensus",
|
"status-tendermint": "Consensus",
|
||||||
"status-ersatz": "Standby",
|
"status-ersatz": "Standby",
|
||||||
"status-pending": "Pending",
|
"status-pending": "Candidate",
|
||||||
|
"ersatzDescription": "To be promoted, a standby validator must have more than the lowest consensus stake, plus a bonus given to existing validators. This currently requires a minimum of {{stakeNeededForPromotion}} stake assuming no penalties. Only one validator per epoch can be promoted.",
|
||||||
|
"pendingDescription1": "Anyone can",
|
||||||
|
"pendingDescriptionLinkText": "set up and run a node on Vega",
|
||||||
|
"pendingDescription2": ". A node can move from Pending into Standby based on how much nomination it attracts, assuming it has proven reliability by sending heartbeats to the network.",
|
||||||
|
"n/a": "N/A",
|
||||||
"Set to": "Set to",
|
"Set to": "Set to",
|
||||||
"pass": "pass",
|
"pass": "pass",
|
||||||
"fail": "fail",
|
"fail": "fail",
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import compact from 'lodash/compact';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -11,7 +10,11 @@ import type {
|
|||||||
RewardFieldsFragment,
|
RewardFieldsFragment,
|
||||||
DelegationFieldsFragment,
|
DelegationFieldsFragment,
|
||||||
} from './__generated___/Rewards';
|
} from './__generated___/Rewards';
|
||||||
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
|
formatNumber,
|
||||||
|
removePaginationWrapper,
|
||||||
|
toBigNum,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||||
|
|
||||||
interface RewardInfoProps {
|
interface RewardInfoProps {
|
||||||
@ -25,10 +28,7 @@ export const RewardInfo = ({ data, currVegaKey }: RewardInfoProps) => {
|
|||||||
const rewards = React.useMemo(() => {
|
const rewards = React.useMemo(() => {
|
||||||
if (!data?.party || !data.party.rewardsConnection?.edges?.length) return [];
|
if (!data?.party || !data.party.rewardsConnection?.edges?.length) return [];
|
||||||
|
|
||||||
return (
|
return removePaginationWrapper(data.party.rewardsConnection.edges);
|
||||||
compact(data.party.rewardsConnection.edges.map((edge) => edge?.node)) ||
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const delegations = React.useMemo(() => {
|
const delegations = React.useMemo(() => {
|
||||||
@ -36,11 +36,7 @@ export const RewardInfo = ({ data, currVegaKey }: RewardInfoProps) => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return removePaginationWrapper(data.party.delegationsConnection.edges);
|
||||||
compact(
|
|
||||||
data.party.delegationsConnection.edges.map((edge) => edge?.node)
|
|
||||||
) || []
|
|
||||||
);
|
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -9,9 +9,11 @@ import { useGetAssociationBreakdown } from '../../../hooks/use-get-association-b
|
|||||||
import { useRefreshBalances } from '../../../hooks/use-refresh-balances';
|
import { useRefreshBalances } from '../../../hooks/use-refresh-balances';
|
||||||
import { useTransaction } from '../../../hooks/use-transaction';
|
import { useTransaction } from '../../../hooks/use-transaction';
|
||||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||||
import { removeDecimal } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
|
removeDecimal,
|
||||||
|
removePaginationWrapper,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { Schema } from '@vegaprotocol/types';
|
import { Schema } from '@vegaprotocol/types';
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import type {
|
import type {
|
||||||
LinkingsFieldsFragment,
|
LinkingsFieldsFragment,
|
||||||
PartyStakeLinkingsQuery,
|
PartyStakeLinkingsQuery,
|
||||||
@ -92,12 +94,9 @@ export const usePollForStakeLinking = (
|
|||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'no-cache',
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const linkings =
|
const linkings = removePaginationWrapper(
|
||||||
compact(
|
res.data?.party?.stakingSummary.linkings.edges
|
||||||
res.data?.party?.stakingSummary.linkings.edges?.map(
|
);
|
||||||
(e) => e?.node
|
|
||||||
)
|
|
||||||
) || [];
|
|
||||||
|
|
||||||
if (!linkings?.length) return;
|
if (!linkings?.length) return;
|
||||||
|
|
||||||
|
15
apps/token/src/routes/staking/home/PreviousEpoch.graphql
Normal file
15
apps/token/src/routes/staking/home/PreviousEpoch.graphql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
query PreviousEpoch($epochId: ID) {
|
||||||
|
epoch(id: $epochId) {
|
||||||
|
id
|
||||||
|
validatorsConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
rewardScore {
|
||||||
|
rawValidatorScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
import { Schema as Types } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type PreviousEpochQueryVariables = Types.Exact<{
|
||||||
|
epochId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string } | null } } | null> | null } | null } };
|
||||||
|
|
||||||
|
|
||||||
|
export const PreviousEpochDocument = gql`
|
||||||
|
query PreviousEpoch($epochId: ID) {
|
||||||
|
epoch(id: $epochId) {
|
||||||
|
id
|
||||||
|
validatorsConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
rewardScore {
|
||||||
|
rawValidatorScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __usePreviousEpochQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `usePreviousEpochQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `usePreviousEpochQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = usePreviousEpochQuery({
|
||||||
|
* variables: {
|
||||||
|
* epochId: // value for 'epochId'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function usePreviousEpochQuery(baseOptions?: Apollo.QueryHookOptions<PreviousEpochQuery, PreviousEpochQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<PreviousEpochQuery, PreviousEpochQueryVariables>(PreviousEpochDocument, options);
|
||||||
|
}
|
||||||
|
export function usePreviousEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PreviousEpochQuery, PreviousEpochQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<PreviousEpochQuery, PreviousEpochQueryVariables>(PreviousEpochDocument, options);
|
||||||
|
}
|
||||||
|
export type PreviousEpochQueryHookResult = ReturnType<typeof usePreviousEpochQuery>;
|
||||||
|
export type PreviousEpochLazyQueryHookResult = ReturnType<typeof usePreviousEpochLazyQuery>;
|
||||||
|
export type PreviousEpochQueryResult = Apollo.QueryResult<PreviousEpochQuery, PreviousEpochQueryVariables>;
|
59
apps/token/src/routes/staking/home/epoch-data.tsx
Normal file
59
apps/token/src/routes/staking/home/epoch-data.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { EpochCountdown } from '../../../components/epoch-countdown';
|
||||||
|
import { useNodesQuery } from './__generated___/Nodes';
|
||||||
|
import { usePreviousEpochQuery } from './__generated___/PreviousEpoch';
|
||||||
|
import { ValidatorTables } from './validator-tables';
|
||||||
|
|
||||||
|
export const EpochData = () => {
|
||||||
|
// errorPolicy due to vegaprotocol/vega issue 5898
|
||||||
|
const { data, error, loading, refetch } = useNodesQuery();
|
||||||
|
const { data: previousEpochData, refetch: previousEpochRefetch } =
|
||||||
|
usePreviousEpochQuery({
|
||||||
|
variables: {
|
||||||
|
epochId: (Number(data?.epoch.id) - 1).toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const epochInterval = setInterval(() => {
|
||||||
|
if (!data?.epoch.timestamps.expiry) return;
|
||||||
|
const now = Date.now();
|
||||||
|
const expiry = new Date(data.epoch.timestamps.expiry).getTime();
|
||||||
|
|
||||||
|
if (now > expiry) {
|
||||||
|
refetch();
|
||||||
|
previousEpochRefetch({
|
||||||
|
epochId: (Number(data?.epoch.id) - 1).toString(),
|
||||||
|
});
|
||||||
|
clearInterval(epochInterval);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(epochInterval);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
data?.epoch.id,
|
||||||
|
data?.epoch.timestamps.expiry,
|
||||||
|
previousEpochRefetch,
|
||||||
|
refetch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
|
{data?.epoch &&
|
||||||
|
data.epoch.timestamps.start &&
|
||||||
|
data?.epoch.timestamps.expiry && (
|
||||||
|
<div className="mb-8">
|
||||||
|
<EpochCountdown
|
||||||
|
id={data.epoch.id}
|
||||||
|
startDate={new Date(data.epoch.timestamps.start)}
|
||||||
|
endDate={new Date(data.epoch.timestamps.expiry)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ValidatorTables data={data} previousEpochData={previousEpochData} />
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
@ -1,251 +0,0 @@
|
|||||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
||||||
import merge from 'lodash/merge';
|
|
||||||
import { NodeList } from './node-list';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import type { PartialDeep } from 'type-fest';
|
|
||||||
import type { NodesFragmentFragment } from './__generated___/Nodes';
|
|
||||||
import { NodesDocument } from './__generated___/Nodes';
|
|
||||||
import { Schema } from '@vegaprotocol/types';
|
|
||||||
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
|
|
||||||
|
|
||||||
jest.mock('../../../components/epoch-countdown', () => ({
|
|
||||||
EpochCountdown: () => <div data-testid="epoch-info"></div>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const nodeFactory = (overrides?: PartialDeep<NodesFragmentFragment>) => {
|
|
||||||
const defaultNode = {
|
|
||||||
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
|
||||||
name: 'high',
|
|
||||||
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
|
|
||||||
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
|
|
||||||
stakedTotal: '14182454495731682635157',
|
|
||||||
pendingStake: '0',
|
|
||||||
rankingScore: {
|
|
||||||
rankingScore: '0.67845061012234727427532760837568',
|
|
||||||
stakeScore: '0.3392701644525644',
|
|
||||||
performanceScore: '0.9998677767864936',
|
|
||||||
votingPower: '3500',
|
|
||||||
status: 'tendermint',
|
|
||||||
__typename: 'RankingScore',
|
|
||||||
},
|
|
||||||
__typename: 'Node',
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
node: merge(defaultNode, overrides),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const MOCK_NODES = {
|
|
||||||
nodesConnection: {
|
|
||||||
edges: [
|
|
||||||
nodeFactory(),
|
|
||||||
nodeFactory({
|
|
||||||
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
|
|
||||||
name: 'medium',
|
|
||||||
pubkey:
|
|
||||||
'ccc3b8362c25b09d20df8ea407b0a476d6b24a0e72bc063d0033c8841652ddd4',
|
|
||||||
stakedTotal: '9618711883996159534058',
|
|
||||||
rankingScore: {
|
|
||||||
rankingScore: '0.4601942440481428',
|
|
||||||
stakeScore: '0.2300971220240714',
|
|
||||||
performanceScore: '1',
|
|
||||||
votingPower: '2408',
|
|
||||||
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
__typename: 'RankingScore',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
nodeFactory({
|
|
||||||
id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81',
|
|
||||||
name: 'low',
|
|
||||||
pubkey:
|
|
||||||
'0931a8fd8cc935458f470e435a05414387cea6f329d648be894fcd44bd517a2b',
|
|
||||||
stakedTotal: '4041343338923442976709',
|
|
||||||
pendingStake: '0',
|
|
||||||
rankingScore: {
|
|
||||||
rankingScore: '0.1932810100133910357676209647912',
|
|
||||||
stakeScore: '0.0966762995515676',
|
|
||||||
performanceScore: '0.999629748500531',
|
|
||||||
votingPower: '1163',
|
|
||||||
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
__typename: 'RankingScore',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
nodeData: {
|
|
||||||
stakedTotal: '27842509718651285145924',
|
|
||||||
totalNodes: 3,
|
|
||||||
inactiveNodes: 0,
|
|
||||||
uptime: 1560.266845703125,
|
|
||||||
__typename: 'NodeData',
|
|
||||||
},
|
|
||||||
epoch: {
|
|
||||||
__typename: 'Epoch',
|
|
||||||
id: '1',
|
|
||||||
timestamps: {
|
|
||||||
__typename: 'EpochTimestamps',
|
|
||||||
start: new Date(0).toISOString(),
|
|
||||||
end: '',
|
|
||||||
expiry: new Date(1000 * 60 * 60 * 24).toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNodeList = (data = MOCK_NODES) => {
|
|
||||||
return render(
|
|
||||||
<AppStateProvider initialState={{ decimals: 18 }}>
|
|
||||||
<MemoryRouter>
|
|
||||||
<MockedProvider
|
|
||||||
mocks={[
|
|
||||||
{
|
|
||||||
request: { query: NodesDocument },
|
|
||||||
result: { data },
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<NodeList />
|
|
||||||
</MockedProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
</AppStateProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
jest.setSystemTime(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Nodes list', () => {
|
|
||||||
it('should render epoch info', async () => {
|
|
||||||
renderNodeList();
|
|
||||||
expect(await screen.findByTestId('epoch-info')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should initially render a list of nodes with the top third hidden', async () => {
|
|
||||||
renderNodeList();
|
|
||||||
|
|
||||||
expect(await screen.findByText('medium')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('low')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('high')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show all validators if button is pressed', async () => {
|
|
||||||
renderNodeList();
|
|
||||||
|
|
||||||
expect(await screen.findByText('medium')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('high')).toBeNull();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.click(screen.getByTestId('show-all-validators'));
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('high')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('medium')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('low')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display the correctly formatted fields in the correct columns', async () => {
|
|
||||||
const MOCK_NODE = {
|
|
||||||
nodesConnection: {
|
|
||||||
edges: [
|
|
||||||
nodeFactory({
|
|
||||||
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
|
|
||||||
name: 'T-800 Terminator',
|
|
||||||
avatarUrl:
|
|
||||||
'https://upload.wikimedia.org/wikipedia/en/9/94/T-800_%28Model_101%29.png',
|
|
||||||
pubkey:
|
|
||||||
'ccc3b8362c25b09d20df8ea407b0a476d6b24a0e72bc063d0033c8841652ddd4',
|
|
||||||
stakedTotal: '9618711883996159534058',
|
|
||||||
rankingScore: {
|
|
||||||
rankingScore: '0.4601942440481428',
|
|
||||||
stakeScore: '0.2300971220240714',
|
|
||||||
performanceScore: '1',
|
|
||||||
votingPower: '2408',
|
|
||||||
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
|
||||||
__typename: 'RankingScore',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
nodeData: {
|
|
||||||
stakedTotal: '9618711883996159534058',
|
|
||||||
totalNodes: 1,
|
|
||||||
inactiveNodes: 0,
|
|
||||||
uptime: 1560.266845703125,
|
|
||||||
__typename: 'NodeData',
|
|
||||||
},
|
|
||||||
epoch: {
|
|
||||||
__typename: 'Epoch',
|
|
||||||
id: '1',
|
|
||||||
timestamps: {
|
|
||||||
__typename: 'EpochTimestamps',
|
|
||||||
start: new Date(0).toISOString(),
|
|
||||||
end: '',
|
|
||||||
expiry: new Date(1000 * 60 * 60 * 24).toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
renderNodeList(MOCK_NODE);
|
|
||||||
|
|
||||||
expect(await screen.findByTestId('validators-grid')).toBeInTheDocument();
|
|
||||||
|
|
||||||
const grid = screen.getByTestId('validators-grid');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.click(screen.getByTestId('show-all-validators'));
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="validator"]')
|
|
||||||
).toHaveTextContent('T-800 Terminator');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="validator"] img')
|
|
||||||
).toHaveAttribute(
|
|
||||||
'src',
|
|
||||||
'https://upload.wikimedia.org/wikipedia/en/9/94/T-800_%28Model_101%29.png'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="status"]')
|
|
||||||
).toHaveTextContent('Consensus');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="totalStakeThisEpoch"]')
|
|
||||||
).toHaveTextContent('9,618.71');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="share"]')
|
|
||||||
).toHaveTextContent('100%');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="validatorStake"]')
|
|
||||||
).toHaveTextContent('9,618.71');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="pendingStake"]')
|
|
||||||
).toHaveTextContent('0');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="rankingScore"]')
|
|
||||||
).toHaveTextContent('0.46019');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="stakeScore"]')
|
|
||||||
).toHaveTextContent('0.2301');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="performanceScore"]')
|
|
||||||
).toHaveTextContent('1.00');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
grid.querySelector('[role="gridcell"][col-id="votingPower"]')
|
|
||||||
).toHaveTextContent('2408');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,352 +0,0 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
AgGridDynamic as AgGrid,
|
|
||||||
AsyncRenderer,
|
|
||||||
Button,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { EpochCountdown } from '../../../components/epoch-countdown';
|
|
||||||
import { BigNumber } from '../../../lib/bignumber';
|
|
||||||
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
|
|
||||||
import { Schema } from '@vegaprotocol/types';
|
|
||||||
import type { ColDef } from 'ag-grid-community';
|
|
||||||
import { useNodesQuery } from './__generated___/Nodes';
|
|
||||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
|
||||||
|
|
||||||
const VALIDATOR = 'validator';
|
|
||||||
const STATUS = 'status';
|
|
||||||
const TOTAL_STAKE_THIS_EPOCH = 'totalStakeThisEpoch';
|
|
||||||
const SHARE = 'share';
|
|
||||||
const VALIDATOR_STAKE = 'validatorStake';
|
|
||||||
const PENDING_STAKE = 'pendingStake';
|
|
||||||
const RANKING_SCORE = 'rankingScore';
|
|
||||||
const STAKE_SCORE = 'stakeScore';
|
|
||||||
const PERFORMANCE_SCORE = 'performanceScore';
|
|
||||||
const VOTING_POWER = 'votingPower';
|
|
||||||
|
|
||||||
interface ValidatorRendererProps {
|
|
||||||
data: { validator: { avatarUrl: string; name: string } };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CanonisedNodeProps {
|
|
||||||
id: string;
|
|
||||||
[VALIDATOR]: {
|
|
||||||
avatarUrl: string | null | undefined;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
[STATUS]: string;
|
|
||||||
[TOTAL_STAKE_THIS_EPOCH]: string;
|
|
||||||
[SHARE]: string;
|
|
||||||
[VALIDATOR_STAKE]: string;
|
|
||||||
[PENDING_STAKE]: string;
|
|
||||||
[RANKING_SCORE]: string;
|
|
||||||
[STAKE_SCORE]: string;
|
|
||||||
[PERFORMANCE_SCORE]: string;
|
|
||||||
[VOTING_POWER]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const statusTranslationKey = (status: Schema.ValidatorStatus) => {
|
|
||||||
// Returns a key for translation
|
|
||||||
const statuses = {
|
|
||||||
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ]: 'status-ersatz',
|
|
||||||
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING]: 'status-pending',
|
|
||||||
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT]:
|
|
||||||
'status-tendermint',
|
|
||||||
};
|
|
||||||
|
|
||||||
return statuses[status];
|
|
||||||
};
|
|
||||||
|
|
||||||
const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
|
|
||||||
const { avatarUrl, name } = data.validator;
|
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
|
||||||
{avatarUrl && (
|
|
||||||
<img
|
|
||||||
className="h-6 w-6 rounded-full mr-2"
|
|
||||||
src={avatarUrl}
|
|
||||||
alt={`Avatar icon for ${name}`}
|
|
||||||
onError={(e) => (e.currentTarget.style.display = 'none')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Custom styling to account for the scrollbar. This is needed because the
|
|
||||||
// AG Grid places the scrollbar over the bottom validator, which obstructs
|
|
||||||
const nodeListGridStyles = `
|
|
||||||
.ag-theme-balham-dark .ag-body-horizontal-scroll {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const NodeList = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
// errorPolicy due to vegaprotocol/vega issue 5898
|
|
||||||
const { data, error, loading, refetch } = useNodesQuery();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const {
|
|
||||||
appState: { decimals },
|
|
||||||
} = useAppState();
|
|
||||||
const [hideTopThird, setHideTopThird] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const epochInterval = setInterval(() => {
|
|
||||||
if (!data?.epoch.timestamps.expiry) return;
|
|
||||||
const now = Date.now();
|
|
||||||
const expiry = new Date(data.epoch.timestamps.expiry).getTime();
|
|
||||||
|
|
||||||
if (now > expiry) {
|
|
||||||
refetch();
|
|
||||||
clearInterval(epochInterval);
|
|
||||||
}
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(epochInterval);
|
|
||||||
};
|
|
||||||
}, [data?.epoch.timestamps.expiry, refetch]);
|
|
||||||
|
|
||||||
const nodes = useMemo(() => {
|
|
||||||
if (!data?.nodesConnection.edges) return [];
|
|
||||||
|
|
||||||
const canonisedNodes = compact(data.nodesConnection.edges).map(
|
|
||||||
({
|
|
||||||
node: {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
avatarUrl,
|
|
||||||
stakedTotal,
|
|
||||||
rankingScore: {
|
|
||||||
rankingScore,
|
|
||||||
stakeScore,
|
|
||||||
status,
|
|
||||||
performanceScore,
|
|
||||||
votingPower,
|
|
||||||
},
|
|
||||||
pendingStake,
|
|
||||||
},
|
|
||||||
}) => {
|
|
||||||
const stakedTotalNum = toBigNum(
|
|
||||||
data?.nodeData?.stakedTotal || 0,
|
|
||||||
decimals
|
|
||||||
);
|
|
||||||
const stakedOnNode = toBigNum(stakedTotal, decimals);
|
|
||||||
const stakedTotalPercentage =
|
|
||||||
stakedTotalNum.isEqualTo(0) || stakedOnNode.isEqualTo(0)
|
|
||||||
? '-'
|
|
||||||
: stakedOnNode
|
|
||||||
.dividedBy(stakedTotalNum)
|
|
||||||
.times(100)
|
|
||||||
.dp(2)
|
|
||||||
.toString() + '%';
|
|
||||||
const translatedStatus = t(statusTranslationKey(status));
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
[VALIDATOR]: {
|
|
||||||
avatarUrl,
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
[STATUS]: translatedStatus,
|
|
||||||
[TOTAL_STAKE_THIS_EPOCH]: formatNumber(
|
|
||||||
toBigNum(stakedTotal, decimals),
|
|
||||||
2
|
|
||||||
),
|
|
||||||
[SHARE]: stakedTotalPercentage,
|
|
||||||
[VALIDATOR_STAKE]: formatNumber(stakedOnNode, 2),
|
|
||||||
[PENDING_STAKE]: formatNumber(toBigNum(pendingStake, decimals), 2),
|
|
||||||
[RANKING_SCORE]: formatNumber(new BigNumber(rankingScore), 5),
|
|
||||||
[STAKE_SCORE]: formatNumber(new BigNumber(stakeScore), 5),
|
|
||||||
[PERFORMANCE_SCORE]: formatNumber(new BigNumber(performanceScore), 5),
|
|
||||||
[VOTING_POWER]: votingPower,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (canonisedNodes.length < 3 || !hideTopThird) {
|
|
||||||
return canonisedNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedByVotingPower = canonisedNodes.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new BigNumber(b[VOTING_POWER]).toNumber() -
|
|
||||||
new BigNumber(a[VOTING_POWER]).toNumber()
|
|
||||||
);
|
|
||||||
|
|
||||||
// The point of identifying and hiding the group that could halt the network
|
|
||||||
// is that we assume the top 1/3 of stake is held by considerably less than
|
|
||||||
// 1/3 of the validators and we really want people not to stake any more to
|
|
||||||
// that group, because we want to make it require as many difference
|
|
||||||
// validators to collude as possible to halt the network, so we hide them.
|
|
||||||
const removeTopThirdOfStakeScores = sortedByVotingPower.reduce(
|
|
||||||
(acc, node) => {
|
|
||||||
if (acc.cumulativeScore < 3333) {
|
|
||||||
acc.cumulativeScore += Number(node[VOTING_POWER]);
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
acc.remaining.push(node);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ remaining: [], cumulativeScore: 0 } as {
|
|
||||||
remaining: CanonisedNodeProps[];
|
|
||||||
cumulativeScore: number;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return removeTopThirdOfStakeScores.remaining;
|
|
||||||
}, [
|
|
||||||
data?.nodeData?.stakedTotal,
|
|
||||||
data?.nodesConnection.edges,
|
|
||||||
decimals,
|
|
||||||
hideTopThird,
|
|
||||||
t,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
|
||||||
|
|
||||||
const NodeListTable = forwardRef<AgGridReact>((_, ref) => {
|
|
||||||
const colDefs = useMemo<ColDef[]>(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
field: VALIDATOR,
|
|
||||||
headerName: t('validator').toString(),
|
|
||||||
cellRenderer: ValidatorRenderer,
|
|
||||||
comparator: ({ name: a }, { name: b }) => {
|
|
||||||
if (a === b) return 0;
|
|
||||||
return a > b ? 1 : -1;
|
|
||||||
},
|
|
||||||
pinned: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: STATUS,
|
|
||||||
headerName: t('status').toString(),
|
|
||||||
comparator: (a, b) => {
|
|
||||||
if (a === b) return 0;
|
|
||||||
return a > b ? 1 : -1;
|
|
||||||
},
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: TOTAL_STAKE_THIS_EPOCH,
|
|
||||||
headerName: t('totalStakeThisEpoch').toString(),
|
|
||||||
width: 160,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: SHARE,
|
|
||||||
headerName: t('share').toString(),
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: VALIDATOR_STAKE,
|
|
||||||
headerName: t('validatorStake').toString(),
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: PENDING_STAKE,
|
|
||||||
headerName: t('nextEpoch').toString(),
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: RANKING_SCORE,
|
|
||||||
headerName: t('rankingScore').toString(),
|
|
||||||
width: 120,
|
|
||||||
sort: 'desc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: STAKE_SCORE,
|
|
||||||
headerName: t('stakeScore').toString(),
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: PERFORMANCE_SCORE,
|
|
||||||
headerName: t('performanceScore').toString(),
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: VOTING_POWER,
|
|
||||||
headerName: t('votingPower').toString(),
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultColDef = useMemo(
|
|
||||||
() => ({
|
|
||||||
sortable: true,
|
|
||||||
resizable: true,
|
|
||||||
comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b),
|
|
||||||
cellStyle: { margin: '10px 0' },
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-testid="validators-grid">
|
|
||||||
{hideTopThird && (
|
|
||||||
<div className="mb-6 py-4 px-4 md:px-12 bg-neutral-900 text-sm text-center">
|
|
||||||
<div className="mb-4">
|
|
||||||
<Button
|
|
||||||
data-testid="show-all-validators"
|
|
||||||
icon="list"
|
|
||||||
className="inline-flex items-center"
|
|
||||||
onClick={() => setHideTopThird(false)}
|
|
||||||
>
|
|
||||||
{t('Reveal top validators')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="font-semibold">
|
|
||||||
{t(
|
|
||||||
'Validators with too great a stake share will have the staking rewards for their delegators penalised.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p className="mb-0">
|
|
||||||
{t(
|
|
||||||
'To avoid penalties and increase decentralisation of the network, delegate to validators below.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<AgGrid
|
|
||||||
domLayout="autoHeight"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
customThemeParams={nodeListGridStyles}
|
|
||||||
overlayNoRowsTemplate={t('noValidators')}
|
|
||||||
ref={ref}
|
|
||||||
rowData={nodes}
|
|
||||||
rowHeight={52}
|
|
||||||
columnDefs={colDefs}
|
|
||||||
defaultColDef={defaultColDef}
|
|
||||||
animateRows={true}
|
|
||||||
suppressCellFocus={true}
|
|
||||||
onCellClicked={(event) => {
|
|
||||||
navigate(event.data.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AsyncRenderer loading={loading} error={error} data={nodes}>
|
|
||||||
{data?.epoch &&
|
|
||||||
data.epoch.timestamps.start &&
|
|
||||||
data?.epoch.timestamps.expiry && (
|
|
||||||
<div className="mb-8">
|
|
||||||
<EpochCountdown
|
|
||||||
id={data.epoch.id}
|
|
||||||
startDate={new Date(data.epoch.timestamps.start)}
|
|
||||||
endDate={new Date(data.epoch.timestamps.expiry)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<NodeListTable ref={gridRef} />
|
|
||||||
</AsyncRenderer>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { StakingIntro } from './staking-intro';
|
import { StakingIntro } from './staking-intro';
|
||||||
import { NodeList } from './node-list';
|
import { EpochData } from './epoch-data';
|
||||||
|
|
||||||
export const Staking = () => {
|
export const Staking = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -10,7 +10,7 @@ export const Staking = () => {
|
|||||||
<StakingIntro />
|
<StakingIntro />
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl uppercase">{t('Validator nodes')}</h2>
|
<h2 className="text-2xl uppercase">{t('Validator nodes')}</h2>
|
||||||
<NodeList data-testid="node-list" />
|
<EpochData data-testid="epoch-data" />
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,248 @@
|
|||||||
|
import merge from 'lodash/merge';
|
||||||
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { ConsensusValidatorsTable } from './consensus-validators-table';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { NodesDocument } from '../__generated___/Nodes';
|
||||||
|
import { PreviousEpochDocument } from '../__generated___/PreviousEpoch';
|
||||||
|
import { Schema } from '@vegaprotocol/types';
|
||||||
|
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||||
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
import type { NodesFragmentFragment } from '../__generated___/Nodes';
|
||||||
|
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
|
||||||
|
|
||||||
|
const nodeFactory = (
|
||||||
|
overrides?: PartialDeep<NodesFragmentFragment>
|
||||||
|
): NodesFragmentFragment => {
|
||||||
|
const defaultNode = {
|
||||||
|
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
||||||
|
name: 'high',
|
||||||
|
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
|
||||||
|
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
|
||||||
|
stakedTotal: '14182454495731682635157',
|
||||||
|
pendingStake: '0',
|
||||||
|
rankingScore: {
|
||||||
|
rankingScore: '0.67845061012234727427532760837568',
|
||||||
|
stakeScore: '0.3392701644525644',
|
||||||
|
performanceScore: '0.9998677767864936',
|
||||||
|
votingPower: '3500',
|
||||||
|
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||||
|
__typename: 'RankingScore',
|
||||||
|
},
|
||||||
|
__typename: 'Node',
|
||||||
|
};
|
||||||
|
return merge(defaultNode, overrides);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MOCK_NODES = [
|
||||||
|
nodeFactory(),
|
||||||
|
nodeFactory({
|
||||||
|
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
|
||||||
|
name: 'medium',
|
||||||
|
pubkey: 'ccc3b8362c25b09d20df8ea407b0a476d6b24a0e72bc063d0033c8841652ddd4',
|
||||||
|
stakedTotal: '9618711883996159534058',
|
||||||
|
rankingScore: {
|
||||||
|
rankingScore: '0.4601942440481428',
|
||||||
|
stakeScore: '0.2300971220240714',
|
||||||
|
performanceScore: '1',
|
||||||
|
votingPower: '2408',
|
||||||
|
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||||
|
__typename: 'RankingScore',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
nodeFactory({
|
||||||
|
id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81',
|
||||||
|
name: 'low',
|
||||||
|
pubkey: '0931a8fd8cc935458f470e435a05414387cea6f329d648be894fcd44bd517a2b',
|
||||||
|
stakedTotal: '4041343338923442976709',
|
||||||
|
pendingStake: '0',
|
||||||
|
rankingScore: {
|
||||||
|
rankingScore: '0.1932810100133910357676209647912',
|
||||||
|
stakeScore: '0.0966762995515676',
|
||||||
|
performanceScore: '0.999629748500531',
|
||||||
|
votingPower: '1163',
|
||||||
|
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||||
|
__typename: 'RankingScore',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
|
||||||
|
epoch: {
|
||||||
|
id: '1',
|
||||||
|
validatorsConnection: {
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
||||||
|
rewardScore: {
|
||||||
|
rawValidatorScore: '0.25',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
|
||||||
|
rewardScore: {
|
||||||
|
rawValidatorScore: '0.3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81',
|
||||||
|
rewardScore: {
|
||||||
|
rawValidatorScore: '0.35',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodesDataMock: MockedResponse<NodesFragmentFragment[]> = {
|
||||||
|
request: {
|
||||||
|
query: NodesDocument,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
...MOCK_NODES,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousEpochMock: MockedResponse<PreviousEpochQuery> = {
|
||||||
|
request: {
|
||||||
|
query: PreviousEpochDocument,
|
||||||
|
variables: {
|
||||||
|
id: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
...MOCK_PREVIOUS_EPOCH,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MOCK_TOTAL_STAKE = '28832590188747439203824';
|
||||||
|
|
||||||
|
const renderValidatorsTable = (
|
||||||
|
data = MOCK_NODES,
|
||||||
|
previousEpochData = MOCK_PREVIOUS_EPOCH
|
||||||
|
) => {
|
||||||
|
return render(
|
||||||
|
<AppStateProvider initialState={{ decimals: 18 }}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider mocks={[nodesDataMock, previousEpochMock]}>
|
||||||
|
<ConsensusValidatorsTable
|
||||||
|
data={data}
|
||||||
|
previousEpochData={previousEpochData}
|
||||||
|
totalStake={MOCK_TOTAL_STAKE}
|
||||||
|
/>
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
</AppStateProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Consensus validators table', () => {
|
||||||
|
it('should initially render a list of nodes with the top third hidden', async () => {
|
||||||
|
renderValidatorsTable();
|
||||||
|
|
||||||
|
expect(await screen.findByText('medium')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('low')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('high')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show all validators if button is pressed', async () => {
|
||||||
|
renderValidatorsTable();
|
||||||
|
|
||||||
|
expect(await screen.findByText('medium')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('high')).toBeNull();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(screen.getByTestId('show-all-validators'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('high')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('medium')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('low')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the correctly formatted fields in the correct columns', async () => {
|
||||||
|
const mockNode = [
|
||||||
|
nodeFactory({
|
||||||
|
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
|
||||||
|
name: 'T-800 Terminator',
|
||||||
|
avatarUrl:
|
||||||
|
'https://upload.wikimedia.org/wikipedia/en/9/94/T-800_%28Model_101%29.png',
|
||||||
|
pubkey:
|
||||||
|
'ccc3b8362c25b09d20df8ea407b0a476d6b24a0e72bc063d0033c8841652ddd4',
|
||||||
|
stakedTotal: '9618711883996159534058',
|
||||||
|
rankingScore: {
|
||||||
|
rankingScore: '0.4601942440481428',
|
||||||
|
stakeScore: '0.2300971220240714',
|
||||||
|
performanceScore: '1',
|
||||||
|
votingPower: '2408',
|
||||||
|
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
|
||||||
|
__typename: 'RankingScore',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderValidatorsTable(mockNode, MOCK_PREVIOUS_EPOCH);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByTestId('consensus-validators-table')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
const grid = screen.getByTestId('consensus-validators-table');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(screen.getByTestId('show-all-validators'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
grid.querySelector('[role="gridcell"][col-id="validator"]')
|
||||||
|
).toHaveTextContent('T-800 Terminator');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
grid.querySelector('[role="gridcell"][col-id="validator"] img')
|
||||||
|
).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
'https://upload.wikimedia.org/wikipedia/en/9/94/T-800_%28Model_101%29.png'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
grid.querySelector('[role="gridcell"][col-id="stake"]')
|
||||||
|
).toHaveTextContent('9,618.71');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
grid.querySelector('[role="gridcell"][col-id="stakeShare"]')
|
||||||
|
).toHaveTextContent('23.01%');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
grid.querySelector('[role="gridcell"][col-id="pendingStake"]')
|
||||||
|
).toHaveTextContent('0');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
grid.querySelector('[role="gridcell"][col-id="totalPenalties"]')
|
||||||
|
).toHaveTextContent('10.07%');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
grid.querySelector('[role="gridcell"][col-id="normalisedVotingPower"]')
|
||||||
|
).toHaveTextContent('24.08%');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,231 @@
|
|||||||
|
import { forwardRef, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { AgGridDynamic as AgGrid, Button } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
||||||
|
import {
|
||||||
|
defaultColDef,
|
||||||
|
NODE_LIST_GRID_STYLES,
|
||||||
|
stakedTotalPercentage,
|
||||||
|
totalPenalties,
|
||||||
|
ValidatorFields,
|
||||||
|
ValidatorRenderer,
|
||||||
|
} from './shared';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import type { ColDef } from 'ag-grid-community';
|
||||||
|
import type { ValidatorsTableProps } from './shared';
|
||||||
|
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
interface CanonisedConsensusNodeProps {
|
||||||
|
id: string;
|
||||||
|
[ValidatorFields.VALIDATOR]: {
|
||||||
|
avatarUrl: string | null | undefined;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
[ValidatorFields.STAKE]: string;
|
||||||
|
[ValidatorFields.STAKE_SHARE]: string;
|
||||||
|
[ValidatorFields.PENDING_STAKE]: string;
|
||||||
|
[ValidatorFields.NORMALISED_VOTING_POWER]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConsensusValidatorsTable = ({
|
||||||
|
data,
|
||||||
|
previousEpochData,
|
||||||
|
totalStake,
|
||||||
|
}: ValidatorsTableProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
appState: { decimals },
|
||||||
|
} = useAppState();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [hideTopThird, setHideTopThird] = useState(true);
|
||||||
|
|
||||||
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
|
||||||
|
const nodes = useMemo(() => {
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
const canonisedNodes = data
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aVotingPower = toBigNum(a.rankingScore.votingPower, 0);
|
||||||
|
const bVotingPower = toBigNum(b.rankingScore.votingPower, 0);
|
||||||
|
return bVotingPower.minus(aVotingPower).toNumber();
|
||||||
|
})
|
||||||
|
.map((node, index) => {
|
||||||
|
const votingPowerRanking = index + 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
votingPowerRanking,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.map(
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
avatarUrl,
|
||||||
|
stakedTotal,
|
||||||
|
rankingScore: { stakeScore, votingPower, performanceScore },
|
||||||
|
pendingStake,
|
||||||
|
votingPowerRanking,
|
||||||
|
}) => {
|
||||||
|
const normalisedVotingPower =
|
||||||
|
toBigNum(votingPower, 0).dividedBy(100).dp(2).toString() + '%';
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
[ValidatorFields.RANKING_INDEX]: votingPowerRanking,
|
||||||
|
[ValidatorFields.VALIDATOR]: {
|
||||||
|
avatarUrl,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
[ValidatorFields.STAKE]: formatNumber(
|
||||||
|
toBigNum(stakedTotal, decimals),
|
||||||
|
2
|
||||||
|
),
|
||||||
|
[ValidatorFields.NORMALISED_VOTING_POWER]: normalisedVotingPower,
|
||||||
|
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
|
||||||
|
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties(
|
||||||
|
previousEpochData,
|
||||||
|
id,
|
||||||
|
performanceScore,
|
||||||
|
stakedTotal,
|
||||||
|
totalStake
|
||||||
|
),
|
||||||
|
[ValidatorFields.PENDING_STAKE]: formatNumber(
|
||||||
|
toBigNum(pendingStake, decimals),
|
||||||
|
2
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (canonisedNodes.length < 3 || !hideTopThird) {
|
||||||
|
return canonisedNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The point of identifying and hiding the group that could halt the network
|
||||||
|
// is that we assume the top 1/3 of stake is held by considerably less than
|
||||||
|
// 1/3 of the validators and we really want people not to stake any more to
|
||||||
|
// that group, because we want to make it require as many difference
|
||||||
|
// validators to collude as possible to halt the network, so we hide them.
|
||||||
|
const removeTopThirdOfStakeScores = canonisedNodes.reduce(
|
||||||
|
(acc, node) => {
|
||||||
|
if (acc.cumulativeScore < 3333) {
|
||||||
|
acc.cumulativeScore += Number(
|
||||||
|
node[ValidatorFields.NORMALISED_VOTING_POWER]
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
acc.remaining.push(node);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ remaining: [], cumulativeScore: 0 } as {
|
||||||
|
remaining: CanonisedConsensusNodeProps[];
|
||||||
|
cumulativeScore: number;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return removeTopThirdOfStakeScores.remaining;
|
||||||
|
}, [data, decimals, hideTopThird, previousEpochData, totalStake]);
|
||||||
|
|
||||||
|
const ConsensusTable = forwardRef<AgGridReact>((_, gridRef) => {
|
||||||
|
const colDefs = useMemo<ColDef[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
field: ValidatorFields.RANKING_INDEX,
|
||||||
|
headerName: '#',
|
||||||
|
width: 40,
|
||||||
|
pinned: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.VALIDATOR,
|
||||||
|
headerName: t(ValidatorFields.VALIDATOR).toString(),
|
||||||
|
cellRenderer: ValidatorRenderer,
|
||||||
|
comparator: ({ name: a }, { name: b }) => {
|
||||||
|
if (a === b) return 0;
|
||||||
|
return a > b ? 1 : -1;
|
||||||
|
},
|
||||||
|
pinned: 'left',
|
||||||
|
width: 240,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.STAKE,
|
||||||
|
headerName: t(ValidatorFields.STAKE).toString(),
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.NORMALISED_VOTING_POWER,
|
||||||
|
headerName: t(ValidatorFields.NORMALISED_VOTING_POWER).toString(),
|
||||||
|
width: 200,
|
||||||
|
sort: 'desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.STAKE_SHARE,
|
||||||
|
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.TOTAL_PENALTIES,
|
||||||
|
headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(),
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.PENDING_STAKE,
|
||||||
|
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="consensus-validators-table">
|
||||||
|
{hideTopThird && (
|
||||||
|
<div className="mb-6 py-4 px-4 md:px-12 bg-neutral-900 text-sm text-center">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Button
|
||||||
|
data-testid="show-all-validators"
|
||||||
|
icon="list"
|
||||||
|
className="inline-flex items-center"
|
||||||
|
onClick={() => setHideTopThird(false)}
|
||||||
|
>
|
||||||
|
{t('Reveal top validators')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{t(
|
||||||
|
'Validators with too great a stake share will have the staking rewards for their delegators penalised.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="mb-0">
|
||||||
|
{t(
|
||||||
|
'To avoid penalties and increase decentralisation of the network, delegate to validators below.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{nodes.length > 0 && (
|
||||||
|
<AgGrid
|
||||||
|
domLayout="autoHeight"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
customThemeParams={NODE_LIST_GRID_STYLES}
|
||||||
|
rowHeight={52}
|
||||||
|
defaultColDef={defaultColDef}
|
||||||
|
animateRows={true}
|
||||||
|
suppressCellFocus={true}
|
||||||
|
overlayNoRowsTemplate={t('noValidators')}
|
||||||
|
ref={gridRef}
|
||||||
|
rowData={nodes}
|
||||||
|
columnDefs={colDefs}
|
||||||
|
onCellClicked={(event) => {
|
||||||
|
navigate(event.data.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <ConsensusTable ref={gridRef} />;
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './validator-tables';
|
@ -0,0 +1,45 @@
|
|||||||
|
import { stakedTotalPercentage, totalPenalties } from './shared';
|
||||||
|
|
||||||
|
const mockPreviousEpochData = {
|
||||||
|
epoch: {
|
||||||
|
id: '1',
|
||||||
|
validatorsConnection: {
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: '0x123',
|
||||||
|
rewardScore: {
|
||||||
|
rawValidatorScore: '0.25',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('stakedTotalPercentage', () => {
|
||||||
|
it('should return the correct percentage as a string, 2dp', () => {
|
||||||
|
expect(stakedTotalPercentage('1.2345')).toBe('123.45%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('totalPenalties', () => {
|
||||||
|
it('should return the correct penalty based on arbitrary values, test 1', () => {
|
||||||
|
expect(
|
||||||
|
totalPenalties(mockPreviousEpochData, '0x123', '0.1', '5000', '100000')
|
||||||
|
).toBe('50%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct penalty based on lower performance score than first test', () => {
|
||||||
|
expect(
|
||||||
|
totalPenalties(mockPreviousEpochData, '0x123', '0.05', '5000', '100000')
|
||||||
|
).toBe('75%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct penalty based on higher amount of stake than other tests (great penalty due to anti-whaling)', () => {
|
||||||
|
expect(
|
||||||
|
totalPenalties(mockPreviousEpochData, '0x123', '0.1', '5000', '5500')
|
||||||
|
).toBe('97.25%');
|
||||||
|
});
|
||||||
|
});
|
102
apps/token/src/routes/staking/home/validator-tables/shared.tsx
Normal file
102
apps/token/src/routes/staking/home/validator-tables/shared.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { removePaginationWrapper, toBigNum } from '@vegaprotocol/react-helpers';
|
||||||
|
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { NodesFragmentFragment } from '../__generated___/Nodes';
|
||||||
|
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export enum ValidatorFields {
|
||||||
|
RANKING_INDEX = 'rankingIndex',
|
||||||
|
VALIDATOR = 'validator',
|
||||||
|
STAKE = 'stake',
|
||||||
|
PENDING_STAKE = 'pendingStake',
|
||||||
|
STAKE_SHARE = 'stakeShare',
|
||||||
|
TOTAL_PENALTIES = 'totalPenalties',
|
||||||
|
NORMALISED_VOTING_POWER = 'normalisedVotingPower',
|
||||||
|
STAKE_NEEDED_FOR_PROMOTION = 'stakeNeededForPromotion',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidatorsTableProps {
|
||||||
|
data: NodesFragmentFragment[] | undefined;
|
||||||
|
previousEpochData: PreviousEpochQuery | undefined;
|
||||||
|
totalStake: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom styling to account for the scrollbar. This is needed because the
|
||||||
|
// AG Grid places the scrollbar over the bottom validator, which obstructs
|
||||||
|
export const NODE_LIST_GRID_STYLES = `
|
||||||
|
.ag-theme-balham-dark .ag-body-horizontal-scroll {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
.ag-theme-balham-dark *:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const stakedTotalPercentage = (stakeScore: string) =>
|
||||||
|
toBigNum(stakeScore, 0).times(100).dp(2).toString() + '%';
|
||||||
|
|
||||||
|
export const totalPenalties = (
|
||||||
|
previousEpochData: PreviousEpochQuery | undefined,
|
||||||
|
id: string,
|
||||||
|
performanceScore: string,
|
||||||
|
stakedTotal: string,
|
||||||
|
totalStake: string
|
||||||
|
) => {
|
||||||
|
const rawValidatorScore = previousEpochData
|
||||||
|
? removePaginationWrapper(
|
||||||
|
previousEpochData.epoch?.validatorsConnection?.edges
|
||||||
|
).find((validator) => validator?.id === id)?.rewardScore
|
||||||
|
?.rawValidatorScore
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const totalPenaltiesCalc =
|
||||||
|
rawValidatorScore !== null
|
||||||
|
? 100 *
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
1 -
|
||||||
|
(Number(performanceScore) * Number(rawValidatorScore)) /
|
||||||
|
(Number(stakedTotal) / Number(totalStake || 0))
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return toBigNum(totalPenaltiesCalc, 0).dp(2).toString() + '%';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultColDef = {
|
||||||
|
sortable: true,
|
||||||
|
resizable: true,
|
||||||
|
autoHeight: true,
|
||||||
|
comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b),
|
||||||
|
cellStyle: { margin: '10px 0' },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ValidatorRendererProps {
|
||||||
|
data: { id: string; validator: { avatarUrl: string; name: string } };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { avatarUrl, name } = data.validator;
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-[1fr_auto] gap-2 items-center">
|
||||||
|
<span className="flex overflow-hidden">
|
||||||
|
{avatarUrl && (
|
||||||
|
<img
|
||||||
|
className="h-6 w-6 rounded-full mr-2"
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={`Avatar icon for ${name}`}
|
||||||
|
onError={(e) => (e.currentTarget.style.display = 'none')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{name}</span>
|
||||||
|
</span>
|
||||||
|
<Link to={data.id}>
|
||||||
|
<Button size="sm" fill={true}>
|
||||||
|
{t('Stake')}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,165 @@
|
|||||||
|
import { forwardRef, useMemo, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
||||||
|
import {
|
||||||
|
defaultColDef,
|
||||||
|
NODE_LIST_GRID_STYLES,
|
||||||
|
stakedTotalPercentage,
|
||||||
|
totalPenalties,
|
||||||
|
ValidatorFields,
|
||||||
|
ValidatorRenderer,
|
||||||
|
} from './shared';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import type { ColDef } from 'ag-grid-community';
|
||||||
|
import type { ValidatorsTableProps } from './shared';
|
||||||
|
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
interface StandbyPendingValidatorsTableProps extends ValidatorsTableProps {
|
||||||
|
stakeNeededForPromotion: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StandbyPendingValidatorsTable = ({
|
||||||
|
data,
|
||||||
|
previousEpochData,
|
||||||
|
totalStake,
|
||||||
|
stakeNeededForPromotion,
|
||||||
|
}: StandbyPendingValidatorsTableProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
appState: { decimals },
|
||||||
|
} = useAppState();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
|
||||||
|
const nodes = useMemo(() => {
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
return data.map(
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
avatarUrl,
|
||||||
|
stakedTotal,
|
||||||
|
rankingScore: { stakeScore, performanceScore },
|
||||||
|
pendingStake,
|
||||||
|
}) => {
|
||||||
|
let individualStakeNeededForPromotion = undefined;
|
||||||
|
|
||||||
|
if (stakeNeededForPromotion) {
|
||||||
|
const stakedTotalBigNum = toBigNum(stakedTotal, 0);
|
||||||
|
const stakeNeededBigNum = toBigNum(stakeNeededForPromotion, 0);
|
||||||
|
const performanceScoreBigNum = toBigNum(performanceScore, 0);
|
||||||
|
|
||||||
|
const calc = stakeNeededBigNum
|
||||||
|
.dividedBy(performanceScoreBigNum)
|
||||||
|
.minus(stakedTotalBigNum);
|
||||||
|
|
||||||
|
individualStakeNeededForPromotion = calc.isGreaterThan(0)
|
||||||
|
? calc.toString()
|
||||||
|
: '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
[ValidatorFields.VALIDATOR]: {
|
||||||
|
avatarUrl,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
[ValidatorFields.STAKE]: formatNumber(
|
||||||
|
toBigNum(stakedTotal, decimals),
|
||||||
|
2
|
||||||
|
),
|
||||||
|
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION]:
|
||||||
|
individualStakeNeededForPromotion || t('n/a'),
|
||||||
|
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
|
||||||
|
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties(
|
||||||
|
previousEpochData,
|
||||||
|
id,
|
||||||
|
performanceScore,
|
||||||
|
stakedTotal,
|
||||||
|
totalStake
|
||||||
|
),
|
||||||
|
[ValidatorFields.PENDING_STAKE]: formatNumber(
|
||||||
|
toBigNum(pendingStake, decimals),
|
||||||
|
2
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
data,
|
||||||
|
decimals,
|
||||||
|
previousEpochData,
|
||||||
|
stakeNeededForPromotion,
|
||||||
|
t,
|
||||||
|
totalStake,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const StandbyPendingTable = forwardRef<AgGridReact>((_, gridRef) => {
|
||||||
|
const colDefs = useMemo<ColDef[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
field: ValidatorFields.VALIDATOR,
|
||||||
|
headerName: t(ValidatorFields.VALIDATOR).toString(),
|
||||||
|
cellRenderer: ValidatorRenderer,
|
||||||
|
comparator: ({ name: a }, { name: b }) => Math.sign(a - b),
|
||||||
|
pinned: 'left',
|
||||||
|
width: 240,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.STAKE,
|
||||||
|
headerName: t(ValidatorFields.STAKE).toString(),
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.STAKE_NEEDED_FOR_PROMOTION,
|
||||||
|
headerName: t(ValidatorFields.STAKE_NEEDED_FOR_PROMOTION).toString(),
|
||||||
|
width: 210,
|
||||||
|
sort: 'asc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.STAKE_SHARE,
|
||||||
|
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.TOTAL_PENALTIES,
|
||||||
|
headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(),
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: ValidatorFields.PENDING_STAKE,
|
||||||
|
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="standby-pending-validators-table">
|
||||||
|
<AgGrid
|
||||||
|
domLayout="autoHeight"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
customThemeParams={NODE_LIST_GRID_STYLES}
|
||||||
|
rowHeight={52}
|
||||||
|
defaultColDef={defaultColDef}
|
||||||
|
animateRows={true}
|
||||||
|
suppressCellFocus={true}
|
||||||
|
overlayNoRowsTemplate={t('noValidators')}
|
||||||
|
ref={gridRef}
|
||||||
|
rowData={nodes}
|
||||||
|
columnDefs={colDefs}
|
||||||
|
onCellClicked={(event) => {
|
||||||
|
navigate(event.data.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <StandbyPendingTable ref={gridRef} />;
|
||||||
|
};
|
@ -0,0 +1,159 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import { ConsensusValidatorsTable } from './consensus-validators-table';
|
||||||
|
import { StandbyPendingValidatorsTable } from './standby-pending-validators-table';
|
||||||
|
import { Schema } from '@vegaprotocol/types';
|
||||||
|
import type {
|
||||||
|
NodesQuery,
|
||||||
|
NodesFragmentFragment,
|
||||||
|
} from '../__generated___/Nodes';
|
||||||
|
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
|
||||||
|
import { formatNumber } from '../../../../lib/format-number';
|
||||||
|
import {
|
||||||
|
createDocsLinks,
|
||||||
|
removePaginationWrapper,
|
||||||
|
toBigNum,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import { Link as UTLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
|
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
||||||
|
|
||||||
|
export interface ValidatorsTableProps {
|
||||||
|
data: NodesQuery | undefined;
|
||||||
|
previousEpochData: PreviousEpochQuery | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SortedValidatorsProps {
|
||||||
|
consensusValidators: NodesFragmentFragment[];
|
||||||
|
standbyValidators: NodesFragmentFragment[];
|
||||||
|
pendingValidators: NodesFragmentFragment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidatorTables = ({
|
||||||
|
data,
|
||||||
|
previousEpochData,
|
||||||
|
}: ValidatorsTableProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { VEGA_DOCS_URL } = useEnvironment();
|
||||||
|
const {
|
||||||
|
appState: { decimals },
|
||||||
|
} = useAppState();
|
||||||
|
const totalStake = useMemo(
|
||||||
|
() => data?.nodeData?.stakedTotal || '0',
|
||||||
|
[data?.nodeData?.stakedTotal]
|
||||||
|
);
|
||||||
|
let stakeNeededForPromotion = undefined;
|
||||||
|
|
||||||
|
const { consensusValidators, standbyValidators, pendingValidators } = useMemo(
|
||||||
|
() =>
|
||||||
|
removePaginationWrapper(data?.nodesConnection.edges).reduce(
|
||||||
|
(acc: SortedValidatorsProps, validator) => {
|
||||||
|
switch (validator.rankingScore?.status) {
|
||||||
|
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT:
|
||||||
|
acc.consensusValidators.push(validator);
|
||||||
|
break;
|
||||||
|
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ:
|
||||||
|
acc.standbyValidators.push(validator);
|
||||||
|
break;
|
||||||
|
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING:
|
||||||
|
acc.pendingValidators.push(validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
consensusValidators: [],
|
||||||
|
standbyValidators: [],
|
||||||
|
pendingValidators: [],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[data?.nodesConnection.edges]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
consensusValidators.length &&
|
||||||
|
(standbyValidators.length || pendingValidators.length)
|
||||||
|
) {
|
||||||
|
const lowestRankingConsensusScore = consensusValidators.reduce(
|
||||||
|
(lowest: NodesFragmentFragment, validator: NodesFragmentFragment) => {
|
||||||
|
if (
|
||||||
|
validator?.rankingScore &&
|
||||||
|
Number(validator.rankingScore) < Number(lowest.rankingScore)
|
||||||
|
) {
|
||||||
|
lowest = validator;
|
||||||
|
}
|
||||||
|
return lowest;
|
||||||
|
}
|
||||||
|
).rankingScore.rankingScore;
|
||||||
|
|
||||||
|
const lowestRankingBigNum = toBigNum(lowestRankingConsensusScore, 0);
|
||||||
|
const totalStakeBigNum = toBigNum(totalStake, decimals);
|
||||||
|
|
||||||
|
stakeNeededForPromotion = formatNumber(
|
||||||
|
lowestRankingBigNum.times(totalStakeBigNum),
|
||||||
|
2
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="validator-tables">
|
||||||
|
{consensusValidators.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h2>{t('status-tendermint')}</h2>
|
||||||
|
<ConsensusValidatorsTable
|
||||||
|
data={consensusValidators}
|
||||||
|
previousEpochData={previousEpochData}
|
||||||
|
totalStake={totalStake}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{standbyValidators.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h2>{t('status-ersatz')}</h2>
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="ersatzDescription"
|
||||||
|
values={{
|
||||||
|
stakeNeededForPromotion,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<StandbyPendingValidatorsTable
|
||||||
|
data={standbyValidators}
|
||||||
|
previousEpochData={previousEpochData}
|
||||||
|
totalStake={totalStake}
|
||||||
|
stakeNeededForPromotion={stakeNeededForPromotion}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{pendingValidators.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h2>{t('status-pending')}</h2>
|
||||||
|
<p>
|
||||||
|
{VEGA_DOCS_URL && (
|
||||||
|
<>
|
||||||
|
<span>{t('pendingDescription1')} </span>
|
||||||
|
<span>
|
||||||
|
<UTLink
|
||||||
|
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
|
||||||
|
target="_blank"
|
||||||
|
data-testid="validator-forum-link"
|
||||||
|
>
|
||||||
|
{t('pendingDescriptionLinkText')}
|
||||||
|
</UTLink>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span>{t('pendingDescription2')}</span>
|
||||||
|
</p>
|
||||||
|
<StandbyPendingValidatorsTable
|
||||||
|
data={pendingValidators}
|
||||||
|
previousEpochData={previousEpochData}
|
||||||
|
totalStake={totalStake}
|
||||||
|
stakeNeededForPromotion={stakeNeededForPromotion}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -3,14 +3,14 @@ import { Schema as Types } from '@vegaprotocol/types';
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
const defaultOptions = {} as const;
|
const defaultOptions = {} as const;
|
||||||
export type StakingNodeFieldsFragment = { __typename?: 'Node', id: string, name: string, pubkey: string, infoUrl: string, location: string, ethereumAddress: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, stakedByOperatorFormatted: string, stakedByDelegatesFormatted: string, stakedTotalFormatted: string, pendingStakeFormatted: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } };
|
export type StakingNodeFieldsFragment = { __typename?: 'Node', id: string, name: string, pubkey: string, infoUrl: string, location: string, ethereumAddress: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } };
|
||||||
|
|
||||||
export type StakingQueryVariables = Types.Exact<{
|
export type StakingQueryVariables = Types.Exact<{
|
||||||
partyId: Types.Scalars['ID'];
|
partyId: Types.Scalars['ID'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type StakingQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, amountFormatted: string, epoch: number, node: { __typename?: 'Node', id: string } } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: string | null, end?: string | null, expiry?: string | null } }, nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, pubkey: string, infoUrl: string, location: string, ethereumAddress: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, stakedByOperatorFormatted: string, stakedByDelegatesFormatted: string, stakedTotalFormatted: string, pendingStakeFormatted: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } } } | null> | null }, nodeData?: { __typename?: 'NodeData', stakedTotal: string, stakedTotalFormatted: string, totalNodes: number, inactiveNodes: number, validatingNodes: number, uptime: number } | null };
|
export type StakingQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string } } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: string | null, end?: string | null, expiry?: string | null } }, nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, pubkey: string, infoUrl: string, location: string, ethereumAddress: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } } } | null> | null }, nodeData?: { __typename?: 'NodeData', stakedTotal: string, totalNodes: number, inactiveNodes: number, uptime: number } | null };
|
||||||
|
|
||||||
export const StakingNodeFieldsFragmentDoc = gql`
|
export const StakingNodeFieldsFragmentDoc = gql`
|
||||||
fragment StakingNodeFields on Node {
|
fragment StakingNodeFields on Node {
|
||||||
|
@ -4,15 +4,18 @@ import { useParams } from 'react-router-dom';
|
|||||||
|
|
||||||
import { EpochCountdown } from '../../../components/epoch-countdown';
|
import { EpochCountdown } from '../../../components/epoch-countdown';
|
||||||
import { BigNumber } from '../../../lib/bignumber';
|
import { BigNumber } from '../../../lib/bignumber';
|
||||||
import { ConnectToVega } from '../../../components/connect-to-vega/connect-to-vega';
|
import { ConnectToVega } from '../../../components/connect-to-vega';
|
||||||
import { StakingForm } from './staking-form';
|
import { StakingForm } from './staking-form';
|
||||||
import { ValidatorTable } from './validator-table';
|
import { ValidatorTable } from './validator-table';
|
||||||
import { YourStake } from './your-stake';
|
import { YourStake } from './your-stake';
|
||||||
import NodeContainer from './nodes-container';
|
import NodeContainer from './nodes-container';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||||
import { addDecimal, toBigNum } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
import compact from 'lodash/compact';
|
addDecimal,
|
||||||
|
removePaginationWrapper,
|
||||||
|
toBigNum,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
import type { StakingQuery } from './__generated___/Staking';
|
import type { StakingQuery } from './__generated___/Staking';
|
||||||
|
|
||||||
interface StakingNodeProps {
|
interface StakingNodeProps {
|
||||||
@ -26,21 +29,20 @@ export const StakingNode = ({ data }: StakingNodeProps) => {
|
|||||||
} = useAppState();
|
} = useAppState();
|
||||||
const { node } = useParams<{ node: string }>();
|
const { node } = useParams<{ node: string }>();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const nodeInfo = React.useMemo(() => {
|
const nodeInfo = React.useMemo(
|
||||||
const canonisedNodes =
|
() =>
|
||||||
compact(data?.nodesConnection?.edges?.map((edge) => edge?.node)) || [];
|
removePaginationWrapper(data?.nodesConnection?.edges).find(
|
||||||
return canonisedNodes.find(({ id }) => id === node);
|
({ id }) => id === node
|
||||||
}, [node, data]);
|
),
|
||||||
|
[node, data]
|
||||||
|
);
|
||||||
|
|
||||||
const currentEpoch = React.useMemo(() => {
|
const currentEpoch = React.useMemo(() => {
|
||||||
return data?.epoch.id;
|
return data?.epoch.id;
|
||||||
}, [data?.epoch.id]);
|
}, [data?.epoch.id]);
|
||||||
|
|
||||||
const delegations = React.useMemo(
|
const delegations = React.useMemo(
|
||||||
() =>
|
() => removePaginationWrapper(data?.party?.delegationsConnection?.edges),
|
||||||
compact(
|
|
||||||
data?.party?.delegationsConnection?.edges?.map((edge) => edge?.node)
|
|
||||||
) || [],
|
|
||||||
[data?.party?.delegationsConnection?.edges]
|
[data?.party?.delegationsConnection?.edges]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -27,6 +26,7 @@ import {
|
|||||||
NetworkParams,
|
NetworkParams,
|
||||||
removeDecimal,
|
removeDecimal,
|
||||||
addDecimal,
|
addDecimal,
|
||||||
|
removePaginationWrapper,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { useBalances } from '../../../lib/balances/balances-store';
|
import { useBalances } from '../../../lib/balances/balances-store';
|
||||||
|
|
||||||
@ -157,8 +157,8 @@ export const StakingForm = ({
|
|||||||
delegationSearch();
|
delegationSearch();
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const delegation = compact(
|
const delegation = removePaginationWrapper(
|
||||||
data.party?.delegationsConnection?.edges?.map((edge) => edge?.node)
|
data.party?.delegationsConnection?.edges
|
||||||
).find((d) => {
|
).find((d) => {
|
||||||
return (
|
return (
|
||||||
d.node.id === nodeId && d.epoch === Number(data.epoch.id) + 1
|
d.node.id === nodeId && d.epoch === Number(data.epoch.id) + 1
|
||||||
|
@ -6,10 +6,20 @@ import { useEnvironment } from '@vegaprotocol/environment';
|
|||||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||||
import { BigNumber } from '../../../lib/bignumber';
|
import { BigNumber } from '../../../lib/bignumber';
|
||||||
import { formatNumber } from '../../../lib/format-number';
|
import { formatNumber } from '../../../lib/format-number';
|
||||||
import { statusTranslationKey } from '../home/node-list';
|
|
||||||
import type { StakingNodeFieldsFragment } from './__generated___/Staking';
|
import type { StakingNodeFieldsFragment } from './__generated___/Staking';
|
||||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||||
|
import { Schema } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
const statuses = {
|
||||||
|
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ]: 'status-ersatz',
|
||||||
|
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING]: 'status-pending',
|
||||||
|
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT]:
|
||||||
|
'status-tendermint',
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusTranslationKey = (status: Schema.ValidatorStatus) =>
|
||||||
|
statuses[status];
|
||||||
|
|
||||||
const ValidatorTableCell = ({
|
const ValidatorTableCell = ({
|
||||||
children,
|
children,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import compact from 'lodash/compact';
|
|
||||||
import { assetsProvider } from '@vegaprotocol/assets';
|
import { assetsProvider } from '@vegaprotocol/assets';
|
||||||
import { marketsProvider } from '@vegaprotocol/market-list';
|
import { marketsProvider } from '@vegaprotocol/market-list';
|
||||||
import {
|
import {
|
||||||
makeDataProvider,
|
makeDataProvider,
|
||||||
makeDerivedDataProvider,
|
makeDerivedDataProvider,
|
||||||
|
removePaginationWrapper,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { Schema } from '@vegaprotocol/types';
|
import { Schema } from '@vegaprotocol/types';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
@ -76,7 +76,7 @@ const getData = (
|
|||||||
responseData: AccountsQuery
|
responseData: AccountsQuery
|
||||||
): AccountFieldsFragment[] | null => {
|
): AccountFieldsFragment[] | null => {
|
||||||
return (
|
return (
|
||||||
compact(responseData.party?.accountsConnection?.edges).map((e) => e.node) ??
|
removePaginationWrapper(responseData.party?.accountsConnection?.edges) ??
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { BigNumber } from 'bignumber.js';
|
import { BigNumber } from 'bignumber.js';
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import { useMarketPositionsQuery } from './__generated__/MarketPositions';
|
import { useMarketPositionsQuery } from './__generated__/MarketPositions';
|
||||||
|
import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
|
||||||
interface Props {
|
interface Props {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
partyId: string;
|
partyId: string;
|
||||||
@ -22,8 +22,8 @@ export const useMarketPositions = ({
|
|||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'no-cache',
|
||||||
});
|
});
|
||||||
|
|
||||||
const accounts = compact(data?.party?.accountsConnection?.edges).map(
|
const accounts = removePaginationWrapper(
|
||||||
(e) => e.node
|
data?.party?.accountsConnection?.edges
|
||||||
);
|
);
|
||||||
const account = accounts.find((nodes) => nodes.market?.id === marketId);
|
const account = accounts.find((nodes) => nodes.market?.id === marketId);
|
||||||
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { BigNumber } from 'bignumber.js';
|
import { BigNumber } from 'bignumber.js';
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { addDecimal, formatNumber } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
|
addDecimal,
|
||||||
|
formatNumber,
|
||||||
|
removePaginationWrapper,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { useMarketPositions } from './use-market-positions';
|
import { useMarketPositions } from './use-market-positions';
|
||||||
import { useMarketDataMarkPrice } from './use-market-data-mark-price';
|
import { useMarketDataMarkPrice } from './use-market-data-mark-price';
|
||||||
import { usePartyMarketDataQuery } from './__generated__/PartyMarketData';
|
import { usePartyMarketDataQuery } from './__generated__/PartyMarketData';
|
||||||
@ -23,8 +26,8 @@ export const useOrderCloseOut = ({
|
|||||||
partyData,
|
partyData,
|
||||||
}: Props): string | null => {
|
}: Props): string | null => {
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const accounts = compact(partyData?.party?.accountsConnection?.edges).map(
|
const accounts = removePaginationWrapper(
|
||||||
(e) => e.node
|
partyData?.party?.accountsConnection?.edges
|
||||||
);
|
);
|
||||||
const account = useSettlementAccount(
|
const account = useSettlementAccount(
|
||||||
market.tradableInstrument.instrument.product.settlementAsset.id,
|
market.tradableInstrument.instrument.product.settlementAsset.id,
|
||||||
@ -51,8 +54,8 @@ export const useOrderCloseOut = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataAccounts = compact(data?.party?.accountsConnection?.edges).map(
|
const dataAccounts = removePaginationWrapper(
|
||||||
(e) => e.node
|
data?.party?.accountsConnection?.edges
|
||||||
);
|
);
|
||||||
const positionAccount = dataAccounts.find(
|
const positionAccount = dataAccounts.find(
|
||||||
(account) => account.market?.id === market.id
|
(account) => account.market?.id === market.id
|
||||||
|
@ -3,6 +3,7 @@ import { useEnvironment } from '@vegaprotocol/environment';
|
|||||||
import { totalFeesPercentage } from '@vegaprotocol/market-list';
|
import { totalFeesPercentage } from '@vegaprotocol/market-list';
|
||||||
import {
|
import {
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
removePaginationWrapper,
|
||||||
t,
|
t,
|
||||||
useDataProvider,
|
useDataProvider,
|
||||||
useYesterday,
|
useYesterday,
|
||||||
@ -21,7 +22,6 @@ import {
|
|||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { generatePath, Link } from 'react-router-dom';
|
import { generatePath, Link } from 'react-router-dom';
|
||||||
|
|
||||||
@ -102,8 +102,8 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
|||||||
|
|
||||||
if (!market) return null;
|
if (!market) return null;
|
||||||
|
|
||||||
const marketAccounts = compact(market.accountsConnection?.edges).map(
|
const marketAccounts = removePaginationWrapper(
|
||||||
(e) => e.node
|
market.accountsConnection?.edges
|
||||||
);
|
);
|
||||||
|
|
||||||
const marketDataPanels = [
|
const marketDataPanels = [
|
||||||
|
@ -6,7 +6,7 @@ const defaultOptions = {} as const;
|
|||||||
export type StatsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
export type StatsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type StatsQuery = { __typename?: 'Query', nodeData?: { __typename?: 'NodeData', stakedTotal: string, totalNodes: number, inactiveNodes: number, validatingNodes: number } | null, statistics: { __typename?: 'Statistics', status: string, blockHeight: string, blockDuration: string, backlogLength: string, txPerBlock: string, tradesPerSecond: string, ordersPerSecond: string, averageOrdersPerBlock: string, vegaTime: string, appVersion: string, chainVersion: string, chainId: string, genesisTime: string } };
|
export type StatsQuery = { __typename?: 'Query', nodeData?: { __typename?: 'NodeData', stakedTotal: string, totalNodes: number, inactiveNodes: number } | null, statistics: { __typename?: 'Statistics', status: string, blockHeight: string, blockDuration: string, backlogLength: string, txPerBlock: string, tradesPerSecond: string, ordersPerSecond: string, averageOrdersPerBlock: string, vegaTime: string, appVersion: string, chainVersion: string, chainId: string, genesisTime: string } };
|
||||||
|
|
||||||
|
|
||||||
export const StatsDocument = gql`
|
export const StatsDocument = gql`
|
||||||
|
@ -12,4 +12,5 @@ export * from './lib/storage';
|
|||||||
export * from './lib/time';
|
export * from './lib/time';
|
||||||
export * from './lib/validate';
|
export * from './lib/validate';
|
||||||
export * from './lib/links';
|
export * from './lib/links';
|
||||||
|
export * from './lib/remove-pagination-wrapper';
|
||||||
export * from './lib/__generated__/ChainId';
|
export * from './lib/__generated__/ChainId';
|
||||||
|
@ -11,3 +11,4 @@ export * from './pagination';
|
|||||||
export * from './remove-0x';
|
export * from './remove-0x';
|
||||||
export * from './time';
|
export * from './time';
|
||||||
export * from './links';
|
export * from './links';
|
||||||
|
export * from './remove-pagination-wrapper';
|
||||||
|
@ -10,6 +10,7 @@ export const createDocsLinks = (docsUrl: string) => ({
|
|||||||
STAKING_GUIDE: `${docsUrl}/concepts/vega-chain/#staking-on-vega`,
|
STAKING_GUIDE: `${docsUrl}/concepts/vega-chain/#staking-on-vega`,
|
||||||
VEGA_WALLET_CONCEPTS_URL: `${docsUrl}/concepts/vega-wallet`,
|
VEGA_WALLET_CONCEPTS_URL: `${docsUrl}/concepts/vega-wallet`,
|
||||||
PROPOSALS_GUIDE: `${docsUrl}/tutorials/proposals`,
|
PROPOSALS_GUIDE: `${docsUrl}/tutorials/proposals`,
|
||||||
|
NODE_OPERATORS: `${docsUrl}/node-operators`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ExternalLinks = {
|
export const ExternalLinks = {
|
||||||
|
33
libs/react-helpers/src/lib/remove-pagination-wrapper.spec.ts
Normal file
33
libs/react-helpers/src/lib/remove-pagination-wrapper.spec.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { removePaginationWrapper } from './remove-pagination-wrapper';
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{ id: '1', name: 'foo' },
|
||||||
|
{ id: '2', name: 'bar' },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('Remove node from edges', () => {
|
||||||
|
it('should remove the node level structure', () => {
|
||||||
|
const edges = [
|
||||||
|
{ node: { id: '1', name: 'foo' } },
|
||||||
|
{ node: { id: '2', name: 'bar' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(removePaginationWrapper(edges)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove any null values', () => {
|
||||||
|
const edges = [
|
||||||
|
{ node: { id: '1', name: 'foo' } },
|
||||||
|
{ node: null },
|
||||||
|
{ node: { id: '2', name: 'bar' } },
|
||||||
|
null,
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(removePaginationWrapper(edges)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array if no edges are provided', () => {
|
||||||
|
expect(removePaginationWrapper(null)).toEqual([]);
|
||||||
|
expect(removePaginationWrapper(undefined)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
28
libs/react-helpers/src/lib/remove-pagination-wrapper.ts
Normal file
28
libs/react-helpers/src/lib/remove-pagination-wrapper.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import compact from 'lodash/compact';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple function that removes the 'node' level from API responses, i.e.
|
||||||
|
*
|
||||||
|
* [
|
||||||
|
* { node: { id: '1', name: 'foo' } },
|
||||||
|
* { node: { id: '2', name: 'bar' } },
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* becomes:
|
||||||
|
*
|
||||||
|
* [
|
||||||
|
* { id: '1', name: 'foo' },
|
||||||
|
* { id: '2', name: 'bar' },
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* It also removes any null values.
|
||||||
|
*
|
||||||
|
* @param edges array
|
||||||
|
* @returns array
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function removePaginationWrapper<T>(
|
||||||
|
edges: Array<{ node: T } | null> | undefined | null
|
||||||
|
): T[] {
|
||||||
|
return compact(edges?.map((edge) => edge?.node));
|
||||||
|
}
|
2
libs/types/src/__generated__/types.ts
generated
2
libs/types/src/__generated__/types.ts
generated
@ -1877,6 +1877,8 @@ export type NodeData = {
|
|||||||
totalNodes: Scalars['Int'];
|
totalNodes: Scalars['Int'];
|
||||||
/** Total uptime for all epochs across all nodes. Or specify a number of epochs */
|
/** Total uptime for all epochs across all nodes. Or specify a number of epochs */
|
||||||
uptime: Scalars['Float'];
|
uptime: Scalars['Float'];
|
||||||
|
/** Number of nodes validating */
|
||||||
|
validatingNodes: Scalars['Int'];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Edge type containing the node and cursor information returned by a NodesConnection */
|
/** Edge type containing the node and cursor information returned by a NodesConnection */
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { useNetworkParamsQuery } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
import compact from 'lodash/compact';
|
removePaginationWrapper,
|
||||||
|
useNetworkParamsQuery,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export interface EthereumConfig {
|
export interface EthereumConfig {
|
||||||
@ -31,9 +33,9 @@ export const useEthereumConfig = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const param = compact(data.networkParametersConnection.edges)
|
const param = removePaginationWrapper(
|
||||||
.map(({ node }) => node)
|
data.networkParametersConnection.edges
|
||||||
?.find((node) => node.key === 'blockchains.ethereumConfig');
|
)?.find(({ key }) => key === 'blockchains.ethereumConfig');
|
||||||
|
|
||||||
if (!param) {
|
if (!param) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
|
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import uniqBy from 'lodash/uniqBy';
|
import uniqBy from 'lodash/uniqBy';
|
||||||
@ -14,6 +13,7 @@ import type {
|
|||||||
WithdrawalEventSubscription,
|
WithdrawalEventSubscription,
|
||||||
WithdrawalEventSubscriptionVariables,
|
WithdrawalEventSubscriptionVariables,
|
||||||
} from './__generated__/Withdrawal';
|
} from './__generated__/Withdrawal';
|
||||||
|
import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
type WithdrawalEdges = { node: WithdrawalFieldsFragment }[];
|
type WithdrawalEdges = { node: WithdrawalFieldsFragment }[];
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ export const useWithdrawals = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return orderBy(
|
return orderBy(
|
||||||
compact(data.party.withdrawalsConnection.edges).map((edge) => edge.node),
|
removePaginationWrapper(data.party.withdrawalsConnection.edges),
|
||||||
'createdTimestamp',
|
'createdTimestamp',
|
||||||
'desc'
|
'desc'
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user