feat(trading): colourful rewards (#5812)
This commit is contained in:
parent
9a37572f51
commit
be6f395ce4
@ -2,8 +2,6 @@ import { useT } from '../../lib/use-t';
|
|||||||
import { ErrorBoundary } from '@sentry/react';
|
import { ErrorBoundary } from '@sentry/react';
|
||||||
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
|
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
|
||||||
import { Intent, Loader, TradingButton } from '@vegaprotocol/ui-toolkit';
|
import { Intent, Loader, TradingButton } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
import { useGameCards } from '../../lib/hooks/use-game-cards';
|
|
||||||
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { Links } from '../../lib/links';
|
import { Links } from '../../lib/links';
|
||||||
@ -18,6 +16,7 @@ import take from 'lodash/take';
|
|||||||
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
import { TeamCard } from '../../components/competitions/team-card';
|
import { TeamCard } from '../../components/competitions/team-card';
|
||||||
import { useMyTeam } from '../../lib/hooks/use-my-team';
|
import { useMyTeam } from '../../lib/hooks/use-my-team';
|
||||||
|
import { useRewards } from '../../lib/hooks/use-rewards';
|
||||||
|
|
||||||
export const CompetitionsHome = () => {
|
export const CompetitionsHome = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@ -28,9 +27,9 @@ export const CompetitionsHome = () => {
|
|||||||
const { data: epochData } = useEpochInfoQuery();
|
const { data: epochData } = useEpochInfoQuery();
|
||||||
const currentEpoch = Number(epochData?.epoch.id);
|
const currentEpoch = Number(epochData?.epoch.id);
|
||||||
|
|
||||||
const { data: gamesData, loading: gamesLoading } = useGameCards({
|
const { data: gamesData, loading: gamesLoading } = useRewards({
|
||||||
onlyActive: true,
|
onlyActive: true,
|
||||||
currentEpoch,
|
scopeToTeams: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: teamsData, loading: teamsLoading } = useTeams();
|
const { data: teamsData, loading: teamsLoading } = useTeams();
|
||||||
|
@ -10,11 +10,7 @@ import {
|
|||||||
VegaIcon,
|
VegaIcon,
|
||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import {
|
import { TransferStatus, type Asset } from '@vegaprotocol/types';
|
||||||
TransferStatus,
|
|
||||||
type Asset,
|
|
||||||
type RecurringTransfer,
|
|
||||||
} from '@vegaprotocol/types';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { Table } from '../../components/table';
|
import { Table } from '../../components/table';
|
||||||
@ -44,11 +40,6 @@ import {
|
|||||||
areTeamGames,
|
areTeamGames,
|
||||||
} from '../../lib/hooks/use-games';
|
} from '../../lib/hooks/use-games';
|
||||||
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
||||||
import {
|
|
||||||
type EnrichedTransfer,
|
|
||||||
isScopedToTeams,
|
|
||||||
useGameCards,
|
|
||||||
} from '../../lib/hooks/use-game-cards';
|
|
||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||||
import {
|
import {
|
||||||
ActiveRewardCard,
|
ActiveRewardCard,
|
||||||
@ -56,6 +47,11 @@ import {
|
|||||||
} from '../../components/rewards-container/active-rewards';
|
} from '../../components/rewards-container/active-rewards';
|
||||||
import { type MarketMap, useMarketsMapProvider } from '@vegaprotocol/markets';
|
import { type MarketMap, useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
|
import {
|
||||||
|
type EnrichedRewardTransfer,
|
||||||
|
isScopedToTeams,
|
||||||
|
useRewards,
|
||||||
|
} from '../../lib/hooks/use-rewards';
|
||||||
|
|
||||||
export const CompetitionsTeam = () => {
|
export const CompetitionsTeam = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@ -78,10 +74,9 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
|||||||
|
|
||||||
const { data: games, loading: gamesLoading } = useGames(teamId);
|
const { data: games, loading: gamesLoading } = useGames(teamId);
|
||||||
|
|
||||||
const { data: epochData, loading: epochLoading } = useEpochInfoQuery();
|
const { data: transfersData, loading: transfersLoading } = useRewards({
|
||||||
const { data: transfersData, loading: transfersLoading } = useGameCards({
|
|
||||||
currentEpoch: Number(epochData?.epoch.id),
|
|
||||||
onlyActive: false,
|
onlyActive: false,
|
||||||
|
scopeToTeams: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: markets } = useMarketsMapProvider();
|
const { data: markets } = useMarketsMapProvider();
|
||||||
@ -112,7 +107,7 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
|||||||
games={areTeamGames(games) ? games : undefined}
|
games={areTeamGames(games) ? games : undefined}
|
||||||
gamesLoading={gamesLoading}
|
gamesLoading={gamesLoading}
|
||||||
transfers={transfersData}
|
transfers={transfersData}
|
||||||
transfersLoading={epochLoading || transfersLoading}
|
transfersLoading={transfersLoading}
|
||||||
allMarkets={markets || undefined}
|
allMarkets={markets || undefined}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
@ -137,7 +132,7 @@ const TeamPage = ({
|
|||||||
members?: Member[];
|
members?: Member[];
|
||||||
games?: TeamGame[];
|
games?: TeamGame[];
|
||||||
gamesLoading?: boolean;
|
gamesLoading?: boolean;
|
||||||
transfers?: EnrichedTransfer[];
|
transfers?: EnrichedRewardTransfer[];
|
||||||
transfersLoading?: boolean;
|
transfersLoading?: boolean;
|
||||||
allMarkets?: MarketMap;
|
allMarkets?: MarketMap;
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
@ -211,7 +206,7 @@ const Games = ({
|
|||||||
}: {
|
}: {
|
||||||
games?: TeamGame[];
|
games?: TeamGame[];
|
||||||
gamesLoading?: boolean;
|
gamesLoading?: boolean;
|
||||||
transfers?: EnrichedTransfer[];
|
transfers?: EnrichedRewardTransfer[];
|
||||||
transfersLoading?: boolean;
|
transfersLoading?: boolean;
|
||||||
allMarkets?: MarketMap;
|
allMarkets?: MarketMap;
|
||||||
}) => {
|
}) => {
|
||||||
@ -451,7 +446,7 @@ const GameTypeCell = ({
|
|||||||
transfer,
|
transfer,
|
||||||
allMarkets,
|
allMarkets,
|
||||||
}: {
|
}: {
|
||||||
transfer?: EnrichedTransfer;
|
transfer?: EnrichedRewardTransfer;
|
||||||
allMarkets?: MarketMap;
|
allMarkets?: MarketMap;
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -474,7 +469,7 @@ const GameTypeCell = ({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className="border-b border-dashed border-vega-clight-200 dark:border-vega-cdark-200 text-left md:truncate md:max-w-[25vw]"
|
className="border-b border-dashed border-vega-clight-200 dark:border-vega-cdark-200 text-left md:truncate md:max-w-[25vw]"
|
||||||
>
|
>
|
||||||
<DispatchMetricInfo transferNode={transfer} allMarkets={allMarkets} />
|
<DispatchMetricInfo reward={transfer} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -490,7 +485,7 @@ const ActiveRewardCardDialog = ({
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onChange: (isOpen: boolean) => void;
|
onChange: (isOpen: boolean) => void;
|
||||||
trigger?: HTMLElement | null;
|
trigger?: HTMLElement | null;
|
||||||
transfer: EnrichedTransfer;
|
transfer: EnrichedRewardTransfer;
|
||||||
allMarkets?: MarketMap;
|
allMarkets?: MarketMap;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@ -516,8 +511,6 @@ const ActiveRewardCardDialog = ({
|
|||||||
<ActiveRewardCard
|
<ActiveRewardCard
|
||||||
transferNode={transfer}
|
transferNode={transfer}
|
||||||
currentEpoch={Number(data?.epoch.id)}
|
currentEpoch={Number(data?.epoch.id)}
|
||||||
kind={transfer.transfer.kind as RecurringTransfer}
|
|
||||||
allMarkets={allMarkets}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/4">
|
<div className="w-1/4">
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import { ActiveRewardCard } from '../rewards-container/active-rewards';
|
import { ActiveRewardCard } from '../rewards-container/active-rewards';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { type EnrichedTransfer } from '../../lib/hooks/use-game-cards';
|
import { type EnrichedRewardTransfer } from '../../lib/hooks/use-rewards';
|
||||||
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
|
||||||
|
|
||||||
export const GamesContainer = ({
|
export const GamesContainer = ({
|
||||||
data,
|
data,
|
||||||
currentEpoch,
|
currentEpoch,
|
||||||
}: {
|
}: {
|
||||||
data: EnrichedTransfer[];
|
data: EnrichedRewardTransfer[];
|
||||||
currentEpoch: number;
|
currentEpoch: number;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { data: markets } = useMarketsMapProvider();
|
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -37,8 +35,6 @@ export const GamesContainer = ({
|
|||||||
key={i}
|
key={i}
|
||||||
transferNode={game}
|
transferNode={game}
|
||||||
currentEpoch={currentEpoch}
|
currentEpoch={currentEpoch}
|
||||||
kind={transfer.kind}
|
|
||||||
allMarkets={markets || undefined}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import {
|
import { ActiveRewardCard, applyFilter } from './active-rewards';
|
||||||
ActiveRewardCard,
|
|
||||||
applyFilter,
|
|
||||||
isActiveReward,
|
|
||||||
} from './active-rewards';
|
|
||||||
import {
|
import {
|
||||||
AccountType,
|
AccountType,
|
||||||
AssetStatus,
|
AssetStatus,
|
||||||
@ -11,54 +7,13 @@ import {
|
|||||||
DistributionStrategy,
|
DistributionStrategy,
|
||||||
EntityScope,
|
EntityScope,
|
||||||
IndividualScope,
|
IndividualScope,
|
||||||
type RecurringTransfer,
|
|
||||||
type TransferNode,
|
|
||||||
TransferStatus,
|
TransferStatus,
|
||||||
type Transfer,
|
type Transfer,
|
||||||
} from '@vegaprotocol/types';
|
} from '@vegaprotocol/types';
|
||||||
|
import { type EnrichedRewardTransfer } from '../../lib/hooks/use-rewards';
|
||||||
jest.mock('./__generated__/Rewards', () => ({
|
|
||||||
useMarketForRewardsQuery: () => ({
|
|
||||||
data: undefined,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/assets', () => ({
|
|
||||||
useAssetDataProvider: () => {
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
assetId: 'asset-1',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('ActiveRewards', () => {
|
describe('ActiveRewards', () => {
|
||||||
const mockRecurringTransfer: RecurringTransfer = {
|
const reward: EnrichedRewardTransfer = {
|
||||||
__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 = {
|
|
||||||
__typename: 'TransferNode',
|
__typename: 'TransferNode',
|
||||||
transfer: {
|
transfer: {
|
||||||
__typename: 'Transfer',
|
__typename: 'Transfer',
|
||||||
@ -86,21 +41,37 @@ describe('ActiveRewards', () => {
|
|||||||
reference: 'reward',
|
reference: 'reward',
|
||||||
status: TransferStatus.STATUS_PENDING,
|
status: TransferStatus.STATUS_PENDING,
|
||||||
timestamp: '2023-12-18T13:05:35.948706Z',
|
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,
|
reason: null,
|
||||||
},
|
},
|
||||||
fees: [],
|
fees: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders with valid props', () => {
|
it('renders with valid props', () => {
|
||||||
render(
|
render(<ActiveRewardCard transferNode={reward} currentEpoch={115432} />);
|
||||||
<ActiveRewardCard
|
|
||||||
transferNode={mockTransferNode}
|
|
||||||
currentEpoch={1}
|
|
||||||
kind={mockRecurringTransfer}
|
|
||||||
allMarkets={{}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/Liquidity provision fees received/i)
|
screen.getByText(/Liquidity provision fees received/i)
|
||||||
@ -108,41 +79,11 @@ describe('ActiveRewards', () => {
|
|||||||
expect(screen.getByText('Individual scope')).toBeInTheDocument();
|
expect(screen.getByText('Individual scope')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Average position')).toBeInTheDocument();
|
expect(screen.getByText('Average position')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Ends in')).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('Assessed over')).toBeInTheDocument();
|
||||||
expect(screen.getByText('1 epoch')).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', () => {
|
describe('applyFilter', () => {
|
||||||
it('returns true when filter matches dispatch metric label', () => {
|
it('returns true when filter matches dispatch metric label', () => {
|
||||||
const transfer = {
|
const transfer = {
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import { useActiveRewardsQuery } from './__generated__/Rewards';
|
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
type IconName,
|
|
||||||
type VegaIconSize,
|
type VegaIconSize,
|
||||||
Icon,
|
|
||||||
Intent,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
VegaIcon,
|
VegaIcon,
|
||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
@ -14,18 +10,12 @@ import {
|
|||||||
TinyScroll,
|
TinyScroll,
|
||||||
truncateMiddle,
|
truncateMiddle,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
|
||||||
import {
|
import {
|
||||||
type Maybe,
|
|
||||||
type Transfer,
|
|
||||||
type TransferNode,
|
type TransferNode,
|
||||||
type RecurringTransfer,
|
|
||||||
DistributionStrategyDescriptionMapping,
|
DistributionStrategyDescriptionMapping,
|
||||||
DistributionStrategyMapping,
|
DistributionStrategyMapping,
|
||||||
EntityScope,
|
EntityScope,
|
||||||
EntityScopeMapping,
|
EntityScopeMapping,
|
||||||
TransferStatus,
|
|
||||||
TransferStatusMapping,
|
|
||||||
DispatchMetric,
|
DispatchMetric,
|
||||||
DispatchMetricDescription,
|
DispatchMetricDescription,
|
||||||
DispatchMetricLabels,
|
DispatchMetricLabels,
|
||||||
@ -36,43 +26,33 @@ import {
|
|||||||
IndividualScopeDescriptionMapping,
|
IndividualScopeDescriptionMapping,
|
||||||
} from '@vegaprotocol/types';
|
} from '@vegaprotocol/types';
|
||||||
import { Card } from '../card/card';
|
import { Card } from '../card/card';
|
||||||
import { useMemo, useState } from 'react';
|
import { type ReactNode, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
type AssetFieldsFragment,
|
type AssetFieldsFragment,
|
||||||
useAssetsMapProvider,
|
type BasicAssetDetails,
|
||||||
} from '@vegaprotocol/assets';
|
} from '@vegaprotocol/assets';
|
||||||
|
import { type MarketFieldsFragment } from '@vegaprotocol/markets';
|
||||||
import {
|
import {
|
||||||
type MarketFieldsFragment,
|
type EnrichedRewardTransfer,
|
||||||
useMarketsMapProvider,
|
useRewards,
|
||||||
getAsset,
|
} from '../../lib/hooks/use-rewards';
|
||||||
} from '@vegaprotocol/markets';
|
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 = {
|
export type Filter = {
|
||||||
searchTerm: string;
|
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 = (
|
export const applyFilter = (
|
||||||
node: TransferNode & {
|
node: TransferNode & {
|
||||||
asset?: AssetFieldsFragment | null;
|
asset?: AssetFieldsFragment | null;
|
||||||
@ -95,7 +75,10 @@ export const applyFilter = (
|
|||||||
transfer.asset?.symbol
|
transfer.asset?.symbol
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(filter.searchTerm.toLowerCase()) ||
|
.includes(filter.searchTerm.toLowerCase()) ||
|
||||||
EntityScopeLabelMapping[transfer.kind.dispatchStrategy.entityScope]
|
(
|
||||||
|
EntityScopeLabelMapping[transfer.kind.dispatchStrategy.entityScope] ||
|
||||||
|
'Unspecified'
|
||||||
|
)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(filter.searchTerm.toLowerCase()) ||
|
.includes(filter.searchTerm.toLowerCase()) ||
|
||||||
node.asset?.name
|
node.asset?.name
|
||||||
@ -114,42 +97,15 @@ export const applyFilter = (
|
|||||||
|
|
||||||
export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { data: activeRewardsData } = useActiveRewardsQuery({
|
const { data } = useRewards({
|
||||||
variables: {
|
onlyActive: true,
|
||||||
isReward: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [filter, setFilter] = useState<Filter>({
|
const [filter, setFilter] = useState<Filter>({
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: assets } = useAssetsMapProvider();
|
if (!data || !data.length) return null;
|
||||||
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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -157,7 +113,8 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
|||||||
className="lg:col-span-full"
|
className="lg:col-span-full"
|
||||||
data-testid="active-rewards-card"
|
data-testid="active-rewards-card"
|
||||||
>
|
>
|
||||||
{enrichedTransfers.length > 1 && (
|
{/** CARDS FILTER */}
|
||||||
|
{data.length > 1 && (
|
||||||
<TradingInput
|
<TradingInput
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
|
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
|
||||||
@ -172,142 +129,32 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
|||||||
prependElement={<VegaIcon name={VegaIconNames.SEARCH} />}
|
prependElement={<VegaIcon name={VegaIconNames.SEARCH} />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{/** CARDS */}
|
||||||
<TinyScroll className="grid gap-x-8 gap-y-10 h-fit grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] md:grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] lg:grid-cols-[repeat(auto-fill,_minmax(320px,_1fr))] xl:grid-cols-[repeat(auto-fill,_minmax(335px,_1fr))] max-h-[40rem] overflow-auto pr-2">
|
<TinyScroll className="grid gap-x-8 gap-y-10 h-fit grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] md:grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] lg:grid-cols-[repeat(auto-fill,_minmax(320px,_1fr))] xl:grid-cols-[repeat(auto-fill,_minmax(335px,_1fr))] max-h-[40rem] overflow-auto pr-2">
|
||||||
{enrichedTransfers
|
{data
|
||||||
.filter((n) => applyFilter(n, filter))
|
.filter((n) => applyFilter(n, filter))
|
||||||
.map((node, i) => {
|
.map((node, i) => (
|
||||||
const { transfer } = node;
|
|
||||||
if (
|
|
||||||
transfer.kind.__typename !== 'RecurringTransfer' ||
|
|
||||||
!transfer.kind.dispatchStrategy?.dispatchMetric
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
node && (
|
|
||||||
<ActiveRewardCard
|
<ActiveRewardCard
|
||||||
key={i}
|
key={i}
|
||||||
transferNode={node}
|
transferNode={node}
|
||||||
kind={transfer.kind}
|
|
||||||
currentEpoch={currentEpoch}
|
currentEpoch={currentEpoch}
|
||||||
allMarkets={markets || {}}
|
|
||||||
/>
|
/>
|
||||||
)
|
))}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TinyScroll>
|
</TinyScroll>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// This was built to be a status indicator for the rewards based on the transfer status
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const StatusIndicator = ({
|
|
||||||
status,
|
|
||||||
reason,
|
|
||||||
}: {
|
|
||||||
status: TransferStatus;
|
|
||||||
reason?: Maybe<string> | undefined;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
const getIconIntent = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case TransferStatus.STATUS_DONE:
|
|
||||||
return { icon: IconNames.TICK_CIRCLE, intent: Intent.Success };
|
|
||||||
case TransferStatus.STATUS_REJECTED:
|
|
||||||
return { icon: IconNames.ERROR, intent: Intent.Danger };
|
|
||||||
default:
|
|
||||||
return { icon: IconNames.HELP, intent: Intent.Primary };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const { icon, intent } = getIconIntent(status);
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
description={
|
|
||||||
<span>
|
|
||||||
{t('Transfer status: {{status}} {{reason}}', {
|
|
||||||
status: TransferStatusMapping[status],
|
|
||||||
reason: reason ? `(${reason})` : '',
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
{
|
|
||||||
'text-gray-700 dark:text-gray-300': intent === Intent.None,
|
|
||||||
'text-vega-blue': intent === Intent.Primary,
|
|
||||||
'text-vega-green dark:text-vega-green': intent === Intent.Success,
|
|
||||||
'dark:text-yellow text-yellow-600': intent === Intent.Warning,
|
|
||||||
'text-vega-red': intent === Intent.Danger,
|
|
||||||
},
|
|
||||||
'flex items-start p-1 align-text-bottom'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon size={3} name={icon as IconName} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type ActiveRewardCardProps = {
|
type ActiveRewardCardProps = {
|
||||||
transferNode: TransferNode & {
|
transferNode: EnrichedRewardTransfer;
|
||||||
asset?: AssetFieldsFragment | null;
|
|
||||||
markets?: (MarketFieldsFragment | null)[];
|
|
||||||
};
|
|
||||||
currentEpoch: number;
|
currentEpoch: number;
|
||||||
kind: RecurringTransfer;
|
|
||||||
allMarkets?: Record<string, MarketFieldsFragment | null>;
|
|
||||||
};
|
};
|
||||||
export const ActiveRewardCard = ({
|
export const ActiveRewardCard = ({
|
||||||
transferNode,
|
transferNode,
|
||||||
currentEpoch,
|
currentEpoch,
|
||||||
kind,
|
|
||||||
allMarkets,
|
|
||||||
}: ActiveRewardCardProps) => {
|
}: ActiveRewardCardProps) => {
|
||||||
const t = useT();
|
// don't display the cards that are scoped to not trading markets
|
||||||
|
const marketSettled = transferNode.markets?.filter(
|
||||||
const { transfer } = transferNode;
|
|
||||||
const { dispatchStrategy } = kind;
|
|
||||||
|
|
||||||
const marketIdsInScope = dispatchStrategy?.marketIdsInScope;
|
|
||||||
const firstMarketData = transferNode.markets?.[0];
|
|
||||||
|
|
||||||
const specificMarkets = useMemo(() => {
|
|
||||||
if (
|
|
||||||
!firstMarketData ||
|
|
||||||
!marketIdsInScope ||
|
|
||||||
marketIdsInScope.length === 0
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (marketIdsInScope.length > 1) {
|
|
||||||
const marketNames =
|
|
||||||
allMarkets &&
|
|
||||||
marketIdsInScope
|
|
||||||
.map((id) => allMarkets[id]?.tradableInstrument?.instrument?.name)
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip description={marketNames}>
|
|
||||||
<span>Specific markets</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span>{firstMarketData?.tradableInstrument?.instrument?.name || ''}</span>
|
|
||||||
);
|
|
||||||
}, [firstMarketData, marketIdsInScope, allMarkets]);
|
|
||||||
|
|
||||||
const dispatchAsset = transferNode.asset;
|
|
||||||
|
|
||||||
if (!dispatchStrategy) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gray out/hide the cards that are related to not trading markets
|
|
||||||
const marketSettled = transferNode.markets?.some(
|
|
||||||
(m) =>
|
(m) =>
|
||||||
m?.state &&
|
m?.state &&
|
||||||
[
|
[
|
||||||
@ -318,78 +165,114 @@ export const ActiveRewardCard = ({
|
|||||||
].includes(m.state)
|
].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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetInActiveMarket =
|
let colour =
|
||||||
allMarkets &&
|
DispatchMetricColourMap[
|
||||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
transferNode.transfer.kind.dispatchStrategy.dispatchMetric
|
||||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
];
|
||||||
return m?.state && MarketState.STATE_ACTIVE === m.state;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const marketSuspended = transferNode.markets?.some(
|
// 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) =>
|
||||||
m?.state === MarketState.STATE_SUSPENDED ||
|
m?.state === MarketState.STATE_SUSPENDED ||
|
||||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||||
);
|
).length === transferNode.markets?.length &&
|
||||||
|
Boolean(transferNode.markets && transferNode.markets.length > 0);
|
||||||
|
|
||||||
// Gray out the cards that are related to suspended markets
|
if (marketSuspended || !transferNode.isAssetTraded) {
|
||||||
// Or settlement assets in markets that are not active and eligible for rewards
|
colour = CardColour.GREY;
|
||||||
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;
|
return (
|
||||||
|
<RewardCard
|
||||||
|
colour={colour}
|
||||||
|
rewardAmount={addDecimalsFormatNumber(
|
||||||
|
transferNode.transfer.amount,
|
||||||
|
transferNode.transfer.asset?.decimals || 0,
|
||||||
|
6
|
||||||
|
)}
|
||||||
|
rewardAsset={transferNode.asset}
|
||||||
|
endsIn={
|
||||||
|
transferNode.transfer.kind.endEpoch != null
|
||||||
|
? transferNode.transfer.kind.endEpoch - currentEpoch
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
dispatchStrategy={transferNode.transfer.kind.dispatchStrategy}
|
||||||
|
dispatchMetricInfo={<DispatchMetricInfo reward={transferNode} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'bg-gradient-to-r col-span-full p-0.5 lg:col-auto h-full',
|
'bg-gradient-to-r col-span-full p-0.5 lg:col-auto h-full',
|
||||||
'rounded-lg',
|
'rounded-lg',
|
||||||
gradientClassName
|
CardColourStyles[colour].gradientClassName
|
||||||
)}
|
)}
|
||||||
data-testid="active-rewards-card"
|
data-testid="active-rewards-card"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
mainClassName,
|
CardColourStyles[colour].mainClassName,
|
||||||
'bg-gradient-to-b bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4 flex flex-col gap-4'
|
'bg-gradient-to-b bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded-md p-4 flex flex-col gap-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between gap-4">
|
<div className="flex justify-between gap-4">
|
||||||
|
{/** ENTITY SCOPE */}
|
||||||
<div className="flex flex-col gap-2 items-center text-center">
|
<div className="flex flex-col gap-2 items-center text-center">
|
||||||
<EntityIcon transfer={transfer} />
|
<EntityIcon entityScope={dispatchStrategy.entityScope} />
|
||||||
{entityScope && (
|
{dispatchStrategy.entityScope && (
|
||||||
<span className="text-muted text-xs" data-testid="entity-scope">
|
<span className="text-muted text-xs" data-testid="entity-scope">
|
||||||
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
{EntityScopeLabelMapping[dispatchStrategy.entityScope] ||
|
||||||
|
t('Unspecified')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/** AMOUNT AND DISTRIBUTION STRATEGY */}
|
||||||
<div className="flex flex-col gap-2 items-center text-center">
|
<div className="flex flex-col gap-2 items-center text-center">
|
||||||
|
{/** AMOUNT */}
|
||||||
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
||||||
<span className="font-glitch" data-testid="reward-value">
|
<span className="font-glitch" data-testid="reward-value">
|
||||||
{addDecimalsFormatNumber(
|
{rewardAmount}
|
||||||
transferNode.transfer.amount,
|
|
||||||
transferNode.transfer.asset?.decimals || 0,
|
|
||||||
6
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="font-alpha">
|
<span className="font-alpha">{rewardAsset?.symbol || ''}</span>
|
||||||
{transferNode.transfer.asset?.symbol}
|
|
||||||
</span>
|
|
||||||
</h3>
|
</h3>
|
||||||
{
|
|
||||||
|
{/** DISTRIBUTION STRATEGY */}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
description={t(
|
description={t(
|
||||||
DistributionStrategyDescriptionMapping[
|
DistributionStrategyDescriptionMapping[
|
||||||
@ -406,9 +289,9 @@ export const ActiveRewardCard = ({
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/** DISTRIBUTION DELAY */}
|
||||||
<div className="flex flex-col gap-2 items-center text-center">
|
<div className="flex flex-col gap-2 items-center text-center">
|
||||||
<CardIcon
|
<CardIcon
|
||||||
iconName={VegaIconNames.LOCK}
|
iconName={VegaIconNames.LOCK}
|
||||||
@ -421,43 +304,38 @@ export const ActiveRewardCard = ({
|
|||||||
data-testid="locked-for"
|
data-testid="locked-for"
|
||||||
>
|
>
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
{t('numberEpochs', '{{count}} epochs', {
|
||||||
count: kind.dispatchStrategy?.lockPeriod,
|
count: dispatchStrategy.lockPeriod,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="border-[0.5px] border-gray-700" />
|
<span className="border-[0.5px] border-gray-700" />
|
||||||
|
{/** DISPATCH METRIC */}
|
||||||
|
{dispatchMetricInfo ? (
|
||||||
|
dispatchMetricInfo
|
||||||
|
) : (
|
||||||
<span data-testid="dispatch-metric-info">
|
<span data-testid="dispatch-metric-info">
|
||||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]}
|
||||||
<Tooltip
|
|
||||||
underline={marketSuspended}
|
|
||||||
description={
|
|
||||||
(marketSuspended || !assetInActiveMarket) &&
|
|
||||||
(specificMarkets
|
|
||||||
? t('Eligible market(s) currently suspended')
|
|
||||||
: !assetInActiveMarket
|
|
||||||
? t('Currently no markets eligible for reward')
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>{specificMarkets || dispatchAsset?.name}</span>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-8 flex-wrap">
|
<div className="flex items-center gap-8 flex-wrap">
|
||||||
{kind.endEpoch && (
|
{/** ENDS IN */}
|
||||||
|
{endsIn != null && (
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="text-muted text-xs">{t('Ends in')} </span>
|
<span className="text-muted text-xs">{t('Ends in')} </span>
|
||||||
<span data-testid="ends-in">
|
<span data-testid="ends-in" data-endsin={endsIn}>
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
{endsIn >= 0
|
||||||
count: kind.endEpoch - currentEpoch,
|
? t('numberEpochs', '{{count}} epochs', {
|
||||||
})}
|
count: endsIn,
|
||||||
|
})
|
||||||
|
: t('Ended')}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{
|
{/** WINDOW LENGTH */}
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
||||||
<span data-testid="assessed-over">
|
<span data-testid="assessed-over">
|
||||||
@ -466,18 +344,19 @@ export const ActiveRewardCard = ({
|
|||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
{/** DISPATCH METRIC DESCRIPTION */}
|
||||||
{dispatchStrategy?.dispatchMetric && (
|
{dispatchStrategy?.dispatchMetric && (
|
||||||
<span className="text-muted text-sm h-[3rem]">
|
<span className="text-muted text-sm h-[3rem]">
|
||||||
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
|
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="border-[0.5px] border-gray-700" />
|
<span className="border-[0.5px] border-gray-700" />
|
||||||
{kind.dispatchStrategy && (
|
{/** REQUIREMENTS */}
|
||||||
|
{dispatchStrategy && (
|
||||||
<RewardRequirements
|
<RewardRequirements
|
||||||
dispatchStrategy={kind.dispatchStrategy}
|
dispatchStrategy={dispatchStrategy}
|
||||||
assetDecimalPlaces={transfer.asset?.decimals}
|
rewardAsset={rewardAsset}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -487,77 +366,67 @@ export const ActiveRewardCard = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DispatchMetricInfo = ({
|
export const DispatchMetricInfo = ({
|
||||||
transferNode,
|
reward,
|
||||||
allMarkets,
|
|
||||||
}: {
|
}: {
|
||||||
transferNode: ActiveRewardCardProps['transferNode'];
|
reward: EnrichedRewardTransfer;
|
||||||
allMarkets?: ActiveRewardCardProps['allMarkets'];
|
|
||||||
}) => {
|
}) => {
|
||||||
const dispatchStrategy =
|
const t = useT();
|
||||||
transferNode.transfer.kind.__typename === 'RecurringTransfer'
|
const dispatchStrategy = reward.transfer.kind.dispatchStrategy;
|
||||||
? transferNode.transfer.kind.dispatchStrategy
|
const marketNames = compact(
|
||||||
: null;
|
reward.markets?.map((m) => m.tradableInstrument.instrument.name)
|
||||||
|
);
|
||||||
|
|
||||||
const dispatchAsset = transferNode.transfer.asset;
|
let additionalDispatchMetricInfo = null;
|
||||||
|
|
||||||
const marketIdsInScope = dispatchStrategy?.marketIdsInScope;
|
// if asset found then display asset symbol
|
||||||
const firstMarketData = transferNode.markets?.[0];
|
if (reward.asset) {
|
||||||
const specificMarkets = useMemo(() => {
|
additionalDispatchMetricInfo = <span>{reward.asset.symbol}</span>;
|
||||||
if (
|
|
||||||
!firstMarketData ||
|
|
||||||
!marketIdsInScope ||
|
|
||||||
marketIdsInScope.length === 0
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
if (marketIdsInScope.length > 1) {
|
// but if scoped to only one market then display market name
|
||||||
const marketNames =
|
if (marketNames.length === 1) {
|
||||||
allMarkets &&
|
additionalDispatchMetricInfo = <span>{marketNames[0]}</span>;
|
||||||
marketIdsInScope
|
}
|
||||||
.map((id) => allMarkets[id]?.tradableInstrument?.instrument?.name)
|
// or if scoped to many markets then indicate it's scoped to "specific markets"
|
||||||
.join(', ');
|
if (marketNames.length > 1) {
|
||||||
|
additionalDispatchMetricInfo = (
|
||||||
return (
|
|
||||||
<Tooltip description={marketNames}>
|
<Tooltip description={marketNames}>
|
||||||
<span>Specific markets</span>
|
<span>{t('Specific markets')}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = firstMarketData?.tradableInstrument?.instrument?.name;
|
|
||||||
if (name) {
|
|
||||||
return <span>{name}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [firstMarketData, marketIdsInScope, allMarkets]);
|
|
||||||
|
|
||||||
if (!dispatchStrategy) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span data-testid="dispatch-metric-info">
|
<span data-testid="dispatch-metric-info">
|
||||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]}
|
||||||
<span>{specificMarkets || dispatchAsset?.name}</span>
|
{additionalDispatchMetricInfo != null && (
|
||||||
|
<> • {additionalDispatchMetricInfo}</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RewardRequirements = ({
|
const RewardRequirements = ({
|
||||||
dispatchStrategy,
|
dispatchStrategy,
|
||||||
assetDecimalPlaces = 0,
|
rewardAsset,
|
||||||
|
vegaAsset,
|
||||||
}: {
|
}: {
|
||||||
dispatchStrategy: DispatchStrategy;
|
dispatchStrategy: DispatchStrategy;
|
||||||
assetDecimalPlaces: number | undefined;
|
rewardAsset?: BasicAssetDetails;
|
||||||
|
vegaAsset?: BasicAssetDetails;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
|
|
||||||
|
const entityLabel = EntityScopeLabelMapping[dispatchStrategy.entityScope];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="flex justify-between flex-wrap items-center gap-3 text-xs">
|
<dl className="flex justify-between flex-wrap items-center gap-3 text-xs">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<dt className="flex items-center gap-1 text-muted">
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
{t('{{entity}} scope', {
|
{entityLabel
|
||||||
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
|
? t('{{entity}} scope', {
|
||||||
})}
|
entity: entityLabel,
|
||||||
|
})
|
||||||
|
: t('Scope')}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="flex items-center gap-1" data-testid="scope">
|
<dd className="flex items-center gap-1" data-testid="scope">
|
||||||
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
||||||
@ -574,7 +443,7 @@ const RewardRequirements = ({
|
|||||||
>
|
>
|
||||||
{addDecimalsFormatNumber(
|
{addDecimalsFormatNumber(
|
||||||
dispatchStrategy?.stakingRequirement || 0,
|
dispatchStrategy?.stakingRequirement || 0,
|
||||||
assetDecimalPlaces
|
vegaAsset?.decimals || 18
|
||||||
)}
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@ -587,7 +456,7 @@ const RewardRequirements = ({
|
|||||||
{addDecimalsFormatNumber(
|
{addDecimalsFormatNumber(
|
||||||
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
||||||
0,
|
0,
|
||||||
assetDecimalPlaces
|
rewardAsset?.decimals || 0
|
||||||
)}
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@ -645,44 +514,65 @@ const RewardEntityScope = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return t('Unspecified');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGradientClasses = (d: DispatchMetric | undefined) => {
|
const CardColourStyles: Record<
|
||||||
switch (d) {
|
CardColour,
|
||||||
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
|
{ gradientClassName: string; mainClassName: string }
|
||||||
return {
|
> = {
|
||||||
gradientClassName: 'from-vega-pink-500 to-vega-purple-400',
|
[CardColour.BLUE]: {
|
||||||
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',
|
gradientClassName: 'from-vega-blue-500 to-vega-green-400',
|
||||||
mainClassName: 'from-vega-blue-400 dark:from-vega-blue-600 to-20%',
|
mainClassName: 'from-vega-blue-400 dark:from-vega-blue-600 to-20%',
|
||||||
};
|
},
|
||||||
case DispatchMetric.DISPATCH_METRIC_VALIDATOR_RANKING:
|
[CardColour.GREEN]: {
|
||||||
default:
|
gradientClassName: 'from-vega-green-500 to-vega-yellow-500',
|
||||||
return {
|
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',
|
gradientClassName: 'from-vega-pink-500 to-vega-purple-400',
|
||||||
mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%',
|
mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%',
|
||||||
};
|
},
|
||||||
}
|
[CardColour.PURPLE]: {
|
||||||
|
gradientClassName: 'from-vega-purple-500 to-vega-blue-400',
|
||||||
|
mainClassName: 'from-vega-purple-400 dark:from-vega-purple-600 to-20%',
|
||||||
|
},
|
||||||
|
[CardColour.WHITE]: {
|
||||||
|
gradientClassName:
|
||||||
|
'from-vega-clight-600 dark:from-vega-clight-900 to-vega-yellow-500 dark:to-vega-yellow-400',
|
||||||
|
mainClassName: 'from-white dark:from-vega-clight-100 to-20%',
|
||||||
|
},
|
||||||
|
[CardColour.YELLOW]: {
|
||||||
|
gradientClassName: 'from-vega-yellow-500 to-vega-orange-400',
|
||||||
|
mainClassName: 'from-vega-yellow-400 dark:from-vega-yellow-600 to-20%',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DispatchMetricColourMap: Record<DispatchMetric, CardColour> = {
|
||||||
|
// Liquidity provision fees received
|
||||||
|
[DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED]: CardColour.BLUE,
|
||||||
|
// Price maker fees paid
|
||||||
|
[DispatchMetric.DISPATCH_METRIC_MAKER_FEES_PAID]: CardColour.PINK,
|
||||||
|
// Price maker fees earned
|
||||||
|
[DispatchMetric.DISPATCH_METRIC_MAKER_FEES_RECEIVED]: CardColour.GREEN,
|
||||||
|
// Total market value
|
||||||
|
[DispatchMetric.DISPATCH_METRIC_MARKET_VALUE]: CardColour.WHITE,
|
||||||
|
// Average position
|
||||||
|
[DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION]: CardColour.ORANGE,
|
||||||
|
// Relative return
|
||||||
|
[DispatchMetric.DISPATCH_METRIC_RELATIVE_RETURN]: CardColour.PURPLE,
|
||||||
|
// Return volatility
|
||||||
|
[DispatchMetric.DISPATCH_METRIC_RETURN_VOLATILITY]: CardColour.YELLOW,
|
||||||
|
// Validator ranking
|
||||||
|
[DispatchMetric.DISPATCH_METRIC_VALIDATOR_RANKING]: CardColour.WHITE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CardIcon = ({
|
const CardIcon = ({
|
||||||
@ -703,36 +593,29 @@ const CardIcon = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const EntityScopeIconMap: Record<EntityScope, VegaIconNames> = {
|
||||||
|
[EntityScope.ENTITY_SCOPE_TEAMS]: VegaIconNames.TEAM,
|
||||||
|
[EntityScope.ENTITY_SCOPE_INDIVIDUALS]: VegaIconNames.MAN,
|
||||||
|
};
|
||||||
|
|
||||||
const EntityIcon = ({
|
const EntityIcon = ({
|
||||||
transfer,
|
entityScope,
|
||||||
size = 18,
|
size = 18,
|
||||||
}: {
|
}: {
|
||||||
transfer: Transfer;
|
entityScope: EntityScope;
|
||||||
size?: VegaIconSize;
|
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 (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
description={
|
description={
|
||||||
<span>{entityScope ? EntityScopeMapping[entityScope] : ''}</span>
|
entityScope ? <span>{EntityScopeMapping[entityScope]}</span> : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="flex items-center p-2 rounded-full border border-gray-600">
|
<span className="flex items-center p-2 rounded-full border border-gray-600">
|
||||||
{iconName && <VegaIcon name={iconName} size={size} />}
|
<VegaIcon
|
||||||
|
name={EntityScopeIconMap[entityScope] || VegaIconNames.QUESTION_MARK}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
type RewardsPageQuery,
|
type RewardsPageQuery,
|
||||||
useRewardsPageQuery,
|
useRewardsPageQuery,
|
||||||
useRewardsEpochQuery,
|
useRewardsEpochQuery,
|
||||||
} from './__generated__/Rewards';
|
} from '../../lib/hooks/__generated__/Rewards';
|
||||||
import {
|
import {
|
||||||
TradingButton,
|
TradingButton,
|
||||||
VegaIcon,
|
VegaIcon,
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
useRewardsHistoryQuery,
|
useRewardsHistoryQuery,
|
||||||
type RewardsHistoryQuery,
|
type RewardsHistoryQuery,
|
||||||
} from './__generated__/Rewards';
|
} from '../../lib/hooks/__generated__/Rewards';
|
||||||
import { useRewardsRowData } from './use-reward-row-data';
|
import { useRewardsRowData } from './use-reward-row-data';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import BigNumber from 'bignumber.js';
|
|||||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||||
import { type Asset } from '@vegaprotocol/assets';
|
import { type Asset } from '@vegaprotocol/assets';
|
||||||
import { type PartyRewardsConnection } from './rewards-history';
|
import { type PartyRewardsConnection } from './rewards-history';
|
||||||
import { type RewardsHistoryQuery } from './__generated__/Rewards';
|
import { type RewardsHistoryQuery } from '../../lib/hooks/__generated__/Rewards';
|
||||||
|
|
||||||
const REWARD_ACCOUNT_TYPES = [
|
const REWARD_ACCOUNT_TYPES = [
|
||||||
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||||
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
242
apps/trading/lib/hooks/use-rewards.spec.ts
Normal file
242
apps/trading/lib/hooks/use-rewards.spec.ts
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
import {
|
||||||
|
type DispatchStrategy,
|
||||||
|
type TransferNode,
|
||||||
|
EntityScope,
|
||||||
|
type TransferKind,
|
||||||
|
TransferStatus,
|
||||||
|
IndividualScope,
|
||||||
|
} from '@vegaprotocol/types';
|
||||||
|
import {
|
||||||
|
type RewardTransfer,
|
||||||
|
isActiveReward,
|
||||||
|
isReward,
|
||||||
|
isScopedToTeams,
|
||||||
|
} from './use-rewards';
|
||||||
|
|
||||||
|
const makeDispatchStrategy = (
|
||||||
|
entityScope: EntityScope,
|
||||||
|
individualScope?: IndividualScope
|
||||||
|
): DispatchStrategy =>
|
||||||
|
({
|
||||||
|
entityScope,
|
||||||
|
individualScope,
|
||||||
|
} as DispatchStrategy);
|
||||||
|
|
||||||
|
const makeReward = (
|
||||||
|
status: TransferStatus,
|
||||||
|
startEpoch: number,
|
||||||
|
endEpoch?: number,
|
||||||
|
dispatchStrategy?: DispatchStrategy,
|
||||||
|
kind: TransferKind['__typename'] = 'OneOffTransfer'
|
||||||
|
): RewardTransfer =>
|
||||||
|
({
|
||||||
|
transfer: {
|
||||||
|
status,
|
||||||
|
kind: {
|
||||||
|
__typename: kind,
|
||||||
|
dispatchStrategy,
|
||||||
|
startEpoch,
|
||||||
|
endEpoch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as RewardTransfer);
|
||||||
|
|
||||||
|
describe('isReward', () => {
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_INDIVIDUALS),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
|
||||||
|
'OneOffTransfer'
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_INDIVIDUALS),
|
||||||
|
'OneOffTransfer'
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
])('checks if given transfer is a reward or not', (input, output) => {
|
||||||
|
expect(isReward(input as TransferNode)).toEqual(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isActiveReward', () => {
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
2,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
3,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
4, // start in 1 epoch
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_DONE, // done, not active any more
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
2, // ended 1 epoch ago
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
3, // ends now, but active until end of epoch
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
])('checks if given reward is active or not', (input, output) => {
|
||||||
|
expect(isActiveReward(input, 3)).toEqual(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isScopedToTeams', () => {
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_TEAMS), // only teams
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(
|
||||||
|
EntityScope.ENTITY_SCOPE_INDIVIDUALS,
|
||||||
|
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM // individual in teams
|
||||||
|
),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(EntityScope.ENTITY_SCOPE_INDIVIDUALS), // not in team
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(
|
||||||
|
EntityScope.ENTITY_SCOPE_INDIVIDUALS,
|
||||||
|
IndividualScope.INDIVIDUAL_SCOPE_ALL // not only in team
|
||||||
|
),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
makeReward(
|
||||||
|
TransferStatus.STATUS_PENDING,
|
||||||
|
1,
|
||||||
|
undefined,
|
||||||
|
makeDispatchStrategy(
|
||||||
|
EntityScope.ENTITY_SCOPE_INDIVIDUALS,
|
||||||
|
IndividualScope.INDIVIDUAL_SCOPE_NOT_IN_TEAM // not in team
|
||||||
|
),
|
||||||
|
'RecurringTransfer'
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
])('checks if given reward is scoped to teams or not', (input, output) => {
|
||||||
|
expect(isScopedToTeams(input)).toEqual(output);
|
||||||
|
});
|
||||||
|
});
|
181
apps/trading/lib/hooks/use-rewards.ts
Normal file
181
apps/trading/lib/hooks/use-rewards.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import {
|
||||||
|
type AssetFieldsFragment,
|
||||||
|
useAssetsMapProvider,
|
||||||
|
} from '@vegaprotocol/assets';
|
||||||
|
import { useActiveRewardsQuery } from './__generated__/Rewards';
|
||||||
|
import {
|
||||||
|
type MarketFieldsFragment,
|
||||||
|
useMarketsMapProvider,
|
||||||
|
getAsset,
|
||||||
|
} from '@vegaprotocol/markets';
|
||||||
|
import {
|
||||||
|
type RecurringTransfer,
|
||||||
|
type TransferNode,
|
||||||
|
TransferStatus,
|
||||||
|
type DispatchStrategy,
|
||||||
|
EntityScope,
|
||||||
|
IndividualScope,
|
||||||
|
MarketState,
|
||||||
|
} from '@vegaprotocol/types';
|
||||||
|
import { type ApolloError } from '@apollo/client';
|
||||||
|
import compact from 'lodash/compact';
|
||||||
|
import { useEpochInfoQuery } from './__generated__/Epoch';
|
||||||
|
|
||||||
|
export type RewardTransfer = TransferNode & {
|
||||||
|
transfer: {
|
||||||
|
kind: RecurringTransfer & {
|
||||||
|
dispatchStrategy: DispatchStrategy;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EnrichedRewardTransfer = RewardTransfer & {
|
||||||
|
/** Dispatch metric asset (reward asset) */
|
||||||
|
asset?: AssetFieldsFragment;
|
||||||
|
/** A flag determining whether a reward asset is being traded on any of the active markets */
|
||||||
|
isAssetTraded?: boolean;
|
||||||
|
/** A list of markets in scope */
|
||||||
|
markets?: MarketFieldsFragment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given transfer is a reward.
|
||||||
|
*
|
||||||
|
* A reward has to be a recurring transfer and has to have a
|
||||||
|
* dispatch strategy.
|
||||||
|
*/
|
||||||
|
export const isReward = (node: TransferNode): node is RewardTransfer => {
|
||||||
|
if (
|
||||||
|
node.transfer.kind.__typename === 'RecurringTransfer' &&
|
||||||
|
node.transfer.kind.dispatchStrategy != null
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given reward (transfer) is active.
|
||||||
|
*/
|
||||||
|
export const isActiveReward = (node: RewardTransfer, currentEpoch: number) => {
|
||||||
|
const { transfer } = node;
|
||||||
|
|
||||||
|
const pending = transfer.status === TransferStatus.STATUS_PENDING;
|
||||||
|
const withinEpochs =
|
||||||
|
transfer.kind.startEpoch <= currentEpoch &&
|
||||||
|
(transfer.kind.endEpoch != null
|
||||||
|
? transfer.kind.endEpoch >= currentEpoch
|
||||||
|
: true);
|
||||||
|
|
||||||
|
if (pending && withinEpochs) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given reward (transfer) is scoped to teams.
|
||||||
|
*
|
||||||
|
* A reward is scoped to teams if it's entity scope is set to teams or
|
||||||
|
* if the scope is set to individuals but the individuals are in a team.
|
||||||
|
*/
|
||||||
|
export const isScopedToTeams = (node: EnrichedRewardTransfer) =>
|
||||||
|
// scoped to teams
|
||||||
|
node.transfer.kind.dispatchStrategy.entityScope ===
|
||||||
|
EntityScope.ENTITY_SCOPE_TEAMS ||
|
||||||
|
// or to individuals
|
||||||
|
(node.transfer.kind.dispatchStrategy.entityScope ===
|
||||||
|
EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
|
||||||
|
// but they have to be in a team
|
||||||
|
node.transfer.kind.dispatchStrategy.individualScope ===
|
||||||
|
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM);
|
||||||
|
|
||||||
|
/** Retrieves rewards (transfers) */
|
||||||
|
export const useRewards = ({
|
||||||
|
// get active by default
|
||||||
|
onlyActive = true,
|
||||||
|
scopeToTeams = false,
|
||||||
|
}: {
|
||||||
|
onlyActive: boolean;
|
||||||
|
scopeToTeams?: boolean;
|
||||||
|
}): {
|
||||||
|
data: EnrichedRewardTransfer[];
|
||||||
|
loading: boolean;
|
||||||
|
error?: ApolloError | Error;
|
||||||
|
} => {
|
||||||
|
const {
|
||||||
|
data: epochData,
|
||||||
|
loading: epochLoading,
|
||||||
|
error: epochError,
|
||||||
|
} = useEpochInfoQuery({
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentEpoch = Number(epochData?.epoch.id);
|
||||||
|
|
||||||
|
const { data, loading, error } = useActiveRewardsQuery({
|
||||||
|
variables: {
|
||||||
|
isReward: true,
|
||||||
|
},
|
||||||
|
skip: onlyActive && isNaN(currentEpoch),
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: assets,
|
||||||
|
loading: assetsLoading,
|
||||||
|
error: assetsError,
|
||||||
|
} = useAssetsMapProvider();
|
||||||
|
const {
|
||||||
|
data: markets,
|
||||||
|
loading: marketsLoading,
|
||||||
|
error: marketsError,
|
||||||
|
} = useMarketsMapProvider();
|
||||||
|
|
||||||
|
const enriched = compact(
|
||||||
|
data?.transfersConnection?.edges?.map((n) => n?.node)
|
||||||
|
)
|
||||||
|
.map((n) => n as TransferNode)
|
||||||
|
// make sure we have only rewards here
|
||||||
|
.filter(isReward)
|
||||||
|
// take only active rewards if required, otherwise take all
|
||||||
|
.filter((node) => (onlyActive ? isActiveReward(node, currentEpoch) : true))
|
||||||
|
// take only those rewards that are scoped to teams if required, otherwise take all
|
||||||
|
.filter((node) => (scopeToTeams ? isScopedToTeams(node) : true))
|
||||||
|
// enrich with dispatch asset and markets in scope details
|
||||||
|
.map((node) => {
|
||||||
|
const asset =
|
||||||
|
assets &&
|
||||||
|
assets[node.transfer.kind.dispatchStrategy.dispatchMetricAssetId];
|
||||||
|
const marketsInScope = compact(
|
||||||
|
node.transfer.kind.dispatchStrategy.marketIdsInScope?.map(
|
||||||
|
(id) => markets && markets[id]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const isAssetTraded =
|
||||||
|
markets &&
|
||||||
|
Object.values(markets).some((m) => {
|
||||||
|
try {
|
||||||
|
const mAsset = getAsset(m);
|
||||||
|
return (
|
||||||
|
mAsset.id ===
|
||||||
|
node.transfer.kind.dispatchStrategy.dispatchMetricAssetId &&
|
||||||
|
m.state === MarketState.STATE_ACTIVE
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// NOOP
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
asset: asset ? asset : undefined,
|
||||||
|
isAssetTraded: isAssetTraded != null ? isAssetTraded : undefined,
|
||||||
|
markets: marketsInScope.length > 0 ? marketsInScope : undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: enriched,
|
||||||
|
loading: loading || assetsLoading || marketsLoading || epochLoading,
|
||||||
|
error: error || assetsError || marketsError || epochError,
|
||||||
|
};
|
||||||
|
};
|
@ -93,16 +93,23 @@ export const useEnabledAssets = () => {
|
|||||||
|
|
||||||
/** Wrapped ETH symbol */
|
/** Wrapped ETH symbol */
|
||||||
const WETH = 'WETH';
|
const WETH = 'WETH';
|
||||||
type WETHDetails = Pick<AssetFieldsFragment, 'symbol' | 'decimals' | 'quantum'>;
|
|
||||||
|
/** VEGA */
|
||||||
|
const VEGA = 'VEGA';
|
||||||
|
|
||||||
|
export type BasicAssetDetails = Pick<
|
||||||
|
AssetFieldsFragment,
|
||||||
|
'symbol' | 'decimals' | 'quantum'
|
||||||
|
>;
|
||||||
/**
|
/**
|
||||||
* Tries to find WETH asset configuration on Vega in order to provide its
|
* Tries to find WETH asset configuration on Vega in order to provide its
|
||||||
* details, otherwise it returns hardcoded values.
|
* details, otherwise it returns hardcoded values.
|
||||||
*/
|
*/
|
||||||
export const useWETH = (): WETHDetails => {
|
export const useWETH = (): BasicAssetDetails => {
|
||||||
const { data } = useAssetsDataProvider();
|
const { data } = useAssetsDataProvider();
|
||||||
if (data) {
|
if (data) {
|
||||||
const weth = data.find((a) => a.symbol.toUpperCase() === WETH);
|
const details = data.find((a) => a.symbol.toUpperCase() === WETH);
|
||||||
if (weth) return weth;
|
if (details) return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -111,3 +118,17 @@ export const useWETH = (): WETHDetails => {
|
|||||||
quantum: '500000000000000', // 1 WETH ~= 2000 qUSD
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user