Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
789e5faf76 | ||
|
|
43ec3921da | ||
|
|
dc25fe53e3 |
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Feature Epic
|
||||
about: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
|
||||
title: 'FEATURE EPIC: '
|
||||
title: 'Epic: '
|
||||
labels: feature-epic
|
||||
---
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ on:
|
||||
jobs:
|
||||
run-tests:
|
||||
name: run-tests
|
||||
runs-on: 8-cores
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
#----------------------------------------------
|
||||
# check-out frontend-monorepo
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
#----------------------------------------------
|
||||
- name: Run tests
|
||||
working-directory: ./console-test
|
||||
run: poetry run pytest -s --numprocesses auto
|
||||
run: poetry run pytest --numprocesses auto
|
||||
- name: Check files
|
||||
run: |
|
||||
ls -al .
|
||||
|
||||
@@ -23,7 +23,7 @@ export const Footer = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<footer className="grid grid-cols-[1fr_auto] items-center text-xs md:text-md lg:flex md:col-span-2 px-4 pt-2 pb-3 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
|
||||
<footer className="grid grid-rows-2 lg:grid-cols-[1fr_auto] text-xs md:text-md lg:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
|
||||
<div className="flex justify-between gap-2 align-middle">
|
||||
{GIT_COMMIT_HASH && (
|
||||
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
||||
@@ -43,12 +43,9 @@ export const Footer = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="content-center flex pr-4 md:border-r border-neutral-700 dark:border-neutral-300">
|
||||
<span className="pr-2">{VEGA_URL && <NodeUrl url={VEGA_URL} />}</span>
|
||||
<Link
|
||||
className="ml-2 underline-offset-4"
|
||||
onClick={() => setNodeSwitcherOpen(true)}
|
||||
>
|
||||
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
||||
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
|
||||
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
|
||||
{t('Change')}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -62,10 +59,7 @@ export const Footer = () => {
|
||||
) : null}
|
||||
</div>
|
||||
<div className="pl-2 align-center lg:align-right lg:flex lg:justify-end gap-2 align-middle lg:max-w-xs lg:ml-auto">
|
||||
<RouteLink
|
||||
to={`/${Routes.DISCLAIMER}`}
|
||||
className="underline underline-offset-4"
|
||||
>
|
||||
<RouteLink to={`/${Routes.DISCLAIMER}`} className="underline">
|
||||
Disclaimer
|
||||
</RouteLink>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { MarketInfoWithData } from '@vegaprotocol/markets';
|
||||
import {
|
||||
PriceMonitoringBoundsInfoPanel,
|
||||
SuccessionLineInfoPanel,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { PriceMonitoringBoundsInfoPanel } from '@vegaprotocol/markets';
|
||||
import {
|
||||
LiquidityInfoPanel,
|
||||
LiquidityMonitoringParametersInfoPanel,
|
||||
@@ -106,8 +103,6 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
<OracleInfoPanel market={market} type="settlementData" />
|
||||
</>
|
||||
)}
|
||||
<h2 className={`${headerClassName} mb-4`}>{t('Succession line')}</h2>
|
||||
<SuccessionLineInfoPanel market={market} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,8 +10,6 @@ import Hash from '../../links/hash';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new';
|
||||
import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update';
|
||||
import { MarketLink } from '../../links';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
|
||||
export type Proposal = components['schemas']['v1ProposalSubmission'];
|
||||
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
|
||||
@@ -55,11 +53,7 @@ export function proposalTypeLabel(terms?: ProposalTerms): string {
|
||||
} else if (has(terms, 'updateAsset')) {
|
||||
return t('Update asset proposal');
|
||||
} else if (has(terms, 'newMarket')) {
|
||||
if (terms?.newMarket?.changes?.successor) {
|
||||
return t('Successor market proposal');
|
||||
} else {
|
||||
return t('New market proposal');
|
||||
}
|
||||
return t('New market proposal');
|
||||
} else if (has(terms, 'updateMarket')) {
|
||||
return t('Update market proposal');
|
||||
} else if (has(terms, 'updateNetworkParameter')) {
|
||||
@@ -91,13 +85,6 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
|
||||
}
|
||||
|
||||
const tx = proposal.terms?.newAsset || proposal.terms?.updateAsset;
|
||||
const isSuccessorMarketProposal =
|
||||
proposal?.terms?.newMarket?.changes?.successor;
|
||||
const parentMarketId =
|
||||
isSuccessorMarketProposal &&
|
||||
proposal?.terms.newMarket?.changes?.successor?.parentMarketId;
|
||||
const insurancePoolFraction =
|
||||
proposal?.terms?.newMarket?.changes?.successor?.insurancePoolFraction;
|
||||
|
||||
// This component is not rendered if no bundle is required
|
||||
const SignatureBundleComponent = proposal.terms?.newAsset
|
||||
@@ -124,30 +111,6 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
|
||||
<Hash text={deterministicId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isSuccessorMarketProposal ? (
|
||||
<>
|
||||
{parentMarketId ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Previous market')}</TableCell>
|
||||
<TableCell>
|
||||
<MarketLink
|
||||
id={
|
||||
proposal.terms?.newMarket.changes.successor.parentMarketId
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{insurancePoolFraction ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Insurance pool fraction')}</TableCell>
|
||||
<TableCell>
|
||||
{formatNumber(Number(insurancePoolFraction) * 100, 0)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
<ProposalSummary
|
||||
id={deterministicId}
|
||||
|
||||
@@ -78,11 +78,7 @@ export function getLabelForProposal(
|
||||
} else if (proposal.terms?.updateAsset) {
|
||||
return t('Proposal: Update asset');
|
||||
} else if (proposal.terms?.newMarket) {
|
||||
if (proposal.terms?.newMarket.changes?.successor) {
|
||||
return t('Proposal: Successor market');
|
||||
} else {
|
||||
return t('Proposal: New market');
|
||||
}
|
||||
return t('Proposal: New market');
|
||||
} else if (proposal.terms?.updateMarket) {
|
||||
return t('Proposal: Update market');
|
||||
} else if (proposal.terms?.updateNetworkParameter) {
|
||||
|
||||
@@ -730,7 +730,6 @@ context(
|
||||
.getByTestId('key-value-table-row')
|
||||
.contains(heading)
|
||||
.parent()
|
||||
.parent()
|
||||
.siblings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
VegaConnectDialog,
|
||||
VegaManageDialog,
|
||||
ViewAsDialog,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { VegaConnectDialog, VegaManageDialog } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
@@ -29,8 +25,6 @@ export const VegaWalletDialogs = () => {
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<ViewAsDialog connector={Connectors.view} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ExternalLinks } from '@vegaprotocol/environment';
|
||||
|
||||
export const DownloadWalletPrompt = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<h3 className="mt-4 mb-2">{t('getWallet')}</h3>
|
||||
<p>
|
||||
<Link className="text-neutral-500" href={ExternalLinks.VEGA_WALLET_URL}>
|
||||
{t('getWalletLink')}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ButtonLink, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ExternalLinks } from '@vegaprotocol/environment';
|
||||
import { useViewAsDialog } from '@vegaprotocol/wallet';
|
||||
|
||||
export const VegaWalletPrompt = () => {
|
||||
const { t } = useTranslation();
|
||||
const setViewAsDialog = useViewAsDialog((state) => state.setOpen);
|
||||
return (
|
||||
<>
|
||||
<h3 className="mt-4 mb-2">{t('getWallet')}</h3>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Link className="text-neutral-500" href={ExternalLinks.VEGA_WALLET_URL}>
|
||||
{t('getWalletLink')}
|
||||
</Link>
|
||||
<ButtonLink
|
||||
className="text-neutral-500"
|
||||
onClick={() => setViewAsDialog(true)}
|
||||
>
|
||||
{t('viewAsParty')}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
WalletCardHeader,
|
||||
WalletCardRow,
|
||||
} from '../wallet-card';
|
||||
import { VegaWalletPrompt } from './vega-wallet-prompt';
|
||||
import { DownloadWalletPrompt } from './download-wallet-prompt';
|
||||
import { usePollForDelegations } from './hooks';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
@@ -85,7 +85,7 @@ const VegaWalletNotConnected = () => {
|
||||
>
|
||||
{t('connectVegaWalletToUseAssociated')}
|
||||
</Button>
|
||||
<VegaWalletPrompt />
|
||||
<DownloadWalletPrompt />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -855,6 +855,5 @@
|
||||
"Estimated time to upgrade": "Estimated time to upgrade",
|
||||
"Upgraded at": "Upgraded at",
|
||||
"dataIsIdentical": "Data is identical",
|
||||
"updatesToMarket": "Updates to market",
|
||||
"viewAsParty": "View as party"
|
||||
"updatesToMarket": "Updates to market"
|
||||
}
|
||||
|
||||
+9
-83
@@ -45,10 +45,8 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
|
||||
|
||||
export const ProposalMarketData = ({
|
||||
marketData,
|
||||
parentMarketData,
|
||||
}: {
|
||||
marketData: MarketInfoWithData;
|
||||
parentMarketData?: MarketInfoWithData;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, open, close } = useMarketDataDialogStore();
|
||||
@@ -60,21 +58,8 @@ export const ProposalMarketData = ({
|
||||
|
||||
const settlementData = marketData.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
|
||||
const parentSettlementData =
|
||||
parentMarketData?.tradableInstrument.instrument?.product
|
||||
?.dataSourceSpecForSettlementData?.data;
|
||||
const terminationData = marketData.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
|
||||
const parentTerminationData =
|
||||
parentMarketData?.tradableInstrument.instrument?.product
|
||||
?.dataSourceSpecForTradingTermination?.data;
|
||||
|
||||
const isParentSettlementDataEqual =
|
||||
parentSettlementData !== undefined &&
|
||||
isEqual(settlementData, parentSettlementData);
|
||||
const isParentTerminationDataEqual =
|
||||
parentTerminationData !== undefined &&
|
||||
isEqual(terminationData, parentTerminationData);
|
||||
|
||||
const getSigners = (data: DataSourceDefinition) => {
|
||||
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
|
||||
@@ -112,22 +97,12 @@ export const ProposalMarketData = ({
|
||||
<AccordionItem
|
||||
itemId="key-details"
|
||||
title={t('Key details')}
|
||||
content={
|
||||
<KeyDetailsInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
content={<KeyDetailsInfoPanel market={marketData} />}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="instrument"
|
||||
title={t('Instrument')}
|
||||
content={
|
||||
<InstrumentInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
content={<InstrumentInfoPanel market={marketData} />}
|
||||
/>
|
||||
{isEqual(
|
||||
getSigners(settlementData),
|
||||
@@ -140,11 +115,6 @@ export const ProposalMarketData = ({
|
||||
<OracleInfoPanel
|
||||
market={marketData}
|
||||
type="settlementData"
|
||||
parentMarket={
|
||||
isParentSettlementDataEqual
|
||||
? undefined
|
||||
: parentMarketData
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -157,11 +127,6 @@ export const ProposalMarketData = ({
|
||||
<OracleInfoPanel
|
||||
market={marketData}
|
||||
type="settlementData"
|
||||
parentMarket={
|
||||
isParentSettlementDataEqual
|
||||
? undefined
|
||||
: parentMarketData
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -170,21 +135,11 @@ export const ProposalMarketData = ({
|
||||
itemId="termination-oracle"
|
||||
title={t('Termination Oracle')}
|
||||
content={
|
||||
<OracleInfoPanel
|
||||
market={marketData}
|
||||
type="termination"
|
||||
parentMarket={
|
||||
isParentTerminationDataEqual
|
||||
? undefined
|
||||
: parentMarketData
|
||||
}
|
||||
/>
|
||||
<OracleInfoPanel market={marketData} type="termination" />
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/*Note: successor markets will not differ in their settlement*/}
|
||||
{/*assets, so no need to pass in parent market data for comparison.*/}
|
||||
<AccordionItem
|
||||
itemId="settlement-asset"
|
||||
title={t('Settlement asset')}
|
||||
@@ -193,42 +148,22 @@ export const ProposalMarketData = ({
|
||||
<AccordionItem
|
||||
itemId="metadata"
|
||||
title={t('Metadata')}
|
||||
content={
|
||||
<MetadataInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
content={<MetadataInfoPanel market={marketData} />}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="risk-model"
|
||||
title={t('Risk model')}
|
||||
content={
|
||||
<RiskModelInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
content={<RiskModelInfoPanel market={marketData} />}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="risk-parameters"
|
||||
title={t('Risk parameters')}
|
||||
content={
|
||||
<RiskParametersInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
content={<RiskParametersInfoPanel market={marketData} />}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="risk-factors"
|
||||
title={t('Risk factors')}
|
||||
content={
|
||||
<RiskFactorsInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
content={<RiskFactorsInfoPanel market={marketData} />}
|
||||
/>
|
||||
{(
|
||||
marketData.priceMonitoringSettings?.parameters?.triggers || []
|
||||
@@ -239,7 +174,6 @@ export const ProposalMarketData = ({
|
||||
content={
|
||||
<PriceMonitoringBoundsInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
triggerIndex={triggerIndex}
|
||||
/>
|
||||
}
|
||||
@@ -249,21 +183,13 @@ export const ProposalMarketData = ({
|
||||
itemId="liqudity-monitoring-parameters"
|
||||
title={t('Liquidity monitoring parameters')}
|
||||
content={
|
||||
<LiquidityMonitoringParametersInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
<LiquidityMonitoringParametersInfoPanel market={marketData} />
|
||||
}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="liquidity-price-range"
|
||||
title={t('Liquidity price range')}
|
||||
content={
|
||||
<LiquidityPriceRangeInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
|
||||
/>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,6 @@ export interface ProposalProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
networkParams: Partial<NetworkParamsResult>;
|
||||
newMarketData?: MarketInfoWithData | null;
|
||||
parentMarketData?: MarketInfoWithData | null;
|
||||
assetData?: AssetQuery | null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
restData: any;
|
||||
@@ -47,7 +46,6 @@ export const Proposal = ({
|
||||
networkParams,
|
||||
restData,
|
||||
newMarketData,
|
||||
parentMarketData,
|
||||
assetData,
|
||||
originalMarketProposalRestData,
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
@@ -159,10 +157,7 @@ export const Proposal = ({
|
||||
|
||||
{newMarketData && (
|
||||
<div className="mb-4">
|
||||
<ProposalMarketData
|
||||
marketData={newMarketData}
|
||||
parentMarketData={parentMarketData ? parentMarketData : undefined}
|
||||
/>
|
||||
<ProposalMarketData marketData={newMarketData} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -14,9 +14,6 @@ import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
|
||||
|
||||
export const ProposalContainer = () => {
|
||||
const [
|
||||
@@ -57,10 +54,6 @@ export const ProposalContainer = () => {
|
||||
skip: !params.proposalId,
|
||||
});
|
||||
|
||||
const successor = useSuccessorMarketProposalDetails(params.proposalId);
|
||||
|
||||
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
|
||||
|
||||
const {
|
||||
state: {
|
||||
data: originalMarketProposalRestData,
|
||||
@@ -103,36 +96,6 @@ export const ProposalContainer = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: parentMarketId,
|
||||
loading: parentMarketIdLoading,
|
||||
error: parentMarketIdError,
|
||||
} = useParentMarketIdQuery({
|
||||
variables: {
|
||||
marketId: newMarketData?.data?.market?.id || '',
|
||||
},
|
||||
skip:
|
||||
!FLAGS.SUCCESSOR_MARKETS ||
|
||||
!isSuccessor ||
|
||||
!newMarketData?.data?.market?.id,
|
||||
});
|
||||
|
||||
const {
|
||||
data: parentMarketData,
|
||||
loading: parentMarketLoading,
|
||||
error: parentMarketError,
|
||||
} = useDataProvider({
|
||||
dataProvider: marketInfoWithDataProvider,
|
||||
skipUpdates: true,
|
||||
variables: {
|
||||
marketId: parentMarketId?.market?.parentMarketID || '',
|
||||
skip:
|
||||
!FLAGS.SUCCESSOR_MARKETS ||
|
||||
!isSuccessor ||
|
||||
!parentMarketId?.market?.parentMarketID,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: assetData,
|
||||
loading: assetLoading,
|
||||
@@ -197,8 +160,6 @@ export const ProposalContainer = () => {
|
||||
newMarketLoading ||
|
||||
assetLoading ||
|
||||
networkParamsLoading ||
|
||||
parentMarketIdLoading ||
|
||||
parentMarketLoading ||
|
||||
(restLoading ? (restLoading as boolean) : false) ||
|
||||
(originalMarketProposalRestLoading
|
||||
? (originalMarketProposalRestLoading as boolean)
|
||||
@@ -211,18 +172,15 @@ export const ProposalContainer = () => {
|
||||
error ||
|
||||
newMarketError ||
|
||||
assetError ||
|
||||
networkParamsError ||
|
||||
parentMarketIdError ||
|
||||
parentMarketError ||
|
||||
restError ||
|
||||
originalMarketProposalRestError ||
|
||||
previouslyEnactedMarketProposalsRestError
|
||||
previouslyEnactedMarketProposalsRestError ||
|
||||
networkParamsError
|
||||
}
|
||||
data={{
|
||||
...data,
|
||||
...networkParams,
|
||||
...(newMarketData ? { newMarketData } : {}),
|
||||
...(parentMarketData ? { parentMarketData } : {}),
|
||||
...(assetData ? { assetData } : {}),
|
||||
...(restData ? { restData } : {}),
|
||||
...(originalMarketProposalRestData
|
||||
@@ -239,7 +197,6 @@ export const ProposalContainer = () => {
|
||||
networkParams={networkParams}
|
||||
restData={restData}
|
||||
newMarketData={newMarketData}
|
||||
parentMarketData={parentMarketData}
|
||||
assetData={assetData}
|
||||
originalMarketProposalRestData={originalMarketProposalRestData}
|
||||
mostRecentlyEnactedAssociatedMarketProposal={
|
||||
|
||||
@@ -26,6 +26,7 @@ module.exports = defineConfig({
|
||||
requestTimeout: 20000,
|
||||
retries: 1,
|
||||
testIsolation: false,
|
||||
experimentalMemoryManagement: true,
|
||||
},
|
||||
env: {
|
||||
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
|
||||
|
||||
@@ -104,11 +104,11 @@ describe('deposit actions', { tags: '@smoke' }, () => {
|
||||
cy.visit('/#/markets/market-1');
|
||||
});
|
||||
|
||||
it('Deposit to trade is visible', () => {
|
||||
it('Deposit to trade is visble', () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
cy.get('[row-id="asset-id"]').contains('tEURO').should('be.visible');
|
||||
cy.contains('[data-testid="deposit"]', 'Deposit').should('be.visible');
|
||||
cy.contains('[data-testid="deposit"]', 'Deposit').click();
|
||||
cy.contains('[data-testid="deposit"]', 'Deposit')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.getByTestId('deposit-submit').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,7 +63,6 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
|
||||
cy.getByTestId(orderSizeField).clear().type('1');
|
||||
cy.getByTestId(orderPriceField).clear().type('1.123456');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId('deal-ticket-error-message-price-limit').should(
|
||||
'have.text',
|
||||
'Price accepts up to 5 decimal places'
|
||||
@@ -74,7 +73,6 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
describe('market order', () => {
|
||||
before(() => {
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
});
|
||||
|
||||
it('must not see the price unit', function () {
|
||||
|
||||
@@ -48,7 +48,6 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
cy.getByTestId(orderPriceField).clear().type('0.1');
|
||||
cy.getByTestId(orderSizeField).clear().type('1');
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId('deal-ticket-warning-auction').should(
|
||||
'have.text',
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
@@ -61,7 +60,6 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
TIFlist.filter((item) => item.code === 'FOK')[0].value
|
||||
);
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId('deal-ticket-error-message-tif').should(
|
||||
'have.text',
|
||||
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('fills', { tags: '@regression' }, () => {
|
||||
cy.getByTestId(tabFills).contains('Market');
|
||||
cy.getByTestId(tabFills)
|
||||
.get(
|
||||
'[role="gridcell"][col-id="market.tradableInstrument.instrument.code"]'
|
||||
'[role="gridcell"][col-id="market.tradableInstrument.instrument.name"]'
|
||||
)
|
||||
.each(($marketSymbol) => {
|
||||
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
|
||||
|
||||
@@ -142,22 +142,22 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
|
||||
it('sorting by Market', () => {
|
||||
visitAndClickPositions();
|
||||
const marketsSortedDefault = [
|
||||
'AAPL.MF21',
|
||||
'BTCUSD.MF21',
|
||||
'ETHBTC.QM21',
|
||||
'SOLUSD',
|
||||
'ACTIVE MARKET',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'ETHBTC Quarterly (30 Jun 2022)',
|
||||
'SUSPENDED MARKET',
|
||||
];
|
||||
const marketsSortedAsc = [
|
||||
'AAPL.MF21',
|
||||
'BTCUSD.MF21',
|
||||
'ETHBTC.QM21',
|
||||
'SOLUSD',
|
||||
'ACTIVE MARKET',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'ETHBTC Quarterly (30 Jun 2022)',
|
||||
'SUSPENDED MARKET',
|
||||
];
|
||||
const marketsSortedDesc = [
|
||||
'SOLUSD',
|
||||
'ETHBTC.QM21',
|
||||
'BTCUSD.MF21',
|
||||
'AAPL.MF21',
|
||||
'SUSPENDED MARKET',
|
||||
'ETHBTC Quarterly (30 Jun 2022)',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'ACTIVE MARKET',
|
||||
];
|
||||
cy.getByTestId(positions).click();
|
||||
// 7004-POSI-003
|
||||
@@ -291,11 +291,9 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
|
||||
|
||||
it('View settlement asset', () => {
|
||||
visitAndClickPositions();
|
||||
cy.get('[col-id="asset"]')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('button[type="button"]').click();
|
||||
});
|
||||
cy.get('[col-id="asset"]').within(() => {
|
||||
cy.get('button[type="button"]').first().click();
|
||||
});
|
||||
// 7004-POSI-008
|
||||
cy.getByTestId(dialogContent).should('be.visible');
|
||||
cy.getByTestId(dialogCloseX).click();
|
||||
@@ -310,7 +308,7 @@ function validatePositionsDisplayed(multiKey = false) {
|
||||
cy.getByTestId('tab-positions').should('be.visible');
|
||||
cy.getByTestId('tab-positions')
|
||||
.get('.ag-center-cols-container .ag-row')
|
||||
.eq(multiKey ? 3 : 1)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get('[col-id="marketName"]')
|
||||
.should('be.visible')
|
||||
|
||||
@@ -12,108 +12,100 @@ const walletTransfer = 'wallet-transfer';
|
||||
|
||||
const ASSET_SEPOLIA_TBTC = 2;
|
||||
|
||||
describe.skip(
|
||||
'transfer fees',
|
||||
{ tags: '@regression', testIsolation: true },
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setVegaWallet();
|
||||
describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setVegaWallet();
|
||||
|
||||
cy.visit('/');
|
||||
cy.getByTestId(manageVegaWallet).click();
|
||||
cy.getByTestId(walletTransfer).click();
|
||||
cy.visit('/');
|
||||
cy.getByTestId(manageVegaWallet).click();
|
||||
cy.getByTestId(walletTransfer).click();
|
||||
|
||||
cy.wait('@Assets');
|
||||
cy.wait('@Accounts');
|
||||
cy.wait('@Assets');
|
||||
cy.wait('@Accounts');
|
||||
|
||||
cy.mockVegaWalletTransaction();
|
||||
});
|
||||
cy.mockVegaWalletTransaction();
|
||||
});
|
||||
|
||||
it('transfer fees tooltips', () => {
|
||||
// 1003-TRAN-015
|
||||
// 1003-TRAN-016
|
||||
// 1003-TRAN-017
|
||||
// 1003-TRAN-018
|
||||
// 1003-TRAN-019
|
||||
cy.getByTestId(transferForm);
|
||||
cy.contains('Enter manually').click();
|
||||
cy.getByTestId(transferForm)
|
||||
.find(toAddressField)
|
||||
.type(
|
||||
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
|
||||
);
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.getByTestId(transferForm)
|
||||
.find(amountField)
|
||||
.type('1', { delay: 100, force: true });
|
||||
it('transfer fees tooltips', () => {
|
||||
// 1003-TRAN-015
|
||||
// 1003-TRAN-016
|
||||
// 1003-TRAN-017
|
||||
// 1003-TRAN-018
|
||||
// 1003-TRAN-019
|
||||
cy.getByTestId(transferForm);
|
||||
cy.contains('Enter manually').click();
|
||||
cy.getByTestId(transferForm)
|
||||
.find(toAddressField)
|
||||
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.getByTestId(transferForm)
|
||||
.find(amountField)
|
||||
.type('1', { delay: 100, force: true });
|
||||
|
||||
/// Check Include Transfer Fee tooltip
|
||||
cy.get('label[for="include-transfer-fee"] div').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
/// Check Include Transfer Fee tooltip
|
||||
cy.get('label[for="include-transfer-fee"] div').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
|
||||
//Check Transfer Fee tooltip
|
||||
cy.contains('div', 'Transfer fee').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
//Check Transfer Fee tooltip
|
||||
cy.contains('div', 'Transfer fee').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
|
||||
//Check Amount to be transferred tooltip
|
||||
cy.contains('div', 'Amount to be transferred').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
//Check Amount to be transferred tooltip
|
||||
cy.contains('div', 'Amount to be transferred').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
|
||||
//Check Total amount (with fee) tooltip
|
||||
cy.contains('div', 'Total amount (with fee)').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
});
|
||||
//Check Total amount (with fee) tooltip
|
||||
cy.contains('div', 'Total amount (with fee)').realHover();
|
||||
cy.get('[data-side="bottom"] div')
|
||||
.should('be.visible')
|
||||
.should('not.be.empty');
|
||||
});
|
||||
|
||||
it('transfer fees', () => {
|
||||
// 1003-TRAN-020
|
||||
// 1003-TRAN-021
|
||||
// 1003-TRAN-022
|
||||
// 1003-TRAN-023
|
||||
cy.getByTestId(transferForm);
|
||||
cy.contains('Enter manually').click();
|
||||
it('transfer fees', () => {
|
||||
// 1003-TRAN-020
|
||||
// 1003-TRAN-021
|
||||
// 1003-TRAN-022
|
||||
// 1003-TRAN-023
|
||||
cy.getByTestId(transferForm);
|
||||
cy.contains('Enter manually').click();
|
||||
|
||||
cy.getByTestId(transferForm)
|
||||
.find(toAddressField)
|
||||
.type(
|
||||
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
|
||||
);
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
|
||||
cy.getByTestId(transferForm)
|
||||
.find(toAddressField)
|
||||
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
|
||||
|
||||
cy.getByTestId(transferForm)
|
||||
.find(amountField)
|
||||
.type('1', { delay: 100, force: true });
|
||||
cy.getByTestId(transferForm)
|
||||
.find(amountField)
|
||||
.type('1', { delay: 100, force: true });
|
||||
|
||||
cy.getByTestId(transferFee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '0.01');
|
||||
cy.getByTestId(transferAmount)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '1.00');
|
||||
cy.getByTestId(totalTransferfee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '1.01');
|
||||
cy.getByTestId(includeTransferFeeRadioBtn).click();
|
||||
cy.getByTestId(transferFee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '0.01');
|
||||
cy.getByTestId(transferAmount)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '0.99');
|
||||
cy.getByTestId(totalTransferfee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '1.00');
|
||||
});
|
||||
}
|
||||
);
|
||||
cy.getByTestId(transferFee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '0.01');
|
||||
cy.getByTestId(transferAmount)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '1.00');
|
||||
cy.getByTestId(totalTransferfee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '1.01');
|
||||
cy.getByTestId(includeTransferFeeRadioBtn).click();
|
||||
cy.getByTestId(transferFee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '0.01');
|
||||
cy.getByTestId(transferAmount)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '0.99');
|
||||
cy.getByTestId(totalTransferfee)
|
||||
.should('be.visible')
|
||||
.should('contain.text', '1.00');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,16 +44,16 @@ const MainGrid = memo(
|
||||
|
||||
return (
|
||||
<ResizableGrid vertical onChange={handleOnLayoutChange}>
|
||||
<ResizableGridPanel
|
||||
preferredSize={sizes[0]}
|
||||
priority={LayoutPriority.High}
|
||||
minSize={200}
|
||||
>
|
||||
<ResizableGrid onChange={handleOnMiddleLayoutChange}>
|
||||
<ResizableGridPanel minSize={75} priority={LayoutPriority.High}>
|
||||
<ResizableGrid
|
||||
proportionalLayout={false}
|
||||
minSize={200}
|
||||
onChange={handleOnMiddleLayoutChange}
|
||||
>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.High}
|
||||
minSize={200}
|
||||
preferredSize={sizesMiddle[0] || '75%'}
|
||||
preferredSize={sizesMiddle[1] || '50%'}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs storageKey="console-trade-grid-main-left">
|
||||
@@ -74,8 +74,8 @@ const MainGrid = memo(
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
preferredSize={sizesMiddle[2] || 300}
|
||||
minSize={200}
|
||||
preferredSize={sizesMiddle[1] || 300}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs storageKey="console-trade-grid-main-right">
|
||||
@@ -91,9 +91,9 @@ const MainGrid = memo(
|
||||
</ResizableGrid>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.Low}
|
||||
preferredSize={sizes[1] || '25%'}
|
||||
minSize={50}
|
||||
priority={LayoutPriority.Low}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs storageKey="console-trade-grid-bottom">
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { OrderbookManager } from '@vegaprotocol/market-depth';
|
||||
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
||||
import { ViewType, useSidebar } from '../sidebar';
|
||||
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
|
||||
import { useStopOrderFormValues } from '@vegaprotocol/deal-ticket';
|
||||
|
||||
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
||||
const update = useDealTicketFormValues((state) => state.updateAll);
|
||||
const useOrderStoreRef = useCreateOrderStore();
|
||||
const updateOrder = useOrderStoreRef((store) => store.update);
|
||||
const updateStoredFormValues = useStopOrderFormValues(
|
||||
(state) => state.update
|
||||
);
|
||||
const setView = useSidebar((store) => store.setView);
|
||||
return (
|
||||
<OrderbookManager
|
||||
marketId={marketId}
|
||||
onClick={(values) => {
|
||||
update(marketId, values);
|
||||
onClick={({ price, size }) => {
|
||||
if (price) {
|
||||
updateOrder(marketId, { price });
|
||||
updateStoredFormValues(marketId, { price });
|
||||
}
|
||||
if (size) {
|
||||
updateOrder(marketId, { size });
|
||||
updateStoredFormValues(marketId, { size });
|
||||
}
|
||||
setView({ type: ViewType.Order });
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { GetStarted } from './get-started';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
describe('GetStarted', () => {
|
||||
const renderComponent = (context: Partial<VegaWalletContextShape> = {}) => {
|
||||
return render(
|
||||
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
|
||||
<GetStarted />
|
||||
</VegaWalletContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
it('renders full get started content if not connected and no browser wallet detected', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByTestId('get-started-banner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders connect prompt if no pubKey but wallet installed', () => {
|
||||
globalThis.window.vega = {} as Vega;
|
||||
renderComponent();
|
||||
expect(screen.getByTestId('order-connect-wallet')).toBeInTheDocument();
|
||||
globalThis.window.vega = undefined as unknown as Vega;
|
||||
});
|
||||
|
||||
it('renders nothing if connected', () => {
|
||||
const { container } = renderComponent({ pubKey: 'my-pubkey' });
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ interface Props {
|
||||
|
||||
export const GetStarted = ({ lead }: Props) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
|
||||
const CANONICAL_URL = VEGA_NETWORKS[VEGA_ENV] || 'https://console.vega.xyz';
|
||||
|
||||
@@ -39,7 +40,7 @@ export const GetStarted = ({ lead }: Props) => {
|
||||
{ 'mt-8': !lead }
|
||||
);
|
||||
|
||||
if (!pubKey && !isBrowserWalletInstalled()) {
|
||||
if (!isBrowserWalletInstalled()) {
|
||||
return (
|
||||
<div className={wrapperClasses} data-testid="get-started-banner">
|
||||
{lead && <h2>{lead}</h2>}
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = {
|
||||
'scope-enum': async (ctx) => [
|
||||
2,
|
||||
'always',
|
||||
['ci', 'docs', 'specs', ...(await utils.getProjects(ctx))],
|
||||
['ci', 'docs', ...(await utils.getProjects(ctx))],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,7 +23,6 @@ const singleRow = {
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'BTCUSD Monthly (30 Jun 2022)',
|
||||
code: 'BTCUSD.MF21',
|
||||
},
|
||||
},
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
@@ -58,7 +57,7 @@ describe('BreakdownTable', () => {
|
||||
});
|
||||
const cells = await screen.findAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'BTCUSD.MF21',
|
||||
'BTCUSD Monthly (30 Jun 2022)',
|
||||
'Margin',
|
||||
'1,256.00 (50%)',
|
||||
'1,256.00',
|
||||
@@ -119,7 +118,6 @@ describe('BreakdownTable', () => {
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'BTCUSD Monthly (30 Jun 2022)',
|
||||
code: 'BTCUSD.MF21',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -31,12 +31,12 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
|
||||
const defs: ColDef[] = [
|
||||
{
|
||||
headerName: t('Market'),
|
||||
field: 'market.tradableInstrument.instrument.code',
|
||||
field: 'market.tradableInstrument.instrument.name',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
AccountFields,
|
||||
'market.tradableInstrument.instrument.code'
|
||||
'market.tradableInstrument.instrument.name'
|
||||
>) => {
|
||||
if (!value) return 'None';
|
||||
return value;
|
||||
|
||||
@@ -28,10 +28,10 @@ export const assetProvider = makeDataProvider<
|
||||
getData,
|
||||
});
|
||||
|
||||
export const useAssetDataProvider = (assetId: string, skip?: boolean) => {
|
||||
export const useAssetDataProvider = (assetId: string) => {
|
||||
return useDataProvider({
|
||||
dataProvider: assetProvider,
|
||||
variables: { assetId: assetId || '' },
|
||||
skip: !assetId || skip,
|
||||
skip: !assetId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,25 +3,29 @@ import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
||||
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
import type { OrderObj } from '@vegaprotocol/orders';
|
||||
import type { OrderFormFields } from '../../hooks/use-order-form';
|
||||
|
||||
export interface DealTicketAmountProps {
|
||||
control: Control<OrderFormValues>;
|
||||
type: Schema.OrderType;
|
||||
control: Control<OrderFormFields>;
|
||||
orderType: Schema.OrderType;
|
||||
marketData: StaticMarketData;
|
||||
marketPrice?: string;
|
||||
market: Market;
|
||||
sizeError?: string;
|
||||
priceError?: string;
|
||||
update: (obj: Partial<OrderObj>) => void;
|
||||
size: string;
|
||||
price?: string;
|
||||
}
|
||||
|
||||
export const DealTicketAmount = ({
|
||||
type,
|
||||
orderType,
|
||||
marketData,
|
||||
marketPrice,
|
||||
...props
|
||||
}: DealTicketAmountProps) => {
|
||||
switch (type) {
|
||||
switch (orderType) {
|
||||
case Schema.OrderType.TYPE_MARKET:
|
||||
return (
|
||||
<DealTicketMarketAmount
|
||||
@@ -33,7 +37,7 @@ export const DealTicketAmount = ({
|
||||
case Schema.OrderType.TYPE_LIMIT:
|
||||
return <DealTicketLimitAmount {...props} />;
|
||||
default: {
|
||||
throw new Error('Invalid ticket type ' + type);
|
||||
throw new Error('Invalid ticket type');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
isStopOrderType,
|
||||
useDealTicketFormValues,
|
||||
} from '../../hooks/use-form-values';
|
||||
DealTicketType,
|
||||
useDealTicketTypeStore,
|
||||
} from '../../hooks/use-type-store';
|
||||
import { StopOrder } from './deal-ticket-stop-order';
|
||||
import {
|
||||
useStaticMarketData,
|
||||
@@ -25,9 +25,7 @@ export const DealTicketContainer = ({
|
||||
marketId,
|
||||
...props
|
||||
}: DealTicketContainerProps) => {
|
||||
const showStopOrder = useDealTicketFormValues((state) =>
|
||||
isStopOrderType(state.formValues[marketId]?.type)
|
||||
);
|
||||
const type = useDealTicketTypeStore((state) => state.type[marketId]);
|
||||
const {
|
||||
data: market,
|
||||
error: marketError,
|
||||
@@ -50,7 +48,9 @@ export const DealTicketContainer = ({
|
||||
reload={reload}
|
||||
>
|
||||
{market && marketData ? (
|
||||
FLAGS.STOP_ORDERS && showStopOrder ? (
|
||||
FLAGS.STOP_ORDERS &&
|
||||
(type === DealTicketType.StopLimit ||
|
||||
type === DealTicketType.StopMarket) ? (
|
||||
<StopOrder
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
export type DealTicketLimitAmountProps = Omit<
|
||||
DealTicketAmountProps,
|
||||
'marketData' | 'type'
|
||||
Omit<DealTicketAmountProps, 'marketData'>,
|
||||
'orderType'
|
||||
>;
|
||||
|
||||
export const DealTicketLimitAmount = ({
|
||||
@@ -14,6 +14,9 @@ export const DealTicketLimitAmount = ({
|
||||
market,
|
||||
sizeError,
|
||||
priceError,
|
||||
update,
|
||||
price,
|
||||
size,
|
||||
}: DealTicketLimitAmountProps) => {
|
||||
const priceStep = toDecimal(market?.decimalPlaces);
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
@@ -59,16 +62,17 @@ export const DealTicketLimitAmount = ({
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-order-size-limit"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={size}
|
||||
onChange={(e) => update({ size: e.target.value })}
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
data-testid="order-size"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -91,17 +95,19 @@ export const DealTicketLimitAmount = ({
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
},
|
||||
// @ts-ignore this fulfills the interface but still errors
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-price-quote"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={price}
|
||||
onChange={(e) => update({ price: e.target.value })}
|
||||
step={priceStep}
|
||||
data-testid="order-price"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,10 @@ import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export type DealTicketMarketAmountProps = Omit<DealTicketAmountProps, 'type'>;
|
||||
export type DealTicketMarketAmountProps = Omit<
|
||||
DealTicketAmountProps,
|
||||
'orderType'
|
||||
>;
|
||||
|
||||
export const DealTicketMarketAmount = ({
|
||||
control,
|
||||
@@ -18,6 +21,8 @@ export const DealTicketMarketAmount = ({
|
||||
marketData,
|
||||
marketPrice,
|
||||
sizeError,
|
||||
update,
|
||||
size,
|
||||
}: DealTicketMarketAmountProps) => {
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
@@ -45,16 +50,17 @@ export const DealTicketMarketAmount = ({
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-order-size-market"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={size}
|
||||
onChange={(e) => update({ size: e.target.value })}
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
data-testid="order-size"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Controller, type Control } from 'react-hook-form';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
import type { OrderObj } from '@vegaprotocol/orders';
|
||||
import type { OrderFormFields } from '../../hooks/use-order-form';
|
||||
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
@@ -11,21 +12,25 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export interface DealTicketSizeIcebergProps {
|
||||
control: Control<OrderFormValues>;
|
||||
control: Control<OrderFormFields>;
|
||||
market: Market;
|
||||
peakSizeError?: string;
|
||||
minimumVisibleSizeError?: string;
|
||||
update: (obj: Partial<OrderObj>) => void;
|
||||
peakSize: string;
|
||||
minimumVisibleSize: string;
|
||||
size: string;
|
||||
peakSize?: string;
|
||||
}
|
||||
|
||||
export const DealTicketSizeIceberg = ({
|
||||
control,
|
||||
market,
|
||||
update,
|
||||
peakSizeError,
|
||||
minimumVisibleSizeError,
|
||||
size,
|
||||
peakSize,
|
||||
minimumVisibleSize,
|
||||
size,
|
||||
}: DealTicketSizeIcebergProps) => {
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
|
||||
@@ -75,7 +80,7 @@ export const DealTicketSizeIceberg = ({
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="peakSize"
|
||||
name="icebergOpts.peakSize"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a peak size'),
|
||||
@@ -92,17 +97,25 @@ export const DealTicketSizeIceberg = ({
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'peakSize'),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-order-peak-size"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={peakSize}
|
||||
onChange={(e) =>
|
||||
update({
|
||||
icebergOpts: {
|
||||
peakSize: e.target.value,
|
||||
minimumVisibleSize,
|
||||
},
|
||||
})
|
||||
}
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
max={size}
|
||||
data-testid="order-peak-size"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -131,7 +144,7 @@ export const DealTicketSizeIceberg = ({
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="minimumVisibleSize"
|
||||
name="icebergOpts.minimumVisibleSize"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a minimum visible size'),
|
||||
@@ -141,7 +154,7 @@ export const DealTicketSizeIceberg = ({
|
||||
'Minimum visible size cannot be lower than ' + sizeStep
|
||||
),
|
||||
},
|
||||
max: peakSize && {
|
||||
max: {
|
||||
value: peakSize,
|
||||
message: t(
|
||||
'Minimum visible size cannot be greater than the peak size (%s)',
|
||||
@@ -150,17 +163,25 @@ export const DealTicketSizeIceberg = ({
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'minimumVisibleSize'),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-order-minimum-size"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={minimumVisibleSize}
|
||||
onChange={(e) =>
|
||||
update({
|
||||
icebergOpts: {
|
||||
peakSize,
|
||||
minimumVisibleSize: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
max={peakSize}
|
||||
data-testid="order-minimum-size"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -6,11 +6,8 @@ import { generateMarket } from '../../test-helpers';
|
||||
import { StopOrder } from './deal-ticket-stop-order';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { StopOrderFormValues } from '../../hooks/use-form-values';
|
||||
import {
|
||||
DealTicketType,
|
||||
useDealTicketFormValues,
|
||||
} from '../../hooks/use-form-values';
|
||||
import type { StopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||
import type { FeatureFlags } from '@vegaprotocol/environment';
|
||||
|
||||
jest.mock('zustand');
|
||||
@@ -134,11 +131,9 @@ describe('StopOrder', () => {
|
||||
expiresAt: '2023-07-27T16:43:27.000',
|
||||
};
|
||||
|
||||
useDealTicketFormValues.setState({
|
||||
useStopOrderFormValues.setState({
|
||||
formValues: {
|
||||
[market.id]: {
|
||||
[DealTicketType.StopLimit]: values,
|
||||
},
|
||||
[market.id]: values,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -212,13 +207,11 @@ describe('StopOrder', () => {
|
||||
// switch to market order type error should disappear
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||
|
||||
// switch back to limit type
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
await userEvent.click(screen.getByTestId(orderTypeLimit));
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// to small value should be invalid
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FormEventHandler } from 'react';
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
toDecimal,
|
||||
validateAmount,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useForm, Controller, useController } from 'react-hook-form';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
Radio,
|
||||
@@ -23,22 +24,22 @@ import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExpirySelector } from './expiry-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
import { timeInForceLabel } from '@vegaprotocol/orders';
|
||||
import { timeInForceLabel, useOrder } from '@vegaprotocol/orders';
|
||||
import {
|
||||
NoWalletWarning,
|
||||
REDUCE_ONLY_TOOLTIP,
|
||||
stopSubmit,
|
||||
getNotionalSize,
|
||||
useNotionalSize,
|
||||
} from './deal-ticket';
|
||||
import { TypeToggle } from './type-selector';
|
||||
import {
|
||||
useDealTicketFormValues,
|
||||
DealTicketType,
|
||||
useStopOrderFormValues,
|
||||
type StopOrderFormValues,
|
||||
dealTicketTypeToOrderType,
|
||||
isStopOrderType,
|
||||
} from '../../hooks/use-form-values';
|
||||
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission';
|
||||
} from '../../hooks/use-stop-order-form-values';
|
||||
import {
|
||||
DealTicketType,
|
||||
useDealTicketTypeStore,
|
||||
} from '../../hooks/use-type-store';
|
||||
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-stop-order-submission';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||
import { validateExpiration } from '../../utils';
|
||||
@@ -49,36 +50,32 @@ export interface StopOrderProps {
|
||||
submit: (order: StopOrdersSubmission) => void;
|
||||
}
|
||||
|
||||
const getDefaultValues = (
|
||||
type: Schema.OrderType,
|
||||
storedValues?: Partial<StopOrderFormValues>
|
||||
): StopOrderFormValues => ({
|
||||
type,
|
||||
const defaultValues: Partial<StopOrderFormValues> = {
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
triggerType: 'price',
|
||||
triggerDirection:
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||
expire: false,
|
||||
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
|
||||
size: '0',
|
||||
...storedValues,
|
||||
});
|
||||
};
|
||||
|
||||
const stopSubmit: FormEventHandler = (e) => e.preventDefault();
|
||||
|
||||
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const setType = useDealTicketFormValues((state) => state.setType);
|
||||
const updateStoredFormValues = useDealTicketFormValues(
|
||||
(state) => state.updateStopOrder
|
||||
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
||||
const [, updateOrder] = useOrder(market.id);
|
||||
const updateStoredFormValues = useStopOrderFormValues(
|
||||
(state) => state.update
|
||||
);
|
||||
const storedFormValues = useDealTicketFormValues(
|
||||
const storedFormValues = useStopOrderFormValues(
|
||||
(state) => state.formValues[market.id]
|
||||
);
|
||||
const dealTicketType = storedFormValues?.type ?? DealTicketType.StopLimit;
|
||||
const type = dealTicketTypeToOrderType(dealTicketType);
|
||||
const { handleSubmit, setValue, watch, control, formState, reset } =
|
||||
const { handleSubmit, setValue, watch, control, formState } =
|
||||
useForm<StopOrderFormValues>({
|
||||
defaultValues: getDefaultValues(type, storedFormValues?.[dealTicketType]),
|
||||
defaultValues: { ...defaultValues, ...storedFormValues },
|
||||
});
|
||||
const { errors } = formState;
|
||||
const lastSubmitTime = useRef(0);
|
||||
@@ -105,22 +102,16 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
const triggerType = watch('triggerType');
|
||||
const triggerPrice = watch('triggerPrice');
|
||||
const timeInForce = watch('timeInForce');
|
||||
const type = watch('type');
|
||||
const rawPrice = watch('price');
|
||||
const rawSize = watch('size');
|
||||
|
||||
useEffect(() => {
|
||||
const size = storedFormValues?.[dealTicketType]?.size;
|
||||
if (size && rawSize !== size) {
|
||||
setValue('size', size);
|
||||
}
|
||||
}, [storedFormValues, dealTicketType, rawSize, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const price = storedFormValues?.[dealTicketType]?.price;
|
||||
if (price && rawPrice !== price) {
|
||||
setValue('price', price);
|
||||
}
|
||||
}, [storedFormValues, dealTicketType, rawPrice, setValue]);
|
||||
if (storedFormValues?.size && rawSize !== storedFormValues?.size) {
|
||||
setValue('size', storedFormValues.size);
|
||||
}
|
||||
if (storedFormValues?.price && rawPrice !== storedFormValues?.price) {
|
||||
setValue('price', storedFormValues.price);
|
||||
}
|
||||
|
||||
const isPriceTrigger = triggerType === 'price';
|
||||
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
|
||||
@@ -136,7 +127,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
: marketPrice
|
||||
);
|
||||
|
||||
const notionalSize = getNotionalSize(
|
||||
const notionalSize = useNotionalSize(
|
||||
price,
|
||||
size,
|
||||
market.decimalPlaces,
|
||||
@@ -162,28 +153,47 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
? formatNumber(triggerPrice, market.decimalPlaces)
|
||||
: undefined;
|
||||
|
||||
useController({
|
||||
name: 'type',
|
||||
control,
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
|
||||
noValidate
|
||||
>
|
||||
<TypeToggle
|
||||
value={dealTicketType}
|
||||
onValueChange={(dealTicketType) => {
|
||||
setType(market.id, dealTicketType);
|
||||
if (isStopOrderType(dealTicketType)) {
|
||||
reset(
|
||||
getDefaultValues(
|
||||
dealTicketTypeToOrderType(dealTicketType),
|
||||
storedFormValues?.[dealTicketType]
|
||||
)
|
||||
);
|
||||
}
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const { value } = field;
|
||||
return (
|
||||
<TypeToggle
|
||||
value={
|
||||
value === Schema.OrderType.TYPE_LIMIT
|
||||
? DealTicketType.StopLimit
|
||||
: DealTicketType.StopMarket
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
const type = value as DealTicketType;
|
||||
setDealTicketType(market.id, type);
|
||||
if (
|
||||
type === DealTicketType.Limit ||
|
||||
type === DealTicketType.Market
|
||||
) {
|
||||
updateOrder({
|
||||
type:
|
||||
type === DealTicketType.Limit
|
||||
? Schema.OrderType.TYPE_LIMIT
|
||||
: Schema.OrderType.TYPE_MARKET,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setValue(
|
||||
'type',
|
||||
type === DealTicketType.StopLimit
|
||||
? Schema.OrderType.TYPE_LIMIT
|
||||
: Schema.OrderType.TYPE_MARKET
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.type && (
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
act,
|
||||
render,
|
||||
renderHook,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { generateMarket, generateMarketData } from '../../test-helpers';
|
||||
import { DealTicket } from './deal-ticket';
|
||||
@@ -9,10 +15,7 @@ import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import type { OrdersQuery } from '@vegaprotocol/orders';
|
||||
import {
|
||||
DealTicketType,
|
||||
useDealTicketFormValues,
|
||||
} from '../../hooks/use-form-values';
|
||||
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
||||
import * as positionsTools from '@vegaprotocol/positions';
|
||||
import { OrdersDocument } from '@vegaprotocol/orders';
|
||||
|
||||
@@ -47,6 +50,9 @@ function generateJsx(mocks: MockedResponse[] = []) {
|
||||
}
|
||||
|
||||
describe('DealTicket', () => {
|
||||
const { result } = renderHook(() => useCreateOrderStore());
|
||||
const useOrderStore = result.current;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
localStorage.clear();
|
||||
@@ -160,11 +166,9 @@ describe('DealTicket', () => {
|
||||
persist: true,
|
||||
};
|
||||
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[expectedOrder.marketId]: {
|
||||
[DealTicketType.Limit]: expectedOrder,
|
||||
},
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -200,11 +204,9 @@ describe('DealTicket', () => {
|
||||
reduceOnly: true,
|
||||
postOnly: false,
|
||||
};
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[expectedOrder.marketId]: {
|
||||
[DealTicketType.Limit]: expectedOrder,
|
||||
},
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -245,11 +247,9 @@ describe('DealTicket', () => {
|
||||
postOnly: true,
|
||||
};
|
||||
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[expectedOrder.marketId]: {
|
||||
[DealTicketType.Limit]: expectedOrder,
|
||||
},
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -295,11 +295,9 @@ describe('DealTicket', () => {
|
||||
},
|
||||
};
|
||||
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[expectedOrder.marketId]: {
|
||||
[DealTicketType.Limit]: expectedOrder,
|
||||
},
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -341,11 +339,9 @@ describe('DealTicket', () => {
|
||||
reduceOnly: false,
|
||||
postOnly: false,
|
||||
};
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[expectedOrder.marketId]: {
|
||||
[DealTicketType.Limit]: expectedOrder,
|
||||
},
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -374,7 +370,6 @@ describe('DealTicket', () => {
|
||||
expect(screen.getByTestId('iceberg')).not.toBeChecked();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it('handles TIF select box dependent on order type', async () => {
|
||||
render(generateJsx());
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { FormEventHandler } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { Controller, useController, useForm } from 'react-hook-form';
|
||||
import { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { DealTicketAmount } from './deal-ticket-amount';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
import {
|
||||
@@ -14,8 +13,7 @@ import { SideSelector } from './side-selector';
|
||||
import { TimeInForceSelector } from './time-in-force-selector';
|
||||
import { TypeSelector } from './type-selector';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
|
||||
import { normalizeOrderSubmission, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
Checkbox,
|
||||
InputError,
|
||||
@@ -53,15 +51,14 @@ import {
|
||||
useAccountBalance,
|
||||
} from '@vegaprotocol/accounts';
|
||||
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import { useOrderForm } from '../../hooks/use-order-form';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import {
|
||||
DealTicketType,
|
||||
dealTicketTypeToOrderType,
|
||||
isStopOrderType,
|
||||
} from '../../hooks/use-form-values';
|
||||
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
import { useDealTicketFormValues } from '../../hooks/use-form-values';
|
||||
useDealTicketTypeStore,
|
||||
} from '../../hooks/use-type-store';
|
||||
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||
import noop from 'lodash/noop';
|
||||
|
||||
@@ -78,42 +75,23 @@ export interface DealTicketProps {
|
||||
onDeposit: (assetId: string) => void;
|
||||
}
|
||||
|
||||
export const getNotionalSize = (
|
||||
export const useNotionalSize = (
|
||||
price: string | null | undefined,
|
||||
size: string | undefined,
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number
|
||||
) => {
|
||||
if (price && size) {
|
||||
return removeDecimal(
|
||||
toBigNum(size, positionDecimalPlaces).multipliedBy(
|
||||
toBigNum(price, decimalPlaces)
|
||||
),
|
||||
decimalPlaces
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const stopSubmit: FormEventHandler = (e) => e.preventDefault();
|
||||
|
||||
const getDefaultValues = (
|
||||
type: Schema.OrderType,
|
||||
storedValues?: Partial<OrderFormValues>
|
||||
): OrderFormValues => ({
|
||||
type,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce:
|
||||
type === Schema.OrderType.TYPE_LIMIT
|
||||
? Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
||||
: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
size: '0',
|
||||
price: '0',
|
||||
expiresAt: undefined,
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
...storedValues,
|
||||
});
|
||||
) =>
|
||||
useMemo(() => {
|
||||
if (price && size) {
|
||||
return removeDecimal(
|
||||
toBigNum(size, positionDecimalPlaces).multipliedBy(
|
||||
toBigNum(price, decimalPlaces)
|
||||
),
|
||||
decimalPlaces
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [price, size, decimalPlaces, positionDecimalPlaces]);
|
||||
|
||||
export const DealTicket = ({
|
||||
market,
|
||||
@@ -125,29 +103,32 @@ export const DealTicket = ({
|
||||
onDeposit,
|
||||
}: DealTicketProps) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const setType = useDealTicketFormValues((state) => state.setType);
|
||||
const storedFormValues = useDealTicketFormValues(
|
||||
(state) => state.formValues[market.id]
|
||||
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
||||
const updateStopOrderFormValues = useStopOrderFormValues(
|
||||
(state) => state.update
|
||||
);
|
||||
const updateStoredFormValues = useDealTicketFormValues(
|
||||
(state) => state.updateOrder
|
||||
);
|
||||
const dealTicketType = storedFormValues?.type ?? DealTicketType.Limit;
|
||||
const type = dealTicketTypeToOrderType(dealTicketType);
|
||||
// store last used tif for market so that when changing OrderType the previous TIF
|
||||
// selection for that type is used when switching back
|
||||
|
||||
const [lastTIF, setLastTIF] = useState({
|
||||
[OrderType.TYPE_MARKET]: OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
[OrderType.TYPE_LIMIT]: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
formState: { errors },
|
||||
errors,
|
||||
order,
|
||||
setError,
|
||||
clearErrors,
|
||||
update,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
} = useForm<OrderFormValues>({
|
||||
defaultValues: getDefaultValues(type, storedFormValues?.[dealTicketType]),
|
||||
});
|
||||
} = useOrderForm(market.id);
|
||||
|
||||
const lastSubmitTime = useRef(0);
|
||||
|
||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
|
||||
const {
|
||||
accountBalance: marginAccountBalance,
|
||||
loading: loadingMarginAccountBalance,
|
||||
@@ -163,54 +144,24 @@ export const DealTicket = ({
|
||||
).toString();
|
||||
|
||||
const { marketState, marketTradingMode } = marketData;
|
||||
const timeInForce = watch('timeInForce');
|
||||
|
||||
const side = watch('side');
|
||||
const rawSize = watch('size');
|
||||
const rawPrice = watch('price');
|
||||
const iceberg = watch('iceberg');
|
||||
const peakSize = watch('peakSize');
|
||||
const normalizedOrder =
|
||||
order &&
|
||||
normalizeOrderSubmission(
|
||||
order,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const size = storedFormValues?.[dealTicketType]?.size;
|
||||
if (size && rawSize !== size) {
|
||||
setValue('size', size);
|
||||
}
|
||||
}, [storedFormValues, dealTicketType, rawSize, setValue]);
|
||||
const price = useMemo(() => {
|
||||
return (
|
||||
normalizedOrder &&
|
||||
marketPrice &&
|
||||
getDerivedPrice(normalizedOrder, marketPrice)
|
||||
);
|
||||
}, [normalizedOrder, marketPrice]);
|
||||
|
||||
useEffect(() => {
|
||||
const price = storedFormValues?.[dealTicketType]?.price;
|
||||
if (price && rawPrice !== price) {
|
||||
setValue('price', price);
|
||||
}
|
||||
}, [storedFormValues, dealTicketType, rawPrice, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch((value, { name, type }) => {
|
||||
updateStoredFormValues(market.id, value);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [watch, market.id, updateStoredFormValues]);
|
||||
|
||||
const normalizedOrder = mapFormValuesToOrderSubmission(
|
||||
{
|
||||
price: rawPrice || undefined,
|
||||
side,
|
||||
size: rawSize,
|
||||
timeInForce,
|
||||
type,
|
||||
},
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
|
||||
const price =
|
||||
normalizedOrder &&
|
||||
marketPrice &&
|
||||
getDerivedPrice(normalizedOrder, marketPrice);
|
||||
|
||||
const notionalSize = getNotionalSize(
|
||||
const notionalSize = useNotionalSize(
|
||||
price,
|
||||
normalizedOrder?.size,
|
||||
market.decimalPlaces,
|
||||
@@ -254,20 +205,22 @@ export const DealTicket = ({
|
||||
const assetSymbol =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
|
||||
const summaryError = useMemo(() => {
|
||||
useEffect(() => {
|
||||
if (!pubKey) {
|
||||
return {
|
||||
setError('summary', {
|
||||
message: t('No public key selected'),
|
||||
type: SummaryValidationType.NoPubKey,
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const marketStateError = validateMarketState(marketState);
|
||||
if (marketStateError !== true) {
|
||||
return {
|
||||
setError('summary', {
|
||||
message: marketStateError,
|
||||
type: SummaryValidationType.MarketState,
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const hasNoBalance =
|
||||
@@ -276,21 +229,24 @@ export const DealTicket = ({
|
||||
hasNoBalance &&
|
||||
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
|
||||
) {
|
||||
return {
|
||||
setError('summary', {
|
||||
message: SummaryValidationType.NoCollateral,
|
||||
type: SummaryValidationType.NoCollateral,
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const marketTradingModeError = validateMarketTradingMode(marketTradingMode);
|
||||
if (marketTradingModeError !== true) {
|
||||
return {
|
||||
setError('summary', {
|
||||
message: marketTradingModeError,
|
||||
type: SummaryValidationType.TradingMode,
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
// No error found above clear the error in case it was active on a previous render
|
||||
clearErrors('summary');
|
||||
}, [
|
||||
marketState,
|
||||
marketTradingMode,
|
||||
@@ -299,83 +255,156 @@ export const DealTicket = ({
|
||||
loadingMarginAccountBalance,
|
||||
loadingGeneralAccountBalance,
|
||||
pubKey,
|
||||
setError,
|
||||
clearErrors,
|
||||
]);
|
||||
|
||||
const disablePostOnlyCheckbox = [
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
].includes(timeInForce);
|
||||
const disablePostOnlyCheckbox = useMemo(() => {
|
||||
const disabled = order
|
||||
? [
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
].includes(order.timeInForce)
|
||||
: true;
|
||||
return disabled;
|
||||
}, [order]);
|
||||
|
||||
const disableReduceOnlyCheckbox = !disablePostOnlyCheckbox;
|
||||
const disableReduceOnlyCheckbox = useMemo(() => {
|
||||
const disabled = order
|
||||
? ![
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
].includes(order.timeInForce)
|
||||
: true;
|
||||
return disabled;
|
||||
}, [order]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(formValues: OrderFormValues) => {
|
||||
(order: OrderSubmission) => {
|
||||
const now = new Date().getTime();
|
||||
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
|
||||
return;
|
||||
}
|
||||
submit(
|
||||
mapFormValuesToOrderSubmission(
|
||||
formValues,
|
||||
market.id,
|
||||
normalizeOrderSubmission(
|
||||
order,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
);
|
||||
lastSubmitTime.current = now;
|
||||
},
|
||||
[submit, market.decimalPlaces, market.positionDecimalPlaces, market.id]
|
||||
[submit, market.decimalPlaces, market.positionDecimalPlaces]
|
||||
);
|
||||
useController({
|
||||
name: 'type',
|
||||
control,
|
||||
rules: {
|
||||
validate: validateType(marketData.marketTradingMode, marketData.trigger),
|
||||
},
|
||||
});
|
||||
|
||||
// if an order doesn't exist one will be created by the store immediately
|
||||
if (!order || !normalizedOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={
|
||||
isReadOnly || !pubKey
|
||||
? stopSubmit
|
||||
: handleSubmit(summaryError ? noop : onSubmit)
|
||||
}
|
||||
onSubmit={isReadOnly ? noop : handleSubmit(onSubmit)}
|
||||
noValidate
|
||||
data-testid="deal-ticket-form"
|
||||
>
|
||||
<TypeSelector
|
||||
value={dealTicketType}
|
||||
onValueChange={(dealTicketType) => {
|
||||
setType(market.id, dealTicketType);
|
||||
if (!isStopOrderType(dealTicketType)) {
|
||||
reset(
|
||||
getDefaultValues(
|
||||
dealTicketTypeToOrderType(dealTicketType),
|
||||
storedFormValues?.[dealTicketType]
|
||||
)
|
||||
);
|
||||
}
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: validateType(
|
||||
marketData.marketTradingMode,
|
||||
marketData.trigger
|
||||
),
|
||||
}}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
errorMessage={errors.type?.message}
|
||||
render={() => (
|
||||
<TypeSelector
|
||||
value={
|
||||
order.type === OrderType.TYPE_LIMIT
|
||||
? DealTicketType.Limit
|
||||
: DealTicketType.Market
|
||||
}
|
||||
onValueChange={(dealTicketType) => {
|
||||
setDealTicketType(market.id, dealTicketType);
|
||||
if (
|
||||
dealTicketType !== DealTicketType.Limit &&
|
||||
dealTicketType !== DealTicketType.Market
|
||||
) {
|
||||
updateStopOrderFormValues(market.id, {
|
||||
type:
|
||||
dealTicketType === DealTicketType.StopLimit
|
||||
? OrderType.TYPE_LIMIT
|
||||
: OrderType.TYPE_MARKET,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const type =
|
||||
dealTicketType === DealTicketType.Limit
|
||||
? OrderType.TYPE_LIMIT
|
||||
: OrderType.TYPE_MARKET;
|
||||
update({
|
||||
type,
|
||||
// when changing type also update the TIF to what was last used of new type
|
||||
timeInForce: lastTIF[type] || order.timeInForce,
|
||||
postOnly:
|
||||
type === OrderType.TYPE_MARKET ? false : order.postOnly,
|
||||
iceberg:
|
||||
type === OrderType.TYPE_MARKET ||
|
||||
[
|
||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(lastTIF[type] || order.timeInForce)
|
||||
? false
|
||||
: order.iceberg,
|
||||
icebergOpts:
|
||||
type === OrderType.TYPE_MARKET ||
|
||||
[
|
||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(lastTIF[type] || order.timeInForce)
|
||||
? undefined
|
||||
: order.icebergOpts,
|
||||
reduceOnly:
|
||||
type === OrderType.TYPE_LIMIT &&
|
||||
![
|
||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(lastTIF[type] || order.timeInForce)
|
||||
? false
|
||||
: order.postOnly,
|
||||
expiresAt: undefined,
|
||||
});
|
||||
clearErrors(['expiresAt', 'price']);
|
||||
}}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
errorMessage={errors.type?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="side"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SideSelector value={field.value} onValueChange={field.onChange} />
|
||||
render={() => (
|
||||
<SideSelector
|
||||
value={order.side}
|
||||
onValueChange={(side) => {
|
||||
update({ side });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<DealTicketAmount
|
||||
type={type}
|
||||
control={control}
|
||||
orderType={order.type}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
marketPrice={marketPrice || undefined}
|
||||
sizeError={errors.size?.message}
|
||||
priceError={errors.price?.message}
|
||||
update={update}
|
||||
size={order.size}
|
||||
price={order.price}
|
||||
/>
|
||||
<Controller
|
||||
name="timeInForce"
|
||||
@@ -386,29 +415,58 @@ export const DealTicket = ({
|
||||
marketData.trigger
|
||||
),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<TimeInForceSelector
|
||||
value={field.value}
|
||||
orderType={type}
|
||||
onSelect={field.onChange}
|
||||
value={order.timeInForce}
|
||||
orderType={order.type}
|
||||
onSelect={(timeInForce) => {
|
||||
// Reset post only and reduce only when changing TIF
|
||||
update({
|
||||
timeInForce,
|
||||
postOnly: [
|
||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(timeInForce)
|
||||
? false
|
||||
: order.postOnly,
|
||||
reduceOnly: ![
|
||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(timeInForce)
|
||||
? false
|
||||
: order.reduceOnly,
|
||||
});
|
||||
// Set TIF value for the given order type, so that when switching
|
||||
// types we know the last used TIF for the given order type
|
||||
setLastTIF((curr) => ({
|
||||
...curr,
|
||||
[order.type]: timeInForce,
|
||||
expiresAt: undefined,
|
||||
}));
|
||||
clearErrors('expiresAt');
|
||||
}}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
errorMessage={errors.timeInForce?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{type === Schema.OrderType.TYPE_LIMIT &&
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
|
||||
{order.type === Schema.OrderType.TYPE_LIMIT &&
|
||||
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
|
||||
<Controller
|
||||
name="expiresAt"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: validateExpiration,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<ExpirySelector
|
||||
value={field.value}
|
||||
onSelect={(expiresAt) => field.onChange(expiresAt)}
|
||||
value={order.expiresAt}
|
||||
onSelect={(expiresAt) =>
|
||||
update({
|
||||
expiresAt: expiresAt || undefined,
|
||||
})
|
||||
}
|
||||
errorMessage={errors.expiresAt?.message}
|
||||
/>
|
||||
)}
|
||||
@@ -418,14 +476,13 @@ export const DealTicket = ({
|
||||
<Controller
|
||||
name="postOnly"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<Checkbox
|
||||
name="post-only"
|
||||
checked={!disablePostOnlyCheckbox && field.value}
|
||||
checked={order.postOnly}
|
||||
disabled={disablePostOnlyCheckbox}
|
||||
onCheckedChange={(postOnly) => {
|
||||
field.onChange(postOnly);
|
||||
setValue('reduceOnly', false);
|
||||
onCheckedChange={() => {
|
||||
update({ postOnly: !order.postOnly, reduceOnly: false });
|
||||
}}
|
||||
label={
|
||||
<Tooltip
|
||||
@@ -450,14 +507,13 @@ export const DealTicket = ({
|
||||
<Controller
|
||||
name="reduceOnly"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
render={() => (
|
||||
<Checkbox
|
||||
name="reduce-only"
|
||||
checked={!disableReduceOnlyCheckbox && field.value}
|
||||
checked={order.reduceOnly}
|
||||
disabled={disableReduceOnlyCheckbox}
|
||||
onCheckedChange={(reduceOnly) => {
|
||||
field.onChange(reduceOnly);
|
||||
setValue('postOnly', false);
|
||||
onCheckedChange={() => {
|
||||
update({ postOnly: false, reduceOnly: !order.reduceOnly });
|
||||
}}
|
||||
label={
|
||||
<Tooltip
|
||||
@@ -478,49 +534,53 @@ export const DealTicket = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{type === Schema.OrderType.TYPE_LIMIT && (
|
||||
<>
|
||||
<div className="flex gap-2 pb-2 justify-between">
|
||||
<Controller
|
||||
name="iceberg"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
name="iceberg"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
label={
|
||||
<Tooltip
|
||||
description={
|
||||
<p>
|
||||
{t(`Trade only a fraction of the order size at once.
|
||||
<div className="flex gap-2 pb-2 justify-between">
|
||||
{order.type === Schema.OrderType.TYPE_LIMIT && (
|
||||
<Controller
|
||||
name="iceberg"
|
||||
control={control}
|
||||
render={() => (
|
||||
<Checkbox
|
||||
name="iceberg"
|
||||
checked={order.iceberg}
|
||||
onCheckedChange={() => {
|
||||
update({ iceberg: !order.iceberg, icebergOpts: undefined });
|
||||
}}
|
||||
label={
|
||||
<Tooltip
|
||||
description={
|
||||
<p>
|
||||
{t(`Trade only a fraction of the order size at once.
|
||||
After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away.
|
||||
For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each.
|
||||
Note that the full volume of the order is not hidden and is still reflected in the order book.`)}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<span className="text-xs">{t('Iceberg')}</span>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{iceberg && (
|
||||
<DealTicketSizeIceberg
|
||||
market={market}
|
||||
peakSizeError={errors.peakSize?.message}
|
||||
minimumVisibleSizeError={errors.minimumVisibleSize?.message}
|
||||
control={control}
|
||||
size={rawSize}
|
||||
peakSize={peakSize}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<span className="text-xs">{t('Iceberg')}</span>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{order.iceberg && (
|
||||
<DealTicketSizeIceberg
|
||||
update={update}
|
||||
market={market}
|
||||
peakSizeError={errors.icebergOpts?.peakSize?.message}
|
||||
minimumVisibleSizeError={
|
||||
errors.icebergOpts?.minimumVisibleSize?.message
|
||||
}
|
||||
control={control}
|
||||
size={order.size}
|
||||
peakSize={order.icebergOpts?.peakSize || ''}
|
||||
minimumVisibleSize={order.icebergOpts?.minimumVisibleSize || ''}
|
||||
/>
|
||||
)}
|
||||
<SummaryMessage
|
||||
error={summaryError}
|
||||
errorMessage={errors.summary?.message}
|
||||
asset={asset}
|
||||
marketTradingMode={marketData.marketTradingMode}
|
||||
balance={balance}
|
||||
@@ -533,7 +593,7 @@ export const DealTicket = ({
|
||||
onClickCollateral={onClickCollateral}
|
||||
onDeposit={onDeposit}
|
||||
/>
|
||||
<DealTicketButton side={side} />
|
||||
<DealTicketButton side={order.side} />
|
||||
<DealTicketFeeDetails
|
||||
order={
|
||||
normalizedOrder && { ...normalizedOrder, price: price || undefined }
|
||||
@@ -559,7 +619,7 @@ export const DealTicket = ({
|
||||
* renders warnings about current state of the market
|
||||
*/
|
||||
interface SummaryMessageProps {
|
||||
error?: { message: string; type: string };
|
||||
errorMessage?: string;
|
||||
asset: { id: string; symbol: string; name: string; decimals: number };
|
||||
marketTradingMode: MarketData['marketTradingMode'];
|
||||
balance: string;
|
||||
@@ -589,7 +649,7 @@ export const NoWalletWarning = ({
|
||||
|
||||
const SummaryMessage = memo(
|
||||
({
|
||||
error,
|
||||
errorMessage,
|
||||
asset,
|
||||
marketTradingMode,
|
||||
balance,
|
||||
@@ -605,7 +665,7 @@ const SummaryMessage = memo(
|
||||
return <NoWalletWarning isReadOnly={isReadOnly} />;
|
||||
}
|
||||
|
||||
if (error?.type === SummaryValidationType.NoCollateral) {
|
||||
if (errorMessage === SummaryValidationType.NoCollateral) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<ZeroBalanceError
|
||||
@@ -619,11 +679,11 @@ const SummaryMessage = memo(
|
||||
|
||||
// If we have any other full error which prevents
|
||||
// submission render that first
|
||||
if (error?.message) {
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<InputError testId="deal-ticket-error-message-summary">
|
||||
{error?.message}
|
||||
{errorMessage}
|
||||
</InputError>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,13 +21,6 @@ interface TimeInForceSelectorProps {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const typeLimitOptions = Object.entries(Schema.OrderTimeInForce);
|
||||
const typeMarketOptions = typeLimitOptions.filter(
|
||||
([_, timeInForce]) =>
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
|
||||
export const TimeInForceSelector = ({
|
||||
value,
|
||||
orderType,
|
||||
@@ -38,8 +31,12 @@ export const TimeInForceSelector = ({
|
||||
}: TimeInForceSelectorProps) => {
|
||||
const options =
|
||||
orderType === Schema.OrderType.TYPE_LIMIT
|
||||
? typeLimitOptions
|
||||
: typeMarketOptions;
|
||||
? Object.entries(Schema.OrderTimeInForce)
|
||||
: Object.entries(Schema.OrderTimeInForce).filter(
|
||||
([_, timeInForce]) =>
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
|
||||
const renderError = (errorType: string) => {
|
||||
if (errorType === MarketModeValidationType.Auction) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { t } from '@vegaprotocol/i18n';
|
||||
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||
import { compileGridData } from '../trading-mode-tooltip';
|
||||
import { MarketModeValidationType } from '../../constants';
|
||||
import { DealTicketType } from '../../hooks/use-form-values';
|
||||
import { DealTicketType } from '../../hooks/use-type-store';
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||
import classNames from 'classnames';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './__generated__/EstimateOrder';
|
||||
export * from './use-estimate-fees';
|
||||
export * from './use-form-values';
|
||||
export * from './use-type-store';
|
||||
export * from './use-stop-order-form-values';
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
import type { OrderTimeInForce, Side, OrderType } from '@vegaprotocol/types';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
export enum DealTicketType {
|
||||
Limit = 'Limit',
|
||||
Market = 'Market',
|
||||
StopLimit = 'StopLimit',
|
||||
StopMarket = 'StopMarket',
|
||||
}
|
||||
|
||||
export interface StopOrderFormValues {
|
||||
side: Side;
|
||||
|
||||
triggerDirection: Schema.StopOrderTriggerDirection;
|
||||
|
||||
triggerType: 'price' | 'trailingPercentOffset';
|
||||
triggerPrice?: string;
|
||||
triggerTrailingPercentOffset?: string;
|
||||
|
||||
type: OrderType;
|
||||
size: string;
|
||||
timeInForce: OrderTimeInForce;
|
||||
price?: string;
|
||||
|
||||
expire: boolean;
|
||||
expiryStrategy?: Schema.StopOrderExpiryStrategy;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export type OrderFormValues = {
|
||||
type: OrderType;
|
||||
side: Side;
|
||||
size: string;
|
||||
timeInForce: OrderTimeInForce;
|
||||
price?: string;
|
||||
expiresAt?: string | undefined;
|
||||
postOnly?: boolean;
|
||||
reduceOnly?: boolean;
|
||||
iceberg?: boolean;
|
||||
peakSize?: string;
|
||||
minimumVisibleSize?: string;
|
||||
};
|
||||
|
||||
type UpdateOrder = (marketId: string, values: Partial<OrderFormValues>) => void;
|
||||
|
||||
type UpdateStopOrder = (
|
||||
marketId: string,
|
||||
values: Partial<StopOrderFormValues>
|
||||
) => void;
|
||||
|
||||
type Store = {
|
||||
updateOrder: UpdateOrder;
|
||||
updateStopOrder: UpdateStopOrder;
|
||||
setType: (marketId: string, value: DealTicketType) => void;
|
||||
updateAll: (
|
||||
marketId: string,
|
||||
values: { size?: string; price?: string }
|
||||
) => void;
|
||||
formValues: Record<
|
||||
string,
|
||||
| {
|
||||
[DealTicketType.Limit]?: Partial<OrderFormValues>;
|
||||
[DealTicketType.Market]?: Partial<OrderFormValues>;
|
||||
[DealTicketType.StopLimit]?: Partial<StopOrderFormValues>;
|
||||
[DealTicketType.StopMarket]?: Partial<StopOrderFormValues>;
|
||||
type?: DealTicketType;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
};
|
||||
|
||||
export const dealTicketTypeToOrderType = (dealTicketType?: DealTicketType) =>
|
||||
dealTicketType === DealTicketType.Limit ||
|
||||
dealTicketType === DealTicketType.StopLimit
|
||||
? Schema.OrderType.TYPE_LIMIT
|
||||
: Schema.OrderType.TYPE_MARKET;
|
||||
|
||||
export const isStopOrderType = (dealTicketType?: DealTicketType) =>
|
||||
dealTicketType === DealTicketType.StopLimit ||
|
||||
dealTicketType === DealTicketType.StopMarket;
|
||||
|
||||
export const useDealTicketFormValues = create<Store>()(
|
||||
immer(
|
||||
persist(
|
||||
subscribeWithSelector((set) => ({
|
||||
formValues: {},
|
||||
updateStopOrder: (marketId, formValues) => {
|
||||
set((state) => {
|
||||
const type =
|
||||
formValues.type === Schema.OrderType.TYPE_LIMIT
|
||||
? DealTicketType.StopLimit
|
||||
: DealTicketType.StopMarket;
|
||||
const market = state.formValues[marketId] || {};
|
||||
if (!state.formValues[marketId]) {
|
||||
state.formValues[marketId] = market;
|
||||
}
|
||||
market[type] = Object.assign(market[type] ?? {}, formValues);
|
||||
});
|
||||
},
|
||||
updateOrder: (marketId, formValues) => {
|
||||
set((state) => {
|
||||
const type =
|
||||
formValues.type === Schema.OrderType.TYPE_LIMIT
|
||||
? DealTicketType.Limit
|
||||
: DealTicketType.Market;
|
||||
const market = state.formValues[marketId] || {};
|
||||
if (!state.formValues[marketId]) {
|
||||
state.formValues[marketId] = market;
|
||||
}
|
||||
market[type] = Object.assign(market[type] ?? {}, formValues);
|
||||
});
|
||||
},
|
||||
updateAll: (
|
||||
marketId: string,
|
||||
formValues: { size?: string; price?: string }
|
||||
) => {
|
||||
set((state) => {
|
||||
const market = state.formValues[marketId] || {};
|
||||
if (!state.formValues[marketId]) {
|
||||
state.formValues[marketId] = market;
|
||||
}
|
||||
for (const type of Object.values(DealTicketType)) {
|
||||
market[type] = Object.assign(market[type] ?? {}, formValues);
|
||||
}
|
||||
});
|
||||
},
|
||||
setType: (marketId, type) => {
|
||||
set((state) => {
|
||||
state.formValues[marketId] = Object.assign(
|
||||
state.formValues[marketId] ?? {},
|
||||
{ type }
|
||||
);
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: 'vega_deal_ticket_store',
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
import omit from 'lodash/omit';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { getDefaultOrder, useCreateOrderStore } from '@vegaprotocol/orders';
|
||||
import { useOrderForm } from './use-order-form';
|
||||
|
||||
jest.mock('zustand');
|
||||
|
||||
describe('useOrderForm', () => {
|
||||
const marketId = 'market-id';
|
||||
const setup = (marketId: string) => {
|
||||
return renderHook(() => useOrderForm(marketId));
|
||||
};
|
||||
const { result } = renderHook(() => useCreateOrderStore());
|
||||
const useOrderStore = result.current;
|
||||
|
||||
it('updates form fields when the order changes', async () => {
|
||||
const order = getDefaultOrder(marketId);
|
||||
const { result } = setup(marketId);
|
||||
// expect default values
|
||||
expect(result.current.order).toEqual(order);
|
||||
expect(result.current.getValues()).toEqual(order);
|
||||
|
||||
const priceUpdate = {
|
||||
...order,
|
||||
price: '100',
|
||||
size: '22',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[marketId]: priceUpdate,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// check order store has updated fields
|
||||
expect(result.current.order).toEqual(priceUpdate);
|
||||
// check react-hook-form has updated fields
|
||||
expect(result.current.getValues()).toEqual(priceUpdate);
|
||||
});
|
||||
|
||||
it('removes persist key on submit', async () => {
|
||||
const order = {
|
||||
...getDefaultOrder(marketId),
|
||||
price: '99',
|
||||
size: '22',
|
||||
};
|
||||
const onSubmit = jest.fn();
|
||||
const { result } = setup(marketId);
|
||||
|
||||
await act(async () => {
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[marketId]: order,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleSubmit(onSubmit)();
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit.mock.calls[0][0]).toEqual(omit(order, 'persist'));
|
||||
expect(onSubmit.mock.calls[0][0].persist).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import omit from 'lodash/omit';
|
||||
import type { OrderObj } from '@vegaprotocol/orders';
|
||||
import { getDefaultOrder, useOrder } from '@vegaprotocol/orders';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
|
||||
export type OrderFormFields = OrderObj & {
|
||||
summary: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Connects the order store to a react-hook-form instance. Any time a field
|
||||
* changes in the store the form will be updated so that validation rules
|
||||
* for those fields are applied
|
||||
*/
|
||||
export const useOrderForm = (marketId: string) => {
|
||||
const [order, update] = useOrder(marketId);
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitted },
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
clearErrors,
|
||||
getValues,
|
||||
} = useForm<OrderFormFields>({
|
||||
// order can be undefined if there is nothing in the store, it
|
||||
// will be created but the form still needs some default values
|
||||
defaultValues: order || getDefaultOrder(marketId),
|
||||
});
|
||||
|
||||
// Keep form fields in sync with the store values,
|
||||
// inputs are updating the store, fields need updating
|
||||
// to ensure validation rules are applied
|
||||
useEffect(() => {
|
||||
if (!order) return;
|
||||
const currOrder = getValues();
|
||||
for (const k in order) {
|
||||
const key = k as keyof typeof order;
|
||||
const curr = currOrder[key];
|
||||
const value = order[key];
|
||||
if (value !== curr) {
|
||||
setValue(key, value, {
|
||||
shouldValidate: isSubmitted, // only apply validation after the form has been submitted and failed
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [order, isSubmitted, getValues, setValue]);
|
||||
|
||||
const handleSubmitWrapper = (cb: (o: OrderSubmission) => void) => {
|
||||
return handleSubmit(() => {
|
||||
// remove the persist and iceberg key from the order in the store, the wallet will reject
|
||||
// an order that contains unrecognized additional keys
|
||||
cb(omit(order, 'persist', 'iceberg'));
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
order,
|
||||
update,
|
||||
control,
|
||||
errors,
|
||||
setError,
|
||||
clearErrors,
|
||||
getValues, // returned for test purposes only
|
||||
handleSubmit: handleSubmitWrapper,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
import type { OrderTimeInForce, Side, OrderType } from '@vegaprotocol/types';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
|
||||
export interface StopOrderFormValues {
|
||||
side: Side;
|
||||
|
||||
triggerDirection: Schema.StopOrderTriggerDirection;
|
||||
|
||||
triggerType: 'price' | 'trailingPercentOffset';
|
||||
triggerPrice: string;
|
||||
triggerTrailingPercentOffset: string;
|
||||
|
||||
type: OrderType;
|
||||
size: string;
|
||||
timeInForce: OrderTimeInForce;
|
||||
price?: string;
|
||||
|
||||
expire: boolean;
|
||||
expiryStrategy?: Schema.StopOrderExpiryStrategy;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
type StopOrderFormValuesMap = {
|
||||
[marketId: string]: Partial<StopOrderFormValues> | undefined;
|
||||
};
|
||||
|
||||
type Update = (
|
||||
marketId: string,
|
||||
formValues: Partial<StopOrderFormValues>,
|
||||
persist?: boolean
|
||||
) => void;
|
||||
|
||||
interface Store {
|
||||
formValues: StopOrderFormValuesMap;
|
||||
update: Update;
|
||||
}
|
||||
|
||||
export const useStopOrderFormValues = create<Store>()(
|
||||
persist(
|
||||
subscribeWithSelector((set) => ({
|
||||
formValues: {},
|
||||
update: (marketId, formValues, persist = true) => {
|
||||
set((state) => {
|
||||
return {
|
||||
formValues: {
|
||||
...state.formValues,
|
||||
[marketId]: {
|
||||
...state.formValues[marketId],
|
||||
...formValues,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: 'vega_stop_order_store',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
|
||||
export enum DealTicketType {
|
||||
Limit = 'Limit',
|
||||
Market = 'Market',
|
||||
StopLimit = 'StopLimit',
|
||||
StopMarket = 'StopMarket',
|
||||
}
|
||||
|
||||
export const useDealTicketTypeStore = create<{
|
||||
set: (marketId: string, type: DealTicketType) => void;
|
||||
type: Record<string, DealTicketType>;
|
||||
}>()(
|
||||
persist(
|
||||
subscribeWithSelector((set) => ({
|
||||
type: {},
|
||||
set: (marketId: string, type: DealTicketType) =>
|
||||
set((state) => ({
|
||||
...state,
|
||||
type: { ...state.type, [marketId]: type },
|
||||
})),
|
||||
})),
|
||||
{
|
||||
name: 'deal_ticket_type',
|
||||
}
|
||||
)
|
||||
);
|
||||
+5
-60
@@ -1,64 +1,12 @@
|
||||
import type {
|
||||
OrderSubmission,
|
||||
StopOrderSetup,
|
||||
StopOrdersSubmission,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type {
|
||||
OrderFormValues,
|
||||
StopOrderFormValues,
|
||||
} from '../hooks/use-form-values';
|
||||
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
|
||||
import type { StopOrderFormValues } from '../hooks/use-stop-order-form-values';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
|
||||
export const mapFormValuesToOrderSubmission = (
|
||||
order: OrderFormValues,
|
||||
marketId: string,
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number
|
||||
): OrderSubmission => ({
|
||||
marketId: marketId,
|
||||
type: order.type,
|
||||
side: order.side,
|
||||
timeInForce: order.timeInForce,
|
||||
price:
|
||||
order.type === Schema.OrderType.TYPE_LIMIT && order.price
|
||||
? removeDecimal(order.price, decimalPlaces)
|
||||
: undefined,
|
||||
size: removeDecimal(order.size, positionDecimalPlaces),
|
||||
expiresAt:
|
||||
order.expiresAt &&
|
||||
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
? toNanoSeconds(order.expiresAt)
|
||||
: undefined,
|
||||
postOnly:
|
||||
order.type === Schema.OrderType.TYPE_MARKET ? false : order.postOnly,
|
||||
reduceOnly:
|
||||
order.type === Schema.OrderType.TYPE_LIMIT &&
|
||||
![
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(order.timeInForce)
|
||||
? false
|
||||
: order.reduceOnly,
|
||||
icebergOpts:
|
||||
(order.type === Schema.OrderType.TYPE_MARKET ||
|
||||
[
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(order.timeInForce)) &&
|
||||
order.iceberg &&
|
||||
order.peakSize &&
|
||||
order.minimumVisibleSize
|
||||
? {
|
||||
peakSize: removeDecimal(order.peakSize, positionDecimalPlaces),
|
||||
minimumVisibleSize: removeDecimal(
|
||||
order.minimumVisibleSize,
|
||||
positionDecimalPlaces
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export const mapFormValuesToStopOrdersSubmission = (
|
||||
data: StopOrderFormValues,
|
||||
marketId: string,
|
||||
@@ -67,8 +15,9 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
): StopOrdersSubmission => {
|
||||
const submission: StopOrdersSubmission = {};
|
||||
const stopOrderSetup: StopOrderSetup = {
|
||||
orderSubmission: mapFormValuesToOrderSubmission(
|
||||
orderSubmission: normalizeOrderSubmission(
|
||||
{
|
||||
marketId,
|
||||
type: data.type,
|
||||
side: data.side,
|
||||
size: data.size,
|
||||
@@ -76,16 +25,12 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
price: data.price,
|
||||
reduceOnly: true,
|
||||
},
|
||||
marketId,
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces
|
||||
),
|
||||
};
|
||||
if (data.triggerType === 'price') {
|
||||
stopOrderSetup.price = removeDecimal(
|
||||
data.triggerPrice ?? '',
|
||||
decimalPlaces
|
||||
);
|
||||
stopOrderSetup.price = removeDecimal(data.triggerPrice, decimalPlaces);
|
||||
} else if (data.triggerType === 'trailingPercentOffset') {
|
||||
stopOrderSetup.trailingPercentOffset = (
|
||||
Number(data.triggerTrailingPercentOffset) / 100
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { mapFormValuesToOrderSubmission } from './map-form-values-to-submission';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
describe('mapFormValuesToOrderSubmission', () => {
|
||||
it('sets and formats price only for limit orders', () => {
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{ price: '100' } as unknown as OrderSubmissionBody['orderSubmission'],
|
||||
'marketId',
|
||||
2,
|
||||
1
|
||||
).price
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
price: '100',
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
} as unknown as OrderSubmissionBody['orderSubmission'],
|
||||
'marketId',
|
||||
2,
|
||||
1
|
||||
).price
|
||||
).toEqual('10000');
|
||||
});
|
||||
|
||||
it('sets and formats expiresAt only for time in force orders', () => {
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
expiresAt: '2022-01-01T00:00:00.000Z',
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
'marketId',
|
||||
2,
|
||||
1
|
||||
).expiresAt
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
expiresAt: '2022-01-01T00:00:00.000Z',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
'marketId',
|
||||
2,
|
||||
1
|
||||
).expiresAt
|
||||
).toEqual('1640995200000000000');
|
||||
});
|
||||
|
||||
it('formats size', () => {
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
size: '100',
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
'marketId',
|
||||
2,
|
||||
1
|
||||
).size
|
||||
).toEqual('1000');
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ describe('FillsTable', () => {
|
||||
positionDecimalPlaces: 5,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'test market',
|
||||
name: 'test market',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
decimals: 2,
|
||||
@@ -71,7 +71,7 @@ describe('FillsTable', () => {
|
||||
render(<FillsTable partyId={partyId} rowData={[{ ...buyerFill }]} />);
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
buyerFill.market?.tradableInstrument.instrument.code || '',
|
||||
buyerFill.market?.tradableInstrument.instrument.name || '',
|
||||
'+3.00',
|
||||
'1.00 BTC',
|
||||
'3.00 BTC',
|
||||
@@ -106,7 +106,7 @@ describe('FillsTable', () => {
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
buyerFill.market?.tradableInstrument.instrument.code || '',
|
||||
buyerFill.market?.tradableInstrument.instrument.name || '',
|
||||
'-3.00',
|
||||
'1.00 BTC',
|
||||
'3.00 BTC',
|
||||
@@ -141,7 +141,7 @@ describe('FillsTable', () => {
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
buyerFill.market?.tradableInstrument.instrument.code || '',
|
||||
buyerFill.market?.tradableInstrument.instrument.name || '',
|
||||
'-3.00',
|
||||
'1.00 BTC',
|
||||
'3.00 BTC',
|
||||
|
||||
@@ -47,7 +47,7 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
|
||||
() => [
|
||||
{
|
||||
headerName: t('Market'),
|
||||
field: 'market.tradableInstrument.instrument.code',
|
||||
field: 'market.tradableInstrument.instrument.name',
|
||||
cellRenderer: 'MarketNameCell',
|
||||
cellRendererParams: { idPath: 'market.id', onMarketClick },
|
||||
},
|
||||
|
||||
@@ -34,8 +34,5 @@ query SuccessorMarket($marketId: ID!) {
|
||||
code
|
||||
}
|
||||
}
|
||||
proposal {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -27,7 +27,7 @@ export type SuccessorMarketQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type SuccessorMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, state: Types.MarketState, tradingMode: Types.MarketTradingMode, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } }, proposal?: { __typename?: 'Proposal', id?: string | null } | null } | null };
|
||||
export type SuccessorMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, state: Types.MarketState, tradingMode: Types.MarketTradingMode, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } } } | null };
|
||||
|
||||
|
||||
export const SuccessorMarketIdDocument = gql`
|
||||
@@ -153,9 +153,6 @@ export const SuccessorMarketDocument = gql`
|
||||
code
|
||||
}
|
||||
}
|
||||
proposal {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -4,10 +4,8 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
Intent,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Lozenge,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import BigNumber from 'bignumber.js';
|
||||
@@ -24,11 +22,9 @@ interface RowProps {
|
||||
unformatted?: boolean;
|
||||
assetSymbol?: string;
|
||||
noBorder?: boolean;
|
||||
parentValue?: ReactNode;
|
||||
hasParentData?: boolean;
|
||||
}
|
||||
|
||||
export const Row = ({
|
||||
const Row = ({
|
||||
field,
|
||||
value,
|
||||
decimalPlaces,
|
||||
@@ -36,14 +32,7 @@ export const Row = ({
|
||||
unformatted,
|
||||
assetSymbol = '',
|
||||
noBorder = true,
|
||||
parentValue,
|
||||
hasParentData,
|
||||
}: RowProps) => {
|
||||
// Note: we need both 'parentValue' and 'hasParentData' to do a conditional
|
||||
// check to differentiate between when parentData itself is missing and when
|
||||
// a specific parentValue is missing. These values are only used when we
|
||||
// have successor market parent data.
|
||||
|
||||
const className = 'text-sm';
|
||||
|
||||
const getFormattedValue = (value: ReactNode) => {
|
||||
@@ -66,10 +55,6 @@ export const Row = ({
|
||||
const formattedValue = getFormattedValue(value);
|
||||
|
||||
if (!formattedValue) return null;
|
||||
|
||||
const newValueInSuccessorMarket = hasParentData && value && !parentValue;
|
||||
const valueDiffersFromParentMarket = parentValue && parentValue !== value;
|
||||
|
||||
return (
|
||||
<KeyValueTableRow
|
||||
key={field}
|
||||
@@ -78,35 +63,10 @@ export const Row = ({
|
||||
dtClassName={className}
|
||||
ddClassName={className}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Tooltip description={tooltipMapping[field]} align="start">
|
||||
<div tabIndex={-1}>{startCase(t(field))}</div>
|
||||
</Tooltip>
|
||||
|
||||
{valueDiffersFromParentMarket && (
|
||||
<Lozenge className="py-0" variant={Intent.Primary}>
|
||||
{t('Updated')}
|
||||
</Lozenge>
|
||||
)}
|
||||
|
||||
{newValueInSuccessorMarket && (
|
||||
<Lozenge className="py-0" variant={Intent.Primary}>
|
||||
{t('Added')}
|
||||
</Lozenge>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ wordBreak: 'break-word' }}>
|
||||
{valueDiffersFromParentMarket ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="line-through">
|
||||
{getFormattedValue(parentValue)}
|
||||
</span>
|
||||
<span>{formattedValue}</span>
|
||||
</div>
|
||||
) : (
|
||||
formattedValue
|
||||
)}
|
||||
</div>
|
||||
<Tooltip description={tooltipMapping[field]} align="start">
|
||||
<div tabIndex={-1}>{startCase(t(field))}</div>
|
||||
</Tooltip>
|
||||
<span style={{ wordBreak: 'break-word' }}>{formattedValue}</span>
|
||||
</KeyValueTableRow>
|
||||
);
|
||||
};
|
||||
@@ -119,7 +79,6 @@ export interface MarketInfoTableProps {
|
||||
children?: ReactNode;
|
||||
assetSymbol?: string;
|
||||
noBorder?: boolean;
|
||||
parentData?: Record<string, ReactNode> | null | undefined;
|
||||
}
|
||||
|
||||
export const MarketInfoTable = ({
|
||||
@@ -130,33 +89,25 @@ export const MarketInfoTable = ({
|
||||
children,
|
||||
assetSymbol,
|
||||
noBorder,
|
||||
parentData,
|
||||
}: MarketInfoTableProps) => {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasParentData = parentData !== undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<KeyValueTable>
|
||||
<>
|
||||
{Object.entries(data).map(([key, value]) => (
|
||||
<Row
|
||||
key={key}
|
||||
field={key}
|
||||
value={value}
|
||||
decimalPlaces={decimalPlaces}
|
||||
assetSymbol={assetSymbol}
|
||||
asPercentage={asPercentage}
|
||||
unformatted={unformatted}
|
||||
noBorder={noBorder}
|
||||
parentValue={parentData?.[key]}
|
||||
hasParentData={hasParentData}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
{Object.entries(data).map(([key, value]) => (
|
||||
<Row
|
||||
key={key}
|
||||
field={key}
|
||||
value={value}
|
||||
decimalPlaces={decimalPlaces}
|
||||
assetSymbol={assetSymbol}
|
||||
asPercentage={asPercentage}
|
||||
unformatted={unformatted}
|
||||
noBorder={noBorder}
|
||||
/>
|
||||
))}
|
||||
</KeyValueTable>
|
||||
<div className="flex flex-col gap-2">{children}</div>
|
||||
</>
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
FLAGS,
|
||||
TokenStaticLinks,
|
||||
useEnvironment,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { TokenStaticLinks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
@@ -38,7 +34,6 @@ import {
|
||||
RiskModelInfoPanel,
|
||||
RiskParametersInfoPanel,
|
||||
SettlementAssetInfoPanel,
|
||||
SuccessionLineInfoPanel,
|
||||
} from './market-info-panels';
|
||||
import type { DataSourceDefinition } from '@vegaprotocol/types';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
@@ -296,13 +291,6 @@ export const MarketInfoAccordion = ({
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{FLAGS.SUCCESSOR_MARKETS && (
|
||||
<AccordionItem
|
||||
itemId="succession-line"
|
||||
title={t('Succession line')}
|
||||
content={<SuccessionLineInfoPanel market={market} />}
|
||||
/>
|
||||
)}
|
||||
</Accordion>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import {
|
||||
ConditionOperator,
|
||||
ConditionOperatorMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { DataSourceProof, SuccessionLineInfoPanel } from './market-info-panels';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { SuccessorMarketIdsDocument } from '../../__generated__';
|
||||
import { DataSourceProof } from './market-info-panels';
|
||||
|
||||
jest.mock('../../hooks/use-oracle-markets', () => ({
|
||||
useOracleMarkets: () => [],
|
||||
@@ -139,93 +137,3 @@ describe('DataSourceProof', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SuccessionLineInfoPanel', () => {
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: SuccessorMarketIdsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
__typename: 'Query',
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'MarketEdge',
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: 'abc',
|
||||
successorMarketID: 'def',
|
||||
parentMarketID: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'MarketEdge',
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: 'def',
|
||||
successorMarketID: 'ghi',
|
||||
parentMarketID: 'abc',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'MarketEdge',
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: 'ghi',
|
||||
successorMarketID: null,
|
||||
parentMarketID: 'def',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each([
|
||||
['abc', 1],
|
||||
['def', 2],
|
||||
['ghi', 3],
|
||||
])(
|
||||
'renders succession line for %s (current position %d)',
|
||||
async (id, number) => {
|
||||
render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<SuccessionLineInfoPanel
|
||||
market={{
|
||||
id,
|
||||
}}
|
||||
/>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const items = screen.getAllByTestId('succession-line-item');
|
||||
expect(items.length).toBe(3);
|
||||
expect(
|
||||
items[0].querySelector(
|
||||
'[data-testid="succession-line-item-market-id"]'
|
||||
)?.textContent
|
||||
).toBe('abc');
|
||||
expect(
|
||||
items[1].querySelector(
|
||||
'[data-testid="succession-line-item-market-id"]'
|
||||
)?.textContent
|
||||
).toBe('def');
|
||||
expect(
|
||||
items[2].querySelector(
|
||||
'[data-testid="succession-line-item-market-id"]'
|
||||
)?.textContent
|
||||
).toBe('ghi');
|
||||
|
||||
expect(
|
||||
items[number - 1].querySelector('[data-testid="icon-bullet"]')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { marketDataProvider } from '../../market-data-provider';
|
||||
import { totalFeesPercentage } from '../../market-utils';
|
||||
import {
|
||||
ExternalLink,
|
||||
Splash,
|
||||
Tooltip,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
@@ -30,32 +23,17 @@ import BigNumber from 'bignumber.js';
|
||||
import type { DataSourceDefinition, SignerKind } from '@vegaprotocol/types';
|
||||
import { ConditionOperatorMapping } from '@vegaprotocol/types';
|
||||
import { MarketTradingModeMapping } from '@vegaprotocol/types';
|
||||
import {
|
||||
DApp,
|
||||
FLAGS,
|
||||
TOKEN_PROPOSAL,
|
||||
useEnvironment,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { Provider } from '../../oracle-schema';
|
||||
import { OracleBasicProfile } from '../../components/oracle-basic-profile';
|
||||
import { useOracleProofs } from '../../hooks';
|
||||
import { OracleDialog } from '../oracle-dialog/oracle-dialog';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import {
|
||||
useParentMarketIdQuery,
|
||||
useSuccessorMarketIdsQuery,
|
||||
useSuccessorMarketQuery,
|
||||
} from '../../__generated__';
|
||||
import { useParentMarketIdQuery } from '../../__generated__';
|
||||
import { useSuccessorMarketProposalDetailsQuery } from '@vegaprotocol/proposals';
|
||||
import type { MarketTradingMode } from '@vegaprotocol/types';
|
||||
import type { Signer } from '@vegaprotocol/types';
|
||||
import classNames from 'classnames';
|
||||
import compact from 'lodash/compact';
|
||||
|
||||
type MarketInfoProps = {
|
||||
market: MarketInfo;
|
||||
parentMarket?: MarketInfo;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
@@ -160,43 +138,21 @@ export const InsurancePoolInfoPanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const KeyDetailsInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => {
|
||||
const { data: parentMarketIdData } = useParentMarketIdQuery({
|
||||
export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
const { data: parentData } = useParentMarketIdQuery({
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
},
|
||||
skip: !FLAGS.SUCCESSOR_MARKETS,
|
||||
});
|
||||
|
||||
const { data: successorProposalDetails } =
|
||||
useSuccessorMarketProposalDetailsQuery({
|
||||
variables: {
|
||||
proposalId: market.proposal?.id || '',
|
||||
},
|
||||
skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id,
|
||||
});
|
||||
|
||||
// The following queries are needed as the parent market could also have been a successor market.
|
||||
// Note: the parent market is only passed to this component if the successor markets flag is enabled,
|
||||
// so that check is not needed in the skip.
|
||||
const { data: grandparentMarketIdData } = useParentMarketIdQuery({
|
||||
const { data: successor } = useSuccessorMarketProposalDetailsQuery({
|
||||
variables: {
|
||||
marketId: parentMarket?.id || '',
|
||||
proposalId: market.proposal?.id || '',
|
||||
},
|
||||
skip: !parentMarket?.id,
|
||||
skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id,
|
||||
});
|
||||
|
||||
const { data: parentSuccessorProposalDetails } =
|
||||
useSuccessorMarketProposalDetailsQuery({
|
||||
variables: {
|
||||
proposalId: parentMarket?.proposal?.id || '',
|
||||
},
|
||||
skip: !parentMarket?.proposal?.id,
|
||||
});
|
||||
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
@@ -207,12 +163,11 @@ export const KeyDetailsInfoPanel = ({
|
||||
? {
|
||||
name: market.tradableInstrument.instrument.name,
|
||||
marketID: market.id,
|
||||
parentMarketID: parentMarketIdData?.market?.parentMarketID || '-',
|
||||
parentMarketID: parentData?.market?.parentMarketID || '-',
|
||||
insurancePoolFraction:
|
||||
(successorProposalDetails?.proposal?.terms.change.__typename ===
|
||||
'NewMarket' &&
|
||||
successorProposalDetails.proposal.terms.change
|
||||
.successorConfiguration?.insurancePoolFraction) ||
|
||||
(successor?.proposal?.terms.change.__typename === 'NewMarket' &&
|
||||
successor.proposal.terms.change.successorConfiguration
|
||||
?.insurancePoolFraction) ||
|
||||
'-',
|
||||
tradingMode:
|
||||
market.tradingMode &&
|
||||
@@ -232,156 +187,11 @@ export const KeyDetailsInfoPanel = ({
|
||||
settlementAssetDecimalPlaces: assetDecimals,
|
||||
}
|
||||
}
|
||||
parentData={
|
||||
parentMarket && {
|
||||
name: parentMarket?.tradableInstrument?.instrument?.name,
|
||||
marketID: parentMarket?.id,
|
||||
parentMarketID: grandparentMarketIdData?.market?.parentMarketID,
|
||||
insurancePoolFraction:
|
||||
parentSuccessorProposalDetails?.proposal?.terms.change
|
||||
.__typename === 'NewMarket' &&
|
||||
parentSuccessorProposalDetails.proposal.terms.change
|
||||
.successorConfiguration?.insurancePoolFraction,
|
||||
tradingMode:
|
||||
parentMarket?.tradingMode &&
|
||||
MarketTradingModeMapping[
|
||||
parentMarket.tradingMode as MarketTradingMode
|
||||
],
|
||||
marketDecimalPlaces: parentMarket?.decimalPlaces,
|
||||
positionDecimalPlaces: parentMarket?.positionDecimalPlaces,
|
||||
settlementAssetDecimalPlaces:
|
||||
parentMarket?.tradableInstrument?.instrument?.product
|
||||
?.settlementAsset?.decimals,
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SuccessionLineItem = ({
|
||||
marketId,
|
||||
isCurrent,
|
||||
}: {
|
||||
marketId: string;
|
||||
isCurrent?: boolean;
|
||||
}) => {
|
||||
const { data } = useSuccessorMarketQuery({
|
||||
variables: {
|
||||
marketId,
|
||||
},
|
||||
});
|
||||
|
||||
const marketData = data?.market;
|
||||
const governanceLink = useLinks(DApp.Token);
|
||||
const proposalLink = marketData?.proposal?.id
|
||||
? governanceLink(TOKEN_PROPOSAL.replace(':id', marketData?.proposal?.id))
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="succession-line-item"
|
||||
className={classNames(
|
||||
'rounded p-2 bg-vega-clight-700 dark:bg-vega-cdark-700',
|
||||
'font-alpha',
|
||||
'flex flex-col '
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
{marketData ? (
|
||||
proposalLink ? (
|
||||
<ExternalLink href={proposalLink}>
|
||||
{marketData.tradableInstrument.instrument.code}
|
||||
</ExternalLink>
|
||||
) : (
|
||||
marketData.tradableInstrument.instrument.code
|
||||
)
|
||||
) : (
|
||||
<span className="block w-20 h-4 mb-1 bg-vega-clight-500 dark:bg-vega-cdark-500 animate-pulse"></span>
|
||||
)}
|
||||
</div>
|
||||
{isCurrent && (
|
||||
<Tooltip description={t('This market')}>
|
||||
<div className="text-vega-clight-200 dark:text-vega-cdark-200 cursor-help">
|
||||
<VegaIcon name={VegaIconNames.BULLET} size={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
{marketData ? (
|
||||
marketData.tradableInstrument.instrument.name
|
||||
) : (
|
||||
<span className="block w-28 h-4 bg-vega-clight-500 dark:bg-vega-cdark-500 animate-pulse"></span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
data-testid="succession-line-item-market-id"
|
||||
className="text-xs truncate mt-1"
|
||||
>
|
||||
{marketId}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SuccessionLink = () => (
|
||||
<div className="text-center leading-none" aria-hidden>
|
||||
<VegaIcon name={VegaIconNames.ARROW_DOWN} size={12} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const buildSuccessionLine = (
|
||||
all: {
|
||||
id: string;
|
||||
successorMarketID?: string | null | undefined;
|
||||
parentMarketID?: string | null | undefined;
|
||||
}[],
|
||||
id: string
|
||||
) => {
|
||||
let line = [id];
|
||||
const find = (id: string, dir?: 'up' | 'down') => {
|
||||
const item = all.find((a) => a.id === id);
|
||||
const anc = dir === 'up' && item?.parentMarketID;
|
||||
const des = dir === 'down' && item?.successorMarketID;
|
||||
if (anc) {
|
||||
line = [anc, ...line];
|
||||
find(anc, 'up');
|
||||
}
|
||||
if (des) {
|
||||
line = [...line, des];
|
||||
find(des, 'down');
|
||||
}
|
||||
};
|
||||
find(id, 'up');
|
||||
find(id, 'down');
|
||||
return line;
|
||||
};
|
||||
export const SuccessionLineInfoPanel = ({
|
||||
market,
|
||||
}: {
|
||||
market: Pick<MarketInfo, 'id'>;
|
||||
}) => {
|
||||
const { data } = useSuccessorMarketIdsQuery();
|
||||
const ids = compact(data?.marketsConnection?.edges.map((e) => e.node));
|
||||
const line = buildSuccessionLine(ids, market.id);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{line.map((id, i) => (
|
||||
<Fragment key={i}>
|
||||
{i > 0 && <SuccessionLink />}
|
||||
<SuccessionLineItem marketId={id} isCurrent={id === market.id} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InstrumentInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => (
|
||||
export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
@@ -389,16 +199,6 @@ export const InstrumentInfoPanel = ({
|
||||
productType: market.tradableInstrument.instrument.product.__typename,
|
||||
quoteName: market.tradableInstrument.instrument.product.quoteName,
|
||||
}}
|
||||
parentData={
|
||||
parentMarket && {
|
||||
marketName: parentMarket?.tradableInstrument?.instrument?.name,
|
||||
code: parentMarket?.tradableInstrument?.instrument?.code,
|
||||
productType:
|
||||
parentMarket?.tradableInstrument?.instrument?.product?.__typename,
|
||||
quoteName:
|
||||
parentMarket?.tradableInstrument?.instrument?.product?.quoteName,
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -411,7 +211,6 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
);
|
||||
|
||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
||||
return asset ? (
|
||||
<>
|
||||
@@ -434,148 +233,54 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getMarketMetadata = (market: MarketInfo) =>
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
?.map((tag) => {
|
||||
const [key, value] = tag.split(':');
|
||||
return { [key]: value };
|
||||
})
|
||||
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
||||
|
||||
export const MetadataInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => (
|
||||
export const MetadataInfoPanel = ({ market }: MarketInfoProps) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
expiryDate: getMarketExpiryDateFormatted(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
),
|
||||
...(getMarketMetadata(market) || {}),
|
||||
...market.tradableInstrument.instrument.metadata.tags
|
||||
?.map((tag) => {
|
||||
const [key, value] = tag.split(':');
|
||||
return { [key]: value };
|
||||
})
|
||||
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
|
||||
}}
|
||||
parentData={
|
||||
parentMarket && {
|
||||
expiryDate: getMarketExpiryDateFormatted(
|
||||
parentMarket.tradableInstrument.instrument.metadata.tags
|
||||
),
|
||||
...(getMarketMetadata(parentMarket) || {}),
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RiskModelInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => {
|
||||
export const RiskModelInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
if (market.tradableInstrument.riskModel.__typename !== 'LogNormalRiskModel') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { tau, riskAversionParameter } = market.tradableInstrument.riskModel;
|
||||
|
||||
let parentData;
|
||||
|
||||
if (
|
||||
parentMarket?.tradableInstrument?.riskModel?.__typename ===
|
||||
'LogNormalRiskModel'
|
||||
) {
|
||||
const {
|
||||
tau: parentTau,
|
||||
riskAversionParameter: parentRiskAversionParameter,
|
||||
} = market.tradableInstrument.riskModel;
|
||||
|
||||
parentData = {
|
||||
tau: parentTau,
|
||||
riskAversionParameter: parentRiskAversionParameter,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<MarketInfoTable
|
||||
data={{ tau, riskAversionParameter }}
|
||||
parentData={parentData}
|
||||
unformatted
|
||||
/>
|
||||
);
|
||||
return <MarketInfoTable data={{ tau, riskAversionParameter }} unformatted />;
|
||||
};
|
||||
|
||||
export const RiskParametersInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => {
|
||||
const marketType = market.tradableInstrument.riskModel.__typename;
|
||||
|
||||
let data, parentData;
|
||||
|
||||
if (marketType === 'LogNormalRiskModel') {
|
||||
export const RiskParametersInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
if (market.tradableInstrument.riskModel.__typename === 'LogNormalRiskModel') {
|
||||
const { r, sigma, mu } = market.tradableInstrument.riskModel.params;
|
||||
data = { r, sigma, mu };
|
||||
|
||||
if (
|
||||
parentMarket?.tradableInstrument?.riskModel.__typename ===
|
||||
'LogNormalRiskModel'
|
||||
) {
|
||||
const parentParams = parentMarket.tradableInstrument.riskModel.params;
|
||||
parentData = {
|
||||
r: parentParams.r,
|
||||
sigma: parentParams.sigma,
|
||||
mu: parentParams.mu,
|
||||
};
|
||||
}
|
||||
} else if (marketType === 'SimpleRiskModel') {
|
||||
return <MarketInfoTable data={{ r, sigma, mu }} unformatted />;
|
||||
}
|
||||
if (market.tradableInstrument.riskModel.__typename === 'SimpleRiskModel') {
|
||||
const { factorLong, factorShort } =
|
||||
market.tradableInstrument.riskModel.params;
|
||||
data = { factorLong, factorShort };
|
||||
|
||||
if (
|
||||
parentMarket?.tradableInstrument?.riskModel.__typename ===
|
||||
'SimpleRiskModel'
|
||||
) {
|
||||
const parentParams = parentMarket.tradableInstrument.riskModel.params;
|
||||
parentData = {
|
||||
factorLong: parentParams.factorLong,
|
||||
factorShort: parentParams.factorShort,
|
||||
};
|
||||
}
|
||||
return <MarketInfoTable data={{ factorLong, factorShort }} unformatted />;
|
||||
}
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return <MarketInfoTable data={data} parentData={parentData} unformatted />;
|
||||
return null;
|
||||
};
|
||||
|
||||
export const RiskFactorsInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => {
|
||||
export const RiskFactorsInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
if (!market.riskFactors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { short, long } = market.riskFactors;
|
||||
|
||||
let parentData;
|
||||
|
||||
if (parentMarket?.riskFactors) {
|
||||
const parentShort = parentMarket.riskFactors.short;
|
||||
const parentLong = parentMarket.riskFactors.long;
|
||||
parentData = { short: parentShort, long: parentLong };
|
||||
}
|
||||
|
||||
return (
|
||||
<MarketInfoTable
|
||||
data={{ short, long }}
|
||||
parentData={parentData}
|
||||
unformatted
|
||||
/>
|
||||
);
|
||||
return <MarketInfoTable data={{ short, long }} unformatted />;
|
||||
};
|
||||
|
||||
export const PriceMonitoringBoundsInfoPanel = ({
|
||||
market,
|
||||
triggerIndex,
|
||||
parentMarket,
|
||||
}: MarketInfoProps & {
|
||||
triggerIndex: number;
|
||||
}) => {
|
||||
@@ -583,35 +288,11 @@ export const PriceMonitoringBoundsInfoPanel = ({
|
||||
dataProvider: marketDataProvider,
|
||||
variables: { marketId: market.id },
|
||||
});
|
||||
|
||||
const { data: parentData } = useDataProvider({
|
||||
dataProvider: marketDataProvider,
|
||||
variables: { marketId: parentMarket?.id || '' },
|
||||
skip:
|
||||
!parentMarket ||
|
||||
!parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[
|
||||
triggerIndex
|
||||
],
|
||||
});
|
||||
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const parentQuoteUnit =
|
||||
parentMarket?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const isParentQuoteUnitEqual = quoteUnit === parentQuoteUnit;
|
||||
|
||||
const trigger =
|
||||
market.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
|
||||
const parentTrigger =
|
||||
parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
|
||||
const isParentTriggerEqual = isEqual(trigger, parentTrigger);
|
||||
|
||||
const bounds = data?.priceMonitoringBounds?.[triggerIndex];
|
||||
const parentBounds = parentData?.priceMonitoringBounds?.[triggerIndex];
|
||||
|
||||
const shouldShowParentData =
|
||||
isParentQuoteUnitEqual && isParentTriggerEqual && !!parentBounds;
|
||||
|
||||
if (!trigger) {
|
||||
console.error(
|
||||
`Could not find data for trigger ${triggerIndex} (market id: ${market.id})`
|
||||
@@ -638,14 +319,6 @@ export const PriceMonitoringBoundsInfoPanel = ({
|
||||
highestPrice: bounds.maxValidPrice,
|
||||
lowestPrice: bounds.minValidPrice,
|
||||
}}
|
||||
parentData={
|
||||
shouldShowParentData
|
||||
? {
|
||||
highestPrice: parentBounds.maxValidPrice,
|
||||
lowestPrice: parentBounds.minValidPrice,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
assetSymbol={quoteUnit}
|
||||
/>
|
||||
@@ -661,31 +334,18 @@ export const PriceMonitoringBoundsInfoPanel = ({
|
||||
|
||||
export const LiquidityMonitoringParametersInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => {
|
||||
const marketData = {
|
||||
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
|
||||
timeWindow:
|
||||
market.liquidityMonitoringParameters.targetStakeParameters.timeWindow,
|
||||
scalingFactor:
|
||||
market.liquidityMonitoringParameters.targetStakeParameters.scalingFactor,
|
||||
};
|
||||
|
||||
const parentMarketData = parentMarket
|
||||
? {
|
||||
triggeringRatio:
|
||||
parentMarket.liquidityMonitoringParameters.triggeringRatio,
|
||||
timeWindow:
|
||||
parentMarket.liquidityMonitoringParameters.targetStakeParameters
|
||||
.timeWindow,
|
||||
scalingFactor:
|
||||
parentMarket.liquidityMonitoringParameters.targetStakeParameters
|
||||
.scalingFactor,
|
||||
}
|
||||
: {};
|
||||
|
||||
return <MarketInfoTable data={marketData} parentData={parentMarketData} />;
|
||||
};
|
||||
}: MarketInfoProps) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
|
||||
timeWindow:
|
||||
market.liquidityMonitoringParameters.targetStakeParameters.timeWindow,
|
||||
scalingFactor:
|
||||
market.liquidityMonitoringParameters.targetStakeParameters
|
||||
.scalingFactor,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
|
||||
const assetDecimals =
|
||||
@@ -712,61 +372,16 @@ export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const LiquidityPriceRangeInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => {
|
||||
export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const parentQuoteUnit =
|
||||
parentMarket?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
|
||||
const liquidityPriceRange = formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
);
|
||||
const parentLiquidityPriceRange = parentMarket
|
||||
? formatNumberPercentage(
|
||||
new BigNumber(parentMarket.lpPriceRange).times(100)
|
||||
)
|
||||
: null;
|
||||
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: marketDataProvider,
|
||||
variables: { marketId: market.id },
|
||||
});
|
||||
|
||||
const { data: parentMarketData } = useDataProvider({
|
||||
dataProvider: marketDataProvider,
|
||||
variables: { marketId: parentMarket?.id || '' },
|
||||
skip: !parentMarket,
|
||||
});
|
||||
|
||||
let parentData;
|
||||
|
||||
if (parentMarket && parentMarketData && quoteUnit === parentQuoteUnit) {
|
||||
parentData = {
|
||||
liquidityPriceRange: `${parentLiquidityPriceRange} of mid price`,
|
||||
lowestPrice:
|
||||
parentMarketData?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(parentMarket.lpPriceRange)
|
||||
.times(parentMarketData.midPrice)
|
||||
.toString(),
|
||||
parentMarket.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
highestPrice:
|
||||
parentMarketData?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(parentMarket.lpPriceRange)
|
||||
.times(parentMarketData.midPrice)
|
||||
.toString(),
|
||||
parentMarket.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
@@ -799,7 +414,6 @@ export const LiquidityPriceRangeInfoPanel = ({
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
parentData={parentData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -808,12 +422,8 @@ export const LiquidityPriceRangeInfoPanel = ({
|
||||
export const OracleInfoPanel = ({
|
||||
market,
|
||||
type,
|
||||
parentMarket,
|
||||
}: MarketInfoProps & { type: 'settlementData' | 'termination' }) => {
|
||||
// If this is a successor market, this component will only receive parent market
|
||||
// data if the termination or settlement data is different from the parent.
|
||||
const product = market.tradableInstrument.instrument.product;
|
||||
const parentProduct = parentMarket?.tradableInstrument?.instrument?.product;
|
||||
const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment();
|
||||
const { data } = useOracleProofs(ORACLE_PROOFS_URL);
|
||||
|
||||
@@ -822,33 +432,12 @@ export const OracleInfoPanel = ({
|
||||
? product.dataSourceSpecForSettlementData.id
|
||||
: product.dataSourceSpecForTradingTermination.id;
|
||||
|
||||
const parentDataSourceSpecId =
|
||||
type === 'settlementData'
|
||||
? parentProduct?.dataSourceSpecForSettlementData?.id
|
||||
: parentProduct?.dataSourceSpecForTradingTermination?.id;
|
||||
|
||||
const dataSourceSpec = (
|
||||
type === 'settlementData'
|
||||
? product.dataSourceSpecForSettlementData.data
|
||||
: product.dataSourceSpecForTradingTermination.data
|
||||
) as DataSourceDefinition;
|
||||
|
||||
const parentDataSourceSpec =
|
||||
type === 'settlementData'
|
||||
? parentProduct?.dataSourceSpecForSettlementData?.data
|
||||
: (parentProduct?.dataSourceSpecForTradingTermination
|
||||
?.data as DataSourceDefinition);
|
||||
|
||||
const isParentDataSourceSpecEqual =
|
||||
parentDataSourceSpec !== undefined &&
|
||||
dataSourceSpec === parentDataSourceSpec;
|
||||
const isParentDataSourceSpecIdEqual =
|
||||
parentDataSourceSpecId !== undefined &&
|
||||
dataSourceSpecId === parentDataSourceSpecId;
|
||||
|
||||
// We'll only provide successor parent data (if it differs) to the
|
||||
// DataSourceProof component. Having an old external link struck through
|
||||
// is unlikely to be useful.
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<DataSourceProof
|
||||
@@ -857,14 +446,7 @@ export const OracleInfoPanel = ({
|
||||
providers={data}
|
||||
type={type}
|
||||
dataSourceSpecId={dataSourceSpecId}
|
||||
parentData={
|
||||
isParentDataSourceSpecEqual ? undefined : parentDataSourceSpec
|
||||
}
|
||||
parentDataSourceSpecId={
|
||||
isParentDataSourceSpecIdEqual ? undefined : parentDataSourceSpecId
|
||||
}
|
||||
/>
|
||||
|
||||
<ExternalLink
|
||||
data-testid="oracle-spec-links"
|
||||
href={`${VEGA_EXPLORER_URL}/oracles/${
|
||||
@@ -886,28 +468,14 @@ export const DataSourceProof = ({
|
||||
providers,
|
||||
type,
|
||||
dataSourceSpecId,
|
||||
parentData,
|
||||
parentDataSourceSpecId,
|
||||
}: {
|
||||
data: DataSourceDefinition;
|
||||
providers: Provider[] | undefined;
|
||||
type: 'settlementData' | 'termination';
|
||||
dataSourceSpecId: string;
|
||||
parentData?: DataSourceDefinition;
|
||||
parentDataSourceSpecId?: string;
|
||||
}) => {
|
||||
// If this is a successor market, we'll only pass parent data to child
|
||||
// components for comparison if the data differs from the parent market.
|
||||
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
|
||||
const signers = data.sourceType.sourceType.signers || [];
|
||||
let parentSigners: Signer[];
|
||||
|
||||
if (
|
||||
parentData &&
|
||||
parentData.sourceType.__typename === 'DataSourceDefinitionExternal'
|
||||
) {
|
||||
parentSigners = parentData.sourceType.sourceType?.signers || [];
|
||||
}
|
||||
|
||||
if (!providers?.length) {
|
||||
return <NoOracleProof type={type} />;
|
||||
@@ -916,14 +484,7 @@ export const DataSourceProof = ({
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{signers.map(({ signer }, i) => {
|
||||
const parentSigner = parentSigners?.find(
|
||||
({ signer: ParentSigner }) =>
|
||||
ParentSigner.__typename === signer.__typename
|
||||
)?.signer;
|
||||
|
||||
const isParentSignerEqual = isEqual(signer, parentSigner);
|
||||
|
||||
return isParentSignerEqual ? (
|
||||
return (
|
||||
<OracleLink
|
||||
key={i}
|
||||
providers={providers}
|
||||
@@ -931,16 +492,6 @@ export const DataSourceProof = ({
|
||||
type={type}
|
||||
dataSourceSpecId={dataSourceSpecId}
|
||||
/>
|
||||
) : (
|
||||
<OracleLink
|
||||
key={i}
|
||||
providers={providers}
|
||||
signer={signer}
|
||||
type={type}
|
||||
dataSourceSpecId={dataSourceSpecId}
|
||||
parentSigner={parentSigner}
|
||||
parentDataSourceSpecId={parentDataSourceSpecId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -974,8 +525,18 @@ export const DataSourceProof = ({
|
||||
return <div>{t('Invalid data source')}</div>;
|
||||
};
|
||||
|
||||
const getSignerProviders = (signer: SignerKind, providers: Provider[]) =>
|
||||
providers.filter((p) => {
|
||||
const OracleLink = ({
|
||||
providers,
|
||||
signer,
|
||||
type,
|
||||
dataSourceSpecId,
|
||||
}: {
|
||||
providers: Provider[];
|
||||
signer: SignerKind;
|
||||
type: 'settlementData' | 'termination';
|
||||
dataSourceSpecId: string;
|
||||
}) => {
|
||||
const signerProviders = providers.filter((p) => {
|
||||
if (signer.__typename === 'PubKey') {
|
||||
if (
|
||||
p.oracle.type === 'public_key' &&
|
||||
@@ -997,62 +558,19 @@ const getSignerProviders = (signer: SignerKind, providers: Provider[]) =>
|
||||
return false;
|
||||
});
|
||||
|
||||
const OracleLink = ({
|
||||
providers,
|
||||
signer,
|
||||
type,
|
||||
dataSourceSpecId,
|
||||
parentSigner,
|
||||
parentDataSourceSpecId,
|
||||
}: {
|
||||
providers: Provider[];
|
||||
signer: SignerKind;
|
||||
type: 'settlementData' | 'termination';
|
||||
dataSourceSpecId: string;
|
||||
parentSigner?: SignerKind;
|
||||
parentDataSourceSpecId?: string;
|
||||
}) => {
|
||||
// If this is a successor market, the parent market data will only have been passed
|
||||
// in if it differs from the current data.
|
||||
const signerProviders = getSignerProviders(signer, providers);
|
||||
const parentSignerProviders = parentSigner
|
||||
? getSignerProviders(parentSigner, providers)
|
||||
: [];
|
||||
|
||||
if (!signerProviders.length) {
|
||||
return <NoOracleProof type={type} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
{signerProviders.map((provider) => {
|
||||
// Making the assumption here that if the provider name is the same,
|
||||
// that it is the same provider that the parent market used.
|
||||
const parentProvider = parentSignerProviders.find(
|
||||
(p) => p.name === provider.name
|
||||
);
|
||||
|
||||
const isParentProviderEqual =
|
||||
parentProvider !== undefined && isEqual(provider, parentProvider);
|
||||
|
||||
// We only want to pass the parent data to the child component if the
|
||||
// data differs from the parent market.
|
||||
return isParentProviderEqual ? (
|
||||
<OracleProfile
|
||||
key={dataSourceSpecId}
|
||||
provider={provider}
|
||||
dataSourceSpecId={dataSourceSpecId}
|
||||
/>
|
||||
) : (
|
||||
<OracleProfile
|
||||
key={dataSourceSpecId}
|
||||
provider={provider}
|
||||
dataSourceSpecId={dataSourceSpecId}
|
||||
parentProvider={parentProvider}
|
||||
parentDataSourceSpecId={parentDataSourceSpecId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{signerProviders.map((provider) => (
|
||||
<OracleProfile
|
||||
key={dataSourceSpecId}
|
||||
provider={provider}
|
||||
dataSourceSpecId={dataSourceSpecId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1075,18 +593,13 @@ const NoOracleProof = ({
|
||||
const OracleProfile = (props: {
|
||||
provider: Provider;
|
||||
dataSourceSpecId: string;
|
||||
parentProvider?: Provider;
|
||||
parentDataSourceSpecId?: string;
|
||||
}) => {
|
||||
// If this is a successor market, the parent market data will only have been passed
|
||||
// in if it differs from the current data.
|
||||
const [open, onChange] = useState(false);
|
||||
return (
|
||||
<div key={props.provider.name}>
|
||||
<OracleBasicProfile
|
||||
provider={props.provider}
|
||||
onClick={() => onChange(!open)}
|
||||
parentProvider={props.parentProvider}
|
||||
/>
|
||||
<OracleDialog {...props} open={open} onChange={onChange} />
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
ExternalLink,
|
||||
Icon,
|
||||
Intent,
|
||||
Lozenge,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
@@ -60,12 +59,10 @@ export const OracleBasicProfile = ({
|
||||
provider,
|
||||
onClick,
|
||||
markets: oracleMarkets,
|
||||
parentProvider,
|
||||
}: {
|
||||
provider: Provider;
|
||||
markets?: OracleMarketSpecFieldsFragment[] | undefined;
|
||||
onClick?: (value?: boolean) => void;
|
||||
parentProvider?: Provider;
|
||||
}) => {
|
||||
const { icon, message, intent } = getVerifiedStatusIcon(provider);
|
||||
|
||||
@@ -81,14 +78,8 @@ export const OracleBasicProfile = ({
|
||||
icon: getLinkIcon(proof.type),
|
||||
}));
|
||||
|
||||
// If this is a successor market and there's a different parent provider,
|
||||
// we'll just show that there's been a change, rather than add old data
|
||||
// in alongside the new provider.
|
||||
return (
|
||||
<>
|
||||
{parentProvider && (
|
||||
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
|
||||
)}
|
||||
<span className="flex gap-1">
|
||||
{provider.url && (
|
||||
<span className="flex align-items-bottom text-md gap-1">
|
||||
|
||||
@@ -11,27 +11,16 @@ export const OracleDialog = ({
|
||||
dataSourceSpecId,
|
||||
open,
|
||||
onChange,
|
||||
parentProvider,
|
||||
}: {
|
||||
dataSourceSpecId: string;
|
||||
provider: Provider;
|
||||
open: boolean;
|
||||
onChange?: (isOpen: boolean) => void;
|
||||
parentProvider?: Provider;
|
||||
}) => {
|
||||
// If this is a successor market, the parent market data will only have been passed
|
||||
// in if it differs from the current data. We'll pass this on to the title component
|
||||
// to show a change, but the full profile showing changes is unwieldy - it's enough
|
||||
// to know from the title that the oracle has changed.
|
||||
const oracleMarkets = useOracleMarkets(provider);
|
||||
return (
|
||||
<Dialog
|
||||
title={
|
||||
<OracleProfileTitle
|
||||
provider={provider}
|
||||
parentProvider={parentProvider}
|
||||
/>
|
||||
}
|
||||
title={<OracleProfileTitle provider={provider} />}
|
||||
aria-labelledby="oracle-proof-dialog"
|
||||
open={open}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
ExternalLink,
|
||||
Icon,
|
||||
Intent,
|
||||
Lozenge,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
@@ -19,30 +18,15 @@ import type { OracleMarketSpecFieldsFragment } from '../../__generated__/OracleM
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const OracleProfileTitle = ({
|
||||
provider,
|
||||
parentProvider,
|
||||
}: {
|
||||
provider: Provider;
|
||||
parentProvider?: Provider;
|
||||
}) => {
|
||||
// If this is a successor market, the parent provider will only have been passed
|
||||
// in if it differs from the current provider. If it is different, we'll just
|
||||
// show the change in name, not icons and proofs.
|
||||
export const OracleProfileTitle = ({ provider }: { provider: Provider }) => {
|
||||
const { icon, intent } = getVerifiedStatusIcon(provider);
|
||||
const verifiedProofs = provider.proofs.filter(
|
||||
(proof) => proof.available === true
|
||||
);
|
||||
return (
|
||||
<span className="flex gap-1">
|
||||
{parentProvider && (
|
||||
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
|
||||
)}
|
||||
{provider.url && (
|
||||
<span>
|
||||
{parentProvider && parentProvider.name && (
|
||||
<span className="line-through">{parentProvider.name}</span>
|
||||
)}
|
||||
<span className="pr-1">{provider.name}</span>
|
||||
<span className="dark:text-vega-light-300 text-vega-dark-300">
|
||||
({verifiedProofs.length})
|
||||
|
||||
@@ -150,9 +150,3 @@ query StopOrders($partyId: ID!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query StopOrderById($stopOrderId: ID!) {
|
||||
stopOrder(id: $stopOrderId) {
|
||||
...StopOrderFields
|
||||
}
|
||||
}
|
||||
|
||||
+2
-44
@@ -32,7 +32,7 @@ export type OrdersUpdateSubscriptionVariables = Types.Exact<{
|
||||
|
||||
export type OrdersUpdateSubscription = { __typename?: 'Subscription', orders?: Array<{ __typename?: 'OrderUpdate', id: string, marketId: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, liquidityProvisionId?: string | null, peggedOrder?: { __typename: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, icebergOrder?: { __typename: 'IcebergOrder', peakSize: string, minimumVisibleSize: string, reservedRemaining: string } | null }> | null };
|
||||
|
||||
export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null };
|
||||
export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null};
|
||||
|
||||
export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } };
|
||||
|
||||
@@ -43,13 +43,6 @@ export type StopOrdersQueryVariables = Types.Exact<{
|
||||
|
||||
export type StopOrdersQuery = { __typename?: 'Query', stopOrders?: { __typename?: 'StopOrderConnection', edges?: Array<{ __typename?: 'StopOrderEdge', node?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null }> | null } | null };
|
||||
|
||||
export type StopOrderByIdQueryVariables = Types.Exact<{
|
||||
stopOrderId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type StopOrderByIdQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null };
|
||||
|
||||
export const OrderFieldsFragmentDoc = gql`
|
||||
fragment OrderFields on Order {
|
||||
id
|
||||
@@ -316,39 +309,4 @@ export function useStopOrdersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
|
||||
}
|
||||
export type StopOrdersQueryHookResult = ReturnType<typeof useStopOrdersQuery>;
|
||||
export type StopOrdersLazyQueryHookResult = ReturnType<typeof useStopOrdersLazyQuery>;
|
||||
export type StopOrdersQueryResult = Apollo.QueryResult<StopOrdersQuery, StopOrdersQueryVariables>;
|
||||
export const StopOrderByIdDocument = gql`
|
||||
query StopOrderById($stopOrderId: ID!) {
|
||||
stopOrder(id: $stopOrderId) {
|
||||
...StopOrderFields
|
||||
}
|
||||
}
|
||||
${StopOrderFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useStopOrderByIdQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useStopOrderByIdQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useStopOrderByIdQuery` 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 } = useStopOrderByIdQuery({
|
||||
* variables: {
|
||||
* stopOrderId: // value for 'stopOrderId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useStopOrderByIdQuery(baseOptions: Apollo.QueryHookOptions<StopOrderByIdQuery, StopOrderByIdQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<StopOrderByIdQuery, StopOrderByIdQueryVariables>(StopOrderByIdDocument, options);
|
||||
}
|
||||
export function useStopOrderByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<StopOrderByIdQuery, StopOrderByIdQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<StopOrderByIdQuery, StopOrderByIdQueryVariables>(StopOrderByIdDocument, options);
|
||||
}
|
||||
export type StopOrderByIdQueryHookResult = ReturnType<typeof useStopOrderByIdQuery>;
|
||||
export type StopOrderByIdLazyQueryHookResult = ReturnType<typeof useStopOrderByIdLazyQuery>;
|
||||
export type StopOrderByIdQueryResult = Apollo.QueryResult<StopOrderByIdQuery, StopOrderByIdQueryVariables>;
|
||||
export type StopOrdersQueryResult = Apollo.QueryResult<StopOrdersQuery, StopOrdersQueryVariables>;
|
||||
@@ -114,11 +114,9 @@ describe('OrderListTable', () => {
|
||||
it('should apply correct formatting applied for an iceberg order', async () => {
|
||||
const icebergOrder = {
|
||||
...limitOrder,
|
||||
size: '100',
|
||||
remaining: '50',
|
||||
icebergOrder: {
|
||||
__typename: 'IcebergOrder',
|
||||
minimumVisibleSize: '1',
|
||||
minimumVisibleSize: '100',
|
||||
peakSize: '50',
|
||||
reservedRemaining: '50',
|
||||
} as OrderFieldsFragment['icebergOrder'],
|
||||
@@ -129,8 +127,8 @@ describe('OrderListTable', () => {
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues: string[] = [
|
||||
icebergOrder.market?.tradableInstrument.instrument.code || '',
|
||||
'0.00',
|
||||
'+1.00',
|
||||
'0.05',
|
||||
'0.10',
|
||||
Schema.OrderTypeMapping[
|
||||
icebergOrder.type || Schema.OrderType.TYPE_LIMIT
|
||||
] + ' (Iceberg)',
|
||||
|
||||
@@ -86,13 +86,21 @@ export const OrderListTable = memo<
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Order>) => {
|
||||
if (data?.icebergOrder) {
|
||||
return data?.size && data.market
|
||||
? BigInt(data.size) -
|
||||
BigInt(data.remaining) -
|
||||
BigInt(data.icebergOrder.reservedRemaining)
|
||||
? toBigNum(
|
||||
(
|
||||
BigInt(data.size) -
|
||||
BigInt(data.remaining) -
|
||||
BigInt(data.icebergOrder.reservedRemaining)
|
||||
).toString(),
|
||||
data.market.positionDecimalPlaces ?? 0
|
||||
).toNumber()
|
||||
: undefined;
|
||||
}
|
||||
return data?.size && data.market
|
||||
? BigInt(data.size) - BigInt(data.remaining)
|
||||
? toBigNum(
|
||||
(BigInt(data.size) - BigInt(data.remaining)).toString(),
|
||||
data.market.positionDecimalPlaces ?? 0
|
||||
).toNumber()
|
||||
: undefined;
|
||||
},
|
||||
valueFormatter: ({
|
||||
@@ -106,8 +114,8 @@ export const OrderListTable = memo<
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
value,
|
||||
data.market.positionDecimalPlaces ?? 0
|
||||
(BigInt(data.size) - BigInt(data.remaining)).toString(),
|
||||
data.market.positionDecimalPlaces
|
||||
);
|
||||
},
|
||||
minWidth: 50,
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
getDateTimeFormat,
|
||||
isNumeric,
|
||||
toBigNum,
|
||||
formatTrigger,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@@ -56,8 +55,29 @@ export const StopOrdersTable = memo<
|
||||
sortable: false,
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string =>
|
||||
data ? formatTrigger(data, data.market.decimalPlaces) : '',
|
||||
value,
|
||||
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string => {
|
||||
if (data && value?.__typename === 'StopOrderPrice') {
|
||||
return `${t('Mark')} ${
|
||||
data?.triggerDirection ===
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
? '<'
|
||||
: '>'
|
||||
} ${addDecimalsFormatNumber(
|
||||
value.price,
|
||||
data.market.decimalPlaces
|
||||
)}`;
|
||||
}
|
||||
if (data && value?.__typename === 'StopOrderTrailingPercentOffset') {
|
||||
return `${t('Mark')} ${
|
||||
data?.triggerDirection ===
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
? '+'
|
||||
: '-'
|
||||
}${(Number(value.trailingPercentOffset) * 100).toFixed(1)}%`;
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './__generated__/OrdersSubscription';
|
||||
export * from './use-has-amendable-order';
|
||||
export * from './use-order-update';
|
||||
export * from './use-order-store';
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
getDefaultOrder,
|
||||
STORAGE_KEY,
|
||||
useOrder,
|
||||
useCreateOrderStore,
|
||||
} from './use-order-store';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
|
||||
jest.mock('zustand');
|
||||
|
||||
describe('useCreateOrderStore', () => {
|
||||
const setup = () => {
|
||||
const { result } = renderHook(() => useCreateOrderStore());
|
||||
return renderHook(() => result.current());
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('has a empty default state', async () => {
|
||||
const { result } = setup();
|
||||
expect(result.current).toEqual({
|
||||
orders: {},
|
||||
update: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('can update', () => {
|
||||
const marketId = 'persisted-market-id';
|
||||
const expectedOrder = {
|
||||
...getDefaultOrder(marketId),
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
persist: true,
|
||||
};
|
||||
const { result } = setup();
|
||||
act(() => {
|
||||
result.current.update(marketId, { type: OrderType.TYPE_LIMIT });
|
||||
});
|
||||
// order should be stored in memory
|
||||
expect(result.current.orders).toEqual({
|
||||
[marketId]: expectedOrder,
|
||||
});
|
||||
// order SHOULD also be in localStorage
|
||||
expect(JSON.parse(localStorage.getItem(STORAGE_KEY) || '')).toEqual({
|
||||
state: {
|
||||
orders: {
|
||||
[marketId]: expectedOrder,
|
||||
},
|
||||
},
|
||||
version: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('can update without persisting', () => {
|
||||
const marketId = 'non-persisted-market-id';
|
||||
const expectedOrder = {
|
||||
...getDefaultOrder(marketId),
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
persist: false,
|
||||
};
|
||||
const { result } = setup();
|
||||
act(() => {
|
||||
result.current.update(marketId, { type: OrderType.TYPE_LIMIT }, false);
|
||||
});
|
||||
// order should be stored in memory
|
||||
expect(result.current.orders).toEqual({
|
||||
[marketId]: expectedOrder,
|
||||
});
|
||||
// order should NOT be in localStorage
|
||||
expect(JSON.parse(localStorage.getItem(STORAGE_KEY) || '')).toEqual({
|
||||
state: {
|
||||
orders: {},
|
||||
},
|
||||
version: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useOrder', () => {
|
||||
const setup = (marketId: string) => {
|
||||
return renderHook(() => useOrder(marketId));
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('creates a new order if it doesnt exist which is only persisted after editing', () => {
|
||||
const marketId = 'market-id';
|
||||
const expectedOrder = {
|
||||
...getDefaultOrder(marketId),
|
||||
persist: false,
|
||||
};
|
||||
const { result } = setup(marketId);
|
||||
expect(result.current).toEqual([expectedOrder, expect.any(Function)]);
|
||||
});
|
||||
|
||||
it('only persists an order if edited', () => {
|
||||
const marketId = 'market-id';
|
||||
const expectedOrder = {
|
||||
...getDefaultOrder(marketId),
|
||||
persist: false,
|
||||
};
|
||||
const { result } = setup(marketId);
|
||||
expect(result.current[0]).toMatchObject({
|
||||
price: expectedOrder.price,
|
||||
persist: false,
|
||||
});
|
||||
|
||||
const update = { price: '500' };
|
||||
act(() => {
|
||||
result.current[1](update);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toMatchObject({
|
||||
...update,
|
||||
persist: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { OrderTimeInForce, Side } from '@vegaprotocol/types';
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type { StateCreator, UseBoundStore, Mutate, StoreApi } from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
|
||||
export type OrderObj = {
|
||||
marketId: string;
|
||||
type: OrderType;
|
||||
side: Side;
|
||||
size: string;
|
||||
timeInForce: OrderTimeInForce;
|
||||
price?: string;
|
||||
expiresAt?: string | undefined;
|
||||
persist: boolean; // key used to determine if order should be kept in localStorage
|
||||
postOnly?: boolean;
|
||||
reduceOnly?: boolean;
|
||||
iceberg?: boolean;
|
||||
icebergOpts?: {
|
||||
peakSize: string;
|
||||
minimumVisibleSize: string;
|
||||
};
|
||||
};
|
||||
|
||||
type OrderMap = { [marketId: string]: OrderObj | undefined };
|
||||
|
||||
type UpdateOrder = (
|
||||
marketId: string,
|
||||
order: Partial<OrderObj>,
|
||||
persist?: boolean
|
||||
) => void;
|
||||
|
||||
interface Store {
|
||||
orders: OrderMap;
|
||||
update: UpdateOrder;
|
||||
}
|
||||
|
||||
export const STORAGE_KEY = 'vega_order_store';
|
||||
|
||||
const orderStateCreator: StateCreator<Store> = (set) => ({
|
||||
orders: {},
|
||||
update: (marketId, order, persist = true) => {
|
||||
set((state) => {
|
||||
const curr = state.orders[marketId];
|
||||
const defaultOrder = getDefaultOrder(marketId);
|
||||
|
||||
return {
|
||||
orders: {
|
||||
...state.orders,
|
||||
[marketId]: {
|
||||
...defaultOrder,
|
||||
...curr,
|
||||
...order,
|
||||
persist,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let store: UseBoundStore<Mutate<StoreApi<Store>, []>> | null = null;
|
||||
const getOrderStore = () => {
|
||||
if (!store) {
|
||||
store = create<Store>()(
|
||||
persist(subscribeWithSelector(orderStateCreator), {
|
||||
name: STORAGE_KEY,
|
||||
partialize: (state) => {
|
||||
// only store the order in localStorage if user has edited, this avoids
|
||||
// bloating localStorage if a user just visits the page but does not
|
||||
// edit the ticket
|
||||
const partializedOrders: OrderMap = {};
|
||||
for (const o in state.orders) {
|
||||
const order = state.orders[o];
|
||||
if (order && order.persist) {
|
||||
partializedOrders[order.marketId] = order;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
orders: partializedOrders,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return store as UseBoundStore<Mutate<StoreApi<Store>, []>>;
|
||||
};
|
||||
|
||||
export const useCreateOrderStore = () => {
|
||||
const useOrderStoreRef = useRef(getOrderStore());
|
||||
return useOrderStoreRef.current;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves an order from the store for a market and
|
||||
* creates one if it doesn't already exist
|
||||
*/
|
||||
export const useOrder = (marketId: string) => {
|
||||
const useOrderStoreRef = useCreateOrderStore();
|
||||
const [order, _update] = useOrderStoreRef((store) => {
|
||||
return [store.orders[marketId], store.update];
|
||||
});
|
||||
|
||||
const update = useCallback(
|
||||
(o: Partial<OrderObj>, persist = true) => {
|
||||
_update(marketId, o, persist);
|
||||
},
|
||||
[marketId, _update]
|
||||
);
|
||||
|
||||
// add new order to store if it doesn't exist, but don't
|
||||
// persist until user has edited
|
||||
useEffect(() => {
|
||||
if (!order) {
|
||||
update(
|
||||
getDefaultOrder(marketId),
|
||||
false // don't persist the order
|
||||
);
|
||||
}
|
||||
}, [order, marketId, update]);
|
||||
|
||||
return [order, update] as const; // make result a tuple
|
||||
};
|
||||
|
||||
export const getDefaultOrder = (marketId: string): OrderObj => ({
|
||||
marketId,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
side: Side.SIDE_BUY,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
size: '0',
|
||||
price: '0',
|
||||
expiresAt: undefined,
|
||||
persist: false,
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
});
|
||||
@@ -113,7 +113,6 @@ const marketsData = [
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'AAVEDAI Monthly (30 Jun 2022)',
|
||||
code: 'AAVEDAI.MF21',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
symbol: 'tDAI',
|
||||
@@ -143,7 +142,6 @@ const marketsData = [
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||
code: 'UNIDAI.MF21',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
symbol: 'tDAI',
|
||||
@@ -185,7 +183,7 @@ describe('getMetrics && rejoinPositionData', () => {
|
||||
expect(metrics[0].marketId).toEqual(
|
||||
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
|
||||
);
|
||||
expect(metrics[0].marketName).toEqual('AAVEDAI.MF21');
|
||||
expect(metrics[0].marketName).toEqual('AAVEDAI Monthly (30 Jun 2022)');
|
||||
expect(metrics[0].marketTradingMode).toEqual(
|
||||
'TRADING_MODE_MONITORING_AUCTION'
|
||||
);
|
||||
@@ -210,7 +208,7 @@ describe('getMetrics && rejoinPositionData', () => {
|
||||
expect(metrics[1].marketId).toEqual(
|
||||
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
|
||||
);
|
||||
expect(metrics[1].marketName).toEqual('UNIDAI.MF21');
|
||||
expect(metrics[1].marketName).toEqual('UNIDAI Monthly (30 Jun 2022)');
|
||||
expect(metrics[1].marketTradingMode).toEqual('TRADING_MODE_CONTINUOUS');
|
||||
expect(metrics[1].notional).toEqual('86976200');
|
||||
expect(metrics[1].openVolume).toEqual('-100');
|
||||
|
||||
@@ -121,7 +121,7 @@ export const getMetrics = (
|
||||
marginAccountBalance: marginAccount?.balance ?? '0',
|
||||
marketDecimalPlaces,
|
||||
marketId: market.id,
|
||||
marketName: market.tradableInstrument.instrument.code,
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
marketTradingMode: market.tradingMode,
|
||||
markPrice: marketData ? marketData.markPrice : undefined,
|
||||
notional: notional
|
||||
|
||||
@@ -41,329 +41,285 @@ const singleRow: Position = {
|
||||
|
||||
const singleRowData = [singleRow];
|
||||
|
||||
describe('Positions', () => {
|
||||
it('should render successfully', async () => {
|
||||
await act(async () => {
|
||||
const { baseElement } = render(
|
||||
<PositionsTable rowData={[]} isReadOnly={false} />
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('render correct columns', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
|
||||
});
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(12);
|
||||
expect(
|
||||
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
|
||||
).toEqual([
|
||||
'Market',
|
||||
'Notional',
|
||||
'Open volume',
|
||||
'Mark price',
|
||||
'Liquidation price',
|
||||
'Settlement asset',
|
||||
'Entry price',
|
||||
'Leverage',
|
||||
'Margin allocated',
|
||||
'Realised PNL',
|
||||
'Unrealised PNL',
|
||||
'Updated',
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders market name', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Does not fail if the market name does not match the split pattern', async () => {
|
||||
const breakingMarketName = 'OP/USD AUG-SEP22 - Incentive';
|
||||
const row = [
|
||||
Object.assign({}, singleRow, { marketName: breakingMarketName }),
|
||||
];
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={row} isReadOnly={false} />);
|
||||
});
|
||||
|
||||
expect(screen.getByText(breakingMarketName)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('add color and sign to amount, displays positive notional value', async () => {
|
||||
let result: RenderResult;
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<PositionsTable rowData={singleRowData} isReadOnly={false} />
|
||||
);
|
||||
});
|
||||
let cells = screen.getAllByRole('gridcell');
|
||||
|
||||
expect(cells[2].classList.contains('text-market-green-600')).toBeTruthy();
|
||||
expect(cells[2].classList.contains('text-market-red')).toBeFalsy();
|
||||
expect(cells[2].textContent).toEqual('+100');
|
||||
expect(cells[1].textContent).toEqual('1,230.0');
|
||||
await act(async () => {
|
||||
result.rerender(
|
||||
<PositionsTable
|
||||
rowData={[{ ...singleRow, openVolume: '-100' }]}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[2].classList.contains('text-market-green-600')).toBeFalsy();
|
||||
expect(cells[2].classList.contains('text-market-red')).toBeTruthy();
|
||||
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
|
||||
expect(cells[1].textContent).toEqual('1,230.0');
|
||||
});
|
||||
|
||||
it('displays mark price', async () => {
|
||||
let result: RenderResult;
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<PositionsTable rowData={singleRowData} isReadOnly={false} />
|
||||
);
|
||||
});
|
||||
|
||||
let cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[3].textContent).toEqual('12.3');
|
||||
|
||||
await act(async () => {
|
||||
result.rerender(
|
||||
<PositionsTable
|
||||
rowData={[
|
||||
{
|
||||
...singleRow,
|
||||
marketTradingMode:
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
},
|
||||
]}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[3].textContent).toEqual('-');
|
||||
});
|
||||
|
||||
it('displays liquidation price', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[4].textContent).toEqual('liquidation price');
|
||||
});
|
||||
|
||||
it('displays leverage', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[7].textContent).toEqual('1.1');
|
||||
});
|
||||
|
||||
it('displays allocated margin', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const cell = cells[8];
|
||||
expect(cell.textContent).toEqual('123,456.00');
|
||||
});
|
||||
|
||||
it('displays realised and unrealised PNL', async () => {
|
||||
// pnl cells should be rendered with asset dps
|
||||
const expectedRealised = addDecimalsFormatNumber(
|
||||
singleRow.realisedPNL,
|
||||
singleRow.decimals
|
||||
it('should render successfully', async () => {
|
||||
await act(async () => {
|
||||
const { baseElement } = render(
|
||||
<PositionsTable rowData={[]} isReadOnly={false} />
|
||||
);
|
||||
const expectedUnrealised = addDecimalsFormatNumber(
|
||||
singleRow.unrealisedPNL,
|
||||
singleRow.decimals
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('render correct columns', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
|
||||
});
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(12);
|
||||
expect(
|
||||
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
|
||||
).toEqual([
|
||||
'Market',
|
||||
'Notional',
|
||||
'Open volume',
|
||||
'Mark price',
|
||||
'Liquidation price',
|
||||
'Settlement asset',
|
||||
'Entry price',
|
||||
'Leverage',
|
||||
'Margin allocated',
|
||||
'Realised PNL',
|
||||
'Unrealised PNL',
|
||||
'Updated',
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders market name', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Does not fail if the market name does not match the split pattern', async () => {
|
||||
const breakingMarketName = 'OP/USD AUG-SEP22 - Incentive';
|
||||
const row = [
|
||||
Object.assign({}, singleRow, { marketName: breakingMarketName }),
|
||||
];
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={row} isReadOnly={false} />);
|
||||
});
|
||||
|
||||
expect(screen.getByText(breakingMarketName)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('add color and sign to amount, displays positive notional value', async () => {
|
||||
let result: RenderResult;
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<PositionsTable rowData={singleRowData} isReadOnly={false} />
|
||||
);
|
||||
});
|
||||
let cells = screen.getAllByRole('gridcell');
|
||||
|
||||
expect(cells[2].classList.contains('text-market-green-600')).toBeTruthy();
|
||||
expect(cells[2].classList.contains('text-market-red')).toBeFalsy();
|
||||
expect(cells[2].textContent).toEqual('+100');
|
||||
expect(cells[1].textContent).toEqual('1,230.0');
|
||||
await act(async () => {
|
||||
result.rerender(
|
||||
<PositionsTable
|
||||
rowData={[{ ...singleRow, openVolume: '-100' }]}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[2].classList.contains('text-market-green-600')).toBeFalsy();
|
||||
expect(cells[2].classList.contains('text-market-red')).toBeTruthy();
|
||||
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
|
||||
expect(cells[1].textContent).toEqual('1,230.0');
|
||||
});
|
||||
|
||||
it('displays mark price', async () => {
|
||||
let result: RenderResult;
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<PositionsTable rowData={singleRowData} isReadOnly={false} />
|
||||
);
|
||||
});
|
||||
|
||||
let cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[3].textContent).toEqual('12.3');
|
||||
|
||||
await act(async () => {
|
||||
result.rerender(
|
||||
<PositionsTable
|
||||
rowData={[
|
||||
{
|
||||
...singleRow,
|
||||
marketTradingMode:
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
},
|
||||
]}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[3].textContent).toEqual('-');
|
||||
});
|
||||
|
||||
it('displays liquidation price', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[4].textContent).toEqual('liquidation price');
|
||||
});
|
||||
|
||||
it('displays leverage', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[7].textContent).toEqual('1.1');
|
||||
});
|
||||
|
||||
it('displays allocated margin', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const cell = cells[8];
|
||||
expect(cell.textContent).toEqual('123,456.00');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[9].textContent).toEqual(expectedRealised);
|
||||
expect(cells[10].textContent).toEqual(expectedUnrealised);
|
||||
});
|
||||
|
||||
it('displays close button', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<PositionsTable
|
||||
rowData={singleRowData}
|
||||
pubKey={singleRowData[0].partyId}
|
||||
onClose={() => {
|
||||
return;
|
||||
}}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[12].textContent).toEqual('Close');
|
||||
});
|
||||
|
||||
it('do not display close button if openVolume is zero', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<PositionsTable
|
||||
rowData={[{ ...singleRow, openVolume: '0' }]}
|
||||
onClose={() => {
|
||||
return;
|
||||
}}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[12].textContent).toEqual('');
|
||||
});
|
||||
|
||||
describe('PNLCell', () => {
|
||||
it('displays realised and unrealised PNL', async () => {
|
||||
// pnl cells should be rendered with asset dps
|
||||
const expectedRealised = addDecimalsFormatNumber(
|
||||
singleRow.realisedPNL,
|
||||
singleRow.decimals
|
||||
);
|
||||
const expectedUnrealised = addDecimalsFormatNumber(
|
||||
singleRow.unrealisedPNL,
|
||||
singleRow.decimals
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[9].textContent).toEqual(expectedRealised);
|
||||
expect(cells[10].textContent).toEqual(expectedUnrealised);
|
||||
});
|
||||
|
||||
it('displays close button', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<PositionsTable
|
||||
rowData={singleRowData}
|
||||
pubKey={singleRowData[0].partyId}
|
||||
onClose={() => {
|
||||
return;
|
||||
}}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[12].textContent).toEqual('Close');
|
||||
});
|
||||
|
||||
it('do not display close button if openVolume is zero', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<PositionsTable
|
||||
rowData={[{ ...singleRow, openVolume: '0' }]}
|
||||
onClose={() => {
|
||||
return;
|
||||
}}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[12].textContent).toEqual('');
|
||||
});
|
||||
|
||||
describe('PNLCell', () => {
|
||||
const props = {
|
||||
data: undefined,
|
||||
valueFormatted: '100',
|
||||
};
|
||||
it('renders a dash if no data', () => {
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders value if no loss socialisation has occurred', () => {
|
||||
const props = {
|
||||
data: undefined,
|
||||
data: {
|
||||
...singleRow,
|
||||
lossSocialisationAmount: '0',
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
it('renders a dash if no data', () => {
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders value if no loss socialisation has occurred', () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
lossSocialisationAmount: '0',
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders value with warning tooltip if loss socialisation occurred', async () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
lossSocializationAmount: '500',
|
||||
decimals: 2,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
const content = screen.getByText(props.valueFormatted);
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
|
||||
await userEvent.hover(content);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
'Lifetime loss socialisation deductions: 5.00'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(tooltip).getByText(
|
||||
`You received less BTC in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenVolumeCell', () => {
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders value with warning tooltip if loss socialisation occurred', async () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
lossSocializationAmount: '500',
|
||||
decimals: 2,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
const content = screen.getByText(props.valueFormatted);
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
|
||||
await userEvent.hover(content);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText('Lifetime loss socialisation deductions: 5.00')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenVolumeCell', () => {
|
||||
const props = {
|
||||
data: undefined,
|
||||
valueFormatted: '100',
|
||||
};
|
||||
it('renders a dash if no data', () => {
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders value if no status is normal', () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status with warning tooltip if not normal', async () => {
|
||||
const props = {
|
||||
data: undefined,
|
||||
data: {
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
it('renders a dash if no data', () => {
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders value if no status is normal', () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status with warning tooltip if orders were closed', async () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
const content = screen.getByText(props.valueFormatted);
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
await userEvent.hover(content);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
`Status: ${PositionStatusMapping[props.data.status]}`
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status with warning tooltip if position was closed out', async () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_CLOSED_OUT,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
const content = screen.getByText(props.valueFormatted);
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
await userEvent.hover(content);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
`Status: ${PositionStatusMapping[props.data.status]}`
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
'You did not have enough BTC collateral to meet the maintenance margin requirements for your position, so it was closed by the network.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
const content = screen.getByText(props.valueFormatted);
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
await userEvent.hover(content);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
`Status: ${PositionStatusMapping[props.data.status]}`
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -478,12 +478,6 @@ export const PNLCell = ({
|
||||
<p className="mb-2">
|
||||
{t('Lifetime loss socialisation deductions: %s', lossesFormatted)}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
`You received less %s in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`,
|
||||
[data.assetSymbol]
|
||||
)}
|
||||
</p>
|
||||
{LOSS_SOCIALIZATION_LINK && (
|
||||
<ExternalLink href={LOSS_SOCIALIZATION_LINK}>
|
||||
{t('Read more about loss socialisation')}
|
||||
@@ -505,51 +499,27 @@ export const OpenVolumeCell = ({
|
||||
return <>-</>;
|
||||
}
|
||||
|
||||
if (data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{valueFormatted}</>;
|
||||
}
|
||||
|
||||
const POSITION_RESOLUTION_LINK = DocsLinks?.POSITION_RESOLUTION ?? '';
|
||||
|
||||
let primaryTooltip;
|
||||
switch (data.status) {
|
||||
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
|
||||
primaryTooltip = t('Your position was closed.');
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
|
||||
primaryTooltip = t('Your open orders were cancelled.');
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_DISTRESSED:
|
||||
primaryTooltip = t('Your position is distressed.');
|
||||
break;
|
||||
}
|
||||
|
||||
let secondaryTooltip;
|
||||
switch (data.status) {
|
||||
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
|
||||
secondaryTooltip = t(
|
||||
`You did not have enough %s collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
|
||||
[data.assetSymbol]
|
||||
);
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
|
||||
secondaryTooltip = t(
|
||||
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
|
||||
);
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_DISTRESSED:
|
||||
secondaryTooltip = t(
|
||||
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
secondaryTooltip = t('Maintained by network');
|
||||
}
|
||||
return (
|
||||
<WarningCell
|
||||
showIcon={data.status !== PositionStatus.POSITION_STATUS_UNSPECIFIED}
|
||||
tooltipContent={
|
||||
<>
|
||||
<p className="mb-2">{primaryTooltip}</p>
|
||||
<p className="mb-2">{secondaryTooltip}</p>
|
||||
<p className="mb-2">
|
||||
{t('Status: %s', PositionStatusMapping[data.status])}
|
||||
{t('Your position was affected by market conditions')}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
'Status: %s',
|
||||
PositionStatusMapping[
|
||||
PositionStatus.POSITION_STATUS_ORDERS_CLOSED
|
||||
]
|
||||
)}
|
||||
</p>
|
||||
{POSITION_RESOLUTION_LINK && (
|
||||
<ExternalLink href={POSITION_RESOLUTION_LINK}>
|
||||
@@ -567,17 +537,15 @@ export const OpenVolumeCell = ({
|
||||
const WarningCell = ({
|
||||
children,
|
||||
tooltipContent,
|
||||
showIcon = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
tooltipContent: ReactNode;
|
||||
showIcon?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip description={tooltipContent}>
|
||||
<div className="w-full flex items-center justify-between underline decoration-dashed underline-offest-2">
|
||||
<span className="text-black dark:text-white mr-1">
|
||||
{showIcon && <Icon name="warning-sign" size={3} />}
|
||||
<Icon name="warning-sign" size={3} />
|
||||
</span>
|
||||
<span className="text-ellipsis overflow-hidden">{children}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { tradesWithMarketProvider } from './trades-data-provider';
|
||||
import { TradesTable } from './trades-table';
|
||||
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
|
||||
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
interface TradesContainerProps {
|
||||
@@ -9,7 +9,8 @@ interface TradesContainerProps {
|
||||
}
|
||||
|
||||
export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||
const update = useDealTicketFormValues((state) => state.updateAll);
|
||||
const useOrderStoreRef = useCreateOrderStore();
|
||||
const updateOrder = useOrderStoreRef((store) => store.update);
|
||||
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: tradesWithMarketProvider,
|
||||
@@ -20,7 +21,9 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||
<TradesTable
|
||||
rowData={data}
|
||||
onClick={(price?: string) => {
|
||||
update(marketId, { price });
|
||||
if (price) {
|
||||
updateOrder(marketId, { price });
|
||||
}
|
||||
}}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
||||
/>
|
||||
|
||||
@@ -136,12 +136,15 @@ export const AnchorButton = forwardRef<HTMLAnchorElement, AnchorButtonProps>(
|
||||
}
|
||||
);
|
||||
|
||||
type ButtonLinkProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'style'>;
|
||||
type ButtonLinkProps = Omit<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'className' | 'style'
|
||||
>;
|
||||
|
||||
export const ButtonLink = forwardRef<HTMLButtonElement, ButtonLinkProps>(
|
||||
({ type = 'button', className, ...props }, ref) => {
|
||||
const style = classnames('inline underline', className);
|
||||
return <button ref={ref} className={style} type={type} {...props} />;
|
||||
({ type = 'button', ...props }, ref) => {
|
||||
const className = classnames('inline underline');
|
||||
return <button ref={ref} className={className} type={type} {...props} />;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export const getIntentBackground = (intent?: Intent) => {
|
||||
return {
|
||||
'bg-neutral-200 dark:bg-neutral-800': intent === undefined,
|
||||
'bg-black dark:bg-white': intent === Intent.None,
|
||||
'bg-vega-blue-300 dark:bg-vega-blue-650': intent === Intent.Primary,
|
||||
'bg-vega-blue-300 dark:bg-vega-blue-700': intent === Intent.Primary,
|
||||
'bg-danger': intent === Intent.Danger,
|
||||
'bg-warning': intent === Intent.Warning,
|
||||
// contrast issues with light mode
|
||||
|
||||
@@ -3,4 +3,3 @@ export * from './number';
|
||||
export * from './range';
|
||||
export * from './size';
|
||||
export * from './strings';
|
||||
export * from './trigger';
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { addDecimalsFormatNumber } from './number';
|
||||
|
||||
export const formatTrigger = (
|
||||
data: Pick<Schema.StopOrder, 'trigger' | 'triggerDirection'> | undefined,
|
||||
marketDecimalPlaces: number,
|
||||
defaultValue = '-'
|
||||
) => {
|
||||
if (data && data?.trigger?.__typename === 'StopOrderPrice') {
|
||||
return `${t('Mark')} ${
|
||||
data?.triggerDirection ===
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
? '<'
|
||||
: '>'
|
||||
} ${addDecimalsFormatNumber(data.trigger.price, marketDecimalPlaces)}`;
|
||||
}
|
||||
if (data && data?.trigger?.__typename === 'StopOrderTrailingPercentOffset') {
|
||||
return `${t('Mark')} ${
|
||||
data?.triggerDirection ===
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
? '+'
|
||||
: '-'
|
||||
}${(Number(data?.trigger.trailingPercentOffset) * 100).toFixed(1)}%`;
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
@@ -1,4 +1,9 @@
|
||||
import { determineId, normalizeOrderAmendment } from './utils';
|
||||
import {
|
||||
determineId,
|
||||
normalizeOrderAmendment,
|
||||
normalizeOrderSubmission,
|
||||
} from './utils';
|
||||
import type { OrderSubmissionBody } from './connectors/vega-connector';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
describe('determineId', () => {
|
||||
it('produces a known result for an ID', () => {
|
||||
@@ -11,6 +16,62 @@ describe('determineId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeOrderSubmission', () => {
|
||||
it('sets and formats price only for limit orders', () => {
|
||||
expect(
|
||||
normalizeOrderSubmission(
|
||||
{ price: '100' } as unknown as OrderSubmissionBody['orderSubmission'],
|
||||
2,
|
||||
1
|
||||
).price
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
normalizeOrderSubmission(
|
||||
{
|
||||
price: '100',
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
} as unknown as OrderSubmissionBody['orderSubmission'],
|
||||
2,
|
||||
1
|
||||
).price
|
||||
).toEqual('10000');
|
||||
});
|
||||
|
||||
it('sets and formats expiresAt only for time in force orders', () => {
|
||||
expect(
|
||||
normalizeOrderSubmission(
|
||||
{
|
||||
expiresAt: '2022-01-01T00:00:00.000Z',
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
2,
|
||||
1
|
||||
).expiresAt
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
normalizeOrderSubmission(
|
||||
{
|
||||
expiresAt: '2022-01-01T00:00:00.000Z',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
2,
|
||||
1
|
||||
).expiresAt
|
||||
).toEqual('1640995200000000000');
|
||||
});
|
||||
|
||||
it('formats size', () => {
|
||||
expect(
|
||||
normalizeOrderSubmission(
|
||||
{
|
||||
size: '100',
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
2,
|
||||
1
|
||||
).size
|
||||
).toEqual('1000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeOrderAmendment', () => {
|
||||
type Order = Parameters<typeof normalizeOrderAmendment>[0];
|
||||
type Market = Parameters<typeof normalizeOrderAmendment>[1];
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import type { Market, Order } from '@vegaprotocol/types';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { OrderTimeInForce, OrderType, AccountType } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { sha3_256 } from 'js-sha3';
|
||||
import type { OrderAmendment, Transaction, Transfer } from './connectors';
|
||||
import type {
|
||||
OrderAmendment,
|
||||
OrderSubmission,
|
||||
Transaction,
|
||||
Transfer,
|
||||
} from './connectors';
|
||||
import type { Exact } from 'type-fest';
|
||||
|
||||
/**
|
||||
@@ -24,6 +29,36 @@ export const encodeTransaction = (tx: Transaction): string => {
|
||||
);
|
||||
};
|
||||
|
||||
export const normalizeOrderSubmission = (
|
||||
order: OrderSubmission,
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number
|
||||
): OrderSubmission => ({
|
||||
marketId: order.marketId,
|
||||
reference: order.reference,
|
||||
type: order.type,
|
||||
side: order.side,
|
||||
timeInForce: order.timeInForce,
|
||||
price:
|
||||
order.type === OrderType.TYPE_LIMIT && order.price
|
||||
? removeDecimal(order.price, decimalPlaces)
|
||||
: undefined,
|
||||
size: removeDecimal(order.size, positionDecimalPlaces),
|
||||
expiresAt:
|
||||
order.expiresAt && order.timeInForce === OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
? toNanoSeconds(order.expiresAt)
|
||||
: undefined,
|
||||
postOnly: order.postOnly,
|
||||
reduceOnly: order.reduceOnly,
|
||||
icebergOpts: order.icebergOpts && {
|
||||
peakSize: removeDecimal(order.icebergOpts.peakSize, positionDecimalPlaces),
|
||||
minimumVisibleSize: removeDecimal(
|
||||
order.icebergOpts.minimumVisibleSize,
|
||||
positionDecimalPlaces
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
export const normalizeOrderAmendment = <T extends Exact<OrderAmendment, T>>(
|
||||
order: Pick<Order, 'id' | 'timeInForce' | 'size' | 'expiresAt'>,
|
||||
market: Pick<Market, 'id' | 'decimalPlaces' | 'positionDecimalPlaces'>,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
getVegaTransactionContentIntent,
|
||||
} from './use-vega-transaction-toasts';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import type { OrderByIdQuery, StopOrderByIdQuery } from '@vegaprotocol/orders';
|
||||
|
||||
jest.mock('@vegaprotocol/assets', () => {
|
||||
const A1 = {
|
||||
@@ -24,7 +23,7 @@ jest.mock('@vegaprotocol/assets', () => {
|
||||
};
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/assets'),
|
||||
useAssetsMapProvider: jest.fn(() => ({ data: { [A1.id]: A1 } })),
|
||||
useAssetsDataProvider: jest.fn(() => ({ data: [A1] })),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -51,7 +50,7 @@ jest.mock('@vegaprotocol/markets', () => {
|
||||
};
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
useMarketsMapProvider: jest.fn(() => ({ data: { [M1.id]: M1 } })),
|
||||
useMarketList: jest.fn(() => ({ data: [M1] })),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -60,64 +59,20 @@ jest.mock('@vegaprotocol/orders', () => {
|
||||
...jest.requireActual('@vegaprotocol/orders'),
|
||||
useOrderByIdQuery: jest.fn(({ variables: { orderId } }) => {
|
||||
if (orderId === '0') {
|
||||
const data: OrderByIdQuery = {
|
||||
orderByID: {
|
||||
id: '0',
|
||||
side: 'SIDE_BUY',
|
||||
size: '10',
|
||||
remaining: '10',
|
||||
timeInForce: 'TIME_IN_FORCE_FOK',
|
||||
type: 'TYPE_MARKET',
|
||||
price: '1234',
|
||||
createdAt: new Date(),
|
||||
status: 'STATUS_ACTIVE',
|
||||
market: { id: 'market-1' },
|
||||
},
|
||||
} as OrderByIdQuery;
|
||||
return {
|
||||
data,
|
||||
};
|
||||
} else {
|
||||
return { data: undefined };
|
||||
}
|
||||
}),
|
||||
useStopOrderByIdQuery: jest.fn(({ variables: { stopOrderId } }) => {
|
||||
if (stopOrderId === '0') {
|
||||
const data: StopOrderByIdQuery = {
|
||||
stopOrder: {
|
||||
id: '0',
|
||||
ocoLinkId: null,
|
||||
expiresAt: null,
|
||||
expiryStrategy: null,
|
||||
triggerDirection: 'TRIGGER_DIRECTION_RISES_ABOVE',
|
||||
status: 'STATUS_CANCELLED',
|
||||
createdAt: '2023-08-03T07:12:36.325927Z',
|
||||
updatedAt: null,
|
||||
partyId: 'party-id',
|
||||
marketId: 'market-1',
|
||||
trigger: {
|
||||
price: '1234',
|
||||
__typename: 'StopOrderPrice',
|
||||
},
|
||||
submission: {
|
||||
marketId: 'market-1',
|
||||
price: '1234',
|
||||
data: {
|
||||
orderByID: {
|
||||
id: '0',
|
||||
side: 'SIDE_BUY',
|
||||
size: '10',
|
||||
side: 'SIDE_SELL',
|
||||
timeInForce: 'TIME_IN_FORCE_FOK',
|
||||
expiresAt: null,
|
||||
type: 'TYPE_MARKET',
|
||||
reference: '',
|
||||
peggedOrder: null,
|
||||
postOnly: false,
|
||||
reduceOnly: true,
|
||||
__typename: 'OrderSubmission',
|
||||
price: '1234',
|
||||
createdAt: new Date(),
|
||||
status: 'STATUS_ACTIVE',
|
||||
market: { id: 'market-1' },
|
||||
},
|
||||
__typename: 'StopOrder',
|
||||
},
|
||||
} as StopOrderByIdQuery;
|
||||
return {
|
||||
data,
|
||||
};
|
||||
} else {
|
||||
return { data: undefined };
|
||||
@@ -188,31 +143,6 @@ const submitOrder: VegaStoredTxState = {
|
||||
},
|
||||
};
|
||||
|
||||
const submitStopOrder: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
body: {
|
||||
stopOrdersSubmission: {
|
||||
risesAbove: {
|
||||
price: '1234',
|
||||
orderSubmission: {
|
||||
marketId: 'market-1',
|
||||
side: Side.SIDE_BUY,
|
||||
size: '10',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
status: VegaTxStatus.Default,
|
||||
error: null,
|
||||
txHash: null,
|
||||
signature: null,
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
const editOrder: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
@@ -278,23 +208,6 @@ const cancelAll: VegaStoredTxState = {
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
const cancelStopOrder: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
body: {
|
||||
stopOrdersCancellation: {
|
||||
marketId: 'market-1',
|
||||
stopOrderId: '0',
|
||||
},
|
||||
},
|
||||
status: VegaTxStatus.Default,
|
||||
error: null,
|
||||
txHash: null,
|
||||
signature: null,
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
const closePosition: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
@@ -356,20 +269,12 @@ describe('VegaTransactionDetails', () => {
|
||||
it.each([
|
||||
{ tx: withdraw, details: 'Withdraw 12.34 $A' },
|
||||
{ tx: submitOrder, details: 'Submit order - activeM1+0.10 @ 12.34 $A' },
|
||||
{
|
||||
tx: submitStopOrder,
|
||||
details: 'Submit stop orderM1+0.10 @ ~ $AMark > 12.34',
|
||||
},
|
||||
{
|
||||
tx: editOrder,
|
||||
details: 'Edit order - activeM1+0.10 @ 12.34 $A+0.11 @ 10.00 $A',
|
||||
},
|
||||
{ tx: cancelOrder, details: 'Cancel orderM1+0.10 @ 12.34 $A' },
|
||||
{ tx: cancelAll, details: 'Cancel all orders' },
|
||||
{
|
||||
tx: cancelStopOrder,
|
||||
details: 'Cancel stop orderM1-0.10 @ 12.34 $AMark > 12.34',
|
||||
},
|
||||
{ tx: closePosition, details: 'Close position for M1' },
|
||||
{ tx: batch, details: 'Batch market instruction' },
|
||||
])('display details for transaction', ({ tx, details }) => {
|
||||
@@ -386,18 +291,12 @@ describe('getVegaTransactionContentIntent', () => {
|
||||
expect(getVegaTransactionContentIntent(submitOrder).intent).toBe(
|
||||
Intent.Success
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(submitStopOrder).intent).toBe(
|
||||
Intent.Primary
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(editOrder).intent).toBe(
|
||||
Intent.Primary
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(cancelOrder).intent).toBe(
|
||||
Intent.Primary
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(cancelStopOrder).intent).toBe(
|
||||
Intent.Primary
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(cancelAll).intent).toBe(
|
||||
Intent.Primary
|
||||
);
|
||||
|
||||
@@ -5,10 +5,10 @@ import type {
|
||||
BatchMarketInstructionSubmissionBody,
|
||||
OrderAmendment,
|
||||
OrderTxUpdateFieldsFragment,
|
||||
OrderCancellationBody,
|
||||
OrderSubmission,
|
||||
VegaStoredTxState,
|
||||
WithdrawalBusEventFieldsFragment,
|
||||
StopOrdersSubmission,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
isTransferTransaction,
|
||||
@@ -36,10 +36,9 @@ import {
|
||||
formatNumber,
|
||||
toBigNum,
|
||||
truncateByChars,
|
||||
formatTrigger,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { useEthWithdrawApprovalsStore } from './use-ethereum-withdraw-approvals-store';
|
||||
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
|
||||
import {
|
||||
@@ -47,9 +46,8 @@ import {
|
||||
getOrderToastTitle,
|
||||
getRejectionReason,
|
||||
useOrderByIdQuery,
|
||||
useStopOrderByIdQuery,
|
||||
} from '@vegaprotocol/orders';
|
||||
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import type { Side } from '@vegaprotocol/types';
|
||||
import { OrderStatusMapping } from '@vegaprotocol/types';
|
||||
import { Size } from '@vegaprotocol/datagrid';
|
||||
@@ -138,8 +136,8 @@ const SubmitOrderDetails = ({
|
||||
data: OrderSubmission;
|
||||
order?: OrderTxUpdateFieldsFragment;
|
||||
}) => {
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
const market = markets?.[order?.marketId || ''];
|
||||
const { data: markets } = useMarketList();
|
||||
const market = markets?.find((m) => m.id === order?.marketId);
|
||||
if (!market) return null;
|
||||
|
||||
const price = order ? order.price : data.price;
|
||||
@@ -174,58 +172,6 @@ const SubmitOrderDetails = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SubmitStopOrderDetails = ({ data }: { data: StopOrdersSubmission }) => {
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
const stopOrderSetup = data.risesAbove || data.fallsBelow;
|
||||
if (!stopOrderSetup) return null;
|
||||
const market = markets?.[stopOrderSetup?.orderSubmission.marketId];
|
||||
if (!market || !stopOrderSetup) return null;
|
||||
|
||||
const { price, size, side } = stopOrderSetup.orderSubmission;
|
||||
let trigger: Schema.StopOrderTrigger | null = null;
|
||||
if (stopOrderSetup.price) {
|
||||
trigger = { price: stopOrderSetup.price, __typename: 'StopOrderPrice' };
|
||||
} else if (stopOrderSetup.trailingPercentOffset) {
|
||||
trigger = {
|
||||
trailingPercentOffset: stopOrderSetup.trailingPercentOffset,
|
||||
__typename: 'StopOrderTrailingPercentOffset',
|
||||
};
|
||||
}
|
||||
const triggerDirection = data.risesAbove
|
||||
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW;
|
||||
return (
|
||||
<Panel>
|
||||
<h4>{t('Submit stop order')}</h4>
|
||||
<p>{market?.tradableInstrument.instrument.code}</p>
|
||||
<p>
|
||||
<SizeAtPrice
|
||||
meta={{
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
asset:
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.symbol,
|
||||
}}
|
||||
side={side}
|
||||
size={size}
|
||||
price={price}
|
||||
/>
|
||||
<br />
|
||||
{trigger &&
|
||||
formatTrigger(
|
||||
{
|
||||
triggerDirection,
|
||||
trigger,
|
||||
},
|
||||
market.decimalPlaces,
|
||||
''
|
||||
)}
|
||||
</p>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
const EditOrderDetails = ({
|
||||
data,
|
||||
order,
|
||||
@@ -237,12 +183,13 @@ const EditOrderDetails = ({
|
||||
variables: { orderId: data.orderId },
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
const { data: markets } = useMarketList();
|
||||
|
||||
const originalOrder = order || orderById?.orderByID;
|
||||
const marketId = order?.marketId || orderById?.orderByID.market.id;
|
||||
const market = markets?.[marketId || ''];
|
||||
if (!originalOrder || !market) return null;
|
||||
if (!originalOrder) return null;
|
||||
const market = markets?.find((m) => m.id === marketId);
|
||||
if (!market) return null;
|
||||
|
||||
const original = (
|
||||
<SizeAtPrice
|
||||
@@ -298,11 +245,11 @@ const CancelOrderDetails = ({
|
||||
const { data: orderById } = useOrderByIdQuery({
|
||||
variables: { orderId },
|
||||
});
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
const { data: markets } = useMarketList();
|
||||
|
||||
const originalOrder = orderById?.orderByID;
|
||||
if (!originalOrder) return null;
|
||||
const market = markets?.[originalOrder.market.id];
|
||||
const market = markets?.find((m) => m.id === originalOrder.market.id);
|
||||
if (!market) return null;
|
||||
|
||||
const original = (
|
||||
@@ -335,52 +282,15 @@ const CancelOrderDetails = ({
|
||||
);
|
||||
};
|
||||
|
||||
const CancelStopOrderDetails = ({ stopOrderId }: { stopOrderId: string }) => {
|
||||
const { data: orderById } = useStopOrderByIdQuery({
|
||||
variables: { stopOrderId },
|
||||
});
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
|
||||
const originalOrder = orderById?.stopOrder;
|
||||
if (!originalOrder) return null;
|
||||
const market = markets?.[originalOrder.marketId];
|
||||
if (!market) return null;
|
||||
|
||||
const original = (
|
||||
<>
|
||||
<SizeAtPrice
|
||||
side={originalOrder.submission.side}
|
||||
size={originalOrder.submission.size}
|
||||
price={originalOrder.submission.price}
|
||||
meta={{
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
asset:
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol,
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
{formatTrigger(originalOrder, market.decimalPlaces, '')}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Panel title={stopOrderId}>
|
||||
<h4>{t('Cancel stop order')}</h4>
|
||||
<p>{market?.tradableInstrument.instrument.code}</p>
|
||||
<p>
|
||||
<s>{original}</s>
|
||||
</p>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
const { data: assets } = useAssetsMapProvider();
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
const { data: assets } = useAssetsDataProvider();
|
||||
const { data: markets } = useMarketList();
|
||||
|
||||
if (isWithdrawTransaction(tx.body)) {
|
||||
const transactionDetails = tx.body;
|
||||
const asset = assets?.[transactionDetails.withdrawSubmission.asset];
|
||||
const asset = assets?.find(
|
||||
(a) => a.id === transactionDetails.withdrawSubmission.asset
|
||||
);
|
||||
if (asset) {
|
||||
const num = formatNumber(
|
||||
toBigNum(transactionDetails.withdrawSubmission.amount, asset.decimals),
|
||||
@@ -402,11 +312,15 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isStopOrdersSubmissionTransaction(tx.body)) {
|
||||
return <SubmitStopOrderDetails data={tx.body.stopOrdersSubmission} />;
|
||||
}
|
||||
|
||||
if (isOrderCancellationTransaction(tx.body)) {
|
||||
// CANCEL ALL (from Portfolio)
|
||||
if (
|
||||
tx.body.orderCancellation.marketId === undefined &&
|
||||
tx.body.orderCancellation.orderId === undefined
|
||||
) {
|
||||
return <Panel>{t('Cancel all orders')}</Panel>;
|
||||
}
|
||||
|
||||
// CANCEL
|
||||
if (
|
||||
tx.body.orderCancellation.orderId &&
|
||||
@@ -422,50 +336,22 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
|
||||
// CANCEL ALL (from Trading)
|
||||
if (tx.body.orderCancellation.marketId) {
|
||||
const marketName =
|
||||
markets?.[tx.body.orderCancellation.marketId]?.tradableInstrument
|
||||
.instrument.code;
|
||||
if (marketName) {
|
||||
return (
|
||||
<Panel>
|
||||
{t('Cancel all orders for')} <strong>{marketName}</strong>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
// CANCEL ALL (from Portfolio)
|
||||
return <Panel>{t('Cancel all orders')}</Panel>;
|
||||
}
|
||||
|
||||
if (isStopOrdersCancellationTransaction(tx.body)) {
|
||||
// CANCEL
|
||||
if (
|
||||
tx.body.stopOrdersCancellation.stopOrderId &&
|
||||
tx.body.stopOrdersCancellation.marketId
|
||||
) {
|
||||
const marketName = markets?.find(
|
||||
(m) =>
|
||||
m.id === (tx.body as OrderCancellationBody).orderCancellation.marketId
|
||||
)?.tradableInstrument.instrument.code;
|
||||
return (
|
||||
<CancelStopOrderDetails
|
||||
stopOrderId={String(tx.body.stopOrdersCancellation.stopOrderId)}
|
||||
/>
|
||||
<Panel>
|
||||
{marketName ? (
|
||||
<>
|
||||
{t('Cancel all orders for')} <strong>{marketName}</strong>
|
||||
</>
|
||||
) : (
|
||||
t('Cancel all orders')
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// CANCEL ALL for market
|
||||
if (tx.body.stopOrdersCancellation.marketId) {
|
||||
const marketName =
|
||||
markets?.[tx.body.stopOrdersCancellation.marketId]?.tradableInstrument
|
||||
.instrument.code;
|
||||
if (marketName) {
|
||||
return (
|
||||
<Panel>
|
||||
{t('Cancel all stop orders for')} <strong>{marketName}</strong>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// CANCEL ALL
|
||||
return <Panel>{t('Cancel all stop orders')}</Panel>;
|
||||
}
|
||||
|
||||
if (isOrderAmendmentTransaction(tx.body)) {
|
||||
@@ -482,7 +368,7 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
const marketId = first(
|
||||
transaction.batchMarketInstructions.cancellations
|
||||
)?.marketId;
|
||||
const market = markets?.[marketId || ''];
|
||||
const market = marketId && markets?.find((m) => m.id === marketId);
|
||||
if (market) {
|
||||
return (
|
||||
<Panel>
|
||||
@@ -499,7 +385,7 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
|
||||
if (isTransferTransaction(tx.body)) {
|
||||
const { amount, to, asset } = tx.body.transfer;
|
||||
const transferAsset = assets?.[asset];
|
||||
const transferAsset = assets?.find((a) => a.id === asset);
|
||||
// only render if we have an asset to avoid unformatted amounts showing
|
||||
if (transferAsset) {
|
||||
const value = addDecimalsFormatNumber(amount, transferAsset.decimals);
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
"@web3-react/walletconnect-v2": "^8.1.3-beta.0",
|
||||
"ag-grid-community": "^29.3.5",
|
||||
"ag-grid-react": "^29.3.5",
|
||||
"allotment": "1.19.2",
|
||||
"allotment": "1.18.1",
|
||||
"alpha-lyrae": "vegaprotocol/alpha-lyrae",
|
||||
"apollo": "^2.33.9",
|
||||
"apollo-link-timeout": "^4.0.0",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# First use & get started steps
|
||||
|
||||
## When first enter the app
|
||||
|
||||
- **Must** When I open Console for the first time I can see what it is i.e. a short description and key features in auto opened dialog window (first use popup) (<a name="0007-FUGS-001" href="#0007-FUGS-001">0007-FUGS-001</a>)
|
||||
- **Must** If my wallet is already connected I don't see the first use popup (<a name="0007-FUGS-002" href="#0007-FUGS-002">0007-FUGS-002</a>)
|
||||
- - **Must** If window.vega is detected (browser wallet is installed), don't open first use popup (<a name="0007-FUGS-003" href="#0007-FUGS-003">0007-FUGS-003</a>)
|
||||
- - **Must** If we detect previous connection using localStorage for desktop/cli wallet, don't open first use popup (<a name="0007-FUGS-004" href="#0007-FUGS-004">0007-FUGS-004</a>)
|
||||
- **Must** There is a call to action to browse markets, linking to the market view market/all (<a name="0007-FUGS-005" href="#0007-FUGS-005">0007-FUGS-005</a>)
|
||||
- **Must** I can see the steps I need to take to get started trading (<a name="0007-FUGS-007" href="#0007-FUGS-006">0007-FUGS-006</a>)
|
||||
- **Must** There is a call to action to get started, triggering the connect modal (<a name="0007-FUGS-007" href="#0007-FUGS-007">0007-FUGS-007</a>)
|
||||
- **Must** There is a link to try out trading on Fairground when I'm on Mainnet (<a name="0007-FUGS-008" href="#0007-FUGS-008">0007-FUGS-008</a>)
|
||||
- **Must** There is a link to trade with real funds on Mainnet when I am on Fairground (<a name="0007-FUGS-010" href="#0007-FUGS-010">0007-FUGS-010</a>)
|
||||
- **Must** When I am on the Fairground version, I can see a warning / call out that this is Fairground meaning I can try out with virtual assets at no risk (<a name="0007-FUGS-011" href="#0007-FUGS-011">0007-FUGS-011</a>)
|
||||
- If I dismiss the popup, I land on the default market (<a name="0007-FUGS-012" href="#0007-FUGS-012">0007-FUGS-012</a>)
|
||||
|
||||
## When first use popup has been seen, but no browser wallet is installed
|
||||
|
||||
- **Must** I can see the steps to get started with a visible call to action to "get started" in the context of the deal ticket, deposit, withdraw, transfer components in the sidebar (<a name="0007-FUGS-013" href="#0007-FUGS-013">0007-FUGS-013</a>)
|
||||
- **Must** Remove buttons from pane containers that prompt to connect wallet (<a name="0007-FUGS-014" href="#0007-FUGS-014">0007-FUGS-014</a>)
|
||||
- **Must** We've replaced "connect wallet" in the top right with "get started" (<a name="0007-FUGS-015" href="#0007-FUGS-015">0007-FUGS-015</a>)
|
||||
- **Must** When I press the get started CTA, I see the wallet connect popup (<a name="0007-FUGS-016" href="#0007-FUGS-016">0007-FUGS-016</a>)
|
||||
- **Must** If I have a wallet installed already I don't see this quick start onboarding, and instead call(s) to action in Console revert to connect wallet, not "get started" (button in nav header) (<a name="0007-FUGS-017" href="#0007-FUGS-017">0007-FUGS-017</a>)
|
||||
@@ -117,15 +117,6 @@ When I look into market info I **Must** see following governance information:
|
||||
- Proposal: (<a name="6002-MDET-301" href="#6002-MDET-301">6002-MDET-301</a>)
|
||||
- link to governance proposal
|
||||
- link to propose a change to market
|
||||
- Succession line: (<a name="6002-MDET-302" href="#6002-MDET-302">6002-MDET-302</a>):
|
||||
a list consisting of an origin market and all of its successors one-by-one.
|
||||
The list **Should** contain:
|
||||
|
||||
- Market Code which links to the corresponding governance proposal
|
||||
- Market Name
|
||||
- Market ID
|
||||
|
||||
When I look at the succession line list I **Should** easily distinguish which market is the currently viewed market so I can see the ancestor-descendant relations between the current and other markets on the list.
|
||||
|
||||
## Market successor
|
||||
|
||||
|
||||
@@ -9694,10 +9694,10 @@ ajv@^8.9.0:
|
||||
require-from-string "^2.0.2"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
allotment@1.19.0:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/allotment/-/allotment-1.19.0.tgz#8241a2e3db45e6b1e23f6ade29e392eab4297958"
|
||||
integrity sha512-qL/1faHUoCOvMstCFGkaGSK/nVoSAVN6NLFuIW4P6D5fvKdgxV8/KNfMFeuQUXReodILp4I6z+5a9zmuqK5t4g==
|
||||
allotment@1.18.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.yarnpkg.com/allotment/-/allotment-1.18.1.tgz#a31b0cb5b301309334dfa352c7704361437058de"
|
||||
integrity sha512-ZhmpHoSvxHyPT5XjzTHeCBjzCMIFn1aGdNAzBCZhj1CcRMp31CaJCi+lfweoOLDbbjJqbFZqUk3xf+p7Mb0x5A==
|
||||
dependencies:
|
||||
classnames "^2.3.0"
|
||||
eventemitter3 "^5.0.0"
|
||||
@@ -9706,10 +9706,10 @@ allotment@1.19.0:
|
||||
lodash.isequal "^4.5.0"
|
||||
use-resize-observer "^9.0.0"
|
||||
|
||||
allotment@1.19.2:
|
||||
version "1.19.2"
|
||||
resolved "https://registry.yarnpkg.com/allotment/-/allotment-1.19.2.tgz#6026a944678958c961dafcfefe9a768af4d6610e"
|
||||
integrity sha512-RiiCLa4AQATPsQDXvleyvdRzSsd2J7naLuY+eK1sn1zeXBKMdsCwWuICZ+ZCeDCrJXjVV+q/mHfB2LT7PEYmTw==
|
||||
allotment@1.19.0:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/allotment/-/allotment-1.19.0.tgz#8241a2e3db45e6b1e23f6ade29e392eab4297958"
|
||||
integrity sha512-qL/1faHUoCOvMstCFGkaGSK/nVoSAVN6NLFuIW4P6D5fvKdgxV8/KNfMFeuQUXReodILp4I6z+5a9zmuqK5t4g==
|
||||
dependencies:
|
||||
classnames "^2.3.0"
|
||||
eventemitter3 "^5.0.0"
|
||||
|
||||
Reference in New Issue
Block a user