vega-frontend-monorepo/apps/trading/components/rewards-container/reward-card.tsx

1058 lines
33 KiB
TypeScript
Raw Normal View History

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 = () => (
<VegaIcon
name={VegaIconNames.TICK}
size={12}
className="text-vega-green-500"
/>
);
const Cross = () => (
<VegaIcon
name={VegaIconNames.CROSS}
size={12}
className="text-vega-red-500"
/>
);
/** Eligibility requirements for rewards */
export type Requirements = {
isEligible: boolean;
stakeAvailable?: bigint;
team?: Partial<Team>;
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 (
<div>
<div
className={classNames(
'bg-gradient-to-r col-span-full p-0.5 lg:col-auto h-full',
'rounded-lg',
CardColourStyles[colour].gradientClassName
)}
data-testid="active-rewards-card"
>
<div
className={classNames(
CardColourStyles[colour].mainClassName,
'bg-gradient-to-b bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded-md p-4 flex flex-col gap-4'
)}
>
<div className="flex justify-between gap-4">
{/** ENTITY SCOPE */}
<div className="flex flex-col gap-2 items-center text-center">
<EntityIcon entityScope={dispatchStrategy.entityScope} />
{dispatchStrategy.entityScope && (
<span className="text-muted text-xs" data-testid="entity-scope">
{EntityScopeLabelMapping[dispatchStrategy.entityScope] ||
t('Unspecified')}
</span>
)}
</div>
{/** AMOUNT AND DISTRIBUTION STRATEGY */}
<div className="flex flex-col gap-2 items-center text-center">
{/** AMOUNT */}
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
<span className="font-glitch" data-testid="reward-value">
{rewardAmount}
</span>
<span className="font-alpha" data-testid="reward-asset">
{transferAsset?.symbol || ''}
</span>
</h3>
{/** DISTRIBUTION STRATEGY */}
<Tooltip
description={
<div className="flex flex-col gap-4">
<p>
{t(
DistributionStrategyDescriptionMapping[
dispatchStrategy.distributionStrategy
]
)}
.
</p>
<p>
{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.'
)}
</p>
{dispatchStrategy.rankTable && (
<RankPayoutTable rankTable={dispatchStrategy.rankTable} />
)}
</div>
}
underline={true}
>
<span className="text-xs" data-testid="distribution-strategy">
{
DistributionStrategyMapping[
dispatchStrategy.distributionStrategy
]
}
</span>
</Tooltip>
</div>
{/** DISTRIBUTION DELAY */}
<div className="flex flex-col gap-2 items-center text-center">
<CardIcon
iconName={VegaIconNames.LOCK}
tooltip={t(
'Number of epochs after distribution to delay vesting of rewards by'
)}
/>
<span
className="text-muted text-xs whitespace-nowrap"
data-testid="locked-for"
>
{t('numberEpochs', '{{count}} epochs', {
count: dispatchStrategy.lockPeriod,
})}
</span>
</div>
</div>
<span className="border-[0.5px] dark:border-vega-cdark-500 border-vega-clight-500" />
{/** DISPATCH METRIC */}
{dispatchMetricInfo ? (
dispatchMetricInfo
) : (
<span data-testid="dispatch-metric-info">
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]}
</span>
)}
<div className="flex items-center gap-8 flex-wrap">
{/** ENDS IN or STARTS IN */}
{startsIn ? (
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Starts in')} </span>
<span data-testid="starts-in" data-startsin={startsIn}>
{t('numberEpochs', '{{count}} epochs', {
count: startsIn,
})}
</span>
</span>
) : (
endsIn && (
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Ends in')} </span>
<span data-testid="ends-in" data-endsin={endsIn}>
{endsIn >= 0
? t('numberEpochs', '{{count}} epochs', {
count: endsIn,
})
: t('Ended')}
</span>
</span>
)
)}
{/** WINDOW LENGTH */}
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Assessed over')}</span>
<span data-testid="assessed-over">
{t('numberEpochs', '{{count}} epochs', {
count: dispatchStrategy.windowLength,
})}
</span>
</span>
{/** CAPPED AT */}
{dispatchStrategy.capRewardFeeMultiple && (
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Capped at')}</span>
<Tooltip
description={t(
'Reward will be capped at {{capRewardFeeMultiple}} X of taker fees paid in the epoch',
{
capRewardFeeMultiple:
dispatchStrategy.capRewardFeeMultiple,
}
)}
>
<span data-testid="cappedAt">
x{dispatchStrategy.capRewardFeeMultiple}
</span>
</Tooltip>
</span>
)}
</div>
{/** DISPATCH METRIC DESCRIPTION */}
{dispatchStrategy?.dispatchMetric && (
<p className="text-muted text-sm h-16">
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
</p>
)}
<span className="border-[0.5px] dark:border-vega-cdark-500 border-vega-clight-500" />
{/** REQUIREMENTS */}
{dispatchStrategy && (
<RewardRequirements
dispatchStrategy={dispatchStrategy}
rewardAsset={rewardAsset}
vegaAsset={vegaAsset}
requirements={requirements}
gameId={gameId}
startsIn={startsIn}
/>
)}
</div>
</div>
</div>
);
};
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 ? (
<Tick />
) : (
<Cross />
)
) : null;
return (
<div>
<div
className={classNames(
'bg-gradient-to-r col-span-full p-0.5 lg:col-auto h-full',
'rounded-lg',
CardColourStyles[colour].gradientClassName
)}
data-testid="active-rewards-card"
>
<div
className={classNames(
CardColourStyles[colour].mainClassName,
'bg-gradient-to-b bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded-md p-4 flex flex-col gap-4'
)}
>
<div className="flex justify-between gap-4">
{/** ENTITY SCOPE */}
<div className="flex flex-col gap-2 items-center text-center">
<EntityIcon entityScope={EntityScope.ENTITY_SCOPE_INDIVIDUALS} />
{
<span className="text-muted text-xs" data-testid="entity-scope">
{EntityScopeLabelMapping[
EntityScope.ENTITY_SCOPE_INDIVIDUALS
] || t('Unspecified')}
</span>
}
</div>
{/** AMOUNT AND DISTRIBUTION STRATEGY */}
<div className="flex flex-col gap-2 items-center text-center">
{/** AMOUNT */}
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
<span className="font-glitch" data-testid="reward-value">
{rewardAmount}
</span>
<span className="font-alpha">{rewardAsset?.symbol || ''}</span>
</h3>
{/** DISTRIBUTION STRATEGY */}
<Tooltip
description={t(
DistributionStrategyDescriptionMapping[
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA
]
)}
underline={true}
>
<span className="text-xs" data-testid="distribution-strategy">
{
DistributionStrategyMapping[
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA
]
}
</span>
</Tooltip>
</div>
{/** DISTRIBUTION DELAY */}
<div className="flex flex-col gap-2 items-center text-center">
<CardIcon
iconName={VegaIconNames.LOCK}
tooltip={t(
'Number of epochs after distribution to delay vesting of rewards by'
)}
/>
<span
className="text-muted text-xs whitespace-nowrap"
data-testid="locked-for"
>
{t('numberEpochs', '{{count}} epochs', {
count: 0,
})}
</span>
</div>
</div>
<span className="border-[0.5px] dark:border-vega-cdark-500 border-vega-clight-500" />
{/** DISPATCH METRIC */}
{
<span data-testid="dispatch-metric-info">
{t('Staking rewards')}
</span>
}
<div className="flex items-center gap-8 flex-wrap">
{/** ENDS IN or STARTS IN */}
{startsIn ? (
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Starts in')} </span>
<span data-testid="starts-in" data-startsin={startsIn}>
{t('numberEpochs', '{{count}} epochs', {
count: startsIn,
})}
</span>
</span>
) : (
endsIn && (
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Ends in')} </span>
<span data-testid="ends-in" data-endsin={endsIn}>
{endsIn >= 0
? t('numberEpochs', '{{count}} epochs', {
count: endsIn,
})
: t('Ended')}
</span>
</span>
)
)}
{/** WINDOW LENGTH */}
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Assessed over')}</span>
<span data-testid="assessed-over">
{t('numberEpochs', '{{count}} epochs', {
count: 1,
})}
</span>
</span>
</div>
{/** DISPATCH METRIC DESCRIPTION */}
{
<p className="text-muted text-sm h-16">
{t(
'Global staking reward for staking $VEGA on the network via the Governance app'
)}
</p>
}
<span className="border-[0.5px] dark:border-vega-cdark-500 border-vega-clight-500" />
{/** REQUIREMENTS */}
<dl className="flex justify-between flex-wrap items-center gap-3 text-xs">
<div className="flex flex-col gap-1">
<dt className="flex items-center gap-1 text-muted">
{t('Team scope')}
</dt>
<dd className="flex items-center gap-1" data-testid="scope">
<Tooltip
description={
IndividualScopeDescriptionMapping[
IndividualScope.INDIVIDUAL_SCOPE_ALL
]
}
>
<span>
{tickOrCross} {t('Individual')}
</span>
</Tooltip>
</dd>
</div>
<div className="flex flex-col gap-1">
<dt className="flex items-center gap-1 text-muted">
{t('Staked VEGA')}
</dt>
<dd
className="flex items-center gap-1"
data-testid="staking-requirement"
>
{stakeAvailable ? (
stakeAvailable > 1 ? (
<Tick />
) : (
<Cross />
)
) : undefined}
{stakeAvailable
? addDecimalsFormatNumber(
stakeAvailable?.toString() || '0',
vegaAsset?.decimals || 18, // vega asset decimals
6
)
: '1.00'}
</dd>
</div>
<div className="flex flex-col gap-1">
<dt className="flex items-center gap-1 text-muted">
{t('Average position')}
</dt>
<dd
className="flex items-center gap-1"
data-testid="average-position"
>
{' '}
{tickOrCross}
{formatNumber(0, 2)}
</dd>
</div>
</dl>
</div>
</div>
</div>
);
};
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 = <span>{reward.dispatchAsset.symbol}</span>;
}
// but if scoped to only one market then display market name
if (marketNames.length === 1) {
additionalDispatchMetricInfo = <span>{marketNames[0]}</span>;
}
// or if scoped to many markets then indicate it's scoped to "specific markets"
if (marketNames.length > 1) {
additionalDispatchMetricInfo = (
<Tooltip description={marketNames.join(', ')}>
<span>{t('Specific markets')}</span>
</Tooltip>
);
}
return (
<span data-testid="dispatch-metric-info" className="h-12">
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]}
{additionalDispatchMetricInfo != null && (
<> {additionalDispatchMetricInfo}</>
)}
</span>
);
};
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 (
<dl className="flex justify-between flex-wrap items-center gap-3 text-xs">
<div className="flex flex-col gap-1">
<dt className="flex items-center gap-1 text-muted">
{entityLabel
? t('{{entity}} scope', {
entity: entityLabel,
})
: t('Scope')}
</dt>
<dd className="flex items-center gap-1" data-testid="scope">
<RewardEntityScope
dispatchStrategy={dispatchStrategy}
requirements={requirements}
/>
</dd>
</div>
<div className="flex flex-col gap-1">
<dt className="flex items-center gap-1 text-muted">
{t('Staked VEGA')}
</dt>
<dd
className="flex items-center gap-1"
data-testid="staking-requirement"
>
{!stakingRequirement
? ''
: requirements &&
(new BigNumber(
stakingRequirement.toString() || 0
).isLessThanOrEqualTo(stakeAvailable?.toString() || 0) ? (
<Tick />
) : (
<Cross />
))}
{!stakingRequirement
? '-'
: requirements && stakeAvailable
? addDecimalsFormatNumber(
(stakeAvailable || 0).toString(),
vegaAsset?.decimals || 18
)
: addDecimalsFormatNumber(
stakingRequirement.toString() || 0,
vegaAsset?.decimals || 18
)}
</dd>
</div>
<div className="flex flex-col gap-1">
<dt className="flex items-center gap-1 text-muted">
{t('Average position')}
</dt>
<Tooltip
description={
averagePosition
? t(
'Your average position is {{averagePosition}}, and the requirement is {{averagePositionRequirements}}',
{
averagePosition: averagePositionFormatted,
averagePositionRequirements:
averagePositionRequirementsFormatted,
}
)
: averagePositionRequirements &&
t(
'The average position requirement is {{averagePositionRequirements}}',
{
averagePositionRequirements:
averagePositionRequirementsFormatted,
}
)
}
>
<dd
className="flex items-center gap-1"
data-testid="average-position"
>
{requirements &&
averagePositionRequirements &&
!startsIn &&
(new BigNumber(averagePosition || 0).isGreaterThan(
averagePositionRequirements
) ? (
<Tick />
) : (
<Cross />
))}
{averagePositionFormatted ||
averagePositionRequirementsFormatted ||
'-'}
</dd>
</Tooltip>
</div>
</dl>
);
};
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 (
<Tooltip
description={
dispatchStrategy.teamScope?.length ? (
<div className="text-xs">
<p className="mb-1">{t('Eligible teams')}</p>
<ul>
{dispatchStrategy.teamScope.map((teamId) => {
if (!teamId) return null;
return <li key={teamId}>{truncateMiddle(teamId)}</li>;
})}
</ul>
</div>
) : (
t('All teams are eligible')
)
}
>
<span>
{dispatchStrategy.teamScope?.length
? t('Some teams')
: t('All teams')}
</span>
</Tooltip>
);
}
if (
dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
dispatchStrategy.individualScope
) {
return (
<Tooltip
description={
IndividualScopeDescriptionMapping[dispatchStrategy.individualScope]
}
>
<span>
{IndividualScopeMapping[dispatchStrategy.individualScope]}
</span>
</Tooltip>
);
}
return t('Unspecified');
}
const isEligible = () => {
const isInTeam =
listedTeams?.find((team) => team === requirements?.team?.teamId) || false;
const teamsList = listedTeams && (
<ul>
{listedTeams.map((teamId) => {
if (!teamId) return null;
return <li key={teamId}>{truncateMiddle(teamId)}</li>;
})}
</ul>
);
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: (
<div className="text-xs">
<p className="mb-1">{t('Eligible teams')}</p> {teamsList}
</div>
),
eligible: isInTeam,
};
}
if (dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_INDIVIDUALS) {
switch (dispatchStrategy.individualScope) {
case IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM:
return {
tooltip: (
<div className="text-xs">
<p className="mb-1">{t('Teams individuals')}</p> {teamsList}
</div>
),
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 ? <Tick /> : <Cross /> : null;
const eligibilityLabel = eligible ? t('Eligible') : t('Not eligible');
return (
<Tooltip description={tooltip}>
<span className="flex items-center gap-1">
{tickOrCross} {requirements ? eligibilityLabel : tooltip}
</span>
</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<DispatchMetric, CardColour> = {
// 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 description={<span>{tooltip}</span>}>
<span className="flex items-center p-2 rounded-full border border-gray-600">
<VegaIcon name={iconName} size={size} />
</span>
</Tooltip>
);
};
const EntityScopeIconMap: Record<EntityScope, VegaIconNames> = {
[EntityScope.ENTITY_SCOPE_TEAMS]: VegaIconNames.TEAM,
[EntityScope.ENTITY_SCOPE_INDIVIDUALS]: VegaIconNames.MAN,
};
const EntityIcon = ({
entityScope,
size = 18,
}: {
entityScope: EntityScope;
size?: VegaIconSize;
}) => {
return (
<Tooltip
description={
entityScope ? <span>{EntityScopeMapping[entityScope]}</span> : undefined
}
>
<span className="flex items-center p-2 rounded-full border border-gray-600">
<VegaIcon
name={EntityScopeIconMap[entityScope] || VegaIconNames.QUESTION_MARK}
size={size}
/>
</span>
</Tooltip>
);
};
2024-03-25 16:25:50 +00:00
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)
);
2024-03-25 16:25:50 +00:00
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
2024-03-25 16:25:50 +00:00
).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 (
<StakingRewardCard
colour={CardColour.WHITE}
rewardAmount={addDecimalsFormatNumber(
transferNode.transfer.amount,
transferNode.transfer.asset?.decimals || 0,
6
)}
rewardAsset={transferNode.transfer.asset || undefined}
startsIn={startsIn > 0 ? startsIn : undefined}
endsIn={endsIn}
requirements={requirements}
gameId={transferNode.transfer.gameId}
/>
);
}
let colour =
DispatchMetricColourMap[
transferNode.transfer.kind.dispatchStrategy.dispatchMetric
];
2024-03-25 16:25:50 +00:00
/**
* 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 (
<RewardCard
colour={colour}
rewardAmount={addDecimalsFormatNumber(
transferNode.transfer.amount,
transferNode.transfer.asset?.decimals || 0,
6
)}
rewardAsset={transferNode.dispatchAsset}
transferAsset={transferNode.transfer.asset || undefined}
startsIn={startsIn > 0 ? startsIn : undefined}
endsIn={endsIn}
dispatchStrategy={transferNode.transfer.kind.dispatchStrategy}
dispatchMetricInfo={<DispatchMetricInfo reward={transferNode} />}
requirements={requirements}
gameId={transferNode.transfer.gameId}
/>
);
};