feat(governance): paginate rewards (#3391)

This commit is contained in:
botond 2023-04-18 15:40:57 +02:00 committed by GitHub
parent ec12c7ecfd
commit 4d266963ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1190 additions and 371 deletions

View File

@ -1,9 +1,8 @@
import type { ObservableQuery } from '@apollo/client';
import { useEffect } from 'react'; import { useEffect } from 'react';
export const useRefreshAfterEpoch = ( export const useRefreshAfterEpoch = (
epochExpiry: string | undefined, epochExpiry: string | undefined,
refetch: ObservableQuery['refetch'] refetch: () => void
) => { ) => {
return useEffect(() => { return useEffect(() => {
const epochInterval = setInterval(() => { const epochInterval = setInterval(() => {

View File

@ -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,
};
};

View File

@ -3,7 +3,7 @@ import { AppStateProvider } from '../../../contexts/app-state/app-state-provider
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table'; import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
const mockData = { const mockData = {
epoch: '4441', epoch: 4441,
rewards: [ rewards: [
{ {
asset: 'tDAI', asset: 'tDAI',

View File

@ -1,32 +1,43 @@
import { useMemo } from 'react'; import { useMemo, useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit';
import { removePaginationWrapper } from '@vegaprotocol/utils'; import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { EpochFieldsFragment } from '../home/__generated__/Rewards';
import { useRewardsQuery } from '../home/__generated__/Rewards'; import { useRewardsQuery } from '../home/__generated__/Rewards';
import { ENV } from '../../../config'; import { ENV } from '../../../config';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table'; import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list'; 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 { t } = useTranslation();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV; const { delegationsPagination } = ENV;
const { data, loading, error } = useRewardsQuery({ const { data, loading, error, refetch } = useRewardsQuery({
notifyOnNetworkStatusChange: true,
variables: { variables: {
partyId: pubKey || '', partyId: pubKey || '',
fromEpoch: epochId - EPOCHS_PAGE_SIZE,
toEpoch: epochId,
delegationsPagination: delegationsPagination delegationsPagination: delegationsPagination
? { ? {
first: Number(delegationsPagination), first: Number(delegationsPagination),
} }
: undefined, : undefined,
// we can use the same value for rewardsPagination as delegationsPagination
rewardsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
}, },
skip: !pubKey, skip: !pubKey,
}); });
@ -39,8 +50,37 @@ export const EpochIndividualRewards = () => {
const epochIndividualRewardSummaries = useMemo(() => { const epochIndividualRewardSummaries = useMemo(() => {
if (!data?.party) return []; if (!data?.party) return [];
return generateEpochIndividualRewardsList(rewards); return generateEpochIndividualRewardsList({
}, [data?.party, rewards]); 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 ( return (
<AsyncRenderer <AsyncRenderer
@ -53,17 +93,24 @@ export const EpochIndividualRewards = () => {
{t('Connected Vega key')}:{' '} {t('Connected Vega key')}:{' '}
<span className="text-white">{pubKey}</span> <span className="text-white">{pubKey}</span>
</p> </p>
{epochIndividualRewardSummaries.length ? ( {epochIndividualRewardSummaries.map(
epochIndividualRewardSummaries.map( (epochIndividualRewardSummary) => (
(epochIndividualRewardSummary) => ( <EpochIndividualRewardsTable
<EpochIndividualRewardsTable data={epochIndividualRewardSummary}
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> </div>
)} )}
/> />

View File

@ -43,6 +43,16 @@ describe('generateEpochIndividualRewardsList', () => {
epoch: { id: '1' }, 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 = { const rewardWrongType: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE, rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '50', amount: '50',
@ -54,20 +64,38 @@ describe('generateEpochIndividualRewardsList', () => {
}; };
it('should return an empty array if no rewards are provided', () => { 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', () => { 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', () => { it('should return reward in the correct format', () => {
const result = generateEpochIndividualRewardsList([reward1]); const result = generateEpochIndividualRewardsList({
rewards: [reward1],
epochId: 1,
});
expect(result[0]).toEqual({ expect(result[0]).toEqual({
epoch: '1', epoch: 1,
rewards: [ rewards: [
{ {
asset: 'USD', asset: 'USD',
@ -105,21 +133,24 @@ describe('generateEpochIndividualRewardsList', () => {
it('should return an array sorted by epoch descending', () => { it('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2, reward3, reward4]; const rewards = [reward1, reward2, reward3, reward4];
const result1 = generateEpochIndividualRewardsList(rewards); const result1 = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
expect(result1[0].epoch).toEqual('2'); expect(result1[0].epoch).toEqual(2);
expect(result1[1].epoch).toEqual('1'); expect(result1[1].epoch).toEqual(1);
const reorderedRewards = [reward4, reward3, reward2, reward1]; 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[0].epoch).toEqual(2);
expect(result2[1].epoch).toEqual('1'); expect(result2[1].epoch).toEqual(1);
}); });
it('correctly calculates the total value of rewards for an asset', () => { it('correctly calculates the total value of rewards for an asset', () => {
const rewards = [reward1, reward4]; const rewards = [reward1, reward4];
const result = generateEpochIndividualRewardsList(rewards); const result = generateEpochIndividualRewardsList({ rewards, epochId: 1 });
expect(result[0].rewards[0].totalAmount).toEqual('200'); expect(result[0].rewards[0].totalAmount).toEqual('200');
}); });
@ -127,11 +158,11 @@ describe('generateEpochIndividualRewardsList', () => {
it('returns data in the expected shape', () => { it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here // Just sanity checking the whole structure here
const rewards = [reward1, reward2, reward3, reward4]; const rewards = [reward1, reward2, reward3, reward4];
const result = generateEpochIndividualRewardsList(rewards); const result = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
expect(result).toEqual([ expect(result).toEqual([
{ {
epoch: '2', epoch: 2,
rewards: [ rewards: [
{ {
asset: 'GBP', 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: [ rewards: [
{ {
asset: 'USD', asset: 'USD',

View File

@ -2,9 +2,10 @@ import { BigNumber } from '../../../lib/bignumber';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets'; import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { RewardFieldsFragment } from '../home/__generated__/Rewards'; import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
import type { AccountType } from '@vegaprotocol/types'; import type { AccountType } from '@vegaprotocol/types';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
export interface EpochIndividualReward { export interface EpochIndividualReward {
epoch: string; epoch: number;
rewards: { rewards: {
asset: string; asset: string;
totalAmount: string; totalAmount: string;
@ -27,11 +28,29 @@ const emptyRowAccountTypes = accountTypes.map((type) => [
}, },
]); ]);
export const generateEpochIndividualRewardsList = ( export const generateEpochIndividualRewardsList = ({
rewards: RewardFieldsFragment[] 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. // 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 epochId = reward.epoch.id;
const assetName = reward.asset.name; const assetName = reward.asset.name;
const rewardType = reward.rewardType; 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 the rewardType is not of a type we display in the table, we skip it.
if (!accountTypes.includes(rewardType)) { if (!accountTypes.includes(rewardType)) {
return map; return acc;
} }
if (!map.has(epochId)) { if (!acc.has(epochId)) {
map.set(epochId, { epoch: epochId, rewards: [] }); return acc;
} }
const epoch = map.get(epochId); const epoch = acc.get(epochId);
let asset = epoch?.rewards.find((r) => r.asset === assetName); let asset = epoch?.rewards.find((r) => r.asset === assetName);
@ -76,8 +95,8 @@ export const generateEpochIndividualRewardsList = (
}); });
} }
return map; return acc;
}, new Map<string, EpochIndividualReward>()); }, map);
return Array.from(epochIndividualRewards.values()).sort( return Array.from(epochIndividualRewards.values()).sort(
(a, b) => Number(b.epoch) - Number(a.epoch) (a, b) => Number(b.epoch) - Number(a.epoch)

View File

@ -1,44 +1,64 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table'; import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
import type {
AggregatedEpochRewardSummary,
RewardType,
RewardItem,
} from './generate-epoch-total-rewards-list';
import { AccountType } from '@vegaprotocol/types'; 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 = { const mockData = {
epoch: 4431, epoch: 4431,
assetRewards: [ 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',
},
],
}; };
describe('EpochTotalRewardsTable', () => { describe('EpochTotalRewardsTable', () => {

View File

@ -48,17 +48,19 @@ export const EpochTotalRewardsTable = ({
}: EpochTotalRewardsGridProps) => { }: EpochTotalRewardsGridProps) => {
return ( return (
<RewardsTable dataTestId="epoch-total-rewards-table" epoch={data.epoch}> <RewardsTable dataTestId="epoch-total-rewards-table" epoch={data.epoch}>
{data.assetRewards.map(({ name, rewards, totalAmount }, i) => ( {Array.from(data.assetRewards.values()).map(
<div className="contents" key={i}> ({ name, rewards, totalAmount }, i) => (
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}> <div className="contents" key={i}>
{name} <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> </div>
{rewards.map(({ rewardType, amount }, i) => ( )
<RewardItem key={i} dataTestId={rewardType} value={amount} /> )}
))}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
))}
</RewardsTable> </RewardsTable>
); );
}; };

View File

@ -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 { useEpochAssetsRewardsQuery } from '../home/__generated__/Rewards';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { generateEpochTotalRewardsList } from './generate-epoch-total-rewards-list'; import { generateEpochTotalRewardsList } from './generate-epoch-total-rewards-list';
import { NoRewards } from '../no-rewards';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table'; 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({ const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
notifyOnNetworkStatusChange: true,
variables: { variables: {
epochRewardSummariesPagination: { epochRewardSummariesFilter: {
first: 10, 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 ( return (
<AsyncRenderer <AsyncRenderer
@ -27,15 +68,22 @@ export const EpochTotalRewards = () => {
className="max-w-full overflow-auto" className="max-w-full overflow-auto"
data-testid="epoch-rewards-total" data-testid="epoch-rewards-total"
> >
{epochTotalRewardSummaries.length === 0 ? ( {Array.from(epochTotalRewardSummaries.values()).map(
<NoRewards /> (epochTotalSummary, index) => (
) : ( <EpochTotalRewardsTable data={epochTotalSummary} key={index} />
<> )
{epochTotalRewardSummaries.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> </div>
)} )}
/> />

View File

@ -3,13 +3,23 @@ import { AccountType } from '@vegaprotocol/types';
describe('generateEpochAssetRewardsList', () => { describe('generateEpochAssetRewardsList', () => {
it('should return an empty array if data is undefined', () => { 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', () => { it('should return an empty map if empty data is provided', () => {
const epochData = { const data = {
assetsConnection: { assetsConnection: {
edges: [], 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', () => { it('should return an empty map if no epochRewardSummaries are provided', () => {
const epochData = { const data = {
assetsConnection: { assetsConnection: {
edges: [ 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)', () => { it('should return a map of unnamed assets if no asset names are provided (should not happen)', () => {
const epochData = { const data = {
assetsConnection: { assetsConnection: {
edges: [], edges: [],
}, },
@ -85,50 +115,80 @@ describe('generateEpochAssetRewardsList', () => {
}, },
}; };
const result = generateEpochTotalRewardsList(epochData); const result = generateEpochTotalRewardsList({ data, epochId: 1 });
expect(result).toEqual([ expect(result).toEqual(
{ new Map([
epoch: 1, [
assetRewards: [ '1',
{ {
assetId: '1', epoch: 1,
name: '', assetRewards: new Map([
rewards: [ [
{ '1',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD, {
amount: '123', assetId: '1',
}, name: '',
{ rewards: new Map([
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE, [
amount: '0', AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
}, {
{ rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES, amount: '123',
amount: '0', },
}, ],
{ [
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES, AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0', {
}, rewardType:
{ AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY, amount: '0',
amount: '0', },
}, ],
{ [
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS, AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0', {
}, rewardType:
], AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
totalAmount: '123', 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', () => { it('should return the aggregated epoch summaries', () => {
const epochData = { const data = {
assetsConnection: { assetsConnection: {
edges: [ edges: [
{ {
@ -180,81 +240,425 @@ describe('generateEpochAssetRewardsList', () => {
}, },
}; };
const result = generateEpochTotalRewardsList(epochData); const result = generateEpochTotalRewardsList({ data, epochId: 2 });
expect(result).toEqual([ expect(result).toEqual(
{ new Map([
epoch: 1, [
assetRewards: [ '1',
{ {
assetId: '1', epoch: 1,
name: 'Asset 1', assetRewards: new Map([
rewards: [ [
{ '1',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD, {
amount: '0', assetId: '1',
}, name: 'Asset 1',
{ rewards: new Map([
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE, [
amount: '100', AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
}, {
{ rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES, amount: '0',
amount: '123', },
}, ],
{ [
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES, AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0', {
}, rewardType:
{ AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY, amount: '100',
amount: '0', },
}, ],
{ [
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS, AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0', {
}, rewardType:
], AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
totalAmount: '223', 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',
},
}, },
], ],
}, },
{ epochRewardSummaries: {
epoch: 2, edges: [
assetRewards: [
{ {
assetId: '1', node: {
name: 'Asset 1', epoch: 1,
rewards: [ assetId: '1',
{ rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD, amount: '123',
amount: '0', },
}, },
{ {
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE, node: {
amount: '0', epoch: 1,
}, assetId: '1',
{ rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES, amount: '100',
amount: '0', },
}, },
{ {
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES, node: {
amount: '0', epoch: 2,
}, assetId: '1',
{ rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY, amount: '6',
amount: '5', },
}, },
{ {
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS, node: {
amount: '0', epoch: 2,
}, assetId: '1',
], rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
totalAmount: '5', 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',
},
],
]),
},
],
])
);
}); });
}); });

View File

@ -5,127 +5,97 @@ import type {
import { removePaginationWrapper } from '@vegaprotocol/utils'; import { removePaginationWrapper } from '@vegaprotocol/utils';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets'; import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { AccountType } from '@vegaprotocol/types'; import type { AccountType } from '@vegaprotocol/types';
import { BigNumber } from '../../../lib/bignumber'; import { calculateEpochOffset } from '../../../lib/epoch-pagination';
interface EpochSummaryWithNamedReward extends EpochRewardSummaryFieldsFragment { interface EpochSummaryWithNamedReward extends EpochRewardSummaryFieldsFragment {
name: string; name: string;
} }
export interface AggregatedEpochRewardSummary { export type RewardType = EpochRewardSummaryFieldsFragment['rewardType'];
export type RewardItem = Pick<
EpochRewardSummaryFieldsFragment,
'rewardType' | 'amount'
>;
export type AggregatedEpochRewardSummary = {
assetId: EpochRewardSummaryFieldsFragment['assetId']; assetId: EpochRewardSummaryFieldsFragment['assetId'];
name: EpochSummaryWithNamedReward['name']; name: EpochSummaryWithNamedReward['name'];
rewards: { rewards: Map<RewardType, RewardItem>;
rewardType: EpochRewardSummaryFieldsFragment['rewardType'];
amount: EpochRewardSummaryFieldsFragment['amount'];
}[];
totalAmount: string; totalAmount: string;
} };
export interface EpochTotalSummary { export type EpochTotalSummary = {
epoch: EpochRewardSummaryFieldsFragment['epoch']; epoch: EpochRewardSummaryFieldsFragment['epoch'];
assetRewards: AggregatedEpochRewardSummary[]; assetRewards: Map<
} EpochRewardSummaryFieldsFragment['assetId'],
AggregatedEpochRewardSummary
>;
};
const emptyRowAccountTypes = Object.keys(RowAccountTypes).map((type) => ({ const emptyRowAccountTypes: Map<RewardType, RewardItem> = new Map();
rewardType: type as AccountType,
amount: '0',
}));
export const generateEpochTotalRewardsList = ( Object.keys(RowAccountTypes).forEach((type) => {
epochData: EpochAssetsRewardsQuery | undefined 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( 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 map: Map<string, EpochTotalSummary> = new Map();
const epochSummariesWithNamedReward: EpochSummaryWithNamedReward[] = const { fromEpoch, toEpoch } = calculateEpochOffset({ epochId, page, size });
epochRewardSummaries.map((epochReward) => ({
...epochReward,
name:
assets.find((asset) => asset.id === epochReward.assetId)?.name || '',
}));
// Aggregating the epoch summaries by epoch number for (let i = toEpoch; i >= fromEpoch; i--) {
const aggregatedEpochSummariesByEpochNumber = map.set(i.toString(), {
epochSummariesWithNamedReward.reduce((acc, epochReward) => { epoch: i,
const epoch = epochReward.epoch; assetRewards: new Map(),
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);
}),
};
}); });
}
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);
}; };

View File

@ -23,12 +23,18 @@ fragment DelegationFields on Delegation {
query Rewards( query Rewards(
$partyId: ID! $partyId: ID!
$delegationsPagination: Pagination $fromEpoch: Int
$toEpoch: Int
$rewardsPagination: Pagination $rewardsPagination: Pagination
$delegationsPagination: Pagination
) { ) {
party(id: $partyId) { party(id: $partyId) {
id id
rewardsConnection(pagination: $rewardsPagination) { rewardsConnection(
fromEpoch: $fromEpoch
toEpoch: $toEpoch
pagination: $rewardsPagination
) {
edges { edges {
node { node {
...RewardFields ...RewardFields
@ -43,14 +49,6 @@ query Rewards(
} }
} }
} }
epoch {
id
timestamps {
start
end
expiry
}
}
} }
fragment EpochRewardSummaryFields on EpochRewardSummary { fragment EpochRewardSummaryFields on EpochRewardSummary {
@ -60,7 +58,10 @@ fragment EpochRewardSummaryFields on EpochRewardSummary {
rewardType rewardType
} }
query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) { query EpochAssetsRewards(
$epochRewardSummariesFilter: RewardSummaryFilter
$epochRewardSummariesPagination: Pagination
) {
assetsConnection { assetsConnection {
edges { edges {
node { node {
@ -69,18 +70,16 @@ query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) {
} }
} }
} }
epochRewardSummaries(pagination: $epochRewardSummariesPagination) { epochRewardSummaries(
filter: $epochRewardSummariesFilter
pagination: $epochRewardSummariesPagination
) {
edges { edges {
node { node {
...EpochRewardSummaryFields ...EpochRewardSummaryFields
} }
} }
} }
epoch {
timestamps {
expiry
}
}
} }
fragment EpochFields on Epoch { fragment EpochFields on Epoch {

View File

@ -9,21 +9,24 @@ export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: stri
export type RewardsQueryVariables = Types.Exact<{ export type RewardsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID']; 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>; 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 EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
export type EpochAssetsRewardsQueryVariables = Types.Exact<{ export type EpochAssetsRewardsQueryVariables = Types.Exact<{
epochRewardSummariesFilter?: Types.InputMaybe<Types.RewardSummaryFilter>;
epochRewardSummariesPagination?: Types.InputMaybe<Types.Pagination>; 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 } }; 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` 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) { party(id: $partyId) {
id id
rewardsConnection(pagination: $rewardsPagination) { rewardsConnection(
fromEpoch: $fromEpoch
toEpoch: $toEpoch
pagination: $rewardsPagination
) {
edges { edges {
node { node {
...RewardFields ...RewardFields
@ -94,14 +101,6 @@ export const RewardsDocument = gql`
} }
} }
} }
epoch {
id
timestamps {
start
end
expiry
}
}
} }
${RewardFieldsFragmentDoc} ${RewardFieldsFragmentDoc}
${DelegationFieldsFragmentDoc}`; ${DelegationFieldsFragmentDoc}`;
@ -119,8 +118,10 @@ ${DelegationFieldsFragmentDoc}`;
* const { data, loading, error } = useRewardsQuery({ * const { data, loading, error } = useRewardsQuery({
* variables: { * variables: {
* partyId: // value for 'partyId' * partyId: // value for 'partyId'
* delegationsPagination: // value for 'delegationsPagination' * fromEpoch: // value for 'fromEpoch'
* toEpoch: // value for 'toEpoch'
* rewardsPagination: // value for 'rewardsPagination' * 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 RewardsLazyQueryHookResult = ReturnType<typeof useRewardsLazyQuery>;
export type RewardsQueryResult = Apollo.QueryResult<RewardsQuery, RewardsQueryVariables>; export type RewardsQueryResult = Apollo.QueryResult<RewardsQuery, RewardsQueryVariables>;
export const EpochAssetsRewardsDocument = gql` export const EpochAssetsRewardsDocument = gql`
query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) { query EpochAssetsRewards($epochRewardSummariesFilter: RewardSummaryFilter, $epochRewardSummariesPagination: Pagination) {
assetsConnection { assetsConnection {
edges { edges {
node { node {
@ -145,18 +146,16 @@ export const EpochAssetsRewardsDocument = gql`
} }
} }
} }
epochRewardSummaries(pagination: $epochRewardSummariesPagination) { epochRewardSummaries(
filter: $epochRewardSummariesFilter
pagination: $epochRewardSummariesPagination
) {
edges { edges {
node { node {
...EpochRewardSummaryFields ...EpochRewardSummaryFields
} }
} }
} }
epoch {
timestamps {
expiry
}
}
} }
${EpochRewardSummaryFieldsFragmentDoc}`; ${EpochRewardSummaryFieldsFragmentDoc}`;
@ -172,6 +171,7 @@ export const EpochAssetsRewardsDocument = gql`
* @example * @example
* const { data, loading, error } = useEpochAssetsRewardsQuery({ * const { data, loading, error } = useEpochAssetsRewardsQuery({
* variables: { * variables: {
* epochRewardSummariesFilter: // value for 'epochRewardSummariesFilter'
* epochRewardSummariesPagination: // value for 'epochRewardSummariesPagination' * epochRewardSummariesPagination: // value for 'epochRewardSummariesPagination'
* }, * },
* }); * });

View File

@ -136,11 +136,15 @@ export const RewardsPage = () => {
</section> </section>
{toggleRewardsView === 'total' ? ( {toggleRewardsView === 'total' ? (
<EpochTotalRewards /> epochData?.epoch ? (
<EpochTotalRewards currentEpoch={epochData?.epoch} />
) : null
) : ( ) : (
<section> <section>
{pubKey && pubKeys?.length ? ( {pubKey && pubKeys?.length ? (
<EpochIndividualRewards /> epochData?.epoch ? (
<EpochIndividualRewards currentEpoch={epochData?.epoch} />
) : null
) : ( ) : (
<ConnectToSeeRewards /> <ConnectToSeeRewards />
)} )}

View File

@ -25,6 +25,7 @@ export * from './nav-dropdown';
export * from './nav'; export * from './nav';
export * from './navigation'; export * from './navigation';
export * from './notification'; export * from './notification';
export * from './pagination';
export * from './popover'; export * from './popover';
export * from './progress-bar'; export * from './progress-bar';
export * from './radio-group'; export * from './radio-group';

View File

@ -0,0 +1 @@
export * from './pagination';

View File

@ -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({});

View File

@ -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>
);
};