Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfe083cbee | ||
|
|
462066959d | ||
|
|
f12f5ab961 | ||
|
|
98b2460d8b | ||
|
|
67c182f3de | ||
|
|
68e0009ab0 |
@@ -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,7 +70,7 @@ 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}
|
||||
@@ -97,7 +84,7 @@ const MainGrid = memo(
|
||||
>
|
||||
<ErrorBoundary feature="funding-payments">
|
||||
<TradingViews.fundingPayments.component
|
||||
marketId={marketId}
|
||||
marketId={market.id}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
@@ -113,7 +100,7 @@ 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
|
||||
@@ -122,7 +109,7 @@ const MainGrid = memo(
|
||||
settings={<TradingViews.trades.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="trades">
|
||||
<TradingViews.trades.component marketId={marketId} />
|
||||
<TradingViews.trades.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -225,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]'
|
||||
@@ -234,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,6 +1,7 @@
|
||||
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';
|
||||
@@ -11,23 +12,17 @@ import {
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import {
|
||||
MarketSuccessorBanner,
|
||||
MarketSuccessorProposalBanner,
|
||||
MarketTerminationBanner,
|
||||
} from '../../components/market-banner';
|
||||
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];
|
||||
|
||||
@@ -76,14 +71,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
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 relative">
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
useUpdateReferees,
|
||||
} from './hooks/use-referral';
|
||||
import classNames from 'classnames';
|
||||
import { Table } from './table';
|
||||
import { Table } from '../../components/table';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getDateFormat,
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
@@ -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 && (
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -9,6 +9,7 @@ export const Card = ({
|
||||
loading = false,
|
||||
highlight = false,
|
||||
testId,
|
||||
noBackgroundOnMobile = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
@@ -16,20 +17,33 @@ export const Card = ({
|
||||
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();
|
||||
@@ -166,6 +168,7 @@ export const FeesContainer = () => {
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
data-testid="volume-discount-card"
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<VolumeTiers
|
||||
tiers={volumeTiers}
|
||||
@@ -179,12 +182,14 @@ export const FeesContainer = () => {
|
||||
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
|
||||
@@ -192,6 +197,7 @@ export const FeesContainer = () => {
|
||||
className="lg:col-span-full"
|
||||
loading={marketsLoading}
|
||||
data-testid="fees-by-market-card"
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<MarketFees
|
||||
markets={markets}
|
||||
@@ -477,44 +483,65 @@ const VolumeTiers = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th data-testid="tier-header">{t('Tier')}</Th>
|
||||
<Th data-testid="discount-header">{t('Discount')}</Th>
|
||||
<Th data-testid="min-volume-header">{t('Min. trading volume')}</Th>
|
||||
<Th data-testid="my-volume-header">
|
||||
{t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: windowLength,
|
||||
})}
|
||||
</Th>
|
||||
<Th data-testid="actions-header" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
|
||||
return (
|
||||
<Tr key={i} data-testid={`tier-row-${i}`}>
|
||||
<Td data-testid={`tier-value-${i}`}>{i + 1}</Td>
|
||||
<Td data-testid={`discount-value-${i}`}>
|
||||
{formatPercentage(Number(tier.volumeDiscountFactor))}%
|
||||
</Td>
|
||||
<Td data-testid={`min-volume-value-${i}`}>
|
||||
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
|
||||
</Td>
|
||||
<Td data-testid={`my-volume-value-${i}`}>
|
||||
{isUserTier ? formatNumber(lastEpochVolume) : ''}
|
||||
</Td>
|
||||
<Td data-testid={`your-tier-${i}`}>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
@@ -524,6 +551,7 @@ const ReferralTiers = ({
|
||||
tierIndex,
|
||||
epochsInSet,
|
||||
referralVolumeInWindow,
|
||||
referralDiscountWindowLength,
|
||||
}: {
|
||||
tiers: Array<{
|
||||
referralDiscountFactor: string;
|
||||
@@ -533,6 +561,7 @@ const ReferralTiers = ({
|
||||
tierIndex: number;
|
||||
epochsInSet: number;
|
||||
referralVolumeInWindow: number;
|
||||
referralDiscountWindowLength: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
@@ -544,60 +573,81 @@ const ReferralTiers = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th data-testid="tier-header">{t('Tier')}</Th>
|
||||
<Th data-testid="discount-header">{t('Discount')}</Th>
|
||||
<Th data-testid="min-volume-header">{t('Min. trading volume')}</Th>
|
||||
<Th data-testid="required-epochs-header">{t('Required epochs')}</Th>
|
||||
<Th data-testid="extra-header" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers).map((tier, 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(
|
||||
tier.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 < tier.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {tier.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} data-testid={`tier-row-${i}`}>
|
||||
<Td data-testid={`tier-value-${i}`}>{i + 1}</Td>
|
||||
<Td data-testid={`discount-value-${i}`}>
|
||||
{formatPercentage(Number(tier.referralDiscountFactor))}%
|
||||
</Td>
|
||||
<Td data-testid={`min-volume-value-${i}`}>
|
||||
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
|
||||
</Td>
|
||||
<Td data-testid={`required-epochs-value-${i}`}>
|
||||
{tier.minimumEpochs}
|
||||
</Td>
|
||||
<Td data-testid={`user-tier-or-unlocks-${i}`}>
|
||||
{isUserTier ? (
|
||||
<YourTier testId={`your-tier-${i}`} />
|
||||
) : (
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -611,7 +661,7 @@ const YourTier = ({ testId }: YourTierProps) => {
|
||||
|
||||
return (
|
||||
<span
|
||||
className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white"
|
||||
className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white text-xs"
|
||||
data-testid={testId}
|
||||
>
|
||||
{t('Your tier')}
|
||||
|
||||
@@ -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}
|
||||
|
||||
+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 };
|
||||
};
|
||||
@@ -231,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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,3 +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,3 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
|
||||
VEGA_VERSION=v0.73.8
|
||||
VEGA_VERSION=v0.73.10
|
||||
LOCAL_SERVER=false
|
||||
@@ -8,6 +8,9 @@ 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, Ports
|
||||
from playwright.sync_api import Browser, Page
|
||||
@@ -100,12 +103,16 @@ def init_vega(request=None):
|
||||
container = docker_client.containers.run(
|
||||
console_image_name, detach=True, ports={"80/tcp": vega.console_port}
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -27,8 +27,6 @@ MIN_VOLUME_VALUE_0 = "min-volume-value-0"
|
||||
MIN_VOLUME_VALUE_1 = "min-volume-value-1"
|
||||
MY_VOLUME_VALUE_0 = "my-volume-value-0"
|
||||
MY_VOLUME_VALUE_1 = "my-volume-value-1"
|
||||
YOUR_TIER_0 = "your-tier-0"
|
||||
YOUR_TIER_1 = "your-tier-1"
|
||||
ORDER_SIZE = "order-size"
|
||||
ORDER_PRICE = "order-price"
|
||||
DISCOUNT_PILL = "discount-pill"
|
||||
@@ -52,6 +50,7 @@ REQUIRED_EPOCHS_VALUE_1 = "required-epochs-value-1"
|
||||
FILLS = "Fills"
|
||||
TAB_FILLS = "tab-fills"
|
||||
FEE_BREAKDOWN_TOOLTIP = "fee-breakdown-tooltip"
|
||||
PINNED_ROW_LOCATOR = ".ag-pinned-left-cols-container .ag-row"
|
||||
ROW_LOCATOR = ".ag-center-cols-container .ag-row"
|
||||
# Col-Ids:
|
||||
COL_INSTRUMENT_CODE = '[col-id="market.tradableInstrument.instrument.code"]'
|
||||
@@ -405,10 +404,10 @@ def test_fees_page_referral_discount_program_referral_benefits(
|
||||
@pytest.mark.parametrize(
|
||||
"tier, discount_program, my_volume_test_id, my_volume_value, your_tier",
|
||||
[
|
||||
(1, "volume", "my-volume-value-0", "103", "your-tier-0"),
|
||||
(2, "volume", "my-volume-value-1", "206", "your-tier-1"),
|
||||
(1, "referral", "my-volume-value-0", "103", "your-tier-0"),
|
||||
(2, "referral", "my-volume-value-1", "206", "your-tier-1"),
|
||||
(1, "volume", "my-volume-value-0", "103", "your-volume-tier-0"),
|
||||
(2, "volume", "my-volume-value-1", "206", "your-volume-tier-1"),
|
||||
(1, "referral", "my-volume-value-0", "103", "your-referral-tier-0"),
|
||||
(2, "referral", "my-volume-value-1", "206", "your-referral-tier-1"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("risk_accepted", "auth", "market_ids")
|
||||
@@ -439,8 +438,8 @@ def test_fees_page_discount_program_discount(
|
||||
expect(page.get_by_test_id(REQUIRED_EPOCHS_VALUE_0)).to_have_text("1")
|
||||
expect(page.get_by_test_id(REQUIRED_EPOCHS_VALUE_1)).to_have_text("2")
|
||||
|
||||
expect(page.get_by_test_id(your_tier)).to_be_visible()
|
||||
expect(page.get_by_test_id(your_tier)).to_have_text("Your tier")
|
||||
expect(page.get_by_test_id(your_tier).nth(1)).to_be_visible()
|
||||
expect(page.get_by_test_id(your_tier).nth(1)).to_have_text("Your tier")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -461,8 +460,9 @@ def test_fees_page_discount_program_fees_by_market(
|
||||
vega_instance, tier, discount_program, market_ids
|
||||
)
|
||||
page.goto("/#/fees")
|
||||
pinned = page.locator(PINNED_ROW_LOCATOR)
|
||||
row = page.locator(ROW_LOCATOR)
|
||||
expect(row.locator(COL_CODE)).to_have_text("BTC:DAI_2023Futr")
|
||||
expect(pinned.locator(COL_CODE)).to_have_text("BTC:DAI_2023Futr")
|
||||
expect(row.locator(COL_FEE_AFTER_DISCOUNT)).to_have_text(fees_after_discount)
|
||||
expect(row.locator(COL_INFRA_FEE)).to_have_text("0.05%")
|
||||
expect(row.locator(COL_MAKER_FEE)).to_have_text("10%")
|
||||
|
||||
@@ -129,7 +129,8 @@ class TestGetStarted:
|
||||
page.get_by_test_id("get-started-button").click()
|
||||
# Assert dialog isn't visible
|
||||
expect(page.get_by_test_id("welcome-dialog")).not_to_be_visible()
|
||||
|
||||
|
||||
@pytest.mark.skip("TODO: this test is flakey, needs fixing")
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_get_started_seen_already(self, simple_market, page: Page):
|
||||
page.goto(f"/#/markets/{simple_market}")
|
||||
@@ -177,10 +178,10 @@ class TestGetStarted:
|
||||
# 0007-FUGS-018
|
||||
expect(page.get_by_test_id("welcome-dialog")).not_to_be_visible()
|
||||
|
||||
class TestBrowseAll:
|
||||
class TestBrowseAll:
|
||||
def test_get_started_browse_all(self, simple_market, vega: VegaServiceNull, page: Page):
|
||||
page.goto("/")
|
||||
print(simple_market)
|
||||
page.get_by_test_id("browse-markets-button").click()
|
||||
# 0007-FUGS-005
|
||||
expect(page).to_have_url(f"http://localhost:{vega.console_port}/#/markets/{simple_market}")
|
||||
expect(page).to_have_url(f"http://localhost:{vega.console_port}/#/markets/{simple_market}")
|
||||
|
||||
@@ -9,16 +9,16 @@ def test_market_selector(continuous_market, page: Page):
|
||||
page.get_by_test_id("header-title").click()
|
||||
# 6001-MARK-066
|
||||
expect(page.get_by_test_id("market-selector")).to_be_visible()
|
||||
|
||||
# 6001-MARK-021
|
||||
# 6001-MARK-022
|
||||
# 6001-MARK-024
|
||||
# 6001-MARK-025
|
||||
btc_market = page.locator('[data-testid="market-selector-list"] a')
|
||||
expect(btc_market.locator("h3")).to_have_text("BTC:DAI_2023Futr")
|
||||
expect(btc_market.locator('[data-testid="market-selector-volume"]')).to_have_text(
|
||||
"0.00"
|
||||
)
|
||||
# tbd - 5465
|
||||
# expect(btc_market.locator('[data-testid="market-selector-volume"]')).to_have_text(
|
||||
# "1"
|
||||
# )
|
||||
expect(btc_market.locator('[data-testid="market-selector-price"]')).to_have_text(
|
||||
"107.50 tDAI"
|
||||
)
|
||||
@@ -43,7 +43,6 @@ def test_market_selector_filter(continuous_market, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id("header-title").click()
|
||||
# 6001-MARK-027
|
||||
|
||||
page.get_by_test_id("product-Spot").click()
|
||||
expect(page.get_by_test_id("market-selector-list")).to_contain_text(
|
||||
"Spot markets coming soon."
|
||||
@@ -58,8 +57,9 @@ def test_market_selector_filter(continuous_market, page: Page):
|
||||
# 6001-MARK-029
|
||||
page.get_by_test_id("search-term").fill("btc")
|
||||
expect(page.locator('[data-testid="market-selector-list"] a')).to_have_count(1)
|
||||
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_have_text(
|
||||
"BTC:DAI_2023107.50 tDAI0.00"
|
||||
# tbd - 5465
|
||||
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_contain_text(
|
||||
"BTC:DAI_2023107.50 tDAI"
|
||||
)
|
||||
|
||||
page.get_by_test_id("search-term").clear()
|
||||
@@ -83,6 +83,7 @@ def test_market_selector_filter(continuous_market, page: Page):
|
||||
page.get_by_test_id("asset-trigger").click()
|
||||
page.get_by_role("menuitemcheckbox").nth(0).get_by_text("tDAI").click()
|
||||
expect(page.locator('[data-testid="market-selector-list"] a')).to_have_count(1)
|
||||
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_have_text(
|
||||
"BTC:DAI_2023107.50 tDAI0.00"
|
||||
# tbd - 5465
|
||||
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_contain_text(
|
||||
"BTC:DAI_2023107.50 tDAI"
|
||||
)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import pytest
|
||||
import re
|
||||
import vega_sim.api.governance as governance
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from vega_sim.service import PeggedOrder
|
||||
from vega_sim.service import PeggedOrder, MarketStateUpdateType
|
||||
import vega_sim.api.governance as governance
|
||||
from actions.vega import submit_order
|
||||
from actions.utils import next_epoch
|
||||
from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
|
||||
|
||||
from vega_sim.api import governance
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
|
||||
@@ -127,6 +126,44 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
|
||||
expect(trading_mode).to_have_text("Continuous")
|
||||
expect(market_state).to_have_text("Active")
|
||||
|
||||
vega.update_market_state(
|
||||
market_id = market_id,
|
||||
proposal_key = MM_WALLET.name,
|
||||
market_state = MarketStateUpdateType.Suspend,
|
||||
forward_time_to_enactment = False
|
||||
)
|
||||
|
||||
expect(
|
||||
page
|
||||
.get_by_test_id("market-banner")
|
||||
.get_by_test_id(f"update-state-banner-{market_id}")
|
||||
).to_be_visible()
|
||||
|
||||
vega.forward("60s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
expect(
|
||||
page.get_by_test_id("market-banner")
|
||||
).to_have_text("Market was suspended by governance")
|
||||
|
||||
# banner should not show after resume
|
||||
vega.update_market_state(
|
||||
market_id = market_id,
|
||||
proposal_key = MM_WALLET.name,
|
||||
market_state = MarketStateUpdateType.Resume,
|
||||
forward_time_to_enactment = False
|
||||
)
|
||||
|
||||
vega.forward("60s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
expect(page.get_by_test_id("market-banner")).not_to_be_visible()
|
||||
|
||||
# TODO test update market, sim will currently auto approve and enacted
|
||||
# a market update proposals
|
||||
|
||||
# put invalid oracle to trigger market termination
|
||||
governance.submit_oracle_data(
|
||||
wallet=vega.wallet,
|
||||
@@ -154,6 +191,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
|
||||
# check market state is now settled
|
||||
expect(trading_mode).to_have_text("No trading")
|
||||
expect(market_state).to_have_text("Settled")
|
||||
expect(
|
||||
page.get_by_test_id("market-banner")
|
||||
).to_have_text("This market has been settled")
|
||||
|
||||
|
||||
""" @pytest.mark.usefixtures("page", "risk_accepted", "continuous_market")
|
||||
@@ -182,19 +222,19 @@ def test_market_closing_banners(page: Page, continuous_market, vega: VegaService
|
||||
forward_time_to_enactment = False,
|
||||
price=110,
|
||||
)
|
||||
|
||||
|
||||
expect(page.locator(".grow")).to_have_text("Trading on Market BTC:DAI_2023 may stop. There are open proposals to close this marketView proposals")
|
||||
|
||||
governance.approve_proposal(
|
||||
governance.approve_proposal(
|
||||
proposal_id=proposalID,
|
||||
wallet=vega.wallet,
|
||||
key_name="market_maker"
|
||||
|
||||
|
||||
)
|
||||
vega.forward("60s")
|
||||
vega.wait_fn(10)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
|
||||
will_close_pattern = r"TRADING ON MARKET BTC:DAI_2023 WILL STOP ON \d+ \w+\nYou will no longer be able to hold a position on this market when it closes in \d+ days \d+ hours\. The final price will be 107\.00 BTC\."
|
||||
match_result = re.fullmatch(will_close_pattern, page.locator(".grow").inner_text())
|
||||
assert match_result is not None
|
||||
|
||||
@@ -42,6 +42,10 @@ def verify_order_value(
|
||||
expect(element).to_be_visible()
|
||||
if is_regex:
|
||||
actual_text = element.text_content()
|
||||
|
||||
if actual_text is None:
|
||||
raise Exception(f"no text found for test_id {test_id}")
|
||||
|
||||
assert re.match(
|
||||
expected_text, actual_text
|
||||
), f"Expected {expected_text}, but got {actual_text}"
|
||||
|
||||
@@ -109,14 +109,15 @@ def test_perps_market_termination_proposed(page: Page, vega: VegaServiceNull):
|
||||
approve_proposal=True,
|
||||
forward_time_to_enactment=False,
|
||||
)
|
||||
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
banner_text = page.get_by_test_id(
|
||||
f"termination-warning-banner-{perpetual_market}"
|
||||
f"update-state-banner-{perpetual_market}"
|
||||
).text_content()
|
||||
pattern = re.compile(
|
||||
r"Trading on Market BTC:DAI_Perpetual may stop on \d{2} [A-Za-z]+\. There is open proposal to close this market\.Proposed final price is 100\.00 BTC\.View proposal"
|
||||
r"Trading on market BTC:DAI_Perpetual may stop on \d{2} [A-Za-z]+\. There is an open proposal to close this market\.Proposed final price is 100\.00 BTC\. View proposal"
|
||||
)
|
||||
assert pattern.search(
|
||||
banner_text
|
||||
|
||||
@@ -1,46 +1,155 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from fixtures.market import setup_continuous_market, setup_simple_successor_market
|
||||
from fixtures.market import setup_continuous_market
|
||||
from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
|
||||
from actions.vega import submit_multiple_orders, submit_order, submit_liquidity
|
||||
from actions.utils import next_epoch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.usefixtures()
|
||||
def successor_market(vega: VegaServiceNull):
|
||||
parent_market_id = setup_continuous_market(vega)
|
||||
tdai_id = vega.find_asset_id(symbol="tDAI")
|
||||
successor_market_id = setup_simple_successor_market(
|
||||
vega, parent_market_id, tdai_id, "successor_market"
|
||||
)
|
||||
vega.submit_termination_and_settlement_data(
|
||||
settlement_key="FJMKnwfZdd48C8NqvYrG",
|
||||
settlement_price=110,
|
||||
market_id=parent_market_id,
|
||||
)
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
return successor_market_id
|
||||
market_banner = "market-banner"
|
||||
|
||||
@pytest.mark.skip("tbd")
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_succession_line(page: Page, successor_market):
|
||||
page.goto(f"/#/markets/{successor_market}")
|
||||
page.get_by_test_id("Info").click()
|
||||
page.get_by_text("Succession line").click()
|
||||
def test_succession_line(vega: VegaServiceNull, page: Page):
|
||||
parent_market_id = setup_continuous_market(vega)
|
||||
tdai_id = vega.find_asset_id(symbol="tDAI")
|
||||
|
||||
expect(page.get_by_test_id("succession-line-item").first).to_contain_text(
|
||||
"BTC:DAI_2023BTC:DAI_2023"
|
||||
page.goto(f"/#/markets/{parent_market_id}")
|
||||
|
||||
# market in normal state no banner shown
|
||||
expect(page.get_by_test_id(market_banner)).not_to_be_attached()
|
||||
|
||||
successor_name = "successor market name"
|
||||
successor_id = propose_successor(vega, parent_market_id, tdai_id, successor_name)
|
||||
|
||||
# Check that the banner notifying about the successor proposal is shown
|
||||
banner = page.get_by_test_id(market_banner)
|
||||
expect(banner).to_be_attached()
|
||||
expect(banner.get_by_text("A successor to this market has been proposed")).to_be_visible()
|
||||
|
||||
next_epoch(vega)
|
||||
|
||||
# Banner should not show after market has enacted
|
||||
expect(page.get_by_test_id(market_banner)).not_to_be_attached()
|
||||
|
||||
# Check that the newly created market has the correct succession line
|
||||
# shown in market info
|
||||
provide_successor_liquidity(vega, successor_id)
|
||||
|
||||
next_epoch(vega)
|
||||
|
||||
page.goto(f"/#/markets/{successor_id}")
|
||||
|
||||
# Page reload required as the data provider doesnt receive the new market
|
||||
# This also clears any toasts which can block the market info panel containing
|
||||
# the succession line
|
||||
page.reload()
|
||||
|
||||
#tbd issue - 5546
|
||||
page.get_by_test_id("Info").click()
|
||||
|
||||
page.get_by_role("button", name="Succession line").click()
|
||||
|
||||
succession_item = page.get_by_test_id("succession-line-item")
|
||||
|
||||
expect(succession_item.nth(1)).to_contain_text(
|
||||
"successor market name"
|
||||
)
|
||||
expect(
|
||||
page.get_by_test_id("succession-line-item").first.get_by_role("link")
|
||||
).to_be_attached
|
||||
expect(page.get_by_test_id("succession-line-item").last).to_contain_text(
|
||||
"successor_marketsuccessor_market"
|
||||
)
|
||||
expect(
|
||||
page.get_by_test_id("succession-line-item").last.get_by_role("link")
|
||||
succession_item.first.get_by_role("link")
|
||||
).to_be_attached
|
||||
expect(
|
||||
page.get_by_test_id("succession-line-item").last.get_by_test_id("icon-bullet")
|
||||
succession_item.last.get_by_role("link")
|
||||
).to_be_attached
|
||||
expect(
|
||||
succession_item.last.get_by_test_id("icon-bullet")
|
||||
).to_be_visible
|
||||
|
||||
page.goto(f"/#/markets/{parent_market_id}")
|
||||
# Settle parent market and check that successor banner is showing
|
||||
vega.submit_termination_and_settlement_data(
|
||||
settlement_key=GOVERNANCE_WALLET.name,
|
||||
settlement_price=100,
|
||||
market_id=parent_market_id,
|
||||
)
|
||||
next_epoch(vega=vega)
|
||||
|
||||
banner = page.get_by_test_id(market_banner)
|
||||
page.wait_for_selector('[data-testid="market-banner"]', state="attached")
|
||||
expect(banner.get_by_text("This market has been succeeded")).to_be_visible()
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_banners(vega: VegaServiceNull, page: Page):
|
||||
|
||||
banner_successor_text = "A successor to this market has been proposed"
|
||||
parent_market_id = setup_continuous_market(vega)
|
||||
tdai_id = vega.find_asset_id(symbol="tDAI")
|
||||
|
||||
page.goto(f"/#/markets/{parent_market_id}")
|
||||
|
||||
# market in normal state no banner shown
|
||||
expect(page.get_by_test_id(market_banner)).not_to_be_attached()
|
||||
|
||||
vega.submit_termination_and_settlement_data(
|
||||
settlement_key=GOVERNANCE_WALLET.name,
|
||||
settlement_price=100,
|
||||
market_id=parent_market_id,
|
||||
)
|
||||
|
||||
successor_name = "successor market name"
|
||||
propose_successor(vega, parent_market_id, tdai_id, successor_name)
|
||||
|
||||
# Check that the banners notifying about the successor proposal and market has been settled are shown
|
||||
banner = page.get_by_test_id(market_banner)
|
||||
expect(banner).to_be_attached()
|
||||
expect(banner.get_by_text(banner_successor_text)).to_be_visible()
|
||||
expect(banner.get_by_text("1/2")).to_be_visible()
|
||||
|
||||
# Check that the banner notifying about the successor proposal and market has been settled are shown still after reload
|
||||
page.reload()
|
||||
expect(banner.get_by_text(banner_successor_text)).to_be_visible()
|
||||
expect(banner.get_by_text("1/2")).to_be_visible()
|
||||
# Check that the banner notifying about the successor proposal is not visible after close those banners
|
||||
banner.get_by_test_id("icon-cross").click()
|
||||
expect(banner.get_by_text("This market has been settled")).to_be_visible()
|
||||
expect(banner.get_by_text("2/2")).to_be_visible()
|
||||
|
||||
# Check that the banners notifying are visible after reload
|
||||
banner.get_by_test_id("icon-cross").click()
|
||||
expect(page.get_by_test_id(market_banner)).not_to_be_attached()
|
||||
page.reload()
|
||||
expect(banner).to_be_attached()
|
||||
expect(banner.get_by_text(banner_successor_text)).to_be_visible()
|
||||
|
||||
def propose_successor(
|
||||
vega: VegaServiceNull, parent_market_id, tdai_id, market_name
|
||||
):
|
||||
market_id = vega.create_simple_market(
|
||||
market_name,
|
||||
proposal_key=MM_WALLET.name,
|
||||
settlement_asset_id=tdai_id,
|
||||
termination_key=MM_WALLET2.name,
|
||||
market_decimals=5,
|
||||
approve_proposal=True,
|
||||
forward_time_to_enactment=False,
|
||||
parent_market_id=parent_market_id,
|
||||
parent_market_insurance_pool_fraction=0.5,
|
||||
)
|
||||
return market_id
|
||||
|
||||
def provide_successor_liquidity(
|
||||
vega: VegaServiceNull, market_id
|
||||
):
|
||||
submit_liquidity(vega, MM_WALLET.name, market_id)
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET.name, market_id, "SIDE_SELL", [[1, 110], [1, 105]]
|
||||
)
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET2.name, market_id, "SIDE_BUY", [[1, 90], [1, 95]]
|
||||
)
|
||||
|
||||
submit_order(vega, "Key 1", market_id, "SIDE_BUY", 1, 110)
|
||||
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
@@ -3,6 +3,9 @@ fragment MarginFields on MarginLevels {
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginFactor
|
||||
marginMode
|
||||
orderMarginLevel
|
||||
asset {
|
||||
id
|
||||
}
|
||||
@@ -33,6 +36,9 @@ subscription MarginsSubscription($partyId: ID!) {
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginFactor
|
||||
marginMode
|
||||
orderMarginLevel
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
|
||||
+9
-3
@@ -3,21 +3,21 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
|
||||
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginFactor: string, marginMode: Types.MarginMode, orderMarginLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
|
||||
|
||||
export type MarginsQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type MarginsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, marginsConnection?: { __typename?: 'MarginConnection', edges?: Array<{ __typename?: 'MarginEdge', node: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
|
||||
export type MarginsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, marginsConnection?: { __typename?: 'MarginConnection', edges?: Array<{ __typename?: 'MarginEdge', node: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginFactor: string, marginMode: Types.MarginMode, orderMarginLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
|
||||
|
||||
export type MarginsSubscriptionSubscriptionVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type MarginsSubscriptionSubscription = { __typename?: 'Subscription', margins: { __typename?: 'MarginLevelsUpdate', marketId: string, asset: string, partyId: string, maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, timestamp: any } };
|
||||
export type MarginsSubscriptionSubscription = { __typename?: 'Subscription', margins: { __typename?: 'MarginLevelsUpdate', marketId: string, asset: string, partyId: string, maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginFactor: string, marginMode: Types.MarginMode, orderMarginLevel: string, timestamp: any } };
|
||||
|
||||
export const MarginFieldsFragmentDoc = gql`
|
||||
fragment MarginFields on MarginLevels {
|
||||
@@ -25,6 +25,9 @@ export const MarginFieldsFragmentDoc = gql`
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginFactor
|
||||
marginMode
|
||||
orderMarginLevel
|
||||
asset {
|
||||
id
|
||||
}
|
||||
@@ -85,6 +88,9 @@ export const MarginsSubscriptionDocument = gql`
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginFactor
|
||||
marginMode
|
||||
orderMarginLevel
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ const update = (
|
||||
searchLevel: delta.searchLevel,
|
||||
initialLevel: delta.initialLevel,
|
||||
collateralReleaseLevel: delta.collateralReleaseLevel,
|
||||
marginFactor: delta.marginFactor,
|
||||
marginMode: delta.marginMode,
|
||||
orderMarginLevel: delta.orderMarginLevel,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: delta.asset,
|
||||
|
||||
@@ -8,9 +8,17 @@ export const StackedCell = ({
|
||||
secondary: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className="leading-4 text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<div data-testid="stack-cell-primary">{primary}</div>
|
||||
<div data-testid="stack-cell-secondary" className="text-muted">
|
||||
<div className="leading-4">
|
||||
<div
|
||||
className="text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
data-testid="stack-cell-primary"
|
||||
>
|
||||
{primary}
|
||||
</div>
|
||||
<div
|
||||
data-testid="stack-cell-secondary"
|
||||
className="text-ellipsis whitespace-nowrap overflow-hidden text-muted"
|
||||
>
|
||||
{secondary}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit';
|
||||
import { DealTicket } from './deal-ticket';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../use-t';
|
||||
import { MarginModeSelector } from './margin-mode-selector';
|
||||
|
||||
interface DealTicketContainerProps {
|
||||
marketId: string;
|
||||
@@ -51,21 +52,26 @@ export const DealTicketContainer = ({
|
||||
reload={reload}
|
||||
>
|
||||
{market && marketData ? (
|
||||
featureFlags.STOP_ORDERS && showStopOrder ? (
|
||||
<StopOrder
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
submit={(stopOrdersSubmission) => create({ stopOrdersSubmission })}
|
||||
/>
|
||||
) : (
|
||||
<DealTicket
|
||||
{...props}
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
marketData={marketData}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
/>
|
||||
)
|
||||
<>
|
||||
<MarginModeSelector marketId={marketId} />
|
||||
{featureFlags.STOP_ORDERS && showStopOrder ? (
|
||||
<StopOrder
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
submit={(stopOrdersSubmission) =>
|
||||
create({ stopOrdersSubmission })
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<DealTicket
|
||||
{...props}
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
marketData={marketData}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p>{t('Could not load market')}</p>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
|
||||
import { marginModeDataProvider } from '@vegaprotocol/positions';
|
||||
import { MarginMode, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/web3';
|
||||
|
||||
export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { data: marginMode } = useDataProvider({
|
||||
dataProvider: marginModeDataProvider,
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
marketId,
|
||||
},
|
||||
skip: !pubKey,
|
||||
});
|
||||
const create = useVegaTransactionStore((state) => state.create);
|
||||
const disabled = isReadOnly;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-2 mb-2">
|
||||
<TradingButton
|
||||
disabled={disabled}
|
||||
size="extra-small"
|
||||
onClick={() =>
|
||||
create({
|
||||
updateMarginMode: {
|
||||
market_id: marketId,
|
||||
mode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
})
|
||||
}
|
||||
intent={
|
||||
!marginMode ||
|
||||
marginMode.marginMode === Types.MarginMode.MARGIN_MODE_CROSS_MARGIN
|
||||
? Intent.Primary
|
||||
: Intent.None
|
||||
}
|
||||
>
|
||||
Cross
|
||||
</TradingButton>
|
||||
<TradingButton
|
||||
disabled={disabled}
|
||||
size="extra-small"
|
||||
onClick={() =>
|
||||
create({
|
||||
updateMarginMode: {
|
||||
market_id: marketId,
|
||||
mode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
|
||||
marginFactor: '0.1',
|
||||
},
|
||||
})
|
||||
}
|
||||
intent={
|
||||
marginMode?.marginMode ===
|
||||
Types.MarginMode.MARGIN_MODE_ISOLATED_MARGIN
|
||||
? Intent.Primary
|
||||
: Intent.None
|
||||
}
|
||||
>
|
||||
Isolated {marginMode?.margin_factor || '10'}x
|
||||
</TradingButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -10,7 +10,7 @@
|
||||
"24h vol": "24h vol",
|
||||
"24h volume": "24h volume",
|
||||
"A percentage of commission earned by the referrer": "A percentage of commission earned by the referrer",
|
||||
"A successors to this market has been proposed": "A successors to this market has been proposed",
|
||||
"A successor to this market has been proposed": "A successor to this market has been proposed",
|
||||
"About the referral program": "About the referral program",
|
||||
"Active": "Active",
|
||||
"All": "All",
|
||||
@@ -28,6 +28,7 @@
|
||||
"By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>": "By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>",
|
||||
"Chart": "Chart",
|
||||
"Change (24h)": "Change (24h)",
|
||||
"Changes have been proposed for this market. <0>View proposals</0>": "Changes have been proposed for this market. <0>View proposals</0>",
|
||||
"Chart": "Chart",
|
||||
"Chart by <0>TradingView</0>": "Chart by <0>TradingView</0>",
|
||||
"checkOutProposalsAndVote": "Check out the terms of the proposals and vote:",
|
||||
@@ -306,9 +307,9 @@
|
||||
"TradingView": "TradingView",
|
||||
"Trading has been terminated as a result of the product definition": "Trading has been terminated as a result of the product definition",
|
||||
"Trading mode": "Trading mode",
|
||||
"Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.": "Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.",
|
||||
"Trading on Market {{name}} may stop. There are open proposals to close this market": "Trading on Market {{name}} may stop. There are open proposals to close this market",
|
||||
"Trading on Market {{name}} will stop on {{date}}": "Trading on Market {{name}} will stop on {{date}}",
|
||||
"Trading on market {{name}} may stop on {{date}}. There is an open proposal to close this market.": "Trading on market {{name}} may stop on {{date}}. There is an open proposal to close this market.",
|
||||
"Trading on market {{name}} may stop. There are open proposals to close this market": "Trading on market {{name}} may stop. There are open proposals to close this market",
|
||||
"Trading on market {{name}} will stop on {{date}}": "Trading on market {{name}} will stop on {{date}}",
|
||||
"Transfer": "Transfer",
|
||||
"Unknown": "Unknown",
|
||||
"Unknown settlement date": "Unknown settlement date",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './last-24h-price-change';
|
||||
export * from './last-24h-volume';
|
||||
export * from './market-info';
|
||||
export * from './oracle-banner';
|
||||
export * from './oracle-basic-profile';
|
||||
export * from './oracle-full-profile';
|
||||
export { OracleDialog } from './oracle-dialog';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './oracle-banner';
|
||||
@@ -1,68 +0,0 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { OracleBanner } from './oracle-banner';
|
||||
|
||||
const mockOracleData = {
|
||||
data: {
|
||||
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',
|
||||
status_reason: 'Reason here',
|
||||
first_verified: '2021-01-01T00:00:00Z',
|
||||
last_verified: '2022-01-01T00:00:00Z',
|
||||
type: 'public_key',
|
||||
public_key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
|
||||
},
|
||||
proofs: [
|
||||
{
|
||||
format: 'url',
|
||||
available: true,
|
||||
type: 'web',
|
||||
url: 'https://proofweb.com',
|
||||
},
|
||||
{
|
||||
format: 'signed_message',
|
||||
available: true,
|
||||
type: 'public_key',
|
||||
public_key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
|
||||
message: 'SOMEHEX',
|
||||
},
|
||||
],
|
||||
github_link:
|
||||
'https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/eth_address-0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC.toml',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('../../hooks', () => ({
|
||||
...jest.requireActual('../../hooks'),
|
||||
useMarketOracle: jest.fn((...args) => {
|
||||
return mockOracleData;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('OracleBanner', () => {
|
||||
it('should render successfully', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={[]} addTypename={false}>
|
||||
<OracleBanner marketId="someMarketId" />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('oracle-banner-status')).toHaveTextContent(
|
||||
'COMPROMISED'
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('oracle-banner-dialog-trigger')
|
||||
).toHaveTextContent('Show more');
|
||||
fireEvent.click(screen.getByTestId('oracle-banner-dialog-trigger'));
|
||||
expect(screen.getByTestId('oracle-full-profile')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useMarketOracle } from '../../hooks';
|
||||
import {
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
ButtonLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { OracleDialog } from '../oracle-dialog';
|
||||
import { useOracleStatuses } from './oracle-statuses';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
export const OracleBanner = ({ marketId }: { marketId: string }) => {
|
||||
const oracleStatuses = useOracleStatuses();
|
||||
const [open, onChange] = useState(false);
|
||||
const { data: settlementOracle } = useMarketOracle(marketId);
|
||||
const { data: tradingTerminationOracle } = useMarketOracle(
|
||||
marketId,
|
||||
'dataSourceSpecForTradingTermination'
|
||||
);
|
||||
let maliciousOracle = null;
|
||||
if (settlementOracle?.provider.oracle.status !== 'GOOD') {
|
||||
maliciousOracle = settlementOracle;
|
||||
} else if (tradingTerminationOracle?.provider.oracle.status !== 'GOOD') {
|
||||
maliciousOracle = tradingTerminationOracle;
|
||||
}
|
||||
if (!maliciousOracle) return null;
|
||||
|
||||
const { provider } = maliciousOracle;
|
||||
return (
|
||||
<>
|
||||
<OracleDialog open={open} onChange={onChange} {...maliciousOracle} />
|
||||
<NotificationBanner intent={Intent.Danger}>
|
||||
<div>
|
||||
<Trans
|
||||
defaults="Oracle status for this market is <0>{{status}}</0>. {{description}} <1>Show more</1>"
|
||||
components={[
|
||||
<span data-testid="oracle-banner-status">status</span>,
|
||||
<ButtonLink
|
||||
onClick={() => onChange(!open)}
|
||||
data-testid="oracle-banner-dialog-trigger"
|
||||
>
|
||||
Show more
|
||||
</ButtonLink>,
|
||||
]}
|
||||
values={{
|
||||
status: provider.oracle.status,
|
||||
description: oracleStatuses[provider.oracle.status],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</NotificationBanner>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useOracleStatuses } from '../oracle-banner/oracle-statuses';
|
||||
import { useOracleStatuses } from '../../hooks';
|
||||
import type { IconName } from '@blueprintjs/icons';
|
||||
import classNames from 'classnames';
|
||||
import { getLinkIcon, useVerifiedStatusIcon } from '../oracle-basic-profile';
|
||||
|
||||
@@ -4,3 +4,5 @@ export * from './use-oracle-proofs';
|
||||
export * from './use-oracle-spec-binding-data';
|
||||
export * from './use-candles';
|
||||
export * from './use-successor-market';
|
||||
export { useMaliciousOracle } from './use-malicious-oracle';
|
||||
export { useOracleStatuses } from './use-oracle-statuses';
|
||||
|
||||
@@ -7,12 +7,13 @@ import { Interval } from '@vegaprotocol/types';
|
||||
export const useCandles = ({ marketId }: { marketId?: string }) => {
|
||||
const fiveDaysAgo = useFiveDaysAgo();
|
||||
const yesterday = useYesterday();
|
||||
const since = new Date(fiveDaysAgo).toISOString();
|
||||
const { data, error } = useThrottledDataProvider({
|
||||
dataProvider: marketCandlesProvider,
|
||||
variables: {
|
||||
marketId: marketId || '',
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
since: new Date(fiveDaysAgo).toISOString(),
|
||||
since,
|
||||
},
|
||||
skip: !marketId,
|
||||
});
|
||||
@@ -22,6 +23,7 @@ export const useCandles = ({ marketId }: { marketId?: string }) => {
|
||||
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
|
||||
isCandleLessThan24hOld(candle, yesterday)
|
||||
);
|
||||
|
||||
return { oneDayCandles, error, fiveDaysCandles };
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useMarketOracle } from './use-market-oracle';
|
||||
|
||||
/**
|
||||
* Returns an oracle if eithe the settlement or termination oracle is marked
|
||||
* as malicious
|
||||
*/
|
||||
export const useMaliciousOracle = (marketId: string) => {
|
||||
const { data: settlementOracle, loading: settlementLoading } =
|
||||
useMarketOracle(marketId);
|
||||
|
||||
const { data: tradingTerminationOracle, loading: terminationLoading } =
|
||||
useMarketOracle(marketId, 'dataSourceSpecForTradingTermination');
|
||||
|
||||
let maliciousOracle = undefined;
|
||||
|
||||
if (settlementOracle?.provider.oracle.status !== 'GOOD') {
|
||||
maliciousOracle = settlementOracle;
|
||||
} else if (tradingTerminationOracle?.provider.oracle.status !== 'GOOD') {
|
||||
maliciousOracle = tradingTerminationOracle;
|
||||
}
|
||||
|
||||
return {
|
||||
data: maliciousOracle,
|
||||
loading: settlementLoading || terminationLoading || false,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { useT } from '../../use-t';
|
||||
import { useT } from '../use-t';
|
||||
|
||||
export const useOracleStatuses = () => {
|
||||
const t = useT();
|
||||
@@ -120,8 +120,7 @@ export const marketTradingModeProvider = makeDerivedDataProvider<
|
||||
MarketDataQueryVariables
|
||||
>(
|
||||
[marketDataProvider],
|
||||
(parts, variables, prevData) =>
|
||||
(parts[0] as ReturnType<typeof getData>)?.marketTradingMode
|
||||
(parts) => (parts[0] as ReturnType<typeof getData>)?.marketTradingMode
|
||||
);
|
||||
|
||||
export const marketStateProvider = makeDerivedDataProvider<
|
||||
@@ -130,8 +129,7 @@ export const marketStateProvider = makeDerivedDataProvider<
|
||||
MarketDataQueryVariables
|
||||
>(
|
||||
[marketDataProvider],
|
||||
(parts, variables, prevData) =>
|
||||
(parts[0] as ReturnType<typeof getData>)?.marketState
|
||||
(parts) => (parts[0] as ReturnType<typeof getData>)?.marketState
|
||||
);
|
||||
|
||||
export const fundingRateProvider = makeDerivedDataProvider<
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './lib/__generated__/Positions';
|
||||
export * from './lib/margin-modes-provider';
|
||||
export * from './lib/positions-data-providers';
|
||||
export * from './lib/positions-table';
|
||||
export * from './lib/positions-manager';
|
||||
|
||||
@@ -83,3 +83,23 @@ query EstimatePosition(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment MarginMode on PartyMarginMode {
|
||||
marketId
|
||||
partyId
|
||||
marginMode
|
||||
margin_factor
|
||||
min_theoretical_margin_factor
|
||||
max_theoretical_leverage
|
||||
atEpoch
|
||||
}
|
||||
|
||||
query MarginModes($partyId: ID!) {
|
||||
partyMarginModes(partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
...MarginMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-1
@@ -29,6 +29,15 @@ export type EstimatePositionQueryVariables = Types.Exact<{
|
||||
|
||||
export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', margin: { __typename?: 'MarginEstimate', worstCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string }, bestCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string } }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null };
|
||||
|
||||
export type MarginModeFragment = { __typename?: 'PartyMarginMode', marketId: string, partyId: string, marginMode: Types.MarginMode, margin_factor?: string | null, min_theoretical_margin_factor?: string | null, max_theoretical_leverage?: string | null, atEpoch: number };
|
||||
|
||||
export type MarginModesQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type MarginModesQuery = { __typename?: 'Query', partyMarginModes?: { __typename?: 'PartyMarginModesConnection', edges?: Array<{ __typename?: 'PartyMarginModeEdge', node: { __typename?: 'PartyMarginMode', marketId: string, partyId: string, marginMode: Types.MarginMode, margin_factor?: string | null, min_theoretical_margin_factor?: string | null, max_theoretical_leverage?: string | null, atEpoch: number } } | null> | null } | null };
|
||||
|
||||
export const PositionFieldsFragmentDoc = gql`
|
||||
fragment PositionFields on Position {
|
||||
realisedPNL
|
||||
@@ -46,6 +55,17 @@ export const PositionFieldsFragmentDoc = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const MarginModeFragmentDoc = gql`
|
||||
fragment MarginMode on PartyMarginMode {
|
||||
marketId
|
||||
partyId
|
||||
marginMode
|
||||
margin_factor
|
||||
min_theoretical_margin_factor
|
||||
max_theoretical_leverage
|
||||
atEpoch
|
||||
}
|
||||
`;
|
||||
export const PositionsDocument = gql`
|
||||
query Positions($partyIds: [ID!]!) {
|
||||
positions(filter: {partyIds: $partyIds}) {
|
||||
@@ -191,4 +211,43 @@ export function useEstimatePositionLazyQuery(baseOptions?: Apollo.LazyQueryHookO
|
||||
}
|
||||
export type EstimatePositionQueryHookResult = ReturnType<typeof useEstimatePositionQuery>;
|
||||
export type EstimatePositionLazyQueryHookResult = ReturnType<typeof useEstimatePositionLazyQuery>;
|
||||
export type EstimatePositionQueryResult = Apollo.QueryResult<EstimatePositionQuery, EstimatePositionQueryVariables>;
|
||||
export type EstimatePositionQueryResult = Apollo.QueryResult<EstimatePositionQuery, EstimatePositionQueryVariables>;
|
||||
export const MarginModesDocument = gql`
|
||||
query MarginModes($partyId: ID!) {
|
||||
partyMarginModes(partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
...MarginMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${MarginModeFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useMarginModesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useMarginModesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useMarginModesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useMarginModesQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useMarginModesQuery(baseOptions: Apollo.QueryHookOptions<MarginModesQuery, MarginModesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<MarginModesQuery, MarginModesQueryVariables>(MarginModesDocument, options);
|
||||
}
|
||||
export function useMarginModesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MarginModesQuery, MarginModesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<MarginModesQuery, MarginModesQueryVariables>(MarginModesDocument, options);
|
||||
}
|
||||
export type MarginModesQueryHookResult = ReturnType<typeof useMarginModesQuery>;
|
||||
export type MarginModesLazyQueryHookResult = ReturnType<typeof useMarginModesLazyQuery>;
|
||||
export type MarginModesQueryResult = Apollo.QueryResult<MarginModesQuery, MarginModesQueryVariables>;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import {
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
} from '@vegaprotocol/data-provider';
|
||||
import {
|
||||
MarginModesDocument,
|
||||
type MarginModesQueryVariables,
|
||||
MarginModesQuery,
|
||||
MarginModeFragment,
|
||||
} from './__generated__/Positions';
|
||||
|
||||
export const marginModesDataProvider = makeDataProvider<
|
||||
MarginModesQuery,
|
||||
MarginModeFragment[],
|
||||
never,
|
||||
never,
|
||||
MarginModesQueryVariables
|
||||
>({
|
||||
query: MarginModesDocument,
|
||||
getData: (responseData: MarginModesQuery | null) =>
|
||||
removePaginationWrapper(responseData?.partyMarginModes?.edges) || [],
|
||||
});
|
||||
|
||||
export const marginModeDataProvider = makeDerivedDataProvider<
|
||||
MarginModeFragment | undefined,
|
||||
never,
|
||||
MarginModesQueryVariables & { marketId: string }
|
||||
>(
|
||||
[
|
||||
(callback, client, variables) =>
|
||||
marginModesDataProvider(callback, client, { partyId: variables.partyId }),
|
||||
],
|
||||
(data, variables) =>
|
||||
(data as MarginModeFragment[]).find(
|
||||
(marginMode) => marginMode.marketId === variables.marketId
|
||||
)
|
||||
);
|
||||
@@ -441,6 +441,9 @@ fragment ProposalListFields on Proposal {
|
||||
... on UpdateNetworkParameter {
|
||||
...UpdateNetworkParameterFields
|
||||
}
|
||||
... on UpdateMarketState {
|
||||
...UpdateMarketStateFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,6 +481,9 @@ fragment MarketViewProposalFields on Proposal {
|
||||
... on NewMarket {
|
||||
...NewMarketSuccessorFields
|
||||
}
|
||||
... on UpdateMarket {
|
||||
...UpdateMarketFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-22
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
marketViewProposalsDataProvider,
|
||||
proposalsDataProvider,
|
||||
useMarketProposals,
|
||||
} from './proposals-data-provider';
|
||||
export * from './__generated__/Proposals';
|
||||
|
||||
@@ -57,6 +57,7 @@ describe('proposals data provider', () => {
|
||||
{
|
||||
...delta,
|
||||
id: '2',
|
||||
// @ts-ignore skipping unnecessary fields
|
||||
terms: { ...delta.terms, change: { __typename: 'UpdateMarket' } },
|
||||
},
|
||||
reload,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Update } from '@vegaprotocol/data-provider';
|
||||
import { useDataProvider, type Update } from '@vegaprotocol/data-provider';
|
||||
import { makeDataProvider } from '@vegaprotocol/data-provider';
|
||||
import produce from 'immer';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
@@ -125,3 +125,17 @@ export const marketViewProposalsDataProvider = makeDataProvider<
|
||||
getData: getMarketProposalsData,
|
||||
getSubscriptionVariables: () => subscriptionVariables,
|
||||
});
|
||||
|
||||
export const useMarketProposals = (
|
||||
variables: {
|
||||
inState?: Types.ProposalState;
|
||||
proposalType?: Types.ProposalType;
|
||||
},
|
||||
skip?: boolean
|
||||
) => {
|
||||
return useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
variables,
|
||||
skip,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -86,8 +86,7 @@ export const TradingView = ({
|
||||
|
||||
// Show volume study by default, second bool arg adds it as a overlay on top of the chart
|
||||
studies.forEach((study) => {
|
||||
const asOverlay = study === 'Volume';
|
||||
activeChart.createStudy(study, asOverlay);
|
||||
activeChart.createStudy(study);
|
||||
});
|
||||
|
||||
// Subscribe to interval changes so it can be persisted in chart settings
|
||||
@@ -127,5 +126,7 @@ const getOverrides = (theme: 'dark' | 'light') => {
|
||||
// colors set here, trading view lets the user set a color
|
||||
'paneProperties.background': theme === 'dark' ? '#05060C' : '#fff',
|
||||
'paneProperties.backgroundType': 'solid',
|
||||
// hide market name within TV chart as its already above
|
||||
'paneProperties.legendProperties.showSeriesTitle': false,
|
||||
};
|
||||
};
|
||||
|
||||
Generated
+70
@@ -1986,8 +1986,14 @@ export type MarginLevels = {
|
||||
initialLevel: Scalars['String'];
|
||||
/** Minimal margin for the position to be maintained in the network (unsigned integer) */
|
||||
maintenanceLevel: Scalars['String'];
|
||||
/** Margin factor, only relevant for isolated margin mode, else 0 */
|
||||
marginFactor: Scalars['String'];
|
||||
/** Margin mode of the party, cross margin or isolated margin */
|
||||
marginMode: MarginMode;
|
||||
/** Market in which the margin is required for this party */
|
||||
market: Market;
|
||||
/** When in isolated margin, the required order margin level, otherwise, 0 */
|
||||
orderMarginLevel: Scalars['String'];
|
||||
/** The party for this margin */
|
||||
party: Party;
|
||||
/** If the margin is between maintenance and search, the network will initiate a collateral search, expressed as unsigned integer */
|
||||
@@ -2010,8 +2016,14 @@ export type MarginLevelsUpdate = {
|
||||
initialLevel: Scalars['String'];
|
||||
/** Minimal margin for the position to be maintained in the network (unsigned integer) */
|
||||
maintenanceLevel: Scalars['String'];
|
||||
/** Margin factor, only relevant for isolated margin mode, else 0 */
|
||||
marginFactor: Scalars['String'];
|
||||
/** Margin mode of the party, cross margin or isolated margin */
|
||||
marginMode: MarginMode;
|
||||
/** Market in which the margin is required for this party */
|
||||
marketId: Scalars['ID'];
|
||||
/** When in isolated margin, the required order margin level, otherwise, 0 */
|
||||
orderMarginLevel: Scalars['String'];
|
||||
/** The party for this margin */
|
||||
partyId: Scalars['ID'];
|
||||
/** If the margin is between maintenance and search, the network will initiate a collateral search (unsigned integer) */
|
||||
@@ -2020,6 +2032,13 @@ export type MarginLevelsUpdate = {
|
||||
timestamp: Scalars['Timestamp'];
|
||||
};
|
||||
|
||||
export enum MarginMode {
|
||||
/** Party is in cross margin mode */
|
||||
MARGIN_MODE_CROSS_MARGIN = 'MARGIN_MODE_CROSS_MARGIN',
|
||||
/** Party is in isolated margin mode */
|
||||
MARGIN_MODE_ISOLATED_MARGIN = 'MARGIN_MODE_ISOLATED_MARGIN'
|
||||
}
|
||||
|
||||
/** Represents a product & associated parameters that can be traded on Vega, has an associated OrderBook and Trade history */
|
||||
export type Market = {
|
||||
__typename?: 'Market';
|
||||
@@ -3118,6 +3137,8 @@ export enum OrderRejectionReason {
|
||||
ORDER_ERROR_INVALID_TIME_IN_FORCE = 'ORDER_ERROR_INVALID_TIME_IN_FORCE',
|
||||
/** Invalid type */
|
||||
ORDER_ERROR_INVALID_TYPE = 'ORDER_ERROR_INVALID_TYPE',
|
||||
/** Party has insufficient funds to cover for the order margin for the new or amended order */
|
||||
ORDER_ERROR_ISOLATED_MARGIN_CHECK_FAILED = 'ORDER_ERROR_ISOLATED_MARGIN_CHECK_FAILED',
|
||||
/** Margin check failed - not enough available margin */
|
||||
ORDER_ERROR_MARGIN_CHECK_FAILED = 'ORDER_ERROR_MARGIN_CHECK_FAILED',
|
||||
/** Market is closed */
|
||||
@@ -3138,6 +3159,8 @@ export enum OrderRejectionReason {
|
||||
ORDER_ERROR_OFFSET_MUST_BE_GREATER_THAN_ZERO = 'ORDER_ERROR_OFFSET_MUST_BE_GREATER_THAN_ZERO',
|
||||
/** Order is out of sequence */
|
||||
ORDER_ERROR_OUT_OF_SEQUENCE = 'ORDER_ERROR_OUT_OF_SEQUENCE',
|
||||
/** Pegged orders are not allowed for a party in isolated margin mode */
|
||||
ORDER_ERROR_PEGGED_ORDERS_NOT_ALLOWED_IN_ISOLATED_MARGIN_MODE = 'ORDER_ERROR_PEGGED_ORDERS_NOT_ALLOWED_IN_ISOLATED_MARGIN_MODE',
|
||||
/** A post-only order would produce an aggressive trade and thus it has been rejected */
|
||||
ORDER_ERROR_POST_ONLY_ORDER_WOULD_TRADE = 'ORDER_ERROR_POST_ONLY_ORDER_WOULD_TRADE',
|
||||
/** A reduce-ony order would not reduce the party's position and thus it has been rejected */
|
||||
@@ -3586,6 +3609,41 @@ export type PartyLockedBalance = {
|
||||
untilEpoch: Scalars['Int'];
|
||||
};
|
||||
|
||||
/** Margin mode selected for the given party and market. */
|
||||
export type PartyMarginMode = {
|
||||
__typename?: 'PartyMarginMode';
|
||||
/** Epoch at which the update happened. */
|
||||
atEpoch: Scalars['Int'];
|
||||
/** Selected margin mode. */
|
||||
marginMode: MarginMode;
|
||||
/** Margin factor for the market. Isolated mode only. */
|
||||
margin_factor?: Maybe<Scalars['String']>;
|
||||
/** Unique ID of the market. */
|
||||
marketId: Scalars['ID'];
|
||||
/** Maximum theoretical leverage for the market. Isolated mode only. */
|
||||
max_theoretical_leverage?: Maybe<Scalars['String']>;
|
||||
/** Minimum theoretical margin factor for the market. Isolated mode only. */
|
||||
min_theoretical_margin_factor?: Maybe<Scalars['String']>;
|
||||
/** Unique ID of the party. */
|
||||
partyId: Scalars['ID'];
|
||||
};
|
||||
|
||||
/** Edge type containing the deposit and cursor information returned by a PartyMarginModeConnection */
|
||||
export type PartyMarginModeEdge = {
|
||||
__typename?: 'PartyMarginModeEdge';
|
||||
cursor: Scalars['String'];
|
||||
node: PartyMarginMode;
|
||||
};
|
||||
|
||||
/** Connection type for retrieving cursor-based paginated party margin modes information */
|
||||
export type PartyMarginModesConnection = {
|
||||
__typename?: 'PartyMarginModesConnection';
|
||||
/** The party margin modes */
|
||||
edges?: Maybe<Array<Maybe<PartyMarginModeEdge>>>;
|
||||
/** The pagination information */
|
||||
pageInfo?: Maybe<PageInfo>;
|
||||
};
|
||||
|
||||
/**
|
||||
* All staking information related to a Party.
|
||||
* Contains the current recognised balance by the network and
|
||||
@@ -4438,6 +4496,12 @@ export type Query = {
|
||||
partiesConnection?: Maybe<PartyConnection>;
|
||||
/** An entity that is trading on the Vega network */
|
||||
party?: Maybe<Party>;
|
||||
/**
|
||||
* List margin modes per party per market
|
||||
*
|
||||
* Get a list of all margin modes, or for a specific market ID, or party ID.
|
||||
*/
|
||||
partyMarginModes?: Maybe<PartyMarginModesConnection>;
|
||||
/** Fetch all positions */
|
||||
positions?: Maybe<PositionConnection>;
|
||||
/** A governance proposal located by either its ID or reference. If both are set, ID is used. */
|
||||
@@ -6211,6 +6275,8 @@ export enum TransferType {
|
||||
TRANSFER_TYPE_INFRASTRUCTURE_FEE_DISTRIBUTE = 'TRANSFER_TYPE_INFRASTRUCTURE_FEE_DISTRIBUTE',
|
||||
/** Infrastructure fee paid from general account */
|
||||
TRANSFER_TYPE_INFRASTRUCTURE_FEE_PAY = 'TRANSFER_TYPE_INFRASTRUCTURE_FEE_PAY',
|
||||
/** Funds moved from order margin account to margin account. */
|
||||
TRANSFER_TYPE_ISOLATED_MARGIN_LOW = 'TRANSFER_TYPE_ISOLATED_MARGIN_LOW',
|
||||
/** Allocates liquidity fee earnings to each liquidity provider's network controlled liquidity fee account. */
|
||||
TRANSFER_TYPE_LIQUIDITY_FEE_ALLOCATE = 'TRANSFER_TYPE_LIQUIDITY_FEE_ALLOCATE',
|
||||
/** Liquidity fee received into general account */
|
||||
@@ -6237,6 +6303,10 @@ export enum TransferType {
|
||||
TRANSFER_TYPE_MTM_LOSS = 'TRANSFER_TYPE_MTM_LOSS',
|
||||
/** Funds added to margin account after mark to market gain */
|
||||
TRANSFER_TYPE_MTM_WIN = 'TRANSFER_TYPE_MTM_WIN',
|
||||
/** Funds released from order margin account to general. */
|
||||
TRANSFER_TYPE_ORDER_MARGIN_HIGH = 'TRANSFER_TYPE_ORDER_MARGIN_HIGH',
|
||||
/** Funds moved from general account to order margin account. */
|
||||
TRANSFER_TYPE_ORDER_MARGIN_LOW = 'TRANSFER_TYPE_ORDER_MARGIN_LOW',
|
||||
/** Funds deducted from margin account after a perpetuals funding loss. */
|
||||
TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS = 'TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS',
|
||||
/** Funds added to margin account after a perpetuals funding gain. */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { toastIconMapping } from '../toast';
|
||||
import { Intent } from '../../utils/intent';
|
||||
import { Icon } from '../icon';
|
||||
import { Icon, VegaIcon, VegaIconNames } from '../icon';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
export const SHORT = '!px-1 !py-1 min-h-fit';
|
||||
@@ -23,8 +23,8 @@ export const NotificationBanner = ({
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex items-center px-1 py-3 border-b min-h-[56px]',
|
||||
'text-[12px] leading-[16px] font-normal',
|
||||
'flex items-center border-b px-2',
|
||||
'text-xs leading-tight font-normal',
|
||||
{
|
||||
'bg-vega-light-100 dark:bg-vega-dark-100 ': intent === Intent.None,
|
||||
'bg-vega-blue-300 dark:bg-vega-blue-700': intent === Intent.Primary,
|
||||
@@ -72,15 +72,15 @@ export const NotificationBanner = ({
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<div className="grow">{children}</div>
|
||||
<div className="grow py-2">{children}</div>
|
||||
{onClose ? (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="notification-banner-close"
|
||||
onClick={onClose}
|
||||
className="ml-2"
|
||||
className="p-2 -mr-2 dark:text-white"
|
||||
>
|
||||
<Icon name="cross" size={4} className="dark:text-white" />
|
||||
<VegaIcon name={VegaIconNames.CROSS} size={14} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -448,7 +448,24 @@ export type CreateReferralSet = {
|
||||
};
|
||||
};
|
||||
|
||||
export enum MarginMode {
|
||||
/** Party is in cross margin mode */
|
||||
MARGIN_MODE_CROSS_MARGIN = 1,
|
||||
/** Party is in isolated margin mode */
|
||||
MARGIN_MODE_ISOLATED_MARGIN = 'MARGIN_MODE_ISOLATED_MARGIN',
|
||||
}
|
||||
export interface UpdateMarginMode {
|
||||
market_id: string;
|
||||
mode: MarginMode;
|
||||
marginFactor?: string;
|
||||
}
|
||||
|
||||
export interface UpdateMarginModeBody {
|
||||
updateMarginMode: UpdateMarginMode;
|
||||
}
|
||||
|
||||
export type Transaction =
|
||||
| UpdateMarginModeBody
|
||||
| StopOrdersSubmissionBody
|
||||
| StopOrdersCancellationBody
|
||||
| OrderSubmissionBody
|
||||
@@ -465,6 +482,10 @@ export type Transaction =
|
||||
| ApplyReferralCode
|
||||
| CreateReferralSet;
|
||||
|
||||
export const isMarginModeUpdateTransaction = (
|
||||
transaction: Transaction
|
||||
): transaction is UpdateMarginModeBody => 'updateMarginMode' in transaction;
|
||||
|
||||
export const isWithdrawTransaction = (
|
||||
transaction: Transaction
|
||||
): transaction is WithdrawSubmissionBody => 'withdrawSubmission' in transaction;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
isStopOrdersSubmissionTransaction,
|
||||
isStopOrdersCancellationTransaction,
|
||||
determineId,
|
||||
isMarginModeUpdateTransaction,
|
||||
} from '@vegaprotocol/wallet';
|
||||
|
||||
import { create } from 'zustand';
|
||||
@@ -58,7 +59,7 @@ export interface VegaTransactionStore {
|
||||
|
||||
export const useVegaTransactionStore = create<VegaTransactionStore>()(
|
||||
subscribeWithSelector((set, get) => ({
|
||||
transactions: [] as VegaStoredTxState[],
|
||||
transactions: [] as (VegaStoredTxState | undefined)[],
|
||||
create: (body: Transaction, order?: OrderTxUpdateFieldsFragment) => {
|
||||
const transactions = get().transactions;
|
||||
const now = new Date();
|
||||
@@ -205,16 +206,23 @@ export const useVegaTransactionStore = create<VegaTransactionStore>()(
|
||||
isStopOrdersCancellationTransaction(transaction.body);
|
||||
const isConfirmedStopOrderSubmission =
|
||||
isStopOrdersSubmissionTransaction(transaction.body);
|
||||
const isConfirmedMarginModeTransaction =
|
||||
isMarginModeUpdateTransaction(transaction.body);
|
||||
|
||||
if (
|
||||
(isConfirmedOrderCancellation ||
|
||||
isConfirmedTransfer ||
|
||||
isConfirmedStopOrderCancellation ||
|
||||
isConfirmedStopOrderSubmission) &&
|
||||
!transactionResult.error &&
|
||||
transactionResult.status
|
||||
isConfirmedOrderCancellation ||
|
||||
isConfirmedTransfer ||
|
||||
isConfirmedStopOrderCancellation ||
|
||||
isConfirmedStopOrderSubmission ||
|
||||
isConfirmedMarginModeTransaction
|
||||
//transactionResult.status
|
||||
) {
|
||||
transaction.status = VegaTxStatus.Complete;
|
||||
if (transactionResult.error) {
|
||||
transaction.status = VegaTxStatus.Error;
|
||||
transaction.error = new Error(transactionResult.error);
|
||||
} else {
|
||||
transaction.status = VegaTxStatus.Complete;
|
||||
}
|
||||
}
|
||||
transaction.dialogOpen = true;
|
||||
transaction.updatedAt = new Date();
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
OrderSubmission,
|
||||
StopOrdersSubmission,
|
||||
StopOrderSetup,
|
||||
UpdateMarginMode,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type {
|
||||
OrderTxUpdateFieldsFragment,
|
||||
@@ -26,6 +27,8 @@ import {
|
||||
isStopOrdersSubmissionTransaction,
|
||||
isStopOrdersCancellationTransaction,
|
||||
isReferralRelatedTransaction,
|
||||
isMarginModeUpdateTransaction,
|
||||
MarginMode,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { useVegaTransactionStore } from './use-vega-transaction-store';
|
||||
import { VegaTxStatus } from './types';
|
||||
@@ -163,6 +166,7 @@ const isClosePositionTransaction = (tx: VegaStoredTxState) => {
|
||||
};
|
||||
|
||||
const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
|
||||
const marginModeUpdate = isMarginModeUpdateTransaction(tx.body);
|
||||
const withdraw = isWithdrawTransaction(tx.body);
|
||||
const submitOrder = isOrderSubmissionTransaction(tx.body);
|
||||
const cancelOrder = isOrderCancellationTransaction(tx.body);
|
||||
@@ -173,6 +177,7 @@ const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
|
||||
const transfer = isTransferTransaction(tx.body);
|
||||
const referral = isReferralRelatedTransaction(tx.body);
|
||||
return (
|
||||
marginModeUpdate ||
|
||||
withdraw ||
|
||||
submitOrder ||
|
||||
cancelOrder ||
|
||||
@@ -445,6 +450,27 @@ const CancelOrderDetails = ({
|
||||
);
|
||||
};
|
||||
|
||||
const MarginModeDetails = ({ data }: { data: UpdateMarginMode }) => {
|
||||
const t = useT();
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
const marketId = data.market_id;
|
||||
const market = marketId && markets?.[marketId];
|
||||
if (!market) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Panel>
|
||||
<h4>{t('Update margin mode')}</h4>
|
||||
<p>{market?.tradableInstrument.instrument.code}</p>
|
||||
{data.mode === MarginMode.MARGIN_MODE_CROSS_MARGIN
|
||||
? t('Cross margin mode')
|
||||
: t('Isolated margin mode {{leverage}}x', {
|
||||
leverage: 1 / Number(data.marginFactor),
|
||||
})}
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const CancelStopOrderDetails = ({ stopOrderId }: { stopOrderId: string }) => {
|
||||
const t = useT();
|
||||
const formatTrigger = useFormatTrigger();
|
||||
@@ -598,6 +624,10 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isMarginModeUpdateTransaction(tx.body)) {
|
||||
return <MarginModeDetails data={tx.body.updateMarginMode} />;
|
||||
}
|
||||
|
||||
if (isClosePositionTransaction(tx)) {
|
||||
const transaction = tx.body as BatchMarketInstructionSubmissionBody;
|
||||
const marketId = first(
|
||||
|
||||
Reference in New Issue
Block a user