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 { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
|
||||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
||||||
@@ -72,17 +72,6 @@ export const MarketPage = () => {
|
|||||||
}
|
}
|
||||||
}, [update, lastMarketId, data?.id]);
|
}, [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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Splash>
|
<Splash>
|
||||||
@@ -118,6 +107,8 @@ export const MarketPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pinnedAsset = data && getAsset(data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TitleUpdater
|
<TitleUpdater
|
||||||
@@ -125,7 +116,11 @@ export const MarketPage = () => {
|
|||||||
marketName={data?.tradableInstrument.instrument.name}
|
marketName={data?.tradableInstrument.instrument.name}
|
||||||
decimalPlaces={data?.decimalPlaces}
|
decimalPlaces={data?.decimalPlaces}
|
||||||
/>
|
/>
|
||||||
{tradeView}
|
{largeScreen ? (
|
||||||
|
<TradeGrid market={data} pinnedAsset={pinnedAsset} />
|
||||||
|
) : (
|
||||||
|
<TradePanels market={data} pinnedAsset={pinnedAsset} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { memo } from 'react';
|
import { memo, type ReactNode } from 'react';
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import { LayoutPriority } from 'allotment';
|
import { LayoutPriority } from 'allotment';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||||
import { OracleBanner, useMarket } from '@vegaprotocol/markets';
|
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
||||||
import type { Market } from '@vegaprotocol/markets';
|
import { type Market } from '@vegaprotocol/markets';
|
||||||
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
|
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
|
||||||
import {
|
import {
|
||||||
ResizableGrid,
|
ResizableGrid,
|
||||||
@@ -13,31 +12,19 @@ import {
|
|||||||
usePaneLayout,
|
usePaneLayout,
|
||||||
} from '../../components/resizable-grid';
|
} from '../../components/resizable-grid';
|
||||||
import { TradingViews } from './trade-views';
|
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 { useT } from '../../lib/use-t';
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
|
import { MarketBanner } from '../../components/market-banner';
|
||||||
|
|
||||||
interface TradeGridProps {
|
interface TradeGridProps {
|
||||||
market: Market | null;
|
market: Market;
|
||||||
pinnedAsset?: PinnedAsset;
|
pinnedAsset?: PinnedAsset | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MainGrid = memo(
|
const MainGrid = memo(
|
||||||
({
|
({ market, pinnedAsset }: { market: Market; pinnedAsset?: PinnedAsset }) => {
|
||||||
marketId,
|
|
||||||
pinnedAsset,
|
|
||||||
}: {
|
|
||||||
marketId: string;
|
|
||||||
pinnedAsset?: PinnedAsset;
|
|
||||||
}) => {
|
|
||||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { data: market } = useMarket(marketId);
|
|
||||||
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'top' });
|
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'top' });
|
||||||
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
|
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
|
||||||
id: 'middle-1',
|
id: 'middle-1',
|
||||||
@@ -65,17 +52,17 @@ const MainGrid = memo(
|
|||||||
menu={<TradingViews.chart.menu />}
|
menu={<TradingViews.chart.menu />}
|
||||||
>
|
>
|
||||||
<ErrorBoundary feature="chart">
|
<ErrorBoundary feature="chart">
|
||||||
<TradingViews.chart.component marketId={marketId} />
|
<TradingViews.chart.component marketId={market.id} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="depth" name={t('Depth')}>
|
<Tab id="depth" name={t('Depth')}>
|
||||||
<ErrorBoundary feature="depth">
|
<ErrorBoundary feature="depth">
|
||||||
<TradingViews.depth.component marketId={marketId} />
|
<TradingViews.depth.component marketId={market.id} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="liquidity" name={t('Liquidity')}>
|
<Tab id="liquidity" name={t('Liquidity')}>
|
||||||
<ErrorBoundary feature="liquidity">
|
<ErrorBoundary feature="liquidity">
|
||||||
<TradingViews.liquidity.component marketId={marketId} />
|
<TradingViews.liquidity.component marketId={market.id} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
{market &&
|
{market &&
|
||||||
@@ -83,7 +70,7 @@ const MainGrid = memo(
|
|||||||
'Perpetual' ? (
|
'Perpetual' ? (
|
||||||
<Tab id="funding-history" name={t('Funding history')}>
|
<Tab id="funding-history" name={t('Funding history')}>
|
||||||
<ErrorBoundary feature="funding-history">
|
<ErrorBoundary feature="funding-history">
|
||||||
<TradingViews.funding.component marketId={marketId} />
|
<TradingViews.funding.component marketId={market.id} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -97,7 +84,7 @@ const MainGrid = memo(
|
|||||||
>
|
>
|
||||||
<ErrorBoundary feature="funding-payments">
|
<ErrorBoundary feature="funding-payments">
|
||||||
<TradingViews.fundingPayments.component
|
<TradingViews.fundingPayments.component
|
||||||
marketId={marketId}
|
marketId={market.id}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -113,7 +100,7 @@ const MainGrid = memo(
|
|||||||
<Tabs storageKey="console-trade-grid-main-right">
|
<Tabs storageKey="console-trade-grid-main-right">
|
||||||
<Tab id="orderbook" name={t('Orderbook')}>
|
<Tab id="orderbook" name={t('Orderbook')}>
|
||||||
<ErrorBoundary feature="orderbook">
|
<ErrorBoundary feature="orderbook">
|
||||||
<TradingViews.orderbook.component marketId={marketId} />
|
<TradingViews.orderbook.component marketId={market.id} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
@@ -122,7 +109,7 @@ const MainGrid = memo(
|
|||||||
settings={<TradingViews.trades.settings />}
|
settings={<TradingViews.trades.settings />}
|
||||||
>
|
>
|
||||||
<ErrorBoundary feature="trades">
|
<ErrorBoundary feature="trades">
|
||||||
<TradingViews.trades.component marketId={marketId} />
|
<TradingViews.trades.component marketId={market.id} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -225,7 +212,6 @@ const MainGrid = memo(
|
|||||||
MainGrid.displayName = 'MainGrid';
|
MainGrid.displayName = 'MainGrid';
|
||||||
|
|
||||||
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
||||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
|
||||||
const wrapperClasses = classNames(
|
const wrapperClasses = classNames(
|
||||||
'h-full grid',
|
'h-full grid',
|
||||||
'grid-rows-[min-content_1fr]'
|
'grid-rows-[min-content_1fr]'
|
||||||
@@ -234,17 +220,10 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className={wrapperClasses}>
|
<div className={wrapperClasses}>
|
||||||
<div>
|
<div>
|
||||||
{featureFlags.SUCCESSOR_MARKETS && (
|
<MarketBanner market={market} />
|
||||||
<>
|
|
||||||
<MarketSuccessorBanner market={market} />
|
|
||||||
<MarketSuccessorProposalBanner marketId={market?.id} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<MarketTerminationBanner market={market} />
|
|
||||||
<OracleBanner marketId={market?.id || ''} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 p-0.5">
|
<div className="min-h-0 p-0.5">
|
||||||
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
|
<MainGrid market={market} pinnedAsset={pinnedAsset} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
||||||
import { type Market } from '@vegaprotocol/markets';
|
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 { useState } from 'react';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@@ -11,23 +12,17 @@ import {
|
|||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import {
|
import { MarketBanner } from '../../components/market-banner';
|
||||||
MarketSuccessorBanner,
|
|
||||||
MarketSuccessorProposalBanner,
|
|
||||||
MarketTerminationBanner,
|
|
||||||
} from '../../components/market-banner';
|
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
import { type TradingView } from './trade-views';
|
import { type TradingView } from './trade-views';
|
||||||
import { TradingViews } from './trade-views';
|
import { TradingViews } from './trade-views';
|
||||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
|
||||||
|
|
||||||
interface TradePanelsProps {
|
interface TradePanelsProps {
|
||||||
market: Market | null;
|
market: Market;
|
||||||
pinnedAsset?: PinnedAsset;
|
pinnedAsset?: PinnedAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
|
||||||
const [view, setView] = useState<TradingView>('chart');
|
const [view, setView] = useState<TradingView>('chart');
|
||||||
const viewCfg = TradingViews[view];
|
const viewCfg = TradingViews[view];
|
||||||
|
|
||||||
@@ -76,14 +71,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
|
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
|
||||||
<div>
|
<div>
|
||||||
{featureFlags.SUCCESSOR_MARKETS && (
|
<MarketBanner market={market} />
|
||||||
<>
|
|
||||||
<MarketSuccessorBanner market={market} />
|
|
||||||
<MarketSuccessorProposalBanner marketId={market?.id} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<MarketTerminationBanner market={market} />
|
|
||||||
<OracleBanner marketId={market?.id || ''} />
|
|
||||||
</div>
|
</div>
|
||||||
<div>{renderMenu()}</div>
|
<div>{renderMenu()}</div>
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
export * from './market-successor-banner';
|
export { MarketBanner } from './market-banner';
|
||||||
export * from './market-successor-proposal-banner';
|
|
||||||
export * from './market-termination-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 { render, screen } from '@testing-library/react';
|
||||||
import type { SingleExecutionResult } from '@apollo/client';
|
|
||||||
import type { MockedResponse } from '@apollo/react-testing';
|
|
||||||
import { MockedProvider } from '@apollo/react-testing';
|
|
||||||
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
||||||
import type { MarketViewProposalsQuery } from '@vegaprotocol/proposals';
|
import { ProposalState } from '@vegaprotocol/types';
|
||||||
import { MarketViewProposalsDocument } from '@vegaprotocol/proposals';
|
|
||||||
import * as Types from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
const marketProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
const proposal = {
|
||||||
request: {
|
__typename: 'Proposal' as const,
|
||||||
query: MarketViewProposalsDocument,
|
id: 'proposal-1',
|
||||||
variables: {
|
state: ProposalState.STATE_OPEN,
|
||||||
inState: Types.ProposalState.STATE_OPEN,
|
terms: {
|
||||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
__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: {
|
const proposal2 = {
|
||||||
proposalsConnection: {
|
__typename: 'Proposal' as const,
|
||||||
edges: [
|
id: 'proposal-2',
|
||||||
{
|
state: ProposalState.STATE_OPEN,
|
||||||
node: {
|
terms: {
|
||||||
__typename: 'Proposal',
|
__typename: 'ProposalTerms' as const,
|
||||||
id: 'proposal-1',
|
closingDatetime: '2023-09-27',
|
||||||
state: Types.ProposalState.STATE_OPEN,
|
enactmentDatetime: '2023-09-28',
|
||||||
terms: {
|
change: {
|
||||||
__typename: 'ProposalTerms',
|
__typename: 'NewMarket' as const,
|
||||||
closingDatetime: '2023-09-27',
|
instrument: {
|
||||||
enactmentDatetime: '2023-09-28',
|
name: 'New second proposal of the market successor',
|
||||||
change: {
|
},
|
||||||
__typename: 'NewMarket',
|
successorConfiguration: {
|
||||||
instrument: {
|
parentMarketId: 'marketId',
|
||||||
name: 'New proposal of the market successor',
|
|
||||||
},
|
|
||||||
successorConfiguration: {
|
|
||||||
parentMarketId: 'marketId',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('MarketSuccessorProposalBanner', () => {
|
describe('MarketSuccessorProposalBanner', () => {
|
||||||
it('should display single proposal', async () => {
|
it('should display single proposal', () => {
|
||||||
render(
|
render(<MarketSuccessorProposalBanner proposals={[proposal]} />);
|
||||||
<MockedProvider mocks={[marketProposalMock]}>
|
|
||||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
screen.getByText('A successors to this market has been proposed')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
expect(
|
expect(
|
||||||
screen
|
screen.getByText('A successor to this market has been proposed')
|
||||||
.getByRole('link')
|
).toBeInTheDocument();
|
||||||
.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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
render(
|
expect(screen.getByRole('link')).toHaveAttribute(
|
||||||
<MockedProvider mocks={[dualProposalMock]}>
|
'href',
|
||||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
expect.stringContaining(proposal.id)
|
||||||
</MockedProvider>
|
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
});
|
||||||
expect(
|
|
||||||
screen.getByText('Successors to this market have been proposed')
|
it('should display plural proposals', () => {
|
||||||
).toBeInTheDocument();
|
const proposals = [proposal, proposal2];
|
||||||
});
|
render(<MarketSuccessorProposalBanner proposals={proposals} />);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen
|
screen.getByText('Successors to this market have been proposed')
|
||||||
.getAllByRole('link')[0]
|
).toBeInTheDocument();
|
||||||
.getAttribute('href')
|
|
||||||
?.endsWith('/proposals/proposal-1') ?? false
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
screen
|
|
||||||
.getAllByRole('link')[1]
|
|
||||||
.getAttribute('href')
|
|
||||||
?.endsWith('/proposals/proposal-2') ?? false
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('banner should be hidden because no proposals', () => {
|
const links = screen.getAllByRole('link');
|
||||||
const { container } = render(
|
expect(links).toHaveLength(proposals.length);
|
||||||
<MockedProvider>
|
expect(links[0]).toHaveAttribute(
|
||||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
'href',
|
||||||
</MockedProvider>
|
expect.stringContaining(proposal.id)
|
||||||
);
|
);
|
||||||
expect(container).toBeEmptyDOMElement();
|
expect(links[1]).toHaveAttribute(
|
||||||
});
|
'href',
|
||||||
|
expect.stringContaining(proposal2.id)
|
||||||
it('banner should be hidden because no proposals for the market', () => {
|
|
||||||
const { container } = render(
|
|
||||||
<MockedProvider mocks={[marketProposalMock]}>
|
|
||||||
<MarketSuccessorProposalBanner marketId="otherMarketId" />
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
);
|
||||||
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 { Fragment } from 'react';
|
||||||
import {
|
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||||
marketViewProposalsDataProvider,
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
type NewMarketSuccessorFieldsFragment,
|
|
||||||
} from '@vegaprotocol/proposals';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ExternalLink,
|
|
||||||
Intent,
|
|
||||||
NotificationBanner,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
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';
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
export const MarketSuccessorProposalBanner = ({
|
export const MarketSuccessorProposalBanner = ({
|
||||||
marketId,
|
proposals,
|
||||||
}: {
|
}: {
|
||||||
marketId?: string;
|
proposals: MarketViewProposalFieldsFragment[];
|
||||||
}) => {
|
}) => {
|
||||||
const t = useT();
|
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);
|
const tokenLink = useLinks(DApp.Governance);
|
||||||
if (visible && successors.length) {
|
|
||||||
return (
|
return (
|
||||||
<NotificationBanner
|
<div>
|
||||||
intent={Intent.Primary}
|
<div className="uppercase mb-1">
|
||||||
onClose={() => {
|
{proposals.length === 1
|
||||||
setVisible(false);
|
? t('A successor to this market has been proposed')
|
||||||
}}
|
: t('Successors to this market have been proposed')}
|
||||||
>
|
</div>
|
||||||
<div className="uppercase mb-1">
|
<div>
|
||||||
{successors.length === 1
|
{t(
|
||||||
? t('A successors to this market has been proposed')
|
'checkOutProposalsAndVote',
|
||||||
: t('Successors to this market have been proposed')}
|
'Check out the terms of the proposals and vote:',
|
||||||
</div>
|
{
|
||||||
<div>
|
count: proposals.length,
|
||||||
{t(
|
}
|
||||||
'checkOutProposalsAndVote',
|
)}{' '}
|
||||||
'Check out the terms of the proposals and vote:',
|
{proposals.map((item, i) => {
|
||||||
{
|
if (item.terms.change.__typename !== 'NewMarket') {
|
||||||
count: successors.length,
|
return null;
|
||||||
}
|
}
|
||||||
)}{' '}
|
|
||||||
{successors.map((item, i) => {
|
const externalLink = tokenLink(
|
||||||
const externalLink = tokenLink(
|
TOKEN_PROPOSAL.replace(':id', item.id || '')
|
||||||
TOKEN_PROPOSAL.replace(':id', item.id || '')
|
);
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={i}>
|
<Fragment key={i}>
|
||||||
<ExternalLink href={externalLink} key={i}>
|
<ExternalLink href={externalLink} key={i}>
|
||||||
{
|
{item.terms.change.instrument.name}
|
||||||
(item.terms?.change as NewMarketSuccessorFieldsFragment)
|
</ExternalLink>
|
||||||
?.instrument.name
|
{i < proposals.length - 1 && ', '}
|
||||||
}
|
</Fragment>
|
||||||
</ExternalLink>
|
);
|
||||||
{i < successors.length - 1 && ', '}
|
})}
|
||||||
</Fragment>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
);
|
||||||
</div>
|
|
||||||
</NotificationBanner>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { 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 sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import { format, formatDuration, intervalToDuration } from 'date-fns';
|
||||||
|
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||||
|
import { ProposalState } from '@vegaprotocol/types';
|
||||||
import {
|
import {
|
||||||
DApp,
|
DApp,
|
||||||
TOKEN_PROPOSAL,
|
TOKEN_PROPOSAL,
|
||||||
TOKEN_PROPOSALS,
|
TOKEN_PROPOSALS,
|
||||||
useLinks,
|
useLinks,
|
||||||
} from '@vegaprotocol/environment';
|
} 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 { useT } from '../../lib/use-t';
|
||||||
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
const filterProposals = (
|
export const MarketUpdateStateBanner = ({
|
||||||
data: MarketViewProposalFieldsFragment[] | null,
|
market,
|
||||||
marketId: string,
|
proposals,
|
||||||
now: number
|
}: {
|
||||||
) =>
|
market: Market;
|
||||||
sortBy(
|
proposals: MarketViewProposalFieldsFragment[];
|
||||||
(data || []).filter(
|
}) => {
|
||||||
(item) =>
|
const t = useT();
|
||||||
item.terms.change.__typename === 'UpdateMarketState' &&
|
const governanceLink = useLinks(DApp.Governance);
|
||||||
item.terms.change.market.id === marketId &&
|
|
||||||
item.terms.change.updateType ===
|
const openProposals = sortBy(
|
||||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
|
proposals.filter((p) => p.state === ProposalState.STATE_OPEN),
|
||||||
item.terms.enactmentDatetime &&
|
(p) => p.terms.enactmentDatetime
|
||||||
new Date(item.terms.enactmentDatetime).getTime() > now
|
|
||||||
),
|
|
||||||
(item) => item.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 getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
|
||||||
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
|
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
|
||||||
@@ -62,126 +138,3 @@ const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
|
|||||||
price,
|
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 };
|
||||||
|
};
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||||
VEGA_VERSION=v0.73.9
|
VEGA_VERSION=v0.73.10
|
||||||
LOCAL_SERVER=false
|
LOCAL_SERVER=false
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
|
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
|
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
|
||||||
VEGA_VERSION=v0.73.8
|
VEGA_VERSION=v0.73.10
|
||||||
LOCAL_SERVER=false
|
LOCAL_SERVER=false
|
||||||
@@ -8,6 +8,9 @@ import docker
|
|||||||
import http.server
|
import http.server
|
||||||
import sys
|
import sys
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from docker.models.containers import Container
|
||||||
|
from docker.errors import APIError
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from vega_sim.null_service import VegaServiceNull, Ports
|
from vega_sim.null_service import VegaServiceNull, Ports
|
||||||
from playwright.sync_api import Browser, Page
|
from playwright.sync_api import Browser, Page
|
||||||
@@ -100,12 +103,16 @@ def init_vega(request=None):
|
|||||||
container = docker_client.containers.run(
|
container = docker_client.containers.run(
|
||||||
console_image_name, detach=True, ports={"80/tcp": vega.console_port}
|
console_image_name, detach=True, ports={"80/tcp": vega.console_port}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not isinstance(container, Container):
|
||||||
|
raise Exception("container instance invalid")
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Container {container.id} started",
|
f"Container {container.id} started",
|
||||||
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
|
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
|
||||||
)
|
)
|
||||||
yield vega
|
yield vega
|
||||||
except docker.errors.APIError as e:
|
except APIError as e:
|
||||||
logger.info(f"Container creation failed.")
|
logger.info(f"Container creation failed.")
|
||||||
logger.info(e)
|
logger.info(e)
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ class TestGetStarted:
|
|||||||
page.get_by_test_id("get-started-button").click()
|
page.get_by_test_id("get-started-button").click()
|
||||||
# Assert dialog isn't visible
|
# Assert dialog isn't visible
|
||||||
expect(page.get_by_test_id("welcome-dialog")).not_to_be_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")
|
@pytest.mark.usefixtures("risk_accepted")
|
||||||
def test_get_started_seen_already(self, simple_market, page: Page):
|
def test_get_started_seen_already(self, simple_market, page: Page):
|
||||||
page.goto(f"/#/markets/{simple_market}")
|
page.goto(f"/#/markets/{simple_market}")
|
||||||
@@ -177,10 +178,10 @@ class TestGetStarted:
|
|||||||
# 0007-FUGS-018
|
# 0007-FUGS-018
|
||||||
expect(page.get_by_test_id("welcome-dialog")).not_to_be_visible()
|
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):
|
def test_get_started_browse_all(self, simple_market, vega: VegaServiceNull, page: Page):
|
||||||
page.goto("/")
|
page.goto("/")
|
||||||
print(simple_market)
|
print(simple_market)
|
||||||
page.get_by_test_id("browse-markets-button").click()
|
page.get_by_test_id("browse-markets-button").click()
|
||||||
# 0007-FUGS-005
|
# 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}")
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ def test_market_selector(continuous_market, page: Page):
|
|||||||
# 6001-MARK-025
|
# 6001-MARK-025
|
||||||
btc_market = page.locator('[data-testid="market-selector-list"] a')
|
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("h3")).to_have_text("BTC:DAI_2023Futr")
|
||||||
expect(btc_market.locator('[data-testid="market-selector-volume"]')).to_have_text(
|
# tbd - 5465
|
||||||
"0.00"
|
# 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(
|
expect(btc_market.locator('[data-testid="market-selector-price"]')).to_have_text(
|
||||||
"107.50 tDAI"
|
"107.50 tDAI"
|
||||||
)
|
)
|
||||||
@@ -56,8 +57,9 @@ def test_market_selector_filter(continuous_market, page: Page):
|
|||||||
# 6001-MARK-029
|
# 6001-MARK-029
|
||||||
page.get_by_test_id("search-term").fill("btc")
|
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')).to_have_count(1)
|
||||||
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_have_text(
|
# tbd - 5465
|
||||||
"BTC:DAI_2023107.50 tDAI0.00"
|
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()
|
page.get_by_test_id("search-term").clear()
|
||||||
@@ -81,6 +83,7 @@ def test_market_selector_filter(continuous_market, page: Page):
|
|||||||
page.get_by_test_id("asset-trigger").click()
|
page.get_by_test_id("asset-trigger").click()
|
||||||
page.get_by_role("menuitemcheckbox").nth(0).get_by_text("tDAI").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')).to_have_count(1)
|
||||||
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_have_text(
|
# tbd - 5465
|
||||||
"BTC:DAI_2023107.50 tDAI0.00"
|
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 pytest
|
||||||
import re
|
import re
|
||||||
import vega_sim.api.governance as governance
|
|
||||||
from playwright.sync_api import Page, expect
|
from playwright.sync_api import Page, expect
|
||||||
from vega_sim.null_service import VegaServiceNull
|
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
|
import vega_sim.api.governance as governance
|
||||||
from actions.vega import submit_order
|
from actions.vega import submit_order
|
||||||
from actions.utils import next_epoch
|
from actions.utils import next_epoch
|
||||||
from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
|
from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
|
||||||
|
from vega_sim.api import governance
|
||||||
|
|
||||||
@pytest.mark.usefixtures("risk_accepted")
|
@pytest.mark.usefixtures("risk_accepted")
|
||||||
def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
|
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(trading_mode).to_have_text("Continuous")
|
||||||
expect(market_state).to_have_text("Active")
|
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
|
# put invalid oracle to trigger market termination
|
||||||
governance.submit_oracle_data(
|
governance.submit_oracle_data(
|
||||||
wallet=vega.wallet,
|
wallet=vega.wallet,
|
||||||
@@ -154,6 +191,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
|
|||||||
# check market state is now settled
|
# check market state is now settled
|
||||||
expect(trading_mode).to_have_text("No trading")
|
expect(trading_mode).to_have_text("No trading")
|
||||||
expect(market_state).to_have_text("Settled")
|
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")
|
""" @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,
|
forward_time_to_enactment = False,
|
||||||
price=110,
|
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")
|
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,
|
proposal_id=proposalID,
|
||||||
wallet=vega.wallet,
|
wallet=vega.wallet,
|
||||||
key_name="market_maker"
|
key_name="market_maker"
|
||||||
|
|
||||||
)
|
)
|
||||||
vega.forward("60s")
|
vega.forward("60s")
|
||||||
vega.wait_fn(10)
|
vega.wait_fn(10)
|
||||||
vega.wait_for_total_catchup()
|
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\."
|
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())
|
match_result = re.fullmatch(will_close_pattern, page.locator(".grow").inner_text())
|
||||||
assert match_result is not None
|
assert match_result is not None
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ def verify_order_value(
|
|||||||
expect(element).to_be_visible()
|
expect(element).to_be_visible()
|
||||||
if is_regex:
|
if is_regex:
|
||||||
actual_text = element.text_content()
|
actual_text = element.text_content()
|
||||||
|
|
||||||
|
if actual_text is None:
|
||||||
|
raise Exception(f"no text found for test_id {test_id}")
|
||||||
|
|
||||||
assert re.match(
|
assert re.match(
|
||||||
expected_text, actual_text
|
expected_text, actual_text
|
||||||
), f"Expected {expected_text}, but got {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,
|
approve_proposal=True,
|
||||||
forward_time_to_enactment=False,
|
forward_time_to_enactment=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
vega.forward("10s")
|
vega.forward("10s")
|
||||||
vega.wait_fn(1)
|
vega.wait_fn(1)
|
||||||
vega.wait_for_total_catchup()
|
vega.wait_for_total_catchup()
|
||||||
banner_text = page.get_by_test_id(
|
banner_text = page.get_by_test_id(
|
||||||
f"termination-warning-banner-{perpetual_market}"
|
f"update-state-banner-{perpetual_market}"
|
||||||
).text_content()
|
).text_content()
|
||||||
pattern = re.compile(
|
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(
|
assert pattern.search(
|
||||||
banner_text
|
banner_text
|
||||||
|
|||||||
@@ -1,46 +1,155 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from playwright.sync_api import Page, expect
|
from playwright.sync_api import Page, expect
|
||||||
from vega_sim.null_service import VegaServiceNull
|
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
|
||||||
|
|
||||||
|
market_banner = "market-banner"
|
||||||
@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
|
|
||||||
|
|
||||||
@pytest.mark.skip("tbd")
|
@pytest.mark.skip("tbd")
|
||||||
@pytest.mark.usefixtures("risk_accepted")
|
@pytest.mark.usefixtures("risk_accepted")
|
||||||
def test_succession_line(page: Page, successor_market):
|
def test_succession_line(vega: VegaServiceNull, page: Page):
|
||||||
page.goto(f"/#/markets/{successor_market}")
|
parent_market_id = setup_continuous_market(vega)
|
||||||
page.get_by_test_id("Info").click()
|
tdai_id = vega.find_asset_id(symbol="tDAI")
|
||||||
page.get_by_text("Succession line").click()
|
|
||||||
|
|
||||||
expect(page.get_by_test_id("succession-line-item").first).to_contain_text(
|
page.goto(f"/#/markets/{parent_market_id}")
|
||||||
"BTC:DAI_2023BTC:DAI_2023"
|
|
||||||
|
# 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(
|
expect(
|
||||||
page.get_by_test_id("succession-line-item").first.get_by_role("link")
|
succession_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")
|
|
||||||
).to_be_attached
|
).to_be_attached
|
||||||
expect(
|
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
|
).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
|
searchLevel
|
||||||
initialLevel
|
initialLevel
|
||||||
collateralReleaseLevel
|
collateralReleaseLevel
|
||||||
|
marginFactor
|
||||||
|
marginMode
|
||||||
|
orderMarginLevel
|
||||||
asset {
|
asset {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
@@ -33,6 +36,9 @@ subscription MarginsSubscription($partyId: ID!) {
|
|||||||
searchLevel
|
searchLevel
|
||||||
initialLevel
|
initialLevel
|
||||||
collateralReleaseLevel
|
collateralReleaseLevel
|
||||||
|
marginFactor
|
||||||
|
marginMode
|
||||||
|
orderMarginLevel
|
||||||
timestamp
|
timestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-3
@@ -3,21 +3,21 @@ import * as Types from '@vegaprotocol/types';
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
const defaultOptions = {} as const;
|
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<{
|
export type MarginsQueryVariables = Types.Exact<{
|
||||||
partyId: Types.Scalars['ID'];
|
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<{
|
export type MarginsSubscriptionSubscriptionVariables = Types.Exact<{
|
||||||
partyId: Types.Scalars['ID'];
|
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`
|
export const MarginFieldsFragmentDoc = gql`
|
||||||
fragment MarginFields on MarginLevels {
|
fragment MarginFields on MarginLevels {
|
||||||
@@ -25,6 +25,9 @@ export const MarginFieldsFragmentDoc = gql`
|
|||||||
searchLevel
|
searchLevel
|
||||||
initialLevel
|
initialLevel
|
||||||
collateralReleaseLevel
|
collateralReleaseLevel
|
||||||
|
marginFactor
|
||||||
|
marginMode
|
||||||
|
orderMarginLevel
|
||||||
asset {
|
asset {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
@@ -85,6 +88,9 @@ export const MarginsSubscriptionDocument = gql`
|
|||||||
searchLevel
|
searchLevel
|
||||||
initialLevel
|
initialLevel
|
||||||
collateralReleaseLevel
|
collateralReleaseLevel
|
||||||
|
marginFactor
|
||||||
|
marginMode
|
||||||
|
orderMarginLevel
|
||||||
timestamp
|
timestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ const update = (
|
|||||||
searchLevel: delta.searchLevel,
|
searchLevel: delta.searchLevel,
|
||||||
initialLevel: delta.initialLevel,
|
initialLevel: delta.initialLevel,
|
||||||
collateralReleaseLevel: delta.collateralReleaseLevel,
|
collateralReleaseLevel: delta.collateralReleaseLevel,
|
||||||
|
marginFactor: delta.marginFactor,
|
||||||
|
marginMode: delta.marginMode,
|
||||||
|
orderMarginLevel: delta.orderMarginLevel,
|
||||||
asset: {
|
asset: {
|
||||||
__typename: 'Asset',
|
__typename: 'Asset',
|
||||||
id: delta.asset,
|
id: delta.asset,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit';
|
|||||||
import { DealTicket } from './deal-ticket';
|
import { DealTicket } from './deal-ticket';
|
||||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||||
import { useT } from '../../use-t';
|
import { useT } from '../../use-t';
|
||||||
|
import { MarginModeSelector } from './margin-mode-selector';
|
||||||
|
|
||||||
interface DealTicketContainerProps {
|
interface DealTicketContainerProps {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
@@ -51,21 +52,26 @@ export const DealTicketContainer = ({
|
|||||||
reload={reload}
|
reload={reload}
|
||||||
>
|
>
|
||||||
{market && marketData ? (
|
{market && marketData ? (
|
||||||
featureFlags.STOP_ORDERS && showStopOrder ? (
|
<>
|
||||||
<StopOrder
|
<MarginModeSelector marketId={marketId} />
|
||||||
market={market}
|
{featureFlags.STOP_ORDERS && showStopOrder ? (
|
||||||
marketPrice={marketPrice}
|
<StopOrder
|
||||||
submit={(stopOrdersSubmission) => create({ stopOrdersSubmission })}
|
market={market}
|
||||||
/>
|
marketPrice={marketPrice}
|
||||||
) : (
|
submit={(stopOrdersSubmission) =>
|
||||||
<DealTicket
|
create({ stopOrdersSubmission })
|
||||||
{...props}
|
}
|
||||||
market={market}
|
/>
|
||||||
marketPrice={marketPrice}
|
) : (
|
||||||
marketData={marketData}
|
<DealTicket
|
||||||
submit={(orderSubmission) => create({ orderSubmission })}
|
{...props}
|
||||||
/>
|
market={market}
|
||||||
)
|
marketPrice={marketPrice}
|
||||||
|
marketData={marketData}
|
||||||
|
submit={(orderSubmission) => create({ orderSubmission })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p>{t('Could not load market')}</p>
|
<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 vol": "24h vol",
|
||||||
"24h volume": "24h volume",
|
"24h volume": "24h volume",
|
||||||
"A percentage of commission earned by the referrer": "A percentage of commission earned by the referrer",
|
"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",
|
"About the referral program": "About the referral program",
|
||||||
"Active": "Active",
|
"Active": "Active",
|
||||||
"All": "All",
|
"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>",
|
"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",
|
"Chart": "Chart",
|
||||||
"Change (24h)": "Change (24h)",
|
"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": "Chart",
|
||||||
"Chart by <0>TradingView</0>": "Chart by <0>TradingView</0>",
|
"Chart by <0>TradingView</0>": "Chart by <0>TradingView</0>",
|
||||||
"checkOutProposalsAndVote": "Check out the terms of the proposals and vote:",
|
"checkOutProposalsAndVote": "Check out the terms of the proposals and vote:",
|
||||||
@@ -306,9 +307,9 @@
|
|||||||
"TradingView": "TradingView",
|
"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 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 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 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}} 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}} will stop on {{date}}": "Trading on market {{name}} will stop on {{date}}",
|
||||||
"Transfer": "Transfer",
|
"Transfer": "Transfer",
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
"Unknown settlement date": "Unknown settlement date",
|
"Unknown settlement date": "Unknown settlement date",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from './last-24h-price-change';
|
export * from './last-24h-price-change';
|
||||||
export * from './last-24h-volume';
|
export * from './last-24h-volume';
|
||||||
export * from './market-info';
|
export * from './market-info';
|
||||||
export * from './oracle-banner';
|
|
||||||
export * from './oracle-basic-profile';
|
export * from './oracle-basic-profile';
|
||||||
export * from './oracle-full-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,
|
VegaIcon,
|
||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useOracleStatuses } from '../oracle-banner/oracle-statuses';
|
import { useOracleStatuses } from '../../hooks';
|
||||||
import type { IconName } from '@blueprintjs/icons';
|
import type { IconName } from '@blueprintjs/icons';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { getLinkIcon, useVerifiedStatusIcon } from '../oracle-basic-profile';
|
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-oracle-spec-binding-data';
|
||||||
export * from './use-candles';
|
export * from './use-candles';
|
||||||
export * from './use-successor-market';
|
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 }) => {
|
export const useCandles = ({ marketId }: { marketId?: string }) => {
|
||||||
const fiveDaysAgo = useFiveDaysAgo();
|
const fiveDaysAgo = useFiveDaysAgo();
|
||||||
const yesterday = useYesterday();
|
const yesterday = useYesterday();
|
||||||
|
const since = new Date(fiveDaysAgo).toISOString();
|
||||||
const { data, error } = useThrottledDataProvider({
|
const { data, error } = useThrottledDataProvider({
|
||||||
dataProvider: marketCandlesProvider,
|
dataProvider: marketCandlesProvider,
|
||||||
variables: {
|
variables: {
|
||||||
marketId: marketId || '',
|
marketId: marketId || '',
|
||||||
interval: Interval.INTERVAL_I1H,
|
interval: Interval.INTERVAL_I1H,
|
||||||
since: new Date(fiveDaysAgo).toISOString(),
|
since,
|
||||||
},
|
},
|
||||||
skip: !marketId,
|
skip: !marketId,
|
||||||
});
|
});
|
||||||
@@ -22,6 +23,7 @@ export const useCandles = ({ marketId }: { marketId?: string }) => {
|
|||||||
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
|
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
|
||||||
isCandleLessThan24hOld(candle, yesterday)
|
isCandleLessThan24hOld(candle, yesterday)
|
||||||
);
|
);
|
||||||
|
|
||||||
return { oneDayCandles, error, fiveDaysCandles };
|
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 = () => {
|
export const useOracleStatuses = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@@ -120,8 +120,7 @@ export const marketTradingModeProvider = makeDerivedDataProvider<
|
|||||||
MarketDataQueryVariables
|
MarketDataQueryVariables
|
||||||
>(
|
>(
|
||||||
[marketDataProvider],
|
[marketDataProvider],
|
||||||
(parts, variables, prevData) =>
|
(parts) => (parts[0] as ReturnType<typeof getData>)?.marketTradingMode
|
||||||
(parts[0] as ReturnType<typeof getData>)?.marketTradingMode
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const marketStateProvider = makeDerivedDataProvider<
|
export const marketStateProvider = makeDerivedDataProvider<
|
||||||
@@ -130,8 +129,7 @@ export const marketStateProvider = makeDerivedDataProvider<
|
|||||||
MarketDataQueryVariables
|
MarketDataQueryVariables
|
||||||
>(
|
>(
|
||||||
[marketDataProvider],
|
[marketDataProvider],
|
||||||
(parts, variables, prevData) =>
|
(parts) => (parts[0] as ReturnType<typeof getData>)?.marketState
|
||||||
(parts[0] as ReturnType<typeof getData>)?.marketState
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fundingRateProvider = makeDerivedDataProvider<
|
export const fundingRateProvider = makeDerivedDataProvider<
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './lib/__generated__/Positions';
|
export * from './lib/__generated__/Positions';
|
||||||
|
export * from './lib/margin-modes-provider';
|
||||||
export * from './lib/positions-data-providers';
|
export * from './lib/positions-data-providers';
|
||||||
export * from './lib/positions-table';
|
export * from './lib/positions-table';
|
||||||
export * from './lib/positions-manager';
|
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 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`
|
export const PositionFieldsFragmentDoc = gql`
|
||||||
fragment PositionFields on Position {
|
fragment PositionFields on Position {
|
||||||
realisedPNL
|
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`
|
export const PositionsDocument = gql`
|
||||||
query Positions($partyIds: [ID!]!) {
|
query Positions($partyIds: [ID!]!) {
|
||||||
positions(filter: {partyIds: $partyIds}) {
|
positions(filter: {partyIds: $partyIds}) {
|
||||||
@@ -191,4 +211,43 @@ export function useEstimatePositionLazyQuery(baseOptions?: Apollo.LazyQueryHookO
|
|||||||
}
|
}
|
||||||
export type EstimatePositionQueryHookResult = ReturnType<typeof useEstimatePositionQuery>;
|
export type EstimatePositionQueryHookResult = ReturnType<typeof useEstimatePositionQuery>;
|
||||||
export type EstimatePositionLazyQueryHookResult = ReturnType<typeof useEstimatePositionLazyQuery>;
|
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 {
|
... on UpdateNetworkParameter {
|
||||||
...UpdateNetworkParameterFields
|
...UpdateNetworkParameterFields
|
||||||
}
|
}
|
||||||
|
... on UpdateMarketState {
|
||||||
|
...UpdateMarketStateFields
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,6 +481,9 @@ fragment MarketViewProposalFields on Proposal {
|
|||||||
... on NewMarket {
|
... on NewMarket {
|
||||||
...NewMarketSuccessorFields
|
...NewMarketSuccessorFields
|
||||||
}
|
}
|
||||||
|
... on UpdateMarket {
|
||||||
|
...UpdateMarketFields
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-22
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
export {
|
export {
|
||||||
marketViewProposalsDataProvider,
|
marketViewProposalsDataProvider,
|
||||||
proposalsDataProvider,
|
proposalsDataProvider,
|
||||||
|
useMarketProposals,
|
||||||
} from './proposals-data-provider';
|
} from './proposals-data-provider';
|
||||||
export * from './__generated__/Proposals';
|
export * from './__generated__/Proposals';
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ describe('proposals data provider', () => {
|
|||||||
{
|
{
|
||||||
...delta,
|
...delta,
|
||||||
id: '2',
|
id: '2',
|
||||||
|
// @ts-ignore skipping unnecessary fields
|
||||||
terms: { ...delta.terms, change: { __typename: 'UpdateMarket' } },
|
terms: { ...delta.terms, change: { __typename: 'UpdateMarket' } },
|
||||||
},
|
},
|
||||||
reload,
|
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 { makeDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import * as Types from '@vegaprotocol/types';
|
import * as Types from '@vegaprotocol/types';
|
||||||
@@ -125,3 +125,17 @@ export const marketViewProposalsDataProvider = makeDataProvider<
|
|||||||
getData: getMarketProposalsData,
|
getData: getMarketProposalsData,
|
||||||
getSubscriptionVariables: () => subscriptionVariables,
|
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
|
// Show volume study by default, second bool arg adds it as a overlay on top of the chart
|
||||||
studies.forEach((study) => {
|
studies.forEach((study) => {
|
||||||
const asOverlay = study === 'Volume';
|
activeChart.createStudy(study);
|
||||||
activeChart.createStudy(study, asOverlay);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to interval changes so it can be persisted in chart settings
|
// 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
|
// colors set here, trading view lets the user set a color
|
||||||
'paneProperties.background': theme === 'dark' ? '#05060C' : '#fff',
|
'paneProperties.background': theme === 'dark' ? '#05060C' : '#fff',
|
||||||
'paneProperties.backgroundType': 'solid',
|
'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'];
|
initialLevel: Scalars['String'];
|
||||||
/** Minimal margin for the position to be maintained in the network (unsigned integer) */
|
/** Minimal margin for the position to be maintained in the network (unsigned integer) */
|
||||||
maintenanceLevel: Scalars['String'];
|
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 in which the margin is required for this party */
|
||||||
market: Market;
|
market: Market;
|
||||||
|
/** When in isolated margin, the required order margin level, otherwise, 0 */
|
||||||
|
orderMarginLevel: Scalars['String'];
|
||||||
/** The party for this margin */
|
/** The party for this margin */
|
||||||
party: Party;
|
party: Party;
|
||||||
/** If the margin is between maintenance and search, the network will initiate a collateral search, expressed as unsigned integer */
|
/** 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'];
|
initialLevel: Scalars['String'];
|
||||||
/** Minimal margin for the position to be maintained in the network (unsigned integer) */
|
/** Minimal margin for the position to be maintained in the network (unsigned integer) */
|
||||||
maintenanceLevel: Scalars['String'];
|
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 in which the margin is required for this party */
|
||||||
marketId: Scalars['ID'];
|
marketId: Scalars['ID'];
|
||||||
|
/** When in isolated margin, the required order margin level, otherwise, 0 */
|
||||||
|
orderMarginLevel: Scalars['String'];
|
||||||
/** The party for this margin */
|
/** The party for this margin */
|
||||||
partyId: Scalars['ID'];
|
partyId: Scalars['ID'];
|
||||||
/** If the margin is between maintenance and search, the network will initiate a collateral search (unsigned integer) */
|
/** 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'];
|
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 */
|
/** Represents a product & associated parameters that can be traded on Vega, has an associated OrderBook and Trade history */
|
||||||
export type Market = {
|
export type Market = {
|
||||||
__typename?: 'Market';
|
__typename?: 'Market';
|
||||||
@@ -3118,6 +3137,8 @@ export enum OrderRejectionReason {
|
|||||||
ORDER_ERROR_INVALID_TIME_IN_FORCE = 'ORDER_ERROR_INVALID_TIME_IN_FORCE',
|
ORDER_ERROR_INVALID_TIME_IN_FORCE = 'ORDER_ERROR_INVALID_TIME_IN_FORCE',
|
||||||
/** Invalid type */
|
/** Invalid type */
|
||||||
ORDER_ERROR_INVALID_TYPE = 'ORDER_ERROR_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 */
|
/** Margin check failed - not enough available margin */
|
||||||
ORDER_ERROR_MARGIN_CHECK_FAILED = 'ORDER_ERROR_MARGIN_CHECK_FAILED',
|
ORDER_ERROR_MARGIN_CHECK_FAILED = 'ORDER_ERROR_MARGIN_CHECK_FAILED',
|
||||||
/** Market is closed */
|
/** 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_ERROR_OFFSET_MUST_BE_GREATER_THAN_ZERO = 'ORDER_ERROR_OFFSET_MUST_BE_GREATER_THAN_ZERO',
|
||||||
/** Order is out of sequence */
|
/** Order is out of sequence */
|
||||||
ORDER_ERROR_OUT_OF_SEQUENCE = 'ORDER_ERROR_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 */
|
/** 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',
|
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 */
|
/** 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'];
|
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.
|
* All staking information related to a Party.
|
||||||
* Contains the current recognised balance by the network and
|
* Contains the current recognised balance by the network and
|
||||||
@@ -4438,6 +4496,12 @@ export type Query = {
|
|||||||
partiesConnection?: Maybe<PartyConnection>;
|
partiesConnection?: Maybe<PartyConnection>;
|
||||||
/** An entity that is trading on the Vega network */
|
/** An entity that is trading on the Vega network */
|
||||||
party?: Maybe<Party>;
|
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 */
|
/** Fetch all positions */
|
||||||
positions?: Maybe<PositionConnection>;
|
positions?: Maybe<PositionConnection>;
|
||||||
/** A governance proposal located by either its ID or reference. If both are set, ID is used. */
|
/** 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',
|
TRANSFER_TYPE_INFRASTRUCTURE_FEE_DISTRIBUTE = 'TRANSFER_TYPE_INFRASTRUCTURE_FEE_DISTRIBUTE',
|
||||||
/** Infrastructure fee paid from general account */
|
/** Infrastructure fee paid from general account */
|
||||||
TRANSFER_TYPE_INFRASTRUCTURE_FEE_PAY = 'TRANSFER_TYPE_INFRASTRUCTURE_FEE_PAY',
|
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. */
|
/** Allocates liquidity fee earnings to each liquidity provider's network controlled liquidity fee account. */
|
||||||
TRANSFER_TYPE_LIQUIDITY_FEE_ALLOCATE = 'TRANSFER_TYPE_LIQUIDITY_FEE_ALLOCATE',
|
TRANSFER_TYPE_LIQUIDITY_FEE_ALLOCATE = 'TRANSFER_TYPE_LIQUIDITY_FEE_ALLOCATE',
|
||||||
/** Liquidity fee received into general account */
|
/** Liquidity fee received into general account */
|
||||||
@@ -6237,6 +6303,10 @@ export enum TransferType {
|
|||||||
TRANSFER_TYPE_MTM_LOSS = 'TRANSFER_TYPE_MTM_LOSS',
|
TRANSFER_TYPE_MTM_LOSS = 'TRANSFER_TYPE_MTM_LOSS',
|
||||||
/** Funds added to margin account after mark to market gain */
|
/** Funds added to margin account after mark to market gain */
|
||||||
TRANSFER_TYPE_MTM_WIN = 'TRANSFER_TYPE_MTM_WIN',
|
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. */
|
/** Funds deducted from margin account after a perpetuals funding loss. */
|
||||||
TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS = 'TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS',
|
TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS = 'TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS',
|
||||||
/** Funds added to margin account after a perpetuals funding gain. */
|
/** Funds added to margin account after a perpetuals funding gain. */
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { toastIconMapping } from '../toast';
|
import { toastIconMapping } from '../toast';
|
||||||
import { Intent } from '../../utils/intent';
|
import { Intent } from '../../utils/intent';
|
||||||
import { Icon } from '../icon';
|
import { Icon, VegaIcon, VegaIconNames } from '../icon';
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
export const SHORT = '!px-1 !py-1 min-h-fit';
|
export const SHORT = '!px-1 !py-1 min-h-fit';
|
||||||
@@ -23,8 +23,8 @@ export const NotificationBanner = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex items-center px-1 py-3 border-b min-h-[56px]',
|
'flex items-center border-b px-2',
|
||||||
'text-[12px] leading-[16px] font-normal',
|
'text-xs leading-tight font-normal',
|
||||||
{
|
{
|
||||||
'bg-vega-light-100 dark:bg-vega-dark-100 ': intent === Intent.None,
|
'bg-vega-light-100 dark:bg-vega-dark-100 ': intent === Intent.None,
|
||||||
'bg-vega-blue-300 dark:bg-vega-blue-700': intent === Intent.Primary,
|
'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 ? (
|
{onClose ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="notification-banner-close"
|
data-testid="notification-banner-close"
|
||||||
onClick={onClose}
|
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>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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 =
|
export type Transaction =
|
||||||
|
| UpdateMarginModeBody
|
||||||
| StopOrdersSubmissionBody
|
| StopOrdersSubmissionBody
|
||||||
| StopOrdersCancellationBody
|
| StopOrdersCancellationBody
|
||||||
| OrderSubmissionBody
|
| OrderSubmissionBody
|
||||||
@@ -465,6 +482,10 @@ export type Transaction =
|
|||||||
| ApplyReferralCode
|
| ApplyReferralCode
|
||||||
| CreateReferralSet;
|
| CreateReferralSet;
|
||||||
|
|
||||||
|
export const isMarginModeUpdateTransaction = (
|
||||||
|
transaction: Transaction
|
||||||
|
): transaction is UpdateMarginModeBody => 'updateMarginMode' in transaction;
|
||||||
|
|
||||||
export const isWithdrawTransaction = (
|
export const isWithdrawTransaction = (
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
): transaction is WithdrawSubmissionBody => 'withdrawSubmission' in transaction;
|
): transaction is WithdrawSubmissionBody => 'withdrawSubmission' in transaction;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
isStopOrdersSubmissionTransaction,
|
isStopOrdersSubmissionTransaction,
|
||||||
isStopOrdersCancellationTransaction,
|
isStopOrdersCancellationTransaction,
|
||||||
determineId,
|
determineId,
|
||||||
|
isMarginModeUpdateTransaction,
|
||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
@@ -58,7 +59,7 @@ export interface VegaTransactionStore {
|
|||||||
|
|
||||||
export const useVegaTransactionStore = create<VegaTransactionStore>()(
|
export const useVegaTransactionStore = create<VegaTransactionStore>()(
|
||||||
subscribeWithSelector((set, get) => ({
|
subscribeWithSelector((set, get) => ({
|
||||||
transactions: [] as VegaStoredTxState[],
|
transactions: [] as (VegaStoredTxState | undefined)[],
|
||||||
create: (body: Transaction, order?: OrderTxUpdateFieldsFragment) => {
|
create: (body: Transaction, order?: OrderTxUpdateFieldsFragment) => {
|
||||||
const transactions = get().transactions;
|
const transactions = get().transactions;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -205,16 +206,23 @@ export const useVegaTransactionStore = create<VegaTransactionStore>()(
|
|||||||
isStopOrdersCancellationTransaction(transaction.body);
|
isStopOrdersCancellationTransaction(transaction.body);
|
||||||
const isConfirmedStopOrderSubmission =
|
const isConfirmedStopOrderSubmission =
|
||||||
isStopOrdersSubmissionTransaction(transaction.body);
|
isStopOrdersSubmissionTransaction(transaction.body);
|
||||||
|
const isConfirmedMarginModeTransaction =
|
||||||
|
isMarginModeUpdateTransaction(transaction.body);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(isConfirmedOrderCancellation ||
|
isConfirmedOrderCancellation ||
|
||||||
isConfirmedTransfer ||
|
isConfirmedTransfer ||
|
||||||
isConfirmedStopOrderCancellation ||
|
isConfirmedStopOrderCancellation ||
|
||||||
isConfirmedStopOrderSubmission) &&
|
isConfirmedStopOrderSubmission ||
|
||||||
!transactionResult.error &&
|
isConfirmedMarginModeTransaction
|
||||||
transactionResult.status
|
//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.dialogOpen = true;
|
||||||
transaction.updatedAt = new Date();
|
transaction.updatedAt = new Date();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
OrderSubmission,
|
OrderSubmission,
|
||||||
StopOrdersSubmission,
|
StopOrdersSubmission,
|
||||||
StopOrderSetup,
|
StopOrderSetup,
|
||||||
|
UpdateMarginMode,
|
||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
import type {
|
import type {
|
||||||
OrderTxUpdateFieldsFragment,
|
OrderTxUpdateFieldsFragment,
|
||||||
@@ -26,6 +27,8 @@ import {
|
|||||||
isStopOrdersSubmissionTransaction,
|
isStopOrdersSubmissionTransaction,
|
||||||
isStopOrdersCancellationTransaction,
|
isStopOrdersCancellationTransaction,
|
||||||
isReferralRelatedTransaction,
|
isReferralRelatedTransaction,
|
||||||
|
isMarginModeUpdateTransaction,
|
||||||
|
MarginMode,
|
||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
import { useVegaTransactionStore } from './use-vega-transaction-store';
|
import { useVegaTransactionStore } from './use-vega-transaction-store';
|
||||||
import { VegaTxStatus } from './types';
|
import { VegaTxStatus } from './types';
|
||||||
@@ -163,6 +166,7 @@ const isClosePositionTransaction = (tx: VegaStoredTxState) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
|
const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
|
||||||
|
const marginModeUpdate = isMarginModeUpdateTransaction(tx.body);
|
||||||
const withdraw = isWithdrawTransaction(tx.body);
|
const withdraw = isWithdrawTransaction(tx.body);
|
||||||
const submitOrder = isOrderSubmissionTransaction(tx.body);
|
const submitOrder = isOrderSubmissionTransaction(tx.body);
|
||||||
const cancelOrder = isOrderCancellationTransaction(tx.body);
|
const cancelOrder = isOrderCancellationTransaction(tx.body);
|
||||||
@@ -173,6 +177,7 @@ const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
|
|||||||
const transfer = isTransferTransaction(tx.body);
|
const transfer = isTransferTransaction(tx.body);
|
||||||
const referral = isReferralRelatedTransaction(tx.body);
|
const referral = isReferralRelatedTransaction(tx.body);
|
||||||
return (
|
return (
|
||||||
|
marginModeUpdate ||
|
||||||
withdraw ||
|
withdraw ||
|
||||||
submitOrder ||
|
submitOrder ||
|
||||||
cancelOrder ||
|
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 CancelStopOrderDetails = ({ stopOrderId }: { stopOrderId: string }) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const formatTrigger = useFormatTrigger();
|
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)) {
|
if (isClosePositionTransaction(tx)) {
|
||||||
const transaction = tx.body as BatchMarketInstructionSubmissionBody;
|
const transaction = tx.body as BatchMarketInstructionSubmissionBody;
|
||||||
const marketId = first(
|
const marketId = first(
|
||||||
|
|||||||
Reference in New Issue
Block a user