feat(trading): filter out suspended transfers (#5640)

Co-authored-by: bwallacee <ben@vega.xyz>
This commit is contained in:
m.ray 2024-01-23 11:38:09 +02:00 committed by GitHub
parent 51199b02ce
commit baf9875c69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 192 additions and 50 deletions

View File

@ -98,6 +98,7 @@ describe('ActiveRewards', () => {
transferNode={mockTransferNode} transferNode={mockTransferNode}
currentEpoch={1} currentEpoch={1}
kind={mockRecurringTransfer} kind={mockRecurringTransfer}
allMarkets={{}}
/> />
); );

View File

@ -1,48 +1,46 @@
import { import { useActiveRewardsQuery } from './__generated__/Rewards';
useActiveRewardsQuery,
useMarketForRewardsQuery,
} 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 {
Icon,
type IconName, type IconName,
type VegaIconSize,
Icon,
Intent, Intent,
Tooltip, Tooltip,
VegaIcon, VegaIcon,
VegaIconNames, VegaIconNames,
type VegaIconSize,
TradingInput, TradingInput,
TinyScroll, TinyScroll,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import { import {
type Maybe,
type Transfer,
type TransferNode,
type RecurringTransfer,
DistributionStrategyDescriptionMapping, DistributionStrategyDescriptionMapping,
DistributionStrategyMapping, DistributionStrategyMapping,
EntityScope, EntityScope,
EntityScopeMapping, EntityScopeMapping,
type Maybe,
type Transfer,
type TransferNode,
TransferStatus, TransferStatus,
TransferStatusMapping, TransferStatusMapping,
DispatchMetric, DispatchMetric,
DispatchMetricDescription, DispatchMetricDescription,
DispatchMetricLabels, DispatchMetricLabels,
type RecurringTransfer,
EntityScopeLabelMapping, EntityScopeLabelMapping,
MarketState,
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import { Card } from '../card/card'; import { Card } from '../card/card';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { import {
type AssetFieldsFragment, type AssetFieldsFragment,
useAssetDataProvider,
useAssetsMapProvider, useAssetsMapProvider,
} from '@vegaprotocol/assets'; } from '@vegaprotocol/assets';
import { import {
type MarketFieldsFragment, type MarketFieldsFragment,
useMarketsMapProvider, useMarketsMapProvider,
getAsset,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
export type Filter = { export type Filter = {
@ -74,7 +72,7 @@ export const isActiveReward = (node: TransferNode, currentEpoch: number) => {
export const applyFilter = ( export const applyFilter = (
node: TransferNode & { node: TransferNode & {
asset?: AssetFieldsFragment | null; asset?: AssetFieldsFragment | null;
marketIds?: (MarketFieldsFragment | null)[]; markets?: (MarketFieldsFragment | null)[];
}, },
filter: Filter filter: Filter
) => { ) => {
@ -85,6 +83,7 @@ export const applyFilter = (
) { ) {
return false; return false;
} }
if ( if (
DispatchMetricLabels[transfer.kind.dispatchStrategy.dispatchMetric] DispatchMetricLabels[transfer.kind.dispatchStrategy.dispatchMetric]
.toLowerCase() .toLowerCase()
@ -98,7 +97,7 @@ export const applyFilter = (
node.asset?.name node.asset?.name
.toLocaleLowerCase() .toLocaleLowerCase()
.includes(filter.searchTerm.toLowerCase()) || .includes(filter.searchTerm.toLowerCase()) ||
node.marketIds?.some((m) => node.markets?.some((m) =>
m?.tradableInstrument?.instrument?.name m?.tradableInstrument?.instrument?.name
.toLocaleLowerCase() .toLocaleLowerCase()
.includes(filter.searchTerm.toLowerCase()) .includes(filter.searchTerm.toLowerCase())
@ -124,7 +123,7 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
const { data: assets } = useAssetsMapProvider(); const { data: assets } = useAssetsMapProvider();
const { data: markets } = useMarketsMapProvider(); const { data: markets } = useMarketsMapProvider();
const transfers = activeRewardsData?.transfersConnection?.edges const enrichedTransfers = activeRewardsData?.transfersConnection?.edges
?.map((e) => e?.node as TransferNode) ?.map((e) => e?.node as TransferNode)
.filter((node) => isActiveReward(node, currentEpoch)) .filter((node) => isActiveReward(node, currentEpoch))
.map((node) => { .map((node) => {
@ -138,19 +137,19 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || '' node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
]; ];
const marketIds = const marketsInScope =
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map( node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
(id) => markets && markets[id] (id) => markets && markets[id]
); );
return { ...node, asset, marketIds }; return { ...node, asset, markets: marketsInScope };
}); });
if (!transfers || !transfers.length) return null; if (!enrichedTransfers || !enrichedTransfers.length) return null;
return ( return (
<Card title={t('Active rewards')} className="lg:col-span-full"> <Card title={t('Active rewards')} className="lg:col-span-full">
{transfers.length > 1 && ( {enrichedTransfers.length > 1 && (
<TradingInput <TradingInput
onChange={(e) => onChange={(e) =>
setFilter((curr) => ({ ...curr, searchTerm: e.target.value })) setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
@ -166,7 +165,7 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
/> />
)} )}
<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">
{transfers {enrichedTransfers
.filter((n) => applyFilter(n, filter)) .filter((n) => applyFilter(n, filter))
.map((node, i) => { .map((node, i) => {
const { transfer } = node; const { transfer } = node;
@ -184,6 +183,7 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
transferNode={node} transferNode={node}
kind={transfer.kind} kind={transfer.kind}
currentEpoch={currentEpoch} currentEpoch={currentEpoch}
allMarkets={markets || {}}
/> />
) )
); );
@ -207,14 +207,8 @@ const StatusIndicator = ({
switch (status) { switch (status) {
case TransferStatus.STATUS_DONE: case TransferStatus.STATUS_DONE:
return { icon: IconNames.TICK_CIRCLE, intent: Intent.Success }; return { icon: IconNames.TICK_CIRCLE, intent: Intent.Success };
case TransferStatus.STATUS_CANCELLED:
return { icon: IconNames.MOON, intent: Intent.None };
case TransferStatus.STATUS_PENDING:
return { icon: IconNames.HELP, intent: Intent.Primary };
case TransferStatus.STATUS_REJECTED: case TransferStatus.STATUS_REJECTED:
return { icon: IconNames.ERROR, intent: Intent.Danger }; return { icon: IconNames.ERROR, intent: Intent.Danger };
case TransferStatus.STATUS_STOPPED:
return { icon: IconNames.ERROR, intent: Intent.Danger };
default: default:
return { icon: IconNames.HELP, intent: Intent.Primary }; return { icon: IconNames.HELP, intent: Intent.Primary };
} }
@ -253,49 +247,117 @@ export const ActiveRewardCard = ({
transferNode, transferNode,
currentEpoch, currentEpoch,
kind, kind,
allMarkets,
}: { }: {
transferNode: TransferNode; transferNode: TransferNode & {
asset?: AssetFieldsFragment | null;
markets?: (MarketFieldsFragment | null)[];
};
currentEpoch: number; currentEpoch: number;
kind: RecurringTransfer; kind: RecurringTransfer;
allMarkets?: Record<string, MarketFieldsFragment | null>;
}) => { }) => {
const t = useT(); const t = useT();
const { transfer } = transferNode; const { transfer } = transferNode;
const { dispatchStrategy } = kind; const { dispatchStrategy } = kind;
const marketIds = dispatchStrategy?.marketIdsInScope;
const { data: marketNameData } = useMarketForRewardsQuery({ const marketIdsInScope = dispatchStrategy?.marketIdsInScope;
variables: { const firstMarketData = transferNode.markets?.[0];
marketId: marketIds ? marketIds[0] : '',
},
});
const marketName = useMemo(() => { const specificMarkets = useMemo(() => {
if (marketNameData && marketIds && marketIds.length > 1) { if (
return 'Specific markets'; !firstMarketData ||
} else if ( !marketIdsInScope ||
marketNameData && marketIdsInScope.length === 0
marketIds &&
marketNameData &&
marketIds.length === 1
) { ) {
return marketNameData?.market?.tradableInstrument?.instrument?.name || ''; return null;
} }
return ''; if (marketIdsInScope.length > 1) {
}, [marketIds, marketNameData]); const marketNames =
allMarkets &&
marketIdsInScope
.map((id) => allMarkets[id]?.tradableInstrument?.instrument?.name)
.join(', ');
const { data: dispatchAsset } = useAssetDataProvider( return (
dispatchStrategy?.dispatchMetricAssetId || '' <Tooltip description={marketNames}>
); <span>Specific markets</span>
</Tooltip>
);
}
return (
<span>{firstMarketData?.tradableInstrument?.instrument?.name || ''}</span>
);
}, [firstMarketData, marketIdsInScope, allMarkets]);
const dispatchAsset = transferNode.asset;
if (!dispatchStrategy) { if (!dispatchStrategy) {
return null; return null;
} }
const { gradientClassName, mainClassName } = getGradientClasses( // Gray out/hide the cards that are related to not trading markets
dispatchStrategy.dispatchMetric const marketSettled = transferNode.markets?.some(
(m) =>
m?.state &&
[
MarketState.STATE_TRADING_TERMINATED,
MarketState.STATE_SETTLED,
MarketState.STATE_CANCELLED,
MarketState.STATE_CLOSED,
].includes(m.state)
); );
const assetInSettledMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return (
m?.state &&
[
MarketState.STATE_TRADING_TERMINATED,
MarketState.STATE_SETTLED,
MarketState.STATE_CANCELLED,
MarketState.STATE_CLOSED,
].includes(m.state)
);
}
return false;
});
if (marketSettled) {
return null;
}
// Gray out the cards that are related to suspended markets
const suspended = transferNode.markets?.some(
(m) =>
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
);
const assetInSuspendedMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return (
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
);
}
return false;
});
// Gray out the cards that are related to suspended markets
const { gradientClassName, mainClassName } =
suspended || assetInSuspendedMarket || assetInSettledMarket
? {
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 entityScope = dispatchStrategy.entityScope;
return ( return (
<div> <div>
@ -373,8 +435,20 @@ export const ActiveRewardCard = ({
<span className="border-[0.5px] border-gray-700" /> <span className="border-[0.5px] border-gray-700" />
<span> <span>
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
{marketName ? `${marketName}` : `${dispatchAsset?.name}`} <Tooltip
underline={suspended}
description={
(suspended || assetInSuspendedMarket) &&
(specificMarkets
? t('Eligible market(s) currently suspended')
: assetInSuspendedMarket
? 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">

View File

@ -0,0 +1,67 @@
import pytest
import vega_sim.proto.vega as vega_protos
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from actions.utils import next_epoch
from wallet_config import MM_WALLET, PARTY_A, PARTY_B
from vega_sim.service import MarketStateUpdateType
import vega_sim.api.governance as governance
@pytest.mark.usefixtures("risk_accepted", "auth")
def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
vega.update_network_parameter(
MM_WALLET.name, parameter="reward.asset", new_value=tDAI_asset_id
)
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_A.name, asset=tDAI_asset_id, amount=100000)
next_epoch(vega=vega)
vega.recurring_transfer(
from_key_name=PARTY_A.name,
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
asset=tDAI_asset_id,
reference="reward",
markets=[continuous_market],
asset_for_metric=tDAI_asset_id,
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
lock_period=5,
amount=100,
factor=1.0,
)
vega.submit_order(
trading_key=PARTY_B.name,
market_id=continuous_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.submit_order(
trading_key=PARTY_A.name,
market_id=continuous_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
next_epoch(vega=vega)
vega.update_market_state(
market_id=continuous_market,
proposal_key=MM_WALLET.name,
market_state=MarketStateUpdateType.Suspend,
forward_time_to_enactment=True,
)
next_epoch(vega=vega)
page.goto("/#/rewards")
expect(page.locator(".from-vega-cdark-400")).to_be_visible()
governance.submit_oracle_data(
wallet=vega.wallet,
payload={"trading.terminated": "true"},
key_name="FJMKnwfZdd48C8NqvYrG",
)
next_epoch(vega=vega)
page.reload()
expect(page.locator(".from-vega-cdark-400")).not_to_be_in_viewport()