From be6f395ce413ef40a806b8bf28cf1f54a4f7dd38 Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 20 Feb 2024 13:20:35 +0100 Subject: [PATCH] feat(trading): colourful rewards (#5812) --- .../competitions/competitions-home.tsx | 7 +- .../competitions/competitions-team.tsx | 35 +- .../competitions/games-container.tsx | 8 +- .../rewards-container/active-rewards.spec.tsx | 117 +--- .../rewards-container/active-rewards.tsx | 657 +++++++----------- .../rewards-container/rewards-container.tsx | 2 +- .../rewards-container/rewards-history.tsx | 2 +- .../rewards-container/use-reward-row-data.ts | 2 +- .../hooks}/Rewards.graphql | 0 .../hooks}/__generated__/Rewards.ts | 0 apps/trading/lib/hooks/use-game-cards.ts | 92 --- apps/trading/lib/hooks/use-rewards.spec.ts | 242 +++++++ apps/trading/lib/hooks/use-rewards.ts | 181 +++++ libs/assets/src/lib/assets-data-provider.ts | 29 +- 14 files changed, 769 insertions(+), 605 deletions(-) rename apps/trading/{components/rewards-container => lib/hooks}/Rewards.graphql (100%) rename apps/trading/{components/rewards-container => lib/hooks}/__generated__/Rewards.ts (100%) delete mode 100644 apps/trading/lib/hooks/use-game-cards.ts create mode 100644 apps/trading/lib/hooks/use-rewards.spec.ts create mode 100644 apps/trading/lib/hooks/use-rewards.ts diff --git a/apps/trading/client-pages/competitions/competitions-home.tsx b/apps/trading/client-pages/competitions/competitions-home.tsx index 034beb8aa..79b1fa6dd 100644 --- a/apps/trading/client-pages/competitions/competitions-home.tsx +++ b/apps/trading/client-pages/competitions/competitions-home.tsx @@ -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(); diff --git a/apps/trading/client-pages/competitions/competitions-team.tsx b/apps/trading/client-pages/competitions/competitions-team.tsx index 88f6e68e5..c5330a724 100644 --- a/apps/trading/client-pages/competitions/competitions-team.tsx +++ b/apps/trading/client-pages/competitions/competitions-team.tsx @@ -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]" > - + ); @@ -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 = ({
diff --git a/apps/trading/components/competitions/games-container.tsx b/apps/trading/components/competitions/games-container.tsx index 0e79ceab5..b3c1ecafb 100644 --- a/apps/trading/components/competitions/games-container.tsx +++ b/apps/trading/components/competitions/games-container.tsx @@ -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} /> ); })} diff --git a/apps/trading/components/rewards-container/active-rewards.spec.tsx b/apps/trading/components/rewards-container/active-rewards.spec.tsx index 23aa595dd..0cbcec3de 100644 --- a/apps/trading/components/rewards-container/active-rewards.spec.tsx +++ b/apps/trading/components/rewards-container/active-rewards.spec.tsx @@ -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( - - ); + render(); 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 = { diff --git a/apps/trading/components/rewards-container/active-rewards.tsx b/apps/trading/components/rewards-container/active-rewards.tsx index 9b8bca17c..96145f8bb 100644 --- a/apps/trading/components/rewards-container/active-rewards.tsx +++ b/apps/trading/components/rewards-container/active-rewards.tsx @@ -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({ 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 ( { className="lg:col-span-full" data-testid="active-rewards-card" > - {enrichedTransfers.length > 1 && ( + {/** CARDS FILTER */} + {data.length > 1 && ( setFilter((curr) => ({ ...curr, searchTerm: e.target.value })) @@ -172,142 +129,32 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => { prependElement={} /> )} + {/** CARDS */} - {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 && ( - - ) - ); - })} + .map((node, i) => ( + + ))} ); }; -// 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 | 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 ( - - {t('Transfer status: {{status}} {{reason}}', { - status: TransferStatusMapping[status], - reason: reason ? `(${reason})` : '', - })} - - } - > - - - - - ); -}; - type ActiveRewardCardProps = { - transferNode: TransferNode & { - asset?: AssetFieldsFragment | null; - markets?: (MarketFieldsFragment | null)[]; - }; + transferNode: EnrichedRewardTransfer; currentEpoch: number; - kind: RecurringTransfer; - allMarkets?: Record; }; 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 ( - - Specific markets - - ); - } - return ( - {firstMarketData?.tradableInstrument?.instrument?.name || ''} - ); - }, [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 ( + - m?.state === MarketState.STATE_SUSPENDED || - m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE + dispatchStrategy={transferNode.transfer.kind.dispatchStrategy} + dispatchMetricInfo={} + /> ); +}; - // 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 (
+ {/** ENTITY SCOPE */}
- - {entityScope && ( + + {dispatchStrategy.entityScope && ( - {EntityScopeLabelMapping[entityScope] || t('Unspecified')} + {EntityScopeLabelMapping[dispatchStrategy.entityScope] || + t('Unspecified')} )}
+ {/** AMOUNT AND DISTRIBUTION STRATEGY */}
+ {/** AMOUNT */}

- {addDecimalsFormatNumber( - transferNode.transfer.amount, - transferNode.transfer.asset?.decimals || 0, - 6 - )} + {rewardAmount} - - {transferNode.transfer.asset?.symbol} - + {rewardAsset?.symbol || ''}

- { - + + { + DistributionStrategyMapping[ dispatchStrategy.distributionStrategy ] - )} - underline={true} - > - - { - DistributionStrategyMapping[ - dispatchStrategy.distributionStrategy - ] - } - - - } + } + +
+ {/** DISTRIBUTION DELAY */}
{t('numberEpochs', '{{count}} epochs', { - count: kind.dispatchStrategy?.lockPeriod, + count: dispatchStrategy.lockPeriod, })}
- - {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '} - - {specificMarkets || dispatchAsset?.name} - - + {/** DISPATCH METRIC */} + {dispatchMetricInfo ? ( + dispatchMetricInfo + ) : ( + + {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} + + )}
- {kind.endEpoch && ( + {/** ENDS IN */} + {endsIn != null && ( {t('Ends in')} - - {t('numberEpochs', '{{count}} epochs', { - count: kind.endEpoch - currentEpoch, - })} + + {endsIn >= 0 + ? t('numberEpochs', '{{count}} epochs', { + count: endsIn, + }) + : t('Ended')} )} - { - - {t('Assessed over')} - - {t('numberEpochs', '{{count}} epochs', { - count: dispatchStrategy.windowLength, - })} - + {/** WINDOW LENGTH */} + + {t('Assessed over')} + + {t('numberEpochs', '{{count}} epochs', { + count: dispatchStrategy.windowLength, + })} - } +
+ {/** DISPATCH METRIC DESCRIPTION */} {dispatchStrategy?.dispatchMetric && ( {t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])} )} - {kind.dispatchStrategy && ( + {/** REQUIREMENTS */} + {dispatchStrategy && ( )}
@@ -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 ( - - Specific markets - - ); - } - - const name = firstMarketData?.tradableInstrument?.instrument?.name; - if (name) { - return {name}; - } - - return null; - }, [firstMarketData, marketIdsInScope, allMarkets]); - - if (!dispatchStrategy) return null; + // if asset found then display asset symbol + if (reward.asset) { + additionalDispatchMetricInfo = {reward.asset.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]} •{' '} - {specificMarkets || dispatchAsset?.name} + {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} + {additionalDispatchMetricInfo != null && ( + <> • {additionalDispatchMetricInfo} + )} ); }; 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 (
- {t('{{entity}} scope', { - entity: EntityScopeLabelMapping[dispatchStrategy.entityScope], - })} + {entityLabel + ? t('{{entity}} scope', { + entity: entityLabel, + }) + : t('Scope')}
@@ -574,7 +443,7 @@ const RewardRequirements = ({ > {addDecimalsFormatNumber( dispatchStrategy?.stakingRequirement || 0, - assetDecimalPlaces + vegaAsset?.decimals || 18 )}
@@ -587,7 +456,7 @@ const RewardRequirements = ({ {addDecimalsFormatNumber( dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement || 0, - assetDecimalPlaces + rewardAsset?.decimals || 0 )}
@@ -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 = { + // 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.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 ( {entityScope ? EntityScopeMapping[entityScope] : ''} + entityScope ? {EntityScopeMapping[entityScope]} : undefined } > - {iconName && } + ); diff --git a/apps/trading/components/rewards-container/rewards-container.tsx b/apps/trading/components/rewards-container/rewards-container.tsx index 2f5c4afd5..887d360e5 100644 --- a/apps/trading/components/rewards-container/rewards-container.tsx +++ b/apps/trading/components/rewards-container/rewards-container.tsx @@ -20,7 +20,7 @@ import { type RewardsPageQuery, useRewardsPageQuery, useRewardsEpochQuery, -} from './__generated__/Rewards'; +} from '../../lib/hooks/__generated__/Rewards'; import { TradingButton, VegaIcon, diff --git a/apps/trading/components/rewards-container/rewards-history.tsx b/apps/trading/components/rewards-container/rewards-history.tsx index 775472246..f56284a63 100644 --- a/apps/trading/components/rewards-container/rewards-history.tsx +++ b/apps/trading/components/rewards-container/rewards-history.tsx @@ -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'; diff --git a/apps/trading/components/rewards-container/use-reward-row-data.ts b/apps/trading/components/rewards-container/use-reward-row-data.ts index 802d255a8..5aa6bbd58 100644 --- a/apps/trading/components/rewards-container/use-reward-row-data.ts +++ b/apps/trading/components/rewards-container/use-reward-row-data.ts @@ -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, diff --git a/apps/trading/components/rewards-container/Rewards.graphql b/apps/trading/lib/hooks/Rewards.graphql similarity index 100% rename from apps/trading/components/rewards-container/Rewards.graphql rename to apps/trading/lib/hooks/Rewards.graphql diff --git a/apps/trading/components/rewards-container/__generated__/Rewards.ts b/apps/trading/lib/hooks/__generated__/Rewards.ts similarity index 100% rename from apps/trading/components/rewards-container/__generated__/Rewards.ts rename to apps/trading/lib/hooks/__generated__/Rewards.ts diff --git a/apps/trading/lib/hooks/use-game-cards.ts b/apps/trading/lib/hooks/use-game-cards.ts deleted file mode 100644 index 5efeadef8..000000000 --- a/apps/trading/lib/hooks/use-game-cards.ts +++ /dev/null @@ -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, - }; -}; diff --git a/apps/trading/lib/hooks/use-rewards.spec.ts b/apps/trading/lib/hooks/use-rewards.spec.ts new file mode 100644 index 000000000..92ed9febd --- /dev/null +++ b/apps/trading/lib/hooks/use-rewards.spec.ts @@ -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); + }); +}); diff --git a/apps/trading/lib/hooks/use-rewards.ts b/apps/trading/lib/hooks/use-rewards.ts new file mode 100644 index 000000000..06f250e8d --- /dev/null +++ b/apps/trading/lib/hooks/use-rewards.ts @@ -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, + }; +}; diff --git a/libs/assets/src/lib/assets-data-provider.ts b/libs/assets/src/lib/assets-data-provider.ts index 964de03a6..1a8cb5969 100644 --- a/libs/assets/src/lib/assets-data-provider.ts +++ b/libs/assets/src/lib/assets-data-provider.ts @@ -93,16 +93,23 @@ export const useEnabledAssets = () => { /** Wrapped ETH symbol */ const WETH = 'WETH'; -type WETHDetails = Pick; + +/** 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 + }; +};