diff --git a/apps/governance/src/hooks/use-refresh-after-epoch.ts b/apps/governance/src/hooks/use-refresh-after-epoch.ts index 539643737..a37643718 100644 --- a/apps/governance/src/hooks/use-refresh-after-epoch.ts +++ b/apps/governance/src/hooks/use-refresh-after-epoch.ts @@ -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(() => { diff --git a/apps/governance/src/lib/epoch-pagination.ts b/apps/governance/src/lib/epoch-pagination.ts new file mode 100644 index 000000000..1da896886 --- /dev/null +++ b/apps/governance/src/lib/epoch-pagination.ts @@ -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, + }; +}; diff --git a/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.spec.tsx b/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.spec.tsx index 4b84c3436..3e34d05ca 100644 --- a/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.spec.tsx +++ b/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.spec.tsx @@ -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', diff --git a/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards.tsx b/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards.tsx index 1dd812357..e70016de7 100644 --- a/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards.tsx +++ b/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards.tsx @@ -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 ( { {t('Connected Vega key')}:{' '} {pubKey}

- {epochIndividualRewardSummaries.length ? ( - epochIndividualRewardSummaries.map( - (epochIndividualRewardSummary) => ( - - ) + {epochIndividualRewardSummaries.map( + (epochIndividualRewardSummary) => ( + ) - ) : ( -

{t('noRewards')}

)} + 1} + hasNextPage={page < totalPages} + onBack={() => refetchData(page - 1)} + onNext={() => refetchData(page + 1)} + onFirst={() => refetchData(1)} + onLast={() => refetchData(totalPages)} + > + {t('Page')} {page} + )} /> diff --git a/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.spec.ts b/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.spec.ts index f0e390136..3f6c1ce18 100644 --- a/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.spec.ts +++ b/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.spec.ts @@ -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', diff --git a/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.ts b/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.ts index 285afa1a2..edf35d72b 100644 --- a/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.ts +++ b/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.ts @@ -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 = 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()); + return acc; + }, map); return Array.from(epochIndividualRewards.values()).sort( (a, b) => Number(b.epoch) - Number(a.epoch) diff --git a/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.spec.tsx b/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.spec.tsx index 6c8cb7394..b06f6a42d 100644 --- a/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.spec.tsx +++ b/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.spec.tsx @@ -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 = 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', () => { diff --git a/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.tsx b/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.tsx index 45f5caa1f..365ab0d03 100644 --- a/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.tsx +++ b/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.tsx @@ -48,17 +48,19 @@ export const EpochTotalRewardsTable = ({ }: EpochTotalRewardsGridProps) => { return ( - {data.assetRewards.map(({ name, rewards, totalAmount }, i) => ( -
-
- {name} + {Array.from(data.assetRewards.values()).map( + ({ name, rewards, totalAmount }, i) => ( +
+
+ {name} +
+ {Array.from(rewards.values()).map(({ rewardType, amount }, i) => ( + + ))} +
- {rewards.map(({ rewardType, amount }, i) => ( - - ))} - -
- ))} + ) + )} ); }; diff --git a/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards.tsx b/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards.tsx index 10410e5b7..f4c8c3ef5 100644 --- a/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards.tsx +++ b/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards.tsx @@ -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 ( { className="max-w-full overflow-auto" data-testid="epoch-rewards-total" > - {epochTotalRewardSummaries.length === 0 ? ( - - ) : ( - <> - {epochTotalRewardSummaries.map((epochTotalSummary, index) => ( - - ))} - + {Array.from(epochTotalRewardSummaries.values()).map( + (epochTotalSummary, index) => ( + + ) )} + 1} + hasNextPage={page < totalPages} + onBack={() => refetchData(page - 1)} + onNext={() => refetchData(page + 1)} + onFirst={() => refetchData(1)} + onLast={() => refetchData(totalPages)} + > + {t('Page')} {page} +
)} /> diff --git a/apps/governance/src/routes/rewards/epoch-total-rewards/generate-epoch-total-rewards-list.spec.ts b/apps/governance/src/routes/rewards/epoch-total-rewards/generate-epoch-total-rewards-list.spec.ts index 51b9d6e31..6d5d114b9 100644 --- a/apps/governance/src/routes/rewards/epoch-total-rewards/generate-epoch-total-rewards-list.spec.ts +++ b/apps/governance/src/routes/rewards/epoch-total-rewards/generate-epoch-total-rewards-list.spec.ts @@ -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', + }, + ], + ]), + }, + ], + ]) + ); }); }); diff --git a/apps/governance/src/routes/rewards/epoch-total-rewards/generate-epoch-total-rewards-list.ts b/apps/governance/src/routes/rewards/epoch-total-rewards/generate-epoch-total-rewards-list.ts index 87c51b010..ddf56cac2 100644 --- a/apps/governance/src/routes/rewards/epoch-total-rewards/generate-epoch-total-rewards-list.ts +++ b/apps/governance/src/routes/rewards/epoch-total-rewards/generate-epoch-total-rewards-list.ts @@ -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; 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 = 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 = 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); }; diff --git a/apps/governance/src/routes/rewards/home/Rewards.graphql b/apps/governance/src/routes/rewards/home/Rewards.graphql index 083231b4d..cb0b3b5cb 100644 --- a/apps/governance/src/routes/rewards/home/Rewards.graphql +++ b/apps/governance/src/routes/rewards/home/Rewards.graphql @@ -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 { diff --git a/apps/governance/src/routes/rewards/home/__generated__/Rewards.ts b/apps/governance/src/routes/rewards/home/__generated__/Rewards.ts index 42f28ba2c..d3caf2905 100644 --- a/apps/governance/src/routes/rewards/home/__generated__/Rewards.ts +++ b/apps/governance/src/routes/rewards/home/__generated__/Rewards.ts @@ -9,21 +9,24 @@ export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: stri export type RewardsQueryVariables = Types.Exact<{ partyId: Types.Scalars['ID']; - delegationsPagination?: Types.InputMaybe; + fromEpoch?: Types.InputMaybe; + toEpoch?: Types.InputMaybe; rewardsPagination?: Types.InputMaybe; + delegationsPagination?: Types.InputMaybe; }>; -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; epochRewardSummariesPagination?: Types.InputMaybe; }>; -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; export type RewardsLazyQueryHookResult = ReturnType; export type RewardsQueryResult = Apollo.QueryResult; 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' * }, * }); diff --git a/apps/governance/src/routes/rewards/home/rewards-page.tsx b/apps/governance/src/routes/rewards/home/rewards-page.tsx index 3d79167a7..d0e92bdc5 100644 --- a/apps/governance/src/routes/rewards/home/rewards-page.tsx +++ b/apps/governance/src/routes/rewards/home/rewards-page.tsx @@ -136,11 +136,15 @@ export const RewardsPage = () => { {toggleRewardsView === 'total' ? ( - + epochData?.epoch ? ( + + ) : null ) : (
{pubKey && pubKeys?.length ? ( - + epochData?.epoch ? ( + + ) : null ) : ( )} diff --git a/libs/ui-toolkit/src/components/index.ts b/libs/ui-toolkit/src/components/index.ts index 897d3ee59..a62a1eba3 100644 --- a/libs/ui-toolkit/src/components/index.ts +++ b/libs/ui-toolkit/src/components/index.ts @@ -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'; diff --git a/libs/ui-toolkit/src/components/pagination/index.tsx b/libs/ui-toolkit/src/components/pagination/index.tsx new file mode 100644 index 000000000..cb727655b --- /dev/null +++ b/libs/ui-toolkit/src/components/pagination/index.tsx @@ -0,0 +1 @@ +export * from './pagination'; diff --git a/libs/ui-toolkit/src/components/pagination/pagination.stories.tsx b/libs/ui-toolkit/src/components/pagination/pagination.stories.tsx new file mode 100644 index 000000000..3d85cabb7 --- /dev/null +++ b/libs/ui-toolkit/src/components/pagination/pagination.stories.tsx @@ -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; + +const Template: ComponentStory = (args) => { + const MAX_PAGE = 3; + const [page, setPage] = useState(1); + + return ( +
+ setPage(Math.max(1, page - 1))} + onNext={() => setPage(Math.min(MAX_PAGE, page + 1))} + > + Page {page} + +
+ ); +}; + +export const Default = Template.bind({}); diff --git a/libs/ui-toolkit/src/components/pagination/pagination.tsx b/libs/ui-toolkit/src/components/pagination/pagination.tsx new file mode 100644 index 000000000..cf824a626 --- /dev/null +++ b/libs/ui-toolkit/src/components/pagination/pagination.tsx @@ -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 ( +
+ {onFirst && ( + + )} + + {children} + + {onLast && ( + + )} +
+ ); +};