Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfe083cbee | ||
|
|
462066959d | ||
|
|
f12f5ab961 | ||
|
|
98b2460d8b | ||
|
|
67c182f3de | ||
|
|
68e0009ab0 | ||
|
|
ad74b12908 | ||
|
|
7fff33e493 | ||
|
|
e7cb2c8276 | ||
|
|
4adaeea40a | ||
|
|
d47d894147 | ||
|
|
515932d401 | ||
|
|
723ff2805d | ||
|
|
424cced4be | ||
|
|
b2c7ee82c5 | ||
|
|
4cc63be4f9 | ||
|
|
9fbb7a13e6 | ||
|
|
1874095222 | ||
|
|
e459e92127 | ||
|
|
4dd65b4923 | ||
|
|
5d792d2458 | ||
|
|
59b2f75b13 | ||
|
|
95775679ca | ||
|
|
4cffee29f8 | ||
|
|
bd70b0c233 | ||
|
|
145792f216 | ||
|
|
10add7c236 | ||
|
|
362a2031c7 | ||
|
|
5aaeb87059 | ||
|
|
d240dddab5 | ||
|
|
0af1d0ba82 | ||
|
|
665ab6693a |
@@ -1,3 +1,4 @@
|
||||
* text eol=lf
|
||||
*.png binary
|
||||
*.ico binary
|
||||
*.woff2 binary
|
||||
|
||||
@@ -24,7 +24,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||
const navigate = useNavigate();
|
||||
const ref = useRef<AgGridReact>(null);
|
||||
const showColumnsOnDesktop = () => {
|
||||
ref.current?.columnApi.setColumnsVisible(
|
||||
ref.current?.api.setColumnsVisible(
|
||||
['id', 'type', 'status'],
|
||||
window.innerWidth > BREAKPOINT_MD
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
|
||||
const gridRef = useRef<AgGridReact>(null);
|
||||
useLayoutEffect(() => {
|
||||
const showColumnsOnDesktop = () => {
|
||||
gridRef.current?.columnApi.setColumnsVisible(
|
||||
gridRef.current?.api.setColumnsVisible(
|
||||
['id', 'state', 'asset'],
|
||||
window.innerWidth > BREAKPOINT_MD
|
||||
);
|
||||
|
||||
@@ -44,11 +44,11 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
||||
const gridRef = useRef<AgGridReact>(null);
|
||||
useLayoutEffect(() => {
|
||||
const showColumnsOnDesktop = () => {
|
||||
gridRef.current?.columnApi.setColumnsVisible(
|
||||
gridRef.current?.api.setColumnsVisible(
|
||||
['voting', 'cDate', 'eDate', 'type'],
|
||||
window.innerWidth > BREAKPOINT_MD
|
||||
);
|
||||
gridRef.current?.columnApi.setColumnWidth(
|
||||
gridRef.current?.api.setColumnWidth(
|
||||
'actions',
|
||||
window.innerWidth > BREAKPOINT_MD ? 221 : 80
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ const Block = () => {
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{blockData && (
|
||||
{blockData && 'result' in blockData && (
|
||||
<>
|
||||
<TableWithTbody className="mb-8">
|
||||
<TableRow modifier="bordered">
|
||||
|
||||
@@ -32,7 +32,7 @@ CYPRESS_FAIRGROUND=false
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_METAMASK_SNAPS=false
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_PRODUCT_PERPETUALS=true
|
||||
NX_UPDATE_MARKET_STATE=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
@@ -23,7 +23,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_METAMASK_SNAPS=false
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_PRODUCT_PERPETUALS=true
|
||||
NX_UPDATE_MARKET_STATE=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
@@ -22,7 +22,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_METAMASK_SNAPS=false
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_PRODUCT_PERPETUALS=true
|
||||
NX_UPDATE_MARKET_STATE=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
@@ -21,7 +21,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_METAMASK_SNAPS=false
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_PRODUCT_PERPETUALS=true
|
||||
NX_UPDATE_MARKET_STATE=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
+2
-2
@@ -281,8 +281,8 @@ describe('VoteBreakdown', () => {
|
||||
});
|
||||
|
||||
it('Progress bar displays status - LP majority', () => {
|
||||
const yesVotesLP = 800;
|
||||
const noVotesLP = 200;
|
||||
const yesVotesLP = 0.8;
|
||||
const noVotesLP = 0.2;
|
||||
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
|
||||
|
||||
renderComponent(
|
||||
|
||||
@@ -105,8 +105,6 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
yesLPPercentage,
|
||||
yesTokens,
|
||||
noTokens,
|
||||
yesEquityLikeShareWeight,
|
||||
noEquityLikeShareWeight,
|
||||
totalEquityLikeShareWeight,
|
||||
requiredMajorityPercentage,
|
||||
requiredMajorityLPPercentage,
|
||||
@@ -135,6 +133,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
.multipliedBy(100),
|
||||
new BigNumber(100)
|
||||
);
|
||||
|
||||
const willPass = willPassByTokenVote || willPassByLPVote;
|
||||
const updateMarketVotePassMethod = willPassByTokenVote
|
||||
? t('byTokenVote')
|
||||
@@ -202,50 +201,24 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('liquidityProviderVotesFor')}:</span>
|
||||
<Tooltip
|
||||
description={formatNumber(
|
||||
yesEquityLikeShareWeight,
|
||||
defaultDP
|
||||
)}
|
||||
description={
|
||||
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
|
||||
}
|
||||
>
|
||||
<button>
|
||||
<CompactVotes number={yesEquityLikeShareWeight} />
|
||||
</button>
|
||||
<button>{yesLPPercentage.toFixed(1)}%</button>
|
||||
</Tooltip>
|
||||
<span>
|
||||
(
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
|
||||
}
|
||||
>
|
||||
<button>{yesLPPercentage.toFixed(0)}%</button>
|
||||
</Tooltip>
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('liquidityProviderVotesAgainst')}:</span>
|
||||
<Tooltip
|
||||
description={formatNumber(
|
||||
noEquityLikeShareWeight,
|
||||
defaultDP
|
||||
)}
|
||||
>
|
||||
<button>
|
||||
<CompactVotes number={noEquityLikeShareWeight} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span>
|
||||
(
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
|
||||
}
|
||||
>
|
||||
<button>{noLPPercentage.toFixed(0)}%</button>
|
||||
<button>{noLPPercentage.toFixed(1)}%</button>
|
||||
</Tooltip>
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,13 +255,8 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
defaultDP
|
||||
)}
|
||||
>
|
||||
<button>
|
||||
<CompactVotes number={totalEquityLikeShareWeight} />
|
||||
</button>
|
||||
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
|
||||
</Tooltip>
|
||||
<span>
|
||||
({totalEquityLikeShareWeight.toFixed(defaultDP)}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -54,8 +54,8 @@ describe('use-vote-information', () => {
|
||||
it('returns all required vote information', () => {
|
||||
const yesVotes = 40;
|
||||
const noVotes = 60;
|
||||
const yesEquityLikeShareWeight = '30';
|
||||
const noEquityLikeShareWeight = '70';
|
||||
const yesEquityLikeShareWeight = '0.30';
|
||||
const noEquityLikeShareWeight = '0.70';
|
||||
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
|
||||
const fixedTokenValue = 1000000000000000000;
|
||||
|
||||
@@ -195,10 +195,10 @@ describe('use-vote-information', () => {
|
||||
});
|
||||
|
||||
it('correctly shows whether an update market proposal will pass by token or LP vote - both failing', () => {
|
||||
const yesVotes = 20;
|
||||
const noVotes = 70;
|
||||
const yesEquityLikeShareWeight = '30';
|
||||
const noEquityLikeShareWeight = '60';
|
||||
const yesVotes = 0.2;
|
||||
const noVotes = 0.7;
|
||||
const yesEquityLikeShareWeight = '0.30';
|
||||
const noEquityLikeShareWeight = '0.60';
|
||||
const fixedTokenValue = 1000000000000000000;
|
||||
|
||||
const proposal = generateProposal({
|
||||
|
||||
@@ -61,7 +61,7 @@ export const useVoteInformation = ({
|
||||
const noEquityLikeShareWeight = !proposal?.votes.no
|
||||
.totalEquityLikeShareWeight
|
||||
? new BigNumber(0)
|
||||
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight);
|
||||
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight).times(100);
|
||||
|
||||
const yesTokens = new BigNumber(
|
||||
addDecimal(proposal?.votes.yes.totalTokens ?? 0, decimals)
|
||||
@@ -70,7 +70,7 @@ export const useVoteInformation = ({
|
||||
const yesEquityLikeShareWeight = !proposal?.votes.yes
|
||||
.totalEquityLikeShareWeight
|
||||
? new BigNumber(0)
|
||||
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight);
|
||||
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight).times(100);
|
||||
|
||||
const totalTokensVoted = yesTokens.plus(noTokens);
|
||||
|
||||
@@ -81,12 +81,7 @@ export const useVoteInformation = ({
|
||||
const yesPercentage = totalTokensVoted.isZero()
|
||||
? new BigNumber(0)
|
||||
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
||||
|
||||
const yesLPPercentage = totalEquityLikeShareWeight.isZero()
|
||||
? new BigNumber(0)
|
||||
: yesEquityLikeShareWeight
|
||||
.multipliedBy(100)
|
||||
.dividedBy(totalEquityLikeShareWeight);
|
||||
const yesLPPercentage = yesEquityLikeShareWeight;
|
||||
|
||||
const noPercentage = totalTokensVoted.isZero()
|
||||
? new BigNumber(0)
|
||||
@@ -103,9 +98,7 @@ export const useVoteInformation = ({
|
||||
);
|
||||
|
||||
const participationLPMet = requiredParticipationLP
|
||||
? totalEquityLikeShareWeight.isGreaterThan(
|
||||
totalSupply.multipliedBy(requiredParticipationLP)
|
||||
)
|
||||
? totalEquityLikeShareWeight.isGreaterThan(requiredParticipationLP)
|
||||
: false;
|
||||
|
||||
const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
|
||||
@@ -120,9 +113,7 @@ export const useVoteInformation = ({
|
||||
.multipliedBy(100)
|
||||
.dividedBy(totalSupply);
|
||||
|
||||
const totalLPTokensPercentage = totalEquityLikeShareWeight
|
||||
.multipliedBy(100)
|
||||
.dividedBy(totalSupply);
|
||||
const totalLPTokensPercentage = totalEquityLikeShareWeight;
|
||||
|
||||
const willPassByTokenVote =
|
||||
participationMet &&
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ export const ProtocolUpgradeProposalContainer = () => {
|
||||
time={
|
||||
pending && time ? (
|
||||
convertToCountdownString(time, '0:00:00:00')
|
||||
) : blockInfo?.result ? (
|
||||
) : blockInfo && 'result' in blockInfo && blockInfo?.result ? (
|
||||
<span title={blockInfo.result.block.header.time}>
|
||||
{formatDateWithLocalTimezone(
|
||||
new Date(blockInfo.result.block.header.time)
|
||||
|
||||
@@ -116,7 +116,8 @@ export const generateYesVotes = (
|
||||
fixedTokenValue?: number,
|
||||
totalEquityLikeShareWeight?: string
|
||||
): Votes => {
|
||||
const votes = Array.from(Array(numberOfVotes)).map(() => {
|
||||
const votes = [];
|
||||
for (let i = 0; i < numberOfVotes; i++) {
|
||||
const vote: Vote = {
|
||||
__typename: 'Vote',
|
||||
value: Schema.VoteValue.VALUE_YES,
|
||||
@@ -152,8 +153,9 @@ export const generateYesVotes = (
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
|
||||
return vote;
|
||||
});
|
||||
votes.push(vote);
|
||||
}
|
||||
|
||||
return {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: votes.length.toString(),
|
||||
@@ -172,7 +174,8 @@ export const generateNoVotes = (
|
||||
fixedTokenValue?: number,
|
||||
totalEquityLikeShareWeight?: string
|
||||
): Votes => {
|
||||
const votes = Array.from(Array(numberOfVotes)).map(() => {
|
||||
const votes = [];
|
||||
for (let i = 0; i < numberOfVotes; i++) {
|
||||
const vote: Vote = {
|
||||
__typename: 'Vote',
|
||||
value: Schema.VoteValue.VALUE_NO,
|
||||
@@ -207,8 +210,9 @@ export const generateNoVotes = (
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
return vote;
|
||||
});
|
||||
votes.push(vote);
|
||||
}
|
||||
|
||||
return {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: votes.length.toString(),
|
||||
|
||||
@@ -25,3 +25,4 @@ NX_ICEBERG_ORDERS=true
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=true
|
||||
NX_TEAM_COMPETITION=true
|
||||
|
||||
@@ -188,12 +188,11 @@ const useNow = () => {
|
||||
return now;
|
||||
};
|
||||
|
||||
const useEvery = (marketId: string) => {
|
||||
const { data: marketTradingMode } = useMarketTradingMode(marketId);
|
||||
const useEvery = (marketId: string, skip: boolean) => {
|
||||
const { data: marketInfo } = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
variables: { marketId },
|
||||
skip: !marketTradingMode || isMarketInAuction(marketTradingMode),
|
||||
skip,
|
||||
});
|
||||
let every: number | undefined = undefined;
|
||||
const sourceType =
|
||||
@@ -211,8 +210,10 @@ const useEvery = (marketId: string) => {
|
||||
return every;
|
||||
};
|
||||
|
||||
const useStartTime = (marketId: string) => {
|
||||
const useStartTime = (marketId: string, skip: boolean) => {
|
||||
const { data: fundingPeriods } = useFundingPeriodsQuery({
|
||||
pollInterval: 5000,
|
||||
skip,
|
||||
variables: {
|
||||
marketId: marketId,
|
||||
pagination: { first: 1 },
|
||||
@@ -246,8 +247,10 @@ const useFormatCountdown = (
|
||||
|
||||
export const FundingCountdown = ({ marketId }: { marketId: string }) => {
|
||||
const now = useNow();
|
||||
const startTime = useStartTime(marketId);
|
||||
const every = useEvery(marketId);
|
||||
const { data: marketTradingMode } = useMarketTradingMode(marketId);
|
||||
const skip = !marketTradingMode || isMarketInAuction(marketTradingMode);
|
||||
const startTime = useStartTime(marketId, skip);
|
||||
const every = useEvery(marketId, skip);
|
||||
|
||||
return (
|
||||
<div data-testid="funding-countdown">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
||||
@@ -72,17 +72,6 @@ export const MarketPage = () => {
|
||||
}
|
||||
}, [update, lastMarketId, data?.id]);
|
||||
|
||||
const pinnedAsset = data && getAsset(data);
|
||||
|
||||
const tradeView = useMemo(() => {
|
||||
if (pinnedAsset) {
|
||||
if (largeScreen) {
|
||||
return <TradeGrid market={data} pinnedAsset={pinnedAsset} />;
|
||||
}
|
||||
return <TradePanels market={data} pinnedAsset={pinnedAsset} />;
|
||||
}
|
||||
}, [largeScreen, data, pinnedAsset]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Splash>
|
||||
@@ -118,6 +107,8 @@ export const MarketPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const pinnedAsset = data && getAsset(data);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleUpdater
|
||||
@@ -125,7 +116,11 @@ export const MarketPage = () => {
|
||||
marketName={data?.tradableInstrument.instrument.name}
|
||||
decimalPlaces={data?.decimalPlaces}
|
||||
/>
|
||||
{tradeView}
|
||||
{largeScreen ? (
|
||||
<TradeGrid market={data} pinnedAsset={pinnedAsset} />
|
||||
) : (
|
||||
<TradePanels market={data} pinnedAsset={pinnedAsset} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { memo } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo, type ReactNode } from 'react';
|
||||
import { LayoutPriority } from 'allotment';
|
||||
import classNames from 'classnames';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { OracleBanner, useMarket } from '@vegaprotocol/markets';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { type Market } from '@vegaprotocol/markets';
|
||||
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
ResizableGrid,
|
||||
@@ -13,31 +12,19 @@ import {
|
||||
usePaneLayout,
|
||||
} from '../../components/resizable-grid';
|
||||
import { TradingViews } from './trade-views';
|
||||
import {
|
||||
MarketSuccessorBanner,
|
||||
MarketSuccessorProposalBanner,
|
||||
MarketTerminationBanner,
|
||||
} from '../../components/market-banner';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { MarketBanner } from '../../components/market-banner';
|
||||
|
||||
interface TradeGridProps {
|
||||
market: Market | null;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
market: Market;
|
||||
pinnedAsset?: PinnedAsset | undefined;
|
||||
}
|
||||
|
||||
const MainGrid = memo(
|
||||
({
|
||||
marketId,
|
||||
pinnedAsset,
|
||||
}: {
|
||||
marketId: string;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}) => {
|
||||
({ market, pinnedAsset }: { market: Market; pinnedAsset?: PinnedAsset }) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const t = useT();
|
||||
const { data: market } = useMarket(marketId);
|
||||
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'top' });
|
||||
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
|
||||
id: 'middle-1',
|
||||
@@ -65,17 +52,17 @@ const MainGrid = memo(
|
||||
menu={<TradingViews.chart.menu />}
|
||||
>
|
||||
<ErrorBoundary feature="chart">
|
||||
<TradingViews.chart.component marketId={marketId} />
|
||||
<TradingViews.chart.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="depth" name={t('Depth')}>
|
||||
<ErrorBoundary feature="depth">
|
||||
<TradingViews.depth.component marketId={marketId} />
|
||||
<TradingViews.depth.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="liquidity" name={t('Liquidity')}>
|
||||
<ErrorBoundary feature="liquidity">
|
||||
<TradingViews.liquidity.component marketId={marketId} />
|
||||
<TradingViews.liquidity.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
{market &&
|
||||
@@ -83,17 +70,21 @@ const MainGrid = memo(
|
||||
'Perpetual' ? (
|
||||
<Tab id="funding-history" name={t('Funding history')}>
|
||||
<ErrorBoundary feature="funding-history">
|
||||
<TradingViews.funding.component marketId={marketId} />
|
||||
<TradingViews.funding.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
) : null}
|
||||
{market &&
|
||||
market.tradableInstrument.instrument.product.__typename ===
|
||||
'Perpetual' ? (
|
||||
<Tab id="funding-payments" name={t('Funding payments')}>
|
||||
<Tab
|
||||
id="funding-payments"
|
||||
name={t('Funding payments')}
|
||||
settings={<TradingViews.fundingPayments.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="funding-payments">
|
||||
<TradingViews.fundingPayments.component
|
||||
marketId={marketId}
|
||||
marketId={market.id}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
@@ -109,12 +100,16 @@ const MainGrid = memo(
|
||||
<Tabs storageKey="console-trade-grid-main-right">
|
||||
<Tab id="orderbook" name={t('Orderbook')}>
|
||||
<ErrorBoundary feature="orderbook">
|
||||
<TradingViews.orderbook.component marketId={marketId} />
|
||||
<TradingViews.orderbook.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="trades" name={t('Trades')}>
|
||||
<Tab
|
||||
id="trades"
|
||||
name={t('Trades')}
|
||||
settings={<TradingViews.trades.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="trades">
|
||||
<TradingViews.trades.component marketId={marketId} />
|
||||
<TradingViews.trades.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -133,6 +128,7 @@ const MainGrid = memo(
|
||||
id="positions"
|
||||
name={t('Positions')}
|
||||
menu={<TradingViews.positions.menu />}
|
||||
settings={<TradingViews.positions.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="positions">
|
||||
<TradingViews.positions.component />
|
||||
@@ -142,17 +138,26 @@ const MainGrid = memo(
|
||||
id="open-orders"
|
||||
name={t('Open')}
|
||||
menu={<TradingViews.activeOrders.menu />}
|
||||
settings={<TradingViews.activeOrders.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="activeOrders">
|
||||
<TradingViews.activeOrders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="closed-orders" name={t('Closed')}>
|
||||
<Tab
|
||||
id="closed-orders"
|
||||
name={t('Closed')}
|
||||
settings={<TradingViews.closedOrders.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="closedOrders">
|
||||
<TradingViews.closedOrders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="rejected-orders" name={t('Rejected')}>
|
||||
<Tab
|
||||
id="rejected-orders"
|
||||
name={t('Rejected')}
|
||||
settings={<TradingViews.rejectedOrders.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="rejectedOrders">
|
||||
<TradingViews.rejectedOrders.component />
|
||||
</ErrorBoundary>
|
||||
@@ -161,25 +166,35 @@ const MainGrid = memo(
|
||||
id="orders"
|
||||
name={t('All')}
|
||||
menu={<TradingViews.orders.menu />}
|
||||
settings={<TradingViews.orders.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="orders">
|
||||
<TradingViews.orders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
{featureFlags.STOP_ORDERS ? (
|
||||
<Tab id="stop-orders" name={t('Stop orders')}>
|
||||
<Tab
|
||||
id="stop-orders"
|
||||
name={t('Stop orders')}
|
||||
settings={<TradingViews.stopOrders.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="stop-orders">
|
||||
<TradingViews.stopOrders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
) : null}
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
<Tab
|
||||
id="fills"
|
||||
name={t('Fills')}
|
||||
settings={<TradingViews.fills.settings />}
|
||||
>
|
||||
<TradingViews.fills.component />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="accounts"
|
||||
name={t('Collateral')}
|
||||
menu={<TradingViews.collateral.menu />}
|
||||
settings={<TradingViews.collateral.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="collateral">
|
||||
<TradingViews.collateral.component
|
||||
@@ -197,7 +212,6 @@ const MainGrid = memo(
|
||||
MainGrid.displayName = 'MainGrid';
|
||||
|
||||
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const wrapperClasses = classNames(
|
||||
'h-full grid',
|
||||
'grid-rows-[min-content_1fr]'
|
||||
@@ -206,17 +220,10 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<div>
|
||||
{featureFlags.SUCCESSOR_MARKETS && (
|
||||
<>
|
||||
<MarketSuccessorBanner market={market} />
|
||||
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||
</>
|
||||
)}
|
||||
<MarketTerminationBanner market={market} />
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
<MarketBanner market={market} />
|
||||
</div>
|
||||
<div className="min-h-0 p-0.5">
|
||||
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
|
||||
<MainGrid market={market} pinnedAsset={pinnedAsset} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { type Market } from '@vegaprotocol/markets';
|
||||
import { OracleBanner } from '@vegaprotocol/markets';
|
||||
// TODO: handle oracle banner
|
||||
// import { OracleBanner } from '@vegaprotocol/markets';
|
||||
import { useState } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import classNames from 'classnames';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import {
|
||||
MarketSuccessorBanner,
|
||||
MarketSuccessorProposalBanner,
|
||||
MarketTerminationBanner,
|
||||
} from '../../components/market-banner';
|
||||
Popover,
|
||||
Splash,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { MarketBanner } from '../../components/market-banner';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { type TradingView } from './trade-views';
|
||||
import { TradingViews } from './trade-views';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
|
||||
interface TradePanelsProps {
|
||||
market: Market | null;
|
||||
market: Market;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const [view, setView] = useState<TradingView>('chart');
|
||||
const viewCfg = TradingViews[view];
|
||||
|
||||
const renderView = () => {
|
||||
const Component = TradingViews[view].component;
|
||||
@@ -44,35 +45,36 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
};
|
||||
|
||||
const renderMenu = () => {
|
||||
const viewCfg = TradingViews[view];
|
||||
|
||||
if ('menu' in viewCfg) {
|
||||
const Menu = viewCfg.menu;
|
||||
|
||||
if ('menu' in viewCfg || 'settings' in viewCfg) {
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
|
||||
<Menu />
|
||||
{'menu' in viewCfg ? <viewCfg.menu /> : null}
|
||||
{'settings' in viewCfg ? (
|
||||
<Popover
|
||||
align="end"
|
||||
trigger={
|
||||
<span className="ml-1 flex items-center justify-center h-6 w-6">
|
||||
<VegaIcon name={VegaIconNames.COG} size={16} />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="p-4 flex justify-end">
|
||||
<viewCfg.settings />
|
||||
</div>
|
||||
</Popover>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
|
||||
<div>
|
||||
{featureFlags.SUCCESSOR_MARKETS && (
|
||||
<>
|
||||
<MarketSuccessorBanner market={market} />
|
||||
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||
</>
|
||||
)}
|
||||
<MarketTerminationBanner market={market} />
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
<MarketBanner market={market} />
|
||||
</div>
|
||||
<div>{renderMenu()}</div>
|
||||
<div className="h-full">
|
||||
<div className="h-full relative">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<div style={{ width, height }} className="overflow-auto">
|
||||
@@ -110,7 +112,9 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
key={key}
|
||||
view={key}
|
||||
isActive={isActive}
|
||||
onClick={() => setView(key)}
|
||||
onClick={() => {
|
||||
setView(key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import { Filter, OpenOrdersMenu } from '@vegaprotocol/orders';
|
||||
import { TradesContainer } from '../../components/trades-container';
|
||||
import {
|
||||
TradesContainer,
|
||||
TradesSettings,
|
||||
} from '../../components/trades-container';
|
||||
import { OrderbookContainer } from '../../components/orderbook-container';
|
||||
import { FillsContainer } from '../../components/fills-container';
|
||||
import { PositionsContainer } from '../../components/positions-container';
|
||||
import { AccountsContainer } from '../../components/accounts-container';
|
||||
import {
|
||||
FillsContainer,
|
||||
FillsSettings,
|
||||
} from '../../components/fills-container';
|
||||
import {
|
||||
PositionsContainer,
|
||||
PositionsSettings,
|
||||
} from '../../components/positions-container';
|
||||
import {
|
||||
AccountsContainer,
|
||||
AccountsSettings,
|
||||
} from '../../components/accounts-container';
|
||||
import { LiquidityContainer } from '../../components/liquidity-container';
|
||||
import { FundingContainer } from '../../components/funding-container';
|
||||
import { FundingPaymentsContainer } from '../../components/funding-payments-container';
|
||||
import { OrdersContainer } from '../../components/orders-container';
|
||||
import { StopOrdersContainer } from '../../components/stop-orders-container';
|
||||
import {
|
||||
FundingPaymentsContainer,
|
||||
FundingPaymentsSettings,
|
||||
} from '../../components/funding-payments-container';
|
||||
import {
|
||||
OrdersContainer,
|
||||
OrdersSettings,
|
||||
} from '../../components/orders-container';
|
||||
import {
|
||||
StopOrdersContainer,
|
||||
StopOrdersSettings,
|
||||
} from '../../components/stop-orders-container';
|
||||
import { AccountsMenu } from '../../components/accounts-menu';
|
||||
import { PositionsMenu } from '../../components/positions-menu';
|
||||
import { ChartContainer, ChartMenu } from '../../components/chart-container';
|
||||
@@ -32,37 +53,46 @@ export const TradingViews = {
|
||||
},
|
||||
fundingPayments: {
|
||||
component: FundingPaymentsContainer,
|
||||
settings: FundingPaymentsSettings,
|
||||
},
|
||||
orderbook: {
|
||||
component: OrderbookContainer,
|
||||
},
|
||||
trades: {
|
||||
component: TradesContainer,
|
||||
settings: TradesSettings,
|
||||
},
|
||||
positions: {
|
||||
component: PositionsContainer,
|
||||
menu: PositionsMenu,
|
||||
settings: PositionsSettings,
|
||||
},
|
||||
activeOrders: {
|
||||
component: () => <OrdersContainer filter={Filter.Open} />,
|
||||
menu: OpenOrdersMenu,
|
||||
settings: () => <OrdersSettings filter={Filter.Open} />,
|
||||
},
|
||||
closedOrders: {
|
||||
component: () => <OrdersContainer filter={Filter.Closed} />,
|
||||
settings: () => <OrdersSettings filter={Filter.Closed} />,
|
||||
},
|
||||
rejectedOrders: {
|
||||
component: () => <OrdersContainer filter={Filter.Rejected} />,
|
||||
settings: () => <OrdersSettings filter={Filter.Rejected} />,
|
||||
},
|
||||
orders: {
|
||||
component: OrdersContainer,
|
||||
menu: OpenOrdersMenu,
|
||||
settings: OrdersSettings,
|
||||
},
|
||||
stopOrders: {
|
||||
component: StopOrdersContainer,
|
||||
settings: StopOrdersSettings,
|
||||
},
|
||||
collateral: {
|
||||
component: AccountsContainer,
|
||||
menu: AccountsMenu,
|
||||
settings: AccountsSettings,
|
||||
},
|
||||
fills: { component: FillsContainer },
|
||||
fills: { component: FillsContainer, settings: FillsSettings },
|
||||
} as const;
|
||||
|
||||
@@ -42,7 +42,7 @@ export const createDataGridSlice: StateCreator<DataGridSlice> = (set) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const useMarketsStore = create<DataGridSlice>()(
|
||||
export const useMarketsStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_market_list_store',
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { MarketsSettings } from './markets-settings';
|
||||
|
||||
export const MarketsPage = () => {
|
||||
const t = useT();
|
||||
@@ -34,7 +35,11 @@ export const MarketsPage = () => {
|
||||
<div className="h-full pt-0.5 pb-3 px-1.5">
|
||||
<div className="h-full my-1 border rounded-sm border-default">
|
||||
<Tabs storageKey="console-markets">
|
||||
<Tab id="open-markets" name={t('Open markets')}>
|
||||
<Tab
|
||||
id="open-markets"
|
||||
name={t('Open markets')}
|
||||
settings={<MarketsSettings />}
|
||||
>
|
||||
<ErrorBoundary feature="markets-open">
|
||||
<OpenMarkets />
|
||||
</ErrorBoundary>
|
||||
@@ -42,6 +47,7 @@ export const MarketsPage = () => {
|
||||
<Tab
|
||||
id="proposed-markets"
|
||||
name={t('Proposed markets')}
|
||||
settings={<MarketsSettings />}
|
||||
menu={
|
||||
<TradingAnchorButton
|
||||
size="extra-small"
|
||||
@@ -56,7 +62,11 @@ export const MarketsPage = () => {
|
||||
<Proposed />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="closed-markets" name={t('Closed markets')}>
|
||||
<Tab
|
||||
id="closed-markets"
|
||||
name={t('Closed markets')}
|
||||
settings={<MarketsSettings />}
|
||||
>
|
||||
<ErrorBoundary feature="markets-closed">
|
||||
<Closed />
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../../components/grid-settings/grid-settings';
|
||||
import { useMarketsStore } from './market-list-table';
|
||||
|
||||
export const MarketsSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={useMarketsStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -5,14 +5,29 @@ import { titlefy } from '@vegaprotocol/utils';
|
||||
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
|
||||
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { AccountsContainer } from '../../components/accounts-container';
|
||||
import {
|
||||
AccountsContainer,
|
||||
AccountsSettings,
|
||||
} from '../../components/accounts-container';
|
||||
import { DepositsContainer } from '../../components/deposits-container';
|
||||
import { FillsContainer } from '../../components/fills-container';
|
||||
import { FundingPaymentsContainer } from '../../components/funding-payments-container';
|
||||
import { PositionsContainer } from '../../components/positions-container';
|
||||
import {
|
||||
FillsContainer,
|
||||
FillsSettings,
|
||||
} from '../../components/fills-container';
|
||||
import {
|
||||
FundingPaymentsContainer,
|
||||
FundingPaymentsSettings,
|
||||
} from '../../components/funding-payments-container';
|
||||
import {
|
||||
PositionsContainer,
|
||||
PositionsSettings,
|
||||
} from '../../components/positions-container';
|
||||
import { PositionsMenu } from '../../components/positions-menu';
|
||||
import { WithdrawalsContainer } from '../../components/withdrawals-container';
|
||||
import { OrdersContainer } from '../../components/orders-container';
|
||||
import {
|
||||
OrdersContainer,
|
||||
OrdersSettings,
|
||||
} from '../../components/orders-container';
|
||||
import { LedgerContainer } from '../../components/ledger-container';
|
||||
import {
|
||||
ResizableGrid,
|
||||
@@ -76,22 +91,27 @@ export const Portfolio = () => {
|
||||
id="positions"
|
||||
name={t('Positions')}
|
||||
menu={<PositionsMenu />}
|
||||
settings={<PositionsSettings />}
|
||||
>
|
||||
<ErrorBoundary feature="portfolio-positions">
|
||||
<PositionsContainer allKeys />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="orders" name={t('Orders')}>
|
||||
<Tab id="orders" name={t('Orders')} settings={<OrdersSettings />}>
|
||||
<ErrorBoundary feature="portfolio-orders">
|
||||
<OrdersContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
<Tab id="fills" name={t('Fills')} settings={<FillsSettings />}>
|
||||
<ErrorBoundary feature="portfolio-fills">
|
||||
<FillsContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="funding-payments" name={t('Funding payments')}>
|
||||
<Tab
|
||||
id="funding-payments"
|
||||
name={t('Funding payments')}
|
||||
settings={<FundingPaymentsSettings />}
|
||||
>
|
||||
<ErrorBoundary feature="portfolio-funding-payments">
|
||||
<FundingPaymentsContainer />
|
||||
</ErrorBoundary>
|
||||
@@ -114,6 +134,7 @@ export const Portfolio = () => {
|
||||
<Tab
|
||||
id="collateral"
|
||||
name={t('Collateral')}
|
||||
settings={<AccountsSettings />}
|
||||
menu={<AccountsMenu />}
|
||||
>
|
||||
<ErrorBoundary feature="portfolio-accounts">
|
||||
|
||||
@@ -296,7 +296,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
<>
|
||||
<div
|
||||
data-testid="referral-apply-code-form"
|
||||
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8"
|
||||
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto md:w-2/3 max-w-md rounded-lg p-8"
|
||||
>
|
||||
<h3 className="calt mb-4 text-center text-2xl">
|
||||
{t('Apply a referral code')}
|
||||
|
||||
@@ -3,7 +3,7 @@ export const GRADIENT =
|
||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||
|
||||
export const SKY_BACKGROUND =
|
||||
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[40%_0px] bg-[length:1440px] bg-no-repeat bg-local';
|
||||
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
|
||||
|
||||
// TODO: Update the links to use the correct referral related pages
|
||||
export const REFERRAL_DOCS_LINK =
|
||||
|
||||
@@ -49,7 +49,7 @@ export const CreateCodeForm = () => {
|
||||
return (
|
||||
<div
|
||||
data-testid="referral-create-code-form"
|
||||
className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
|
||||
className="md:w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
|
||||
>
|
||||
<h3 className="mb-4 text-2xl text-center calt">
|
||||
{t('Create a referral code')}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useCallback } from 'react';
|
||||
import { useRefereesQuery } from './__generated__/Referees';
|
||||
import compact from 'lodash/compact';
|
||||
import pick from 'lodash/pick';
|
||||
import type {
|
||||
ReferralSetsQuery,
|
||||
ReferralSetsQueryVariables,
|
||||
@@ -122,6 +123,66 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
};
|
||||
};
|
||||
|
||||
type Referee = NonNullable<
|
||||
NonNullable<ReturnType<typeof useReferral>['data']>['referee']
|
||||
>;
|
||||
|
||||
type RefereeProperties = (keyof Referee)[];
|
||||
|
||||
const findReferee = (referee: Referee, referees: Referee[]) =>
|
||||
referees.find((r) => r.refereeId === referee?.refereeId) || referee;
|
||||
|
||||
const updateReferee = (
|
||||
referee: Referee,
|
||||
referees: Referee[],
|
||||
properties: RefereeProperties
|
||||
) => ({
|
||||
...referee,
|
||||
...pick(findReferee(referee, referees), properties),
|
||||
});
|
||||
|
||||
export const useUpdateReferees = (
|
||||
referral: ReturnType<typeof useReferral>,
|
||||
aggregationEpochs: number,
|
||||
properties: RefereeProperties,
|
||||
skip?: boolean
|
||||
): ReturnType<typeof useReferral> => {
|
||||
const { data, loading, error, refetch } = useRefereesQuery({
|
||||
variables: {
|
||||
code: referral?.data?.code as string,
|
||||
aggregationEpochs,
|
||||
},
|
||||
skip: skip || !referral?.data?.code,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
context: { isEnlargedTimeout: true },
|
||||
});
|
||||
const refetchAll = useCallback(() => {
|
||||
refetch();
|
||||
referral.refetch();
|
||||
}, [refetch, referral]);
|
||||
if (!referral.data || skip) {
|
||||
return referral;
|
||||
}
|
||||
const referees = compact(
|
||||
removePaginationWrapper(data?.referralSetReferees.edges)
|
||||
);
|
||||
|
||||
return {
|
||||
data: data && {
|
||||
...referral.data,
|
||||
referees: referral.data.referees.map((referee) =>
|
||||
updateReferee(referee, referees, properties)
|
||||
),
|
||||
referee:
|
||||
referral.data.referee &&
|
||||
updateReferee(referral.data.referee, referees, properties),
|
||||
},
|
||||
loading: loading || referral.loading,
|
||||
error: error || referral.error,
|
||||
refetch: refetchAll,
|
||||
};
|
||||
};
|
||||
|
||||
const retrieveReferralSetData = (data: ReferralSetsQuery | undefined) =>
|
||||
data?.referralSets.edges && data.referralSets.edges.length > 0
|
||||
? data.referralSets.edges[0]?.node
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Table } from './table';
|
||||
import { Table } from '../../components/table';
|
||||
|
||||
export const HowItWorksTable = () => {
|
||||
const t = useT();
|
||||
|
||||
@@ -5,16 +5,16 @@ import { useT } from '../../lib/use-t';
|
||||
export const LandingBanner = () => {
|
||||
const t = useT();
|
||||
return (
|
||||
<div className={classNames('relative mb-20')}>
|
||||
<div className={classNames('relative mb-10 lg:mb-20')}>
|
||||
<div className="">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute top-20 right-[120px] md:right-[240px] max-sm:hidden"
|
||||
className="absolute top-20 right-[220px] md:right-[240px] max-sm:hidden"
|
||||
>
|
||||
<AnimatedDudeWithWire />
|
||||
</div>
|
||||
<div className="pt-20 sm:w-[50%]">
|
||||
<h1 className="text-6xl font-alpha calt mb-10">
|
||||
<div className="pt-10 lg:pt-20 sm:w-[50%]">
|
||||
<h1 className="text-3xl _text-[6vw] lg:!text-6xl leading-[1em] font-alpha calt mb-10">
|
||||
{t('Vega community referrals')}
|
||||
</h1>
|
||||
<p className="text-lg mb-1">
|
||||
@@ -22,7 +22,7 @@ export const LandingBanner = () => {
|
||||
'Referral programs can be proposed and created via community governance.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-lg mb-10">
|
||||
<p className="text-lg mb-1">
|
||||
{t(
|
||||
'Once live, users can generate referral codes to share with their friends and earn commission on their trades, while referred traders can access fee discounts based on the running volume of the group.'
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Layout = ({
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[1440px]',
|
||||
'mx-auto px-16 md:px-32 pb-32',
|
||||
'mx-auto px-4 lg:px-32 pb-32',
|
||||
'relative z-0',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -151,6 +151,27 @@ const MOCK_REFEREES: RefereesQuery = {
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_REFEREES_30: RefereesQuery = {
|
||||
referralSetReferees: {
|
||||
__typename: 'ReferralSetRefereeConnection',
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
joinedAt: '2023-11-21T14:17:09.257235Z',
|
||||
refereeId:
|
||||
'0987654321098765432109876543210987654321098765432109876543219876',
|
||||
referralSetId:
|
||||
'3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
|
||||
totalRefereeGeneratedRewards: '12340',
|
||||
totalRefereeNotionalTakerVolume: '56780',
|
||||
__typename: 'ReferralSetReferee',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const programMock: MockedResponse<ReferralProgramQuery> = {
|
||||
request: {
|
||||
query: ReferralProgramDocument,
|
||||
@@ -262,6 +283,19 @@ const refereesMock: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
|
||||
},
|
||||
};
|
||||
|
||||
const refereesMock30: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
|
||||
request: {
|
||||
query: RefereesDocument,
|
||||
variables: {
|
||||
code: MOCK_REFERRER_SET.referralSets.edges[0]?.node.id as string,
|
||||
aggregationEpochs: 30,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: MOCK_REFEREES_30,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@vegaprotocol/wallet', () => {
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/wallet'),
|
||||
@@ -297,6 +331,7 @@ describe('ReferralStatistics', () => {
|
||||
noReferralSetAsRefereeMock,
|
||||
stakeAvailableMock,
|
||||
refereesMock,
|
||||
refereesMock30,
|
||||
]}
|
||||
showWarnings={false}
|
||||
>
|
||||
@@ -313,6 +348,10 @@ describe('ReferralStatistics', () => {
|
||||
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
||||
'referrer'
|
||||
);
|
||||
// gets commision from 30 epochs query
|
||||
expect(queryByTestId('total-commission-value')).toHaveTextContent(
|
||||
'12,340'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,12 +4,17 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
TextChildrenTooltip as Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
|
||||
import {
|
||||
DEFAULT_AGGREGATION_DAYS,
|
||||
useReferral,
|
||||
useUpdateReferees,
|
||||
} from './hooks/use-referral';
|
||||
import classNames from 'classnames';
|
||||
import { Table } from './table';
|
||||
import { Table } from '../../components/table';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getDateFormat,
|
||||
@@ -41,11 +46,17 @@ export const ReferralStatistics = () => {
|
||||
role: 'referee',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
const { data: referrer, refetch: referrerRefetch } = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
|
||||
const { data: referrer, refetch: referrerRefetch } = useUpdateReferees(
|
||||
useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
}),
|
||||
DEFAULT_AGGREGATION_DAYS,
|
||||
['totalRefereeGeneratedRewards'],
|
||||
DEFAULT_AGGREGATION_DAYS === program.details?.windowLength
|
||||
);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
refereeRefetch();
|
||||
@@ -76,11 +87,9 @@ export const ReferralStatistics = () => {
|
||||
export const useStats = ({
|
||||
data,
|
||||
program,
|
||||
as,
|
||||
}: {
|
||||
data?: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
as?: 'referrer' | 'referee';
|
||||
}) => {
|
||||
const { benefitTiers } = program;
|
||||
const { data: epochData } = useCurrentEpochInfoQuery({
|
||||
@@ -181,7 +190,7 @@ export const Statistics = ({
|
||||
nextBenefitTierValue,
|
||||
nextBenefitTierVolumeValue,
|
||||
nextBenefitTierEpochsValue,
|
||||
} = useStats({ data, program, as });
|
||||
} = useStats({ data, program });
|
||||
|
||||
const isApplyCodePreview = useMemo(
|
||||
() => data.referee === null,
|
||||
@@ -293,11 +302,27 @@ export const Statistics = ({
|
||||
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
||||
const totalCommissionTile = (
|
||||
<StatTile
|
||||
title={t('totalCommission', 'Total commission (last {{count}} epochs)', {
|
||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
})}
|
||||
description={<QUSDTooltip />}
|
||||
testId="total-commission"
|
||||
title={
|
||||
<Trans
|
||||
i18nKey="totalCommission"
|
||||
defaults="Total commission (<0>last {{count}} epochs</0>)"
|
||||
values={{
|
||||
count: DEFAULT_AGGREGATION_DAYS,
|
||||
}}
|
||||
components={[
|
||||
<Tooltip
|
||||
key="1"
|
||||
description={t(
|
||||
'Depending on data node retention you may not be able see the full 30 days'
|
||||
)}
|
||||
>
|
||||
last 30 epochs
|
||||
</Tooltip>,
|
||||
]}
|
||||
/>
|
||||
}
|
||||
description={<QUSDTooltip />}
|
||||
>
|
||||
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
||||
</StatTile>
|
||||
@@ -503,12 +528,21 @@ export const RefereesTable = ({
|
||||
displayName: (
|
||||
<Trans
|
||||
i18nKey="referralStatisticsCommission"
|
||||
defaults="Commission earned in <0>qUSD</0> (last {{count}} epochs)"
|
||||
defaults="Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)"
|
||||
components={[
|
||||
<QUSDTooltip key="0" />,
|
||||
<Tooltip
|
||||
key="1"
|
||||
description={t(
|
||||
'Depending on data node retention you may not be able see the full 30 days'
|
||||
)}
|
||||
>
|
||||
last 30 epochs
|
||||
</Tooltip>,
|
||||
]}
|
||||
values={{
|
||||
count:
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
count: DEFAULT_AGGREGATION_DAYS,
|
||||
}}
|
||||
components={[<QUSDTooltip key="qusd" />]}
|
||||
ns={ns}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -72,7 +72,7 @@ export const Referrals = () => {
|
||||
{showNav && <Nav />}
|
||||
<div
|
||||
className={classNames({
|
||||
'py-16': showNav,
|
||||
'py-8 lg:py-16': showNav,
|
||||
'h-[300px] relative': loading || error,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -3,11 +3,12 @@ import {
|
||||
getDateTimeFormat,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { Table } from './table';
|
||||
import { Table } from '../../components/table';
|
||||
import classNames from 'classnames';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
import { Tag } from './tag';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { Tag } from '../../components/helpers/tag';
|
||||
import { getTierColor, getTierGradient } from '../../components/helpers/tiers';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ExternalLink, truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
DApp,
|
||||
@@ -19,25 +20,6 @@ import {
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
// rainbow-ish order
|
||||
const TIER_COLORS: Array<ComponentProps<typeof Tag>['color']> = [
|
||||
'pink',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'blue',
|
||||
'purple',
|
||||
];
|
||||
|
||||
const getTierColor = (tier: number) => {
|
||||
const tiers = Object.keys(TIER_COLORS).length;
|
||||
let index = Math.abs(tier - 1);
|
||||
if (tier >= tiers) {
|
||||
index = index % tiers;
|
||||
}
|
||||
return TIER_COLORS[index];
|
||||
};
|
||||
|
||||
const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -53,10 +35,12 @@ const StakingTier = ({
|
||||
tier,
|
||||
referralRewardMultiplier,
|
||||
minimumStakedTokens,
|
||||
max,
|
||||
}: {
|
||||
tier: number;
|
||||
referralRewardMultiplier: string;
|
||||
minimumStakedTokens: string;
|
||||
max?: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const minimum = addDecimalsFormatNumber(minimumStakedTokens, 18);
|
||||
@@ -97,7 +81,7 @@ const StakingTier = ({
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Tag color={getTierColor(tier)}>
|
||||
<Tag color={getTierColor(tier, max)}>
|
||||
{t('Multiplier')} {referralRewardMultiplier}x
|
||||
</Tag>
|
||||
<p className="mt-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
@@ -159,7 +143,9 @@ export const TiersContainer = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-3xl mt-10">{t('Current program details')}</h2>
|
||||
<h2 className="text-3xl mt-10 font-alpha calt">
|
||||
{t('Current program details')}
|
||||
</h2>
|
||||
{details?.id && (
|
||||
<p>
|
||||
<Trans
|
||||
@@ -198,7 +184,18 @@ export const TiersContainer = () => {
|
||||
</div>
|
||||
|
||||
{/* Container */}
|
||||
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white rounded-lg p-6 mt-1 mb-20">
|
||||
<div
|
||||
className={classNames(
|
||||
'md:bg-vega-clight-800',
|
||||
'md:dark:bg-vega-cdark-800',
|
||||
'md:text-black',
|
||||
'md:dark:text-white',
|
||||
'md:rounded-lg',
|
||||
'md:p-6',
|
||||
'mt-1',
|
||||
'mb-20'
|
||||
)}
|
||||
>
|
||||
{/* Benefit tiers */}
|
||||
<div className="flex flex-col mb-5">
|
||||
<h3 className="text-2xl calt">{t('Benefit tiers')}</h3>
|
||||
@@ -261,6 +258,7 @@ const StakingTiers = ({
|
||||
<StakingTier
|
||||
key={i}
|
||||
tier={tier}
|
||||
max={data.length}
|
||||
referralRewardMultiplier={referralRewardMultiplier}
|
||||
minimumStakedTokens={minimumStakedTokens}
|
||||
/>
|
||||
@@ -322,22 +320,7 @@ const TiersTable = ({
|
||||
className="bg-white dark:bg-vega-cdark-900"
|
||||
data={data.map((d) => ({
|
||||
...d,
|
||||
className: classNames({
|
||||
'from-vega-yellow-400 dark:from-vega-yellow-600 to-20% bg-highlight':
|
||||
'yellow' === getTierColor(d.tier),
|
||||
'from-vega-green-400 dark:from-vega-green-600 to-20% bg-highlight':
|
||||
'green' === getTierColor(d.tier),
|
||||
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
|
||||
'blue' === getTierColor(d.tier),
|
||||
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
|
||||
'purple' === getTierColor(d.tier),
|
||||
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
|
||||
'pink' === getTierColor(d.tier),
|
||||
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
|
||||
'orange' === getTierColor(d.tier),
|
||||
'from-vega-clight-200 dark:from-vega-cdark-200 to-20% bg-highlight':
|
||||
'none' === getTierColor(d.tier),
|
||||
}),
|
||||
className: classNames(getTierGradient(d.tier, data.length)),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -19,9 +19,11 @@ export const Tile = ({
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg overflow-hidden relative',
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white',
|
||||
'p-6',
|
||||
'text-black dark:text-white',
|
||||
'overflow-hidden relative',
|
||||
'p-3 md:p-6',
|
||||
'rounded-lg',
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800',
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -31,7 +33,7 @@ export const Tile = ({
|
||||
};
|
||||
|
||||
type StatTileProps = {
|
||||
title: string;
|
||||
title: ReactNode;
|
||||
testId?: string;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
@@ -55,7 +57,10 @@ export const StatTile = ({
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<div data-testid={`${testId}-value`} className="text-5xl text-left">
|
||||
<div
|
||||
data-testid={`${testId}-value`}
|
||||
className="text-2xl lg:text-5xl text-left"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{description && (
|
||||
@@ -70,7 +75,7 @@ export const StatTile = ({
|
||||
export const NoProgramTile = ({ title }: Pick<StatTileProps, 'title'>) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Tile title={title}>
|
||||
<Tile>
|
||||
<h3 className="mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100 calt">
|
||||
{title}
|
||||
</h3>
|
||||
@@ -119,7 +124,7 @@ export const CodeTile = ({
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative bg-rainbow bg-clip-text text-transparent text-5xl overflow-hidden',
|
||||
'relative bg-rainbow bg-clip-text text-transparent text-2xl lg:text-5xl overflow-hidden',
|
||||
FADE_OUT_STYLE
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { Teams } from './teams';
|
||||
@@ -0,0 +1,7 @@
|
||||
export const Teams = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Teams</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -73,7 +73,7 @@ export const AccountsContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
const useAccountStore = create<DataGridSlice>()(
|
||||
export const useAccountStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_accounts_store',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../grid-settings/grid-settings';
|
||||
import { useAccountStore } from './accounts-container';
|
||||
|
||||
export const AccountsSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={useAccountStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -1 +1,2 @@
|
||||
export * from './accounts-container';
|
||||
export * from './accounts-settings';
|
||||
|
||||
@@ -8,25 +8,42 @@ export const Card = ({
|
||||
className,
|
||||
loading = false,
|
||||
highlight = false,
|
||||
testId,
|
||||
noBackgroundOnMobile = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
highlight?: boolean;
|
||||
testId?: string;
|
||||
noBackgroundOnMobile?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
data-testid={testId}
|
||||
className={classNames(
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full p-0.5 lg:col-auto',
|
||||
'rounded-lg',
|
||||
'col-span-full lg:col-auto',
|
||||
{
|
||||
'rounded-lg bg-vega-clight-800 dark:bg-vega-cdark-800 p-0.5':
|
||||
!noBackgroundOnMobile,
|
||||
'mt-3 md:mt-0 md:rounded-lg md:bg-vega-clight-800 md:dark:bg-vega-cdark-800 md:p-0.5':
|
||||
noBackgroundOnMobile,
|
||||
},
|
||||
{
|
||||
'bg-rainbow': highlight,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4">
|
||||
<div
|
||||
className={classNames('h-full w-full', {
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 rounded p-4':
|
||||
!noBackgroundOnMobile,
|
||||
'md:bg-vega-clight-800 md:dark:bg-vega-cdark-800 md:rounded md:p-4':
|
||||
noBackgroundOnMobile,
|
||||
})}
|
||||
>
|
||||
<h2 className="mb-3">{title}</h2>
|
||||
{loading ? <CardLoader /> : children}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MarketFees } from './market-fees';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
import { Table, Td, Th, THead, Tr } from './table';
|
||||
import { Table as SimpleTable } from '../../components/table';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { Links } from '../../lib/links';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import classNames from 'classnames';
|
||||
import { getTierGradient } from '../helpers/tiers';
|
||||
|
||||
export const FeesContainer = () => {
|
||||
const t = useT();
|
||||
@@ -84,7 +86,10 @@ export const FeesContainer = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-4 gap-3">
|
||||
<div
|
||||
className="grid auto-rows-min grid-cols-4 gap-3"
|
||||
data-testid="fees-container"
|
||||
>
|
||||
{isConnected && (
|
||||
<>
|
||||
<Card
|
||||
@@ -124,7 +129,10 @@ export const FeesContainer = () => {
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-muted pt-3 text-sm">
|
||||
<p
|
||||
className="text-muted pt-3 text-sm"
|
||||
data-testid="no-volume-discount"
|
||||
>
|
||||
{t('No volume discount program active')}
|
||||
</p>
|
||||
)}
|
||||
@@ -133,17 +141,22 @@ export const FeesContainer = () => {
|
||||
title={t('Referral benefits')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
data-testid="referral-benefits-card"
|
||||
>
|
||||
{isReferrer ? (
|
||||
<ReferrerInfo code={code} />
|
||||
<ReferrerInfo code={code} data-testid="referrer-info" />
|
||||
) : isReferralProgramRunning ? (
|
||||
<ReferralBenefits
|
||||
setRunningNotionalTakerVolume={referralVolumeInWindow}
|
||||
epochsInSet={epochsInSet}
|
||||
epochs={referralDiscountWindowLength}
|
||||
data-testid="referral-benefits"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-muted pt-3 text-sm">
|
||||
<p
|
||||
className="text-muted pt-3 text-sm"
|
||||
data-testid="no-referral-program"
|
||||
>
|
||||
{t('No referral program active')}
|
||||
</p>
|
||||
)}
|
||||
@@ -154,6 +167,8 @@ export const FeesContainer = () => {
|
||||
title={t('Volume discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
data-testid="volume-discount-card"
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<VolumeTiers
|
||||
tiers={volumeTiers}
|
||||
@@ -166,18 +181,23 @@ export const FeesContainer = () => {
|
||||
title={t('Referral discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
data-testid="referral-discount-card"
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<ReferralTiers
|
||||
tiers={referralTiers}
|
||||
tierIndex={referralTierIndex}
|
||||
epochsInSet={epochsInSet}
|
||||
referralVolumeInWindow={referralVolumeInWindow}
|
||||
referralDiscountWindowLength={referralDiscountWindowLength}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Fees by market')}
|
||||
className="lg:col-span-full"
|
||||
loading={marketsLoading}
|
||||
data-testid="fees-by-market-card"
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<MarketFees
|
||||
markets={markets}
|
||||
@@ -245,7 +265,7 @@ export const TradingFees = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<div className="pt-4" data-testid="trading-fees">
|
||||
<div className="leading-none">
|
||||
<p className="block text-3xl leading-none" data-testid="adjusted-fees">
|
||||
{minAdjustedTotal !== undefined && maxAdjustedTotal !== undefined
|
||||
@@ -255,7 +275,7 @@ export const TradingFees = ({
|
||||
: `${formatPercentage(adjustedTotal)}%`}
|
||||
</p>
|
||||
<CardTable>
|
||||
<tr className="text-default">
|
||||
<tr className="text-default" data-testid="total-fee-before-discount">
|
||||
<CardTableTH>{t('Total fee before discount')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{minTotal !== undefined && maxTotal !== undefined
|
||||
@@ -265,7 +285,7 @@ export const TradingFees = ({
|
||||
: `${formatPercentage(total.toNumber())}%`}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-testid="infrastructure-fees">
|
||||
<CardTableTH>{t('Infrastructure')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(
|
||||
@@ -274,14 +294,14 @@ export const TradingFees = ({
|
||||
%
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-testid="maker-fees">
|
||||
<CardTableTH>{t('Maker')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(Number(params.market_fee_factors_makerFee))}%
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
{minLiq && maxLiq && (
|
||||
<tr>
|
||||
<tr data-testid="liquidity-fees">
|
||||
<CardTableTH>{t('Liquidity')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
|
||||
@@ -317,21 +337,19 @@ export const CurrentVolume = ({
|
||||
const currentVolume = new BigNumber(windowLengthVolume);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<div className="flex flex-col gap-3 pt-4" data-testid="current-volume">
|
||||
<CardStat
|
||||
value={
|
||||
currentVolume.isZero()
|
||||
? `<${formatNumberRounded(requiredForNextTier)}`
|
||||
: formatNumberRounded(currentVolume)
|
||||
}
|
||||
value={formatNumberRounded(currentVolume)}
|
||||
text={t('pastEpochs', 'Past {{count}} epochs', {
|
||||
count: windowLength,
|
||||
})}
|
||||
testId="past-epochs-volume"
|
||||
/>
|
||||
{requiredForNextTier.isGreaterThan(0) && (
|
||||
<CardStat
|
||||
value={formatNumber(requiredForNextTier)}
|
||||
text={t('Required for next tier')}
|
||||
testId="required-for-next-tier"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -349,7 +367,7 @@ const ReferralBenefits = ({
|
||||
}) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<div className="flex flex-col gap-3 pt-4" data-testid="referral-benefits">
|
||||
<CardStat
|
||||
// all sets volume (not just current party)
|
||||
value={formatNumber(setRunningNotionalTakerVolume)}
|
||||
@@ -360,8 +378,13 @@ const ReferralBenefits = ({
|
||||
count: epochs,
|
||||
}
|
||||
)}
|
||||
testId="running-notional-taker-volume"
|
||||
/>
|
||||
<CardStat
|
||||
value={epochsInSet}
|
||||
text={t('epochs in referral set')}
|
||||
testId="epochs-in-referral-set"
|
||||
/>
|
||||
<CardStat value={epochsInSet} text={t('epochs in referral set')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -389,7 +412,7 @@ const TotalDiscount = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<div className="pt-4" data-testid="total-discount-card-stats">
|
||||
<CardStat
|
||||
description={
|
||||
<>
|
||||
@@ -399,9 +422,10 @@ const TotalDiscount = ({
|
||||
}
|
||||
value={formatPercentage(totalDiscount) + '%'}
|
||||
highlight={true}
|
||||
testId="total-discount"
|
||||
/>
|
||||
<CardTable>
|
||||
<tr>
|
||||
<tr data-testid="volume-discount-row">
|
||||
<CardTableTH>{t('Volume discount')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(volumeDiscount)}%
|
||||
@@ -415,7 +439,7 @@ const TotalDiscount = ({
|
||||
)}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-testid="referral-discount-row">
|
||||
<CardTableTH>{t('Referral discount')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(referralDiscount)}%
|
||||
@@ -459,36 +483,65 @@ const VolumeTiers = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<THead>
|
||||
<tr>
|
||||
<Th>{t('Tier')}</Th>
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>
|
||||
{t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: windowLength,
|
||||
})}
|
||||
</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(tier.volumeDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : null}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
<SimpleTable
|
||||
className="bg-white dark:bg-vega-cdark-900"
|
||||
columns={[
|
||||
{ name: 'tier', displayName: t('Tier'), testId: 'col-tier-value' },
|
||||
{
|
||||
name: 'discount',
|
||||
displayName: t('Discount'),
|
||||
testId: 'discount-value',
|
||||
},
|
||||
{
|
||||
name: 'minTradingVolume',
|
||||
displayName: t('Min. trading volume'),
|
||||
testId: 'min-volume-value',
|
||||
},
|
||||
{
|
||||
name: 'myVolume',
|
||||
displayName: t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: windowLength,
|
||||
}),
|
||||
testId: 'my-volume-value',
|
||||
},
|
||||
{
|
||||
name: 'indicator',
|
||||
className: 'max-md:hidden',
|
||||
testId: 'your-tier',
|
||||
},
|
||||
]}
|
||||
data={Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
const indicator = isUserTier ? (
|
||||
<YourTier testId={`your-volume-tier-${i}`} />
|
||||
) : null;
|
||||
const tierIndicator = (
|
||||
<div className="flex justify-between">
|
||||
<span data-testid={`tier-value-${i}`}>{i + 1}</span>
|
||||
<span className="md:hidden">{indicator}</span>
|
||||
</div>
|
||||
);
|
||||
return {
|
||||
tier: tierIndicator,
|
||||
discount: (
|
||||
<>{formatPercentage(Number(tier.volumeDiscountFactor))}%</>
|
||||
),
|
||||
minTradingVolume: (
|
||||
<>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</>
|
||||
),
|
||||
myVolume: isUserTier ? (
|
||||
formatNumber(lastEpochVolume)
|
||||
) : (
|
||||
<span className="md:hidden">-</span>
|
||||
),
|
||||
indicator: indicator,
|
||||
className: classNames(
|
||||
getTierGradient(i + 1, tiers.length),
|
||||
'text-xs'
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -498,6 +551,7 @@ const ReferralTiers = ({
|
||||
tierIndex,
|
||||
epochsInSet,
|
||||
referralVolumeInWindow,
|
||||
referralDiscountWindowLength,
|
||||
}: {
|
||||
tiers: Array<{
|
||||
referralDiscountFactor: string;
|
||||
@@ -507,6 +561,7 @@ const ReferralTiers = ({
|
||||
tierIndex: number;
|
||||
epochsInSet: number;
|
||||
referralVolumeInWindow: number;
|
||||
referralDiscountWindowLength: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
@@ -518,55 +573,97 @@ const ReferralTiers = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<THead>
|
||||
<tr>
|
||||
<Th>{t('Tier')}</Th>
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>{t('Required epochs')}</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers).map((t, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
<SimpleTable
|
||||
className="bg-white dark:bg-vega-cdark-900"
|
||||
columns={[
|
||||
{ name: 'tier', displayName: t('Tier'), testId: 'col-tier-value' },
|
||||
{
|
||||
name: 'discount',
|
||||
displayName: t('Discount'),
|
||||
tooltip: t(
|
||||
"The proportion of the referee's taker fees to be discounted"
|
||||
),
|
||||
testId: 'discount-value',
|
||||
},
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'minTradingVolume',
|
||||
'Min. trading volume (last {{count}} epochs)',
|
||||
{
|
||||
count: referralDiscountWindowLength,
|
||||
}
|
||||
),
|
||||
tooltip: t(
|
||||
'The minimum running notional for the given benefit tier'
|
||||
),
|
||||
testId: 'min-volume-value',
|
||||
},
|
||||
{
|
||||
name: 'epochs',
|
||||
displayName: t('Min. epochs'),
|
||||
tooltip: t(
|
||||
'The minimum number of epochs the party needs to be in the referral set to be eligible for the benefit'
|
||||
),
|
||||
testId: 'required-epochs-value',
|
||||
},
|
||||
{
|
||||
name: 'indicator',
|
||||
className: 'max-md:hidden',
|
||||
testId: 'user-tier-or-unlocks',
|
||||
},
|
||||
]}
|
||||
data={Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
const requiredVolume = Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
|
||||
let unlocksIn = null;
|
||||
const indicator = isUserTier ? (
|
||||
<YourTier testId={`your-referral-tier-${i}`} />
|
||||
) : referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < tier.minimumEpochs ? (
|
||||
<span className="text-muted text-xs">
|
||||
Unlocks in {tier.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
if (
|
||||
referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < t.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const tierIndicator = (
|
||||
<div className="flex justify-between">
|
||||
<span data-testid={`tier-value-${i}`}>{i + 1}</span>
|
||||
<span className="md:hidden">{indicator}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{t.minimumEpochs}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
return {
|
||||
tier: tierIndicator,
|
||||
discount: (
|
||||
<>{formatPercentage(Number(tier.referralDiscountFactor))}%</>
|
||||
),
|
||||
volume: formatNumber(tier.minimumRunningNotionalTakerVolume),
|
||||
epochs: tier.minimumEpochs,
|
||||
indicator,
|
||||
className: classNames(
|
||||
getTierGradient(i + 1, tiers.length),
|
||||
'text-xs'
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const YourTier = () => {
|
||||
interface YourTierProps {
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
const YourTier = ({ testId }: YourTierProps) => {
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<span className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white">
|
||||
<span
|
||||
className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white text-xs"
|
||||
data-testid={testId}
|
||||
>
|
||||
{t('Your tier')}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -8,43 +8,53 @@ import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler';
|
||||
import { Links } from '../../lib/links';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { useMemo } from 'react';
|
||||
import { type ColDef } from 'ag-grid-community/dist/lib/entities/colDef';
|
||||
|
||||
const useFeesTableColumnDefs = () => {
|
||||
const useFeesTableColumnDefs = (): ColDef[] => {
|
||||
const t = useT();
|
||||
return useMemo(
|
||||
() => [
|
||||
{ field: 'code', cellRenderer: 'MarketCodeCell' },
|
||||
{
|
||||
field: 'feeAfterDiscount',
|
||||
headerName: t('Total fee after discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'infraFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'makerFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'liquidityFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'totalFee',
|
||||
headerName: t('Total fee before discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
],
|
||||
() =>
|
||||
[
|
||||
{
|
||||
field: 'code',
|
||||
cellRenderer: 'MarketCodeCell',
|
||||
pinned: 'left',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'liquidityFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'feeAfterDiscount',
|
||||
headerName: t('Total fee after discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'totalFee',
|
||||
headerName: t('Total fee before discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'infraFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'makerFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
] as ColDef[],
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
const feesTableDefaultColDef = {
|
||||
flex: 1,
|
||||
minWidth: 62,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
suppressMovable: true,
|
||||
pinned: false,
|
||||
};
|
||||
|
||||
const components = {
|
||||
@@ -62,6 +72,8 @@ export const MarketFees = ({
|
||||
}) => {
|
||||
const navigateWithMeta = useNavigateWithMeta();
|
||||
|
||||
const colDef = useFeesTableColumnDefs();
|
||||
|
||||
const rows = compact(markets || []).map((m) => {
|
||||
const infraFee = new BigNumber(m.fees.factors.infrastructureFee);
|
||||
const makerFee = new BigNumber(m.fees.factors.makerFee);
|
||||
@@ -88,9 +100,9 @@ export const MarketFees = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="border rounded-sm border-default">
|
||||
<div className="border rounded-lg md:rounded-sm overflow-hidden border-default">
|
||||
<AgGrid
|
||||
columnDefs={useFeesTableColumnDefs()}
|
||||
columnDefs={colDef}
|
||||
rowData={rows}
|
||||
getRowId={({ data }) => data.id}
|
||||
defaultColDef={feesTableDefaultColDef}
|
||||
|
||||
@@ -3,21 +3,31 @@ import type { ReactNode } from 'react';
|
||||
|
||||
const cellClass = 'px-4 py-2 text-xs font-normal text-left last:text-right';
|
||||
|
||||
export const Th = ({ children }: { children?: ReactNode }) => {
|
||||
export const Th = ({ children, ...props }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<th className={classNames(cellClass, 'text-secondary leading-none py-3')}>
|
||||
<th
|
||||
className={classNames(cellClass, 'text-secondary leading-none py-3')}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
export const Td = ({ children }: { children?: ReactNode }) => {
|
||||
return <th className={cellClass}>{children}</th>;
|
||||
export const Td = ({ children, ...props }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<th className={cellClass} {...props}>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
export const Tr = ({ children }: { children?: ReactNode }) => {
|
||||
export const Tr = ({ children, ...props }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<tr className="hover:bg-vega-clight-600 dark:hover:bg-vega-cdark-700">
|
||||
<tr
|
||||
className="hover:bg-vega-clight-600 dark:hover:bg-vega-cdark-700"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ export const FillsContainer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const useFillsStore = create<DataGridSlice>()(
|
||||
export const useFillsStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_fills_store',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../grid-settings/grid-settings';
|
||||
import { useFillsStore } from './fills-container';
|
||||
|
||||
export const FillsSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={useFillsStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -1 +1,2 @@
|
||||
export * from './fills-container';
|
||||
export * from './fills-settings';
|
||||
|
||||
@@ -45,7 +45,7 @@ export const FundingPaymentsContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
const useFundingPaymentsStore = create<DataGridSlice>()(
|
||||
export const useFundingPaymentsStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_funding_payments_store',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../grid-settings/grid-settings';
|
||||
import { useFundingPaymentsStore } from './funding-payments-container';
|
||||
|
||||
export const FundingPaymentsSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={useFundingPaymentsStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -1 +1,2 @@
|
||||
export * from './funding-payments-container';
|
||||
export * from './funding-payments-settings';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useT } from '../../lib/use-t';
|
||||
import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const GridSettings = ({
|
||||
updateGridStore,
|
||||
}: {
|
||||
updateGridStore: (gridStore: DataGridStore) => void;
|
||||
}) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
updateGridStore({
|
||||
columnState: undefined,
|
||||
filterModel: undefined,
|
||||
})
|
||||
}
|
||||
size="extra-small"
|
||||
>
|
||||
{t('Reset Columns')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
+9
-1
@@ -2,7 +2,15 @@ import type { HTMLAttributes } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type TagProps = {
|
||||
color?: 'yellow' | 'green' | 'blue' | 'purple' | 'pink' | 'orange' | 'none';
|
||||
color?:
|
||||
| 'yellow'
|
||||
| 'green'
|
||||
| 'blue'
|
||||
| 'purple'
|
||||
| 'pink'
|
||||
| 'orange'
|
||||
| 'red'
|
||||
| 'none';
|
||||
};
|
||||
export const Tag = ({
|
||||
color = 'none',
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import type { Tag } from './tag';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// rainbow-ish order
|
||||
export const TIER_COLORS: Array<ComponentProps<typeof Tag>['color']> = [
|
||||
'none', // worst tier if 8 tiers, otherwise relative to the best tier
|
||||
'red',
|
||||
'pink',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'blue',
|
||||
'purple', // best tier
|
||||
];
|
||||
|
||||
export const getTierColor = (tier: number, max = TIER_COLORS.length) => {
|
||||
const available =
|
||||
max < TIER_COLORS.length
|
||||
? TIER_COLORS.slice(TIER_COLORS.length - max)
|
||||
: TIER_COLORS;
|
||||
const tiers = Object.keys(available).length;
|
||||
let index = Math.abs(tier - 1);
|
||||
if (tier >= tiers) {
|
||||
index = index % tiers;
|
||||
}
|
||||
return available[index];
|
||||
};
|
||||
|
||||
export const getTierGradient = (tier: number, max = TIER_COLORS.length) =>
|
||||
classNames({
|
||||
'from-vega-yellow-400 dark:from-vega-yellow-600 to-20% bg-highlight':
|
||||
'yellow' === getTierColor(tier, max),
|
||||
'from-vega-green-400 dark:from-vega-green-600 to-20% bg-highlight':
|
||||
'green' === getTierColor(tier, max),
|
||||
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
|
||||
'blue' === getTierColor(tier, max),
|
||||
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
|
||||
'purple' === getTierColor(tier, max),
|
||||
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
|
||||
'pink' === getTierColor(tier, max),
|
||||
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
|
||||
'orange' === getTierColor(tier, max),
|
||||
'from-vega-red-400 dark:from-vega-red-600 to-20% bg-highlight':
|
||||
'red' === getTierColor(tier, max),
|
||||
'from-vega-clight-600 dark:from-vega-cdark-600 to-20% bg-highlight':
|
||||
'none' === getTierColor(tier, max),
|
||||
});
|
||||
@@ -1,3 +1 @@
|
||||
export * from './market-successor-banner';
|
||||
export * from './market-successor-proposal-banner';
|
||||
export * from './market-termination-banner';
|
||||
export { MarketBanner } from './market-banner';
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useMarketState,
|
||||
type Market,
|
||||
useMaliciousOracle,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { useState } from 'react';
|
||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { MarketSuspendedBanner } from './market-suspended-banner';
|
||||
import { MarketUpdateBanner } from './market-update-banner';
|
||||
import { MarketUpdateStateBanner } from './market-update-state-banner';
|
||||
import { MarketSettledBanner } from './market-settled-banner';
|
||||
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
||||
import { MarketOracleBanner, type Oracle } from './market-oracle-banner';
|
||||
import {
|
||||
useSuccessorMarketProposals,
|
||||
useUpdateMarketProposals,
|
||||
useUpdateMarketStateProposals,
|
||||
} from './use-market-proposals';
|
||||
|
||||
type UpdateMarketBanner = {
|
||||
kind: 'UpdateMarket';
|
||||
proposals: MarketViewProposalFieldsFragment[];
|
||||
};
|
||||
|
||||
type UpdateMarketStateBanner = {
|
||||
kind: 'UpdateMarketState';
|
||||
proposals: MarketViewProposalFieldsFragment[];
|
||||
};
|
||||
|
||||
type NewMarketBanner = {
|
||||
kind: 'NewMarket'; // aka a proposal of NewMarket which succeeds the current market
|
||||
proposals: MarketViewProposalFieldsFragment[];
|
||||
};
|
||||
|
||||
type SettledBanner = {
|
||||
kind: 'Settled';
|
||||
market: Market;
|
||||
};
|
||||
|
||||
type SuspendedBanner = {
|
||||
kind: 'Suspended';
|
||||
market: Market;
|
||||
};
|
||||
|
||||
type OracleBanner = {
|
||||
kind: 'Oracle';
|
||||
oracle: Oracle;
|
||||
};
|
||||
|
||||
type Banner =
|
||||
| UpdateMarketBanner
|
||||
| UpdateMarketStateBanner
|
||||
| NewMarketBanner
|
||||
| SettledBanner
|
||||
| SuspendedBanner
|
||||
| OracleBanner;
|
||||
|
||||
export const MarketBanner = ({ market }: { market: Market }) => {
|
||||
const { data: marketState } = useMarketState(market.id);
|
||||
|
||||
const { proposals: successorProposals, loading: successorLoading } =
|
||||
useSuccessorMarketProposals(market.id);
|
||||
|
||||
const { proposals: updateMarketProposals, loading: updateMarketLoading } =
|
||||
useUpdateMarketProposals(market.id);
|
||||
|
||||
const {
|
||||
proposals: updateMarketStateProposals,
|
||||
loading: updateMarketStateLoading,
|
||||
} = useUpdateMarketStateProposals(market.id);
|
||||
|
||||
const { data: maliciousOracle, loading: oracleLoading } = useMaliciousOracle(
|
||||
market.id
|
||||
);
|
||||
|
||||
const loading =
|
||||
successorLoading ||
|
||||
updateMarketLoading ||
|
||||
updateMarketStateLoading ||
|
||||
oracleLoading;
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const banners = compact([
|
||||
updateMarketStateProposals.length
|
||||
? {
|
||||
kind: 'UpdateMarketState' as const,
|
||||
proposals: updateMarketStateProposals,
|
||||
}
|
||||
: undefined,
|
||||
updateMarketProposals.length
|
||||
? {
|
||||
kind: 'UpdateMarket' as const,
|
||||
proposals: updateMarketProposals,
|
||||
}
|
||||
: undefined,
|
||||
successorProposals.length
|
||||
? {
|
||||
kind: 'NewMarket' as const,
|
||||
proposals: successorProposals,
|
||||
}
|
||||
: undefined,
|
||||
marketState === MarketState.STATE_SETTLED
|
||||
? {
|
||||
kind: 'Settled' as const,
|
||||
market,
|
||||
}
|
||||
: undefined,
|
||||
marketState === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||
? {
|
||||
kind: 'Suspended' as const,
|
||||
market,
|
||||
}
|
||||
: undefined,
|
||||
maliciousOracle !== undefined
|
||||
? {
|
||||
kind: 'Oracle' as const,
|
||||
oracle: maliciousOracle,
|
||||
}
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
return <BannerQueue banners={banners} market={market} />;
|
||||
};
|
||||
|
||||
const BannerQueue = ({
|
||||
banners,
|
||||
market,
|
||||
}: {
|
||||
banners: Banner[];
|
||||
market: Market;
|
||||
}) => {
|
||||
// for showing banner by index
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const banner = banners[index];
|
||||
if (!banner) return null;
|
||||
|
||||
let content = null;
|
||||
let intent = Intent.Primary;
|
||||
|
||||
switch (banner.kind) {
|
||||
case 'UpdateMarket': {
|
||||
content = <MarketUpdateBanner proposals={banner.proposals} />;
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarketState': {
|
||||
content = (
|
||||
<MarketUpdateStateBanner proposals={banner.proposals} market={market} />
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'NewMarket': {
|
||||
content = <MarketSuccessorProposalBanner proposals={banner.proposals} />;
|
||||
intent = Intent.Warning;
|
||||
break;
|
||||
}
|
||||
case 'Settled': {
|
||||
content = <MarketSettledBanner market={market} />;
|
||||
break;
|
||||
}
|
||||
case 'Suspended': {
|
||||
content = <MarketSuspendedBanner />;
|
||||
intent = Intent.Warning;
|
||||
break;
|
||||
}
|
||||
case 'Oracle': {
|
||||
// @ts-ignore oracle cannot be undefined
|
||||
content = <MarketOracleBanner oracle={banner.oracle} />;
|
||||
intent = Intent.Danger;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const showCount = banners.length > 1;
|
||||
|
||||
const onClose = () => {
|
||||
setIndex((x) => x + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={intent}
|
||||
onClose={onClose}
|
||||
data-testid="market-banner"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{content}
|
||||
{showCount ? (
|
||||
<p>
|
||||
{index + 1}/{banners.length}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</NotificationBanner>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MarketOracleBanner } from './market-oracle-banner';
|
||||
|
||||
jest.mock('@vegaprotocol/markets', () => ({
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
OracleDialog: ({ open }: { open: boolean }) => {
|
||||
return <div data-testid="oracle-dialog">{open ? 'open' : 'closed'}</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
const oracle = {
|
||||
dataSourceSpecId: 'someId',
|
||||
provider: {
|
||||
name: 'Test oracle',
|
||||
url: 'https://zombo.com',
|
||||
description_markdown:
|
||||
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
|
||||
oracle: {
|
||||
status: 'COMPROMISED' as const,
|
||||
status_reason: 'Reason here',
|
||||
first_verified: '2021-01-01T00:00:00Z',
|
||||
last_verified: '2022-01-01T00:00:00Z',
|
||||
type: 'public_key' as const,
|
||||
public_key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
|
||||
},
|
||||
proofs: [
|
||||
{
|
||||
format: 'url' as const,
|
||||
available: true,
|
||||
type: 'web' as const,
|
||||
url: 'https://proofweb.com',
|
||||
},
|
||||
{
|
||||
format: 'signed_message' as const,
|
||||
available: true,
|
||||
type: 'public_key' as const,
|
||||
public_key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
|
||||
message: 'SOMEHEX',
|
||||
},
|
||||
],
|
||||
github_link:
|
||||
'https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/eth_address-0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC.toml',
|
||||
},
|
||||
};
|
||||
|
||||
describe('MarketOracleBanner', () => {
|
||||
it('should render successfully', async () => {
|
||||
render(<MarketOracleBanner oracle={oracle} />);
|
||||
|
||||
expect(screen.getByTestId('oracle-dialog')).toHaveTextContent('closed');
|
||||
expect(screen.getByTestId('oracle-banner-status')).toHaveTextContent(
|
||||
'COMPROMISED'
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Show more' }));
|
||||
expect(screen.getByTestId('oracle-dialog')).toHaveTextContent('open');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import {
|
||||
OracleDialog,
|
||||
useOracleStatuses,
|
||||
type useMaliciousOracle,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export type Oracle = ReturnType<typeof useMaliciousOracle>['data'];
|
||||
|
||||
export const MarketOracleBanner = ({
|
||||
oracle,
|
||||
}: {
|
||||
oracle: NonNullable<Oracle>;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const oracleStatuses = useOracleStatuses();
|
||||
|
||||
return (
|
||||
<>
|
||||
<OracleDialog open={open} onChange={setOpen} {...oracle} />
|
||||
<p>
|
||||
<Trans
|
||||
defaults="Oracle status for this market is <0>{{status}}</0>. {{description}} <1>Show more</1>"
|
||||
components={[
|
||||
<span key="oracle-status" data-testid="oracle-banner-status">
|
||||
status
|
||||
</span>,
|
||||
<ButtonLink
|
||||
key="oracle-button"
|
||||
onClick={() => setOpen((x) => !x)}
|
||||
data-testid="oracle-banner-dialog-trigger"
|
||||
>
|
||||
Show more
|
||||
</ButtonLink>,
|
||||
]}
|
||||
values={{
|
||||
status: oracle.provider.oracle.status,
|
||||
description: oracleStatuses[oracle.provider.oracle.status],
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { addDays, subDays, subHours } from 'date-fns';
|
||||
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
|
||||
import {
|
||||
SuccessorMarketDocument,
|
||||
MarketCandlesDocument,
|
||||
MarketCandlesUpdateDocument,
|
||||
SuccessorMarketIdDocument,
|
||||
type Market,
|
||||
type SuccessorMarketQuery,
|
||||
type SuccessorMarketQueryVariables,
|
||||
type MarketCandlesQuery,
|
||||
type MarketCandlesQueryVariables,
|
||||
type MarketCandlesUpdateSubscription,
|
||||
type MarketCandlesUpdateSubscriptionVariables,
|
||||
type SuccessorMarketIdQuery,
|
||||
type SuccessorMarketIdQueryVariables,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import { Interval, MarketState, MarketTradingMode } from '@vegaprotocol/types';
|
||||
import { MarketSettledBanner } from './market-settled-banner';
|
||||
|
||||
describe('MarketSettledBanner', () => {
|
||||
const now = 1701388800000;
|
||||
const origDateNow = Date.now;
|
||||
|
||||
beforeAll(() => {
|
||||
Date.now = () => now;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Date.now = origDateNow;
|
||||
});
|
||||
|
||||
const marketId = 'market-0';
|
||||
const successorId = 'successor-id';
|
||||
const successorName = 'successor-name';
|
||||
|
||||
const successorMarketIdMock: MockedResponse<
|
||||
SuccessorMarketIdQuery,
|
||||
SuccessorMarketIdQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: SuccessorMarketIdDocument,
|
||||
variables: {
|
||||
marketId: marketId,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
market: {
|
||||
successorMarketID: successorId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const successorMock: MockedResponse<
|
||||
SuccessorMarketQuery,
|
||||
SuccessorMarketQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: SuccessorMarketDocument,
|
||||
variables: {
|
||||
marketId: successorId,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
market: {
|
||||
id: successorId,
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
positionDecimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: successorName,
|
||||
code: 'code',
|
||||
},
|
||||
},
|
||||
proposal: {
|
||||
id: 'proposal-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const since = subDays(new Date(now), 5).toISOString();
|
||||
const successorCandlesMock: MockedResponse<
|
||||
MarketCandlesQuery,
|
||||
MarketCandlesQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: MarketCandlesDocument,
|
||||
variables: {
|
||||
marketId: successorId,
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
since,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
candlesConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
high: '100',
|
||||
low: '100',
|
||||
open: '100',
|
||||
close: '100',
|
||||
volume: '100',
|
||||
periodStart: subHours(new Date(now), 1).toISOString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
high: '100',
|
||||
low: '100',
|
||||
open: '100',
|
||||
close: '200',
|
||||
volume: '100',
|
||||
periodStart: subHours(new Date(now), 2).toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const successorCandlesUpdateMock: MockedResponse<
|
||||
MarketCandlesUpdateSubscription,
|
||||
MarketCandlesUpdateSubscriptionVariables
|
||||
> = {
|
||||
request: {
|
||||
query: MarketCandlesUpdateDocument,
|
||||
variables: {
|
||||
marketId: successorId,
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
// @ts-ignore this query doesn't need the 'since' variable, but the data-provider
|
||||
// uses all variables for both the query and the subscription so its needed here
|
||||
// to match the mock
|
||||
since,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
candles: {
|
||||
high: '100',
|
||||
low: '100',
|
||||
open: '100',
|
||||
close: '100',
|
||||
volume: '100',
|
||||
periodStart: '2020-01-01T00:00:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (market: Market, mocks?: MockedResponse[]) => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MarketSettledBanner market={market} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
it('shows that the market is settled if there is no successor', () => {
|
||||
const market = createMarketFragment({
|
||||
id: marketId,
|
||||
successorMarketID: undefined,
|
||||
});
|
||||
renderComponent(market, [successorMarketIdMock, successorMock]);
|
||||
expect(
|
||||
screen.getByText('This market has been settled')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows that the market has been succeeded with an active market', async () => {
|
||||
const market = createMarketFragment({
|
||||
id: marketId,
|
||||
successorMarketID: successorId,
|
||||
marketTimestamps: {
|
||||
close: addDays(new Date(now), 1).toISOString(),
|
||||
},
|
||||
});
|
||||
renderComponent(market, [
|
||||
successorMarketIdMock,
|
||||
successorMock,
|
||||
successorCandlesMock,
|
||||
successorCandlesUpdateMock,
|
||||
]);
|
||||
expect(
|
||||
await screen.findByText('This market has been succeeded')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText(/has a 24h trading volume of 200$/i)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('This market expires in 1 day.')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: successorName })).toHaveAttribute(
|
||||
'href',
|
||||
`/markets/${successorId}`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { isBefore, formatDuration, intervalToDuration } from 'date-fns';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
useCandles,
|
||||
useSuccessorMarket,
|
||||
type Market,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getMarketExpiryDate,
|
||||
isNumeric,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { Links } from '../../lib/links';
|
||||
|
||||
const getExpiryDate = (tags: string[], close?: string): Date | null => {
|
||||
const expiryDate = getMarketExpiryDate(tags);
|
||||
return expiryDate || (close && new Date(close)) || null;
|
||||
};
|
||||
|
||||
export const MarketSettledBanner = ({ market }: { market: Market }) => {
|
||||
const t = useT();
|
||||
const { data: successorMarket } = useSuccessorMarket(market.id);
|
||||
|
||||
const expiry = market
|
||||
? getExpiryDate(
|
||||
market.tradableInstrument.instrument.metadata.tags || [],
|
||||
market.marketTimestamps.close
|
||||
)
|
||||
: null;
|
||||
|
||||
const duration =
|
||||
expiry && isBefore(new Date(Date.now()), expiry)
|
||||
? intervalToDuration({ start: new Date(Date.now()), end: expiry })
|
||||
: null;
|
||||
|
||||
const { oneDayCandles } = useCandles({
|
||||
marketId: successorMarket?.id,
|
||||
});
|
||||
|
||||
const candleVolume = oneDayCandles?.length
|
||||
? calcCandleVolume(oneDayCandles)
|
||||
: null;
|
||||
|
||||
const successorVolume =
|
||||
candleVolume && isNumeric(successorMarket?.positionDecimalPlaces)
|
||||
? addDecimalsFormatNumber(
|
||||
candleVolume,
|
||||
successorMarket?.positionDecimalPlaces as number
|
||||
)
|
||||
: null;
|
||||
|
||||
if (successorMarket) {
|
||||
return (
|
||||
<div>
|
||||
<p>{t('This market has been succeeded')}</p>
|
||||
{duration && (
|
||||
<div className="mt-1">
|
||||
<span>
|
||||
{t('This market expires in {{duration}}.', {
|
||||
duration: formatDuration(duration, {
|
||||
format: [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
],
|
||||
}),
|
||||
})}
|
||||
</span>
|
||||
<>
|
||||
{' '}
|
||||
{successorVolume ? (
|
||||
<Trans
|
||||
defaults="The successor market <0>{{instrumentName}}</0> has a 24h trading volume of {{successorVolume}}"
|
||||
values={{
|
||||
successorVolume,
|
||||
instrumentName:
|
||||
successorMarket?.tradableInstrument.instrument.name,
|
||||
}}
|
||||
components={[
|
||||
<Link
|
||||
to={Links.MARKET(successorMarket.id)}
|
||||
key="link"
|
||||
target="_blank"
|
||||
>
|
||||
successor market name
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
defaults="The successor market is <0>{{instrumentName}}</0>"
|
||||
values={{
|
||||
instrumentName:
|
||||
successorMarket?.tradableInstrument.instrument.name,
|
||||
}}
|
||||
components={[
|
||||
<Link
|
||||
to={Links.MARKET(successorMarket.id)}
|
||||
key="link"
|
||||
target="_blank"
|
||||
>
|
||||
successor market name
|
||||
</Link>,
|
||||
]}
|
||||
ns={ns}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <p>{t('This market has been settled')}</p>;
|
||||
};
|
||||
@@ -1,155 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { MarketSuccessorBanner } from './market-successor-banner';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import * as allUtils from '@vegaprotocol/utils';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
|
||||
const market = {
|
||||
id: 'marketId',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
metadata: {
|
||||
tags: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
marketTimestamps: {
|
||||
close: null,
|
||||
},
|
||||
} as unknown as Market;
|
||||
|
||||
let mockDataSuccessorMarket: PartialDeep<Market> | null = null;
|
||||
let mockDataMarketState: Market['state'] | null = null;
|
||||
jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
...jest.requireActual('@vegaprotocol/data-provider'),
|
||||
useDataProvider: jest.fn().mockImplementation((args) => {
|
||||
if (args.skip) {
|
||||
return {
|
||||
data: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: mockDataSuccessorMarket,
|
||||
error: null,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
jest.mock('@vegaprotocol/utils', () => ({
|
||||
...jest.requireActual('@vegaprotocol/utils'),
|
||||
getMarketExpiryDate: jest.fn(),
|
||||
}));
|
||||
let mockCandles = {};
|
||||
jest.mock('@vegaprotocol/markets', () => ({
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
useMarketState: (marketId: string) =>
|
||||
marketId
|
||||
? {
|
||||
data: mockDataMarketState,
|
||||
}
|
||||
: { data: undefined },
|
||||
useSuccessorMarket: (marketId: string) =>
|
||||
marketId
|
||||
? {
|
||||
data: mockDataSuccessorMarket,
|
||||
}
|
||||
: { data: undefined },
|
||||
useCandles: () => mockCandles,
|
||||
}));
|
||||
|
||||
describe('MarketSuccessorBanner', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockDataSuccessorMarket = {
|
||||
id: 'successorMarketID',
|
||||
state: Types.MarketState.STATE_ACTIVE,
|
||||
tradingMode: Types.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Successor Market Name',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
describe('should be hidden', () => {
|
||||
it('when no market', () => {
|
||||
const { container } = render(<MarketSuccessorBanner market={null} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('no successor market data', () => {
|
||||
mockDataSuccessorMarket = null;
|
||||
const { container } = render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should be displayed', () => {
|
||||
it('should be rendered', () => {
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText('This market has been succeeded')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'Successor Market Name' })
|
||||
).toHaveAttribute('href', '/#/markets/successorMarketID');
|
||||
});
|
||||
|
||||
it('no successor market data, market settled', () => {
|
||||
mockDataSuccessorMarket = null;
|
||||
mockDataMarketState = Types.MarketState.STATE_SETTLED;
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText('This market has been settled')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display optionally successor volume', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
positionDecimalPlaces: 3,
|
||||
};
|
||||
mockCandles = {
|
||||
oneDayCandles: [
|
||||
{ volume: 123 },
|
||||
{ volume: 456 },
|
||||
{ volume: 789 },
|
||||
{ volume: 99999 },
|
||||
],
|
||||
};
|
||||
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText('has a 24h trading volume of 101.367', {
|
||||
exact: false,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display optionally duration', () => {
|
||||
jest
|
||||
.spyOn(allUtils, 'getMarketExpiryDate')
|
||||
.mockReturnValue(
|
||||
new Date(Date.now() + 24 * 60 * 60 * 1000 + 60 * 1000)
|
||||
);
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText(/^This market expires in 1 day/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,146 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { isBefore, formatDuration, intervalToDuration } from 'date-fns';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
useCandles,
|
||||
useMarketState,
|
||||
useSuccessorMarket,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getMarketExpiryDate,
|
||||
isNumeric,
|
||||
} from '@vegaprotocol/utils';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const getExpiryDate = (tags: string[], close?: string): Date | null => {
|
||||
const expiryDate = getMarketExpiryDate(tags);
|
||||
return expiryDate || (close && new Date(close)) || null;
|
||||
};
|
||||
|
||||
export const MarketSuccessorBanner = ({
|
||||
market,
|
||||
}: {
|
||||
market: Market | null;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { data: marketState } = useMarketState(market?.id);
|
||||
const isSettled = marketState === Types.MarketState.STATE_SETTLED;
|
||||
const { data: successorData, loading } = useSuccessorMarket(market?.id);
|
||||
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
const expiry = market
|
||||
? getExpiryDate(
|
||||
market.tradableInstrument.instrument.metadata.tags || [],
|
||||
market.marketTimestamps.close
|
||||
)
|
||||
: null;
|
||||
|
||||
const duration =
|
||||
expiry && isBefore(new Date(), expiry)
|
||||
? intervalToDuration({ start: new Date(), end: expiry })
|
||||
: null;
|
||||
|
||||
const { oneDayCandles } = useCandles({
|
||||
marketId: successorData?.id,
|
||||
});
|
||||
|
||||
const candleVolume = oneDayCandles?.length
|
||||
? calcCandleVolume(oneDayCandles)
|
||||
: null;
|
||||
|
||||
const successorVolume =
|
||||
candleVolume && isNumeric(successorData?.positionDecimalPlaces)
|
||||
? addDecimalsFormatNumber(
|
||||
candleVolume,
|
||||
successorData?.positionDecimalPlaces as number
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!loading && (isSettled || successorData) && visible) {
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Primary}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="uppercase">
|
||||
{successorData
|
||||
? t('This market has been succeeded')
|
||||
: t('This market has been settled')}
|
||||
</div>
|
||||
{(duration || successorData) && (
|
||||
<div className="mt-1">
|
||||
{duration && (
|
||||
<span>
|
||||
{t('This market expires in {{duration}}.', {
|
||||
duration: formatDuration(duration, {
|
||||
format: [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
],
|
||||
}),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{successorData && (
|
||||
<>
|
||||
{' '}
|
||||
{successorVolume ? (
|
||||
<Trans
|
||||
defaults="The successor market <0>{{instrumentName}}</0> has a 24h trading volume of {{successorVolume}}"
|
||||
values={{
|
||||
successorVolume,
|
||||
instrumentName:
|
||||
successorData?.tradableInstrument.instrument.name,
|
||||
}}
|
||||
components={[
|
||||
<ExternalLink
|
||||
href={`/#/markets/${successorData?.id}`}
|
||||
key="link"
|
||||
>
|
||||
successor market name
|
||||
</ExternalLink>,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
defaults="The successor market is <0>{{instrumentName}}</0>"
|
||||
values={{
|
||||
instrumentName:
|
||||
successorData?.tradableInstrument.instrument.name,
|
||||
}}
|
||||
components={[
|
||||
<ExternalLink
|
||||
href={`/#/markets/${successorData?.id}`}
|
||||
key="link"
|
||||
>
|
||||
successor market name
|
||||
</ExternalLink>,
|
||||
]}
|
||||
ns={ns}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</NotificationBanner>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -1,164 +1,77 @@
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import type { SingleExecutionResult } from '@apollo/client';
|
||||
import type { MockedResponse } from '@apollo/react-testing';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
||||
import type { MarketViewProposalsQuery } from '@vegaprotocol/proposals';
|
||||
import { MarketViewProposalsDocument } from '@vegaprotocol/proposals';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
|
||||
const marketProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
request: {
|
||||
query: MarketViewProposalsDocument,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
const proposal = {
|
||||
__typename: 'Proposal' as const,
|
||||
id: 'proposal-1',
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms' as const,
|
||||
closingDatetime: '2023-09-27',
|
||||
enactmentDatetime: '2023-09-28',
|
||||
change: {
|
||||
__typename: 'NewMarket' as const,
|
||||
instrument: {
|
||||
name: 'New proposal of the market successor',
|
||||
},
|
||||
successorConfiguration: {
|
||||
parentMarketId: 'marketId',
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Proposal',
|
||||
id: 'proposal-1',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
closingDatetime: '2023-09-27',
|
||||
enactmentDatetime: '2023-09-28',
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
instrument: {
|
||||
name: 'New proposal of the market successor',
|
||||
},
|
||||
successorConfiguration: {
|
||||
parentMarketId: 'marketId',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const proposal2 = {
|
||||
__typename: 'Proposal' as const,
|
||||
id: 'proposal-2',
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms' as const,
|
||||
closingDatetime: '2023-09-27',
|
||||
enactmentDatetime: '2023-09-28',
|
||||
change: {
|
||||
__typename: 'NewMarket' as const,
|
||||
instrument: {
|
||||
name: 'New second proposal of the market successor',
|
||||
},
|
||||
successorConfiguration: {
|
||||
parentMarketId: 'marketId',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('MarketSuccessorProposalBanner', () => {
|
||||
it('should display single proposal', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={[marketProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('A successors to this market has been proposed')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it('should display single proposal', () => {
|
||||
render(<MarketSuccessorProposalBanner proposals={[proposal]} />);
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByRole('link')
|
||||
.getAttribute('href')
|
||||
?.endsWith('/proposals/proposal-1') ?? false
|
||||
).toBe(true);
|
||||
});
|
||||
it('should display plural proposals', async () => {
|
||||
const dualProposalMock = {
|
||||
...marketProposalMock,
|
||||
result: {
|
||||
...marketProposalMock.result,
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
...((
|
||||
marketProposalMock?.result as SingleExecutionResult<MarketViewProposalsQuery>
|
||||
)?.data?.proposalsConnection?.edges ?? []),
|
||||
{
|
||||
node: {
|
||||
__typename: 'Proposal',
|
||||
id: 'proposal-2',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
instrument: {
|
||||
name: 'New second proposal of the market successor',
|
||||
},
|
||||
successorConfiguration: {
|
||||
parentMarketId: 'marketId',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
screen.getByText('A successor to this market has been proposed')
|
||||
).toBeInTheDocument();
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[dualProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
expect(screen.getByRole('link')).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(proposal.id)
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('Successors to this market have been proposed')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display plural proposals', () => {
|
||||
const proposals = [proposal, proposal2];
|
||||
render(<MarketSuccessorProposalBanner proposals={proposals} />);
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('link')[0]
|
||||
.getAttribute('href')
|
||||
?.endsWith('/proposals/proposal-1') ?? false
|
||||
).toBe(true);
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('link')[1]
|
||||
.getAttribute('href')
|
||||
?.endsWith('/proposals/proposal-2') ?? false
|
||||
).toBe(true);
|
||||
});
|
||||
screen.getByText('Successors to this market have been proposed')
|
||||
).toBeInTheDocument();
|
||||
|
||||
it('banner should be hidden because no proposals', () => {
|
||||
const { container } = render(
|
||||
<MockedProvider>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
const links = screen.getAllByRole('link');
|
||||
expect(links).toHaveLength(proposals.length);
|
||||
expect(links[0]).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(proposal.id)
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('banner should be hidden because no proposals for the market', () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={[marketProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="otherMarketId" />
|
||||
</MockedProvider>
|
||||
expect(links[1]).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(proposal2.id)
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('banner should be hidden after user close click', async () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={[marketProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('A successors to this market has been proposed')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
await act(() => {
|
||||
screen.getByTestId('notification-banner-close').click();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,89 +1,51 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import {
|
||||
marketViewProposalsDataProvider,
|
||||
type NewMarketSuccessorFieldsFragment,
|
||||
} from '@vegaprotocol/proposals';
|
||||
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Fragment } from 'react';
|
||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
export const MarketSuccessorProposalBanner = ({
|
||||
marketId,
|
||||
proposals,
|
||||
}: {
|
||||
marketId?: string;
|
||||
proposals: MarketViewProposalFieldsFragment[];
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { data: proposals } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip: !marketId,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
},
|
||||
});
|
||||
|
||||
const successors =
|
||||
proposals?.filter((item) => {
|
||||
if (item.terms.change.__typename === 'NewMarket') {
|
||||
const newMarket = item.terms.change;
|
||||
if (
|
||||
newMarket.successorConfiguration?.parentMarketId === marketId &&
|
||||
item.state === Types.ProposalState.STATE_OPEN
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}) ?? [];
|
||||
const [visible, setVisible] = useState(true);
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
if (visible && successors.length) {
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Primary}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="uppercase mb-1">
|
||||
{successors.length === 1
|
||||
? t('A successors to this market has been proposed')
|
||||
: t('Successors to this market have been proposed')}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'checkOutProposalsAndVote',
|
||||
'Check out the terms of the proposals and vote:',
|
||||
{
|
||||
count: successors.length,
|
||||
}
|
||||
)}{' '}
|
||||
{successors.map((item, i) => {
|
||||
const externalLink = tokenLink(
|
||||
TOKEN_PROPOSAL.replace(':id', item.id || '')
|
||||
);
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<ExternalLink href={externalLink} key={i}>
|
||||
{
|
||||
(item.terms?.change as NewMarketSuccessorFieldsFragment)
|
||||
?.instrument.name
|
||||
}
|
||||
</ExternalLink>
|
||||
{i < successors.length - 1 && ', '}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</NotificationBanner>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="uppercase mb-1">
|
||||
{proposals.length === 1
|
||||
? t('A successor to this market has been proposed')
|
||||
: t('Successors to this market have been proposed')}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'checkOutProposalsAndVote',
|
||||
'Check out the terms of the proposals and vote:',
|
||||
{
|
||||
count: proposals.length,
|
||||
}
|
||||
)}{' '}
|
||||
{proposals.map((item, i) => {
|
||||
if (item.terms.change.__typename !== 'NewMarket') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const externalLink = tokenLink(
|
||||
TOKEN_PROPOSAL.replace(':id', item.id || '')
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<ExternalLink href={externalLink} key={i}>
|
||||
{item.terms.change.instrument.name}
|
||||
</ExternalLink>
|
||||
{i < proposals.length - 1 && ', '}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
export const MarketSuspendedBanner = () => {
|
||||
const t = useT();
|
||||
return <p>{t('Market was suspended by governance')}</p>;
|
||||
};
|
||||
@@ -1,230 +0,0 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { MarketViewProposalsQuery } from '@vegaprotocol/proposals';
|
||||
import { MarketViewProposalsDocument } from '@vegaprotocol/proposals';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { MarketTerminationBanner } from './market-termination-banner';
|
||||
|
||||
const marketMock = {
|
||||
id: 'market-1',
|
||||
decimalPlaces: 3,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'tDAI',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Market;
|
||||
|
||||
const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
request: {
|
||||
query: MarketViewProposalsDocument,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '1',
|
||||
state: Types.ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-09-30T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-1',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market one name',
|
||||
code: 'Market one',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '2',
|
||||
state: Types.ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-10-01T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-2',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market two name',
|
||||
code: 'Market two',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const openProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
request: {
|
||||
query: MarketViewProposalsDocument,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '3',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-10-01T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-3',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market three name',
|
||||
code: 'Market three',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '4',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-10-11T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-3',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market three name',
|
||||
code: 'Market three',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '5',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-10-01T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-4',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market four name',
|
||||
code: 'Market four',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const mocks: MockedResponse[] = [passedProposalMock, openProposalMock];
|
||||
|
||||
describe('MarketTerminationBanner', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-09-28T10:10:10.000Z'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should be properly rendered', async () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MarketTerminationBanner market={marketMock} />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container).not.toBeEmptyDOMElement();
|
||||
});
|
||||
expect(
|
||||
screen.getByTestId('termination-warning-banner-market-1')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render link to proposals', async () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MarketTerminationBanner market={{ ...marketMock, id: 'market-3' }} />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container).not.toBeEmptyDOMElement();
|
||||
});
|
||||
expect(screen.getByText('View proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render link to proposal', async () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MarketTerminationBanner market={{ ...marketMock, id: 'market-4' }} />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container).not.toBeEmptyDOMElement();
|
||||
});
|
||||
expect(screen.getByText('View proposal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { MarketUpdateBanner } from './market-update-banner';
|
||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
|
||||
describe('MarketUpdateBanner', () => {
|
||||
const change = {
|
||||
__typename: 'UpdateMarket' as const,
|
||||
marketId: 'update-market-id',
|
||||
};
|
||||
|
||||
const openProposal = {
|
||||
__typename: 'Proposal' as const,
|
||||
id: 'proposal-1',
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms' as const,
|
||||
closingDatetime: '2023-09-27',
|
||||
enactmentDatetime: '2023-09-28',
|
||||
change,
|
||||
},
|
||||
};
|
||||
const passedProposal = {
|
||||
__typename: 'Proposal' as const,
|
||||
id: 'proposal-2',
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms' as const,
|
||||
closingDatetime: '2023-09-27',
|
||||
enactmentDatetime: '2023-09-28',
|
||||
change,
|
||||
},
|
||||
};
|
||||
|
||||
it('renders content for a single open proposal', () => {
|
||||
render(
|
||||
<MarketUpdateBanner
|
||||
proposals={[openProposal as MarketViewProposalFieldsFragment]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/^changes have been proposed/i)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'View proposal' })).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(openProposal.id as string)
|
||||
);
|
||||
});
|
||||
|
||||
it('renders content for a single passed proposal', () => {
|
||||
render(
|
||||
<MarketUpdateBanner
|
||||
proposals={[passedProposal as MarketViewProposalFieldsFragment]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/^proposal set to change market/i)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'View proposal' })).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(passedProposal.id)
|
||||
);
|
||||
});
|
||||
|
||||
it('renders content for multiple passed proposals', () => {
|
||||
const proposals = [
|
||||
openProposal,
|
||||
openProposal,
|
||||
] as MarketViewProposalFieldsFragment[];
|
||||
|
||||
render(<MarketUpdateBanner proposals={proposals} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
new RegExp(`^There are ${proposals.length} open proposals`)
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'View proposals' })
|
||||
).toHaveAttribute('href', expect.stringContaining('proposals'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
DApp,
|
||||
TOKEN_PROPOSAL,
|
||||
TOKEN_PROPOSALS,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
export const MarketUpdateBanner = ({
|
||||
proposals,
|
||||
}: {
|
||||
proposals: MarketViewProposalFieldsFragment[];
|
||||
}) => {
|
||||
const governanceLink = useLinks(DApp.Governance);
|
||||
const t = useT();
|
||||
const openProposals = sortBy(
|
||||
proposals.filter((p) => p.state === ProposalState.STATE_OPEN),
|
||||
(p) => p.terms.enactmentDatetime
|
||||
);
|
||||
const passedProposals = sortBy(
|
||||
proposals.filter((p) => p.state === ProposalState.STATE_PASSED),
|
||||
(p) => p.terms.enactmentDatetime
|
||||
);
|
||||
|
||||
let content = null;
|
||||
|
||||
if (openProposals.length > 1) {
|
||||
content = (
|
||||
<p>
|
||||
{t('There are {{count}} open proposals to change this market', {
|
||||
count: openProposals.length,
|
||||
})}
|
||||
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)}>
|
||||
{t('View proposals')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
);
|
||||
} else if (passedProposals.length) {
|
||||
const proposal = passedProposals[0];
|
||||
const proposalLink = governanceLink(
|
||||
TOKEN_PROPOSAL.replace(':id', proposal?.id || '')
|
||||
);
|
||||
|
||||
content = (
|
||||
<p>
|
||||
{t('Proposal set to change market on {{date}}.', {
|
||||
date: format(new Date(proposal.terms.enactmentDatetime), 'dd MMMM'),
|
||||
})}
|
||||
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>,
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
const proposal = openProposals[0];
|
||||
const proposalLink = governanceLink(
|
||||
TOKEN_PROPOSAL.replace(':id', proposal?.id || '')
|
||||
);
|
||||
content = (
|
||||
<p>
|
||||
{t('Changes have been proposed for this market.')}{' '}
|
||||
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return <div data-testid="market-proposal-notification">{content}</div>;
|
||||
};
|
||||
+107
-154
@@ -1,44 +1,120 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { format, formatDuration, intervalToDuration } from 'date-fns';
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { marketViewProposalsDataProvider } from '@vegaprotocol/proposals';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { getQuoteName } from '@vegaprotocol/markets';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { type ReactNode } from 'react';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { format, formatDuration, intervalToDuration } from 'date-fns';
|
||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import {
|
||||
DApp,
|
||||
TOKEN_PROPOSAL,
|
||||
TOKEN_PROPOSALS,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { getQuoteName, type Market } from '@vegaprotocol/markets';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const filterProposals = (
|
||||
data: MarketViewProposalFieldsFragment[] | null,
|
||||
marketId: string,
|
||||
now: number
|
||||
) =>
|
||||
sortBy(
|
||||
(data || []).filter(
|
||||
(item) =>
|
||||
item.terms.change.__typename === 'UpdateMarketState' &&
|
||||
item.terms.change.market.id === marketId &&
|
||||
item.terms.change.updateType ===
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
|
||||
item.terms.enactmentDatetime &&
|
||||
new Date(item.terms.enactmentDatetime).getTime() > now
|
||||
),
|
||||
(item) => item.terms.enactmentDatetime
|
||||
export const MarketUpdateStateBanner = ({
|
||||
market,
|
||||
proposals,
|
||||
}: {
|
||||
market: Market;
|
||||
proposals: MarketViewProposalFieldsFragment[];
|
||||
}) => {
|
||||
const t = useT();
|
||||
const governanceLink = useLinks(DApp.Governance);
|
||||
|
||||
const openProposals = sortBy(
|
||||
proposals.filter((p) => p.state === ProposalState.STATE_OPEN),
|
||||
(p) => p.terms.enactmentDatetime
|
||||
);
|
||||
const passedProposals = sortBy(
|
||||
proposals.filter((p) => p.state === ProposalState.STATE_PASSED),
|
||||
(p) => p.terms.enactmentDatetime
|
||||
);
|
||||
|
||||
if (!passedProposals.length && !openProposals.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = market.tradableInstrument.instrument.code;
|
||||
const assetSymbol = getQuoteName(market);
|
||||
|
||||
const proposalLink =
|
||||
!passedProposals.length && openProposals[0]?.id
|
||||
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
|
||||
: undefined;
|
||||
|
||||
const proposalsLink =
|
||||
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
|
||||
|
||||
let content: ReactNode;
|
||||
|
||||
if (passedProposals.length) {
|
||||
const { date, duration, price } = getMessageVariables(passedProposals[0]);
|
||||
content = (
|
||||
<>
|
||||
<p className="uppercase mb-1">
|
||||
{t('Trading on market {{name}} will stop on {{date}}', {
|
||||
name,
|
||||
date,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'You will no longer be able to hold a position on this market when it closes in {{duration}}.',
|
||||
{ duration }
|
||||
)}{' '}
|
||||
{price &&
|
||||
assetSymbol &&
|
||||
t('The final price will be {{price}} {{assetSymbol}}.', {
|
||||
price: addDecimalsFormatNumber(price, market.decimalPlaces),
|
||||
assetSymbol,
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
} else if (openProposals.length > 1) {
|
||||
content = (
|
||||
<>
|
||||
<p className="uppercase mb-1">
|
||||
{t(
|
||||
'Trading on market {{name}} may stop. There are open proposals to close this market',
|
||||
{ name }
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<ExternalLink href={proposalsLink}>
|
||||
{t('View proposals')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const { date, price } = getMessageVariables(openProposals[0]);
|
||||
content = (
|
||||
<>
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'Trading on market {{name}} may stop on {{date}}. There is an open proposal to close this market.',
|
||||
{ name, date }
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{price &&
|
||||
assetSymbol &&
|
||||
t('Proposed final price is {{price}} {{assetSymbol}}.', {
|
||||
price: addDecimalsFormatNumber(price, market.decimalPlaces),
|
||||
assetSymbol,
|
||||
})}{' '}
|
||||
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <div data-testid={`update-state-banner-${market.id}`}>{content}</div>;
|
||||
};
|
||||
|
||||
const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
|
||||
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
|
||||
@@ -62,126 +138,3 @@ const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
|
||||
price,
|
||||
};
|
||||
};
|
||||
|
||||
export const MarketTerminationBanner = ({
|
||||
market,
|
||||
}: {
|
||||
market: Market | null;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const [visible, setVisible] = useState(true);
|
||||
const skip = !market || !visible;
|
||||
const { data: passedProposalsData } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: openProposalsData } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const governanceLink = useLinks(DApp.Governance);
|
||||
|
||||
if (!market) return null;
|
||||
const now = Date.now();
|
||||
const passedProposals = filterProposals(passedProposalsData, market.id, now);
|
||||
const openProposals = filterProposals(openProposalsData, market.id, now);
|
||||
|
||||
const name = market.tradableInstrument.instrument.code;
|
||||
if (!passedProposals.length && !openProposals.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetSymbol = getQuoteName(market);
|
||||
const proposalLink =
|
||||
!passedProposals.length && openProposals[0]?.id
|
||||
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
|
||||
: undefined;
|
||||
const proposalsLink =
|
||||
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
|
||||
let content: ReactNode;
|
||||
if (passedProposals.length) {
|
||||
const { date, duration, price } = getMessageVariables(passedProposals[0]);
|
||||
content = (
|
||||
<>
|
||||
<div className="uppercase mb-1">
|
||||
{t('Trading on Market {{name}} will stop on {{date}}', {
|
||||
name,
|
||||
date,
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'You will no longer be able to hold a position on this market when it closes in {{duration}}.',
|
||||
{ duration }
|
||||
)}{' '}
|
||||
{price &&
|
||||
assetSymbol &&
|
||||
t('The final price will be {{price}} {{assetSymbol}}.', {
|
||||
price: addDecimalsFormatNumber(price, market.decimalPlaces),
|
||||
assetSymbol,
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else if (openProposals.length > 1) {
|
||||
content = (
|
||||
<>
|
||||
<div className="uppercase mb-1">
|
||||
{t(
|
||||
'Trading on Market {{name}} may stop. There are open proposals to close this market',
|
||||
{ name }
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<ExternalLink href={proposalsLink}>
|
||||
{t('View proposals')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const { date, price } = getMessageVariables(openProposals[0]);
|
||||
content = (
|
||||
<>
|
||||
<div className="uppercase mb-1">
|
||||
{t(
|
||||
'Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.',
|
||||
{ name, date }
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{price &&
|
||||
assetSymbol &&
|
||||
t('Proposed final price is {{price}} {{assetSymbol}}.', {
|
||||
price: addDecimalsFormatNumber(price, market.decimalPlaces),
|
||||
assetSymbol,
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={openProposals.length ? Intent.Warning : Intent.Info}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
data-testid={`termination-warning-banner-${market.id}`}
|
||||
>
|
||||
{content}
|
||||
</NotificationBanner>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
useMarketProposals,
|
||||
type MarketViewProposalFieldsFragment,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { ProposalState, ProposalType } from '@vegaprotocol/types';
|
||||
|
||||
const isPending = (p: MarketViewProposalFieldsFragment) => {
|
||||
return [ProposalState.STATE_OPEN, ProposalState.STATE_PASSED].includes(
|
||||
p.state
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdateMarketStateProposals = (
|
||||
marketId: string,
|
||||
inState?: ProposalState
|
||||
) => {
|
||||
const { data, error, loading } = useMarketProposals({
|
||||
proposalType: ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
inState,
|
||||
});
|
||||
|
||||
const proposals = data
|
||||
? data.filter(isPending).filter((p) => {
|
||||
const change = p.terms.change;
|
||||
|
||||
if (
|
||||
change.__typename === 'UpdateMarketState' &&
|
||||
change.market.id === marketId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
: [];
|
||||
|
||||
return { data, error, loading, proposals };
|
||||
};
|
||||
|
||||
export const useUpdateMarketProposals = (
|
||||
marketId: string,
|
||||
inState?: ProposalState
|
||||
) => {
|
||||
const { data, error, loading } = useMarketProposals({
|
||||
proposalType: ProposalType.TYPE_UPDATE_MARKET,
|
||||
inState,
|
||||
});
|
||||
|
||||
const proposals = data
|
||||
? data.filter(isPending).filter((p) => {
|
||||
const change = p.terms.change;
|
||||
if (
|
||||
change.__typename === 'UpdateMarket' &&
|
||||
change.marketId === marketId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
: [];
|
||||
|
||||
return { data, error, loading, proposals };
|
||||
};
|
||||
|
||||
export const useSuccessorMarketProposals = (
|
||||
marketId: string,
|
||||
inState?: ProposalState
|
||||
) => {
|
||||
const { data, loading, error } = useMarketProposals({
|
||||
proposalType: ProposalType.TYPE_NEW_MARKET,
|
||||
inState,
|
||||
});
|
||||
|
||||
const proposals = data
|
||||
? data.filter(isPending).filter((p) => {
|
||||
const change = p.terms.change;
|
||||
if (
|
||||
change.__typename === 'NewMarket' &&
|
||||
change.successorConfiguration?.parentMarketId === marketId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
: [];
|
||||
|
||||
return { data, error, loading, proposals };
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export const MarketHeader = () => {
|
||||
title={
|
||||
<Popover
|
||||
open={open}
|
||||
onChange={setOpen}
|
||||
onOpenChange={setOpen}
|
||||
trigger={
|
||||
<HeaderTitle>
|
||||
<span>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import {
|
||||
FilterStatusValue,
|
||||
STORAGE_KEY,
|
||||
useOrderListGridState,
|
||||
} from './orders-container';
|
||||
import { STORAGE_KEY, useOrderListGridState } from './orders-container';
|
||||
import { Filter } from '@vegaprotocol/orders';
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
|
||||
@@ -16,31 +12,6 @@ describe('useOrderListGridState', () => {
|
||||
return renderHook(() => useOrderListGridState(filter));
|
||||
};
|
||||
|
||||
it.each(Object.values(Filter))(
|
||||
'providers correct AgGrid filter for %s',
|
||||
(filter) => {
|
||||
const { result } = setup(filter);
|
||||
expect(typeof result.current.updateGridState).toBe('function');
|
||||
expect(result.current.gridState).toEqual({
|
||||
columnState: undefined,
|
||||
filterModel: {
|
||||
status: {
|
||||
value: FilterStatusValue[filter],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('provides correct AgGrid filter for all', () => {
|
||||
const { result } = setup(undefined);
|
||||
expect(typeof result.current.updateGridState).toBe('function');
|
||||
expect(result.current.gridState).toEqual({
|
||||
columnState: undefined,
|
||||
filterModel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(Object.values(Filter))(
|
||||
'sets and stores column state and filters for %s',
|
||||
(filter) => {
|
||||
@@ -59,12 +30,7 @@ describe('useOrderListGridState', () => {
|
||||
|
||||
expect(result.current.gridState).toEqual({
|
||||
columnState: undefined,
|
||||
filterModel: {
|
||||
...filterModel,
|
||||
status: {
|
||||
value: FilterStatusValue[filter],
|
||||
},
|
||||
},
|
||||
filterModel,
|
||||
});
|
||||
|
||||
const columnState = [{ colId: 'status', width: 200 }];
|
||||
@@ -77,12 +43,7 @@ describe('useOrderListGridState', () => {
|
||||
|
||||
expect(result.current.gridState).toEqual({
|
||||
columnState,
|
||||
filterModel: {
|
||||
...filterModel,
|
||||
status: {
|
||||
value: FilterStatusValue[filter],
|
||||
},
|
||||
},
|
||||
filterModel,
|
||||
});
|
||||
|
||||
const storeKeyMap = {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
||||
import { OrderStatus } from '@vegaprotocol/types';
|
||||
import { Links } from '../../lib/links';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { GridSettings } from '../grid-settings/grid-settings';
|
||||
|
||||
const resolveNoRowsMessage = (
|
||||
filter: Filter | undefined,
|
||||
@@ -38,6 +39,24 @@ export const FilterStatusValue = {
|
||||
[Filter.Rejected]: [OrderStatus.STATUS_REJECTED],
|
||||
};
|
||||
|
||||
export const DefaultFilterModel = {
|
||||
[Filter.Open]: {
|
||||
status: {
|
||||
value: FilterStatusValue[Filter.Open],
|
||||
},
|
||||
},
|
||||
[Filter.Closed]: {
|
||||
status: {
|
||||
value: FilterStatusValue[Filter.Closed],
|
||||
},
|
||||
},
|
||||
[Filter.Rejected]: {
|
||||
status: {
|
||||
value: FilterStatusValue[Filter.Rejected],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export interface OrderContainerProps {
|
||||
filter?: Filter;
|
||||
}
|
||||
@@ -54,7 +73,8 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
|
||||
(newState) => {
|
||||
updateGridState(filter, newState);
|
||||
},
|
||||
AUTO_SIZE_COLUMNS
|
||||
AUTO_SIZE_COLUMNS,
|
||||
filter && DefaultFilterModel[filter]
|
||||
);
|
||||
|
||||
if (!pubKey) {
|
||||
@@ -80,7 +100,7 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
|
||||
};
|
||||
|
||||
export const STORAGE_KEY = 'vega_order_list_store';
|
||||
const useOrderListStore = create<{
|
||||
export const useOrderListStore = create<{
|
||||
open: DataGridStore;
|
||||
closed: DataGridStore;
|
||||
rejected: DataGridStore;
|
||||
@@ -149,34 +169,19 @@ export const useOrderListGridState = (filter: Filter | undefined) => {
|
||||
case Filter.Open: {
|
||||
return {
|
||||
columnState: store.open.columnState,
|
||||
filterModel: {
|
||||
...store.open.filterModel,
|
||||
status: {
|
||||
value: FilterStatusValue[Filter.Open],
|
||||
},
|
||||
},
|
||||
filterModel: store.open.filterModel,
|
||||
};
|
||||
}
|
||||
case Filter.Closed: {
|
||||
return {
|
||||
columnState: store.closed.columnState,
|
||||
filterModel: {
|
||||
...store.closed.filterModel,
|
||||
status: {
|
||||
value: FilterStatusValue[Filter.Closed],
|
||||
},
|
||||
},
|
||||
filterModel: store.closed.filterModel,
|
||||
};
|
||||
}
|
||||
case Filter.Rejected: {
|
||||
return {
|
||||
columnState: store.rejected.columnState,
|
||||
filterModel: {
|
||||
...store.rejected.filterModel,
|
||||
status: {
|
||||
value: FilterStatusValue[Filter.Rejected],
|
||||
},
|
||||
},
|
||||
filterModel: store.rejected.filterModel,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
@@ -187,3 +192,14 @@ export const useOrderListGridState = (filter: Filter | undefined) => {
|
||||
|
||||
return { gridState, updateGridState };
|
||||
};
|
||||
|
||||
export const OrdersSettings = ({ filter }: { filter?: Filter }) => {
|
||||
const updateGridState = useOrderListStore((state) => state.update);
|
||||
return (
|
||||
<GridSettings
|
||||
updateGridStore={(gridStore: DataGridStore) =>
|
||||
updateGridState(filter, gridStore)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './positions-container';
|
||||
export * from './positions-settings';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../grid-settings/grid-settings';
|
||||
import { usePositionsStore } from './positions-container';
|
||||
|
||||
export const PositionsSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={usePositionsStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -69,11 +69,13 @@ describe('RewardPot', () => {
|
||||
balance: '100',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
// should include this in total:
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
balance: '100',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
// should include this in total:
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
balance: '50',
|
||||
@@ -138,20 +140,20 @@ describe('RewardPot', () => {
|
||||
|
||||
renderComponent(props);
|
||||
|
||||
expect(screen.getByTestId('total-rewards')).toHaveTextContent(
|
||||
`7.00 ${rewardAsset.symbol}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Locked/).nextElementSibling).toHaveTextContent(
|
||||
'2.50'
|
||||
);
|
||||
expect(screen.getByText(/Vesting/).nextElementSibling).toHaveTextContent(
|
||||
'4.50'
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/Available to withdraw/).nextElementSibling
|
||||
).toHaveTextContent('1.50');
|
||||
|
||||
// should be sum of the above
|
||||
expect(screen.getByTestId('total-rewards')).toHaveTextContent(
|
||||
`8.50 ${rewardAsset.symbol}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ export const RewardsContainer = () => {
|
||||
},
|
||||
// Inclusion of activity streak in query currently fails
|
||||
errorPolicy: 'ignore',
|
||||
// polling here so that as rewards are are moved to ACCOUNT_TYPE_VESTED_REWARDS the vesting stats information stays
|
||||
// almost up to sync with accounts updating from subscriptions. There is a chance the data could be out
|
||||
// of sync for 10s if you happen to be on the page at the end of an epoch
|
||||
pollInterval: 10000,
|
||||
});
|
||||
|
||||
if (!epochData?.epoch || !assetMap) return null;
|
||||
@@ -227,6 +231,7 @@ export const RewardsContainer = () => {
|
||||
title={t('Rewards history')}
|
||||
className="lg:col-span-full"
|
||||
loading={rewardsLoading}
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<RewardsHistoryContainer
|
||||
epoch={Number(epochData?.epoch.id)}
|
||||
@@ -295,7 +300,9 @@ export const RewardPot = ({
|
||||
: [0];
|
||||
const totalVesting = BigNumber.sum.apply(null, vestingBalances);
|
||||
|
||||
const totalRewards = totalLocked.plus(totalVesting);
|
||||
const totalRewards = totalLocked
|
||||
.plus(totalVesting)
|
||||
.plus(totalVestedRewardsByRewardAsset);
|
||||
|
||||
let rewardAsset = undefined;
|
||||
|
||||
|
||||
@@ -99,16 +99,25 @@ describe('RewardsHistoryTable', () => {
|
||||
it('renders table with accounts summed up by asset', () => {
|
||||
render(<RewardHistoryTable {...props} />);
|
||||
|
||||
const container = within(
|
||||
const containerLeft = within(
|
||||
document.querySelector('.ag-pinned-left-cols-container') as HTMLElement
|
||||
);
|
||||
const pinnedRows = containerLeft.getAllByRole('row');
|
||||
|
||||
const containerCenter = within(
|
||||
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||
);
|
||||
const rows = container.getAllByRole('row');
|
||||
const rows = containerCenter.getAllByRole('row');
|
||||
expect(rows).toHaveLength(
|
||||
Object.keys(groupBy(rewardSummaries, 'node.assetId')).length
|
||||
);
|
||||
|
||||
let row = within(rows[0]);
|
||||
let cells = row.getAllByRole('gridcell');
|
||||
let pinnedRow = within(pinnedRows[0]);
|
||||
let cells = [
|
||||
...pinnedRow.getAllByRole('gridcell'),
|
||||
...row.getAllByRole('gridcell'),
|
||||
];
|
||||
|
||||
let assetCell = getCell(cells, 'asset.symbol');
|
||||
expect(assetCell.getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
@@ -140,7 +149,11 @@ describe('RewardsHistoryTable', () => {
|
||||
|
||||
// Second row
|
||||
row = within(rows[1]);
|
||||
cells = row.getAllByRole('gridcell');
|
||||
pinnedRow = within(pinnedRows[1]);
|
||||
cells = [
|
||||
...pinnedRow.getAllByRole('gridcell'),
|
||||
...row.getAllByRole('gridcell'),
|
||||
];
|
||||
|
||||
assetCell = getCell(cells, 'asset.symbol');
|
||||
expect(assetCell.getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
|
||||
@@ -93,6 +93,7 @@ export const RewardsHistoryContainer = ({
|
||||
|
||||
const defaultColDef = {
|
||||
flex: 1,
|
||||
minWidth: 62,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
};
|
||||
@@ -195,6 +196,8 @@ export const RewardHistoryTable = ({
|
||||
return <StackedCell primary={value} secondary={data.asset.name} />;
|
||||
},
|
||||
sort: 'desc',
|
||||
pinned: 'left',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'infrastructureFees',
|
||||
@@ -257,7 +260,7 @@ export const RewardHistoryTable = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<h4 className="text-muted flex items-center gap-2 text-sm">
|
||||
<label htmlFor="fromEpoch">{t('From epoch')}</label>
|
||||
<EpochInput
|
||||
@@ -329,15 +332,17 @@ export const RewardHistoryTable = ({
|
||||
</TradingButton>
|
||||
</div>
|
||||
</div>
|
||||
<AgGrid
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
rowData={rowData}
|
||||
rowHeight={45}
|
||||
domLayout="autoHeight"
|
||||
// Show loading message without wiping out the current rows
|
||||
overlayNoRowsTemplate={loading ? t('Loading...') : t('No rows')}
|
||||
/>
|
||||
<div className="border rounded-lg md:rounded-sm overflow-hidden border-default">
|
||||
<AgGrid
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
rowData={rowData}
|
||||
rowHeight={45}
|
||||
domLayout="autoHeight"
|
||||
// Show loading message without wiping out the current rows
|
||||
overlayNoRowsTemplate={loading ? t('Loading...') : t('No rows')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './stop-orders-container';
|
||||
|
||||
export * from './stop-orders-settings';
|
||||
|
||||
@@ -35,7 +35,7 @@ export const StopOrdersContainer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const useStopOrdersStore = create<DataGridSlice>()(
|
||||
export const useStopOrdersStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_stop_orders_store',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../grid-settings/grid-settings';
|
||||
import { useStopOrdersStore } from './stop-orders-container';
|
||||
|
||||
export const StopOrdersSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={useStopOrdersStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './table';
|
||||
+10
-6
@@ -1,13 +1,17 @@
|
||||
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef, type ReactNode, type HTMLAttributes } from 'react';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
|
||||
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
||||
export const GRADIENT =
|
||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||
|
||||
type TableColumnDefinition = {
|
||||
displayName?: ReactNode;
|
||||
name: string;
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
type TableProps = {
|
||||
@@ -42,7 +46,7 @@ export const Table = forwardRef<
|
||||
key={name}
|
||||
col-id={name}
|
||||
className={classNames(
|
||||
'px-5 py-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
|
||||
'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
|
||||
INNER_BORDER_STYLE
|
||||
)}
|
||||
>
|
||||
@@ -66,7 +70,7 @@ export const Table = forwardRef<
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'border-separate border rounded-md border-spacing-0',
|
||||
'border-separate border rounded-md border-spacing-0 overflow-hidden',
|
||||
BORDER_COLOR,
|
||||
GRADIENT,
|
||||
className
|
||||
@@ -82,10 +86,10 @@ export const Table = forwardRef<
|
||||
'max-md:flex flex-col w-full': !noCollapse,
|
||||
})}
|
||||
>
|
||||
{columns.map(({ name, displayName, className }, j) => (
|
||||
{columns.map(({ name, displayName, className, testId }, j) => (
|
||||
<td
|
||||
className={classNames(
|
||||
'px-5 py-3 text-base',
|
||||
'px-5 py-3',
|
||||
{
|
||||
'max-md:flex max-md:flex-col max-md:justify-between':
|
||||
!noCollapse,
|
||||
@@ -110,7 +114,7 @@ export const Table = forwardRef<
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
<span>{d[name]}</span>
|
||||
<span data-testid={`${testId || name}-${i}`}>{d[name]}</span>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
@@ -1 +1,2 @@
|
||||
export * from './trades-container';
|
||||
export * from './trades-settings';
|
||||
|
||||
@@ -17,7 +17,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||
return <TradesManager marketId={marketId} gridProps={gridStoreCallbacks} />;
|
||||
};
|
||||
|
||||
const useTradesStore = create<DataGridSlice>()(
|
||||
export const useTradesStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_trades_store',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../grid-settings/grid-settings';
|
||||
import { useTradesStore } from './trades-container';
|
||||
|
||||
export const TradesSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={useTradesStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -1,2 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||
VEGA_VERSION=v0.73.9
|
||||
VEGA_VERSION=v0.73.10
|
||||
LOCAL_SERVER=false
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
|
||||
VEGA_VERSION=v0.73.9
|
||||
VEGA_VERSION=v0.73.10
|
||||
LOCAL_SERVER=false
|
||||
@@ -1,2 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
|
||||
VEGA_VERSION=v0.73.8
|
||||
VEGA_VERSION=v0.73.10
|
||||
LOCAL_SERVER=false
|
||||
@@ -24,6 +24,12 @@ poetry shell
|
||||
|
||||
5. **Install python dependencies**
|
||||
|
||||
To make sure you are on the latest version of our market-sim branch.
|
||||
|
||||
```bash
|
||||
poetry update vega-sim
|
||||
```
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
@@ -121,11 +127,7 @@ yarn nx serve trading
|
||||
|
||||
```
|
||||
|
||||
Once console is served you can use the flag --local-server
|
||||
|
||||
```bash
|
||||
poetry run pytest -k "test_name" -s --headed --local-server
|
||||
```
|
||||
Once console is served you can update the .env file to have local_server to true.
|
||||
|
||||
## Running Tests in Parallel 🔢
|
||||
|
||||
|
||||
@@ -55,14 +55,14 @@ def change_keys(page: Page, vega: VegaServiceNull, key_name):
|
||||
page.reload()
|
||||
|
||||
|
||||
def forward_time(vega: VegaServiceNull, forward_epoch: bool = False):
|
||||
def forward_time(vega:VegaServiceNull, forward_epoch: bool = False):
|
||||
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
if forward_epoch:
|
||||
next_epoch(vega)
|
||||
|
||||
|
||||
# This is for when the element will initially load but contain an outdated value. It will wait for the element to contain the expected text, returning False after a timeout or exception
|
||||
def selector_contains_text(page: Page, selector, expected_text, timeout=5000):
|
||||
try:
|
||||
@@ -71,3 +71,4 @@ def selector_contains_text(page: Page, selector, expected_text, timeout=5000):
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
@@ -6,9 +6,13 @@ import requests
|
||||
import time
|
||||
import docker
|
||||
import http.server
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from docker.models.containers import Container
|
||||
from docker.errors import APIError
|
||||
from contextlib import contextmanager
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from vega_sim.null_service import VegaServiceNull, Ports
|
||||
from playwright.sync_api import Browser, Page
|
||||
from config import console_image_name, vega_version
|
||||
from datetime import datetime, timedelta
|
||||
@@ -19,7 +23,6 @@ from fixtures.market import (
|
||||
setup_perps_market,
|
||||
)
|
||||
|
||||
import sys
|
||||
|
||||
# Workaround for current xdist issue with displaying live logs from multiple workers
|
||||
# https://github.com/pytest-dev/pytest-xdist/issues/402
|
||||
@@ -28,6 +31,8 @@ sys.stdout = sys.stderr
|
||||
docker_client = docker.from_env()
|
||||
logger = logging.getLogger()
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
@@ -49,16 +54,24 @@ def pytest_configure(config):
|
||||
level=config.getini("log_file_level"),
|
||||
)
|
||||
|
||||
|
||||
class CustomHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
# Set the path to your website's directory here
|
||||
if self.path == '/':
|
||||
self.path = 'dist/apps/trading/exported/index.html'
|
||||
if self.path == "/":
|
||||
self.path = "dist/apps/trading/exported/index.html"
|
||||
return http.server.SimpleHTTPRequestHandler.do_GET(self)
|
||||
|
||||
|
||||
# Start VegaServiceNull
|
||||
@contextmanager
|
||||
def init_vega(request=None):
|
||||
local_server = os.getenv("LOCAL_SERVER", "false").lower() == "true"
|
||||
port_config = None
|
||||
if local_server:
|
||||
port_config = {
|
||||
Ports.DATA_NODE_REST: 8001,
|
||||
}
|
||||
default_seconds = 1
|
||||
seconds_per_block = default_seconds
|
||||
if request and hasattr(request, "param"):
|
||||
@@ -70,50 +83,49 @@ def init_vega(request=None):
|
||||
)
|
||||
logger.info(f"Using console image: {console_image_name}")
|
||||
logger.info(f"Using vega version: {vega_version}")
|
||||
with VegaServiceNull(
|
||||
run_with_console=False,
|
||||
launch_graphql=False,
|
||||
retain_log_files=True,
|
||||
use_full_vega_wallet=True,
|
||||
store_transactions=True,
|
||||
transactions_per_block=1000,
|
||||
seconds_per_block=seconds_per_block,
|
||||
genesis_time= datetime.now() - timedelta(days=1),
|
||||
) as vega:
|
||||
|
||||
vega_service_args = {
|
||||
"run_with_console": False,
|
||||
"launch_graphql": False,
|
||||
"retain_log_files": True,
|
||||
"use_full_vega_wallet": True,
|
||||
"store_transactions": True,
|
||||
"transactions_per_block": 1000,
|
||||
"seconds_per_block": seconds_per_block,
|
||||
"genesis_time": datetime.now() - timedelta(days=1),
|
||||
}
|
||||
|
||||
if port_config is not None:
|
||||
vega_service_args["port_config"] = port_config
|
||||
|
||||
with VegaServiceNull(**vega_service_args) as vega:
|
||||
try:
|
||||
container = docker_client.containers.run(
|
||||
console_image_name, detach=True, ports={"80/tcp": vega.console_port}
|
||||
)
|
||||
# docker setup
|
||||
|
||||
if not isinstance(container, Container):
|
||||
raise Exception("container instance invalid")
|
||||
|
||||
logger.info(
|
||||
f"Container {container.id} started",
|
||||
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
|
||||
)
|
||||
yield vega
|
||||
except docker.errors.APIError as e:
|
||||
except APIError as e:
|
||||
logger.info(f"Container creation failed.")
|
||||
logger.info(e)
|
||||
raise e
|
||||
finally:
|
||||
logger.info(f"Stopping container {container.id}")
|
||||
container.stop()
|
||||
# Remove the container
|
||||
logger.info(f"Removing container {container.id}")
|
||||
container.remove()
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--local-server", action="store_true", default=False,
|
||||
help="Build and serve locally instead of using a container"
|
||||
)
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def local_server(pytestconfig):
|
||||
return pytestconfig.getoption("--local-server")
|
||||
|
||||
@contextmanager
|
||||
def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRequest, local_server: bool):
|
||||
def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRequest):
|
||||
local_server = os.getenv("LOCAL_SERVER", "false").lower() == "true"
|
||||
server_port = "4200" if local_server else str(vega.console_port)
|
||||
with browser.new_context(
|
||||
viewport={"width": 1920, "height": 1080},
|
||||
@@ -125,9 +137,7 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
|
||||
attempts = 0
|
||||
while attempts < 100:
|
||||
try:
|
||||
code = requests.get(
|
||||
f"http://localhost:{server_port}/"
|
||||
).status_code
|
||||
code = requests.get(f"http://localhost:{server_port}/").status_code
|
||||
if code == 200:
|
||||
break
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
@@ -172,8 +182,8 @@ def vega(request):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page(vega, browser, request, local_server):
|
||||
with init_page(vega, browser, request, local_server) as page_instance:
|
||||
def page(vega, browser, request):
|
||||
with init_page(vega, browser, request) as page_instance:
|
||||
yield page_instance
|
||||
|
||||
|
||||
|
||||
@@ -245,3 +245,11 @@ def setup_perps_market(
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
return market_id
|
||||
|
||||
|
||||
def market_exists(vega: VegaService, market_id: str):
|
||||
if market_id is None:
|
||||
return False
|
||||
all_markets = vega.all_markets()
|
||||
market_ids = [market.id for market in all_markets]
|
||||
return market_id in market_ids
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
@@ -1161,7 +1161,7 @@ profile = ["pytest-profiling", "snakeviz"]
|
||||
type = "git"
|
||||
url = "https://github.com/vegaprotocol/vega-market-sim.git/"
|
||||
reference = "fix/genesis_panic"
|
||||
resolved_reference = "7ab04931924380db8000544b7f3d65fcb39b5467"
|
||||
resolved_reference = "de30d2d4c7a1b81a830527ca76473e23ef59de12"
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from datetime import datetime, timedelta
|
||||
from conftest import init_vega
|
||||
from fixtures.market import setup_continuous_market
|
||||
@@ -24,7 +24,7 @@ def continuous_market(vega):
|
||||
return setup_continuous_market(vega)
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_limit_buy_order_GTT(continuous_market, vega: VegaService, page: Page):
|
||||
def test_limit_buy_order_GTT(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(tif).select_option("Good 'til Time (GTT)")
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
@@ -52,7 +52,7 @@ def test_limit_buy_order_GTT(continuous_market, vega: VegaService, page: Page):
|
||||
)
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_limit_buy_order(continuous_market, vega: VegaService, page: Page):
|
||||
def test_limit_buy_order(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
@@ -69,7 +69,7 @@ def test_limit_buy_order(continuous_market, vega: VegaService, page: Page):
|
||||
)
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_limit_sell_order(continuous_market, vega: VegaService, page: Page):
|
||||
def test_limit_sell_order(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
page.get_by_test_id(order_price).fill("100")
|
||||
@@ -93,7 +93,7 @@ def test_limit_sell_order(continuous_market, vega: VegaService, page: Page):
|
||||
)
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_market_sell_order(continuous_market, vega: VegaService, page: Page):
|
||||
def test_market_sell_order(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(market_order).click()
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
@@ -117,7 +117,7 @@ def test_market_sell_order(continuous_market, vega: VegaService, page: Page):
|
||||
)
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_market_buy_order(continuous_market, vega: VegaService, page: Page):
|
||||
def test_market_buy_order(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(market_order).click()
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from actions.vega import submit_order
|
||||
from actions.utils import wait_for_toast_confirmation
|
||||
|
||||
@@ -13,7 +13,7 @@ market_trading_mode = "market-trading-mode"
|
||||
|
||||
@pytest.mark.skip("tbd")
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_margin_and_fees_estimations(continuous_market, vega: VegaService, page: Page):
|
||||
def test_margin_and_fees_estimations(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
# setup continuous trading market with one user buy trade
|
||||
market_id = continuous_market
|
||||
page.goto(f"/#/markets/{market_id}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from actions.vega import submit_order
|
||||
from datetime import datetime, timedelta
|
||||
from conftest import init_vega
|
||||
@@ -38,7 +38,7 @@ timeInForce_col = '[col-id="submission.timeInForce"]'
|
||||
updatedAt_col = '[col-id="updatedAt"]'
|
||||
close_toast = "toast-close"
|
||||
|
||||
def create_position(vega: VegaService, market_id):
|
||||
def create_position(vega: VegaServiceNull, market_id):
|
||||
submit_order(vega, "Key 1", market_id, "SIDE_SELL", 100, 110)
|
||||
submit_order(vega, "Key 1", market_id, "SIDE_BUY", 100, 110)
|
||||
vega.forward("10s")
|
||||
@@ -69,7 +69,7 @@ def test_stop_order_form_error_validation(continuous_market, page: Page):
|
||||
|
||||
@pytest.mark.skip("core issue")
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_submit_stop_order_rejected(continuous_market, vega: VegaService, page: Page):
|
||||
def test_submit_stop_order_rejected(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(stop_orders_tab).click()
|
||||
page.get_by_test_id(stop_order_btn).click()
|
||||
@@ -108,7 +108,7 @@ def test_submit_stop_order_rejected(continuous_market, vega: VegaService, page:
|
||||
@pytest.mark.skip("core issue")
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_submit_stop_market_order_triggered(
|
||||
continuous_market, vega: VegaService, page: Page
|
||||
continuous_market, vega: VegaServiceNull, page: Page
|
||||
):
|
||||
# 7002-SORD-071
|
||||
# 7002-SORD-074
|
||||
@@ -166,7 +166,7 @@ def test_submit_stop_market_order_triggered(
|
||||
@pytest.mark.skip("core issue")
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_submit_stop_limit_order_pending(
|
||||
continuous_market, vega: VegaService, page: Page
|
||||
continuous_market, vega: VegaServiceNull, page: Page
|
||||
):
|
||||
# 7002-SORD-071
|
||||
# 7002-SORD-074
|
||||
@@ -227,7 +227,7 @@ def test_submit_stop_limit_order_pending(
|
||||
@pytest.mark.skip("core issue")
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_submit_stop_limit_order_cancel(
|
||||
continuous_market, vega: VegaService, page: Page
|
||||
continuous_market, vega: VegaServiceNull, page: Page
|
||||
):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(stop_orders_tab).click()
|
||||
@@ -348,7 +348,7 @@ class TestStopOcoValidation:
|
||||
@pytest.mark.skip("core issue")
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_maximum_number_of_active_stop_orders(
|
||||
self, continuous_market, vega: VegaService, page: Page
|
||||
self, continuous_market, vega: VegaServiceNull, page: Page
|
||||
):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(stop_orders_tab).click()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from actions.vega import submit_order
|
||||
from actions.utils import wait_for_toast_confirmation
|
||||
|
||||
@@ -42,7 +42,7 @@ trigger_price_oco = "triggerPrice-oco"
|
||||
order_size_oco = "order-size-oco"
|
||||
order_limit_price_oco = "order-price-oco"
|
||||
|
||||
def create_position(vega: VegaService, market_id):
|
||||
def create_position(vega: VegaServiceNull, market_id):
|
||||
submit_order(vega, "Key 1", market_id, "SIDE_SELL", 100, 110)
|
||||
submit_order(vega, "Key 1", market_id, "SIDE_BUY", 100, 110)
|
||||
vega.wait_fn(1)
|
||||
@@ -51,7 +51,7 @@ def create_position(vega: VegaService, market_id):
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_submit_stop_order_market_oco_rejected(
|
||||
continuous_market, vega: VegaService, page: Page
|
||||
continuous_market, vega: VegaServiceNull, page: Page
|
||||
):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(stop_orders_tab).click()
|
||||
@@ -128,7 +128,7 @@ def test_submit_stop_order_market_oco_rejected(
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_submit_stop_oco_market_order_triggered(
|
||||
continuous_market, vega: VegaService, page: Page
|
||||
continuous_market, vega: VegaServiceNull, page: Page
|
||||
):
|
||||
create_position(vega, continuous_market)
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
@@ -205,7 +205,7 @@ def test_submit_stop_oco_market_order_triggered(
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_submit_stop_oco_market_order_pending(
|
||||
continuous_market, vega: VegaService, page: Page
|
||||
continuous_market, vega: VegaServiceNull, page: Page
|
||||
):
|
||||
create_position(vega, continuous_market)
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
@@ -237,7 +237,7 @@ def test_submit_stop_oco_market_order_pending(
|
||||
|
||||
@pytest.mark.usefixtures("page", "continuous_market", "auth", "risk_accepted")
|
||||
def test_submit_stop_oco_limit_order_pending(
|
||||
continuous_market, vega: VegaService, page: Page
|
||||
continuous_market, vega: VegaServiceNull, page: Page
|
||||
):
|
||||
create_position(vega, continuous_market)
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
@@ -288,7 +288,7 @@ def test_submit_stop_oco_limit_order_pending(
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_submit_stop_oco_limit_order_cancel(
|
||||
continuous_market, vega: VegaService, page: Page
|
||||
continuous_market, vega: VegaServiceNull, page: Page
|
||||
):
|
||||
create_position(vega, continuous_market)
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user