feat(trading): filter out suspended transfers (#5640)
Co-authored-by: bwallacee <ben@vega.xyz>
This commit is contained in:
parent
51199b02ce
commit
baf9875c69
@ -98,6 +98,7 @@ describe('ActiveRewards', () => {
|
|||||||
transferNode={mockTransferNode}
|
transferNode={mockTransferNode}
|
||||||
currentEpoch={1}
|
currentEpoch={1}
|
||||||
kind={mockRecurringTransfer}
|
kind={mockRecurringTransfer}
|
||||||
|
allMarkets={{}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
67
apps/trading/e2e/tests/rewards/test_filtered_cards.py
Normal file
67
apps/trading/e2e/tests/rewards/test_filtered_cards.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user