Feat/score changes (#696)

* chore: add query and generate types for shwoing scores

* fix: add key prop

* chore: add tests for new functionality

* style: remove debug

* Update libs/types/apollo.config.js
This commit is contained in:
Dexter Edwards 2022-07-07 13:37:41 +01:00 committed by GitHub
parent 80e6b3b8fc
commit c7e65080b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 301 additions and 13 deletions

View File

@ -545,5 +545,9 @@
"proposalTerms": "Proposal terms", "proposalTerms": "Proposal terms",
"currentlySetTo": "Currently set to ", "currentlySetTo": "Currently set to ",
"pass": "Pass", "pass": "Pass",
"fail": "Fail" "fail": "Fail",
"rankingScore": "Ranking score",
"stakeScore": "Stake score",
"performanceScore": "Performance score",
"votingPower": "Voting score"
} }

View File

@ -25,6 +25,26 @@ export interface Nodes_nodes_epochData {
online: number; online: number;
} }
export interface Nodes_nodes_rankingScore {
__typename: "RankingScore";
/**
* The ranking score of the validator
*/
rankingScore: string;
/**
* The stake based score of the validator (no anti-whaling)
*/
stakeScore: string;
/**
* The performance score of the validator
*/
performanceScore: string;
/**
* The tendermint voting power of the validator (uint32)
*/
votingPower: string;
}
export interface Nodes_nodes { export interface Nodes_nodes {
__typename: "Node"; __typename: "Node";
/** /**
@ -66,6 +86,10 @@ export interface Nodes_nodes {
pendingStakeFormatted: string; pendingStakeFormatted: string;
epochData: Nodes_nodes_epochData | null; epochData: Nodes_nodes_epochData | null;
status: NodeStatus; status: NodeStatus;
/**
* Ranking scores and status for the validator for the current epoch
*/
rankingScore: Nodes_nodes_rankingScore;
} }
export interface Nodes_nodeData { export interface Nodes_nodeData {

View File

@ -100,6 +100,26 @@ export interface Staking_nodes_epochData {
online: number; online: number;
} }
export interface Staking_nodes_rankingScore {
__typename: "RankingScore";
/**
* The ranking score of the validator
*/
rankingScore: string;
/**
* The stake based score of the validator (no anti-whaling)
*/
stakeScore: string;
/**
* The performance score of the validator
*/
performanceScore: string;
/**
* The tendermint voting power of the validator (uint32)
*/
votingPower: string;
}
export interface Staking_nodes { export interface Staking_nodes {
__typename: "Node"; __typename: "Node";
/** /**
@ -145,6 +165,10 @@ export interface Staking_nodes {
pendingStakeFormatted: string; pendingStakeFormatted: string;
epochData: Staking_nodes_epochData | null; epochData: Staking_nodes_epochData | null;
status: NodeStatus; status: NodeStatus;
/**
* Ranking scores and status for the validator for the current epoch
*/
rankingScore: Staking_nodes_rankingScore;
} }
export interface Staking_nodeData { export interface Staking_nodeData {

View File

@ -0,0 +1,204 @@
import { render, screen, waitFor, within } from '@testing-library/react';
import { NodeList, NODES_QUERY } from './node-list';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { addDecimal } from '@vegaprotocol/react-helpers';
import type { Nodes_nodes } from './__generated__/Nodes';
jest.mock('../../components/epoch-countdown', () => ({
EpochCountdown: () => <div data-testid="epoch-info"></div>,
}));
const nodeFactory = (overrides?: Partial<Nodes_nodes>) => ({
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
name: 'Skynet',
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
infoUrl: 'https://en.wikipedia.org/wiki/Skynet_(Terminator)',
location: '',
stakedByOperator: '3000000000000000000000',
stakedByDelegates: '11182454495731682635157',
stakedTotal: '14182454495731682635157',
pendingStake: '0',
stakedByOperatorFormatted: addDecimal(
overrides?.stakedByOperator || '3000000000000000000000',
18
),
stakedByDelegatesFormatted: addDecimal(
overrides?.stakedByDelegates || '11182454495731682635157',
18
),
stakedTotalFormatted: addDecimal(
overrides?.stakedTotal || '14182454495731682635157',
18
),
pendingStakeFormatted: addDecimal(overrides?.pendingStake || '0', 18),
epochData: null,
status: 'Validator',
rankingScore: {
rankingScore: '0.67845061012234727427532760837568',
stakeScore: '0.3392701644525644',
performanceScore: '0.9998677767864936',
votingPower: '2407',
__typename: 'RankingScore',
},
__typename: 'Node',
...overrides,
});
const MOCK_NODES = {
nodes: [
nodeFactory(),
nodeFactory({
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
name: 'T-800 Terminator',
pubkey:
'ccc3b8362c25b09d20df8ea407b0a476d6b24a0e72bc063d0033c8841652ddd4',
infoUrl: 'https://en.wikipedia.org/wiki/Terminator_(character)',
stakedByOperator: '3000000000000000000000',
stakedByDelegates: '6618711883996159534058',
stakedTotal: '9618711883996159534058',
rankingScore: {
rankingScore: '0.4601942440481428',
stakeScore: '0.2300971220240714',
performanceScore: '1',
votingPower: '2408',
__typename: 'RankingScore',
},
}),
nodeFactory({
id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81',
name: 'NCC-1701-E',
pubkey:
'0931a8fd8cc935458f470e435a05414387cea6f329d648be894fcd44bd517a2b',
infoUrl: 'https://en.wikipedia.org/wiki/USS_Enterprise_(NCC-1701-E)',
stakedByOperator: '3000000000000000000000',
stakedByDelegates: '1041343338923442976709',
stakedTotal: '4041343338923442976709',
pendingStake: '0',
rankingScore: {
rankingScore: '0.1932810100133910357676209647912',
stakeScore: '0.0966762995515676',
performanceScore: '0.999629748500531',
votingPower: '1163',
__typename: 'RankingScore',
},
}),
],
nodeData: {
stakedTotal: '27842509718651285145924',
stakedTotalFormatted: addDecimal('27842509718651285145924', 18),
totalNodes: 3,
inactiveNodes: 0,
validatingNodes: 3,
uptime: 1560.266845703125,
__typename: 'NodeData',
},
};
const renderNodeList = () => {
return render(
<MemoryRouter>
<MockedProvider
mocks={[
{
request: { query: NODES_QUERY },
result: { data: MOCK_NODES },
},
]}
>
<NodeList
epoch={{
__typename: 'Epoch',
id: '1',
timestamps: {
__typename: 'EpochTimestamps',
start: new Date(0).toISOString(),
end: '',
expiry: new Date(1000 * 60 * 60 * 24).toISOString(),
},
}}
party={{
__typename: 'Party',
id: 'foo',
delegations: [
{
__typename: 'Delegation',
amount: '0',
amountFormatted: '0',
epoch: 1,
node: {
__typename: 'Node',
id: 'bar',
},
},
],
stake: {
__typename: 'PartyStake',
currentStakeAvailable: '0',
currentStakeAvailableFormatted: '0',
},
}}
/>
</MockedProvider>
</MemoryRouter>
);
};
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
describe('Nodes list', () => {
it('should render epoch info', async () => {
renderNodeList();
await waitFor(() => {
expect(screen.getByText(MOCK_NODES.nodes[0].name)).toBeInTheDocument();
});
expect(screen.getByTestId('epoch-info')).toBeInTheDocument();
});
it('should a lit of all nodes', async () => {
renderNodeList();
await waitFor(() => {
expect(screen.getByText(MOCK_NODES.nodes[0].name)).toBeInTheDocument();
});
const items = screen.queryAllByTestId('node-list-item');
expect(items).toHaveLength(3);
for (const item of MOCK_NODES.nodes) {
expect(screen.getByText(item.name)).toBeInTheDocument();
}
});
it('should list the total stake and rewards of each node', async () => {
renderNodeList();
await waitFor(() => {
expect(screen.getByText(MOCK_NODES.nodes[0].name)).toBeInTheDocument();
});
const items = screen.queryAllByTestId('node-list-item');
const item = within(items[0]);
const rows = item.getAllByRole('row');
const expectedValues = [
['Total stake', '14,182.45 (50.94%)'],
['Ranking score', '0.6785'],
['Stake score', '0.3393'],
['Performance score', '0.9999'],
['Voting score', '2,407.0000'],
];
for (const [i, r] of rows.entries()) {
const row = within(r);
const cell = row.getByRole('cell');
const header = row.getByRole('rowheader');
expect(header).toHaveTextContent(expectedValues[i][0]);
expect(cell).toHaveTextContent(expectedValues[i][1]);
}
});
});

View File

@ -8,7 +8,7 @@ import { EpochCountdown } from '../../components/epoch-countdown';
import { BigNumber } from '../../lib/bignumber'; import { BigNumber } from '../../lib/bignumber';
import { formatNumber } from '../../lib/format-number'; import { formatNumber } from '../../lib/format-number';
import { truncateMiddle } from '../../lib/truncate-middle'; import { truncateMiddle } from '../../lib/truncate-middle';
import type { Nodes } from './__generated__/Nodes'; import type { Nodes, Nodes_nodes_rankingScore } from './__generated__/Nodes';
import type { Staking_epoch, Staking_party } from './__generated__/Staking'; import type { Staking_epoch, Staking_party } from './__generated__/Staking';
export const NODES_QUERY = gql` export const NODES_QUERY = gql`
@ -33,6 +33,13 @@ export const NODES_QUERY = gql`
online online
} }
status status
rankingScore {
rankingScore
stakeScore
performanceScore
votingPower
stakeScore
}
} }
nodeData { nodeData {
stakedTotal stakedTotal
@ -54,7 +61,10 @@ const NodeListTr = ({ children }: { children: React.ReactNode }) => (
); );
const NodeListTh = ({ children }: { children: React.ReactNode }) => ( const NodeListTh = ({ children }: { children: React.ReactNode }) => (
<th className="flex-1 break-words py-1 pr-4 pl-0 text-white-60 font-normal"> <th
role="rowheader"
className="flex-1 break-words py-1 pr-4 pl-0 text-white-60 font-normal"
>
{children} {children}
</th> </th>
); );
@ -113,6 +123,7 @@ export const NodeList = ({ epoch, party }: NodeListProps) => {
userStake, userStake,
userStakePercentage, userStakePercentage,
epoch, epoch,
scores: node.rankingScore,
}; };
}); });
@ -160,6 +171,7 @@ export interface NodeListItemProps {
stakedTotalPercentage: string; stakedTotalPercentage: string;
userStake: BigNumber; userStake: BigNumber;
userStakePercentage: string; userStakePercentage: string;
scores: Nodes_nodes_rankingScore;
} }
export const NodeListItem = ({ export const NodeListItem = ({
@ -169,6 +181,7 @@ export const NodeListItem = ({
stakedTotalPercentage, stakedTotalPercentage,
userStake, userStake,
userStakePercentage, userStakePercentage,
scores,
}: NodeListItemProps) => { }: NodeListItemProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -186,24 +199,36 @@ export const NodeListItem = ({
<span <span
className="uppercase text-white-60" className="uppercase text-white-60"
title={`${t('id')}: ${id}`} title={`${t('id')}: ${id}`}
data-testid="node-list-item-name"
> >
{truncateMiddle(id)} {truncateMiddle(id)}
</span> </span>
</> </>
)} )}
</Link> </Link>
<table className="flex-1 text-body border-collapse mt-4"> <table
className="flex-1 text-body border-collapse mt-4"
data-testid="node-list-item-table"
>
<tbody> <tbody>
<NodeListTr> <NodeListTr>
<NodeListTh>{t('Total stake')}</NodeListTh> <NodeListTh>{t('Total stake')}</NodeListTh>
<NodeListTd>{formatNumber(stakedOnNode, 2)}</NodeListTd> <NodeListTd>
<NodeListTd>{stakedTotalPercentage}</NodeListTd> {formatNumber(stakedOnNode, 2)} ({stakedTotalPercentage})
</NodeListTd>
</NodeListTr> </NodeListTr>
<NodeListTr> {scores
<NodeListTh>{t('Your stake')}</NodeListTh> ? Object.entries(scores)
<NodeListTd>{formatNumber(userStake, 2)}</NodeListTd> .filter(([key]) => key !== '__typename')
<NodeListTd>{userStakePercentage}</NodeListTd> .map(([key, value]) => (
<NodeListTr key={`${id}_${key}`}>
<NodeListTh>{t(key)}</NodeListTh>
<NodeListTd>
{formatNumber(new BigNumber(value), 4)}
</NodeListTd>
</NodeListTr> </NodeListTr>
))
: null}
</tbody> </tbody>
</table> </table>
</li> </li>

View File

@ -53,6 +53,13 @@ export const STAKING_QUERY = gql`
online online
} }
status status
rankingScore {
rankingScore
stakeScore
performanceScore
votingPower
stakeScore
}
} }
nodeData { nodeData {
stakedTotal stakedTotal