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 compact from 'lodash/compact';
|
||||
import {
|
||||
DealTicketManager,
|
||||
usePartyBalanceQuery,
|
||||
} from '@vegaprotocol/deal-ticket';
|
||||
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 { DealTicketSteps } from './deal-ticket-steps';
|
||||
import { DealTicketBalance } from './deal-ticket-balance';
|
||||
@ -46,8 +49,8 @@ export const DealTicketContainer = () => {
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
const accounts = compact(partyData?.party?.accountsConnection?.edges).map(
|
||||
(e) => e.node
|
||||
const accounts = removePaginationWrapper(
|
||||
partyData?.party?.accountsConnection?.edges
|
||||
);
|
||||
|
||||
const loader = <Loader />;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import compact from 'lodash/compact';
|
||||
import { Stepper } from '../stepper';
|
||||
import {
|
||||
getDefaultOrder,
|
||||
@ -25,6 +24,7 @@ import {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimal,
|
||||
formatNumber,
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
useOrderSubmit,
|
||||
@ -82,8 +82,8 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
const accounts = compact(partyBalance?.party?.accountsConnection?.edges).map(
|
||||
(e) => e.node
|
||||
const accounts = removePaginationWrapper(
|
||||
partyBalance?.party?.accountsConnection?.edges
|
||||
);
|
||||
const maxTrade = useMaximumPositionSize({
|
||||
partyId: pubKey || '',
|
||||
|
@ -7,14 +7,14 @@ import {
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
removePaginationWrapper,
|
||||
suitableForSyntaxHighlighter,
|
||||
t,
|
||||
useNetworkParamsQuery,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { suitableForSyntaxHighlighter } from '@vegaprotocol/react-helpers';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/react-helpers';
|
||||
import { useNetworkParamsQuery } from '@vegaprotocol/react-helpers';
|
||||
import compact from 'lodash/compact';
|
||||
|
||||
const PERCENTAGE_PARAMS = [
|
||||
'governance.proposal.asset.requiredMajority',
|
||||
@ -107,9 +107,7 @@ export const NetworkParametersTable = ({
|
||||
error={error}
|
||||
render={(data) => {
|
||||
const ascParams = orderBy(
|
||||
compact(data.networkParametersConnection.edges).map(
|
||||
({ node }) => node
|
||||
) || [],
|
||||
removePaginationWrapper(data.networkParametersConnection.edges),
|
||||
(param) => param.key,
|
||||
'asc'
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=DEVNET
|
||||
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_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
|
@ -1,7 +1,7 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=TESTNET
|
||||
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_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
|
@ -88,7 +88,7 @@ const Web3Container = ({
|
||||
<AppLoader>
|
||||
<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}>
|
||||
<AppRouter />
|
||||
</TemplateSidebar>
|
||||
|
@ -4,7 +4,6 @@ import keyBy from 'lodash/keyBy';
|
||||
import uniq from 'lodash/uniq';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import compact from 'lodash/compact';
|
||||
|
||||
import noIcon from '../../images/token-no-icon.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 { isAssetTypeERC20 } from '@vegaprotocol/assets';
|
||||
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 { addDecimal } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
@ -66,9 +65,9 @@ export const usePollForDelegations = () => {
|
||||
})
|
||||
.then((res) => {
|
||||
if (!mounted) return;
|
||||
const canonisedDelegations = compact(
|
||||
const canonisedDelegations = removePaginationWrapper(
|
||||
res.data.party?.delegationsConnection?.edges
|
||||
).map(({ node }) => node);
|
||||
);
|
||||
const filter =
|
||||
canonisedDelegations.filter((d) => {
|
||||
return d.epoch.toString() === res.data.epoch.id;
|
||||
@ -85,9 +84,9 @@ export const usePollForDelegations = () => {
|
||||
decimals
|
||||
)
|
||||
);
|
||||
const accounts = compact(
|
||||
const accounts = removePaginationWrapper(
|
||||
res.data.party?.accountsConnection?.edges
|
||||
).map((e) => e.node);
|
||||
);
|
||||
setAccounts(
|
||||
accounts
|
||||
.filter(
|
||||
|
@ -574,19 +574,25 @@
|
||||
"proposalTerms": "Proposal terms",
|
||||
"currentlySetTo": "Currently expected to ",
|
||||
"finalOutcomeMayDiffer": "Final outcome may differ",
|
||||
"rankingScore": "Ranking score",
|
||||
"stakeScore": "Stake score",
|
||||
"performanceScore": "Performance",
|
||||
"votingPower": "Voting power",
|
||||
"normalisedVotingPower": "Normalised voting power",
|
||||
"noValidators": "No validators",
|
||||
"validator": "Validator",
|
||||
"totalStakeThisEpoch": "Total stake this epoch",
|
||||
"share": "Share",
|
||||
"validatorStake": "Validator stake",
|
||||
"stake": "Stake",
|
||||
"stakeShare": "Stake share",
|
||||
"pendingStake": "Pending stake",
|
||||
"totalPenalties": "Total penalties",
|
||||
"noPenaltyDataFromLastEpoch": "No penalty data from last epoch",
|
||||
"stakeNeededForPromotion": "Stake needed for promotion",
|
||||
"Nodes": "Nodes",
|
||||
"status-tendermint": "Consensus",
|
||||
"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",
|
||||
"pass": "pass",
|
||||
"fail": "fail",
|
||||
|
@ -1,4 +1,3 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { format } from 'date-fns';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -11,7 +10,11 @@ import type {
|
||||
RewardFieldsFragment,
|
||||
DelegationFieldsFragment,
|
||||
} 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';
|
||||
|
||||
interface RewardInfoProps {
|
||||
@ -25,10 +28,7 @@ export const RewardInfo = ({ data, currVegaKey }: RewardInfoProps) => {
|
||||
const rewards = React.useMemo(() => {
|
||||
if (!data?.party || !data.party.rewardsConnection?.edges?.length) return [];
|
||||
|
||||
return (
|
||||
compact(data.party.rewardsConnection.edges.map((edge) => edge?.node)) ||
|
||||
[]
|
||||
);
|
||||
return removePaginationWrapper(data.party.rewardsConnection.edges);
|
||||
}, [data]);
|
||||
|
||||
const delegations = React.useMemo(() => {
|
||||
@ -36,11 +36,7 @@ export const RewardInfo = ({ data, currVegaKey }: RewardInfoProps) => {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
compact(
|
||||
data.party.delegationsConnection.edges.map((edge) => edge?.node)
|
||||
) || []
|
||||
);
|
||||
return removePaginationWrapper(data.party.delegationsConnection.edges);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
|
@ -9,9 +9,11 @@ import { useGetAssociationBreakdown } from '../../../hooks/use-get-association-b
|
||||
import { useRefreshBalances } from '../../../hooks/use-refresh-balances';
|
||||
import { useTransaction } from '../../../hooks/use-transaction';
|
||||
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 compact from 'lodash/compact';
|
||||
import type {
|
||||
LinkingsFieldsFragment,
|
||||
PartyStakeLinkingsQuery,
|
||||
@ -92,12 +94,9 @@ export const usePollForStakeLinking = (
|
||||
fetchPolicy: 'no-cache',
|
||||
})
|
||||
.then((res) => {
|
||||
const linkings =
|
||||
compact(
|
||||
res.data?.party?.stakingSummary.linkings.edges?.map(
|
||||
(e) => e?.node
|
||||
)
|
||||
) || [];
|
||||
const linkings = removePaginationWrapper(
|
||||
res.data?.party?.stakingSummary.linkings.edges
|
||||
);
|
||||
|
||||
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 { StakingIntro } from './staking-intro';
|
||||
import { NodeList } from './node-list';
|
||||
import { EpochData } from './epoch-data';
|
||||
|
||||
export const Staking = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -10,7 +10,7 @@ export const Staking = () => {
|
||||
<StakingIntro />
|
||||
<section>
|
||||
<h2 className="text-2xl uppercase">{t('Validator nodes')}</h2>
|
||||
<NodeList data-testid="node-list" />
|
||||
<EpochData data-testid="epoch-data" />
|
||||
</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 * as Apollo from '@apollo/client';
|
||||
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<{
|
||||
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`
|
||||
fragment StakingNodeFields on Node {
|
||||
|
@ -4,15 +4,18 @@ import { useParams } from 'react-router-dom';
|
||||
|
||||
import { EpochCountdown } from '../../../components/epoch-countdown';
|
||||
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 { ValidatorTable } from './validator-table';
|
||||
import { YourStake } from './your-stake';
|
||||
import NodeContainer from './nodes-container';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||
import { addDecimal, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import compact from 'lodash/compact';
|
||||
import {
|
||||
addDecimal,
|
||||
removePaginationWrapper,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type { StakingQuery } from './__generated___/Staking';
|
||||
|
||||
interface StakingNodeProps {
|
||||
@ -26,21 +29,20 @@ export const StakingNode = ({ data }: StakingNodeProps) => {
|
||||
} = useAppState();
|
||||
const { node } = useParams<{ node: string }>();
|
||||
const { t } = useTranslation();
|
||||
const nodeInfo = React.useMemo(() => {
|
||||
const canonisedNodes =
|
||||
compact(data?.nodesConnection?.edges?.map((edge) => edge?.node)) || [];
|
||||
return canonisedNodes.find(({ id }) => id === node);
|
||||
}, [node, data]);
|
||||
const nodeInfo = React.useMemo(
|
||||
() =>
|
||||
removePaginationWrapper(data?.nodesConnection?.edges).find(
|
||||
({ id }) => id === node
|
||||
),
|
||||
[node, data]
|
||||
);
|
||||
|
||||
const currentEpoch = React.useMemo(() => {
|
||||
return data?.epoch.id;
|
||||
}, [data?.epoch.id]);
|
||||
|
||||
const delegations = React.useMemo(
|
||||
() =>
|
||||
compact(
|
||||
data?.party?.delegationsConnection?.edges?.map((edge) => edge?.node)
|
||||
) || [],
|
||||
() => removePaginationWrapper(data?.party?.delegationsConnection?.edges),
|
||||
[data?.party?.delegationsConnection?.edges]
|
||||
);
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import compact from 'lodash/compact';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@ -27,6 +26,7 @@ import {
|
||||
NetworkParams,
|
||||
removeDecimal,
|
||||
addDecimal,
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useBalances } from '../../../lib/balances/balances-store';
|
||||
|
||||
@ -157,8 +157,8 @@ export const StakingForm = ({
|
||||
delegationSearch();
|
||||
|
||||
if (data) {
|
||||
const delegation = compact(
|
||||
data.party?.delegationsConnection?.edges?.map((edge) => edge?.node)
|
||||
const delegation = removePaginationWrapper(
|
||||
data.party?.delegationsConnection?.edges
|
||||
).find((d) => {
|
||||
return (
|
||||
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 { BigNumber } from '../../../lib/bignumber';
|
||||
import { formatNumber } from '../../../lib/format-number';
|
||||
import { statusTranslationKey } from '../home/node-list';
|
||||
import type { StakingNodeFieldsFragment } from './__generated___/Staking';
|
||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||
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 = ({
|
||||
children,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { assetsProvider } from '@vegaprotocol/assets';
|
||||
import { marketsProvider } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { Schema } from '@vegaprotocol/types';
|
||||
import produce from 'immer';
|
||||
@ -76,7 +76,7 @@ const getData = (
|
||||
responseData: AccountsQuery
|
||||
): AccountFieldsFragment[] | null => {
|
||||
return (
|
||||
compact(responseData.party?.accountsConnection?.edges).map((e) => e.node) ??
|
||||
removePaginationWrapper(responseData.party?.accountsConnection?.edges) ??
|
||||
null
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import compact from 'lodash/compact';
|
||||
import { useMarketPositionsQuery } from './__generated__/MarketPositions';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
|
||||
interface Props {
|
||||
marketId: string;
|
||||
partyId: string;
|
||||
@ -22,8 +22,8 @@ export const useMarketPositions = ({
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
const accounts = compact(data?.party?.accountsConnection?.edges).map(
|
||||
(e) => e.node
|
||||
const accounts = removePaginationWrapper(
|
||||
data?.party?.accountsConnection?.edges
|
||||
);
|
||||
const account = accounts.find((nodes) => nodes.market?.id === marketId);
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import compact from 'lodash/compact';
|
||||
import type { OrderSubmissionBody } 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 { useMarketDataMarkPrice } from './use-market-data-mark-price';
|
||||
import { usePartyMarketDataQuery } from './__generated__/PartyMarketData';
|
||||
@ -23,8 +26,8 @@ export const useOrderCloseOut = ({
|
||||
partyData,
|
||||
}: Props): string | null => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const accounts = compact(partyData?.party?.accountsConnection?.edges).map(
|
||||
(e) => e.node
|
||||
const accounts = removePaginationWrapper(
|
||||
partyData?.party?.accountsConnection?.edges
|
||||
);
|
||||
const account = useSettlementAccount(
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id,
|
||||
@ -51,8 +54,8 @@ export const useOrderCloseOut = ({
|
||||
)
|
||||
);
|
||||
|
||||
const dataAccounts = compact(data?.party?.accountsConnection?.edges).map(
|
||||
(e) => e.node
|
||||
const dataAccounts = removePaginationWrapper(
|
||||
data?.party?.accountsConnection?.edges
|
||||
);
|
||||
const positionAccount = dataAccounts.find(
|
||||
(account) => account.market?.id === market.id
|
||||
|
@ -3,6 +3,7 @@ import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { totalFeesPercentage } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
formatNumber,
|
||||
removePaginationWrapper,
|
||||
t,
|
||||
useDataProvider,
|
||||
useYesterday,
|
||||
@ -21,7 +22,6 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import pick from 'lodash/pick';
|
||||
import compact from 'lodash/compact';
|
||||
import { useMemo } from 'react';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
|
||||
@ -102,8 +102,8 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
|
||||
if (!market) return null;
|
||||
|
||||
const marketAccounts = compact(market.accountsConnection?.edges).map(
|
||||
(e) => e.node
|
||||
const marketAccounts = removePaginationWrapper(
|
||||
market.accountsConnection?.edges
|
||||
);
|
||||
|
||||
const marketDataPanels = [
|
||||
|
@ -6,7 +6,7 @@ const defaultOptions = {} as const;
|
||||
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`
|
||||
|
@ -12,4 +12,5 @@ export * from './lib/storage';
|
||||
export * from './lib/time';
|
||||
export * from './lib/validate';
|
||||
export * from './lib/links';
|
||||
export * from './lib/remove-pagination-wrapper';
|
||||
export * from './lib/__generated__/ChainId';
|
||||
|
@ -11,3 +11,4 @@ export * from './pagination';
|
||||
export * from './remove-0x';
|
||||
export * from './time';
|
||||
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`,
|
||||
VEGA_WALLET_CONCEPTS_URL: `${docsUrl}/concepts/vega-wallet`,
|
||||
PROPOSALS_GUIDE: `${docsUrl}/tutorials/proposals`,
|
||||
NODE_OPERATORS: `${docsUrl}/node-operators`,
|
||||
});
|
||||
|
||||
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'];
|
||||
/** Total uptime for all epochs across all nodes. Or specify a number of epochs */
|
||||
uptime: Scalars['Float'];
|
||||
/** Number of nodes validating */
|
||||
validatingNodes: Scalars['Int'];
|
||||
};
|
||||
|
||||
/** Edge type containing the node and cursor information returned by a NodesConnection */
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { useNetworkParamsQuery } from '@vegaprotocol/react-helpers';
|
||||
import compact from 'lodash/compact';
|
||||
import {
|
||||
removePaginationWrapper,
|
||||
useNetworkParamsQuery,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export interface EthereumConfig {
|
||||
@ -31,9 +33,9 @@ export const useEthereumConfig = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const param = compact(data.networkParametersConnection.edges)
|
||||
.map(({ node }) => node)
|
||||
?.find((node) => node.key === 'blockchains.ethereumConfig');
|
||||
const param = removePaginationWrapper(
|
||||
data.networkParametersConnection.edges
|
||||
)?.find(({ key }) => key === 'blockchains.ethereumConfig');
|
||||
|
||||
if (!param) {
|
||||
return null;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import compact from 'lodash/compact';
|
||||
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
@ -14,6 +13,7 @@ import type {
|
||||
WithdrawalEventSubscription,
|
||||
WithdrawalEventSubscriptionVariables,
|
||||
} from './__generated__/Withdrawal';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
|
||||
|
||||
type WithdrawalEdges = { node: WithdrawalFieldsFragment }[];
|
||||
|
||||
@ -47,7 +47,7 @@ export const useWithdrawals = () => {
|
||||
}
|
||||
|
||||
return orderBy(
|
||||
compact(data.party.withdrawalsConnection.edges).map((edge) => edge.node),
|
||||
removePaginationWrapper(data.party.withdrawalsConnection.edges),
|
||||
'createdTimestamp',
|
||||
'desc'
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user