fix(trading): rewards page updates (#5437)

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
m.ray 2023-12-06 06:05:45 +02:00 committed by GitHub
parent a52e60d6a2
commit 3cd393dac0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 329 additions and 54 deletions

View File

@ -1,4 +1,5 @@
import groupBy from 'lodash/groupBy';
import uniq from 'lodash/uniq';
import type { Account } from '@vegaprotocol/accounts';
import { useAccounts } from '@vegaprotocol/accounts';
import {
@ -31,6 +32,12 @@ import { ViewType, useSidebar } from '../sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { RewardsHistoryContainer } from './rewards-history';
import { useT } from '../../lib/use-t';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
const ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA = [
'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba', // USDT mainnet
'8ba0b10971f0c4747746cd01ff05a53ae75ca91eba1d4d050b527910c983e27e', // USDT testnet
];
export const RewardsContainer = () => {
const t = useT();
@ -40,34 +47,67 @@ export const RewardsContainer = () => {
NetworkParams.rewards_activityStreak_benefitTiers,
NetworkParams.rewards_vesting_baseRate,
]);
const { data: accounts, loading: accountsLoading } = useAccounts(pubKey);
const { data: assetMap } = useAssetsMapProvider();
const { data: epochData } = useRewardsEpochQuery();
// No need to specify the fromEpoch as it will by default give you the last
// Note activityStreak in query will fail
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
variables: {
partyId: pubKey || '',
},
// Inclusion of activity streak in query currently fails
errorPolicy: 'ignore',
});
if (!epochData?.epoch) return null;
if (!epochData?.epoch || !assetMap) return null;
const loading = paramsLoading || accountsLoading || rewardsLoading;
const rewardAccounts = accounts
? accounts.filter((a) =>
[
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
].includes(a.type)
)
? accounts
.filter((a) =>
[
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
].includes(a.type)
)
.filter((a) => new BigNumber(a.balance).isGreaterThan(0))
: [];
const rewardAssetsMap = groupBy(
rewardAccounts.filter((a) => a.asset.id !== params.reward_asset),
'asset.id'
);
const rewardAccountsAssetMap = groupBy(rewardAccounts, 'asset.id');
const lockedBalances = rewardsData?.party?.vestingBalancesSummary
.lockedBalances
? rewardsData.party.vestingBalancesSummary.lockedBalances.filter((b) =>
new BigNumber(b.balance).isGreaterThan(0)
)
: [];
const lockedAssetMap = groupBy(lockedBalances, 'asset.id');
const vestingBalances = rewardsData?.party?.vestingBalancesSummary
.vestingBalances
? rewardsData.party.vestingBalancesSummary.vestingBalances.filter((b) =>
new BigNumber(b.balance).isGreaterThan(0)
)
: [];
const vestingAssetMap = groupBy(vestingBalances, 'asset.id');
// each asset reward pot is made up of:
// available to withdraw - ACCOUNT_TYPE_VESTED_REWARDS
// vesting - vestingBalancesSummary.vestingBalances
// locked - vestingBalancesSummary.lockedBalances
//
// there can be entires for the same asset in each list so we need a uniq list of assets
const assets = uniq([
...Object.keys(rewardAccountsAssetMap),
...Object.keys(lockedAssetMap),
...Object.keys(vestingAssetMap),
]);
return (
<div className="grid auto-rows-min grid-cols-6 gap-3">
@ -117,28 +157,72 @@ export const RewardsContainer = () => {
</Card>
{/* Show all other reward pots, most of the time users will not have other rewards */}
{Object.keys(rewardAssetsMap).map((assetId) => {
const asset = rewardAssetsMap[assetId][0].asset;
return (
<Card
key={assetId}
title={t('{{assetSymbol}} Reward pot', {
assetSymbol: asset.symbol,
})}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
>
<RewardPot
pubKey={pubKey}
accounts={accounts}
assetId={assetId}
vestingBalancesSummary={
rewardsData?.party?.vestingBalancesSummary
}
/>
</Card>
);
})}
{assets
.filter((assetId) => assetId !== params.reward_asset)
.map((assetId) => {
const asset = assetMap ? assetMap[assetId] : null;
if (!asset) return null;
// Following code is for mitigating an issue due to a core bug where locked and vesting
// balances were incorrectly increased for infrastructure rewards for USDT on mainnet
//
// We don't want to incorrectly show the wring locked/vesting values, but we DO want to
// show the user that they have rewards available to withdraw
if (ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA.includes(asset.id)) {
const accountsForAsset = rewardAccountsAssetMap[asset.id];
const vestedAccount = accountsForAsset?.find(
(a) => a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
);
// No vested rewards available to withdraw, so skip over USDT
if (!vestedAccount || Number(vestedAccount.balance) <= 0) {
return null;
}
return (
<Card
key={assetId}
title={t('{{assetSymbol}} Reward pot', {
assetSymbol: asset.symbol,
})}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
>
<RewardPot
pubKey={pubKey}
accounts={accounts}
assetId={assetId}
// Ensure that these values are shown as 0
vestingBalancesSummary={{
lockedBalances: [],
vestingBalances: [],
}}
/>
</Card>
);
}
return (
<Card
key={assetId}
title={t('{{assetSymbol}} Reward pot', {
assetSymbol: asset.symbol,
})}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
>
<RewardPot
pubKey={pubKey}
accounts={accounts}
assetId={assetId}
vestingBalancesSummary={
rewardsData?.party?.vestingBalancesSummary
}
/>
</Card>
);
})}
<Card
title={t('Rewards history')}
className="lg:col-span-full"
@ -147,6 +231,7 @@ export const RewardsContainer = () => {
<RewardsHistoryContainer
epoch={Number(epochData?.epoch.id)}
pubKey={pubKey}
assets={assetMap}
/>
</Card>
</div>
@ -313,14 +398,14 @@ export const RewardPot = ({
export const Vesting = ({
pubKey,
baseRate,
multiplier = '1',
multiplier,
}: {
pubKey: string | null;
baseRate: string;
multiplier?: string;
}) => {
const t = useT();
const rate = new BigNumber(baseRate).times(multiplier);
const rate = new BigNumber(baseRate).times(multiplier || 1);
const rateFormatted = formatPercentage(Number(rate));
const baseRateFormatted = formatPercentage(Number(baseRate));
@ -335,7 +420,7 @@ export const Vesting = ({
{pubKey && (
<tr>
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
<CardTableTD>{multiplier}x</CardTableTD>
<CardTableTD>{multiplier ? `${multiplier}x` : '-'}</CardTableTD>
</tr>
)}
</CardTable>
@ -345,16 +430,16 @@ export const Vesting = ({
export const Multipliers = ({
pubKey,
streakMultiplier = '1',
hoarderMultiplier = '1',
streakMultiplier,
hoarderMultiplier,
}: {
pubKey: string | null;
streakMultiplier?: string;
hoarderMultiplier?: string;
}) => {
const t = useT();
const combinedMultiplier = new BigNumber(streakMultiplier).times(
hoarderMultiplier
const combinedMultiplier = new BigNumber(streakMultiplier || 1).times(
hoarderMultiplier || 1
);
if (!pubKey) {
@ -375,11 +460,15 @@ export const Multipliers = ({
<CardTable>
<tr>
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
<CardTableTD>{streakMultiplier}x</CardTableTD>
<CardTableTD>
{streakMultiplier ? `${streakMultiplier}x` : '-'}
</CardTableTD>
</tr>
<tr>
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
<CardTableTD>{hoarderMultiplier}x</CardTableTD>
<CardTableTD>
{hoarderMultiplier ? `${hoarderMultiplier}x` : '-'}
</CardTableTD>
</tr>
</CardTable>
</div>

View File

@ -61,6 +61,14 @@ const rewardSummaries = [
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
},
},
{
node: {
epoch: 7,
assetId: assets.asset2.id,
amount: '300',
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
},
},
];
const getCell = (cells: HTMLElement[], colId: string) => {
@ -69,7 +77,7 @@ const getCell = (cells: HTMLElement[], colId: string) => {
);
};
describe('RewarsHistoryTable', () => {
describe('RewardsHistoryTable', () => {
const props = {
epochRewardSummaries: {
edges: rewardSummaries,
@ -88,7 +96,7 @@ describe('RewarsHistoryTable', () => {
loading: false,
};
it('Renders table with accounts summed up by asset', () => {
it('renders table with accounts summed up by asset', () => {
render(<RewardHistoryTable {...props} />);
const container = within(
@ -110,17 +118,27 @@ describe('RewarsHistoryTable', () => {
assets.asset2.name
);
// First row
const marketCreationCell = getCell(cells, 'marketCreation');
expect(
marketCreationCell.getByTestId('stack-cell-primary')
).toHaveTextContent('300');
expect(
marketCreationCell.getByTestId('stack-cell-secondary')
).toHaveTextContent('100.00%');
).toHaveTextContent('50.00%');
const infrastructureFeesCell = getCell(cells, 'infrastructureFees');
expect(
infrastructureFeesCell.getByTestId('stack-cell-primary')
).toHaveTextContent('300');
expect(
infrastructureFeesCell.getByTestId('stack-cell-secondary')
).toHaveTextContent('50.00%');
let totalCell = getCell(cells, 'total');
expect(totalCell.getByText('300.00')).toBeInTheDocument();
expect(totalCell.getByText('600.00')).toBeInTheDocument();
// Second row
row = within(rows[1]);
cells = row.getAllByRole('gridcell');

View File

@ -2,10 +2,7 @@ import debounce from 'lodash/debounce';
import { useMemo, useState } from 'react';
import BigNumber from 'bignumber.js';
import type { ColDef, ValueFormatterFunc } from 'ag-grid-community';
import {
useAssetsMapProvider,
type AssetFieldsFragment,
} from '@vegaprotocol/assets';
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
import {
addDecimalsFormatNumberQuantum,
formatNumberPercentage,
@ -26,17 +23,17 @@ import { useT } from '../../lib/use-t';
export const RewardsHistoryContainer = ({
epoch,
pubKey,
assets,
}: {
pubKey: string | null;
epoch: number;
assets: Record<string, AssetFieldsFragment>;
}) => {
const [epochVariables, setEpochVariables] = useState(() => ({
from: epoch - 1,
to: epoch,
}));
const { data: assets } = useAssetsMapProvider();
// No need to specify the fromEpoch as it will by default give you the last
const { refetch, data, loading } = useRewardsHistoryQuery({
variables: {
@ -154,10 +151,12 @@ export const RewardHistoryTable = ({
const rewardValueFormatter: ValueFormatterFunc<RewardRow> = ({
data,
value,
...rest
}) => {
if (!value || !data) {
return '-';
}
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
@ -197,6 +196,11 @@ export const RewardHistoryTable = ({
},
sort: 'desc',
},
{
field: 'infrastructureFees',
valueFormatter: rewardValueFormatter,
cellRenderer: rewardCellRenderer,
},
{
field: 'staking',
valueFormatter: rewardValueFormatter,

View File

@ -0,0 +1,159 @@
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
import { getRewards } from './use-reward-row-data';
import * as Schema from '@vegaprotocol/types';
const asset1 = {
id: 'asset1',
name: 'USD (KRW)',
symbol: 'USD-KRW',
decimals: 6,
quantum: '1000000',
status: Schema.AssetStatus.STATUS_ENABLED,
// @ts-ignore not needed
source: {},
} as AssetFieldsFragment;
const asset2 = {
id: 'asset2',
name: 'tDAI TEST',
symbol: 'tDAI',
decimals: 5,
quantum: '1',
status: Schema.AssetStatus.STATUS_ENABLED,
// @ts-ignore not needed
source: {},
} as AssetFieldsFragment;
const asset3 = {
id: 'asset3',
name: 'Tether USD',
symbol: 'USDT',
decimals: 6,
quantum: '1000000',
status: Schema.AssetStatus.STATUS_ENABLED,
// @ts-ignore not needed
source: {},
} as AssetFieldsFragment;
const asset4 = {
id: 'asset4',
name: 'USDT-T',
symbol: 'USDT-T',
decimals: 18,
quantum: '1',
status: Schema.AssetStatus.STATUS_ENABLED,
// @ts-ignore not needed
source: {},
} as AssetFieldsFragment;
const assets: Record<string, AssetFieldsFragment> = {
asset1,
asset2,
asset3,
asset4,
};
const testData = {
rewards: [
{
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
assetId: 'asset1',
amount: '31897424',
},
{
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
assetId: 'asset2',
amount: '57',
},
{
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
assetId: 'asset3',
amount: '5501',
},
{
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
assetId: 'asset3',
amount: '5501',
},
{
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
assetId: 'asset4',
amount: '5501',
},
{
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
assetId: 'asset4',
amount: '456',
},
{
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING,
assetId: 'asset4',
amount: '4565',
},
],
assets,
};
describe('getRewards', () => {
it('should return the correct rewards when infra fees are included', () => {
const rewards = getRewards(testData.rewards, testData.assets);
expect(rewards).toEqual([
{
asset: asset1,
infrastructureFees: 31897424,
staking: 0,
priceTaking: 0,
priceMaking: 0,
liquidityProvision: 0,
marketCreation: 0,
averagePosition: 0,
relativeReturns: 0,
returnsVolatility: 0,
validatorRanking: 0,
total: 31897424,
},
{
asset: asset2,
infrastructureFees: 57,
staking: 0,
priceTaking: 0,
priceMaking: 0,
liquidityProvision: 0,
marketCreation: 0,
averagePosition: 0,
relativeReturns: 0,
returnsVolatility: 0,
validatorRanking: 0,
total: 57,
},
{
asset: asset3,
infrastructureFees: 5501,
staking: 0,
priceTaking: 0,
priceMaking: 0,
liquidityProvision: 0,
marketCreation: 0,
averagePosition: 5501,
relativeReturns: 0,
returnsVolatility: 0,
validatorRanking: 0,
total: 11002,
},
{
asset: asset4,
infrastructureFees: 0,
staking: 0,
priceTaking: 0,
priceMaking: 5501,
liquidityProvision: 456,
marketCreation: 0,
averagePosition: 0,
relativeReturns: 0,
returnsVolatility: 0,
validatorRanking: 4565,
total: 10522,
},
]);
});
});

View File

@ -16,9 +16,10 @@ const REWARD_ACCOUNT_TYPES = [
AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN,
AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY,
AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING,
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
];
const getRewards = (
export const getRewards = (
rewards: Array<{
rewardType: AccountType;
assetId: string;
@ -56,6 +57,9 @@ const getRewards = (
return {
asset,
infrastructureFees: totals.get(
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE
),
staking: totals.get(AccountType.ACCOUNT_TYPE_GLOBAL_REWARD),
priceTaking: totals.get(AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES),
priceMaking: totals.get(
@ -101,7 +105,8 @@ export const useRewardsRowData = ({
assetId: r.asset.id,
amount: r.amount,
}));
return getRewards(rewards, assets);
const result = getRewards(rewards, assets);
return result;
}
const rewards = removePaginationWrapper(epochRewardSummaries?.edges);