feat(trading): colourful rewards (#5812)

This commit is contained in:
Art 2024-02-20 13:20:35 +01:00 committed by GitHub
parent 9a37572f51
commit be6f395ce4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 769 additions and 605 deletions

View File

@ -2,8 +2,6 @@ import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '@sentry/react';
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
import { Intent, Loader, TradingButton } from '@vegaprotocol/ui-toolkit';
import { useGameCards } from '../../lib/hooks/use-game-cards';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import { Link, useNavigate } from 'react-router-dom';
import { Links } from '../../lib/links';
@ -18,6 +16,7 @@ import take from 'lodash/take';
import { usePageTitle } from '../../lib/hooks/use-page-title';
import { TeamCard } from '../../components/competitions/team-card';
import { useMyTeam } from '../../lib/hooks/use-my-team';
import { useRewards } from '../../lib/hooks/use-rewards';
export const CompetitionsHome = () => {
const t = useT();
@ -28,9 +27,9 @@ export const CompetitionsHome = () => {
const { data: epochData } = useEpochInfoQuery();
const currentEpoch = Number(epochData?.epoch.id);
const { data: gamesData, loading: gamesLoading } = useGameCards({
const { data: gamesData, loading: gamesLoading } = useRewards({
onlyActive: true,
currentEpoch,
scopeToTeams: true,
});
const { data: teamsData, loading: teamsLoading } = useTeams();

View File

@ -10,11 +10,7 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
TransferStatus,
type Asset,
type RecurringTransfer,
} from '@vegaprotocol/types';
import { TransferStatus, type Asset } from '@vegaprotocol/types';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { Table } from '../../components/table';
@ -44,11 +40,6 @@ import {
areTeamGames,
} from '../../lib/hooks/use-games';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import {
type EnrichedTransfer,
isScopedToTeams,
useGameCards,
} from '../../lib/hooks/use-game-cards';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import {
ActiveRewardCard,
@ -56,6 +47,11 @@ import {
} from '../../components/rewards-container/active-rewards';
import { type MarketMap, useMarketsMapProvider } from '@vegaprotocol/markets';
import format from 'date-fns/format';
import {
type EnrichedRewardTransfer,
isScopedToTeams,
useRewards,
} from '../../lib/hooks/use-rewards';
export const CompetitionsTeam = () => {
const t = useT();
@ -78,10 +74,9 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
const { data: games, loading: gamesLoading } = useGames(teamId);
const { data: epochData, loading: epochLoading } = useEpochInfoQuery();
const { data: transfersData, loading: transfersLoading } = useGameCards({
currentEpoch: Number(epochData?.epoch.id),
const { data: transfersData, loading: transfersLoading } = useRewards({
onlyActive: false,
scopeToTeams: true,
});
const { data: markets } = useMarketsMapProvider();
@ -112,7 +107,7 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
games={areTeamGames(games) ? games : undefined}
gamesLoading={gamesLoading}
transfers={transfersData}
transfersLoading={epochLoading || transfersLoading}
transfersLoading={transfersLoading}
allMarkets={markets || undefined}
refetch={refetch}
/>
@ -137,7 +132,7 @@ const TeamPage = ({
members?: Member[];
games?: TeamGame[];
gamesLoading?: boolean;
transfers?: EnrichedTransfer[];
transfers?: EnrichedRewardTransfer[];
transfersLoading?: boolean;
allMarkets?: MarketMap;
refetch: () => void;
@ -211,7 +206,7 @@ const Games = ({
}: {
games?: TeamGame[];
gamesLoading?: boolean;
transfers?: EnrichedTransfer[];
transfers?: EnrichedRewardTransfer[];
transfersLoading?: boolean;
allMarkets?: MarketMap;
}) => {
@ -451,7 +446,7 @@ const GameTypeCell = ({
transfer,
allMarkets,
}: {
transfer?: EnrichedTransfer;
transfer?: EnrichedRewardTransfer;
allMarkets?: MarketMap;
}) => {
const [open, setOpen] = useState(false);
@ -474,7 +469,7 @@ const GameTypeCell = ({
ref={ref}
className="border-b border-dashed border-vega-clight-200 dark:border-vega-cdark-200 text-left md:truncate md:max-w-[25vw]"
>
<DispatchMetricInfo transferNode={transfer} allMarkets={allMarkets} />
<DispatchMetricInfo reward={transfer} />
</button>
</>
);
@ -490,7 +485,7 @@ const ActiveRewardCardDialog = ({
open: boolean;
onChange: (isOpen: boolean) => void;
trigger?: HTMLElement | null;
transfer: EnrichedTransfer;
transfer: EnrichedRewardTransfer;
allMarkets?: MarketMap;
}) => {
const t = useT();
@ -516,8 +511,6 @@ const ActiveRewardCardDialog = ({
<ActiveRewardCard
transferNode={transfer}
currentEpoch={Number(data?.epoch.id)}
kind={transfer.transfer.kind as RecurringTransfer}
allMarkets={allMarkets}
/>
</div>
<div className="w-1/4">

View File

@ -1,17 +1,15 @@
import { ActiveRewardCard } from '../rewards-container/active-rewards';
import { useT } from '../../lib/use-t';
import { type EnrichedTransfer } from '../../lib/hooks/use-game-cards';
import { useMarketsMapProvider } from '@vegaprotocol/markets';
import { type EnrichedRewardTransfer } from '../../lib/hooks/use-rewards';
export const GamesContainer = ({
data,
currentEpoch,
}: {
data: EnrichedTransfer[];
data: EnrichedRewardTransfer[];
currentEpoch: number;
}) => {
const t = useT();
const { data: markets } = useMarketsMapProvider();
if (!data || data.length === 0) {
return (
@ -37,8 +35,6 @@ export const GamesContainer = ({
key={i}
transferNode={game}
currentEpoch={currentEpoch}
kind={transfer.kind}
allMarkets={markets || undefined}
/>
);
})}

View File

@ -1,9 +1,5 @@
import { render, screen } from '@testing-library/react';
import {
ActiveRewardCard,
applyFilter,
isActiveReward,
} from './active-rewards';
import { ActiveRewardCard, applyFilter } from './active-rewards';
import {
AccountType,
AssetStatus,
@ -11,54 +7,13 @@ import {
DistributionStrategy,
EntityScope,
IndividualScope,
type RecurringTransfer,
type TransferNode,
TransferStatus,
type Transfer,
} from '@vegaprotocol/types';
jest.mock('./__generated__/Rewards', () => ({
useMarketForRewardsQuery: () => ({
data: undefined,
}),
}));
jest.mock('@vegaprotocol/assets', () => ({
useAssetDataProvider: () => {
return {
data: {
assetId: 'asset-1',
},
};
},
}));
import { type EnrichedRewardTransfer } from '../../lib/hooks/use-rewards';
describe('ActiveRewards', () => {
const mockRecurringTransfer: RecurringTransfer = {
__typename: 'RecurringTransfer',
startEpoch: 115332,
endEpoch: 115432,
factor: '1',
dispatchStrategy: {
__typename: 'DispatchStrategy',
dispatchMetric: DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED,
dispatchMetricAssetId:
'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
marketIdsInScope: null,
entityScope: EntityScope.ENTITY_SCOPE_INDIVIDUALS,
individualScope: IndividualScope.INDIVIDUAL_SCOPE_ALL,
teamScope: null,
nTopPerformers: '',
stakingRequirement: '',
notionalTimeWeightedAveragePositionRequirement: '',
windowLength: 1,
lockPeriod: 0,
distributionStrategy: DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
rankTable: null,
},
};
const mockTransferNode: TransferNode = {
const reward: EnrichedRewardTransfer = {
__typename: 'TransferNode',
transfer: {
__typename: 'Transfer',
@ -86,21 +41,37 @@ describe('ActiveRewards', () => {
reference: 'reward',
status: TransferStatus.STATUS_PENDING,
timestamp: '2023-12-18T13:05:35.948706Z',
kind: mockRecurringTransfer,
kind: {
__typename: 'RecurringTransfer',
startEpoch: 115332,
endEpoch: 115432,
factor: '1',
dispatchStrategy: {
__typename: 'DispatchStrategy',
dispatchMetric: DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED,
dispatchMetricAssetId:
'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
marketIdsInScope: null,
entityScope: EntityScope.ENTITY_SCOPE_INDIVIDUALS,
individualScope: IndividualScope.INDIVIDUAL_SCOPE_ALL,
teamScope: null,
nTopPerformers: '',
stakingRequirement: '',
notionalTimeWeightedAveragePositionRequirement: '',
windowLength: 1,
lockPeriod: 0,
distributionStrategy:
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
rankTable: null,
},
},
reason: null,
},
fees: [],
};
it('renders with valid props', () => {
render(
<ActiveRewardCard
transferNode={mockTransferNode}
currentEpoch={1}
kind={mockRecurringTransfer}
allMarkets={{}}
/>
);
render(<ActiveRewardCard transferNode={reward} currentEpoch={115432} />);
expect(
screen.getByText(/Liquidity provision fees received/i)
@ -108,41 +79,11 @@ describe('ActiveRewards', () => {
expect(screen.getByText('Individual scope')).toBeInTheDocument();
expect(screen.getByText('Average position')).toBeInTheDocument();
expect(screen.getByText('Ends in')).toBeInTheDocument();
expect(screen.getByText('115431 epochs')).toBeInTheDocument();
expect(screen.getByText('1 epoch')).toBeInTheDocument();
expect(screen.getByText('Assessed over')).toBeInTheDocument();
expect(screen.getByText('1 epoch')).toBeInTheDocument();
});
describe('isActiveReward', () => {
it('returns true for valid active reward', () => {
const node = {
transfer: {
kind: {
__typename: 'RecurringTransfer',
dispatchStrategy: {},
endEpoch: 10,
},
status: TransferStatus.STATUS_PENDING,
},
} as TransferNode;
expect(isActiveReward(node, 5)).toBeTruthy();
});
it('returns false for invalid active reward', () => {
const node = {
transfer: {
kind: {
__typename: 'RecurringTransfer',
dispatchStrategy: {},
endEpoch: 10,
},
status: TransferStatus.STATUS_PENDING,
},
} as TransferNode;
expect(isActiveReward(node, 15)).toBeFalsy();
});
});
describe('applyFilter', () => {
it('returns true when filter matches dispatch metric label', () => {
const transfer = {

View File

@ -1,12 +1,8 @@
import { useActiveRewardsQuery } from './__generated__/Rewards';
import { useT } from '../../lib/use-t';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import classNames from 'classnames';
import {
type IconName,
type VegaIconSize,
Icon,
Intent,
Tooltip,
VegaIcon,
VegaIconNames,
@ -14,18 +10,12 @@ import {
TinyScroll,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
import {
type Maybe,
type Transfer,
type TransferNode,
type RecurringTransfer,
DistributionStrategyDescriptionMapping,
DistributionStrategyMapping,
EntityScope,
EntityScopeMapping,
TransferStatus,
TransferStatusMapping,
DispatchMetric,
DispatchMetricDescription,
DispatchMetricLabels,
@ -36,43 +26,33 @@ import {
IndividualScopeDescriptionMapping,
} from '@vegaprotocol/types';
import { Card } from '../card/card';
import { useMemo, useState } from 'react';
import { type ReactNode, useState } from 'react';
import {
type AssetFieldsFragment,
useAssetsMapProvider,
type BasicAssetDetails,
} from '@vegaprotocol/assets';
import { type MarketFieldsFragment } from '@vegaprotocol/markets';
import {
type MarketFieldsFragment,
useMarketsMapProvider,
getAsset,
} from '@vegaprotocol/markets';
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 isActiveReward = (node: TransferNode, currentEpoch: number) => {
const { transfer } = node;
if (transfer.kind.__typename !== 'RecurringTransfer') {
return false;
}
const { dispatchStrategy } = transfer.kind;
if (!dispatchStrategy) {
return false;
}
if (transfer.kind.endEpoch && transfer.kind.endEpoch < currentEpoch) {
return false;
}
if (transfer.status !== TransferStatus.STATUS_PENDING) {
return false;
}
return true;
};
export const applyFilter = (
node: TransferNode & {
asset?: AssetFieldsFragment | null;
@ -95,7 +75,10 @@ export const applyFilter = (
transfer.asset?.symbol
.toLowerCase()
.includes(filter.searchTerm.toLowerCase()) ||
EntityScopeLabelMapping[transfer.kind.dispatchStrategy.entityScope]
(
EntityScopeLabelMapping[transfer.kind.dispatchStrategy.entityScope] ||
'Unspecified'
)
.toLowerCase()
.includes(filter.searchTerm.toLowerCase()) ||
node.asset?.name
@ -114,42 +97,15 @@ export const applyFilter = (
export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
const t = useT();
const { data: activeRewardsData } = useActiveRewardsQuery({
variables: {
isReward: true,
},
const { data } = useRewards({
onlyActive: true,
});
const [filter, setFilter] = useState<Filter>({
searchTerm: '',
});
const { data: assets } = useAssetsMapProvider();
const { data: markets } = useMarketsMapProvider();
const enrichedTransfers = activeRewardsData?.transfersConnection?.edges
?.map((e) => e?.node as TransferNode)
.filter((node) => isActiveReward(node, currentEpoch))
.map((node) => {
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
return node;
}
const asset =
assets &&
assets[
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
];
const marketsInScope =
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
(id) => markets && markets[id]
);
return { ...node, asset, markets: marketsInScope };
});
if (!enrichedTransfers || !enrichedTransfers.length) return null;
if (!data || !data.length) return null;
return (
<Card
@ -157,7 +113,8 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
className="lg:col-span-full"
data-testid="active-rewards-card"
>
{enrichedTransfers.length > 1 && (
{/** CARDS FILTER */}
{data.length > 1 && (
<TradingInput
onChange={(e) =>
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
@ -172,142 +129,32 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
prependElement={<VegaIcon name={VegaIconNames.SEARCH} />}
/>
)}
{/** CARDS */}
<TinyScroll className="grid gap-x-8 gap-y-10 h-fit grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] md:grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] lg:grid-cols-[repeat(auto-fill,_minmax(320px,_1fr))] xl:grid-cols-[repeat(auto-fill,_minmax(335px,_1fr))] max-h-[40rem] overflow-auto pr-2">
{enrichedTransfers
{data
.filter((n) => applyFilter(n, filter))
.map((node, i) => {
const { transfer } = node;
if (
transfer.kind.__typename !== 'RecurringTransfer' ||
!transfer.kind.dispatchStrategy?.dispatchMetric
) {
return null;
}
return (
node && (
<ActiveRewardCard
key={i}
transferNode={node}
kind={transfer.kind}
currentEpoch={currentEpoch}
allMarkets={markets || {}}
/>
)
);
})}
.map((node, i) => (
<ActiveRewardCard
key={i}
transferNode={node}
currentEpoch={currentEpoch}
/>
))}
</TinyScroll>
</Card>
);
};
// This was built to be a status indicator for the rewards based on the transfer status
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const StatusIndicator = ({
status,
reason,
}: {
status: TransferStatus;
reason?: Maybe<string> | undefined;
}) => {
const t = useT();
const getIconIntent = (status: string) => {
switch (status) {
case TransferStatus.STATUS_DONE:
return { icon: IconNames.TICK_CIRCLE, intent: Intent.Success };
case TransferStatus.STATUS_REJECTED:
return { icon: IconNames.ERROR, intent: Intent.Danger };
default:
return { icon: IconNames.HELP, intent: Intent.Primary };
}
};
const { icon, intent } = getIconIntent(status);
return (
<Tooltip
description={
<span>
{t('Transfer status: {{status}} {{reason}}', {
status: TransferStatusMapping[status],
reason: reason ? `(${reason})` : '',
})}
</span>
}
>
<span
className={classNames(
{
'text-gray-700 dark:text-gray-300': intent === Intent.None,
'text-vega-blue': intent === Intent.Primary,
'text-vega-green dark:text-vega-green': intent === Intent.Success,
'dark:text-yellow text-yellow-600': intent === Intent.Warning,
'text-vega-red': intent === Intent.Danger,
},
'flex items-start p-1 align-text-bottom'
)}
>
<Icon size={3} name={icon as IconName} />
</span>
</Tooltip>
);
};
type ActiveRewardCardProps = {
transferNode: TransferNode & {
asset?: AssetFieldsFragment | null;
markets?: (MarketFieldsFragment | null)[];
};
transferNode: EnrichedRewardTransfer;
currentEpoch: number;
kind: RecurringTransfer;
allMarkets?: Record<string, MarketFieldsFragment | null>;
};
export const ActiveRewardCard = ({
transferNode,
currentEpoch,
kind,
allMarkets,
}: ActiveRewardCardProps) => {
const t = useT();
const { transfer } = transferNode;
const { dispatchStrategy } = kind;
const marketIdsInScope = dispatchStrategy?.marketIdsInScope;
const firstMarketData = transferNode.markets?.[0];
const specificMarkets = useMemo(() => {
if (
!firstMarketData ||
!marketIdsInScope ||
marketIdsInScope.length === 0
) {
return null;
}
if (marketIdsInScope.length > 1) {
const marketNames =
allMarkets &&
marketIdsInScope
.map((id) => allMarkets[id]?.tradableInstrument?.instrument?.name)
.join(', ');
return (
<Tooltip description={marketNames}>
<span>Specific markets</span>
</Tooltip>
);
}
return (
<span>{firstMarketData?.tradableInstrument?.instrument?.name || ''}</span>
);
}, [firstMarketData, marketIdsInScope, allMarkets]);
const dispatchAsset = transferNode.asset;
if (!dispatchStrategy) {
return null;
}
// Gray out/hide the cards that are related to not trading markets
const marketSettled = transferNode.markets?.some(
// don't display the cards that are scoped to not trading markets
const marketSettled = transferNode.markets?.filter(
(m) =>
m?.state &&
[
@ -318,97 +165,133 @@ export const ActiveRewardCard = ({
].includes(m.state)
);
if (marketSettled) {
// 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;
}
const assetInActiveMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return m?.state && MarketState.STATE_ACTIVE === m.state;
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 (
<RewardCard
colour={colour}
rewardAmount={addDecimalsFormatNumber(
transferNode.transfer.amount,
transferNode.transfer.asset?.decimals || 0,
6
)}
rewardAsset={transferNode.asset}
endsIn={
transferNode.transfer.kind.endEpoch != null
? transferNode.transfer.kind.endEpoch - currentEpoch
: undefined
}
return false;
});
const marketSuspended = transferNode.markets?.some(
(m) =>
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
dispatchStrategy={transferNode.transfer.kind.dispatchStrategy}
dispatchMetricInfo={<DispatchMetricInfo reward={transferNode} />}
/>
);
};
// Gray out the cards that are related to suspended markets
// Or settlement assets in markets that are not active and eligible for rewards
const { gradientClassName, mainClassName } =
marketSuspended || !assetInActiveMarket
? {
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
}
: getGradientClasses(dispatchStrategy.dispatchMetric);
const entityScope = dispatchStrategy.entityScope;
const RewardCard = ({
colour,
rewardAmount,
rewardAsset,
dispatchStrategy,
endsIn,
dispatchMetricInfo,
}: {
colour: CardColour;
rewardAmount: string;
/** The asset linked to the dispatch strategy via `dispatchMetricAssetId` property. */
rewardAsset?: BasicAssetDetails;
/** The transfer's dispatch strategy. */
dispatchStrategy: DispatchStrategy;
/** The number of epochs until the transfer stops. */
endsIn?: number;
/** The VEGA asset details, required to format the min staking amount. */
vegaAsset?: BasicAssetDetails;
dispatchMetricInfo?: ReactNode;
}) => {
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',
gradientClassName
CardColourStyles[colour].gradientClassName
)}
data-testid="active-rewards-card"
>
<div
className={classNames(
mainClassName,
'bg-gradient-to-b bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4 flex flex-col gap-4'
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 transfer={transfer} />
{entityScope && (
<EntityIcon entityScope={dispatchStrategy.entityScope} />
{dispatchStrategy.entityScope && (
<span className="text-muted text-xs" data-testid="entity-scope">
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
{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">
{addDecimalsFormatNumber(
transferNode.transfer.amount,
transferNode.transfer.asset?.decimals || 0,
6
)}
{rewardAmount}
</span>
<span className="font-alpha">
{transferNode.transfer.asset?.symbol}
</span>
<span className="font-alpha">{rewardAsset?.symbol || ''}</span>
</h3>
{
<Tooltip
description={t(
DistributionStrategyDescriptionMapping[
{/** DISTRIBUTION STRATEGY */}
<Tooltip
description={t(
DistributionStrategyDescriptionMapping[
dispatchStrategy.distributionStrategy
]
)}
underline={true}
>
<span className="text-xs" data-testid="distribution-strategy">
{
DistributionStrategyMapping[
dispatchStrategy.distributionStrategy
]
)}
underline={true}
>
<span className="text-xs" data-testid="distribution-strategy">
{
DistributionStrategyMapping[
dispatchStrategy.distributionStrategy
]
}
</span>
</Tooltip>
}
}
</span>
</Tooltip>
</div>
{/** DISTRIBUTION DELAY */}
<div className="flex flex-col gap-2 items-center text-center">
<CardIcon
iconName={VegaIconNames.LOCK}
@ -421,63 +304,59 @@ export const ActiveRewardCard = ({
data-testid="locked-for"
>
{t('numberEpochs', '{{count}} epochs', {
count: kind.dispatchStrategy?.lockPeriod,
count: dispatchStrategy.lockPeriod,
})}
</span>
</div>
</div>
<span className="border-[0.5px] border-gray-700" />
<span data-testid="dispatch-metric-info">
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
<Tooltip
underline={marketSuspended}
description={
(marketSuspended || !assetInActiveMarket) &&
(specificMarkets
? t('Eligible market(s) currently suspended')
: !assetInActiveMarket
? t('Currently no markets eligible for reward')
: '')
}
>
<span>{specificMarkets || dispatchAsset?.name}</span>
</Tooltip>
</span>
{/** DISPATCH METRIC */}
{dispatchMetricInfo ? (
dispatchMetricInfo
) : (
<span data-testid="dispatch-metric-info">
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]}
</span>
)}
<div className="flex items-center gap-8 flex-wrap">
{kind.endEpoch && (
{/** ENDS IN */}
{endsIn != null && (
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Ends in')} </span>
<span data-testid="ends-in">
{t('numberEpochs', '{{count}} epochs', {
count: kind.endEpoch - currentEpoch,
})}
<span data-testid="ends-in" data-endsin={endsIn}>
{endsIn >= 0
? t('numberEpochs', '{{count}} epochs', {
count: endsIn,
})
: t('Ended')}
</span>
</span>
)}
{
<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>
{/** 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>
</div>
{/** DISPATCH METRIC DESCRIPTION */}
{dispatchStrategy?.dispatchMetric && (
<span className="text-muted text-sm h-[3rem]">
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
</span>
)}
<span className="border-[0.5px] border-gray-700" />
{kind.dispatchStrategy && (
{/** REQUIREMENTS */}
{dispatchStrategy && (
<RewardRequirements
dispatchStrategy={kind.dispatchStrategy}
assetDecimalPlaces={transfer.asset?.decimals}
dispatchStrategy={dispatchStrategy}
rewardAsset={rewardAsset}
/>
)}
</div>
@ -487,77 +366,67 @@ export const ActiveRewardCard = ({
};
export const DispatchMetricInfo = ({
transferNode,
allMarkets,
reward,
}: {
transferNode: ActiveRewardCardProps['transferNode'];
allMarkets?: ActiveRewardCardProps['allMarkets'];
reward: EnrichedRewardTransfer;
}) => {
const dispatchStrategy =
transferNode.transfer.kind.__typename === 'RecurringTransfer'
? transferNode.transfer.kind.dispatchStrategy
: null;
const t = useT();
const dispatchStrategy = reward.transfer.kind.dispatchStrategy;
const marketNames = compact(
reward.markets?.map((m) => m.tradableInstrument.instrument.name)
);
const dispatchAsset = transferNode.transfer.asset;
let additionalDispatchMetricInfo = null;
const marketIdsInScope = dispatchStrategy?.marketIdsInScope;
const firstMarketData = transferNode.markets?.[0];
const specificMarkets = useMemo(() => {
if (
!firstMarketData ||
!marketIdsInScope ||
marketIdsInScope.length === 0
) {
return null;
}
if (marketIdsInScope.length > 1) {
const marketNames =
allMarkets &&
marketIdsInScope
.map((id) => allMarkets[id]?.tradableInstrument?.instrument?.name)
.join(', ');
return (
<Tooltip description={marketNames}>
<span>Specific markets</span>
</Tooltip>
);
}
const name = firstMarketData?.tradableInstrument?.instrument?.name;
if (name) {
return <span>{name}</span>;
}
return null;
}, [firstMarketData, marketIdsInScope, allMarkets]);
if (!dispatchStrategy) return null;
// if asset found then display asset symbol
if (reward.asset) {
additionalDispatchMetricInfo = <span>{reward.asset.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}>
<span>{t('Specific markets')}</span>
</Tooltip>
);
}
return (
<span data-testid="dispatch-metric-info">
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
<span>{specificMarkets || dispatchAsset?.name}</span>
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]}
{additionalDispatchMetricInfo != null && (
<> {additionalDispatchMetricInfo}</>
)}
</span>
);
};
const RewardRequirements = ({
dispatchStrategy,
assetDecimalPlaces = 0,
rewardAsset,
vegaAsset,
}: {
dispatchStrategy: DispatchStrategy;
assetDecimalPlaces: number | undefined;
rewardAsset?: BasicAssetDetails;
vegaAsset?: BasicAssetDetails;
}) => {
const t = useT();
const entityLabel = EntityScopeLabelMapping[dispatchStrategy.entityScope];
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">
{t('{{entity}} scope', {
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
})}
{entityLabel
? t('{{entity}} scope', {
entity: entityLabel,
})
: t('Scope')}
</dt>
<dd className="flex items-center gap-1" data-testid="scope">
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
@ -574,7 +443,7 @@ const RewardRequirements = ({
>
{addDecimalsFormatNumber(
dispatchStrategy?.stakingRequirement || 0,
assetDecimalPlaces
vegaAsset?.decimals || 18
)}
</dd>
</div>
@ -587,7 +456,7 @@ const RewardRequirements = ({
{addDecimalsFormatNumber(
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
0,
assetDecimalPlaces
rewardAsset?.decimals || 0
)}
</dd>
</div>
@ -645,44 +514,65 @@ const RewardEntityScope = ({
);
}
return null;
return t('Unspecified');
};
const getGradientClasses = (d: DispatchMetric | undefined) => {
switch (d) {
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
return {
gradientClassName: 'from-vega-pink-500 to-vega-purple-400',
mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%',
};
case DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED:
return {
gradientClassName: 'from-vega-green-500 to-vega-yellow-500',
mainClassName: 'from-vega-green-400 dark:from-vega-green-600 to-20%',
};
case DispatchMetric.DISPATCH_METRIC_MAKER_FEES_PAID:
return {
gradientClassName: 'from-vega-orange-500 to-vega-pink-400',
mainClassName: 'from-vega-orange-400 dark:from-vega-orange-600 to-20%',
};
case DispatchMetric.DISPATCH_METRIC_MARKET_VALUE:
case DispatchMetric.DISPATCH_METRIC_RELATIVE_RETURN:
return {
gradientClassName: 'from-vega-purple-500 to-vega-blue-400',
mainClassName: 'from-vega-purple-400 dark:from-vega-purple-600 to-20%',
};
case DispatchMetric.DISPATCH_METRIC_RETURN_VOLATILITY:
return {
gradientClassName: 'from-vega-blue-500 to-vega-green-400',
mainClassName: 'from-vega-blue-400 dark:from-vega-blue-600 to-20%',
};
case DispatchMetric.DISPATCH_METRIC_VALIDATOR_RANKING:
default:
return {
gradientClassName: 'from-vega-pink-500 to-vega-purple-400',
mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%',
};
}
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 = ({
@ -703,36 +593,29 @@ const CardIcon = ({
);
};
const EntityScopeIconMap: Record<EntityScope, VegaIconNames> = {
[EntityScope.ENTITY_SCOPE_TEAMS]: VegaIconNames.TEAM,
[EntityScope.ENTITY_SCOPE_INDIVIDUALS]: VegaIconNames.MAN,
};
const EntityIcon = ({
transfer,
entityScope,
size = 18,
}: {
transfer: Transfer;
entityScope: EntityScope;
size?: VegaIconSize;
}) => {
if (transfer.kind.__typename !== 'RecurringTransfer') {
return null;
}
const entityScope = transfer.kind.dispatchStrategy?.entityScope;
const getIconName = () => {
switch (entityScope) {
case EntityScope.ENTITY_SCOPE_TEAMS:
return VegaIconNames.TEAM;
case EntityScope.ENTITY_SCOPE_INDIVIDUALS:
return VegaIconNames.MAN;
default:
return VegaIconNames.QUESTION_MARK;
}
};
const iconName = getIconName();
return (
<Tooltip
description={
<span>{entityScope ? EntityScopeMapping[entityScope] : ''}</span>
entityScope ? <span>{EntityScopeMapping[entityScope]}</span> : undefined
}
>
<span className="flex items-center p-2 rounded-full border border-gray-600">
{iconName && <VegaIcon name={iconName} size={size} />}
<VegaIcon
name={EntityScopeIconMap[entityScope] || VegaIconNames.QUESTION_MARK}
size={size}
/>
</span>
</Tooltip>
);

View File

@ -20,7 +20,7 @@ import {
type RewardsPageQuery,
useRewardsPageQuery,
useRewardsEpochQuery,
} from './__generated__/Rewards';
} from '../../lib/hooks/__generated__/Rewards';
import {
TradingButton,
VegaIcon,

View File

@ -16,7 +16,7 @@ import {
import {
useRewardsHistoryQuery,
type RewardsHistoryQuery,
} from './__generated__/Rewards';
} from '../../lib/hooks/__generated__/Rewards';
import { useRewardsRowData } from './use-reward-row-data';
import { useT } from '../../lib/use-t';

View File

@ -4,7 +4,7 @@ import BigNumber from 'bignumber.js';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { type Asset } from '@vegaprotocol/assets';
import { type PartyRewardsConnection } from './rewards-history';
import { type RewardsHistoryQuery } from './__generated__/Rewards';
import { type RewardsHistoryQuery } from '../../lib/hooks/__generated__/Rewards';
const REWARD_ACCOUNT_TYPES = [
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,

View File

@ -1,92 +0,0 @@
import compact from 'lodash/compact';
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
import { isActiveReward } from '../../components/rewards-container/active-rewards';
import {
type RecurringTransfer,
type TransferNode,
EntityScope,
IndividualScope,
} from '@vegaprotocol/types';
import {
type AssetFieldsFragment,
useAssetsMapProvider,
} from '@vegaprotocol/assets';
import {
type MarketFieldsFragment,
useMarketsMapProvider,
} from '@vegaprotocol/markets';
import { type ApolloError } from '@apollo/client';
export type EnrichedTransfer = TransferNode & {
asset?: AssetFieldsFragment | null;
markets?: (MarketFieldsFragment | null)[];
};
type RecurringTransferKind = EnrichedTransfer & {
transfer: {
kind: RecurringTransfer;
};
};
export const isScopedToTeams = (
node: TransferNode
): node is RecurringTransferKind =>
node.transfer.kind.__typename === 'RecurringTransfer' &&
// scoped to teams
(node.transfer.kind.dispatchStrategy?.entityScope ===
EntityScope.ENTITY_SCOPE_TEAMS ||
// or to individuals
(node.transfer.kind.dispatchStrategy?.entityScope ===
EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
// but they have to be in a team
node.transfer.kind.dispatchStrategy.individualScope ===
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM));
export const useGameCards = ({
currentEpoch,
onlyActive,
}: {
currentEpoch: number;
onlyActive: boolean;
}): { data: EnrichedTransfer[]; loading: boolean; error?: ApolloError } => {
const { data, loading, error } = useActiveRewardsQuery({
variables: {
isReward: true,
},
fetchPolicy: 'cache-and-network',
});
const { data: assets, loading: assetsLoading } = useAssetsMapProvider();
const { data: markets, loading: marketsLoading } = useMarketsMapProvider();
const games = compact(data?.transfersConnection?.edges?.map((n) => n?.node))
.map((n) => n as TransferNode)
.filter((node) => {
const active = onlyActive ? isActiveReward(node, currentEpoch) : true;
return active && isScopedToTeams(node);
})
.map((node) => {
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
return node;
}
const asset =
assets &&
assets[
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
];
const marketsInScope =
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
(id) => markets && markets[id]
);
return { ...node, asset, markets: marketsInScope };
});
return {
data: games,
loading: loading || assetsLoading || marketsLoading,
error,
};
};

View File

@ -0,0 +1,242 @@
import {
type DispatchStrategy,
type TransferNode,
EntityScope,
type TransferKind,
TransferStatus,
IndividualScope,
} from '@vegaprotocol/types';
import {
type RewardTransfer,
isActiveReward,
isReward,
isScopedToTeams,
} from './use-rewards';
const makeDispatchStrategy = (
entityScope: EntityScope,
individualScope?: IndividualScope
): DispatchStrategy =>
({
entityScope,
individualScope,
} as DispatchStrategy);
const makeReward = (
status: TransferStatus,
startEpoch: number,
endEpoch?: number,
dispatchStrategy?: DispatchStrategy,
kind: TransferKind['__typename'] = 'OneOffTransfer'
): RewardTransfer =>
({
transfer: {
status,
kind: {
__typename: kind,
dispatchStrategy,
startEpoch,
endEpoch,
},
},
} as RewardTransfer);
describe('isReward', () => {
it.each([
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
'RecurringTransfer'
),
true,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_INDIVIDUALS),
'RecurringTransfer'
),
true,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
undefined,
'RecurringTransfer'
),
false,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
'OneOffTransfer'
),
false,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_INDIVIDUALS),
'OneOffTransfer'
),
false,
],
])('checks if given transfer is a reward or not', (input, output) => {
expect(isReward(input as TransferNode)).toEqual(output);
});
});
describe('isActiveReward', () => {
it.each([
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
'RecurringTransfer'
),
true,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
2,
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
'RecurringTransfer'
),
true,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
3,
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
'RecurringTransfer'
),
true,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
4, // start in 1 epoch
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
'RecurringTransfer'
),
false,
],
[
makeReward(
TransferStatus.STATUS_DONE, // done, not active any more
1,
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
'RecurringTransfer'
),
false,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
2, // ended 1 epoch ago
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
'RecurringTransfer'
),
false,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
3, // ends now, but active until end of epoch
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
'RecurringTransfer'
),
true,
],
])('checks if given reward is active or not', (input, output) => {
expect(isActiveReward(input, 3)).toEqual(output);
});
});
describe('isScopedToTeams', () => {
it.each([
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS), // only teams
'RecurringTransfer'
),
true,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
makeDispatchStrategy(
EntityScope.ENTITY_SCOPE_INDIVIDUALS,
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM // individual in teams
),
'RecurringTransfer'
),
true,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_INDIVIDUALS), // not in team
'RecurringTransfer'
),
false,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
makeDispatchStrategy(
EntityScope.ENTITY_SCOPE_INDIVIDUALS,
IndividualScope.INDIVIDUAL_SCOPE_ALL // not only in team
),
'RecurringTransfer'
),
false,
],
[
makeReward(
TransferStatus.STATUS_PENDING,
1,
undefined,
makeDispatchStrategy(
EntityScope.ENTITY_SCOPE_INDIVIDUALS,
IndividualScope.INDIVIDUAL_SCOPE_NOT_IN_TEAM // not in team
),
'RecurringTransfer'
),
false,
],
])('checks if given reward is scoped to teams or not', (input, output) => {
expect(isScopedToTeams(input)).toEqual(output);
});
});

View File

@ -0,0 +1,181 @@
import {
type AssetFieldsFragment,
useAssetsMapProvider,
} from '@vegaprotocol/assets';
import { useActiveRewardsQuery } from './__generated__/Rewards';
import {
type MarketFieldsFragment,
useMarketsMapProvider,
getAsset,
} from '@vegaprotocol/markets';
import {
type RecurringTransfer,
type TransferNode,
TransferStatus,
type DispatchStrategy,
EntityScope,
IndividualScope,
MarketState,
} from '@vegaprotocol/types';
import { type ApolloError } from '@apollo/client';
import compact from 'lodash/compact';
import { useEpochInfoQuery } from './__generated__/Epoch';
export type RewardTransfer = TransferNode & {
transfer: {
kind: RecurringTransfer & {
dispatchStrategy: DispatchStrategy;
};
};
};
export type EnrichedRewardTransfer = RewardTransfer & {
/** Dispatch metric asset (reward asset) */
asset?: AssetFieldsFragment;
/** A flag determining whether a reward asset is being traded on any of the active markets */
isAssetTraded?: boolean;
/** A list of markets in scope */
markets?: MarketFieldsFragment[];
};
/**
* Checks if given transfer is a reward.
*
* A reward has to be a recurring transfer and has to have a
* dispatch strategy.
*/
export const isReward = (node: TransferNode): node is RewardTransfer => {
if (
node.transfer.kind.__typename === 'RecurringTransfer' &&
node.transfer.kind.dispatchStrategy != null
) {
return true;
}
return false;
};
/**
* Checks if given reward (transfer) is active.
*/
export const isActiveReward = (node: RewardTransfer, currentEpoch: number) => {
const { transfer } = node;
const pending = transfer.status === TransferStatus.STATUS_PENDING;
const withinEpochs =
transfer.kind.startEpoch <= currentEpoch &&
(transfer.kind.endEpoch != null
? transfer.kind.endEpoch >= currentEpoch
: true);
if (pending && withinEpochs) return true;
return false;
};
/**
* Checks if given reward (transfer) is scoped to teams.
*
* A reward is scoped to teams if it's entity scope is set to teams or
* if the scope is set to individuals but the individuals are in a team.
*/
export const isScopedToTeams = (node: EnrichedRewardTransfer) =>
// scoped to teams
node.transfer.kind.dispatchStrategy.entityScope ===
EntityScope.ENTITY_SCOPE_TEAMS ||
// or to individuals
(node.transfer.kind.dispatchStrategy.entityScope ===
EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
// but they have to be in a team
node.transfer.kind.dispatchStrategy.individualScope ===
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM);
/** Retrieves rewards (transfers) */
export const useRewards = ({
// get active by default
onlyActive = true,
scopeToTeams = false,
}: {
onlyActive: boolean;
scopeToTeams?: boolean;
}): {
data: EnrichedRewardTransfer[];
loading: boolean;
error?: ApolloError | Error;
} => {
const {
data: epochData,
loading: epochLoading,
error: epochError,
} = useEpochInfoQuery({
fetchPolicy: 'network-only',
});
const currentEpoch = Number(epochData?.epoch.id);
const { data, loading, error } = useActiveRewardsQuery({
variables: {
isReward: true,
},
skip: onlyActive && isNaN(currentEpoch),
fetchPolicy: 'cache-and-network',
});
const {
data: assets,
loading: assetsLoading,
error: assetsError,
} = useAssetsMapProvider();
const {
data: markets,
loading: marketsLoading,
error: marketsError,
} = useMarketsMapProvider();
const enriched = compact(
data?.transfersConnection?.edges?.map((n) => n?.node)
)
.map((n) => n as TransferNode)
// make sure we have only rewards here
.filter(isReward)
// take only active rewards if required, otherwise take all
.filter((node) => (onlyActive ? isActiveReward(node, currentEpoch) : true))
// take only those rewards that are scoped to teams if required, otherwise take all
.filter((node) => (scopeToTeams ? isScopedToTeams(node) : true))
// enrich with dispatch asset and markets in scope details
.map((node) => {
const asset =
assets &&
assets[node.transfer.kind.dispatchStrategy.dispatchMetricAssetId];
const marketsInScope = compact(
node.transfer.kind.dispatchStrategy.marketIdsInScope?.map(
(id) => markets && markets[id]
)
);
const isAssetTraded =
markets &&
Object.values(markets).some((m) => {
try {
const mAsset = getAsset(m);
return (
mAsset.id ===
node.transfer.kind.dispatchStrategy.dispatchMetricAssetId &&
m.state === MarketState.STATE_ACTIVE
);
} catch {
// NOOP
}
return false;
});
return {
...node,
asset: asset ? asset : undefined,
isAssetTraded: isAssetTraded != null ? isAssetTraded : undefined,
markets: marketsInScope.length > 0 ? marketsInScope : undefined,
};
});
return {
data: enriched,
loading: loading || assetsLoading || marketsLoading || epochLoading,
error: error || assetsError || marketsError || epochError,
};
};

View File

@ -93,16 +93,23 @@ export const useEnabledAssets = () => {
/** Wrapped ETH symbol */
const WETH = 'WETH';
type WETHDetails = Pick<AssetFieldsFragment, 'symbol' | 'decimals' | 'quantum'>;
/** VEGA */
const VEGA = 'VEGA';
export type BasicAssetDetails = Pick<
AssetFieldsFragment,
'symbol' | 'decimals' | 'quantum'
>;
/**
* Tries to find WETH asset configuration on Vega in order to provide its
* details, otherwise it returns hardcoded values.
*/
export const useWETH = (): WETHDetails => {
export const useWETH = (): BasicAssetDetails => {
const { data } = useAssetsDataProvider();
if (data) {
const weth = data.find((a) => a.symbol.toUpperCase() === WETH);
if (weth) return weth;
const details = data.find((a) => a.symbol.toUpperCase() === WETH);
if (details) return details;
}
return {
@ -111,3 +118,17 @@ export const useWETH = (): WETHDetails => {
quantum: '500000000000000', // 1 WETH ~= 2000 qUSD
};
};
export const useVEGA = (): BasicAssetDetails => {
const { data } = useAssetsDataProvider();
if (data) {
const details = data.find((a) => a.symbol.toUpperCase() === VEGA);
if (details) return details;
}
return {
symbol: VEGA,
decimals: 18,
quantum: '1000000000000000000', // 1 VEGA ~= 1 qUSD
};
};