import { useT } from '../../lib/use-t'; import { addDecimalsFormatNumber, formatNumber } from '@vegaprotocol/utils'; import classNames from 'classnames'; import { type VegaIconSize, Tooltip, VegaIcon, VegaIconNames, truncateMiddle, } from '@vegaprotocol/ui-toolkit'; import { DistributionStrategyDescriptionMapping, DistributionStrategyMapping, EntityScope, EntityScopeMapping, DispatchMetric, DispatchMetricDescription, DispatchMetricLabels, EntityScopeLabelMapping, MarketState, type DispatchStrategy, IndividualScopeDescriptionMapping, AccountType, DistributionStrategy, IndividualScope, type Asset, type Team, IndividualScopeMapping, } from '@vegaprotocol/types'; import { type ReactNode } from 'react'; import { type BasicAssetDetails } from '@vegaprotocol/assets'; import { type EnrichedRewardTransfer } from '../../lib/hooks/use-rewards'; import compact from 'lodash/compact'; import BigNumber from 'bignumber.js'; import { useTWAPQuery } from '../../lib/hooks/__generated__/Rewards'; import { RankPayoutTable } from './rank-table'; const Tick = () => ( ); const Cross = () => ( ); /** Eligibility requirements for rewards */ export type Requirements = { isEligible: boolean; stakeAvailable?: bigint; team?: Partial; pubKey: string; }; const RewardCard = ({ colour, rewardAmount, rewardAsset, transferAsset, vegaAsset, dispatchStrategy, startsIn, endsIn, dispatchMetricInfo, requirements, gameId, }: { 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 starts. */ startsIn?: number; /** The number of epochs until the transfer stops. */ endsIn?: number; dispatchMetricInfo?: ReactNode; /** Eligibility requirements for rewards */ requirements?: Requirements; /** The game id of the transfer */ gameId?: string | null; }) => { const t = useT(); return (
{/** ENTITY SCOPE */}
{dispatchStrategy.entityScope && ( {EntityScopeLabelMapping[dispatchStrategy.entityScope] || t('Unspecified')} )}
{/** AMOUNT AND DISTRIBUTION STRATEGY */}
{/** AMOUNT */}

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

{/** DISTRIBUTION STRATEGY */}

{t( DistributionStrategyDescriptionMapping[ dispatchStrategy.distributionStrategy ] )} .

{dispatchStrategy.rankTable && t( 'Payout percentages are base estimates assuming no individual reward multipliers are active. If users in teams have active multipliers, the reward amounts may vary.' )}

{dispatchStrategy.rankTable && ( )}
} underline={true} > { DistributionStrategyMapping[ dispatchStrategy.distributionStrategy ] }
{/** DISTRIBUTION DELAY */}
{t('numberEpochs', '{{count}} epochs', { count: dispatchStrategy.lockPeriod, })}
{/** DISPATCH METRIC */} {dispatchMetricInfo ? ( dispatchMetricInfo ) : ( {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} )}
{/** ENDS IN or STARTS IN */} {startsIn ? ( {t('Starts in')} {t('numberEpochs', '{{count}} epochs', { count: startsIn, })} ) : ( endsIn && ( {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, })} {/** CAPPED AT */} {dispatchStrategy.capRewardFeeMultiple && ( {t('Capped at')} x{dispatchStrategy.capRewardFeeMultiple} )}
{/** DISPATCH METRIC DESCRIPTION */} {dispatchStrategy?.dispatchMetric && (

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

)} {/** REQUIREMENTS */} {dispatchStrategy && ( )}
); }; const StakingRewardCard = ({ colour, rewardAmount, rewardAsset, startsIn, endsIn, requirements, vegaAsset, gameId, }: { colour: CardColour; rewardAmount: string; /** The asset linked to the dispatch strategy via `dispatchMetricAssetId` property. */ rewardAsset?: Asset; /** The number of epochs until the transfer starts. */ startsIn?: number; /** The number of epochs until the transfer stops. */ endsIn?: number; /** The VEGA asset details, required to format the min staking amount. */ vegaAsset?: BasicAssetDetails; /** Eligibility requirements for rewards */ requirements?: Requirements; /** The game id of the transfer */ gameId?: string | null; }) => { const t = useT(); const stakeAvailable = requirements?.stakeAvailable; const tickOrCross = requirements ? ( stakeAvailable && stakeAvailable > 1 ? ( ) : ( ) ) : null; 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 or STARTS IN */} {startsIn ? ( {t('Starts in')} {t('numberEpochs', '{{count}} epochs', { count: startsIn, })} ) : ( endsIn && ( {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')}
{tickOrCross} {t('Individual')}
{t('Staked VEGA')}
{stakeAvailable ? ( stakeAvailable > 1 ? ( ) : ( ) ) : undefined} {stakeAvailable ? addDecimalsFormatNumber( stakeAvailable?.toString() || '0', vegaAsset?.decimals || 18, // vega asset decimals 6 ) : '1.00'}
{t('Average position')}
{' '} {tickOrCross} {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, requirements, gameId, startsIn, }: { dispatchStrategy: DispatchStrategy; rewardAsset?: BasicAssetDetails; vegaAsset?: BasicAssetDetails; requirements?: Requirements; gameId?: string | null; startsIn?: number; }) => { const t = useT(); const entityLabel = EntityScopeLabelMapping[dispatchStrategy.entityScope]; const stakingRequirement = dispatchStrategy.stakingRequirement; const stakeAvailable = requirements?.stakeAvailable; const averagePositionRequirements = dispatchStrategy.notionalTimeWeightedAveragePositionRequirement; const { data: twap } = useTWAPQuery({ variables: { gameId: gameId || '', partyId: requirements?.pubKey || '', assetId: rewardAsset?.id || '', }, skip: !requirements, errorPolicy: 'ignore', }); const averagePosition = twap?.timeWeightedNotionalPosition?.timeWeightedNotionalPosition; const averagePositionFormatted = averagePosition && addDecimalsFormatNumber(averagePosition, rewardAsset?.decimals || 0); const averagePositionRequirementsFormatted = averagePositionRequirements && addDecimalsFormatNumber( averagePositionRequirements, rewardAsset?.decimals || 0 ); return (
{entityLabel ? t('{{entity}} scope', { entity: entityLabel, }) : t('Scope')}
{t('Staked VEGA')}
{!stakingRequirement ? '' : requirements && (new BigNumber( stakingRequirement.toString() || 0 ).isLessThanOrEqualTo(stakeAvailable?.toString() || 0) ? ( ) : ( ))} {!stakingRequirement ? '-' : requirements && stakeAvailable ? addDecimalsFormatNumber( (stakeAvailable || 0).toString(), vegaAsset?.decimals || 18 ) : addDecimalsFormatNumber( stakingRequirement.toString() || 0, vegaAsset?.decimals || 18 )}
{t('Average position')}
{requirements && averagePositionRequirements && !startsIn && (new BigNumber(averagePosition || 0).isGreaterThan( averagePositionRequirements ) ? ( ) : ( ))} {averagePositionFormatted || averagePositionRequirementsFormatted || '-'}
); }; const RewardEntityScope = ({ dispatchStrategy, requirements, }: { dispatchStrategy: DispatchStrategy; requirements?: Requirements; }) => { const t = useT(); const listedTeams = dispatchStrategy.teamScope; if (!requirements) { 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 isEligible = () => { const isInTeam = listedTeams?.find((team) => team === requirements?.team?.teamId) || false; const teamsList = listedTeams && (
    {listedTeams.map((teamId) => { if (!teamId) return null; return
  • {truncateMiddle(teamId)}
  • ; })}
); if ( dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_TEAMS && !listedTeams && !requirements?.team ) { return { tooltip: t('Not in a team'), eligible: false }; } if ( dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_TEAMS && !listedTeams ) { return { tooltip: t('All teams'), eligible: true }; } if ( dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_TEAMS && listedTeams ) { return { tooltip: (

{t('Eligible teams')}

{teamsList}
), eligible: isInTeam, }; } if (dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_INDIVIDUALS) { switch (dispatchStrategy.individualScope) { case IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM: return { tooltip: (

{t('Teams individuals')}

{teamsList}
), eligible: isInTeam, }; case IndividualScope.INDIVIDUAL_SCOPE_NOT_IN_TEAM: return { tooltip: t('Solo individuals'), eligible: true }; case IndividualScope.INDIVIDUAL_SCOPE_ALL: return { tooltip: t('All individuals'), eligible: true }; } } return { tooltip: t('Unspecified'), eligible: false }; }; const { tooltip, eligible } = isEligible(); const tickOrCross = requirements ? eligible ? : : null; const eligibilityLabel = eligible ? t('Eligible') : t('Not eligible'); return ( {tickOrCross} {requirements ? eligibilityLabel : tooltip} ); }; enum CardColour { BLUE = 'BLUE', GREEN = 'GREEN', GREY = 'GREY', ORANGE = 'ORANGE', PINK = 'PINK', PURPLE = 'PURPLE', WHITE = 'WHITE', YELLOW = 'YELLOW', } 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 } > ); }; export const areAllMarketsSettled = (transferNode: EnrichedRewardTransfer) => { const settledMarkets = transferNode.markets?.filter( (m) => m?.data?.marketState && [ MarketState.STATE_TRADING_TERMINATED, MarketState.STATE_SETTLED, MarketState.STATE_CANCELLED, MarketState.STATE_CLOSED, ].includes(m?.data?.marketState) ); return ( settledMarkets?.length === transferNode.markets?.length && Boolean(transferNode.markets && transferNode.markets.length > 0) ); }; export const areAllMarketsSuspended = ( transferNode: EnrichedRewardTransfer ) => { return ( transferNode.markets?.filter( (m) => m?.data?.marketState === MarketState.STATE_SUSPENDED || m?.data?.marketState === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE ).length === transferNode.markets?.length && Boolean(transferNode.markets && transferNode.markets.length > 0) ); }; export const ActiveRewardCard = ({ transferNode, currentEpoch, requirements, }: { transferNode: EnrichedRewardTransfer; currentEpoch: number; requirements?: Requirements; }) => { const startsIn = transferNode.transfer.kind.startEpoch - currentEpoch; const endsIn = transferNode.transfer.kind.endEpoch != null ? transferNode.transfer.kind.endEpoch - currentEpoch : undefined; if ( !transferNode.transfer.kind.dispatchStrategy && transferNode.transfer.toAccountType === AccountType.ACCOUNT_TYPE_GLOBAL_REWARD ) { return ( 0 ? startsIn : undefined} endsIn={endsIn} requirements={requirements} gameId={transferNode.transfer.gameId} /> ); } let colour = DispatchMetricColourMap[ transferNode.transfer.kind.dispatchStrategy.dispatchMetric ]; /** * Display the card as grey if any of the condition is `true`: * * - all markets scoped to the reward are settled * - all markets scoped to the reward are suspended * - the reward's asset is not actively traded on any of the active markets * - it start in the future * */ if ( areAllMarketsSettled(transferNode) || areAllMarketsSuspended(transferNode) || !transferNode.isAssetTraded || startsIn > 0 ) { colour = CardColour.GREY; } return ( 0 ? startsIn : undefined} endsIn={endsIn} dispatchStrategy={transferNode.transfer.kind.dispatchStrategy} dispatchMetricInfo={} requirements={requirements} gameId={transferNode.transfer.gameId} /> ); };