fix(trading): rewards page updates (#5437)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
parent
a52e60d6a2
commit
3cd393dac0
@ -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>
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user