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
+ };
+};