feat(governance,ui-toolkit): individual rewards table (#2928)

This commit is contained in:
Sam Keen 2023-02-20 14:30:11 +00:00 committed by GitHub
parent 4b83a10475
commit ac53b1f97a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 892 additions and 406 deletions

View File

@ -1,7 +1,6 @@
import type { Event } from 'ethers';
import uniqBy from 'lodash/uniqBy'; import uniqBy from 'lodash/uniqBy';
import { create } from 'zustand';
import create from 'zustand'; import type { Event } from 'ethers';
export type PendingTxsStore = { export type PendingTxsStore = {
pendingBalances: Event[]; pendingBalances: Event[];

View File

@ -432,6 +432,7 @@
"associatedWithVegaKeys": "Associated with Vega keys", "associatedWithVegaKeys": "Associated with Vega keys",
"thisEpoch": "This Epoch", "thisEpoch": "This Epoch",
"nextEpoch": "Next epoch", "nextEpoch": "Next epoch",
"toSeeYourRewardsConnectYourWallet": "TO SEE YOUR REWARDS, CONNECT YOUR WALLET",
"rewardsIntro": "Earn rewards and infrastructure fees for trading and maintaining the network.", "rewardsIntro": "Earn rewards and infrastructure fees for trading and maintaining the network.",
"rewardsCallout": "Rewards are credited {{duration}} after the epoch ends.", "rewardsCallout": "Rewards are credited {{duration}} after the epoch ends.",
"rewardsCalloutDetail": "This delay is set by a network parameter", "rewardsCalloutDetail": "This delay is set by a network parameter",
@ -457,6 +458,7 @@
"rewardsColMarketCreationHeader": "MARKET CREATION", "rewardsColMarketCreationHeader": "MARKET CREATION",
"rewardsColMarketCreationTooltip": "Market creation rewards are paid out to the creator of any market that exceeds a set threshold of cumulative volume in a given epoch, currently [rewards.marketCreationQuantumMultiple]", "rewardsColMarketCreationTooltip": "Market creation rewards are paid out to the creator of any market that exceeds a set threshold of cumulative volume in a given epoch, currently [rewards.marketCreationQuantumMultiple]",
"rewardsColTotalHeader": "TOTAL", "rewardsColTotalHeader": "TOTAL",
"ofTotalDistributed": "of total distributed",
"checkBackSoon": "Check back soon", "checkBackSoon": "Check back soon",
"yourStake": "Your stake", "yourStake": "Your stake",
"reward": "Reward", "reward": "Reward",

View File

@ -0,0 +1,25 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../contexts/app-state/app-state-provider';
import { ConnectToSeeRewards } from './connect-to-see-rewards';
describe('ConnectToSeeRewards', () => {
it('should render button correctly', () => {
const { getByTestId } = render(
<AppStateProvider>
<ConnectToSeeRewards />
</AppStateProvider>
);
expect(getByTestId('connect-to-vega-wallet-btn')).toBeInTheDocument();
});
it('should render the correct text', () => {
const { getByText } = render(
<AppStateProvider>
<ConnectToSeeRewards />
</AppStateProvider>
);
expect(
getByText('TO SEE YOUR REWARDS, CONNECT YOUR WALLET')
).toBeInTheDocument();
});
});

View File

@ -0,0 +1,40 @@
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Button } from '@vegaprotocol/ui-toolkit';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { SubHeading } from '../../components/heading';
export const ConnectToSeeRewards = () => {
const { appDispatch } = useAppState();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { t } = useTranslation();
const classes = classNames(
'flex flex-col items-center justify-center h-[300px] w-full',
'border border-vega-dark-200'
);
return (
<div className={classes}>
<SubHeading title={t('toSeeYourRewardsConnectYourWallet')} />
<Button
data-testid="connect-to-vega-wallet-btn"
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
{t('connectVegaWallet')}
</Button>
</div>
);
};

View File

@ -1,79 +0,0 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
import { BigNumber } from '../../../lib/bignumber';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import { format } from 'date-fns';
import { DATE_FORMAT_DETAILED } from '../../../lib/date-formats';
import type {
DelegationFieldsFragment,
RewardFieldsFragment,
} from '../home/__generated__/Rewards';
interface RewardTableProps {
reward: RewardFieldsFragment;
delegations: DelegationFieldsFragment[] | [];
}
export const RewardTable = ({ reward, delegations }: RewardTableProps) => {
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
// Get your stake for epoch in which you have rewards
const stakeForEpoch = useMemo(() => {
if (!delegations.length) return '0';
const delegationsForEpoch = delegations
.filter((d) => d.epoch.toString() === reward.epoch.id)
.map((d) => toBigNum(d.amount, decimals));
if (delegationsForEpoch.length) {
return BigNumber.sum.apply(null, [
new BigNumber(0),
...delegationsForEpoch,
]);
}
return new BigNumber(0);
}, [decimals, delegations, reward.epoch.id]);
return (
<div className="mb-24">
<h3 className="text-lg text-white mb-4">
{t('Epoch')} {reward.epoch.id}
</h3>
<KeyValueTable>
<KeyValueTableRow>
{t('rewardType')}
<span>{reward.rewardType}</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('yourStake')}
<span>{stakeForEpoch.toString()}</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('reward')}
<span>
{formatNumber(toBigNum(reward.amount, decimals))}{' '}
{reward.asset.symbol}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('shareOfReward')}
<span>
{new BigNumber(reward.percentageOfTotal).dp(2).toString()}%
</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('received')}
<span>
{format(new Date(reward.receivedAt), DATE_FORMAT_DETAILED)}
</span>
</KeyValueTableRow>
</KeyValueTable>
</div>
);
};

