2022-12-02 00:33:30 +00:00
|
|
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
|
|
|
import { useEnvironment } from '@vegaprotocol/environment';
|
|
|
|
import { ButtonLink, Link } from '@vegaprotocol/ui-toolkit';
|
2023-02-28 18:56:29 +00:00
|
|
|
import { MarketProposalNotification } from '@vegaprotocol/proposals';
|
2023-05-18 11:22:54 +00:00
|
|
|
import type { Market } from '@vegaprotocol/markets';
|
2023-09-20 09:11:03 +00:00
|
|
|
import {
|
|
|
|
fromNanoSeconds,
|
|
|
|
getExpiryDate,
|
|
|
|
getMarketExpiryDate,
|
|
|
|
} from '@vegaprotocol/utils';
|
2023-02-28 18:56:29 +00:00
|
|
|
import { t } from '@vegaprotocol/i18n';
|
2023-09-20 09:11:03 +00:00
|
|
|
import {
|
|
|
|
Last24hPriceChange,
|
|
|
|
Last24hVolume,
|
|
|
|
getAsset,
|
|
|
|
getDataSourceSpecForSettlementSchedule,
|
|
|
|
marketInfoProvider,
|
|
|
|
useFundingPeriodsQuery,
|
|
|
|
useFundingRate,
|
|
|
|
} from '@vegaprotocol/markets';
|
2023-07-24 08:37:18 +00:00
|
|
|
import { MarketState as State } from '@vegaprotocol/types';
|
2023-05-16 16:57:36 +00:00
|
|
|
import { HeaderStat } from '../../components/header';
|
2022-12-02 00:33:30 +00:00
|
|
|
import { MarketMarkPrice } from '../../components/market-mark-price';
|
2023-01-16 17:51:30 +00:00
|
|
|
import { HeaderStatMarketTradingMode } from '../../components/market-trading-mode';
|
2023-07-24 08:37:18 +00:00
|
|
|
import { MarketState } from '../../components/market-state';
|
2022-12-28 17:37:19 +00:00
|
|
|
import { MarketLiquiditySupplied } from '../../components/liquidity-supplied';
|
2023-09-20 09:11:03 +00:00
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
2022-12-02 00:33:30 +00:00
|
|
|
|
2023-07-24 08:37:18 +00:00
|
|
|
interface MarketHeaderStatsProps {
|
2023-09-20 09:11:03 +00:00
|
|
|
market: Market;
|
2022-12-02 00:33:30 +00:00
|
|
|
}
|
|
|
|
|
2023-07-24 08:37:18 +00:00
|
|
|
export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
2022-12-02 00:33:30 +00:00
|
|
|
const { VEGA_EXPLORER_URL } = useEnvironment();
|
|
|
|
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
|
|
|
|
2023-09-20 09:11:03 +00:00
|
|
|
const asset = getAsset(market);
|
2022-12-02 00:33:30 +00:00
|
|
|
|
|
|
|
return (
|
2023-07-24 08:37:18 +00:00
|
|
|
<>
|
2023-09-20 09:11:03 +00:00
|
|
|
{market.tradableInstrument.instrument.product.__typename === 'Future' && (
|
|
|
|
<HeaderStat
|
|
|
|
heading={t('Expiry')}
|
|
|
|
description={
|
2023-07-24 08:37:18 +00:00
|
|
|
<ExpiryTooltipContent
|
|
|
|
market={market}
|
|
|
|
explorerUrl={VEGA_EXPLORER_URL}
|
2023-05-16 16:57:36 +00:00
|
|
|
/>
|
2023-09-20 09:11:03 +00:00
|
|
|
}
|
|
|
|
testId="market-expiry"
|
|
|
|
>
|
|
|
|
<ExpiryLabel market={market} />
|
|
|
|
</HeaderStat>
|
|
|
|
)}
|
|
|
|
{market.tradableInstrument.instrument.product.__typename ===
|
|
|
|
'Perpetual' && (
|
|
|
|
<HeaderStat
|
|
|
|
heading={`${t('Funding')} / ${t('Countdown')}`}
|
|
|
|
testId="market-funding"
|
|
|
|
>
|
|
|
|
<div className="flex justify-between gap-2">
|
|
|
|
<FundingRate marketId={market.id} />
|
|
|
|
<FundingCountdown marketId={market.id} />
|
|
|
|
</div>
|
|
|
|
</HeaderStat>
|
|
|
|
)}
|
2023-07-24 08:37:18 +00:00
|
|
|
<HeaderStat heading={t('Price')} testId="market-price">
|
|
|
|
<MarketMarkPrice
|
2023-09-20 09:11:03 +00:00
|
|
|
marketId={market.id}
|
|
|
|
decimalPlaces={market.decimalPlaces}
|
2023-07-24 08:37:18 +00:00
|
|
|
/>
|
|
|
|
</HeaderStat>
|
|
|
|
<HeaderStat heading={t('Change (24h)')} testId="market-change">
|
|
|
|
<Last24hPriceChange
|
2023-09-20 09:11:03 +00:00
|
|
|
marketId={market.id}
|
|
|
|
decimalPlaces={market.decimalPlaces}
|
2023-07-24 08:37:18 +00:00
|
|
|
/>
|
|
|
|
</HeaderStat>
|
|
|
|
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
|
|
|
|
<Last24hVolume
|
2023-09-20 09:11:03 +00:00
|
|
|
marketId={market.id}
|
|
|
|
positionDecimalPlaces={market.positionDecimalPlaces}
|
2023-07-24 08:37:18 +00:00
|
|
|
/>
|
|
|
|
</HeaderStat>
|
|
|
|
<HeaderStatMarketTradingMode
|
2023-09-20 09:11:03 +00:00
|
|
|
marketId={market.id}
|
|
|
|
initialTradingMode={market.tradingMode}
|
2023-07-24 08:37:18 +00:00
|
|
|
/>
|
|
|
|
<MarketState market={market} />
|
|
|
|
{asset ? (
|
|
|
|
<HeaderStat
|
|
|
|
heading={t('Settlement asset')}
|
|
|
|
testId="market-settlement-asset"
|
|
|
|
>
|
|
|
|
<div>
|
|
|
|
<ButtonLink
|
|
|
|
onClick={(e) => {
|
|
|
|
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
|
|
|
|
}}
|
2022-12-02 00:33:30 +00:00
|
|
|
>
|
2023-07-24 08:37:18 +00:00
|
|
|
{asset.symbol}
|
|
|
|
</ButtonLink>
|
|
|
|
</div>
|
|
|
|
</HeaderStat>
|
|
|
|
) : null}
|
|
|
|
<MarketLiquiditySupplied
|
2023-09-20 09:11:03 +00:00
|
|
|
marketId={market.id}
|
2023-07-24 08:37:18 +00:00
|
|
|
assetDecimals={asset?.decimals || 0}
|
|
|
|
/>
|
2023-09-20 09:11:03 +00:00
|
|
|
<MarketProposalNotification marketId={market.id} />
|
2023-07-24 08:37:18 +00:00
|
|
|
</>
|
2022-12-02 00:33:30 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
type ExpiryLabelProps = {
|
2023-09-20 09:11:03 +00:00
|
|
|
market: Market;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const FundingRate = ({ marketId }: { marketId: string }) => {
|
|
|
|
const { data: fundingRate } = useFundingRate(marketId);
|
|
|
|
return (
|
|
|
|
<div data-testid="funding-rate">
|
|
|
|
{fundingRate ? `${(Number(fundingRate) * 100).toFixed(4)}%` : '-'}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const padStart = (n: number) => n.toString().padStart(2, '0');
|
|
|
|
|
|
|
|
export const FundingCountdown = ({ marketId }: { marketId: string }) => {
|
|
|
|
const { data: fundingPeriods } = useFundingPeriodsQuery({
|
|
|
|
variables: {
|
|
|
|
marketId: marketId,
|
|
|
|
pagination: { first: 1 },
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const { data: marketInfo } = useDataProvider({
|
|
|
|
dataProvider: marketInfoProvider,
|
|
|
|
variables: { marketId },
|
|
|
|
});
|
|
|
|
|
|
|
|
const [now, setNow] = useState(Date.now());
|
|
|
|
useEffect(() => {
|
|
|
|
const interval = setInterval(() => setNow(Date.now()), 1000);
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const node = fundingPeriods?.fundingPeriods.edges?.[0]?.node;
|
|
|
|
let startTime: number | undefined = undefined;
|
|
|
|
if (node && node.startTime && !node.endTime) {
|
|
|
|
startTime = fromNanoSeconds(node.startTime).getTime();
|
|
|
|
}
|
|
|
|
let diffFormatted = t('Unknown');
|
|
|
|
let every: number | undefined = undefined;
|
|
|
|
const sourceType =
|
|
|
|
marketInfo &&
|
|
|
|
getDataSourceSpecForSettlementSchedule(
|
|
|
|
marketInfo.tradableInstrument.instrument.product
|
|
|
|
)?.data.sourceType.sourceType;
|
|
|
|
|
|
|
|
if (sourceType?.__typename === 'DataSourceSpecConfigurationTimeTrigger') {
|
|
|
|
every = sourceType.triggers?.[0]?.every ?? undefined;
|
|
|
|
if (every) {
|
|
|
|
every *= 1000;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (startTime && every) {
|
|
|
|
const diff = every - ((now - startTime) % every);
|
|
|
|
const hours = (diff / 3.6e6) | 0;
|
|
|
|
const mins = ((diff % 3.6e6) / 6e4) | 0;
|
|
|
|
const secs = Math.round((diff % 6e4) / 1e3);
|
|
|
|
diffFormatted = `${padStart(hours)}:${padStart(mins)}:${padStart(secs)}`;
|
|
|
|
}
|
|
|
|
return <div data-testid="funding-countdown">{diffFormatted}</div>;
|
2022-12-02 00:33:30 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const ExpiryLabel = ({ market }: ExpiryLabelProps) => {
|
2023-09-20 09:11:03 +00:00
|
|
|
const content = market.tradableInstrument.instrument.metadata.tags
|
|
|
|
? getExpiryDate(
|
|
|
|
market.tradableInstrument.instrument.metadata.tags,
|
|
|
|
market.marketTimestamps.close,
|
|
|
|
market.state
|
|
|
|
)
|
|
|
|
: '-';
|
2022-12-02 00:33:30 +00:00
|
|
|
return <div data-testid="trading-expiry">{content}</div>;
|
|
|
|
};
|
|
|
|
|
|
|
|
type ExpiryTooltipContentProps = {
|
2023-02-09 14:20:31 +00:00
|
|
|
market: Market;
|
2022-12-02 00:33:30 +00:00
|
|
|
explorerUrl?: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
const ExpiryTooltipContent = ({
|
|
|
|
market,
|
|
|
|
explorerUrl,
|
|
|
|
}: ExpiryTooltipContentProps) => {
|
2023-09-20 09:11:03 +00:00
|
|
|
if (market.marketTimestamps.close === null) {
|
2022-12-02 00:33:30 +00:00
|
|
|
const oracleId =
|
2023-09-20 09:11:03 +00:00
|
|
|
market.tradableInstrument.instrument.product.__typename === 'Future'
|
|
|
|
? market.tradableInstrument.instrument.product
|
|
|
|
.dataSourceSpecForTradingTermination?.id
|
|
|
|
: undefined;
|
2022-12-02 00:33:30 +00:00
|
|
|
|
2023-01-16 17:36:06 +00:00
|
|
|
const metadataExpiryDate = getMarketExpiryDate(
|
|
|
|
market.tradableInstrument.instrument.metadata.tags
|
|
|
|
);
|
|
|
|
|
|
|
|
const isExpired =
|
|
|
|
metadataExpiryDate &&
|
|
|
|
Date.now() - metadataExpiryDate.valueOf() > 0 &&
|
|
|
|
(market.state === State.STATE_TRADING_TERMINATED ||
|
|
|
|
market.state === State.STATE_SETTLED);
|
|
|
|
|
2022-12-02 00:33:30 +00:00
|
|
|
return (
|
2023-01-16 17:36:06 +00:00
|
|
|
<section data-testid="expiry-tooltip">
|
2022-12-02 00:33:30 +00:00
|
|
|
<p className="mb-2">
|
|
|
|
{t(
|
|
|
|
'This market expires when triggered by its oracle, not on a set date.'
|
|
|
|
)}
|
|
|
|
</p>
|
2023-01-16 17:36:06 +00:00
|
|
|
{metadataExpiryDate && !isExpired && (
|
|
|
|
<p className="mb-2">
|
|
|
|
{t(
|
|
|
|
'This timestamp is user curated metadata and does not drive any on-chain functionality.'
|
|
|
|
)}
|
|
|
|
</p>
|
|
|
|
)}
|
2022-12-02 00:33:30 +00:00
|
|
|
{explorerUrl && oracleId && (
|
|
|
|
<Link href={`${explorerUrl}/oracles#${oracleId}`} target="_blank">
|
|
|
|
{t('View oracle specification')}
|
|
|
|
</Link>
|
|
|
|
)}
|
|
|
|
</section>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
};
|