Compare commits

...
Author SHA1 Message Date
Dexter ba45e090f3 chore: make log level customisable 2023-04-19 17:16:18 +01:00
Ciaran McGhie 48072eb4c3 chore(react-helpers): ugprade react-helpers to 0.2.0 and ui-toolkit to 0.11.0 (#3469) 2023-04-19 16:07:35 +01:00
Sam Keen ae92ff2580 feat(governance): protocol upgrade proposals list and details (#3363) 2023-04-19 13:44:36 +01:00
dexturr c99b7fbb02 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-19 12:07:50 +00:00
Joe Tsang a9b1767a42 fix(governance): fix flaky tests due to wallet teardown (#3460) 2023-04-19 11:54:04 +01:00
Ciaran McGhie 3cd60ad3e6 chore(ui-toolkit): bump ui toolkit to 0.10.0 (#3458) 2023-04-19 09:46:57 +01:00
Art 2f4bc9484b feat(environment): github releases (#3426) 2023-04-19 09:42:37 +01:00
dexturr d4f5643799 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-19 06:09:43 +00:00
dexturr 72c49e62a8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-19 00:14:26 +00:00
dexturr 0988edc129 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-18 18:07:34 +00:00
botond 4d266963ae feat(governance): paginate rewards (#3391) 2023-04-18 14:40:57 +01:00
65 changed files with 42900 additions and 509 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_ENV=CUSTOM
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
# App flags
NX_EXPLORER_ASSETS=1
+1 -1
View File
@@ -4,4 +4,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
NX_VEGA_ENV=CUSTOM
NX_BLOCK_EXPLORER=
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
+1 -1
View File
@@ -13,7 +13,7 @@ NX_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
#Test configuration variables
@@ -12,7 +12,6 @@ import {
generateFreeFormProposalTitle,
getProposalIdFromList,
getProposalInformationFromTable,
getSortOrderOfSuppliedArray,
getSubmittedProposalFromProposalList,
goToMakeNewProposal,
governanceProposalType,
@@ -24,9 +23,9 @@ import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/stakin
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
const proposalDetailsTitle = '[data-testid="proposal-title"]';
const openProposals = '[data-testid="open-proposals"]';
const voteStatus = '[data-testid="vote-status"]';
const proposalClosingDate = '[data-testid="vote-details"]';
const viewProposalButton = '[data-testid="view-proposal-btn"]';
describe('Governance flow for proposal list', { tags: '@slow' }, function () {
@@ -45,16 +44,8 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
});
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
const minCloseDays = 2;
const maxCloseDays = 3;
// 3001-VOTE-005
const proposalDays = [
minCloseDays + 1,
maxCloseDays,
minCloseDays + 3,
minCloseDays + 2,
];
const proposalDays = [364, 50, 2];
for (let index = 0; index < proposalDays.length; index++) {
goToMakeNewProposal(governanceProposalType.RAW);
enterRawProposalBody(
@@ -64,19 +55,12 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
waitForProposalSync();
}
const arrayOfProposals: string[] = [];
navigateTo(navigation.proposals);
cy.get(proposalDetailsTitle)
.each((proposalTitleElement) => {
arrayOfProposals.push(proposalTitleElement.text());
})
.then(() => {
cy.wrap(getSortOrderOfSuppliedArray(arrayOfProposals)).should(
'equal',
'descending'
);
});
cy.get(openProposals).within(() => {
cy.get(proposalClosingDate).first().should('contain.text', 'year');
cy.get(proposalClosingDate).should('contain.text', 'months');
cy.get(proposalClosingDate).last().should('contain.text', 'days');
});
});
it('Newly created proposals list - able to filter by proposerID to show it in list', function () {
@@ -15,11 +15,13 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it('should display announcement banner', function () {
it.skip('should display announcement banner', function () {
cy.getByTestId('app-announcement')
.should('be.visible')
.should('contain.text', 'TEST ANNOUNCEMENT!')
.within(() => {
cy.getByTestId('external-link').should('exist');
cy.getByTestId('external-link')
.should('have.attr', 'href', 'https://fairground.wtf')
.and('have.text', 'CLICK LINK');
});
cy.getByTestId('app-announcement-close').should('be.visible').click();
cy.getByTestId('app-announcement').should('not.exist');
@@ -333,6 +333,7 @@ context(
.parent()
.siblings()
.invoke('text')
.should('have.length.above', 3)
.then((el) => {
const value = parseFloat(el);
cy.wrap(value).should('be.gte', parseFloat(expectedAmount));
@@ -150,18 +150,6 @@ export function waitForProposalSync() {
});
}
export function getSortOrderOfSuppliedArray(suppliedArray: string[]) {
const tempArray = [];
for (let index = 1; index < suppliedArray.length; index++) {
tempArray.push(
suppliedArray[index - 1].localeCompare(suppliedArray[index])
);
}
if (tempArray.every((n) => n <= 0)) return 'ascending';
else if (tempArray.every((n) => n >= 0)) return 'descending';
else return 'unsorted';
}
export function goToMakeNewProposal(proposalType: string) {
navigateTo(navigation.proposals);
cy.get(newProposalButton).should('be.visible').click();
@@ -105,24 +105,25 @@ async function vegaWalletTeardownStaking(stakingBridgeContract: StakingBridge) {
{ timeout: transactionTimeout, log: false }
).then((stakeBalance) => {
if (Number(stakeBalance) != 0) {
cy.get('[data-testid="vega-wallet-balance-unstaked"]:visible').within(
() => {
cy.get(associatedAmountInWallet)
.invoke('text')
.then(($walletAmount) => {
cy.wrap(
stakingBridgeContract.remove_stake(
String(stakeBalance),
vegaWalletPubKey
),
{ timeout: transactionTimeout, log: false }
);
cy.get(associatedAmountInWallet, {
cy.get(vegaWalletContainer).within(() => {
cy.getByTestId('currency-value')
.invoke('text')
.then(($associatedAmount) => {
cy.wrap(
stakingBridgeContract.remove_stake(
String(stakeBalance),
vegaWalletPubKey
),
{ timeout: transactionTimeout, log: false }
);
cy.get("[data-testid='currency-value']")
.first()
.invoke('text', {
timeout: transactionTimeout,
}).should('not.have.text', $walletAmount);
});
}
);
})
.should('not.eq', $associatedAmount);
});
});
}
});
}
@@ -134,8 +135,6 @@ async function vegaWalletTeardownVesting(vestingContract: TokenVesting) {
log: false,
}).then((vestingAmount) => {
if (Number(vestingAmount) != 0) {
// Wait needed to allow time for ganache to process tx for stakingBridgeContract.remove_stake
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wrap(
vestingContract.remove_stake(String(vestingAmount), vegaWalletPubKey),
{ timeout: transactionTimeout, log: false }
+1 -1
View File
@@ -5,7 +5,7 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_FAIRGROUND=false
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_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_VEGA_CONFIG_URL=''
NX_VEGA_URL=http://localhost:3028/query
@@ -1,9 +1,8 @@
import type { ObservableQuery } from '@apollo/client';
import { useEffect } from 'react';
export const useRefreshAfterEpoch = (
epochExpiry: string | undefined,
refetch: ObservableQuery['refetch']
refetch: () => void
) => {
return useEffect(() => {
const epochInterval = setInterval(() => {
+16 -2
View File
@@ -595,6 +595,7 @@
"noPercentage": "No percentage",
"proposalTerms": "Proposal terms",
"currentlySetTo": "Currently expected to ",
"currently": "currently",
"finalOutcomeMayDiffer": "Final outcome may differ",
"votingPower": "Voting power",
"normalisedVotingPower": "Normalised voting power",
@@ -767,7 +768,7 @@
"performancePenalty": "Performance penalty",
"overstaked": "Overstaked",
"overstakedPenalty": "Overstaked penalty",
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject.",
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject. Network upgrades are proposed and approved by validators.",
"homeProposalsButtonText": "Browse, vote, and propose",
"homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.",
"homeValidatorsButtonText": "Browse, and stake",
@@ -775,5 +776,18 @@
"homeRewardsButtonText": "See rewards",
"homeVegaTokenIntro": "VEGA Token is a governance asset used to make and vote on proposals, and nominate validators.",
"homeVegaTokenButtonText": "Manage tokens",
"downloadProposalJson": "Download proposal as JSON"
"downloadProposalJson": "Download proposal as JSON",
"networkUpgrade": "Network Upgrade",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED": "Approved",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING": "Pending",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED": "Rejected",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED": "Unspecified",
"vegaRelease{release}": "Vega Release {{release}}",
"upgradeBlockHeight": "Upgrade block height",
"vegaReleaseTag": "Vega release tag",
"approvalStatus": "Approval status",
"approvers": "Approvers",
"approval (% validator voting power)": "approval (% validator voting power)",
"67% voting power required": "67% voting power required",
"Token": "Token"
}
@@ -0,0 +1,15 @@
export const calculateEpochOffset = ({
epochId,
page,
size,
}: {
epochId: number;
page: number;
size: number;
}) => {
// offset the epoch by the current page number times the page size while making sure it doesn't go below the minimum epoch value
return {
fromEpoch: Math.max(0, epochId - size * page) + 1,
toEpoch: epochId - size * page + size,
};
};
+88 -20
View File
@@ -10,23 +10,31 @@ import {
import { useDocumentTitle } from '../../hooks/use-document-title';
import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
import Routes from '../routes';
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import { getNotRejectedProposals } from '../proposals/proposals/proposals-container';
import { useProtocolUpgradesQuery } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import {
getNotRejectedProposals,
getNotRejectedProtocolUpgradeProposals,
} from '../proposals/proposals/proposals-container';
import { Heading } from '../../components/heading';
import * as Schema from '@vegaprotocol/types';
import type { RouteChildProps } from '..';
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
import type { NodesFragmentFragment } from '../staking/home/__generated__/Nodes';
import type { ProtocolUpgradeProposalFieldsFragment } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals';
const nodesToShow = 6;
const HomeProposals = ({
proposals,
protocolUpgradeProposals,
}: {
proposals: ProposalFieldsFragment[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => {
const { t } = useTranslation();
@@ -47,6 +55,10 @@ const HomeProposals = ({
data-testid="home-proposal-list"
className="grid md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-6"
>
{protocolUpgradeProposals.map((proposal, index) => (
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))}
{proposals.map((proposal) => (
<ProposalsListItem key={proposal.id} proposal={proposal} />
))}
@@ -107,20 +119,7 @@ const HomeNodes = ({
{trimmedActiveNodes.map(({ id, avatarUrl, name }) => (
<div key={id} data-testid="validators" className="col-span-2">
<Link to={`${Routes.VALIDATORS}/${id}`}>
<RoundedWrapper paddingBottom={true} border={false}>
<div className="flex items-center justify-center m-[-1rem] p-4 bg-neutral-900 hover:bg-neutral-800">
{avatarUrl && (
<img
className="h-6 w-6 rounded-full mr-2"
src={avatarUrl}
alt={`Avatar icon for ${name}`}
/>
)}
<span className="text-sm">{name}</span>
</div>
</RoundedWrapper>
</Link>
<ValidatorDetailsLink id={id} avatarUrl={avatarUrl} name={name} />
</div>
))}
</div>
@@ -136,6 +135,35 @@ const HomeNodes = ({
);
};
interface ValidatorDetailsLinkProps {
id: string;
avatarUrl: string | null | undefined;
name: string;
}
export const ValidatorDetailsLink = ({
id,
avatarUrl,
name,
}: ValidatorDetailsLinkProps) => {
return (
<Link to={`${Routes.VALIDATORS}/${id}`}>
<RoundedWrapper paddingBottom={true} border={false}>
<div className="flex items-center justify-center m-[-1rem] p-3 bg-neutral-900 hover:bg-neutral-800">
{avatarUrl && (
<img
className="h-6 w-6 rounded-full mr-2"
src={avatarUrl}
alt={`Avatar icon for ${name}`}
/>
)}
<span className="text-sm">{name}</span>
</div>
</RoundedWrapper>
</Link>
);
};
const GovernanceHome = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
@@ -149,6 +177,16 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
errorPolicy: 'ignore',
});
const {
data: protocolUpgradesData,
loading: protocolUpgradesLoading,
error: protocolUpgradesError,
} = useProtocolUpgradesQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
});
const {
data: validatorsData,
error: validatorsError,
@@ -163,11 +201,38 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
proposalsData
? getNotRejectedProposals<ProposalFieldsFragment>(
proposalsData.proposalsConnection
).slice(0, 3)
)
: [],
[proposalsData]
);
const protocolUpgradeProposals = useMemo(
() =>
protocolUpgradesData
? getNotRejectedProtocolUpgradeProposals<ProtocolUpgradeProposalFieldsFragment>(
protocolUpgradesData.protocolUpgradeProposals
).filter(
(p) =>
Number(p.upgradeBlockHeight) >
Number(protocolUpgradesData.lastBlockHeight)
)
: [],
[protocolUpgradesData]
);
const totalProposalsDesired = 4;
const protocolUpgradeProposalsToShow = protocolUpgradeProposals.slice(
0,
totalProposalsDesired
);
const proposalsToShow =
protocolUpgradeProposalsToShow.length === totalProposalsDesired
? []
: proposals.slice(
0,
totalProposalsDesired - protocolUpgradeProposalsToShow.length
);
const activeNodes = removePaginationWrapper(
validatorsData?.nodesConnection.edges
);
@@ -182,11 +247,14 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
return (
<AsyncRenderer
loading={proposalsLoading || validatorsLoading}
error={proposalsError || validatorsError}
data={proposalsData && validatorsData}
loading={proposalsLoading || protocolUpgradesLoading || validatorsLoading}
error={proposalsError || protocolUpgradesError || validatorsError}
data={proposalsData && protocolUpgradesData && validatorsData}
>
<HomeProposals proposals={proposals} />
<HomeProposals
proposals={proposalsToShow}
protocolUpgradeProposals={protocolUpgradeProposalsToShow}
/>
<HomeNodes
activeNodes={activeNodes}
@@ -63,7 +63,7 @@ const renderComponent = (proposals: ProposalQuery['proposal'][]) => (
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsList proposals={proposals} />
<ProposalsList proposals={proposals} protocolUpgradeProposals={[]} />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
@@ -3,17 +3,21 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Heading, SubHeading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item';
import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter';
import Routes from '../../../routes';
import { Button } from '@vegaprotocol/ui-toolkit';
import { Link } from 'react-router-dom';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { ExternalLinks } from '@vegaprotocol/utils';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
interface ProposalsListProps {
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
lastBlockHeight?: string;
}
interface SortedProposalsProps {
@@ -21,7 +25,16 @@ interface SortedProposalsProps {
closed: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
}
export const ProposalsList = ({ proposals }: ProposalsListProps) => {
interface SortedProtocolUpgradeProposalsProps {
open: ProtocolUpgradeProposalFieldsFragment[];
closed: ProtocolUpgradeProposalFieldsFragment[];
}
export const ProposalsList = ({
proposals,
protocolUpgradeProposals,
lastBlockHeight,
}: ProposalsListProps) => {
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
const sortedProposals = proposals.reduce(
@@ -39,6 +52,21 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
}
);
const sortedProtocolUpgradeProposals = protocolUpgradeProposals.reduce(
(acc: SortedProtocolUpgradeProposalsProps, proposal) => {
if (Number(proposal?.upgradeBlockHeight) > Number(lastBlockHeight)) {
acc.open.push(proposal);
} else {
acc.closed.push(proposal);
}
return acc;
},
{
open: [],
closed: [],
}
);
const filterPredicate = (
p: ProposalFieldsFragment | ProposalQuery['proposal']
) =>
@@ -65,7 +93,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
</div>
<p className="mb-8">
{t(
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network.`
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.`
)}{' '}
<ExternalLink
data-testid="proposal-documentation-link"
@@ -80,8 +108,15 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
)}
<section className="-mx-4 p-4 mb-8 bg-neutral-800">
<SubHeading title={t('openProposals')} />
{sortedProposals.open.length > 0 ? (
{sortedProposals.open.length > 0 ||
sortedProtocolUpgradeProposals.open.length > 0 ? (
<ul data-testid="open-proposals">
{sortedProtocolUpgradeProposals.open.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal?.id} proposal={proposal} />
))}
@@ -94,8 +129,16 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
</section>
<section>
<SubHeading title={t('closedProposals')} />
{sortedProposals.closed.length > 0 ? (
{sortedProposals.closed.length > 0 ||
sortedProtocolUpgradeProposals.closed.length > 0 ? (
<ul data-testid="closed-proposals">
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
{sortedProposals.closed.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal?.id} proposal={proposal} />
))}
@@ -0,0 +1 @@
export * from './protocol-update-proposal-detail-approvals';
@@ -0,0 +1,84 @@
import { useTranslation } from 'react-i18next';
import {
RoundedWrapper,
KeyValueTable,
KeyValueTableRow,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { ValidatorDetailsLink } from '../../../home';
import { getNormalisedVotingPower } from '../../../staking/shared';
import type { NodesFragmentFragment } from '../../../staking/home/__generated__/Nodes';
export interface ProtocolUpdateProposalDetailApprovalsProps {
totalConsensusValidators: number;
// Consensus validators that have approved the proposal
consensusApprovals: NodesFragmentFragment[];
consensusApprovalsVotingPowerPercentage: string;
}
export const ProtocolUpdateProposalDetailApprovals = ({
totalConsensusValidators,
consensusApprovals,
consensusApprovalsVotingPowerPercentage,
}: ProtocolUpdateProposalDetailApprovalsProps) => {
const { t } = useTranslation();
return (
<>
<div className="mb-10">
<SubHeading title={t('approvalStatus')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="protocol-upgrade-approval-status">
<KeyValueTableRow noBorder={true}>
<span>{`${consensusApprovalsVotingPowerPercentage} ${t(
'approval (% validator voting power)'
)}`}</span>
<span>({t('67% voting power required')})</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
<div className="mb-10">
<SubHeading
title={`${t('approvers')} (${
consensusApprovals.length
}/${totalConsensusValidators} validators)`}
/>
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="protocol-upgrade-approvers">
<KeyValueTableRow>
<div className="mb-2">{t('validator')}</div>
<div className="text-white mb-2">{t('votingPower')}</div>
</KeyValueTableRow>
{consensusApprovals.map((validator, index) => (
<KeyValueTableRow
noBorder={index === consensusApprovals?.length - 1}
key={validator.pubkey}
>
<div
className="-mb-3 mt-1"
data-testid={`validator-${validator.id}`}
>
<ValidatorDetailsLink
id={validator.id}
avatarUrl={validator.avatarUrl}
name={validator.name}
/>
</div>
<span>
{getNormalisedVotingPower(
validator.rankingScore.votingPower,
2
)}
</span>
</KeyValueTableRow>
))}
</KeyValueTable>
</RoundedWrapper>
</div>
</>
);
};
@@ -0,0 +1,86 @@
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import * as Schema from '@vegaprotocol/types';
import { ProtocolUpdateProposalDetailApprovals } from './protocol-update-proposal-detail-approvals';
import { getNormalisedVotingPower } from '../../../staking/shared';
import type { ProtocolUpdateProposalDetailApprovalsProps } from './protocol-update-proposal-detail-approvals';
import type { NodesFragmentFragment } from '../../../staking/home/__generated__/Nodes';
describe('ProtocolUpdateProposalDetailApprovals', () => {
const mockValidators: NodesFragmentFragment[] = [
{
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
name: 'Marvin',
avatarUrl:
'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
pubkey:
'6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
stakedTotal: '14182454495731682635157',
stakedByOperator: '1000000000000000000000',
stakedByDelegates: '13182454495731682635157',
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',
},
];
const defaultProps = {
totalConsensusValidators: 3,
consensusApprovals: mockValidators,
consensusApprovalsVotingPowerPercentage: '75%',
};
const renderComponent = (
props: Partial<ProtocolUpdateProposalDetailApprovalsProps> = {}
) => {
const mergedProps = {
...defaultProps,
...props,
};
return render(
<BrowserRouter>
<ProtocolUpdateProposalDetailApprovals {...mergedProps} />
</BrowserRouter>
);
};
it('renders without crashing', () => {
renderComponent();
expect(
screen.getByTestId('protocol-upgrade-approval-status')
).toBeInTheDocument();
expect(
screen.getByTestId('protocol-upgrade-approvers')
).toBeInTheDocument();
});
it('renders the correct number of consensus approvers', () => {
renderComponent();
const validators = screen.getAllByTestId(/validator-/i);
expect(validators.length).toBe(mockValidators.length);
});
it('renders the correct approval status with the required percentage', () => {
renderComponent();
expect(screen.getByText(/75% approval/i)).toBeInTheDocument();
expect(screen.getByText(/67% voting power required/i)).toBeInTheDocument();
});
it('renders the correct voting power for each validator', () => {
renderComponent();
mockValidators.forEach((validator) => {
const normalisedVotingPower = getNormalisedVotingPower(
validator.rankingScore.votingPower,
2
);
expect(screen.getByText(normalisedVotingPower)).toBeInTheDocument();
});
});
});
@@ -0,0 +1 @@
export * from './protocol-upgrade-proposal-detail-header';
@@ -0,0 +1,18 @@
import { render } from '@testing-library/react';
import { ProtocolUpgradeProposalDetailHeader } from './protocol-upgrade-proposal-detail-header';
describe('ProtocolUpgradeProposalDetailHeader', () => {
it('should render successfully', () => {
const { baseElement } = render(
<ProtocolUpgradeProposalDetailHeader releaseTag="v1.0.0" />
);
expect(baseElement).toBeTruthy();
});
it('should render the release tag', () => {
const { getByText } = render(
<ProtocolUpgradeProposalDetailHeader releaseTag="v1.0.0" />
);
expect(getByText('Vega Release v1.0.0')).toBeTruthy();
});
});
@@ -0,0 +1,21 @@
import { useTranslation } from 'react-i18next';
import { Intent, Lozenge } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
export interface ProtocolUpgradeProposalDetailHeaderProps {
releaseTag: string;
}
export const ProtocolUpgradeProposalDetailHeader = ({
releaseTag,
}: ProtocolUpgradeProposalDetailHeaderProps) => {
const { t } = useTranslation();
return (
<>
<Heading title={t('vegaRelease{release}', { release: releaseTag })} />
<div className="mb-10">
<Lozenge variant={Intent.Success}>{t('networkUpgrade')}</Lozenge>
</div>
</>
);
};
@@ -0,0 +1 @@
export * from './protocol-upgrade-proposal-detail-info';
@@ -0,0 +1,44 @@
// jest tests for the protocol upgrade proposal detail info component
import { render } from '@testing-library/react';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { ProtocolUpgradeProposalDetailInfo } from './protocol-upgrade-proposal-detail-info';
const renderComponent = () =>
render(
<ProtocolUpgradeProposalDetailInfo
proposal={{
vegaReleaseTag: 'v0.1.234',
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
upgradeBlockHeight: '12345',
approvers: [],
}}
/>
);
describe('ProtocolUpgradeProposalDetailInfo', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should render the upgrade block height', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('protocol-upgrade-block-height')).toHaveTextContent(
'12345'
);
});
it('should render the state', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent('Pending');
});
it('should render the vega release tag', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('protocol-upgrade-release-tag')).toHaveTextContent(
'v0.1.234'
);
});
});
@@ -0,0 +1,58 @@
import { useTranslation } from 'react-i18next';
import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
export interface ProtocolUpgradeProposalDetailInfoProps {
proposal: ProtocolUpgradeProposalFieldsFragment;
lastBlockHeight?: string;
}
export const ProtocolUpgradeProposalDetailInfo = ({
proposal,
lastBlockHeight,
}: ProtocolUpgradeProposalDetailInfoProps) => {
const { t } = useTranslation();
return (
<div className="mb-10">
<SubHeading title={t('proposal')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="protocol-upgrade-proposal-details">
<KeyValueTableRow>
<span className="uppercase">{t('upgradeBlockHeight')}</span>
<span data-testid="protocol-upgrade-block-height">
{proposal.upgradeBlockHeight}{' '}
{lastBlockHeight && (
<>
({t('currently')} {lastBlockHeight})
</>
)}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span className="uppercase">{t('state')}</span>
<span data-testid="protocol-upgrade-state">
{t(`${proposal.status}`)}
</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span className="uppercase">{t('vegaReleaseTag')}</span>
<span data-testid="protocol-upgrade-release-tag">
{proposal.vegaReleaseTag}
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
);
};
@@ -0,0 +1,69 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { ProtocolUpgradeProposalsListItem } from './protocol-upgrade-proposals-list-item';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
const proposal = {
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
vegaReleaseTag: 'v1.0.0',
upgradeBlockHeight: '12345',
} as ProtocolUpgradeProposalFieldsFragment;
const renderComponent = (proposal: ProtocolUpgradeProposalFieldsFragment) =>
render(
<BrowserRouter>
<ProtocolUpgradeProposalsListItem proposal={proposal} />
</BrowserRouter>
);
describe('ProtocolUpgradeProposalsListItem', () => {
it('renders the correct status icon for each proposal status', () => {
const statuses = [
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED,
icon: 'protocol-upgrade-proposal-status-icon-rejected',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
icon: 'protocol-upgrade-proposal-status-icon-pending',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED,
icon: 'protocol-upgrade-proposal-status-icon-approved',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED,
icon: 'protocol-upgrade-proposal-status-icon-unspecified',
},
];
statuses.forEach(({ status, icon }) => {
renderComponent({ ...proposal, status });
const statusIcon = screen.getByTestId(icon);
expect(statusIcon).toBeInTheDocument();
});
});
it('renders the correct Vega release tag', () => {
renderComponent(proposal);
const releaseTag = screen.getByTestId(
'protocol-upgrade-proposal-release-tag'
);
expect(releaseTag).toHaveTextContent(proposal.vegaReleaseTag);
});
it('renders the correct upgrade block height', () => {
renderComponent(proposal);
const blockHeight = screen.getByTestId(
'protocol-upgrade-proposal-block-height'
);
expect(blockHeight).toHaveTextContent(proposal.upgradeBlockHeight);
});
});
@@ -0,0 +1,124 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
Button,
Icon,
Intent,
Lozenge,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { stripFullStops } from '@vegaprotocol/utils';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import Routes from '../../../routes';
interface ProtocolProposalsListItemProps {
proposal: ProtocolUpgradeProposalFieldsFragment;
}
export const ProtocolUpgradeProposalsListItem = ({
proposal,
}: ProtocolProposalsListItemProps) => {
const { t } = useTranslation();
if (!proposal || !proposal.upgradeBlockHeight) return null;
let proposalStatusIcon: ReactNode;
switch (proposal.status) {
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED:
proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-rejected">
<Icon name={'cross'} />
</div>
);
break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING:
proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-pending">
<Icon name={'time'} />
</div>
);
break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED:
proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-approved">
<Icon name={'tick'} />
</div>
);
break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED:
proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-unspecified">
<Icon name={'disable'} />
</div>
);
break;
}
return (
<li
id={proposal.upgradeBlockHeight}
data-testid="protocol-upgrade-proposals-list-item"
>
<RoundedWrapper paddingBottom={true} heightFull={true}>
<div
data-testid="protocol-upgrade-proposal-title"
className="text-sm mb-2"
>
<SubHeading title={`Vega release ${proposal.vegaReleaseTag}`} />
</div>
<div className="text-sm">
<div
data-testid="protocol-upgrade-proposal-type"
className="flex items-center gap-2 mb-4"
>
<Lozenge variant={Intent.Success}>{t('networkUpgrade')}</Lozenge>
</div>
<div
data-testid="protocol-upgrade-proposal-release-tag"
className="mb-2"
>
<span className="pr-2">{t('vegaReleaseTag')}</span>
<Lozenge>{proposal.vegaReleaseTag}</Lozenge>
</div>
<div
data-testid="protocol-upgrade-proposal-block-height"
className="mb-2"
>
<span className="pr-2">{t('upgradeBlockHeight')}</span>
<Lozenge>{proposal.upgradeBlockHeight}</Lozenge>
</div>
<div className="grid grid-cols-[1fr_auto] mt-3 items-start gap-2">
<div className="col-start-1 row-start-1 text-white">
<div
data-testid="protocol-upgrade-proposal-status"
className="flex items-center gap-2"
>
<span>{t(`${proposal.status}`)}</span>
<span>{proposalStatusIcon}</span>
</div>
</div>
<div className="col-start-2 row-start-2 justify-self-end">
<Link
to={`${Routes.PROPOSALS}/protocol-upgrade/${stripFullStops(
proposal.vegaReleaseTag
)}`}
>
<Button data-testid="view-proposal-btn" size="sm">
{t('View')}
</Button>
</Link>
</div>
</div>
</div>
</RoundedWrapper>
</li>
);
};
@@ -5,13 +5,18 @@ import { useTranslation } from 'react-i18next';
import { SplashLoader } from '../../../components/splash-loader';
import { ProposalsList } from '../components/proposals-list';
import { useProposalsQuery } from './__generated__/Proposals';
import type { ProposalFieldsFragment } from './__generated__/Proposals';
import type { NodeConnection, NodeEdge } from '@vegaprotocol/utils';
import { getNodes } from '@vegaprotocol/utils';
import flow from 'lodash/flow';
import { ProposalState } from '@vegaprotocol/types';
import {
ProposalState,
ProtocolUpgradeProposalStatus,
} from '@vegaprotocol/types';
import type { NodeConnection, NodeEdge } from '@vegaprotocol/utils';
import type { ProposalFieldsFragment } from './__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import orderBy from 'lodash/orderBy';
import { useProtocolUpgradesQuery } from '../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
const orderByDate = (arr: ProposalFieldsFragment[]) =>
orderBy(
@@ -20,6 +25,15 @@ const orderByDate = (arr: ProposalFieldsFragment[]) =>
['desc', 'desc']
);
const orderByUpgradeBlockHeight = (
arr: ProtocolUpgradeProposalFieldsFragment[]
) =>
orderBy(
arr,
[(p) => p?.upgradeBlockHeight, (p) => p.vegaReleaseTag],
['desc', 'desc']
);
export function getNotRejectedProposals<T extends ProposalFieldsFragment>(
data?: NodeConnection<NodeEdge<T>> | null
): T[] {
@@ -32,6 +46,21 @@ export function getNotRejectedProposals<T extends ProposalFieldsFragment>(
])(data);
}
export function getNotRejectedProtocolUpgradeProposals<
T extends ProtocolUpgradeProposalFieldsFragment
>(data?: NodeConnection<NodeEdge<T>> | null): T[] {
return flow([
(data) =>
getNodes<ProtocolUpgradeProposalFieldsFragment>(data, (p) =>
p
? p.status !==
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED
: false
),
orderByUpgradeBlockHeight,
])(data);
}
export const ProposalsContainer = () => {
const { t } = useTranslation();
const { data, loading, error } = useProposalsQuery({
@@ -40,6 +69,16 @@ export const ProposalsContainer = () => {
errorPolicy: 'ignore',
});
const {
data: protocolUpgradesData,
loading: protocolUpgradesLoading,
error: protocolUpgradesError,
} = useProtocolUpgradesQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
});
const proposals = useMemo(
() =>
getNotRejectedProposals<ProposalFieldsFragment>(
@@ -48,15 +87,25 @@ export const ProposalsContainer = () => {
[data]
);
if (error) {
const protocolUpgradeProposals = useMemo(
() =>
protocolUpgradesData
? getNotRejectedProtocolUpgradeProposals<ProtocolUpgradeProposalFieldsFragment>(
protocolUpgradesData.protocolUpgradeProposals
)
: [],
[protocolUpgradesData]
);
if (error || protocolUpgradesError) {
return (
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
<pre>{error.message}</pre>
<pre>{error?.message || protocolUpgradesError?.message}</pre>
</Callout>
);
}
if (loading) {
if (loading || protocolUpgradesLoading) {
return (
<Splash>
<SplashLoader />
@@ -64,5 +113,11 @@ export const ProposalsContainer = () => {
);
}
return <ProposalsList proposals={proposals} />;
return (
<ProposalsList
proposals={proposals}
protocolUpgradeProposals={protocolUpgradeProposals}
lastBlockHeight={protocolUpgradesData?.lastBlockHeight}
/>
);
};
@@ -0,0 +1,17 @@
fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal {
upgradeBlockHeight
vegaReleaseTag
approvers
status
}
query ProtocolUpgrades {
lastBlockHeight
protocolUpgradeProposals {
edges {
node {
...ProtocolUpgradeProposalFields
}
}
}
}
@@ -0,0 +1,59 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ProtocolUpgradeProposalFieldsFragment = { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array<string>, status: Types.ProtocolUpgradeProposalStatus };
export type ProtocolUpgradesQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ProtocolUpgradesQuery = { __typename?: 'Query', lastBlockHeight: string, protocolUpgradeProposals?: { __typename?: 'ProtocolUpgradeProposalConnection', edges?: Array<{ __typename?: 'ProtocolUpgradeProposalEdge', node: { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array<string>, status: Types.ProtocolUpgradeProposalStatus } }> | null } | null };
export const ProtocolUpgradeProposalFieldsFragmentDoc = gql`
fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal {
upgradeBlockHeight
vegaReleaseTag
approvers
status
}
`;
export const ProtocolUpgradesDocument = gql`
query ProtocolUpgrades {
lastBlockHeight
protocolUpgradeProposals {
edges {
node {
...ProtocolUpgradeProposalFields
}
}
}
}
${ProtocolUpgradeProposalFieldsFragmentDoc}`;
/**
* __useProtocolUpgradesQuery__
*
* To run a query within a React component, call `useProtocolUpgradesQuery` and pass it any options that fit your needs.
* When your component renders, `useProtocolUpgradesQuery` 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 } = useProtocolUpgradesQuery({
* variables: {
* },
* });
*/
export function useProtocolUpgradesQuery(baseOptions?: Apollo.QueryHookOptions<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>(ProtocolUpgradesDocument, options);
}
export function useProtocolUpgradesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>(ProtocolUpgradesDocument, options);
}
export type ProtocolUpgradesQueryHookResult = ReturnType<typeof useProtocolUpgradesQuery>;
export type ProtocolUpgradesLazyQueryHookResult = ReturnType<typeof useProtocolUpgradesLazyQuery>;
export type ProtocolUpgradesQueryResult = Apollo.QueryResult<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>;
@@ -0,0 +1 @@
export { ProtocolUpgradeProposalContainer as default } from './protocol-upgrade-proposal-container';
@@ -0,0 +1,78 @@
import { useEffect, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { removePaginationWrapper, stripFullStops } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import { ProtocolUpgradeProposal } from './protocol-upgrade-proposal';
import { ProposalNotFound } from '../components/proposal-not-found';
import { useProtocolUpgradesQuery } from './__generated__/ProtocolUpgradeProposals';
import { useNodesQuery } from '../../staking/home/__generated__/Nodes';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
export const ProtocolUpgradeProposalContainer = () => {
const params = useParams<{ proposalReleaseTag: string }>();
const { data, loading, error, refetch } = useProtocolUpgradesQuery({
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
skip: !params.proposalReleaseTag,
});
useEffect(() => {
const interval = setInterval(refetch, 1000);
return () => clearInterval(interval);
}, [refetch]);
const {
data: nodesData,
error: nodesError,
loading: nodesLoading,
refetch: nodesRefetch,
} = useNodesQuery();
useRefreshAfterEpoch(nodesData?.epoch.timestamps.expiry, nodesRefetch);
const protocolUpgradeProposal = useMemo(() => {
if (!data?.protocolUpgradeProposals) return null;
const proposals = removePaginationWrapper(
data.protocolUpgradeProposals.edges
);
return proposals.find(
({ vegaReleaseTag }) =>
params.proposalReleaseTag &&
params.proposalReleaseTag === stripFullStops(vegaReleaseTag)
);
}, [data, params.proposalReleaseTag]);
const consensusValidators = useMemo(() => {
if (!nodesData?.nodesConnection.edges) return null;
const nodes = removePaginationWrapper(nodesData.nodesConnection.edges);
return nodes.filter(
({ rankingScore: { status } }) =>
status === Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT
);
}, [nodesData]);
return (
<AsyncRenderer
loading={loading || nodesLoading}
error={error || nodesError}
data={{ ...data, ...nodesData }}
>
{protocolUpgradeProposal ? (
<ProtocolUpgradeProposal
proposal={protocolUpgradeProposal}
lastBlockHeight={data?.lastBlockHeight}
consensusValidators={consensusValidators}
/>
) : (
<ProposalNotFound />
)}
</AsyncRenderer>
);
};
@@ -0,0 +1,134 @@
import { render } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
import {
ProtocolUpgradeProposal,
getConsensusApprovals,
getConsensusApprovalsVotingPower,
getConsensusApprovalsVotingPowerPercentage,
} from './protocol-upgrade-proposal';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { getNormalisedVotingPower } from '../../staking/shared';
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
import type { ProtocolUpgradeProposalFieldsFragment } from './__generated__/ProtocolUpgradeProposals';
const mockProposal: ProtocolUpgradeProposalFieldsFragment = {
vegaReleaseTag: 'v0.1.234',
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
upgradeBlockHeight: '12345',
approvers: [],
};
const mockConsensusValidators: NodesFragmentFragment[] = [
{
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
name: 'Marvin',
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
stakedTotal: '14182454495731682635157',
stakedByOperator: '1000000000000000000000',
stakedByDelegates: '13182454495731682635157',
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',
},
{
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b5',
name: 'Barvin',
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Barvin-TV-3.jpg',
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b291b',
stakedTotal: '1418245449573168263',
stakedByOperator: '100000000000000000',
stakedByDelegates: '1318245449573168263',
pendingStake: '0',
rankingScore: {
rankingScore: '0.5',
stakeScore: '0.3',
performanceScore: '0.98',
votingPower: '2500',
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
__typename: 'RankingScore',
},
__typename: 'Node',
},
];
const renderComponent = () =>
render(
<ProtocolUpgradeProposal proposal={mockProposal} consensusValidators={[]} />
);
describe('ProtocolUpgradeProposal', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
});
describe('getConsensusApprovals', () => {
it('returns an empty array when there are no matching approvers', () => {
const result = getConsensusApprovals(mockConsensusValidators, {
...mockProposal,
approvers: [],
});
expect(result).toEqual([]);
});
it('returns the correct consensus approvals when there are matching approvers', () => {
const expectedApprovals = mockConsensusValidators[1];
const result = getConsensusApprovals(mockConsensusValidators, {
...mockProposal,
approvers: [
'6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b291b',
],
});
expect(result.length).toEqual(1);
expect(result[0].id).toEqual(expectedApprovals.id);
});
});
describe('getConsensusApprovalsVotingPower', () => {
it('returns 0 when the input array is empty', () => {
const result = getConsensusApprovalsVotingPower([]);
expect(result).toBe(0);
});
it('returns the correct sum of voting power for a given array of consensus validators', () => {
const expectedSum = mockConsensusValidators.reduce(
(acc, curr) => acc + Number(curr.rankingScore.votingPower),
0
);
const result = getConsensusApprovalsVotingPower(mockConsensusValidators);
expect(result).toBe(expectedSum);
});
});
describe('getConsensusApprovalsVotingPowerPercentage', () => {
it('returns "0%" when the input value is 0 or less', () => {
const resultZero = getConsensusApprovalsVotingPowerPercentage(0);
const resultNegative = getConsensusApprovalsVotingPowerPercentage(-10);
expect(resultZero).toBe('0%');
expect(resultNegative).toBe('0%');
});
it('returns the correct percentage string when the input value is greater than 0', () => {
const inputValue = 12345;
const expectedPercentage = getNormalisedVotingPower(
inputValue.toString(),
2
);
const result = getConsensusApprovalsVotingPowerPercentage(inputValue);
expect(result).toBe(expectedPercentage);
});
});
@@ -0,0 +1,83 @@
import { useMemo } from 'react';
import { ProtocolUpgradeProposalDetailHeader } from '../components/protocol-upgrade-proposal-detail-header';
import { ProtocolUpdateProposalDetailApprovals } from '../components/protocol-upgrade-proposal-detail-approvals';
import { ProtocolUpgradeProposalDetailInfo } from '../components/protocol-upgrade-proposal-detail-info';
import { getNormalisedVotingPower } from '../../staking/shared';
import type { ProtocolUpgradeProposalFieldsFragment } from './__generated__/ProtocolUpgradeProposals';
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
export interface ProtocolUpgradeProposalProps {
proposal: ProtocolUpgradeProposalFieldsFragment;
consensusValidators: NodesFragmentFragment[] | null;
lastBlockHeight?: string;
}
export const getConsensusApprovals = (
consensusValidators: NodesFragmentFragment[],
proposal: ProtocolUpgradeProposalFieldsFragment
) =>
consensusValidators?.filter(({ pubkey }) =>
proposal.approvers?.includes(pubkey)
);
export const getConsensusApprovalsVotingPower = (
consensusValidators: NodesFragmentFragment[]
) =>
consensusValidators?.reduce<number>(
(acc: number, curr: NodesFragmentFragment) => {
return acc + Number(curr.rankingScore.votingPower);
},
0
);
export const getConsensusApprovalsVotingPowerPercentage = (
consensusApprovalsVotingPower: number
) =>
consensusApprovalsVotingPower > 0
? getNormalisedVotingPower(consensusApprovalsVotingPower.toString(), 2)
: '0%';
export const ProtocolUpgradeProposal = ({
proposal,
lastBlockHeight,
consensusValidators,
}: ProtocolUpgradeProposalProps) => {
const consensusApprovals = useMemo(
() => getConsensusApprovals(consensusValidators || [], proposal),
[consensusValidators, proposal]
);
const consensusApprovalsVotingPower = useMemo(
() => getConsensusApprovalsVotingPower(consensusApprovals || []),
[consensusApprovals]
);
const consensusApprovalsVotingPowerPercentage = useMemo(
() =>
getConsensusApprovalsVotingPowerPercentage(consensusApprovalsVotingPower),
[consensusApprovalsVotingPower]
);
return (
<section data-testid="protocol-upgrade-proposal">
<ProtocolUpgradeProposalDetailHeader
releaseTag={proposal.vegaReleaseTag}
/>
<ProtocolUpgradeProposalDetailInfo
proposal={proposal}
lastBlockHeight={lastBlockHeight}
/>
{consensusValidators && consensusApprovals && (
<ProtocolUpdateProposalDetailApprovals
consensusApprovals={consensusApprovals}
consensusApprovalsVotingPowerPercentage={
consensusApprovalsVotingPowerPercentage
}
totalConsensusValidators={consensusValidators.length}
/>
)}
</section>
);
};
@@ -3,7 +3,7 @@ import { AppStateProvider } from '../../../contexts/app-state/app-state-provider
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
const mockData = {
epoch: '4441',
epoch: 4441,
rewards: [
{
asset: 'tDAI',
@@ -1,32 +1,43 @@
import { useMemo } from 'react';
import { useMemo, useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { EpochFieldsFragment } from '../home/__generated__/Rewards';
import { useRewardsQuery } from '../home/__generated__/Rewards';
import { ENV } from '../../../config';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
export const EpochIndividualRewards = () => {
const EPOCHS_PAGE_SIZE = 10;
type EpochTotalRewardsProps = {
currentEpoch: EpochFieldsFragment;
};
export const EpochIndividualRewards = ({
currentEpoch,
}: EpochTotalRewardsProps) => {
// we start from the previous epoch when displaying rewards data, because the current one has no calculated data while ongoing
const epochId = Number(currentEpoch.id) - 1;
const totalPages = Math.ceil(epochId / EPOCHS_PAGE_SIZE);
const [page, setPage] = useState(1);
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV;
const { data, loading, error } = useRewardsQuery({
const { data, loading, error, refetch } = useRewardsQuery({
notifyOnNetworkStatusChange: true,
variables: {
partyId: pubKey || '',
fromEpoch: epochId - EPOCHS_PAGE_SIZE,
toEpoch: epochId,
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
// we can use the same value for rewardsPagination as delegationsPagination
rewardsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
},
skip: !pubKey,
});
@@ -39,8 +50,37 @@ export const EpochIndividualRewards = () => {
const epochIndividualRewardSummaries = useMemo(() => {
if (!data?.party) return [];
return generateEpochIndividualRewardsList(rewards);
}, [data?.party, rewards]);
return generateEpochIndividualRewardsList({
rewards,
epochId,
page,
size: EPOCHS_PAGE_SIZE,
});
}, [data?.party, epochId, page, rewards]);
const refetchData = useCallback(
async (toPage?: number) => {
const targetPage = toPage ?? page;
await refetch({
partyId: pubKey || '',
...calculateEpochOffset({ epochId, page, size: EPOCHS_PAGE_SIZE }),
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
});
setPage(targetPage);
},
[epochId, page, refetch, delegationsPagination, pubKey]
);
useEffect(() => {
// when the epoch changes, we want to refetch the data to update the current page
if (data) {
refetchData();
}
}, [epochId, data, refetchData]);
return (
<AsyncRenderer
@@ -53,17 +93,24 @@ export const EpochIndividualRewards = () => {
{t('Connected Vega key')}:{' '}
<span className="text-white">{pubKey}</span>
</p>
{epochIndividualRewardSummaries.length ? (
epochIndividualRewardSummaries.map(
(epochIndividualRewardSummary) => (
<EpochIndividualRewardsTable
data={epochIndividualRewardSummary}
/>
)
{epochIndividualRewardSummaries.map(
(epochIndividualRewardSummary) => (
<EpochIndividualRewardsTable
data={epochIndividualRewardSummary}
/>
)
) : (
<p>{t('noRewards')}</p>
)}
<Pagination
isLoading={loading}
hasPrevPage={page > 1}
hasNextPage={page < totalPages}
onBack={() => refetchData(page - 1)}
onNext={() => refetchData(page + 1)}
onFirst={() => refetchData(1)}
onLast={() => refetchData(totalPages)}
>
{t('Page')} {page}
</Pagination>
</div>
)}
/>
@@ -43,6 +43,16 @@ describe('generateEpochIndividualRewardsList', () => {
epoch: { id: '1' },
};
const reward5: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '150',
percentageOfTotal: '0.15',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD' },
party: { id: 'blah' },
epoch: { id: '3' },
};
const rewardWrongType: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '50',
@@ -54,20 +64,38 @@ describe('generateEpochIndividualRewardsList', () => {
};
it('should return an empty array if no rewards are provided', () => {
expect(generateEpochIndividualRewardsList([])).toEqual([]);
expect(
generateEpochIndividualRewardsList({ rewards: [], epochId: 1 })
).toEqual([
{
epoch: 1,
rewards: [],
},
]);
});
it('should filter out any rewards of the wrong type', () => {
const result = generateEpochIndividualRewardsList([rewardWrongType]);
const result = generateEpochIndividualRewardsList({
rewards: [rewardWrongType],
epochId: 1,
});
expect(result).toEqual([]);
expect(result).toEqual([
{
epoch: 1,
rewards: [],
},
]);
});
it('should return reward in the correct format', () => {
const result = generateEpochIndividualRewardsList([reward1]);
const result = generateEpochIndividualRewardsList({
rewards: [reward1],
epochId: 1,
});
expect(result[0]).toEqual({
epoch: '1',
epoch: 1,
rewards: [
{
asset: 'USD',
@@ -105,21 +133,24 @@ describe('generateEpochIndividualRewardsList', () => {
it('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2, reward3, reward4];
const result1 = generateEpochIndividualRewardsList(rewards);
const result1 = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
expect(result1[0].epoch).toEqual('2');
expect(result1[1].epoch).toEqual('1');
expect(result1[0].epoch).toEqual(2);
expect(result1[1].epoch).toEqual(1);
const reorderedRewards = [reward4, reward3, reward2, reward1];
const result2 = generateEpochIndividualRewardsList(reorderedRewards);
const result2 = generateEpochIndividualRewardsList({
rewards: reorderedRewards,
epochId: 2,
});
expect(result2[0].epoch).toEqual('2');
expect(result2[1].epoch).toEqual('1');
expect(result2[0].epoch).toEqual(2);
expect(result2[1].epoch).toEqual(1);
});
it('correctly calculates the total value of rewards for an asset', () => {
const rewards = [reward1, reward4];
const result = generateEpochIndividualRewardsList(rewards);
const result = generateEpochIndividualRewardsList({ rewards, epochId: 1 });
expect(result[0].rewards[0].totalAmount).toEqual('200');
});
@@ -127,11 +158,11 @@ describe('generateEpochIndividualRewardsList', () => {
it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here
const rewards = [reward1, reward2, reward3, reward4];
const result = generateEpochIndividualRewardsList(rewards);
const result = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
expect(result).toEqual([
{
epoch: '2',
epoch: 2,
rewards: [
{
asset: 'GBP',
@@ -196,7 +227,165 @@ describe('generateEpochIndividualRewardsList', () => {
],
},
{
epoch: '1',
epoch: 1,
rewards: [
{
asset: 'USD',
totalAmount: '200',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
},
]);
});
it('returns data correctly for the requested epoch range', () => {
const rewards = [reward1, reward2, reward3, reward4, reward5];
const resultPageOne = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
page: 1,
size: 2,
});
expect(resultPageOne).toEqual([
{
epoch: 3,
rewards: [
{
asset: 'USD',
totalAmount: '150',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '150',
percentageOfTotal: '0.15',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
},
{
epoch: 2,
rewards: [
{
asset: 'GBP',
totalAmount: '200',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '200',
percentageOfTotal: '0.2',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
{
asset: 'EUR',
totalAmount: '50',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '50',
percentageOfTotal: '0.05',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
},
]);
const resultPageTwo = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
page: 2,
size: 2,
});
expect(resultPageTwo).toEqual([
{
epoch: 1,
rewards: [
{
asset: 'USD',
@@ -2,9 +2,10 @@ import { BigNumber } from '../../../lib/bignumber';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
import type { AccountType } from '@vegaprotocol/types';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
export interface EpochIndividualReward {
epoch: string;
epoch: number;
rewards: {
asset: string;
totalAmount: string;
@@ -27,11 +28,29 @@ const emptyRowAccountTypes = accountTypes.map((type) => [
},
]);
export const generateEpochIndividualRewardsList = (
rewards: RewardFieldsFragment[]
) => {
export const generateEpochIndividualRewardsList = ({
rewards,
epochId,
page = 1,
size = 10,
}: {
rewards: RewardFieldsFragment[];
epochId: number;
page?: number;
size?: number;
}) => {
const map: Map<string, EpochIndividualReward> = new Map();
const { fromEpoch, toEpoch } = calculateEpochOffset({ epochId, page, size });
for (let i = toEpoch; i >= fromEpoch; i--) {
map.set(i.toString(), {
epoch: i,
rewards: [],
});
}
// We take the rewards and aggregate them by epoch and asset.
const epochIndividualRewards = rewards.reduce((map, reward) => {
const epochIndividualRewards = rewards.reduce((acc, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.name;
const rewardType = reward.rewardType;
@@ -40,14 +59,14 @@ export const generateEpochIndividualRewardsList = (
// if the rewardType is not of a type we display in the table, we skip it.
if (!accountTypes.includes(rewardType)) {
return map;
return acc;
}
if (!map.has(epochId)) {
map.set(epochId, { epoch: epochId, rewards: [] });
if (!acc.has(epochId)) {
return acc;
}
const epoch = map.get(epochId);
const epoch = acc.get(epochId);
let asset = epoch?.rewards.find((r) => r.asset === assetName);
@@ -76,8 +95,8 @@ export const generateEpochIndividualRewardsList = (
});
}
return map;
}, new Map<string, EpochIndividualReward>());
return acc;
}, map);
return Array.from(epochIndividualRewards.values()).sort(
(a, b) => Number(b.epoch) - Number(a.epoch)
@@ -1,44 +1,64 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
import type {
AggregatedEpochRewardSummary,
RewardType,
RewardItem,
} from './generate-epoch-total-rewards-list';
import { AccountType } from '@vegaprotocol/types';
const assetId =
'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663';
const rewardsList = [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '295',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
];
const rewards: Map<RewardType, RewardItem> = new Map();
rewardsList.forEach((r) => {
rewards.set(r.rewardType, r);
});
const assetRewards: Map<
AggregatedEpochRewardSummary['assetId'],
AggregatedEpochRewardSummary
> = new Map();
assetRewards.set(assetId, {
assetId,
name: 'tDAI TEST',
rewards,
totalAmount: '295',
});
const mockData = {
epoch: 4431,
assetRewards: [
{
assetId:
'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '295',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '295',
},
],
assetRewards,
};
describe('EpochTotalRewardsTable', () => {
@@ -48,17 +48,19 @@ export const EpochTotalRewardsTable = ({
}: EpochTotalRewardsGridProps) => {
return (
<RewardsTable dataTestId="epoch-total-rewards-table" epoch={data.epoch}>
{data.assetRewards.map(({ name, rewards, totalAmount }, i) => (
<div className="contents" key={i}>
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
{name}
{Array.from(data.assetRewards.values()).map(
({ name, rewards, totalAmount }, i) => (
<div className="contents" key={i}>
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
{name}
</div>
{Array.from(rewards.values()).map(({ rewardType, amount }, i) => (
<RewardItem key={i} dataTestId={rewardType} value={amount} />
))}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
{rewards.map(({ rewardType, amount }, i) => (
<RewardItem key={i} dataTestId={rewardType} value={amount} />
))}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
))}
)
)}
</RewardsTable>
);
};
@@ -1,21 +1,62 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit';
import type { EpochFieldsFragment } from '../home/__generated__/Rewards';
import { useEpochAssetsRewardsQuery } from '../home/__generated__/Rewards';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { generateEpochTotalRewardsList } from './generate-epoch-total-rewards-list';
import { NoRewards } from '../no-rewards';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
export const EpochTotalRewards = () => {
const EPOCHS_PAGE_SIZE = 10;
type EpochTotalRewardsProps = {
currentEpoch: EpochFieldsFragment;
};
export const EpochTotalRewards = ({ currentEpoch }: EpochTotalRewardsProps) => {
// we start from the previous epoch when displaying rewards data, because the current one has no calculated data while ongoing
const epochId = Number(currentEpoch.id) - 1;
const totalPages = Math.ceil(epochId / EPOCHS_PAGE_SIZE);
const { t } = useTranslation();
const [page, setPage] = useState(1);
const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
notifyOnNetworkStatusChange: true,
variables: {
epochRewardSummariesPagination: {
first: 10,
epochRewardSummariesFilter: {
fromEpoch: epochId - EPOCHS_PAGE_SIZE,
},
},
});
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
const epochTotalRewardSummaries = generateEpochTotalRewardsList(data) || [];
const refetchData = useCallback(
async (toPage?: number) => {
const targetPage = toPage ?? page;
await refetch({
epochRewardSummariesFilter: calculateEpochOffset({
epochId,
page: targetPage,
size: EPOCHS_PAGE_SIZE,
}),
});
setPage(targetPage);
},
[epochId, page, refetch]
);
useEffect(() => {
// when the epoch changes, we want to refetch the data to update the current page
if (data) {
refetchData();
}
}, [epochId, data, refetchData]);
const epochTotalRewardSummaries =
generateEpochTotalRewardsList({
data,
epochId,
page,
size: EPOCHS_PAGE_SIZE,
}) || [];
return (
<AsyncRenderer
@@ -27,15 +68,22 @@ export const EpochTotalRewards = () => {
className="max-w-full overflow-auto"
data-testid="epoch-rewards-total"
>
{epochTotalRewardSummaries.length === 0 ? (
<NoRewards />
) : (
<>
{epochTotalRewardSummaries.map((epochTotalSummary, index) => (
<EpochTotalRewardsTable data={epochTotalSummary} key={index} />
))}
</>
{Array.from(epochTotalRewardSummaries.values()).map(
(epochTotalSummary, index) => (
<EpochTotalRewardsTable data={epochTotalSummary} key={index} />
)
)}
<Pagination
isLoading={loading}
hasPrevPage={page > 1}
hasNextPage={page < totalPages}
onBack={() => refetchData(page - 1)}
onNext={() => refetchData(page + 1)}
onFirst={() => refetchData(1)}
onLast={() => refetchData(totalPages)}
>
{t('Page')} {page}
</Pagination>
</div>
)}
/>
@@ -3,13 +3,23 @@ import { AccountType } from '@vegaprotocol/types';
describe('generateEpochAssetRewardsList', () => {
it('should return an empty array if data is undefined', () => {
const result = generateEpochTotalRewardsList(undefined);
const result = generateEpochTotalRewardsList({ epochId: 1 });
expect(result).toEqual([]);
expect(result).toEqual(
new Map([
[
'1',
{
epoch: 1,
assetRewards: new Map(),
},
],
])
);
});
it('should return an empty array if empty data is provided', () => {
const epochData = {
it('should return an empty map if empty data is provided', () => {
const data = {
assetsConnection: {
edges: [],
},
@@ -23,13 +33,23 @@ describe('generateEpochAssetRewardsList', () => {
},
};
const result = generateEpochTotalRewardsList(epochData);
const result = generateEpochTotalRewardsList({ data, epochId: 1 });
expect(result).toEqual([]);
expect(result).toEqual(
new Map([
[
'1',
{
epoch: 1,
assetRewards: new Map(),
},
],
])
);
});
it('should return an empty array if no epochRewardSummaries are provided', () => {
const epochData = {
it('should return an empty map if no epochRewardSummaries are provided', () => {
const data = {
assetsConnection: {
edges: [
{
@@ -56,13 +76,23 @@ describe('generateEpochAssetRewardsList', () => {
},
};
const result = generateEpochTotalRewardsList(epochData);
const result = generateEpochTotalRewardsList({ data, epochId: 1 });
expect(result).toEqual([]);
expect(result).toEqual(
new Map([
[
'1',
{
epoch: 1,
assetRewards: new Map(),
},
],
])
);
});
it('should return an array of unnamed assets if no asset names are provided (should not happen)', () => {
const epochData = {
it('should return a map of unnamed assets if no asset names are provided (should not happen)', () => {
const data = {
assetsConnection: {
edges: [],
},
@@ -85,50 +115,80 @@ describe('generateEpochAssetRewardsList', () => {
},
};
const result = generateEpochTotalRewardsList(epochData);
const result = generateEpochTotalRewardsList({ data, epochId: 1 });
expect(result).toEqual([
{
epoch: 1,
assetRewards: [
expect(result).toEqual(
new Map([
[
'1',
{
assetId: '1',
name: '',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '123',
epoch: 1,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: '',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '123',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '123',
},
],
]),
},
],
},
]);
])
);
});
it('should return an array of aggregated epoch summaries', () => {
const epochData = {
it('should return the aggregated epoch summaries', () => {
const data = {
assetsConnection: {
edges: [
{
@@ -180,81 +240,425 @@ describe('generateEpochAssetRewardsList', () => {
},
};
const result = generateEpochTotalRewardsList(epochData);
const result = generateEpochTotalRewardsList({ data, epochId: 2 });
expect(result).toEqual([
{
epoch: 1,
assetRewards: [
expect(result).toEqual(
new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '223',
epoch: 1,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '223',
},
],
]),
},
],
[
'2',
{
epoch: 2,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '5',
},
],
]),
},
],
])
);
});
it('should return the requested range for aggregated epoch summaries', () => {
const data = {
assetsConnection: {
edges: [
{
node: {
id: '1',
name: 'Asset 1',
},
},
{
node: {
id: '2',
name: 'Asset 2',
},
},
],
},
{
epoch: 2,
assetRewards: [
epochRewardSummaries: {
edges: [
{
assetId: '1',
name: 'Asset 1',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '5',
node: {
epoch: 1,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
},
{
node: {
epoch: 1,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
},
{
node: {
epoch: 2,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '6',
},
},
{
node: {
epoch: 2,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '27',
},
},
{
node: {
epoch: 3,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '15',
},
},
],
},
]);
epoch: {
timestamps: {
expiry: null,
},
},
};
const resultPageOne = generateEpochTotalRewardsList({
data,
epochId: 3,
page: 1,
size: 2,
});
expect(resultPageOne).toEqual(
new Map([
[
'2',
{
epoch: 2,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '33',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '33',
},
],
]),
},
],
[
'3',
{
epoch: 3,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '15',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '15',
},
],
]),
},
],
])
);
const resultPageTwo = generateEpochTotalRewardsList({
data,
epochId: 3,
page: 2,
size: 2,
});
expect(resultPageTwo).toEqual(
new Map([
[
'1',
{
epoch: 1,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '223',
},
],
]),
},
],
])
);
});
});
@@ -5,127 +5,97 @@ import type {
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { AccountType } from '@vegaprotocol/types';
import { BigNumber } from '../../../lib/bignumber';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
interface EpochSummaryWithNamedReward extends EpochRewardSummaryFieldsFragment {
name: string;
}
export interface AggregatedEpochRewardSummary {
export type RewardType = EpochRewardSummaryFieldsFragment['rewardType'];
export type RewardItem = Pick<
EpochRewardSummaryFieldsFragment,
'rewardType' | 'amount'
>;
export type AggregatedEpochRewardSummary = {
assetId: EpochRewardSummaryFieldsFragment['assetId'];
name: EpochSummaryWithNamedReward['name'];
rewards: {
rewardType: EpochRewardSummaryFieldsFragment['rewardType'];
amount: EpochRewardSummaryFieldsFragment['amount'];
}[];
rewards: Map<RewardType, RewardItem>;
totalAmount: string;
}
};
export interface EpochTotalSummary {
export type EpochTotalSummary = {
epoch: EpochRewardSummaryFieldsFragment['epoch'];
assetRewards: AggregatedEpochRewardSummary[];
}
assetRewards: Map<
EpochRewardSummaryFieldsFragment['assetId'],
AggregatedEpochRewardSummary
>;
};
const emptyRowAccountTypes = Object.keys(RowAccountTypes).map((type) => ({
rewardType: type as AccountType,
amount: '0',
}));
const emptyRowAccountTypes: Map<RewardType, RewardItem> = new Map();
export const generateEpochTotalRewardsList = (
epochData: EpochAssetsRewardsQuery | undefined
) => {
Object.keys(RowAccountTypes).forEach((type) => {
emptyRowAccountTypes.set(type as AccountType, {
rewardType: type as AccountType,
amount: '0',
});
});
export const generateEpochTotalRewardsList = ({
data,
epochId,
page = 1,
size = 10,
}: {
data?: EpochAssetsRewardsQuery | undefined;
epochId: number;
page?: number;
size?: number;
}) => {
const epochRewardSummaries = removePaginationWrapper(
epochData?.epochRewardSummaries?.edges
data?.epochRewardSummaries?.edges
);
const assets = removePaginationWrapper(epochData?.assetsConnection?.edges);
const assets = removePaginationWrapper(data?.assetsConnection?.edges);
// Because the epochRewardSummaries don't have the asset name, we need to find it in the assets list
const epochSummariesWithNamedReward: EpochSummaryWithNamedReward[] =
epochRewardSummaries.map((epochReward) => ({
...epochReward,
name:
assets.find((asset) => asset.id === epochReward.assetId)?.name || '',
}));
const map: Map<string, EpochTotalSummary> = new Map();
const { fromEpoch, toEpoch } = calculateEpochOffset({ epochId, page, size });
// Aggregating the epoch summaries by epoch number
const aggregatedEpochSummariesByEpochNumber =
epochSummariesWithNamedReward.reduce((acc, epochReward) => {
const epoch = epochReward.epoch;
const epochSummaryIndex = acc.findIndex(
(epochSummary) => epochSummary[0].epoch === epoch
);
if (epochSummaryIndex === -1) {
acc.push([epochReward]);
} else {
acc[epochSummaryIndex].push(epochReward);
}
return acc;
}, [] as EpochSummaryWithNamedReward[][]);
// Now aggregate the array of arrays of epoch summaries by asset rewards.
const epochTotalRewards: EpochTotalSummary[] =
aggregatedEpochSummariesByEpochNumber.map((epochSummaries) => {
const assetRewards = epochSummaries.reduce((acc, epochSummary) => {
const assetRewardIndex = acc.findIndex(
(assetReward) =>
assetReward.assetId === epochSummary.assetId &&
assetReward.name === epochSummary.name
);
if (assetRewardIndex === -1) {
acc.push({
assetId: epochSummary.assetId,
name: epochSummary.name,
rewards: [
...emptyRowAccountTypes.map((emptyRowAccountType) => {
if (
emptyRowAccountType.rewardType === epochSummary.rewardType
) {
return {
rewardType: epochSummary.rewardType,
amount: epochSummary.amount,
};
} else {
return emptyRowAccountType;
}
}),
],
totalAmount: epochSummary.amount,
});
} else {
acc[assetRewardIndex].rewards = acc[assetRewardIndex].rewards.map(
(reward) => {
if (reward.rewardType === epochSummary.rewardType) {
return {
rewardType: epochSummary.rewardType,
amount: (
Number(reward.amount) + Number(epochSummary.amount)
).toString(),
};
} else {
return reward;
}
}
);
acc[assetRewardIndex].totalAmount = (
Number(acc[assetRewardIndex].totalAmount) +
Number(epochSummary.amount)
).toString();
}
return acc;
}, [] as AggregatedEpochRewardSummary[]);
return {
epoch: epochSummaries[0].epoch,
assetRewards: assetRewards.sort((a, b) => {
return new BigNumber(b.totalAmount).comparedTo(a.totalAmount);
}),
};
for (let i = toEpoch; i >= fromEpoch; i--) {
map.set(i.toString(), {
epoch: i,
assetRewards: new Map(),
});
}
return epochTotalRewards;
return epochRewardSummaries.reduce((acc, reward) => {
const epoch = acc.get(reward.epoch.toString());
if (epoch) {
const matchingAsset = assets.find((asset) => asset.id === reward.assetId);
const assetWithRewards = epoch.assetRewards.get(reward.assetId);
const rewards =
assetWithRewards?.rewards || new Map(emptyRowAccountTypes);
const rewardItem = rewards?.get(reward.rewardType);
const amount = (
(Number(rewardItem?.amount) || 0) + Number(reward.amount)
).toString();
rewards?.set(reward.rewardType, {
rewardType: reward.rewardType,
amount,
});
epoch.assetRewards.set(reward.assetId, {
assetId: reward.assetId,
name: matchingAsset?.name || '',
rewards: rewards || new Map(emptyRowAccountTypes),
totalAmount: (
Number(reward.amount) + Number(assetWithRewards?.totalAmount || 0)
).toString(),
});
}
return acc;
}, map);
};
@@ -23,12 +23,18 @@ fragment DelegationFields on Delegation {
query Rewards(
$partyId: ID!
$delegationsPagination: Pagination
$fromEpoch: Int
$toEpoch: Int
$rewardsPagination: Pagination
$delegationsPagination: Pagination
) {
party(id: $partyId) {
id
rewardsConnection(pagination: $rewardsPagination) {
rewardsConnection(
fromEpoch: $fromEpoch
toEpoch: $toEpoch
pagination: $rewardsPagination
) {
edges {
node {
...RewardFields
@@ -43,14 +49,6 @@ query Rewards(
}
}
}
epoch {
id
timestamps {
start
end
expiry
}
}
}
fragment EpochRewardSummaryFields on EpochRewardSummary {
@@ -60,7 +58,10 @@ fragment EpochRewardSummaryFields on EpochRewardSummary {
rewardType
}
query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) {
query EpochAssetsRewards(
$epochRewardSummariesFilter: RewardSummaryFilter
$epochRewardSummariesPagination: Pagination
) {
assetsConnection {
edges {
node {
@@ -69,18 +70,16 @@ query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) {
}
}
}
epochRewardSummaries(pagination: $epochRewardSummariesPagination) {
epochRewardSummaries(
filter: $epochRewardSummariesFilter
pagination: $epochRewardSummariesPagination
) {
edges {
node {
...EpochRewardSummaryFields
}
}
}
epoch {
timestamps {
expiry
}
}
}
fragment EpochFields on Epoch {
@@ -9,21 +9,24 @@ export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: stri
export type RewardsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
delegationsPagination?: Types.InputMaybe<Types.Pagination>;
fromEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
toEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
rewardsPagination?: Types.InputMaybe<Types.Pagination>;
delegationsPagination?: Types.InputMaybe<Types.Pagination>;
}>;
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } } };
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null };
export type EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
export type EpochAssetsRewardsQueryVariables = Types.Exact<{
epochRewardSummariesFilter?: Types.InputMaybe<Types.RewardSummaryFilter>;
epochRewardSummariesPagination?: Types.InputMaybe<Types.Pagination>;
}>;
export type EpochAssetsRewardsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string } } | null> | null } | null, epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null, epoch: { __typename?: 'Epoch', timestamps: { __typename?: 'EpochTimestamps', expiry?: any | null } } };
export type EpochAssetsRewardsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string } } | null> | null } | null, epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null };
export type EpochFieldsFragment = { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } };
@@ -76,10 +79,14 @@ export const EpochFieldsFragmentDoc = gql`
}
`;
export const RewardsDocument = gql`
query Rewards($partyId: ID!, $delegationsPagination: Pagination, $rewardsPagination: Pagination) {
query Rewards($partyId: ID!, $fromEpoch: Int, $toEpoch: Int, $rewardsPagination: Pagination, $delegationsPagination: Pagination) {
party(id: $partyId) {
id
rewardsConnection(pagination: $rewardsPagination) {
rewardsConnection(
fromEpoch: $fromEpoch
toEpoch: $toEpoch
pagination: $rewardsPagination
) {
edges {
node {
...RewardFields
@@ -94,14 +101,6 @@ export const RewardsDocument = gql`
}
}
}
epoch {
id
timestamps {
start
end
expiry
}
}
}
${RewardFieldsFragmentDoc}
${DelegationFieldsFragmentDoc}`;
@@ -119,8 +118,10 @@ ${DelegationFieldsFragmentDoc}`;
* const { data, loading, error } = useRewardsQuery({
* variables: {
* partyId: // value for 'partyId'
* delegationsPagination: // value for 'delegationsPagination'
* fromEpoch: // value for 'fromEpoch'
* toEpoch: // value for 'toEpoch'
* rewardsPagination: // value for 'rewardsPagination'
* delegationsPagination: // value for 'delegationsPagination'
* },
* });
*/
@@ -136,7 +137,7 @@ export type RewardsQueryHookResult = ReturnType<typeof useRewardsQuery>;
export type RewardsLazyQueryHookResult = ReturnType<typeof useRewardsLazyQuery>;
export type RewardsQueryResult = Apollo.QueryResult<RewardsQuery, RewardsQueryVariables>;
export const EpochAssetsRewardsDocument = gql`
query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) {
query EpochAssetsRewards($epochRewardSummariesFilter: RewardSummaryFilter, $epochRewardSummariesPagination: Pagination) {
assetsConnection {
edges {
node {
@@ -145,18 +146,16 @@ export const EpochAssetsRewardsDocument = gql`
}
}
}
epochRewardSummaries(pagination: $epochRewardSummariesPagination) {
epochRewardSummaries(
filter: $epochRewardSummariesFilter
pagination: $epochRewardSummariesPagination
) {
edges {
node {
...EpochRewardSummaryFields
}
}
}
epoch {
timestamps {
expiry
}
}
}
${EpochRewardSummaryFieldsFragmentDoc}`;
@@ -172,6 +171,7 @@ export const EpochAssetsRewardsDocument = gql`
* @example
* const { data, loading, error } = useEpochAssetsRewardsQuery({
* variables: {
* epochRewardSummariesFilter: // value for 'epochRewardSummariesFilter'
* epochRewardSummariesPagination: // value for 'epochRewardSummariesPagination'
* },
* });
@@ -136,11 +136,15 @@ export const RewardsPage = () => {
</section>
{toggleRewardsView === 'total' ? (
<EpochTotalRewards />
epochData?.epoch ? (
<EpochTotalRewards currentEpoch={epochData?.epoch} />
) : null
) : (
<section>
{pubKey && pubKeys?.length ? (
<EpochIndividualRewards />
epochData?.epoch ? (
<EpochIndividualRewards currentEpoch={epochData?.epoch} />
) : null
) : (
<ConnectToSeeRewards />
)}
@@ -109,6 +109,13 @@ const LazyProposal = React.lazy(
)
);
const LazyProtocolUpgradeProposal = React.lazy(
() =>
import(
/* webpackChunkName: "route-governance-protocol-upgrade-proposal", webpackPrefetch: true */ './proposals/protocol-upgrade'
)
);
const LazyProposalsList = React.lazy(
() =>
import(
@@ -257,6 +264,10 @@ const routerConfig = [
},
{ path: 'proposals', element: <LazyProposalsList /> },
{ path: ':proposalId', element: <LazyProposal /> },
{
path: 'protocol-upgrade/:proposalReleaseTag',
element: <LazyProtocolUpgradeProposal />,
},
{ path: 'rejected', element: <LazyRejectedProposalsList /> },
],
},
+2 -2
View File
@@ -20,8 +20,8 @@ export const getLastEpochScoreAndPerformance = (
};
};
export const getNormalisedVotingPower = (votingPower: string) =>
formatNumberPercentage(new BigNumber(votingPower).dividedBy(100), 2);
export const getNormalisedVotingPower = (votingPower: string, decimals = 2) =>
formatNumberPercentage(new BigNumber(votingPower).dividedBy(100), decimals);
export const getUnnormalisedVotingPower = (
validatorScore: string | null | undefined
+146 -29
View File
@@ -1,11 +1,44 @@
[
{
"tranche_id": 57,
"tranche_start": "2024-04-01T00:00:00.000Z",
"tranche_end": "2024-10-01T00:00:00.000Z",
"total_added": "2500",
"total_removed": "0",
"locked_amount": "2500",
"deposits": [
{
"amount": "2500",
"user": "0x6ad69157EfB1E17EBf6614D352D098589EaE3060",
"tx": "0xd89dcda79394278ed62c6d2d36c94d90c59a6cf2c0135903796db75c1797b644"
}
],
"withdrawals": [],
"users": [
{
"address": "0x6ad69157EfB1E17EBf6614D352D098589EaE3060",
"deposits": [
{
"amount": "2500",
"user": "0x6ad69157EfB1E17EBf6614D352D098589EaE3060",
"tranche_id": 57,
"tx": "0xd89dcda79394278ed62c6d2d36c94d90c59a6cf2c0135903796db75c1797b644"
}
],
"withdrawals": [],
"total_tokens": "2500",
"withdrawn_tokens": "0",
"remaining_tokens": "2500"
}
]
},
{
"tranche_id": 56,
"tranche_start": "2023-04-20T00:00:00.000Z",
"tranche_end": "2023-05-20T00:00:00.000Z",
"total_added": "15711.125",
"total_added": "15761.125",
"total_removed": "0",
"locked_amount": "15711.125",
"locked_amount": "15761.125",
"deposits": [
{
"amount": "241.5",
@@ -87,6 +120,11 @@
"user": "0x697cEF6741F519621fE14c72041e4B8B1f7e2c8A",
"tx": "0xc7483de16af39896b3ae052dd8a6e01fb3dc3794ceb9d2a76d81436992af320e"
},
{
"amount": "50",
"user": "0x9E53F72210479BF46aE7ABF77c48B0d611b3c3dB",
"tx": "0x3ce5a0e3e1eed31ca786cb22ee1b7bea1936491604306b10390d93198602d746"
},
{
"amount": "50",
"user": "0x15024E62134A8BFFCce11f5ce58CeCDe853038D7",
@@ -376,6 +414,21 @@
"withdrawn_tokens": "0",
"remaining_tokens": "103.5"
},
{
"address": "0x9E53F72210479BF46aE7ABF77c48B0d611b3c3dB",
"deposits": [
{
"amount": "50",
"user": "0x9E53F72210479BF46aE7ABF77c48B0d611b3c3dB",
"tranche_id": 56,
"tx": "0x3ce5a0e3e1eed31ca786cb22ee1b7bea1936491604306b10390d93198602d746"
}
],
"withdrawals": [],
"total_tokens": "50",
"withdrawn_tokens": "0",
"remaining_tokens": "50"
},
{
"address": "0x15024E62134A8BFFCce11f5ce58CeCDe853038D7",
"deposits": [
@@ -547,10 +600,15 @@
"tranche_id": 55,
"tranche_start": "2023-09-01T00:00:00.000Z",
"tranche_end": "2024-03-01T00:00:00.000Z",
"total_added": "17500",
"total_added": "30000",
"total_removed": "0",
"locked_amount": "17500",
"locked_amount": "30000",
"deposits": [
{
"amount": "12500",
"user": "0x52dD21Bd0A8fBD31f4D5002c34b98d0c328791c8",
"tx": "0xd89dcda79394278ed62c6d2d36c94d90c59a6cf2c0135903796db75c1797b644"
},
{
"amount": "12500",
"user": "0x52dD21Bd0A8fBD31f4D5002c34b98d0c328791c8",
@@ -567,6 +625,12 @@
{
"address": "0x52dD21Bd0A8fBD31f4D5002c34b98d0c328791c8",
"deposits": [
{
"amount": "12500",
"user": "0x52dD21Bd0A8fBD31f4D5002c34b98d0c328791c8",
"tranche_id": 55,
"tx": "0xd89dcda79394278ed62c6d2d36c94d90c59a6cf2c0135903796db75c1797b644"
},
{
"amount": "12500",
"user": "0x52dD21Bd0A8fBD31f4D5002c34b98d0c328791c8",
@@ -575,9 +639,9 @@
}
],
"withdrawals": [],
"total_tokens": "12500",
"total_tokens": "25000",
"withdrawn_tokens": "0",
"remaining_tokens": "12500"
"remaining_tokens": "25000"
},
{
"address": "0xbCDBCC3Ff54D659f2CC737bEf49B10D92dF3FDbf",
@@ -602,7 +666,7 @@
"tranche_end": "2023-05-06T00:00:00.000Z",
"total_added": "8865",
"total_removed": "362.31015740205",
"locked_amount": "5243.2918055555557635",
"locked_amount": "4874.1459548611110345",
"deposits": [
{
"amount": "33",
@@ -1212,10 +1276,15 @@
"tranche_id": 50,
"tranche_start": "2023-10-01T00:00:00.000Z",
"tranche_end": "2024-04-01T00:00:00.000Z",
"total_added": "2500",
"total_added": "10000",
"total_removed": "0",
"locked_amount": "2500",
"locked_amount": "10000",
"deposits": [
{
"amount": "7500",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xd89dcda79394278ed62c6d2d36c94d90c59a6cf2c0135903796db75c1797b644"
},
{
"amount": "2500",
"user": "0x6ad69157EfB1E17EBf6614D352D098589EaE3060",
@@ -1224,6 +1293,21 @@
],
"withdrawals": [],
"users": [
{
"address": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"deposits": [
{
"amount": "7500",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 50,
"tx": "0xd89dcda79394278ed62c6d2d36c94d90c59a6cf2c0135903796db75c1797b644"
}
],
"withdrawals": [],
"total_tokens": "7500",
"withdrawn_tokens": "0",
"remaining_tokens": "7500"
},
{
"address": "0x6ad69157EfB1E17EBf6614D352D098589EaE3060",
"deposits": [
@@ -4323,7 +4407,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "86666.297",
"total_removed": "0",
"locked_amount": "54788.2476184172999611065",
"locked_amount": "54491.6293588377368963476",
"deposits": [
{
"amount": "86666.297",
@@ -4389,7 +4473,7 @@
"tranche_end": "2023-06-01T00:00:00.000Z",
"total_added": "2500",
"total_removed": "0",
"locked_amount": "600.8763227513228",
"locked_amount": "583.71664504477005",
"deposits": [
{
"amount": "2500",
@@ -4510,7 +4594,7 @@
"tranche_end": "2023-09-01T00:00:00.000Z",
"total_added": "17500",
"total_removed": "0",
"locked_amount": "12910.415408615137",
"locked_amount": "12791.60329232085475",
"deposits": [
{
"amount": "12500",
@@ -4777,7 +4861,7 @@
"tranche_end": "2023-08-01T00:00:00.000Z",
"total_added": "37500",
"total_removed": "12294.731717775",
"locked_amount": "21701.062768569675",
"locked_amount": "21442.24553023327125",
"deposits": [
{
"amount": "7500",
@@ -5141,7 +5225,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "129999.45",
"total_removed": "0",
"locked_amount": "54738.260236187147286555",
"locked_amount": "54441.912603447519962385",
"deposits": [
{
"amount": "129999.45",
@@ -5174,7 +5258,7 @@
"tranche_end": "2024-04-01T00:00:00.000Z",
"total_added": "54144.7663",
"total_removed": "0",
"locked_amount": "51591.9435766059257577711",
"locked_amount": "51407.13764596631692808494",
"deposits": [
{
"amount": "54144.7663",
@@ -5207,7 +5291,7 @@
"tranche_end": "2023-09-03T00:00:00.000Z",
"total_added": "62600",
"total_removed": "0",
"locked_amount": "23624.00451547437818",
"locked_amount": "23409.75395104008482",
"deposits": [
{
"amount": "10000",
@@ -5400,7 +5484,7 @@
"tranche_end": "2023-09-17T00:00:00.000Z",
"total_added": "5000",
"total_removed": "0",
"locked_amount": "2078.6821410451545",
"locked_amount": "2061.56947615423635",
"deposits": [
{
"amount": "5000",
@@ -5941,7 +6025,7 @@
"tranche_end": "2023-05-01T00:00:00.000Z",
"total_added": "22500",
"total_removed": "6871.43713545",
"locked_amount": "1584.173572744015575",
"locked_amount": "1428.88322974217355",
"deposits": [
{
"amount": "7500",
@@ -6425,7 +6509,7 @@
"tranche_end": "2023-06-02T00:00:00.000Z",
"total_added": "1939928.38",
"total_removed": "928642.9598472029154",
"locked_amount": "237807.5623674632953210932",
"locked_amount": "231168.093511598737240552",
"deposits": [
{
"amount": "1852091.69",
@@ -40241,7 +40325,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "3732368.4671",
"total_removed": "715655.108029600523393",
"locked_amount": "389928.657761997798045454489",
"locked_amount": "379726.105748070797206746073",
"deposits": [
{
"amount": "1998.95815",
@@ -41633,8 +41717,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "15870102.715470999700000001",
"total_removed": "834257.00587199386490352",
"locked_amount": "6682349.57025217979014250580920627858799674",
"total_removed": "835090.78677275839915352",
"locked_amount": "6646171.9758772447834505578413098637685861",
"deposits": [
{
"amount": "16249.93",
@@ -42188,6 +42272,11 @@
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x94df9afacf00f3d2a01e8856c83050a2737cd31f7736ae472bfbb3f19e66f5e4"
},
{
"amount": "833.78090076453425",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xa56a064890829624fb37f887adb87987dfafd709ebea2cdf423c91a38e91b82f"
},
{
"amount": "998.454771147362375",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -44280,6 +44369,12 @@
"tranche_id": 2,
"tx": "0x94df9afacf00f3d2a01e8856c83050a2737cd31f7736ae472bfbb3f19e66f5e4"
},
{
"amount": "833.78090076453425",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0xa56a064890829624fb37f887adb87987dfafd709ebea2cdf423c91a38e91b82f"
},
{
"amount": "998.454771147362375",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -45716,8 +45811,8 @@
}
],
"total_tokens": "259998.8875",
"withdrawn_tokens": "149900.199617162944125",
"remaining_tokens": "110098.687882837055875"
"withdrawn_tokens": "150733.980517927478375",
"remaining_tokens": "109264.906982072521625"
},
{
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
@@ -48068,8 +48163,8 @@
"tranche_start": "2021-11-05T00:00:00.000Z",
"tranche_end": "2023-05-05T00:00:00.000Z",
"total_added": "14597706.0446472999",
"total_removed": "5844057.348231362114420626",
"locked_amount": "447657.2297616420148104652270758603",
"total_removed": "5845111.643096907348272126",
"locked_amount": "414258.305630822730645455902270797",
"deposits": [
{
"amount": "129284.449",
@@ -48343,6 +48438,16 @@
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xca4c0571969c50f47990b214485913df8e2b7d1d92cdd6ac4c8bcc6ce760ce0b"
},
{
"amount": "683.131185507772482",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x5cee329248d32efdbd9e7f2b223f1714843a154b530cafd9f5df43a1d45c625b"
},
{
"amount": "371.1636800374613695",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xc4a521e74b6b9c9c01e7fbd950f4096d3e6473a773818b4fdf0c9d974ee5fc7e"
},
{
"amount": "37462.74710421168752574",
"user": "0x66827bCD635f2bB1779d68c46aEB16541bCA6ba8",
@@ -51618,6 +51723,18 @@
"tranche_id": 3,
"tx": "0xca4c0571969c50f47990b214485913df8e2b7d1d92cdd6ac4c8bcc6ce760ce0b"
},
{
"amount": "683.131185507772482",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0x5cee329248d32efdbd9e7f2b223f1714843a154b530cafd9f5df43a1d45c625b"
},
{
"amount": "371.1636800374613695",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0xc4a521e74b6b9c9c01e7fbd950f4096d3e6473a773818b4fdf0c9d974ee5fc7e"
},
{
"amount": "1383.345213676043696",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -54356,8 +54473,8 @@
}
],
"total_tokens": "359123.469575",
"withdrawn_tokens": "347720.524441911345933",
"remaining_tokens": "11402.945133088654067"
"withdrawn_tokens": "348774.8193074565797845",
"remaining_tokens": "10348.6502675434202155"
},
{
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
@@ -57992,7 +58109,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "472355.6199999996",
"total_removed": "43120.0853616251849",
"locked_amount": "61786.424749621320244693540030452",
"locked_amount": "60169.7720627409513147623313039",
"deposits": [
{
"amount": "3000",
+2 -2
View File
@@ -11,7 +11,7 @@ NX_VEGA_URL=http://localhost:3028/query
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
+2
View File
@@ -1,3 +1,5 @@
export * from './use-environment';
export * from './use-links';
export * from './use-node-health';
export * from './use-vega-releases';
export * from './use-vega-release';
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
import { renderHook, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import {
GITHUB_VEGA_DEV_RELEASES,
GITHUB_VEGA_RELEASES,
} from './use-vega-releases';
import {
GITHUB_VEGA_RELEASES_DATA,
GITHUB_VEGA_DEV_RELEASES_DATA,
} from './mocks/github-releases';
import { useVegaRelease } from './use-vega-release';
describe('useVegaRelease', () => {
beforeEach(() => {
fetchMock.get(GITHUB_VEGA_RELEASES, GITHUB_VEGA_RELEASES_DATA);
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_DEV_RELEASES_DATA);
});
afterEach(() => {
fetchMock.reset();
});
it('should return release information by given tag', async () => {
const { result } = renderHook(() => useVegaRelease('v0.70.1'));
await waitFor(() => {
expect(result.current).toBeTruthy();
expect(result.current?.tagName).toEqual('v0.70.1');
expect(result.current?.htmlUrl).toEqual(
'https://github.com/vegaprotocol/vega/releases/tag/v0.70.1'
);
expect(result.current?.name).toEqual('v0.70.1');
expect(result.current?.isDraft).toBeFalsy();
});
});
it('should return undefined when a release cannot be found', async () => {
const { result } = renderHook(() => useVegaRelease('v0.70.1'));
await waitFor(() => {
expect(result.current).toEqual(undefined);
});
});
});
@@ -0,0 +1,9 @@
import { useMemo } from 'react';
import { useVegaReleases } from './use-vega-releases';
export const useVegaRelease = (tag: string, includeDevReleases = false) => {
const { data } = useVegaReleases();
return useMemo(() => {
return data?.find((r) => r.tagName === tag);
}, [data, tag]);
};
@@ -0,0 +1,58 @@
import { renderHook, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import {
GITHUB_VEGA_DEV_RELEASES,
GITHUB_VEGA_RELEASES,
useVegaReleases,
} from './use-vega-releases';
import {
GITHUB_VEGA_RELEASES_DATA,
GITHUB_VEGA_DEV_RELEASES_DATA,
} from './mocks/github-releases';
describe('useVegaReleases', () => {
afterEach(() => {
fetchMock.reset();
});
it('should return an empty list when request is unsuccessful', async () => {
fetchMock.get(GITHUB_VEGA_RELEASES, 404);
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, 404);
const { result } = renderHook(() => useVegaReleases());
expect(result.current.loading).toBeTruthy();
await waitFor(() => {
expect(result.current.loading).toBeFalsy();
expect(result.current.data).toEqual([]);
});
});
it('should return a list of releases', async () => {
fetchMock.get(GITHUB_VEGA_RELEASES, GITHUB_VEGA_RELEASES_DATA);
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_DEV_RELEASES_DATA);
const { result } = renderHook(() => useVegaReleases());
expect(result.current.loading).toBeTruthy();
await waitFor(() => {
expect(result.current.loading).toBeFalsy();
expect(result.current).not.toEqual([]);
const data = result.current.data;
expect(data?.[0].tagName).toEqual('v0.70.3');
expect(data?.[0].htmlUrl).toEqual(
'https://github.com/vegaprotocol/vega/releases/tag/v0.70.3'
);
expect(data?.[1].tagName).toEqual('v0.70.2');
expect(data?.[2].tagName).toEqual('v0.70.1');
});
});
it('should return a list of releases including dev releases', async () => {
fetchMock.get(GITHUB_VEGA_RELEASES, GITHUB_VEGA_RELEASES_DATA);
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_DEV_RELEASES_DATA);
const { result } = renderHook(() => useVegaReleases(true));
await waitFor(() => {
expect(result.current.data?.length).toBeGreaterThan(30);
expect(fetchMock.calls().length).toEqual(2);
});
});
});
@@ -0,0 +1,124 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { useCallback, useEffect, useState } from 'react';
import z from 'zod';
export const GITHUB_VEGA_RELEASES =
'https://api.github.com/repos/vegaprotocol/vega/releases';
export const GITHUB_VEGA_DEV_RELEASES =
'https://api.github.com/repos/vegaprotocol/vega-dev-releases/releases';
const GithubReleaseSchema = z.object({
url: z.string(),
html_url: z.string(),
id: z.number(),
tag_name: z.string(),
name: z.string().nullable(),
draft: z.boolean(),
created_at: z.string().datetime(),
});
const GithubReleasesSchema = z.array(GithubReleaseSchema);
type ReleaseInfo = {
id: number;
name: string;
tagName: string;
htmlUrl: string;
url: string;
isDraft: boolean;
createdAt: Date;
isDevRelease: boolean;
};
const toReleaseInfo = (
releaseData: z.infer<typeof GithubReleaseSchema>,
isDevRelease: boolean
): ReleaseInfo => ({
id: releaseData.id,
name: releaseData.name || '',
tagName: releaseData.tag_name,
htmlUrl: releaseData.html_url,
url: releaseData.url,
isDraft: releaseData.draft,
createdAt: new Date(releaseData.created_at),
isDevRelease,
});
enum ReleaseFeed {
// @ts-ignore TS18033 - allowed in 5.0
Vega = GITHUB_VEGA_RELEASES,
// @ts-ignore TS18033
VegaDev = GITHUB_VEGA_DEV_RELEASES,
}
const fetchReleases = async (feed: ReleaseFeed) => {
const response = await fetch(String(feed), {
cache: 'force-cache',
});
if (response.ok) {
const json = await response.json();
const data = GithubReleasesSchema.parse(json);
return data.map((d) => toReleaseInfo(d, feed === ReleaseFeed.VegaDev));
}
return [];
};
type State = {
loading: boolean;
data: ReleaseInfo[] | null;
error: Error | null | undefined;
};
/**
* Retrieves a list of vega releases from github.
* First element is the newest.
*/
export const useVegaReleases = (includeDevReleases = false) => {
const [state, setState] = useState<State>({
loading: true,
data: null,
error: null,
});
const fetchData = useCallback(() => {
let mounted = true;
Promise.all(
includeDevReleases
? [fetchReleases(ReleaseFeed.Vega), fetchReleases(ReleaseFeed.VegaDev)]
: [fetchReleases(ReleaseFeed.Vega)]
)
.then(([vega, vegaDev]) => {
if (mounted) {
setState({
loading: false,
data: (vegaDev ? vega.concat(vegaDev) : vega).sort(
(a, b) => b.createdAt.valueOf() - a.createdAt.valueOf()
),
error: null,
});
}
})
.catch((err) => {
if (mounted) {
setState({
loading: false,
data: null,
error: err,
});
}
});
return () => {
mounted = false;
};
}, [includeDevReleases, setState]);
useEffect(() => {
fetchData();
}, [fetchData]);
return state;
};
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/react-helpers",
"version": "0.1.0"
"version": "0.2.0"
}
+2 -4
View File
@@ -1,13 +1,11 @@
import { useRef } from 'react';
import { BrowserTracing } from '@sentry/tracing';
import * as Sentry from '@sentry/browser';
import type { LocalLogger } from '@vegaprotocol/utils';
import type { LocalLogger, LoggerConf } from '@vegaprotocol/utils';
import { localLoggerFactory } from '@vegaprotocol/utils';
interface Props {
interface Props extends LoggerConf {
dsn?: string;
application?: string;
tags?: string[];
}
export const useLogger = ({ dsn, ...props }: Props) => {
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/ui-toolkit",
"version": "0.9.0"
"version": "0.11.0"
}
+1
View File
@@ -25,6 +25,7 @@ export * from './nav-dropdown';
export * from './nav';
export * from './navigation';
export * from './notification';
export * from './pagination';
export * from './popover';
export * from './progress-bar';
export * from './radio-group';
@@ -0,0 +1 @@
export * from './pagination';
@@ -0,0 +1,28 @@
import { useState } from 'react';
import { Pagination } from './pagination';
import type { ComponentStory, ComponentMeta } from '@storybook/react';
export default {
title: 'Pagination',
component: Pagination,
} as ComponentMeta<typeof Pagination>;
const Template: ComponentStory<typeof Pagination> = (args) => {
const MAX_PAGE = 3;
const [page, setPage] = useState(1);
return (
<div>
<Pagination
hasPrevPage={page !== 1}
hasNextPage={page < MAX_PAGE}
onBack={() => setPage(Math.max(1, page - 1))}
onNext={() => setPage(Math.min(MAX_PAGE, page + 1))}
>
Page {page}
</Pagination>
</div>
);
};
export const Default = Template.bind({});
@@ -0,0 +1,73 @@
import type { ReactNode } from 'react';
import { Button } from '../button';
import { Icon } from '../icon';
export type PaginationProps = {
hasPrevPage: boolean;
hasNextPage: boolean;
isLoading?: boolean;
children?: ReactNode;
onBack: () => void;
onNext: () => void;
onFirst?: () => void;
onLast?: () => void;
};
const buttonClass = 'rounded-full w-[34px] h-[34px]';
export const Pagination = ({
hasPrevPage,
hasNextPage,
isLoading,
children,
onBack,
onNext,
onFirst,
onLast,
}: PaginationProps) => {
return (
<div className={'flex gap-2 my-2 items-center justify-center'}>
{onFirst && (
<Button
size="sm"
data-testid="goto-first-page"
disabled={isLoading || !hasPrevPage}
className={buttonClass}
onClick={onFirst}
>
<Icon name="double-chevron-left" ariaLabel="Back" />
</Button>
)}
<Button
size="sm"
data-testid="goto-previous-page"
disabled={isLoading || !hasPrevPage}
className={buttonClass}
onClick={onBack}
>
<Icon name="chevron-left" ariaLabel="Back" />
</Button>
{children}
<Button
size="sm"
data-testid="goto-next-page"
disabled={isLoading || !hasNextPage}
className={buttonClass}
onClick={onNext}
>
<Icon name="chevron-right" ariaLabel="Next" />
</Button>
{onLast && (
<Button
size="sm"
data-testid="goto-last-page"
disabled={isLoading || !hasNextPage}
className={buttonClass}
onClick={onLast}
>
<Icon name="double-chevron-right" ariaLabel="Back" />
</Button>
)}
</div>
);
};
+39 -1
View File
@@ -1,4 +1,10 @@
import { truncateByChars, ELLIPSIS, shorten, titlefy } from './strings';
import {
truncateByChars,
ELLIPSIS,
shorten,
titlefy,
stripFullStops,
} from './strings';
describe('truncateByChars', () => {
it.each([
@@ -49,3 +55,35 @@ describe('titlefy', () => {
expect(titlefy(words)).toEqual(o);
});
});
describe('stripFullStops', () => {
describe('stripFullStops', () => {
it('removes full stops from a version string', () => {
const input = 'v0.70.0-12075-8fa496c8';
const expectedResult = 'v0700-12075-8fa496c8';
expect(stripFullStops(input)).toBe(expectedResult);
});
it('does not alter a string without full stops', () => {
const input = 'v0700-12075-8fa496c8';
expect(stripFullStops(input)).toBe(input);
});
it('removes full stops from a string with consecutive full stops', () => {
const input = 'v0..70...0-12075-8fa496c8';
const expectedResult = 'v0700-12075-8fa496c8';
expect(stripFullStops(input)).toBe(expectedResult);
});
it('removes full stops from a string with only full stops', () => {
const input = '..........';
const expectedResult = '';
expect(stripFullStops(input)).toBe(expectedResult);
});
it('does not alter an empty string', () => {
const input = '';
expect(stripFullStops(input)).toBe(input);
});
});
});
+4
View File
@@ -26,3 +26,7 @@ export function titlefy(words: (string | null | undefined)[]) {
.join(TITLE_SEPARATOR);
return title;
}
export function stripFullStops(input: string) {
return input.replace(/\./g, '');
}
+1 -1
View File
@@ -21,7 +21,7 @@ type ConsoleMethod = {
}[keyof Console] &
string;
interface LoggerConf {
export interface LoggerConf {
application?: string;
tags?: string[];
logLevel?: LogLevelsType;