View File

@ -0,0 +1,63 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
const mockData = {
epoch: '4441',
rewards: [
{
asset: 'tDAI',
totalAmount: '5',
rewardTypes: {
ACCOUNT_TYPE_GLOBAL_REWARD: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_FEES_INFRASTRUCTURE: {
amount: '5',
percentageOfTotal: '0.00305237260923',
},
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_FEES_LIQUIDITY: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
};
describe('EpochIndividualRewardsTable', () => {
it('should render correctly', () => {
const { getByTestId } = render(
<AppStateProvider>
<EpochIndividualRewardsTable data={mockData} />
</AppStateProvider>
);
expect(getByTestId('epoch-individual-rewards-table')).toBeInTheDocument();
expect(getByTestId('individual-rewards-asset')).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD')).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES')
).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
).toBeInTheDocument();
});
});

View File

@ -0,0 +1,111 @@
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import {
rowGridItemStyles,
RewardsTable,
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { EpochIndividualReward } from './generate-epoch-individual-rewards-list';
import { useTranslation } from 'react-i18next';
interface EpochIndividualRewardsGridProps {
data: EpochIndividualReward;
}
interface RewardItemProps {
value: string;
percentageOfTotal?: string;
dataTestId: string;
last?: boolean;
}
const DisplayReward = ({
reward,
percentageOfTotal,
}: {
reward: string;
percentageOfTotal?: string;
}) => {
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
if (Number(reward) === 0) {
return <span className="text-vega-dark-300">-</span>;
}
return (
<Tooltip
description={
<div className="flex flex-col items-start">
<span>{formatNumber(toBigNum(reward, decimals), decimals)}</span>
{percentageOfTotal && (
<span className="text-vega-dark-300">
({percentageOfTotal}% {t('ofTotalDistributed')})
</span>
)}
</div>
}
>
<button>
<div className="flex flex-col items-start">
<span>{formatNumber(toBigNum(reward, decimals))}</span>
{percentageOfTotal && (
<span className="text-vega-dark-300">
({formatNumber(toBigNum(percentageOfTotal, 4)).toString()}%)
</span>
)}
</div>
</button>
</Tooltip>
);
};
const RewardItem = ({
value,
percentageOfTotal,
dataTestId,
last,
}: RewardItemProps) => (
<div data-testid={dataTestId} className={rowGridItemStyles(last)}>
<div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
<div className="overflow-auto p-5">
<DisplayReward reward={value} percentageOfTotal={percentageOfTotal} />
</div>
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
</div>
);
export const EpochIndividualRewardsTable = ({
data,
}: EpochIndividualRewardsGridProps) => {
return (
<RewardsTable
dataTestId="epoch-individual-rewards-table"
epoch={Number(data.epoch)}
>
{data.rewards.map(({ asset, rewardTypes, totalAmount }, i) => (
<div className="contents" key={i}>
<div
data-testid="individual-rewards-asset"
className={`${rowGridItemStyles()} p-5`}
>
{asset}
</div>
{Object.entries(rewardTypes).map(
([key, { amount, percentageOfTotal }]) => (
<RewardItem
key={key}
value={amount}
percentageOfTotal={percentageOfTotal}
dataTestId={key}
/>
)
)}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
))}
</RewardsTable>
);
};

View File

@ -5,9 +5,10 @@ import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
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 { RewardTable } from './reward-table'; import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
export const RewardInfo = () => { export const EpochIndividualRewards = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV; const { delegationsPagination } = ENV;
@ -30,13 +31,10 @@ export const RewardInfo = () => {
return removePaginationWrapper(data.party.rewardsConnection.edges); return removePaginationWrapper(data.party.rewardsConnection.edges);
}, [data]); }, [data]);
const delegations = useMemo(() => { const epochIndividualRewardSummaries = useMemo(() => {
if (!data?.party || !data.party.delegationsConnection?.edges?.length) { if (!data?.party) return [];
return []; return generateEpochIndividualRewardsList(rewards);
} }, [data?.party, rewards]);
return removePaginationWrapper(data.party.delegationsConnection.edges);
}, [data]);
return ( return (
<AsyncRenderer <AsyncRenderer
@ -45,20 +43,18 @@ export const RewardInfo = () => {
data={data} data={data}
render={() => ( render={() => (
<div> <div>
<p> <p className="mb-10">
{t('Connected Vega key')}: {pubKey} {t('Connected Vega key')}:{' '}
<span className="text-white">{pubKey}</span>
</p> </p>
{rewards.length ? ( {epochIndividualRewardSummaries.length ? (
rewards.map((reward, i) => { epochIndividualRewardSummaries.map(
if (!reward) return null; (epochIndividualRewardSummary) => (
return ( <EpochIndividualRewardsTable
<RewardTable data={epochIndividualRewardSummary}
key={i}
reward={reward}
delegations={delegations || []}
/> />
); )
}) )
) : ( ) : (
<p>{t('noRewards')}</p> <p>{t('noRewards')}</p>
)} )}

View File

@ -0,0 +1,235 @@
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
import { AccountType } from '@vegaprotocol/types';
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
describe('generateEpochIndividualRewardsList', () => {
const reward1: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD' },
party: { id: 'blah' },
epoch: { id: '1' },
};
const reward2: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '50',
percentageOfTotal: '0.05',
receivedAt: new Date(),
asset: { id: 'eur', symbol: 'EUR' },
party: { id: 'blah' },
epoch: { id: '2' },
};
const reward3: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'gbp', symbol: 'GBP' },
party: { id: 'blah' },
epoch: { id: '2' },
};
const reward4: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD' },
party: { id: 'blah' },
epoch: { id: '1' },
};
const rewardWrongType: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '50',
percentageOfTotal: '0.05',
receivedAt: new Date(),
asset: { id: 'eur', symbol: 'EUR' },
party: { id: 'blah' },
epoch: { id: '2' },
};
it('should return an empty array if no rewards are provided', () => {
expect(generateEpochIndividualRewardsList([])).toEqual([]);
});
it('should filter out any rewards of the wrong type', () => {
const result = generateEpochIndividualRewardsList([rewardWrongType]);
expect(result).toEqual([]);
});
it('should return reward in the correct format', () => {
const result = generateEpochIndividualRewardsList([reward1]);
expect(result[0]).toEqual({
epoch: '1',
rewards: [
{
asset: 'USD',
totalAmount: '100',
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: '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('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2, reward3, reward4];
const result1 = generateEpochIndividualRewardsList(rewards);
expect(result1[0].epoch).toEqual('2');
expect(result1[1].epoch).toEqual('1');
const reorderedRewards = [reward4, reward3, reward2, reward1];
const result2 = generateEpochIndividualRewardsList(reorderedRewards);
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);
expect(result[0].rewards[0].totalAmount).toEqual('200');
});
it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here
const rewards = [reward1, reward2, reward3, reward4];
const result = generateEpochIndividualRewardsList(rewards);
expect(result).toEqual([
{
epoch: '2',
rewards: [
{
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',
},
},
},
{
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',
},
},
},
],
},
{
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',
},
},
},
],
},
]);
});
});

