feat(governance): see user stake on validators table (#3250)

Co-authored-by: Joe <joe@vega.xyz>
This commit is contained in:
Sam Keen 2023-03-28 15:28:15 +01:00 committed by GitHub
parent c25858037a
commit eca212eee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 475 additions and 596 deletions

View File

@ -74,7 +74,7 @@ context('Validators Page - verify elements on page', function () {
}); });
it('Should be able to see validator stake', function () { it('Should be able to see validator stake', function () {
cy.get('[col-id="stake"] > div > span > span') cy.getByTestId('total-stake')
.should('have.length.at.least', 1) .should('have.length.at.least', 1)
.each(($stake) => { .each(($stake) => {
cy.wrap($stake).should('not.be.empty'); cy.wrap($stake).should('not.be.empty');
@ -82,7 +82,7 @@ context('Validators Page - verify elements on page', function () {
}); });
it('Should be able to see validator stake tooltip', function () { it('Should be able to see validator stake tooltip', function () {
cy.get('[col-id="stake"] > div > span > span').first().realHover(); cy.getByTestId('total-stake').first().realHover();
cy.get(stakedByOperatorToolTip) cy.get(stakedByOperatorToolTip)
.invoke('text') .invoke('text')
@ -96,17 +96,15 @@ context('Validators Page - verify elements on page', function () {
}); });
it('Should be able to see validator normalised voting power', function () { it('Should be able to see validator normalised voting power', function () {
cy.get('[col-id="normalisedVotingPower"] > div > span > span') cy.getByTestId('normalised-voting-power')
.should('have.length.at.least', 1) .should('have.length.at.least', 1)
.each(($vPower) => { .each(($vPower) => {
cy.wrap($vPower).should('not.be.empty'); cy.wrap($vPower).should('not.be.empty');
}); });
}); });
it('Should be able to see validator voting power tooltip', function () { it('Should be able to see validator normalised voting power tooltip', function () {
cy.get('[col-id="normalisedVotingPower"] > div > span > span') cy.getByTestId('normalised-voting-power').first().realHover();
.first()
.realHover();
cy.get(unnormalisedVotingPowerToolTip) cy.get(unnormalisedVotingPowerToolTip)
.invoke('text') .invoke('text')
@ -118,7 +116,7 @@ context('Validators Page - verify elements on page', function () {
// 2002-SINC-018 // 2002-SINC-018
it('Should be able to see validator total penalties', function () { it('Should be able to see validator total penalties', function () {
cy.get('[col-id="totalPenalties"] > div > span > span') cy.getByTestId('total-penalty')
.should('have.length.at.least', 1) .should('have.length.at.least', 1)
.each(($penalties) => { .each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%'); cy.wrap($penalties).should('contain.text', '0%');
@ -126,7 +124,7 @@ context('Validators Page - verify elements on page', function () {
}); });
it('Should be able to see validator penalties tooltip', function () { it('Should be able to see validator penalties tooltip', function () {
cy.get('[col-id="totalPenalties"] > div > span > span').realHover(); cy.getByTestId('total-penalty').realHover();
cy.get(performancePenaltyToolTip) cy.get(performancePenaltyToolTip)
.invoke('text') .invoke('text')
@ -140,7 +138,7 @@ context('Validators Page - verify elements on page', function () {
}); });
it('Should be able to see validator pending stake', function () { it('Should be able to see validator pending stake', function () {
cy.get('[col-id="pendingStake"] > div > span') cy.getByTestId('total-pending-stake')
.should('have.length.at.least', 1) .should('have.length.at.least', 1)
.each(($pendingStake) => { .each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00'); cy.wrap($pendingStake).should('contain.text', '0.00');

View File

@ -14,8 +14,8 @@ const associateWalletRadioButton = '[data-testid="associate-radio-wallet"]';
const associateContractRadioButton = '[data-testid="associate-radio-contract"]'; const associateContractRadioButton = '[data-testid="associate-radio-contract"]';
const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]'; const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]';
const stakeValidatorListPendingStake = '[col-id="pendingStake"]'; const stakeValidatorListPendingStake = '[col-id="pendingStake"]';
const stakeValidatorListTotalStake = '[col-id="stake"] > div > span'; const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = '[col-id="stakeShare"] > div > span'; const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListName = '[col-id="validator"]'; const stakeValidatorListName = '[col-id="validator"]';
const vegaKeySelector = '#vega-key-selector'; const vegaKeySelector = '#vega-key-selector';
const dialogCloseButton = '[data-testid="dialog-close"]'; const dialogCloseButton = '[data-testid="dialog-close"]';
@ -185,11 +185,11 @@ export function validateValidatorListTotalStakeAndShare(
cy.contains('Loading...', epochTimeout).should('not.exist'); cy.contains('Loading...', epochTimeout).should('not.exist');
waitForBeginningOfEpoch(); waitForBeginningOfEpoch();
cy.get(`[row-id="${positionOnList}"]`).within(() => { cy.get(`[row-id="${positionOnList}"]`).within(() => {
cy.get(stakeValidatorListTotalStake, epochTimeout).should( cy.getByTestId(stakeValidatorListTotalStake, epochTimeout).should(
'have.text', 'have.text',
expectedTotalStake expectedTotalStake
); );
cy.get(stakeValidatorListTotalShare, epochTimeout).should( cy.getByTestId(stakeValidatorListTotalShare, epochTimeout).should(
'have.text', 'have.text',
expectedTotalShare expectedTotalShare
); );

View File

@ -602,11 +602,14 @@
"noValidators": "No validators", "noValidators": "No validators",
"validator": "Validator", "validator": "Validator",
"stake": "Stake", "stake": "Stake",
"myStake": "My stake",
"stakeShare": "Stake share", "stakeShare": "Stake share",
"stakedByOperator": "Staked by operator", "stakedByOperator": "Staked by operator",
"stakedByDelegates": "Staked by delegates", "stakedByDelegates": "Staked by delegates",
"stakedByMe": "Staked by me",
"totalStake": "Total stake", "totalStake": "Total stake",
"pendingStake": "Pending stake", "pendingStake": "Pending stake",
"myPendingStake": "My pending stake",
"totalPenalties": "Total penalties", "totalPenalties": "Total penalties",
"noPenaltyDataFromLastEpoch": "No penalty data from last epoch", "noPenaltyDataFromLastEpoch": "No penalty data from last epoch",
"stakeNeededForPromotion": "Stake needed for promotion", "stakeNeededForPromotion": "Stake needed for promotion",

View File

@ -12,14 +12,14 @@ import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
import { ProposalsListItem } from '../proposals/components/proposals-list-item'; import { ProposalsListItem } from '../proposals/components/proposals-list-item';
import Routes from '../routes'; import Routes from '../routes';
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils'; import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated___/Nodes'; import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals'; import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import { getNotRejectedProposals } from '../proposals/proposals/proposals-container'; import { getNotRejectedProposals } from '../proposals/proposals/proposals-container';
import { Heading } from '../../components/heading'; import { Heading } from '../../components/heading';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import type { RouteChildProps } from '..'; import type { RouteChildProps } from '..';
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
import type { NodesFragmentFragment } from '../staking/home/__generated___/Nodes'; import type { NodesFragmentFragment } from '../staking/home/__generated__/Nodes';
const nodesToShow = 6; const nodesToShow = 6;

View File

@ -104,7 +104,7 @@ export const RewardsPage = () => {
</section> </section>
)} )}
<section className="grid xl:grid-cols-2 gap-12 items-center mb-8"> <section className="grid xl:grid-cols-[1fr_auto] gap-12 items-center mb-8">
<div> <div>
<SubHeading title={t('rewardsAndFeesReceived')} /> <SubHeading title={t('rewardsAndFeesReceived')} />
<p> <p>
@ -114,7 +114,7 @@ export const RewardsPage = () => {
</p> </p>
</div> </div>
<div className="max-w-[600px]"> <div className="w-[440px]">
<Toggle <Toggle
name="epoch-reward-view-toggle" name="epoch-reward-view-toggle"
toggles={[ toggles={[

View File

@ -23,6 +23,17 @@ fragment StakingNodeFields on Node {
} }
} }
fragment StakingDelegationFields on Delegation {
amount
epoch
node {
id
}
party {
id
}
}
query Staking($partyId: ID!, $delegationsPagination: Pagination) { query Staking($partyId: ID!, $delegationsPagination: Pagination) {
party(id: $partyId) { party(id: $partyId) {
id id
@ -32,11 +43,7 @@ query Staking($partyId: ID!, $delegationsPagination: Pagination) {
delegationsConnection(pagination: $delegationsPagination) { delegationsConnection(pagination: $delegationsPagination) {
edges { edges {
node { node {
amount ...StakingDelegationFields
epoch
node {
id
}
} }
} }
} }

View File

@ -5,13 +5,15 @@ 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, 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 StakingDelegationFieldsFragment = { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string }, party: { __typename?: 'Party', id: string } };
export type StakingQueryVariables = Types.Exact<{ export type StakingQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID']; partyId: Types.Scalars['ID'];
delegationsPagination?: Types.InputMaybe<Types.Pagination>; delegationsPagination?: Types.InputMaybe<Types.Pagination>;
}>; }>;
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?: any | null, end?: any | null, expiry?: any | 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 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 }, party: { __typename?: 'Party', id: string } } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | 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 {
@ -39,6 +41,18 @@ export const StakingNodeFieldsFragmentDoc = gql`
} }
} }
`; `;
export const StakingDelegationFieldsFragmentDoc = gql`
fragment StakingDelegationFields on Delegation {
amount
epoch
node {
id
}
party {
id
}
}
`;
export const StakingDocument = gql` export const StakingDocument = gql`
query Staking($partyId: ID!, $delegationsPagination: Pagination) { query Staking($partyId: ID!, $delegationsPagination: Pagination) {
party(id: $partyId) { party(id: $partyId) {
@ -49,11 +63,7 @@ export const StakingDocument = gql`
delegationsConnection(pagination: $delegationsPagination) { delegationsConnection(pagination: $delegationsPagination) {
edges { edges {
node { node {
amount ...StakingDelegationFields
epoch
node {
id
}
} }
} }
} }
@ -80,7 +90,8 @@ export const StakingDocument = gql`
uptime uptime
} }
} }
${StakingNodeFieldsFragmentDoc}`; ${StakingDelegationFieldsFragmentDoc}
${StakingNodeFieldsFragmentDoc}`;
/** /**
* __useStakingQuery__ * __useStakingQuery__

View File

@ -1,106 +0,0 @@
import * 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;
rankingScore: {
__typename?: 'RankingScore';
performanceScore: string;
};
};
} | null> | null;
} | null;
};
};
export const PreviousEpochDocument = gql`
query PreviousEpoch($epochId: ID) {
epoch(id: $epochId) {
id
validatorsConnection {
edges {
node {
id
rewardScore {
rawValidatorScore
}
rankingScore {
performanceScore
}
}
}
}
}
}
`;
/**
* __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
>;

View File

@ -1,114 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type LinkingsFieldsFragment = {
__typename?: 'StakeLinking';
id: string;
txHash: string;
status: Types.StakeLinkingStatus;
};
export type PartyStakeLinkingsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type PartyStakeLinkingsQuery = {
__typename?: 'Query';
party?: {
__typename?: 'Party';
id: string;
stakingSummary: {
__typename?: 'StakingSummary';
linkings: {
__typename?: 'StakesConnection';
edges?: Array<{
__typename?: 'StakeLinkingEdge';
node: {
__typename?: 'StakeLinking';
id: string;
txHash: string;
status: Types.StakeLinkingStatus;
};
} | null> | null;
};
};
} | null;
};
export const LinkingsFieldsFragmentDoc = gql`
fragment LinkingsFields on StakeLinking {
id
txHash
status
}
`;
export const PartyStakeLinkingsDocument = gql`
query PartyStakeLinkings($partyId: ID!) {
party(id: $partyId) {
id
stakingSummary {
linkings {
edges {
node {
...LinkingsFields
}
}
}
}
}
}
${LinkingsFieldsFragmentDoc}
`;
/**
* __usePartyStakeLinkingsQuery__
*
* To run a query within a React component, call `usePartyStakeLinkingsQuery` and pass it any options that fit your needs.
* When your component renders, `usePartyStakeLinkingsQuery` 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 } = usePartyStakeLinkingsQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function usePartyStakeLinkingsQuery(
baseOptions: Apollo.QueryHookOptions<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>(PartyStakeLinkingsDocument, options);
}
export function usePartyStakeLinkingsLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>(PartyStakeLinkingsDocument, options);
}
export type PartyStakeLinkingsQueryHookResult = ReturnType<
typeof usePartyStakeLinkingsQuery
>;
export type PartyStakeLinkingsLazyQueryHookResult = ReturnType<
typeof usePartyStakeLinkingsLazyQuery
>;
export type PartyStakeLinkingsQueryResult = Apollo.QueryResult<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>;

View File

@ -15,7 +15,7 @@ import {
} from '../../../hooks/transaction-reducer'; } from '../../../hooks/transaction-reducer';
import Routes from '../../routes'; import Routes from '../../routes';
import { truncateMiddle } from '../../../lib/truncate-middle'; import { truncateMiddle } from '../../../lib/truncate-middle';
import type { LinkingsFieldsFragment } from './__generated___/PartyStakeLinkings'; import type { LinkingsFieldsFragment } from './__generated__/PartyStakeLinkings';
export const AssociateTransaction = ({ export const AssociateTransaction = ({
amount, amount,

View File

@ -15,8 +15,8 @@ import type {
LinkingsFieldsFragment, LinkingsFieldsFragment,
PartyStakeLinkingsQuery, PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables, PartyStakeLinkingsQueryVariables,
} from './__generated___/PartyStakeLinkings'; } from './__generated__/PartyStakeLinkings';
import { PartyStakeLinkingsDocument } from './__generated___/PartyStakeLinkings'; import { PartyStakeLinkingsDocument } from './__generated__/PartyStakeLinkings';
export const useAddStake = ( export const useAddStake = (
address: string, address: string,

View File

@ -1,149 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type NodesFragmentFragment = {
__typename?: 'Node';
avatarUrl?: string | null;
id: string;
name: string;
pubkey: string;
stakedByOperator: string;
stakedByDelegates: string;
stakedTotal: string;
pendingStake: string;
rankingScore: {
__typename?: 'RankingScore';
rankingScore: string;
stakeScore: string;
performanceScore: string;
votingPower: string;
status: Types.ValidatorStatus;
};
};
export type NodesQueryVariables = Types.Exact<{ [key: string]: never }>;
export type NodesQuery = {
__typename?: 'Query';
epoch: {
__typename?: 'Epoch';
id: string;
timestamps: {
__typename?: 'EpochTimestamps';
start?: any | null;
end?: any | null;
expiry?: any | null;
};
};
nodesConnection: {
__typename?: 'NodesConnection';
edges?: Array<{
__typename?: 'NodeEdge';
node: {
__typename?: 'Node';
avatarUrl?: string | null;
id: string;
name: string;
pubkey: string;
stakedByOperator: string;
stakedByDelegates: string;
stakedTotal: string;
pendingStake: string;
rankingScore: {
__typename?: 'RankingScore';
rankingScore: string;
stakeScore: string;
performanceScore: string;
votingPower: string;
status: Types.ValidatorStatus;
};
};
} | null> | null;
};
nodeData?: { __typename?: 'NodeData'; stakedTotal: string } | null;
};
export const NodesFragmentFragmentDoc = gql`
fragment NodesFragment on Node {
avatarUrl
id
name
pubkey
stakedByOperator
stakedByDelegates
stakedTotal
pendingStake
rankingScore {
rankingScore
stakeScore
performanceScore
votingPower
status
}
}
`;
export const NodesDocument = gql`
query Nodes {
epoch {
id
timestamps {
start
end
expiry
}
}
nodesConnection {
edges {
node {
...NodesFragment
}
}
}
nodeData {
stakedTotal
}
}
${NodesFragmentFragmentDoc}
`;
/**
* __useNodesQuery__
*
* To run a query within a React component, call `useNodesQuery` and pass it any options that fit your needs.
* When your component renders, `useNodesQuery` 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 } = useNodesQuery({
* variables: {
* },
* });
*/
export function useNodesQuery(
baseOptions?: Apollo.QueryHookOptions<NodesQuery, NodesQueryVariables>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<NodesQuery, NodesQueryVariables>(
NodesDocument,
options
);
}
export function useNodesLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<NodesQuery, NodesQueryVariables>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<NodesQuery, NodesQueryVariables>(
NodesDocument,
options
);
}
export type NodesQueryHookResult = ReturnType<typeof useNodesQuery>;
export type NodesLazyQueryHookResult = ReturnType<typeof useNodesLazyQuery>;
export type NodesQueryResult = Apollo.QueryResult<
NodesQuery,
NodesQueryVariables
>;

View File

@ -1,36 +1,53 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { EpochCountdown } from '../../../components/epoch-countdown'; import { EpochCountdown } from '../../../components/epoch-countdown';
import { useNodesQuery } from './__generated___/Nodes'; import { useNodesQuery } from './__generated__/Nodes';
import { usePreviousEpochQuery } from '../__generated___/PreviousEpoch'; import { useStakingQuery } from '../__generated__/Staking';
import { usePreviousEpochQuery } from '../__generated__/PreviousEpoch';
import { ValidatorTables } from './validator-tables'; import { ValidatorTables } from './validator-tables';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch'; import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useVegaWallet } from '@vegaprotocol/wallet';
export const EpochData = () => { export const EpochData = () => {
// errorPolicy due to vegaprotocol/vega issue 5898 // errorPolicy due to vegaprotocol/vega issue 5898
const { data, error, loading, refetch } = useNodesQuery(); const { pubKey } = useVegaWallet();
const {
data: nodesData,
error: nodesError,
loading: nodesLoading,
refetch,
} = useNodesQuery();
const { data: userStakingData } = useStakingQuery({
variables: {
partyId: pubKey || '',
},
});
const { data: previousEpochData } = usePreviousEpochQuery({ const { data: previousEpochData } = usePreviousEpochQuery({
variables: { variables: {
epochId: (Number(data?.epoch.id) - 1).toString(), epochId: (Number(nodesData?.epoch.id) - 1).toString(),
}, },
skip: !data?.epoch.id, skip: !nodesData?.epoch.id,
}); });
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch); useRefreshAfterEpoch(nodesData?.epoch.timestamps.expiry, refetch);
return ( return (
<AsyncRenderer loading={loading} error={error} data={data}> <AsyncRenderer loading={nodesLoading} error={nodesError} data={nodesData}>
{data?.epoch && {nodesData?.epoch &&
data.epoch.timestamps.start && nodesData.epoch.timestamps.start &&
data?.epoch.timestamps.expiry && ( nodesData?.epoch.timestamps.expiry && (
<div className="mb-10"> <div className="mb-10">
<EpochCountdown <EpochCountdown
id={data.epoch.id} id={nodesData.epoch.id}
startDate={new Date(data.epoch.timestamps.start)} startDate={new Date(nodesData.epoch.timestamps.start)}
endDate={new Date(data.epoch.timestamps.expiry)} endDate={new Date(nodesData.epoch.timestamps.expiry)}
/> />
</div> </div>
)} )}
<ValidatorTables data={data} previousEpochData={previousEpochData} /> <ValidatorTables
nodesData={nodesData}
userStakingData={userStakingData}
previousEpochData={previousEpochData}
/>
</AsyncRenderer> </AsyncRenderer>
); );
}; };

View File

@ -3,14 +3,15 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
import { ConsensusValidatorsTable } from './consensus-validators-table'; import { ConsensusValidatorsTable } from './consensus-validators-table';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { NodesDocument } from '../__generated___/Nodes'; import { NodesDocument } from '../__generated__/Nodes';
import { PreviousEpochDocument } from '../../__generated___/PreviousEpoch'; import { PreviousEpochDocument } from '../../__generated__/PreviousEpoch';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
import type { NodesFragmentFragment } from '../__generated___/Nodes'; import type { NodesFragmentFragment } from '../__generated__/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch'; import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import type { ValidatorsView } from './validator-tables';
const nodeFactory = ( const nodeFactory = (
overrides?: PartialDeep<NodesFragmentFragment> overrides?: PartialDeep<NodesFragmentFragment>
@ -142,7 +143,8 @@ const MOCK_TOTAL_STAKE = '28832590188747439203824';
const renderValidatorsTable = ( const renderValidatorsTable = (
data = MOCK_NODES, data = MOCK_NODES,
previousEpochData = MOCK_PREVIOUS_EPOCH previousEpochData = MOCK_PREVIOUS_EPOCH,
validatorsView: ValidatorsView = 'all'
) => { ) => {
return render( return render(
<AppStateProvider initialState={{ decimals: 18 }}> <AppStateProvider initialState={{ decimals: 18 }}>
@ -152,6 +154,7 @@ const renderValidatorsTable = (
data={data} data={data}
previousEpochData={previousEpochData} previousEpochData={previousEpochData}
totalStake={MOCK_TOTAL_STAKE} totalStake={MOCK_TOTAL_STAKE}
validatorsView={validatorsView}
/> />
</MockedProvider> </MockedProvider>
</MemoryRouter> </MemoryRouter>

View File

@ -19,7 +19,9 @@ import {
import { import {
defaultColDef, defaultColDef,
NODE_LIST_GRID_STYLES, NODE_LIST_GRID_STYLES,
PendingStakeRenderer,
stakedTotalPercentage, stakedTotalPercentage,
StakeShareRenderer,
TotalPenaltiesRenderer, TotalPenaltiesRenderer,
TotalStakeRenderer, TotalStakeRenderer,
ValidatorFields, ValidatorFields,
@ -54,6 +56,9 @@ interface CanonisedConsensusNodeProps {
[ValidatorFields.OVERSTAKING_PENALTY]: string; [ValidatorFields.OVERSTAKING_PENALTY]: string;
[ValidatorFields.TOTAL_PENALTIES]: string; [ValidatorFields.TOTAL_PENALTIES]: string;
[ValidatorFields.PENDING_STAKE]: string; [ValidatorFields.PENDING_STAKE]: string;
[ValidatorFields.STAKED_BY_USER]: string | undefined;
[ValidatorFields.PENDING_USER_STAKE]: string | undefined;
[ValidatorFields.USER_STAKE_SHARE]: string | undefined;
} }
const getRowHeight = (params: RowHeightParams) => { const getRowHeight = (params: RowHeightParams) => {
@ -61,7 +66,7 @@ const getRowHeight = (params: RowHeightParams) => {
// Note: this value will change if the height of the top third cell renderer changes // Note: this value will change if the height of the top third cell renderer changes
return 138; return 138;
} }
return 52; return 68;
}; };
const TopThirdCellRenderer = ( const TopThirdCellRenderer = (
@ -117,6 +122,7 @@ export const ConsensusValidatorsTable = ({
data, data,
previousEpochData, previousEpochData,
totalStake, totalStake,
validatorsView,
}: ValidatorsTableProps) => { }: ValidatorsTableProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
@ -129,7 +135,7 @@ export const ConsensusValidatorsTable = ({
const nodes = useMemo(() => { const nodes = useMemo(() => {
if (!data || !previousEpochData) return []; if (!data || !previousEpochData) return [];
const canonisedNodes = data let canonisedNodes = data
.sort((a, b) => { .sort((a, b) => {
const aVotingPower = new BigNumber(a.rankingScore.votingPower); const aVotingPower = new BigNumber(a.rankingScore.votingPower);
const bVotingPower = new BigNumber(b.rankingScore.votingPower); const bVotingPower = new BigNumber(b.rankingScore.votingPower);
@ -154,6 +160,9 @@ export const ConsensusValidatorsTable = ({
rankingScore: { stakeScore, votingPower }, rankingScore: { stakeScore, votingPower },
pendingStake, pendingStake,
votingPowerRanking, votingPowerRanking,
stakedByUser,
pendingUserStake,
userStakeShare,
}) => { }) => {
const { rawValidatorScore, performanceScore } = const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, id); getLastEpochScoreAndPerformance(previousEpochData, id);
@ -201,12 +210,28 @@ export const ConsensusValidatorsTable = ({
totalStake totalStake
), ),
[ValidatorFields.PENDING_STAKE]: pendingStake, [ValidatorFields.PENDING_STAKE]: pendingStake,
decimals, [ValidatorFields.STAKED_BY_USER]: stakedByUser
? formatNumber(toBigNum(stakedByUser, decimals), 2)
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? stakedTotalPercentage(userStakeShare)
: undefined,
}; };
} }
); );
if (canonisedNodes.length < 3 || !hideTopThird) { if (validatorsView === 'myStake') {
canonisedNodes = canonisedNodes.filter(
(node) => node[ValidatorFields.STAKED_BY_USER] !== undefined
);
}
if (
canonisedNodes.length < 3 ||
!hideTopThird ||
validatorsView === 'myStake'
) {
return canonisedNodes; return canonisedNodes;
} }
@ -291,7 +316,14 @@ export const ConsensusValidatorsTable = ({
}, },
...remaining, ...remaining,
]; ];
}, [data, decimals, hideTopThird, previousEpochData, totalStake]); }, [
data,
decimals,
hideTopThird,
previousEpochData,
totalStake,
validatorsView,
]);
const ConsensusTable = forwardRef<AgGridReact>((_, gridRef) => { const ConsensusTable = forwardRef<AgGridReact>((_, gridRef) => {
const colDefs = useMemo<ColDef[]>( const colDefs = useMemo<ColDef[]>(
@ -311,7 +343,7 @@ export const ConsensusValidatorsTable = ({
return a > b ? 1 : -1; return a > b ? 1 : -1;
}, },
pinned: 'left', pinned: 'left',
width: 240, width: 260,
}, },
{ {
field: ValidatorFields.STAKE, field: ValidatorFields.STAKE,
@ -320,6 +352,20 @@ export const ConsensusValidatorsTable = ({
cellRenderer: TotalStakeRenderer, cellRenderer: TotalStakeRenderer,
width: 120, width: 120,
}, },
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
cellRenderer: PendingStakeRenderer,
width: 120,
},
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
cellRenderer: StakeShareRenderer,
width: 120,
},
{ {
field: ValidatorFields.NORMALISED_VOTING_POWER, field: ValidatorFields.NORMALISED_VOTING_POWER,
headerName: t(ValidatorFields.NORMALISED_VOTING_POWER).toString(), headerName: t(ValidatorFields.NORMALISED_VOTING_POWER).toString(),
@ -328,12 +374,6 @@ export const ConsensusValidatorsTable = ({
width: 200, width: 200,
sort: 'desc', sort: 'desc',
}, },
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
width: 100,
},
{ {
field: ValidatorFields.TOTAL_PENALTIES, field: ValidatorFields.TOTAL_PENALTIES,
headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(), headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(),
@ -341,14 +381,6 @@ export const ConsensusValidatorsTable = ({
cellRenderer: TotalPenaltiesRenderer, cellRenderer: TotalPenaltiesRenderer,
width: 120, width: 120,
}, },
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
valueFormatter: ({ value }) =>
formatNumber(toBigNum(value, decimals), 2),
width: 110,
},
], ],
[] []
); );

View File

@ -1,70 +0,0 @@
import {
getLastEpochScoreAndPerformance,
getTotalPenalties,
} from '../../shared';
import { stakedTotalPercentage } from './shared';
const MOCK_PREVIOUS_EPOCH = {
epoch: {
id: '1',
validatorsConnection: {
edges: [
{
node: {
id: '0x123',
rewardScore: {
rawValidatorScore: '0.25',
},
rankingScore: {
performanceScore: '0.75',
},
},
},
],
},
},
};
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(
getTotalPenalties(
getLastEpochScoreAndPerformance(MOCK_PREVIOUS_EPOCH, '0x123')
.rawValidatorScore,
'0.1',
'5000',
'100000'
)
).toBe('50.00%');
});
it('should return the correct penalty based on lower performance score than first test', () => {
expect(
getTotalPenalties(
getLastEpochScoreAndPerformance(MOCK_PREVIOUS_EPOCH, '0x123')
.rawValidatorScore,
'0.05',
'5000',
'100000'
)
).toBe('75.00%');
});
it('should return the correct penalty based on higher amount of stake than other tests (great penalty due to anti-whaling)', () => {
expect(
getTotalPenalties(
getLastEpochScoreAndPerformance(MOCK_PREVIOUS_EPOCH, '0x123')
.rawValidatorScore,
'0.1',
'5000',
'5500'
)
).toBe('97.25%');
});
});

View File

@ -10,9 +10,12 @@ import {
Tooltip, Tooltip,
TooltipCellComponent, TooltipCellComponent,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { NodesFragmentFragment } from '../__generated___/Nodes'; import { BigNumber } from '../../../../lib/bignumber';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch'; import type { NodesFragmentFragment } from '../__generated__/Nodes';
import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import { useAppState } from '../../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../../contexts/app-state/app-state-context';
import type { StakingDelegationFieldsFragment } from '../../__generated__/Staking';
import type { ValidatorsView } from './validator-tables';
export enum ValidatorFields { export enum ValidatorFields {
RANKING_INDEX = 'rankingIndex', RANKING_INDEX = 'rankingIndex',
@ -31,12 +34,49 @@ export enum ValidatorFields {
PERFORMANCE_PENALTY = 'performancePenalty', PERFORMANCE_PENALTY = 'performancePenalty',
OVERSTAKED_AMOUNT = 'overstakedAmount', OVERSTAKED_AMOUNT = 'overstakedAmount',
OVERSTAKING_PENALTY = 'overstakingPenalty', OVERSTAKING_PENALTY = 'overstakingPenalty',
// the following are additional fields added to the validator object displaying user data
STAKED_BY_USER = 'stakedByUser',
PENDING_USER_STAKE = 'pendingUserStake',
USER_STAKE_SHARE = 'userStakeShare',
} }
export const addUserDataToValidator = (
validator: NodesFragmentFragment,
currentEpochUserStaking: StakingDelegationFieldsFragment | undefined,
nextEpochUserStaking: StakingDelegationFieldsFragment | undefined,
currentUserStakeAvailable: string
) => {
return {
...validator,
[ValidatorFields.STAKED_BY_USER]:
currentEpochUserStaking && Number(currentEpochUserStaking?.amount) > 0
? currentEpochUserStaking.amount
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: nextEpochUserStaking
? new BigNumber(nextEpochUserStaking?.amount)
.minus(new BigNumber(currentEpochUserStaking?.amount || 0))
.toString()
: undefined,
[ValidatorFields.USER_STAKE_SHARE]:
currentEpochUserStaking && Number(currentEpochUserStaking.amount) > 0
? new BigNumber(currentEpochUserStaking.amount).dividedBy(
new BigNumber(currentUserStakeAvailable)
)
: undefined,
};
};
export type ValidatorWithUserData = NodesFragmentFragment & {
stakedByUser?: string;
pendingUserStake?: string;
userStakeShare?: string;
};
export interface ValidatorsTableProps { export interface ValidatorsTableProps {
data: NodesFragmentFragment[] | undefined; data: ValidatorWithUserData[] | undefined;
previousEpochData: PreviousEpochQuery | undefined; previousEpochData: PreviousEpochQuery | undefined;
totalStake: string; totalStake: string;
validatorsView: ValidatorsView;
} }
// Custom styling to account for the scrollbar. This is needed because the // Custom styling to account for the scrollbar. This is needed because the
@ -58,19 +98,23 @@ export const defaultColDef = {
resizable: true, resizable: true,
autoHeight: true, autoHeight: true,
comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b), comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b),
cellStyle: { margin: '10px 0', padding: '0 12px' },
tooltipComponent: TooltipCellComponent, tooltipComponent: TooltipCellComponent,
cellStyle: { display: 'flex', alignItems: 'center', padding: '0 10px' },
}; };
interface ValidatorRendererProps { interface ValidatorRendererProps {
data: { id: string; validator: { avatarUrl: string; name: string } }; data: {
id: string;
validator: { avatarUrl: string; name: string };
stakedByUser: string | undefined;
};
} }
export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => { export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { avatarUrl, name } = data.validator; const { avatarUrl, name } = data.validator;
return ( return (
<div className="grid grid-cols-[1fr_auto] gap-2 items-center"> <div className="w-[238px] grid grid-cols-[1fr_auto] gap-2 items-center">
<span className="flex overflow-hidden"> <span className="flex overflow-hidden">
{avatarUrl && ( {avatarUrl && (
<img <img
@ -83,9 +127,19 @@ export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
<span>{name}</span> <span>{name}</span>
</span> </span>
<Link to={data.id}> <Link to={data.id}>
<Button size="sm" fill={true}> {data.stakedByUser ? (
{t('Stake')} <Button
</Button> data-testid="my-stake-btn"
size="sm"
className="text-vega-green border-vega-green"
>
{t('myStake')}
</Button>
) : (
<Button data-testid="stake-btn" size="sm" fill={true}>
{t('Stake')}
</Button>
)}
</Link> </Link>
</div> </div>
); );
@ -102,8 +156,14 @@ export const StakeNeededForPromotionRenderer = ({
data, data,
}: StakeNeededForPromotionRendererProps) => { }: StakeNeededForPromotionRendererProps) => {
return ( return (
<Tooltip description={data.stakeNeededForPromotionDescription}> <Tooltip
<span> description={
<span data-testid="stake-needed-for-promotion-tooltip">
{data.stakeNeededForPromotionDescription}
</span>
}
>
<span data-testid="stake-needed-for-promotion">
{data.stakeNeededForPromotion && {data.stakeNeededForPromotion &&
formatNumber(data.stakeNeededForPromotion, 2)} formatNumber(data.stakeNeededForPromotion, 2)}
</span> </span>
@ -134,7 +194,59 @@ export const VotingPowerRenderer = ({ data }: VotingPowerRendererProps) => {
</> </>
} }
> >
<span>{data.normalisedVotingPower}</span> <span data-testid="normalised-voting-power">
{data.normalisedVotingPower}
</span>
</Tooltip>
);
};
interface PendingStakeRendererProps {
data: {
pendingStake: string;
pendingUserStake: string | undefined;
};
}
export const PendingStakeRenderer = ({ data }: PendingStakeRendererProps) => {
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
return (
<Tooltip
description={
<>
<div data-testid="pending-stake-tooltip">
{t('pendingStake')}:{' '}
{formatNumber(toBigNum(data.pendingStake, decimals), decimals)}
</div>
{data.pendingUserStake && (
<div
className="text-vega-green border-t border-t-vega-dark-200 mt-1.5 pt-1"
data-testid="pending-user-stake-tooltip"
>
{t('myPendingStake')}:{' '}
{formatNumber(
toBigNum(data.pendingUserStake, decimals),
decimals
)}
</div>
)}
</>
}
>
<div className="flex flex-col">
{data.pendingUserStake && data.pendingStake !== '0' && (
<span data-testid="pending-user-stake" className="text-vega-green">
{formatNumber(toBigNum(data.pendingUserStake, decimals), 2)}
</span>
)}
<span data-testid="total-pending-stake">
{formatNumber(toBigNum(data.pendingStake, decimals), 2)}
</span>
</div>
</Tooltip> </Tooltip>
); );
}; };
@ -144,6 +256,7 @@ interface TotalStakeRendererProps {
stake: string; stake: string;
stakedByDelegates: string; stakedByDelegates: string;
stakedByOperator: string; stakedByOperator: string;
stakedByUser: string | undefined;
}; };
} }
@ -165,18 +278,52 @@ export const TotalStakeRenderer = ({ data }: TotalStakeRendererProps) => {
<div data-testid="staked-delegates-tooltip"> <div data-testid="staked-delegates-tooltip">
{t('stakedByDelegates')}: {data.stakedByDelegates.toString()} {t('stakedByDelegates')}: {data.stakedByDelegates.toString()}
</div> </div>
<div data-testid="total-staked-tooltip"> <div className="font-bold" data-testid="total-staked-tooltip">
{t('totalStake')}:{' '} {t('totalStake')}: {formattedStake}
<span className="font-bold">{formattedStake}</span>
</div> </div>
{data.stakedByUser && (
<div
className="text-vega-green border-t border-t-vega-dark-200 mt-1.5 pt-1"
data-testid="staked-by-user-tooltip"
>
{t('stakedByMe')}: {data.stakedByUser}
</div>
)}
</> </>
} }
> >
<span>{formattedStake}</span> <div className="flex flex-col">
{data.stakedByUser && (
<span data-testid="user-stake" className="text-vega-green">
{data.stakedByUser}
</span>
)}
<span data-testid="total-stake">{formattedStake}</span>
</div>
</Tooltip> </Tooltip>
); );
}; };
interface StakeShareRendererProps {
data: {
stakeShare: string;
userStakeShare: string | undefined;
};
}
export const StakeShareRenderer = ({ data }: StakeShareRendererProps) => {
return (
<div className="flex flex-col">
{data.userStakeShare && (
<span data-testid="user-stake-share" className="text-vega-green">
{data.userStakeShare}
</span>
)}
<span data-testid="total-stake-share">{data.stakeShare}</span>
</div>
);
};
interface TotalPenaltiesRendererProps { interface TotalPenaltiesRendererProps {
data: { data: {
performanceScore: string; performanceScore: string;
@ -209,7 +356,7 @@ export const TotalPenaltiesRenderer = ({
</> </>
} }
> >
<span>{data.totalPenalties}</span> <span data-testid="total-penalty">{data.totalPenalties}</span>
</Tooltip> </Tooltip>
); );
}; };

View File

@ -21,6 +21,8 @@ import {
ValidatorRenderer, ValidatorRenderer,
TotalPenaltiesRenderer, TotalPenaltiesRenderer,
TotalStakeRenderer, TotalStakeRenderer,
StakeShareRenderer,
PendingStakeRenderer,
} from './shared'; } from './shared';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community'; import type { ColDef } from 'ag-grid-community';
@ -38,6 +40,7 @@ export const StandbyPendingValidatorsTable = ({
totalStake, totalStake,
stakeNeededForPromotion, stakeNeededForPromotion,
stakeNeededForPromotionDescription, stakeNeededForPromotionDescription,
validatorsView,
}: StandbyPendingValidatorsTableProps) => { }: StandbyPendingValidatorsTableProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
@ -47,7 +50,7 @@ export const StandbyPendingValidatorsTable = ({
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const nodes = useMemo(() => { let nodes = useMemo(() => {
if (!data) return []; if (!data) return [];
return data return data
@ -75,6 +78,9 @@ export const StandbyPendingValidatorsTable = ({
rankingScore: { stakeScore }, rankingScore: { stakeScore },
pendingStake, pendingStake,
votingPowerRanking, votingPowerRanking,
stakedByUser,
pendingUserStake,
userStakeShare,
}) => { }) => {
const { rawValidatorScore, performanceScore } = const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, id); getLastEpochScoreAndPerformance(previousEpochData, id);
@ -152,6 +158,13 @@ export const StandbyPendingValidatorsTable = ({
totalStake totalStake
), ),
[ValidatorFields.PENDING_STAKE]: pendingStake, [ValidatorFields.PENDING_STAKE]: pendingStake,
[ValidatorFields.STAKED_BY_USER]: stakedByUser
? formatNumber(toBigNum(stakedByUser, decimals), 2)
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? stakedTotalPercentage(userStakeShare)
: undefined,
}; };
} }
); );
@ -165,6 +178,12 @@ export const StandbyPendingValidatorsTable = ({
totalStake, totalStake,
]); ]);
if (validatorsView === 'myStake') {
nodes = nodes.filter(
(node) => node[ValidatorFields.STAKED_BY_USER] !== undefined
);
}
const StandbyPendingTable = forwardRef<AgGridReact>((_, gridRef) => { const StandbyPendingTable = forwardRef<AgGridReact>((_, gridRef) => {
const colDefs = useMemo<ColDef[]>( const colDefs = useMemo<ColDef[]>(
() => [ () => [
@ -180,7 +199,7 @@ export const StandbyPendingValidatorsTable = ({
cellRenderer: ValidatorRenderer, cellRenderer: ValidatorRenderer,
comparator: ({ name: a }, { name: b }) => Math.sign(a - b), comparator: ({ name: a }, { name: b }) => Math.sign(a - b),
pinned: 'left', pinned: 'left',
width: 240, width: 260,
}, },
{ {
field: ValidatorFields.STAKE, field: ValidatorFields.STAKE,
@ -189,6 +208,20 @@ export const StandbyPendingValidatorsTable = ({
cellRenderer: TotalStakeRenderer, cellRenderer: TotalStakeRenderer,
width: 120, width: 120,
}, },
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
cellRenderer: PendingStakeRenderer,
width: 120,
},
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
cellRenderer: StakeShareRenderer,
width: 100,
},
{ {
field: ValidatorFields.STAKE_NEEDED_FOR_PROMOTION, field: ValidatorFields.STAKE_NEEDED_FOR_PROMOTION,
headerName: t(ValidatorFields.STAKE_NEEDED_FOR_PROMOTION).toString(), headerName: t(ValidatorFields.STAKE_NEEDED_FOR_PROMOTION).toString(),
@ -199,12 +232,6 @@ export const StandbyPendingValidatorsTable = ({
width: 210, width: 210,
sort: 'asc', sort: 'asc',
}, },
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
width: 100,
},
{ {
field: ValidatorFields.TOTAL_PENALTIES, field: ValidatorFields.TOTAL_PENALTIES,
headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(), headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(),
@ -212,14 +239,6 @@ export const StandbyPendingValidatorsTable = ({
cellRenderer: TotalPenaltiesRenderer, cellRenderer: TotalPenaltiesRenderer,
width: 120, width: 120,
}, },
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
valueFormatter: ({ value }) =>
formatNumber(toBigNum(value, decimals), 2),
width: 110,
},
], ],
[] []
); );
@ -230,7 +249,7 @@ export const StandbyPendingValidatorsTable = ({
domLayout="autoHeight" domLayout="autoHeight"
style={{ width: '100%' }} style={{ width: '100%' }}
customThemeParams={NODE_LIST_GRID_STYLES} customThemeParams={NODE_LIST_GRID_STYLES}
rowHeight={52} rowHeight={68}
defaultColDef={defaultColDef} defaultColDef={defaultColDef}
tooltipShowDelay={0} tooltipShowDelay={0}
animateRows={true} animateRows={true}

View File

@ -1,27 +1,30 @@
import { useMemo } from 'react'; import { useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { ConsensusValidatorsTable } from './consensus-validators-table'; import BigNumber from 'bignumber.js';
import { StandbyPendingValidatorsTable } from './standby-pending-validators-table';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { formatNumber } from '../../../../lib/format-number';
import { import {
createDocsLinks, createDocsLinks,
removePaginationWrapper, removePaginationWrapper,
toBigNum, toBigNum,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { Link as UTLink } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { Link as UTLink, Toggle } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '../../../../lib/format-number';
import { SubHeading } from '../../../../components/heading';
import { useAppState } from '../../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../../contexts/app-state/app-state-context';
import type { import { addUserDataToValidator } from './shared';
NodesQuery, import { ConsensusValidatorsTable } from './consensus-validators-table';
NodesFragmentFragment, import { StandbyPendingValidatorsTable } from './standby-pending-validators-table';
} from '../__generated___/Nodes'; import type { NodesQuery, NodesFragmentFragment } from '../__generated__/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch'; import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import BigNumber from 'bignumber.js'; import type { StakingQuery } from '../../__generated__/Staking';
import type { StakingDelegationFieldsFragment } from '../../__generated__/Staking';
import type { ValidatorWithUserData } from './shared';
export interface ValidatorsTableProps { export interface ValidatorsTableProps {
data: NodesQuery | undefined; nodesData: NodesQuery | undefined;
userStakingData: StakingQuery | undefined;
previousEpochData: PreviousEpochQuery | undefined; previousEpochData: PreviousEpochQuery | undefined;
} }
@ -31,8 +34,11 @@ interface SortedValidatorsProps {
pendingValidators: NodesFragmentFragment[]; pendingValidators: NodesFragmentFragment[];
} }
export type ValidatorsView = 'all' | 'myStake';
export const ValidatorTables = ({ export const ValidatorTables = ({
data, nodesData,
userStakingData,
previousEpochData, previousEpochData,
}: ValidatorsTableProps) => { }: ValidatorsTableProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -40,25 +46,69 @@ export const ValidatorTables = ({
const { const {
appState: { decimals }, appState: { decimals },
} = useAppState(); } = useAppState();
const [validatorsView, setValidatorsView] = useState<ValidatorsView>('all');
const totalStake = useMemo( const totalStake = useMemo(
() => data?.nodeData?.stakedTotal || '0', () => nodesData?.nodeData?.stakedTotal || '0',
[data?.nodeData?.stakedTotal] [nodesData?.nodeData?.stakedTotal]
); );
const epochId = useMemo(() => nodesData?.epoch.id, [nodesData?.epoch.id]);
const currentUserStakeAvailable = useMemo(
() => userStakingData?.party?.stakingSummary.currentStakeAvailable || '0',
[userStakingData?.party?.stakingSummary.currentStakeAvailable]
);
let stakeNeededForPromotion = undefined; let stakeNeededForPromotion = undefined;
let delegations: StakingDelegationFieldsFragment[] | undefined = undefined;
if (userStakingData) {
delegations = removePaginationWrapper(
userStakingData?.party?.delegationsConnection?.edges
);
}
const { consensusValidators, standbyValidators, pendingValidators } = useMemo( const { consensusValidators, standbyValidators, pendingValidators } = useMemo(
() => () =>
removePaginationWrapper(data?.nodesConnection.edges).reduce( removePaginationWrapper(nodesData?.nodesConnection.edges).reduce(
(acc: SortedValidatorsProps, validator) => { (acc: SortedValidatorsProps, validator) => {
const validatorId = validator.id;
const currentDelegation = delegations?.find(
(d) => d.node.id === validatorId && d.epoch === Number(epochId)
);
const nextDelegation = delegations?.find(
(d) => d.node.id === validatorId && d.epoch === Number(epochId) + 1
);
switch (validator.rankingScore?.status) { switch (validator.rankingScore?.status) {
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT: case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT:
acc.consensusValidators.push(validator); acc.consensusValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break; break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ: case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ:
acc.standbyValidators.push(validator); acc.standbyValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break; break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING: case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING:
acc.pendingValidators.push(validator); acc.pendingValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
} }
return acc; return acc;
}, },
@ -68,7 +118,12 @@ export const ValidatorTables = ({
pendingValidators: [], pendingValidators: [],
} }
), ),
[data?.nodesConnection.edges] [
currentUserStakeAvailable,
delegations,
epochId,
nodesData?.nodesConnection.edges,
]
); );
if ( if (
@ -76,7 +131,7 @@ export const ValidatorTables = ({
(standbyValidators.length || pendingValidators.length) (standbyValidators.length || pendingValidators.length)
) { ) {
const lowestRankingConsensusScore = consensusValidators.reduce( const lowestRankingConsensusScore = consensusValidators.reduce(
(lowest: NodesFragmentFragment, validator: NodesFragmentFragment) => { (lowest: ValidatorWithUserData, validator: ValidatorWithUserData) => {
if ( if (
Number(validator.rankingScore.rankingScore) < Number(validator.rankingScore.rankingScore) <
Number(lowest.rankingScore.rankingScore) Number(lowest.rankingScore.rankingScore)
@ -98,19 +153,42 @@ export const ValidatorTables = ({
).toString(); ).toString();
} }
return ( return (
<div data-testid="validator-tables"> <section data-testid="validator-tables">
<div className="grid w-full justify-end">
<div className="w-[400px]">
<Toggle
name="validators-view-toggle"
toggles={[
{
label: t('ALL VALIDATORS'),
value: 'all',
},
{
label: t('STAKED BY ME'),
value: 'myStake',
},
]}
checkedValue={validatorsView}
onChange={(e) =>
setValidatorsView(e.target.value as ValidatorsView)
}
/>
</div>
</div>
{consensusValidators.length > 0 && ( {consensusValidators.length > 0 && (
<> <div className="mb-10">
<SubHeading title={t('status-tendermint')} /> <SubHeading title={t('status-tendermint')} />
<ConsensusValidatorsTable <ConsensusValidatorsTable
data={consensusValidators} data={consensusValidators}
previousEpochData={previousEpochData} previousEpochData={previousEpochData}
totalStake={totalStake} totalStake={totalStake}
validatorsView={validatorsView}
/> />
</> </div>
)} )}
{standbyValidators.length > 0 && ( {standbyValidators.length > 0 && (
<> <div className="mb-10">
<SubHeading title={t('status-ersatz')} /> <SubHeading title={t('status-ersatz')} />
<p> <p>
<Trans <Trans
@ -126,8 +204,9 @@ export const ValidatorTables = ({
totalStake={totalStake} totalStake={totalStake}
stakeNeededForPromotion={stakeNeededForPromotion} stakeNeededForPromotion={stakeNeededForPromotion}
stakeNeededForPromotionDescription="StakeNeededForPromotionStandbyDescription" stakeNeededForPromotionDescription="StakeNeededForPromotionStandbyDescription"
validatorsView={validatorsView}
/> />
</> </div>
)} )}
{pendingValidators.length > 0 && ( {pendingValidators.length > 0 && (
<> <>
@ -155,9 +234,10 @@ export const ValidatorTables = ({
totalStake={totalStake} totalStake={totalStake}
stakeNeededForPromotion={stakeNeededForPromotion} stakeNeededForPromotion={stakeNeededForPromotion}
stakeNeededForPromotionDescription="StakeNeededForPromotionCandidateDescription" stakeNeededForPromotionDescription="StakeNeededForPromotionCandidateDescription"
validatorsView={validatorsView}
/> />
</> </>
)} )}
</div> </section>
); );
}; };

View File

@ -20,8 +20,8 @@ import NodeContainer from './nodes-container';
import { useAppState } from '../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../contexts/app-state/app-state-context';
import { Heading, SubHeading } from '../../../components/heading'; import { Heading, SubHeading } from '../../../components/heading';
import Routes from '../../routes'; import Routes from '../../routes';
import type { StakingQuery } from './__generated__/Staking'; import type { StakingQuery } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch'; import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
interface StakingNodeProps { interface StakingNodeProps {
data?: StakingQuery; data?: StakingQuery;
@ -116,13 +116,6 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
t('validatorTitle', { nodeName: t('validatorTitleFallback') }) t('validatorTitle', { nodeName: t('validatorTitleFallback') })
} }
/> />
<section className="mb-4">
<ValidatorTable
node={nodeInfo}
stakedTotal={addDecimal(data?.nodeData?.stakedTotal || '0', decimals)}
previousEpochData={previousEpochData}
/>
</section>
{data?.epoch.timestamps.start && data?.epoch.timestamps.expiry && ( {data?.epoch.timestamps.start && data?.epoch.timestamps.expiry && (
<section className="mb-10"> <section className="mb-10">
<EpochCountdown <EpochCountdown
@ -157,6 +150,13 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
<ConnectToVega /> <ConnectToVega />
</> </>
)} )}
<section className="mb-4">
<ValidatorTable
node={nodeInfo}
stakedTotal={addDecimal(data?.nodeData?.stakedTotal || '0', decimals)}
previousEpochData={previousEpochData}
/>
</section>
</div> </div>
); );
}; };

View File

@ -4,11 +4,11 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch'; import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { SplashLoader } from '../../../components/splash-loader'; import { SplashLoader } from '../../../components/splash-loader';
import { useStakingQuery } from './__generated__/Staking'; import { useStakingQuery } from '../__generated__/Staking';
import { usePreviousEpochQuery } from '../__generated___/PreviousEpoch'; import { usePreviousEpochQuery } from '../__generated__/PreviousEpoch';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import type { StakingQuery } from './__generated__/Staking'; import type { StakingQuery } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch'; import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
// TODO should only request a single node. When migrating from deprecated APIs we should address this. // TODO should only request a single node. When migrating from deprecated APIs we should address this.

View File

@ -28,8 +28,8 @@ import {
getStakePercentage, getStakePercentage,
} from '../shared'; } from '../shared';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { StakingNodeFieldsFragment } from './__generated__/Staking'; import type { StakingNodeFieldsFragment } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch'; import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
const statuses = { const statuses = {
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ]: 'status-ersatz', [Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ]: 'status-ersatz',
@ -111,7 +111,7 @@ export const ValidatorTable = ({
<div className="my-12" data-testid="validator-table"> <div className="my-12" data-testid="validator-table">
<SubHeading title={t('profile')} /> <SubHeading title={t('profile')} />
<RoundedWrapper> <RoundedWrapper paddingBottom={true}>
<KeyValueTable data-testid="validator-table-profile"> <KeyValueTable data-testid="validator-table-profile">
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('id')}</span> <span>{t('id')}</span>
@ -156,7 +156,7 @@ export const ValidatorTable = ({
</div> </div>
<SubHeading title={t('ADDRESS')} /> <SubHeading title={t('ADDRESS')} />
<RoundedWrapper marginBottomLarge={true}> <RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-address"> <KeyValueTable data-testid="validator-table-address">
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span> <span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
@ -187,7 +187,7 @@ export const ValidatorTable = ({
</RoundedWrapper> </RoundedWrapper>
<SubHeading title={t('STAKE')} /> <SubHeading title={t('STAKE')} />
<RoundedWrapper marginBottomLarge={true}> <RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-stake"> <KeyValueTable data-testid="validator-table-stake">
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('STAKED BY OPERATOR')}</span> <span>{t('STAKED BY OPERATOR')}</span>
@ -238,7 +238,7 @@ export const ValidatorTable = ({
</RoundedWrapper> </RoundedWrapper>
<SubHeading title={t('PENALTIES')} /> <SubHeading title={t('PENALTIES')} />
<RoundedWrapper marginBottomLarge={true}> <RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-penalties"> <KeyValueTable data-testid="validator-table-penalties">
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span> <span>{t('OVERSTAKED PENALTY')}</span>
@ -270,7 +270,7 @@ export const ValidatorTable = ({
</RoundedWrapper> </RoundedWrapper>
<SubHeading title={t('VOTING POWER')} /> <SubHeading title={t('VOTING POWER')} />
<RoundedWrapper marginBottomLarge={true}> <RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-voting-power"> <KeyValueTable data-testid="validator-table-voting-power">
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span> <span>{t('UNNORMALISED VOTING POWER')}</span>

View File

@ -23,7 +23,7 @@ export const YourStake = ({
return ( return (
<div data-testid="your-stake"> <div data-testid="your-stake">
<SubHeading title={t('Your stake')} /> <SubHeading title={t('Your stake')} />
<RoundedWrapper> <RoundedWrapper paddingBottom={true}>
<KeyValueTable> <KeyValueTable>
<KeyValueTableRow> <KeyValueTableRow>
{t('Your Stake On Node (This Epoch)')} {t('Your Stake On Node (This Epoch)')}

View File

@ -2,7 +2,7 @@ import {
formatNumberPercentage, formatNumberPercentage,
removePaginationWrapper, removePaginationWrapper,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import type { PreviousEpochQuery } from './__generated___/PreviousEpoch'; import type { PreviousEpochQuery } from './__generated__/PreviousEpoch';
import { BigNumber } from '../../lib/bignumber'; import { BigNumber } from '../../lib/bignumber';
export const getLastEpochScoreAndPerformance = ( export const getLastEpochScoreAndPerformance = (

View File

@ -1 +1 @@
GRAPHQL_SCHEMA_PATH=https://api.n06.testnet.vega.xyz/graphql GRAPHQL_SCHEMA_PATH=https://api.n07.testnet.vega.xyz/graphql

View File

@ -88,6 +88,7 @@ export const DepositStatusMapping: {
export const IntervalMapping: { export const IntervalMapping: {
[T in Interval]: string; [T in Interval]: string;
} = { } = {
// @ts-ignore - temporarily suppressing this as it's a valid value
INTERVAL_BLOCK: '1 block', INTERVAL_BLOCK: '1 block',
INTERVAL_I15M: 'I15M', INTERVAL_I15M: 'I15M',
INTERVAL_I1D: 'I1D', INTERVAL_I1D: 'I1D',