import groupBy from 'lodash/groupBy'; import uniq from 'lodash/uniq'; import type { Account } from '@vegaprotocol/accounts'; import { useAccounts } from '@vegaprotocol/accounts'; import { NetworkParams, useNetworkParams, } from '@vegaprotocol/network-parameters'; import { AccountType } from '@vegaprotocol/types'; import { useVegaWallet } from '@vegaprotocol/wallet-react'; import BigNumber from 'bignumber.js'; import { Card, CardStat, CardTable, CardTableTD, CardTableTH, } from '../card/card'; import { type RewardsPageQuery, useRewardsPageQuery, useRewardsEpochQuery, } from '../../lib/hooks/__generated__/Rewards'; import { TradingButton, VegaIcon, VegaIconNames, } from '@vegaprotocol/ui-toolkit'; import { formatPercentage } from '../fees-container/utils'; import { addDecimalsFormatNumberQuantum } from '@vegaprotocol/utils'; 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'; import { ActiveRewards } from './active-rewards'; import { ActivityStreak } from './streaks/activity-streaks'; import { RewardHoarderBonus } from './streaks/reward-hoarder-bonus'; import classNames from 'classnames'; const ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA = [ 'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba', // USDT mainnet '8ba0b10971f0c4747746cd01ff05a53ae75ca91eba1d4d050b527910c983e27e', // USDT testnet ]; export const RewardsContainer = () => { const t = useT(); const { pubKey } = useVegaWallet(); const { params, loading: paramsLoading } = useNetworkParams([ NetworkParams.reward_asset, NetworkParams.rewards_activityStreak_benefitTiers, NetworkParams.rewards_vesting_benefitTiers, NetworkParams.rewards_vesting_baseRate, ]); const { data: accounts, loading: accountsLoading } = useAccounts(pubKey); const { data: assetMap } = useAssetsMapProvider(); const { data: epochData } = useRewardsEpochQuery(); const { rewards_activityStreak_benefitTiers, rewards_vesting_benefitTiers } = params || {}; const activityStreakBenefitTiers = JSON.parse( rewards_activityStreak_benefitTiers ); const vestingBenefitTiers = JSON.parse(rewards_vesting_benefitTiers); // 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', // polling here so that as rewards are are moved to ACCOUNT_TYPE_VESTED_REWARDS the vesting stats information stays // almost up to sync with accounts updating from subscriptions. There is a chance the data could be out // of sync for 10s if you happen to be on the page at the end of an epoch pollInterval: 10000, }); const partyActivityStreak = rewardsData?.party?.activityStreak; const vestingDetails = rewardsData?.party?.vestingStats; 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) ) .filter((a) => new BigNumber(a.balance).isGreaterThan(0)) : []; 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 (
{/* Always show reward information for vega */} {/* Show all other reward pots, most of the time users will not have other rewards */} {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 ( ); } return ( ); })}
{pubKey && activityStreakBenefitTiers.tiers?.length > 0 && ( 4, }, 'hidden md:block' )} > )} {pubKey && vestingBenefitTiers.tiers?.length > 0 && ( 4, }, 'hidden md:block' )} > )}
); }; export type VestingBalances = NonNullable< RewardsPageQuery['party'] >['vestingBalancesSummary']; export type RewardPotProps = { pubKey: string | undefined; accounts: Account[] | null; assetId: string; // VEGA vestingBalancesSummary: VestingBalances | undefined; }; export const RewardPot = ({ pubKey, accounts, assetId, vestingBalancesSummary, }: RewardPotProps) => { const t = useT(); // TODO: Opening the sidebar for the first time works, but then clicking on redeem // for a different asset does not update the form const currentRouteId = useGetCurrentRouteId(); const setViews = useSidebar((store) => store.setViews); // All vested rewards accounts const availableRewardAssetAccounts = accounts ? accounts.filter((a) => { return ( a.asset.id === assetId && a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS ); }) : []; // Sum of all vested reward account balances const totalVestedRewardsByRewardAsset = BigNumber.sum.apply( null, availableRewardAssetAccounts.length ? availableRewardAssetAccounts.map((a) => a.balance) : [0] ); const lockedEntries = vestingBalancesSummary?.lockedBalances?.filter( (b) => b.asset.id === assetId ); const lockedBalances = lockedEntries?.length ? lockedEntries.map((e) => e.balance) : [0]; const totalLocked = BigNumber.sum.apply(null, lockedBalances); const vestingEntries = vestingBalancesSummary?.vestingBalances?.filter( (b) => b.asset.id === assetId ); const vestingBalances = vestingEntries?.length ? vestingEntries.map((e) => e.balance) : [0]; const totalVesting = BigNumber.sum.apply(null, vestingBalances); const totalRewards = totalLocked .plus(totalVesting) .plus(totalVestedRewardsByRewardAsset); let rewardAsset = undefined; if (availableRewardAssetAccounts.length) { rewardAsset = availableRewardAssetAccounts[0].asset; } else if (lockedEntries?.length) { rewardAsset = lockedEntries[0].asset; } else if (vestingEntries?.length) { rewardAsset = vestingEntries[0].asset; } if (!pubKey) { return (

{t('Not connected')}

); } return (
{rewardAsset ? ( <>
{t('Locked {{assetSymbol}}', { assetSymbol: rewardAsset.symbol, })} {addDecimalsFormatNumberQuantum( totalLocked.toString(), rewardAsset.decimals, rewardAsset.quantum )} {t('Vesting {{assetSymbol}}', { assetSymbol: rewardAsset.symbol, })} {addDecimalsFormatNumberQuantum( totalVesting.toString(), rewardAsset.decimals, rewardAsset.quantum )} {t('Available to withdraw this epoch')} {addDecimalsFormatNumberQuantum( totalVestedRewardsByRewardAsset.toString(), rewardAsset.decimals, rewardAsset.quantum )} {totalVestedRewardsByRewardAsset.isGreaterThan(0) && (
setViews( { type: ViewType.Transfer, assetId }, currentRouteId ) } size="small" data-testid="redeem-rewards-button" > {t('Redeem rewards')}
)}
) : (

{t('No rewards')}

)}
); }; export const Vesting = ({ pubKey, baseRate, multiplier, }: { pubKey: string | undefined; baseRate: string; multiplier?: string; }) => { const t = useT(); const rate = new BigNumber(baseRate).times(multiplier || 1); const rateFormatted = formatPercentage(Number(rate)); const baseRateFormatted = formatPercentage(Number(baseRate)); return (
{t('Base rate')} {baseRateFormatted}% {pubKey && ( {t('Vesting multiplier')} {multiplier ? `${multiplier}x` : '-'} )}
); }; export const Multipliers = ({ pubKey, streakMultiplier, hoarderMultiplier, }: { pubKey: string | undefined; streakMultiplier?: string; hoarderMultiplier?: string; }) => { const t = useT(); const combinedMultiplier = new BigNumber(streakMultiplier || 1).times( hoarderMultiplier || 1 ); if (!pubKey) { return (

{t('Not connected')}

); } return (
{t('Streak reward multiplier')} {streakMultiplier ? `${streakMultiplier}x` : '-'} {t('Hoarder reward multiplier')} {hoarderMultiplier ? `${hoarderMultiplier}x` : '-'}
); };