View File

@ -0,0 +1,79 @@
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';
export interface EpochIndividualReward {
epoch: string;
rewards: {
asset: string;
totalAmount: string;
rewardTypes: {
[key in AccountType]?: {
amount: string;
percentageOfTotal: string;
};
};
}[];
}
const accountTypes = Object.keys(RowAccountTypes);
const emptyRowAccountTypes = accountTypes.map((type) => [
type,
{
amount: '0',
percentageOfTotal: '0',
},
]);
export const generateEpochIndividualRewardsList = (
rewards: RewardFieldsFragment[]
) => {
// We take the rewards and aggregate them by epoch and asset.
const epochIndividualRewards = rewards.reduce((map, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.symbol;
const rewardType = reward.rewardType;
const amount = reward.amount;
const percentageOfTotal = reward.percentageOfTotal;
// if the rewardType is not of a type we display in the table, we skip it.
if (!accountTypes.includes(rewardType)) {
return map;
}
if (!map.has(epochId)) {
map.set(epochId, { epoch: epochId, rewards: [] });
}
const epoch = map.get(epochId);
let asset = epoch?.rewards.find((r) => r.asset === assetName);
if (!asset) {
asset = {
asset: assetName,
totalAmount: '0',
rewardTypes: Object.fromEntries(emptyRowAccountTypes),
};
epoch?.rewards.push(asset);
}
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
// totalAmount is the sum of all rewardTypes amounts
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
(sum, rewardType) => {
return new BigNumber(sum).plus(rewardType.amount).toString();
},
'0'
);
return map;
}, new Map<string, EpochIndividualReward>());
return Array.from(epochIndividualRewards.values()).sort(
(a, b) => Number(b.epoch) - Number(a.epoch)
);
};

