import { useT } from '../../lib/use-t'; import { addDecimalsFormatNumber, formatNumber } from '@vegaprotocol/utils'; import classNames from 'classnames'; import { type VegaIconSize, Tooltip, VegaIcon, VegaIconNames, TradingInput, TinyScroll, truncateMiddle, } from '@vegaprotocol/ui-toolkit'; import { type TransferNode, DistributionStrategyDescriptionMapping, DistributionStrategyMapping, EntityScope, EntityScopeMapping, DispatchMetric, DispatchMetricDescription, DispatchMetricLabels, EntityScopeLabelMapping, MarketState, type DispatchStrategy, IndividualScopeMapping, IndividualScopeDescriptionMapping, AccountType, DistributionStrategy, IndividualScope, type Asset, } from '@vegaprotocol/types'; import { Card } from '../card/card'; import { type ReactNode, useState } from 'react'; import { type AssetFieldsFragment, type BasicAssetDetails, } from '@vegaprotocol/assets'; import { type MarketFieldsFragment } from '@vegaprotocol/markets'; import { type EnrichedRewardTransfer, useRewards, } from '../../lib/hooks/use-rewards'; import compact from 'lodash/compact'; enum CardColour { BLUE = 'BLUE', GREEN = 'GREEN', GREY = 'GREY', ORANGE = 'ORANGE', PINK = 'PINK', PURPLE = 'PURPLE', WHITE = 'WHITE', YELLOW = 'YELLOW', } export type Filter = { searchTerm: string; }; export const applyFilter = ( node: TransferNode & { asset?: AssetFieldsFragment | null; markets?: (MarketFieldsFragment | null)[]; }, filter: Filter ) => { const { transfer } = node; // if the transfer is a staking reward then it should be displayed if (transfer.toAccountType === AccountType.ACCOUNT_TYPE_GLOBAL_REWARD) { return true; } if (transfer.kind.__typename !== 'RecurringTransfer') { return false; } if ( (transfer.kind.dispatchStrategy?.dispatchMetric && DispatchMetricLabels[transfer.kind.dispatchStrategy.dispatchMetric] .toLowerCase() .includes(filter.searchTerm.toLowerCase())) || transfer.asset?.symbol .toLowerCase() .includes(filter.searchTerm.toLowerCase()) || ( (transfer.kind.dispatchStrategy && EntityScopeLabelMapping[transfer.kind.dispatchStrategy.entityScope]) || 'Unspecified' ) .toLowerCase() .includes(filter.searchTerm.toLowerCase()) || node.asset?.name .toLocaleLowerCase() .includes(filter.searchTerm.toLowerCase()) || node.markets?.some((m) => m?.tradableInstrument?.instrument?.name .toLocaleLowerCase() .includes(filter.searchTerm.toLowerCase()) ) ) { return true; } return false; }; export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => { const t = useT(); const { data } = useRewards({ onlyActive: true, }); const [filter, setFilter] = useState({ searchTerm: '', }); if (!data || !data.length) return null; return ( {/** CARDS FILTER */} {data.length > 1 && ( setFilter((curr) => ({ ...curr, searchTerm: e.target.value })) } value={filter.searchTerm} type="text" placeholder={t( 'Search by reward dispatch metric, entity scope or asset name' )} data-testid="search-term" className="mb-4 w-20 mr-2" prependElement={} /> )} {/** CARDS */} {data .filter((n) => applyFilter(n, filter)) .map((node, i) => ( ))} ); }; type ActiveRewardCardProps = { transferNode: EnrichedRewardTransfer; currentEpoch: number; }; export const ActiveRewardCard = ({ transferNode, currentEpoch, }: ActiveRewardCardProps) => { // don't display the cards that are scoped to not trading markets const marketSettled = transferNode.markets?.filter( (m) => m?.state && [ MarketState.STATE_TRADING_TERMINATED, MarketState.STATE_SETTLED, MarketState.STATE_CANCELLED, MarketState.STATE_CLOSED, ].includes(m.state) ); // hide the card if all of the markets are being marked as e.g. settled if ( marketSettled?.length === transferNode.markets?.length && Boolean(transferNode.markets && transferNode.markets.length > 0) ) { return null; } if ( !transferNode.transfer.kind.dispatchStrategy && transferNode.transfer.toAccountType === AccountType.ACCOUNT_TYPE_GLOBAL_REWARD ) { return ( ); } let colour = DispatchMetricColourMap[ transferNode.transfer.kind.dispatchStrategy.dispatchMetric ]; // grey out of any of the markets is suspended or // if the asset is not currently traded on any of the active markets const marketSuspended = transferNode.markets?.filter( (m) => m?.state === MarketState.STATE_SUSPENDED || m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE ).length === transferNode.markets?.length && Boolean(transferNode.markets && transferNode.markets.length > 0); if (marketSuspended || !transferNode.isAssetTraded) { colour = CardColour.GREY; } return ( } /> ); }; const RewardCard = ({ colour, rewardAmount, rewardAsset, transferAsset, vegaAsset, dispatchStrategy, endsIn, dispatchMetricInfo, }: { colour: CardColour; rewardAmount: string; /** The asset linked to the dispatch strategy via `dispatchMetricAssetId` property. */ rewardAsset?: BasicAssetDetails; /** The VEGA asset details, required to format the min staking amount. */ transferAsset?: Asset | undefined; /** The VEGA asset details, required to format the min staking amount. */ vegaAsset?: BasicAssetDetails; /** The transfer's dispatch strategy. */ dispatchStrategy: DispatchStrategy; /** The number of epochs until the transfer stops. */ endsIn?: number; dispatchMetricInfo?: ReactNode; }) => { const t = useT(); return (
{/** ENTITY SCOPE */}
{dispatchStrategy.entityScope && ( {EntityScopeLabelMapping[dispatchStrategy.entityScope] || t('Unspecified')} )}
{/** AMOUNT AND DISTRIBUTION STRATEGY */}
{/** AMOUNT */}

{rewardAmount} {transferAsset?.symbol || ''}

{/** DISTRIBUTION STRATEGY */} { DistributionStrategyMapping[ dispatchStrategy.distributionStrategy ] }
{/** DISTRIBUTION DELAY */}
{t('numberEpochs', '{{count}} epochs', { count: dispatchStrategy.lockPeriod, })}
{/** DISPATCH METRIC */} {dispatchMetricInfo ? ( dispatchMetricInfo ) : ( {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} )}
{/** ENDS IN */} {endsIn != null && ( {t('Ends in')} {endsIn >= 0 ? t('numberEpochs', '{{count}} epochs', { count: endsIn, }) : t('Ended')} )} {/** WINDOW LENGTH */} {t('Assessed over')} {t('numberEpochs', '{{count}} epochs', { count: dispatchStrategy.windowLength, })}
{/** DISPATCH METRIC DESCRIPTION */} {dispatchStrategy?.dispatchMetric && (

{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}

)} {/** REQUIREMENTS */} {dispatchStrategy && ( )}
); }; const StakingRewardCard = ({ colour, rewardAmount, rewardAsset, endsIn, }: { colour: CardColour; rewardAmount: string; /** The asset linked to the dispatch strategy via `dispatchMetricAssetId` property. */ rewardAsset?: Asset; /** The number of epochs until the transfer stops. */ endsIn?: number; /** The VEGA asset details, required to format the min staking amount. */ vegaAsset?: BasicAssetDetails; }) => { const t = useT(); return (
{/** ENTITY SCOPE */}
{ {EntityScopeLabelMapping[ EntityScope.ENTITY_SCOPE_INDIVIDUALS ] || t('Unspecified')} }
{/** AMOUNT AND DISTRIBUTION STRATEGY */}
{/** AMOUNT */}

{rewardAmount} {rewardAsset?.symbol || ''}

{/** DISTRIBUTION STRATEGY */} { DistributionStrategyMapping[ DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA ] }
{/** DISTRIBUTION DELAY */}
{t('numberEpochs', '{{count}} epochs', { count: 0, })}
{/** DISPATCH METRIC */} { {t('Staking rewards')} }
{/** ENDS IN */} {endsIn != null && ( {t('Ends in')} {endsIn >= 0 ? t('numberEpochs', '{{count}} epochs', { count: endsIn, }) : t('Ended')} )} {/** WINDOW LENGTH */} {t('Assessed over')} {t('numberEpochs', '{{count}} epochs', { count: 1, })}
{/** DISPATCH METRIC DESCRIPTION */} {

{t( 'Global staking reward for staking $VEGA on the network via the Governance app' )}

} {/** REQUIREMENTS */}
{t('Team scope')}
{t('Individual')}
{t('Staked VEGA')}
{formatNumber(1, 2)}
{t('Average position')}
{formatNumber(0, 2)}
); }; export const DispatchMetricInfo = ({ reward, }: { reward: EnrichedRewardTransfer; }) => { const t = useT(); const dispatchStrategy = reward.transfer.kind.dispatchStrategy; const marketNames = compact( reward.markets?.map((m) => m.tradableInstrument.instrument.name) ); let additionalDispatchMetricInfo = null; // if asset found then display asset symbol if (reward.dispatchAsset) { additionalDispatchMetricInfo = {reward.dispatchAsset.symbol}; } // but if scoped to only one market then display market name if (marketNames.length === 1) { additionalDispatchMetricInfo = {marketNames[0]}; } // or if scoped to many markets then indicate it's scoped to "specific markets" if (marketNames.length > 1) { additionalDispatchMetricInfo = ( {t('Specific markets')} ); } return ( {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {additionalDispatchMetricInfo != null && ( <> • {additionalDispatchMetricInfo} )} ); }; const RewardRequirements = ({ dispatchStrategy, rewardAsset, vegaAsset, }: { dispatchStrategy: DispatchStrategy; rewardAsset?: BasicAssetDetails; vegaAsset?: BasicAssetDetails; }) => { const t = useT(); const entityLabel = EntityScopeLabelMapping[dispatchStrategy.entityScope]; return (
{entityLabel ? t('{{entity}} scope', { entity: entityLabel, }) : t('Scope')}
{t('Staked VEGA')}
{addDecimalsFormatNumber( dispatchStrategy?.stakingRequirement || 0, vegaAsset?.decimals || 18 )}
{t('Average position')}
{addDecimalsFormatNumber( dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement || 0, rewardAsset?.decimals || 0 )}
); }; const RewardEntityScope = ({ dispatchStrategy, }: { dispatchStrategy: DispatchStrategy; }) => { const t = useT(); if (dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_TEAMS) { return (

{t('Eligible teams')}

    {dispatchStrategy.teamScope.map((teamId) => { if (!teamId) return null; return
  • {truncateMiddle(teamId)}
  • ; })}
) : ( t('All teams are eligible') ) } > {dispatchStrategy.teamScope?.length ? t('Some teams') : t('All teams')}
); } if ( dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_INDIVIDUALS && dispatchStrategy.individualScope ) { return ( {IndividualScopeMapping[dispatchStrategy.individualScope]} ); } return t('Unspecified'); }; const CardColourStyles: Record< CardColour, { gradientClassName: string; mainClassName: string } > = { [CardColour.BLUE]: { gradientClassName: 'from-vega-blue-500 to-vega-green-400', mainClassName: 'from-vega-blue-400 dark:from-vega-blue-600 to-20%', }, [CardColour.GREEN]: { gradientClassName: 'from-vega-green-500 to-vega-yellow-500', mainClassName: 'from-vega-green-400 dark:from-vega-green-600 to-20%', }, [CardColour.GREY]: { gradientClassName: 'from-vega-cdark-500 to-vega-clight-200', mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%', }, [CardColour.ORANGE]: { gradientClassName: 'from-vega-orange-500 to-vega-pink-400', mainClassName: 'from-vega-orange-400 dark:from-vega-orange-600 to-20%', }, [CardColour.PINK]: { gradientClassName: 'from-vega-pink-500 to-vega-purple-400', mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%', }, [CardColour.PURPLE]: { gradientClassName: 'from-vega-purple-500 to-vega-blue-400', mainClassName: 'from-vega-purple-400 dark:from-vega-purple-600 to-20%', }, [CardColour.WHITE]: { gradientClassName: 'from-vega-clight-600 dark:from-vega-clight-900 to-vega-yellow-500 dark:to-vega-yellow-400', mainClassName: 'from-white dark:from-vega-clight-100 to-20%', }, [CardColour.YELLOW]: { gradientClassName: 'from-vega-yellow-500 to-vega-orange-400', mainClassName: 'from-vega-yellow-400 dark:from-vega-yellow-600 to-20%', }, }; const DispatchMetricColourMap: Record = { // Liquidity provision fees received [DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED]: CardColour.BLUE, // Price maker fees paid [DispatchMetric.DISPATCH_METRIC_MAKER_FEES_PAID]: CardColour.PINK, // Price maker fees earned [DispatchMetric.DISPATCH_METRIC_MAKER_FEES_RECEIVED]: CardColour.GREEN, // Total market value [DispatchMetric.DISPATCH_METRIC_MARKET_VALUE]: CardColour.WHITE, // Average position [DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION]: CardColour.ORANGE, // Relative return [DispatchMetric.DISPATCH_METRIC_RELATIVE_RETURN]: CardColour.PURPLE, // Return volatility [DispatchMetric.DISPATCH_METRIC_RETURN_VOLATILITY]: CardColour.YELLOW, // Validator ranking [DispatchMetric.DISPATCH_METRIC_VALIDATOR_RANKING]: CardColour.WHITE, }; const CardIcon = ({ size = 18, iconName, tooltip, }: { size?: VegaIconSize; iconName: VegaIconNames; tooltip: string; }) => { return ( {tooltip}}> ); }; const EntityScopeIconMap: Record = { [EntityScope.ENTITY_SCOPE_TEAMS]: VegaIconNames.TEAM, [EntityScope.ENTITY_SCOPE_INDIVIDUALS]: VegaIconNames.MAN, }; const EntityIcon = ({ entityScope, size = 18, }: { entityScope: EntityScope; size?: VegaIconSize; }) => { return ( {EntityScopeMapping[entityScope]} : undefined } > ); };