View File

@ -1,4 +1,5 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table'; import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
import { AccountType } from '@vegaprotocol/types'; import { AccountType } from '@vegaprotocol/types';
@ -10,10 +11,30 @@ const mockData = {
'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663', 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST', name: 'tDAI TEST',
rewards: [ rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{ {
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE, rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '295', 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', totalAmount: '295',
}, },
@ -22,15 +43,25 @@ const mockData = {
describe('EpochTotalRewardsTable', () => { describe('EpochTotalRewardsTable', () => {
it('should render correctly', () => { it('should render correctly', () => {
const { getByTestId } = render(<EpochTotalRewardsTable data={mockData} />); const { getByTestId } = render(
<AppStateProvider>
<EpochTotalRewardsTable data={mockData} />
</AppStateProvider>
);
expect(getByTestId('epoch-total-rewards-table')).toBeInTheDocument(); expect(getByTestId('epoch-total-rewards-table')).toBeInTheDocument();
expect(getByTestId('asset')).toBeInTheDocument(); expect(getByTestId('asset')).toBeInTheDocument();
expect(getByTestId('global')).toBeInTheDocument(); expect(getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD')).toBeInTheDocument();
expect(getByTestId('infra')).toBeInTheDocument(); expect(getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')).toBeInTheDocument();
expect(getByTestId('taker')).toBeInTheDocument(); expect(
expect(getByTestId('maker')).toBeInTheDocument(); getByTestId('ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES')
expect(getByTestId('liquidity')).toBeInTheDocument(); ).toBeInTheDocument();
expect(getByTestId('market-maker')).toBeInTheDocument(); expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
).toBeInTheDocument();
expect(getByTestId('total')).toBeInTheDocument(); expect(getByTestId('total')).toBeInTheDocument();
}); });
}); });

View File

@ -1,19 +1,14 @@
import classNames from 'classnames'; import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
import { useTranslation } from 'react-i18next'; import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/react-helpers'; import { useAppState } from '../../../contexts/app-state/app-state-context';
import { Tooltip, Icon } from '@vegaprotocol/ui-toolkit'; import {
import { AccountType } from '@vegaprotocol/types'; rowGridItemStyles,
import { SubHeading } from '../../../components/heading'; RewardsTable,
import type { AggregatedEpochSummary } from './generate-epoch-total-rewards-list'; } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { EpochTotalSummary } from './generate-epoch-total-rewards-list';
interface EpochTotalRewardsGridProps { interface EpochTotalRewardsGridProps {
data: AggregatedEpochSummary; data: EpochTotalSummary;
}
interface ColumnHeaderProps {
title: string;
tooltipContent?: string;
className?: string;
} }
interface RewardItemProps { interface RewardItemProps {
@ -22,61 +17,28 @@ interface RewardItemProps {
last?: boolean; last?: boolean;
} }
const displayReward = (reward: string) => { const DisplayReward = ({ reward }: { reward: string }) => {
const {
appState: { decimals },
} = useAppState();
if (Number(reward) === 0) { if (Number(reward) === 0) {
return <span className="text-vega-dark-300">0</span>; return <span className="text-vega-dark-300">-</span>;
} }
if (reward.split('.')[1] && reward.split('.')[1].length > 4) { return (
return ( <Tooltip description={formatNumber(toBigNum(reward, decimals), decimals)}>
<Tooltip description={formatNumber(reward)}> <button>{formatNumber(toBigNum(reward, decimals))}</button>
<button>{formatNumber(Number(reward).toFixed(4))}</button> </Tooltip>
</Tooltip> );
);
}
return <span>{formatNumber(reward)}</span>;
}; };
const gridStyles = classNames(
'grid grid-cols-[repeat(8,minmax(100px,auto))] max-w-full overflow-auto',
`border-t border-vega-dark-200`,
'text-sm'
);
const headerGridItemStyles = (last = false) =>
classNames('border-r border-b border-b-vega-dark-200', 'py-3 px-5', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
const rowGridItemStyles = (last = false) =>
classNames('relative', 'border-r border-b border-b-vega-dark-150', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
const ColumnHeader = ({
title,
tooltipContent,
className,
}: ColumnHeaderProps) => (
<div className={className}>
<h2 className="mb-1 text-sm text-vega-dark-300">{title}</h2>
{tooltipContent && (
<Tooltip description={tooltipContent}>
<button>
<Icon name={'info-sign'} className="text-vega-dark-200" />
</button>
</Tooltip>
)}
</div>
);
const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => ( const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
<div data-testid={dataTestId} className={rowGridItemStyles(last)}> <div data-testid={dataTestId} className={rowGridItemStyles(last)}>
<div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" /> <div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
<div className="overflow-auto p-5">{displayReward(value)}</div> <div className="overflow-auto p-5">
<DisplayReward reward={value} />
</div>
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" /> <div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
</div> </div>
); );
@ -84,129 +46,19 @@ const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
export const EpochTotalRewardsTable = ({ export const EpochTotalRewardsTable = ({
data, data,
}: EpochTotalRewardsGridProps) => { }: EpochTotalRewardsGridProps) => {
const { t } = useTranslation();
const rowData = data.assetRewards.map(({ name, rewards, totalAmount }) => ({
name,
ACCOUNT_TYPE_GLOBAL_REWARD:
rewards
.filter((r) => r.rewardType === AccountType.ACCOUNT_TYPE_GLOBAL_REWARD)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_FEES_INFRASTRUCTURE:
rewards
.filter(
(r) => r.rewardType === AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE
)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES:
rewards
.filter(
(r) =>
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES
)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES:
rewards
.filter(
(r) =>
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES
)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_FEES_LIQUIDITY:
rewards
.filter((r) => r.rewardType === AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS:
rewards
.filter(
(r) =>
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS
)
.map((r) => r.amount)[0] || '0',
totalAmount: totalAmount,
}));
return ( return (
<div data-testid="epoch-total-rewards-table" className="mb-12"> <RewardsTable dataTestId="epoch-total-rewards-table" epoch={data.epoch}>
<SubHeading title={`EPOCH ${data.epoch}`} /> {data.assetRewards.map(({ name, rewards, totalAmount }, i) => (
<div className="contents" key={i}>
<div className={gridStyles}> <div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
<ColumnHeader {name}
title={t('rewardsColAssetHeader')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColStakingHeader')}
tooltipContent={t('rewardsColStakingTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColInfraHeader')}
tooltipContent={t('rewardsColInfraTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColPriceTakingHeader')}
tooltipContent={t('rewardsColPriceTakingTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColPriceMakingHeader')}
tooltipContent={t('rewardsColPriceMakingTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColLiquidityProvisionHeader')}
tooltipContent={t('rewardsColLiquidityProvisionTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColMarketCreationHeader')}
tooltipContent={t('rewardsColMarketCreationTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColTotalHeader')}
className={headerGridItemStyles(true)}
/>
{rowData.map((row, i) => (
<div className="contents" key={i}>
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
{row.name}
</div>
<RewardItem
dataTestId="global"
value={row.ACCOUNT_TYPE_GLOBAL_REWARD}
/>
<RewardItem
dataTestId="infra"
value={row.ACCOUNT_TYPE_FEES_INFRASTRUCTURE}
/>
<RewardItem
dataTestId="taker"
value={row.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES}
/>
<RewardItem
dataTestId="maker"
value={row.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES}
/>
<RewardItem
dataTestId="liquidity"
value={row.ACCOUNT_TYPE_FEES_LIQUIDITY}
/>
<RewardItem
dataTestId="market-maker"
value={row.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS}
/>
<RewardItem
dataTestId="total"
value={row.totalAmount}
last={true}
/>
</div> </div>
))} {rewards.map(({ rewardType, amount }, i) => (
</div> <RewardItem key={i} dataTestId={rewardType} value={amount} />
</div> ))}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
))}
</RewardsTable>
); );
}; };

View File

@ -5,7 +5,7 @@ import { generateEpochTotalRewardsList } from './generate-epoch-total-rewards-li
import { NoRewards } from '../no-rewards'; import { NoRewards } from '../no-rewards';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table'; import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
export const EpochRewards = () => { export const EpochTotalRewards = () => {
const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({ const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
variables: { variables: {
epochRewardSummariesPagination: { epochRewardSummariesPagination: {
@ -15,7 +15,7 @@ export const EpochRewards = () => {
}); });
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch); useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
const epochRewardSummaries = generateEpochTotalRewardsList(data) || []; const epochTotalRewardSummaries = generateEpochTotalRewardsList(data) || [];
return ( return (
<AsyncRenderer <AsyncRenderer
@ -27,12 +27,12 @@ export const EpochRewards = () => {
className="max-w-full overflow-auto" className="max-w-full overflow-auto"
data-testid="epoch-rewards-total" data-testid="epoch-rewards-total"
> >
{epochRewardSummaries.length === 0 ? ( {epochTotalRewardSummaries.length === 0 ? (
<NoRewards /> <NoRewards />
) : ( ) : (
<> <>
{epochRewardSummaries.map((aggregatedEpochSummary) => ( {epochTotalRewardSummaries.map((epochTotalSummary, index) => (
<EpochTotalRewardsTable data={aggregatedEpochSummary} /> <EpochTotalRewardsTable data={epochTotalSummary} key={index} />
))} ))}
</> </>
)} )}

View File

@ -61,7 +61,7 @@ describe('generateEpochAssetRewardsList', () => {
expect(result).toEqual([]); expect(result).toEqual([]);
}); });
it('should return an array of unnamed assets if no assets are provided (should not happen)', () => { it('should return an array of unnamed assets if no asset names are provided (should not happen)', () => {
const epochData = { const epochData = {
assetsConnection: { assetsConnection: {
edges: [], edges: [],
@ -72,18 +72,10 @@ describe('generateEpochAssetRewardsList', () => {
node: { node: {
epoch: 1, epoch: 1,
assetId: '1', assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE, rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '123', amount: '123',
}, },
}, },
{
node: {
epoch: 2,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
},
], ],
}, },
epoch: { epoch: {
@ -104,30 +96,34 @@ describe('generateEpochAssetRewardsList', () => {
name: '', name: '',
rewards: [ rewards: [
{ {
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE, rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '123', 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', totalAmount: '123',
}, },
], ],
}, },
{
epoch: 2,
assetRewards: [
{
assetId: '1',
name: '',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
],
totalAmount: '5',
},
],
},
]); ]);
}); });
@ -155,7 +151,7 @@ describe('generateEpochAssetRewardsList', () => {
node: { node: {
epoch: 1, epoch: 1,
assetId: '1', assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE, rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123', amount: '123',
}, },
}, },
@ -167,22 +163,6 @@ describe('generateEpochAssetRewardsList', () => {
amount: '100', amount: '100',
}, },
}, },
{
node: {
epoch: 1,
assetId: '2',
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '17.9873',
},
},
{
node: {
epoch: 1,
assetId: '2',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '1',
},
},
{ {
node: { node: {
epoch: 2, epoch: 2,
@ -211,30 +191,31 @@ describe('generateEpochAssetRewardsList', () => {
name: 'Asset 1', name: 'Asset 1',
rewards: [ rewards: [
{ {
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE, rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '123', amount: '0',
}, },
{ {
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE, rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100', amount: '100',
}, },
],
totalAmount: '223',
},
{
assetId: '2',
name: 'Asset 2',
rewards: [
{ {
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE, rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '17.9873', amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
}, },
{ {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY, rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '1', amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
}, },
], ],
totalAmount: '18.9873', totalAmount: '223',
}, },
], ],
}, },
@ -245,10 +226,30 @@ describe('generateEpochAssetRewardsList', () => {
assetId: '1', assetId: '1',
name: 'Asset 1', name: 'Asset 1',
rewards: [ 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, rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5', amount: '5',
}, },
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
], ],
totalAmount: '5', totalAmount: '5',
}, },

View File

@ -3,6 +3,8 @@ import type {
EpochRewardSummaryFieldsFragment, EpochRewardSummaryFieldsFragment,
} from '../home/__generated__/Rewards'; } from '../home/__generated__/Rewards';
import { removePaginationWrapper } from '@vegaprotocol/react-helpers'; import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { AccountType } from '@vegaprotocol/types';
interface EpochSummaryWithNamedReward extends EpochRewardSummaryFieldsFragment { interface EpochSummaryWithNamedReward extends EpochRewardSummaryFieldsFragment {
name: string; name: string;
@ -18,11 +20,16 @@ export interface AggregatedEpochRewardSummary {
totalAmount: string; totalAmount: string;
} }
export interface AggregatedEpochSummary { export interface EpochTotalSummary {
epoch: EpochRewardSummaryFieldsFragment['epoch']; epoch: EpochRewardSummaryFieldsFragment['epoch'];
assetRewards: AggregatedEpochRewardSummary[]; assetRewards: AggregatedEpochRewardSummary[];
} }
const emptyRowAccountTypes = Object.keys(RowAccountTypes).map((type) => ({
rewardType: type as AccountType,
amount: '0',
}));
export const generateEpochTotalRewardsList = ( export const generateEpochTotalRewardsList = (
epochData: EpochAssetsRewardsQuery | undefined epochData: EpochAssetsRewardsQuery | undefined
) => { ) => {
@ -58,7 +65,7 @@ export const generateEpochTotalRewardsList = (
}, [] as EpochSummaryWithNamedReward[][]); }, [] as EpochSummaryWithNamedReward[][]);
// Now aggregate the array of arrays of epoch summaries by asset rewards. // Now aggregate the array of arrays of epoch summaries by asset rewards.
const aggregatedEpochSummaries: AggregatedEpochSummary[] = const epochTotalRewards: EpochTotalSummary[] =
aggregatedEpochSummariesByEpochNumber.map((epochSummaries) => { aggregatedEpochSummariesByEpochNumber.map((epochSummaries) => {
const assetRewards = epochSummaries.reduce((acc, epochSummary) => { const assetRewards = epochSummaries.reduce((acc, epochSummary) => {
const assetRewardIndex = acc.findIndex( const assetRewardIndex = acc.findIndex(
@ -72,18 +79,36 @@ export const generateEpochTotalRewardsList = (
assetId: epochSummary.assetId, assetId: epochSummary.assetId,
name: epochSummary.name, name: epochSummary.name,
rewards: [ rewards: [
{ ...emptyRowAccountTypes.map((emptyRowAccountType) => {
rewardType: epochSummary.rewardType, if (
amount: epochSummary.amount, emptyRowAccountType.rewardType === epochSummary.rewardType
}, ) {
return {
rewardType: epochSummary.rewardType,
amount: epochSummary.amount,
};
} else {
return emptyRowAccountType;
}
}),
], ],
totalAmount: epochSummary.amount, totalAmount: epochSummary.amount,
}); });
} else { } else {
acc[assetRewardIndex].rewards.push({ acc[assetRewardIndex].rewards = acc[assetRewardIndex].rewards.map(
rewardType: epochSummary.rewardType, (reward) => {
amount: epochSummary.amount, if (reward.rewardType === epochSummary.rewardType) {
}); return {
rewardType: epochSummary.rewardType,
amount: (
Number(reward.amount) + Number(epochSummary.amount)
).toString(),
};
} else {
return reward;
}
}
);
acc[assetRewardIndex].totalAmount = ( acc[assetRewardIndex].totalAmount = (
Number(acc[assetRewardIndex].totalAmount) + Number(acc[assetRewardIndex].totalAmount) +
Number(epochSummary.amount) Number(epochSummary.amount)
@ -99,5 +124,5 @@ export const generateEpochTotalRewardsList = (
}; };
}); });
return aggregatedEpochSummaries; return epochTotalRewards;
}; };

View File

@ -1,34 +1,30 @@
// @ts-ignore No types available for duration-js // @ts-ignore No types available for duration-js
import Duration from 'duration-js'; import Duration from 'duration-js';
import React, { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { formatDistance } from 'date-fns'; import { formatDistance } from 'date-fns';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Button,
Callout, Callout,
Intent, Intent,
AsyncRenderer, AsyncRenderer,
Toggle, Toggle,
ExternalLink, ExternalLink,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { import {
useNetworkParams, useNetworkParams,
NetworkParams, NetworkParams,
createDocsLinks, createDocsLinks,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import {
AppStateActionType,
useAppState,
} from '../../../contexts/app-state/app-state-context';
import { useEpochQuery } from './__generated__/Rewards'; import { useEpochQuery } from './__generated__/Rewards';
import { EpochCountdown } from '../../../components/epoch-countdown'; import { EpochCountdown } from '../../../components/epoch-countdown';
import { Heading, SubHeading } from '../../../components/heading'; import { Heading, SubHeading } from '../../../components/heading';
import { RewardInfo } from '../epoch-individual-awards/reward-info'; import { EpochIndividualRewards } from '../epoch-individual-rewards/epoch-individual-rewards';
import { EpochRewards } from '../epoch-total-rewards/epoch-rewards';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch'; import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { ConnectToSeeRewards } from '../connect-to-see-rewards';
import { EpochTotalRewards } from '../epoch-total-rewards/epoch-total-rewards';
type RewardsView = 'total' | 'individual'; type RewardsView = 'total' | 'individual';
@ -39,25 +35,21 @@ export const RewardsPage = () => {
const [toggleRewardsView, setToggleRewardsView] = const [toggleRewardsView, setToggleRewardsView] =
useState<RewardsView>('total'); useState<RewardsView>('total');
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { appDispatch } = useAppState();
const {
params,
loading: paramsLoading,
error: paramsError,
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
const { const {
data: epochData, data: epochData,
loading: epochLoading, loading: epochLoading,
error: epochError, error: epochError,
refetch, refetch,
} = useEpochQuery(); } = useEpochQuery();
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch); useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
const {
params,
loading: paramsLoading,
error: paramsError,
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
const payoutDuration = useMemo(() => { const payoutDuration = useMemo(() => {
if (!params) { if (!params) {
return 0; return 0;
@ -103,8 +95,8 @@ export const RewardsPage = () => {
</div> </div>
) : null} ) : null}
{epochData && {epochData?.epoch &&
epochData.epoch.id && epochData.epoch?.id &&
epochData.epoch.timestamps.start && epochData.epoch.timestamps.start &&
epochData.epoch.timestamps.expiry && ( epochData.epoch.timestamps.expiry && (
<section className="mb-16"> <section className="mb-16">
@ -148,26 +140,13 @@ export const RewardsPage = () => {
</section> </section>
{toggleRewardsView === 'total' ? ( {toggleRewardsView === 'total' ? (
<EpochRewards /> <EpochTotalRewards />
) : ( ) : (
<section> <section>
{pubKey && pubKeys?.length ? ( {pubKey && pubKeys?.length ? (
<RewardInfo /> <EpochIndividualRewards />
) : ( ) : (
<div> <ConnectToSeeRewards />
<Button
data-testid="connect-to-vega-wallet-btn"
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
{t('connectVegaWallet')}
</Button>
</div>
)} )}
</section> </section>
)} )}

View File

@ -0,0 +1,125 @@
import classNames from 'classnames';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import type { ReactNode } from 'react';
import { SubHeading } from '../../../components/heading';
import { AccountType } from '@vegaprotocol/types';
// This is the data structure that matters for defining which Account types
// are displayed in the rewards tables. It sets column titles and tooltips,
// and is used to filter the data that is passed to functions to generate
// the table rows. It's important to preserve the order.
export const RowAccountTypes = {
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
columnTitle: 'rewardsColStakingHeader',
description: 'rewardsColStakingTooltip',
},
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
columnTitle: 'rewardsColInfraHeader',
description: 'rewardsColInfraTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
columnTitle: 'rewardsColPriceTakingHeader',
description: 'rewardsColPriceTakingTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
columnTitle: 'rewardsColPriceMakingHeader',
description: 'rewardsColPriceMakingTooltip',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
columnTitle: 'rewardsColLiquidityProvisionHeader',
description: 'rewardsColLiquidityProvisionTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
columnTitle: 'rewardsColMarketCreationHeader',
description: 'rewardsColMarketCreationTooltip',
},
};
interface ColumnHeaderProps {
title: string;
tooltipContent?: string;
className?: string;
}
const headerGridItemStyles = (last = false) =>
classNames('border-r border-b border-b-vega-dark-200', 'py-3 px-5', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
export const rowGridItemStyles = (last = false) =>
classNames('relative', 'border-r border-b border-b-vega-dark-150', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
const gridStyles = classNames(
'grid grid-cols-[repeat(8,minmax(100px,auto))] max-w-full overflow-auto',
`border-t border-vega-dark-200`,
'text-sm'
);
const ColumnHeader = ({
title,
tooltipContent,
className,
}: ColumnHeaderProps) => (
<div className={className}>
<h2 className="mb-1 text-sm text-vega-dark-300">{title}</h2>
{tooltipContent && (
<Tooltip description={tooltipContent}>
<button>
<Icon name={'info-sign'} className="text-vega-dark-200" />
</button>
</Tooltip>
)}
</div>
);
const ColumnHeaders = () => {
const { t } = useTranslation();
return (
<div className="contents">
<ColumnHeader
title={t('rewardsColAssetHeader')}
className={headerGridItemStyles()}
/>
{Object.values(RowAccountTypes).map(({ columnTitle, description }) => (
<ColumnHeader
key={columnTitle}
title={t(columnTitle)}
tooltipContent={t(description)}
className={headerGridItemStyles()}
/>
))}
<ColumnHeader
title={t('rewardsColTotalHeader')}
className={headerGridItemStyles(true)}
/>
</div>
);
};
export interface RewardTableProps {
dataTestId: string;
epoch: number;
children: ReactNode;
}
// Rewards table children will be the row items. Make sure they contain
// the same number of columns and map to the data of the ColumnHeaders component.
export const RewardsTable = ({
dataTestId,
epoch,
children,
}: RewardTableProps) => (
<div data-testid={dataTestId} className="mb-12">
<SubHeading title={`EPOCH ${epoch}`} />
<div className={gridStyles}>
<ColumnHeaders />
{children}
</div>
</div>
);

View File

@ -35,7 +35,7 @@ export const Toggle = ({
); );
const radioClasses = classnames('sr-only', 'peer'); const radioClasses = classnames('sr-only', 'peer');
const buttonClasses = classnames( const buttonClasses = classnames(
'relative inline-block w-full text-center', 'relative inline-flex w-full h-full text-center items-center justify-center',
'peer-checked:rounded-full', 'peer-checked:rounded-full',
{ {
'peer-checked:bg-neutral-400 dark:peer-checked:bg-white dark:peer-checked:text-black': 'peer-checked:bg-neutral-400 dark:peer-checked:bg-white dark:peer-checked:text-black':
@ -76,7 +76,9 @@ export const Toggle = ({
} }
className={radioClasses} className={radioClasses}
/> />
<span className={buttonClasses}>{label}</span> <span className={buttonClasses}>
<span>{label}</span>
</span>
</label> </label>
); );
})} })}