Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eba8e3ca8b | ||
|
|
6894fe1264 | ||
|
|
93e4b5fdb9 | ||
|
|
1566fc3984 | ||
|
|
39962a0246 | ||
|
|
8d31510d5e | ||
|
|
26d5a67604 | ||
|
|
9cc8f5a377 | ||
|
|
2e11cf4dfa | ||
|
|
9e4ba9f275 | ||
|
|
aac0c25d09 | ||
|
|
d20c2a08ae | ||
|
|
4e6ba5fe3d | ||
|
|
276d1d7c7b | ||
|
|
4990c5808d | ||
|
|
cdd91c24f2 | ||
|
|
e3eb13ca72 | ||
|
|
3f0ebbf33d | ||
|
|
026aa3964b | ||
|
|
aa4c5a4a57 | ||
|
|
0874314d2a |
@@ -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: 'Epic: '
|
||||
title: 'FEATURE EPIC: '
|
||||
labels: feature-epic
|
||||
---
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ on:
|
||||
jobs:
|
||||
run-tests:
|
||||
name: run-tests
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
runs-on: 8-cores
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
#----------------------------------------------
|
||||
# check-out frontend-monorepo
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
#----------------------------------------------
|
||||
- name: Run tests
|
||||
working-directory: ./console-test
|
||||
run: poetry run pytest --numprocesses auto
|
||||
run: poetry run pytest -s --numprocesses auto
|
||||
- name: Check files
|
||||
run: |
|
||||
ls -al .
|
||||
|
||||
@@ -23,7 +23,7 @@ export const Footer = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
<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,9 +43,12 @@ export const Footer = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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)}>
|
||||
<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)}
|
||||
>
|
||||
{t('Change')}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -59,7 +62,10 @@ 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">
|
||||
<RouteLink
|
||||
to={`/${Routes.DISCLAIMER}`}
|
||||
className="underline underline-offset-4"
|
||||
>
|
||||
Disclaimer
|
||||
</RouteLink>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { MarketInfoWithData } from '@vegaprotocol/markets';
|
||||
import { PriceMonitoringBoundsInfoPanel } from '@vegaprotocol/markets';
|
||||
import {
|
||||
PriceMonitoringBoundsInfoPanel,
|
||||
SuccessionLineInfoPanel,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
LiquidityInfoPanel,
|
||||
LiquidityMonitoringParametersInfoPanel,
|
||||
@@ -103,6 +106,8 @@ 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,6 +10,8 @@ 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'];
|
||||
@@ -53,7 +55,11 @@ export function proposalTypeLabel(terms?: ProposalTerms): string {
|
||||
} else if (has(terms, 'updateAsset')) {
|
||||
return t('Update asset proposal');
|
||||
} else if (has(terms, 'newMarket')) {
|
||||
return t('New market proposal');
|
||||
if (terms?.newMarket?.changes?.successor) {
|
||||
return t('Successor market proposal');
|
||||
} else {
|
||||
return t('New market proposal');
|
||||
}
|
||||
} else if (has(terms, 'updateMarket')) {
|
||||
return t('Update market proposal');
|
||||
} else if (has(terms, 'updateNetworkParameter')) {
|
||||
@@ -85,6 +91,13 @@ 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
|
||||
@@ -111,6 +124,30 @@ 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,7 +78,11 @@ export function getLabelForProposal(
|
||||
} else if (proposal.terms?.updateAsset) {
|
||||
return t('Proposal: Update asset');
|
||||
} else if (proposal.terms?.newMarket) {
|
||||
return t('Proposal: New market');
|
||||
if (proposal.terms?.newMarket.changes?.successor) {
|
||||
return t('Proposal: Successor market');
|
||||
} else {
|
||||
return t('Proposal: New market');
|
||||
}
|
||||
} else if (proposal.terms?.updateMarket) {
|
||||
return t('Proposal: Update market');
|
||||
} else if (proposal.terms?.updateNetworkParameter) {
|
||||
|
||||
@@ -730,6 +730,7 @@ context(
|
||||
.getByTestId('key-value-table-row')
|
||||
.contains(heading)
|
||||
.parent()
|
||||
.parent()
|
||||
.siblings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { VegaConnectDialog, VegaManageDialog } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
VegaConnectDialog,
|
||||
VegaManageDialog,
|
||||
ViewAsDialog,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
@@ -25,6 +29,8 @@ export const VegaWalletDialogs = () => {
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<ViewAsDialog connector={Connectors.view} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
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 { DownloadWalletPrompt } from './download-wallet-prompt';
|
||||
import { VegaWalletPrompt } from './vega-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>
|
||||
<DownloadWalletPrompt />
|
||||
<VegaWalletPrompt />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -855,5 +855,6 @@
|
||||
"Estimated time to upgrade": "Estimated time to upgrade",
|
||||
"Upgraded at": "Upgraded at",
|
||||
"dataIsIdentical": "Data is identical",
|
||||
"updatesToMarket": "Updates to market"
|
||||
"updatesToMarket": "Updates to market",
|
||||
"viewAsParty": "View as party"
|
||||
}
|
||||
|
||||
+83
-9
@@ -45,8 +45,10 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
|
||||
|
||||
export const ProposalMarketData = ({
|
||||
marketData,
|
||||
parentMarketData,
|
||||
}: {
|
||||
marketData: MarketInfoWithData;
|
||||
parentMarketData?: MarketInfoWithData;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, open, close } = useMarketDataDialogStore();
|
||||
@@ -58,8 +60,21 @@ 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') {
|
||||
@@ -97,12 +112,22 @@ export const ProposalMarketData = ({
|
||||
<AccordionItem
|
||||
itemId="key-details"
|
||||
title={t('Key details')}
|
||||
content={<KeyDetailsInfoPanel market={marketData} />}
|
||||
content={
|
||||
<KeyDetailsInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="instrument"
|
||||
title={t('Instrument')}
|
||||
content={<InstrumentInfoPanel market={marketData} />}
|
||||
content={
|
||||
<InstrumentInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{isEqual(
|
||||
getSigners(settlementData),
|
||||
@@ -115,6 +140,11 @@ export const ProposalMarketData = ({
|
||||
<OracleInfoPanel
|
||||
market={marketData}
|
||||
type="settlementData"
|
||||
parentMarket={
|
||||
isParentSettlementDataEqual
|
||||
? undefined
|
||||
: parentMarketData
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -127,6 +157,11 @@ export const ProposalMarketData = ({
|
||||
<OracleInfoPanel
|
||||
market={marketData}
|
||||
type="settlementData"
|
||||
parentMarket={
|
||||
isParentSettlementDataEqual
|
||||
? undefined
|
||||
: parentMarketData
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -135,11 +170,21 @@ export const ProposalMarketData = ({
|
||||
itemId="termination-oracle"
|
||||
title={t('Termination Oracle')}
|
||||
content={
|
||||
<OracleInfoPanel market={marketData} type="termination" />
|
||||
<OracleInfoPanel
|
||||
market={marketData}
|
||||
type="termination"
|
||||
parentMarket={
|
||||
isParentTerminationDataEqual
|
||||
? undefined
|
||||
: parentMarketData
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/*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')}
|
||||
@@ -148,22 +193,42 @@ export const ProposalMarketData = ({
|
||||
<AccordionItem
|
||||
itemId="metadata"
|
||||
title={t('Metadata')}
|
||||
content={<MetadataInfoPanel market={marketData} />}
|
||||
content={
|
||||
<MetadataInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="risk-model"
|
||||
title={t('Risk model')}
|
||||
content={<RiskModelInfoPanel market={marketData} />}
|
||||
content={
|
||||
<RiskModelInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="risk-parameters"
|
||||
title={t('Risk parameters')}
|
||||
content={<RiskParametersInfoPanel market={marketData} />}
|
||||
content={
|
||||
<RiskParametersInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="risk-factors"
|
||||
title={t('Risk factors')}
|
||||
content={<RiskFactorsInfoPanel market={marketData} />}
|
||||
content={
|
||||
<RiskFactorsInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{(
|
||||
marketData.priceMonitoringSettings?.parameters?.triggers || []
|
||||
@@ -174,6 +239,7 @@ export const ProposalMarketData = ({
|
||||
content={
|
||||
<PriceMonitoringBoundsInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
triggerIndex={triggerIndex}
|
||||
/>
|
||||
}
|
||||
@@ -183,13 +249,21 @@ export const ProposalMarketData = ({
|
||||
itemId="liqudity-monitoring-parameters"
|
||||
title={t('Liquidity monitoring parameters')}
|
||||
content={
|
||||
<LiquidityMonitoringParametersInfoPanel market={marketData} />
|
||||
<LiquidityMonitoringParametersInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<AccordionItem
|
||||
itemId="liquidity-price-range"
|
||||
title={t('Liquidity price range')}
|
||||
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
|
||||
content={
|
||||
<LiquidityPriceRangeInfoPanel
|
||||
market={marketData}
|
||||
parentMarket={parentMarketData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ 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;
|
||||
@@ -46,6 +47,7 @@ export const Proposal = ({
|
||||
networkParams,
|
||||
restData,
|
||||
newMarketData,
|
||||
parentMarketData,
|
||||
assetData,
|
||||
originalMarketProposalRestData,
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
@@ -157,7 +159,10 @@ export const Proposal = ({
|
||||
|
||||
{newMarketData && (
|
||||
<div className="mb-4">
|
||||
<ProposalMarketData marketData={newMarketData} />
|
||||
<ProposalMarketData
|
||||
marketData={newMarketData}
|
||||
parentMarketData={parentMarketData ? parentMarketData : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ 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 [
|
||||
@@ -54,6 +57,10 @@ export const ProposalContainer = () => {
|
||||
skip: !params.proposalId,
|
||||
});
|
||||
|
||||
const successor = useSuccessorMarketProposalDetails(params.proposalId);
|
||||
|
||||
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
|
||||
|
||||
const {
|
||||
state: {
|
||||
data: originalMarketProposalRestData,
|
||||
@@ -96,6 +103,36 @@ 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,
|
||||
@@ -160,6 +197,8 @@ export const ProposalContainer = () => {
|
||||
newMarketLoading ||
|
||||
assetLoading ||
|
||||
networkParamsLoading ||
|
||||
parentMarketIdLoading ||
|
||||
parentMarketLoading ||
|
||||
(restLoading ? (restLoading as boolean) : false) ||
|
||||
(originalMarketProposalRestLoading
|
||||
? (originalMarketProposalRestLoading as boolean)
|
||||
@@ -172,15 +211,18 @@ export const ProposalContainer = () => {
|
||||
error ||
|
||||
newMarketError ||
|
||||
assetError ||
|
||||
networkParamsError ||
|
||||
parentMarketIdError ||
|
||||
parentMarketError ||
|
||||
restError ||
|
||||
originalMarketProposalRestError ||
|
||||
previouslyEnactedMarketProposalsRestError ||
|
||||
networkParamsError
|
||||
previouslyEnactedMarketProposalsRestError
|
||||
}
|
||||
data={{
|
||||
...data,
|
||||
...networkParams,
|
||||
...(newMarketData ? { newMarketData } : {}),
|
||||
...(parentMarketData ? { parentMarketData } : {}),
|
||||
...(assetData ? { assetData } : {}),
|
||||
...(restData ? { restData } : {}),
|
||||
...(originalMarketProposalRestData
|
||||
@@ -197,6 +239,7 @@ export const ProposalContainer = () => {
|
||||
networkParams={networkParams}
|
||||
restData={restData}
|
||||
newMarketData={newMarketData}
|
||||
parentMarketData={parentMarketData}
|
||||
assetData={assetData}
|
||||
originalMarketProposalRestData={originalMarketProposalRestData}
|
||||
mostRecentlyEnactedAssociatedMarketProposal={
|
||||
|
||||
@@ -5,12 +5,11 @@ describe('charts', { tags: '@smoke' }, () => {
|
||||
cy.setOnBoardingViewed();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
cy.getByTestId('Depth').click();
|
||||
});
|
||||
|
||||
it('can see market depth chart', () => {
|
||||
// 6006-DEPC-001
|
||||
cy.getByTestId('Depth').click();
|
||||
cy.getByTestId('tab-depth').should('be.visible');
|
||||
cy.get('.depth-chart-module_canvas__260De').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,11 +104,11 @@ describe('deposit actions', { tags: '@smoke' }, () => {
|
||||
cy.visit('/#/markets/market-1');
|
||||
});
|
||||
|
||||
it('Deposit to trade is visble', () => {
|
||||
it('Deposit to trade is visible', () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
cy.contains('[data-testid="deposit"]', 'Deposit')
|
||||
.should('be.visible')
|
||||
.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.getByTestId('deposit-submit').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,7 +155,10 @@ describe(
|
||||
{ name: 'Volume', infoText: 'Volume: 55,000' },
|
||||
];
|
||||
cy.get(indicatorInfo).eq(1).realHover();
|
||||
cy.get('.close-button-module_closeButton__2ifkl').click({ force: true });
|
||||
cy.get('.chart__wrapper [data-testid="split-view-view"]')
|
||||
.last()
|
||||
.find('[role="button"][title="Close"]')
|
||||
.click({ force: true });
|
||||
cy.get(indicatorInfo).should('have.length', 1);
|
||||
|
||||
checkMenuItemCheckbox('Studies', studyInfo);
|
||||
|
||||
@@ -63,6 +63,7 @@ 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'
|
||||
@@ -73,6 +74,7 @@ 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,6 +48,7 @@ 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'
|
||||
@@ -60,6 +61,7 @@ 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.name"]'
|
||||
'[role="gridcell"][col-id="market.tradableInstrument.instrument.code"]'
|
||||
)
|
||||
.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 = [
|
||||
'ACTIVE MARKET',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'ETHBTC Quarterly (30 Jun 2022)',
|
||||
'SUSPENDED MARKET',
|
||||
'AAPL.MF21',
|
||||
'BTCUSD.MF21',
|
||||
'ETHBTC.QM21',
|
||||
'SOLUSD',
|
||||
];
|
||||
const marketsSortedAsc = [
|
||||
'ACTIVE MARKET',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'ETHBTC Quarterly (30 Jun 2022)',
|
||||
'SUSPENDED MARKET',
|
||||
'AAPL.MF21',
|
||||
'BTCUSD.MF21',
|
||||
'ETHBTC.QM21',
|
||||
'SOLUSD',
|
||||
];
|
||||
const marketsSortedDesc = [
|
||||
'SUSPENDED MARKET',
|
||||
'ETHBTC Quarterly (30 Jun 2022)',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'ACTIVE MARKET',
|
||||
'SOLUSD',
|
||||
'ETHBTC.QM21',
|
||||
'BTCUSD.MF21',
|
||||
'AAPL.MF21',
|
||||
];
|
||||
cy.getByTestId(positions).click();
|
||||
// 7004-POSI-003
|
||||
@@ -251,11 +251,13 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
|
||||
it('I can see warnings', () => {
|
||||
visitAndClickPositions();
|
||||
|
||||
cy.get('[col-id="openVolume"]').within(() => {
|
||||
cy.get('[aria-label="warning-sign icon"]')
|
||||
.should('be.visible')
|
||||
.realHover();
|
||||
});
|
||||
cy.get('[col-id="openVolume"]')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('[aria-label="warning-sign icon"]')
|
||||
.should('be.visible')
|
||||
.realHover();
|
||||
});
|
||||
// 7004-POSI-011
|
||||
cy.getByTestId(tooltipContent).should('be.visible');
|
||||
});
|
||||
@@ -289,9 +291,11 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
|
||||
|
||||
it('View settlement asset', () => {
|
||||
visitAndClickPositions();
|
||||
cy.get('[col-id="asset"]').within(() => {
|
||||
cy.get('button[type="button"]').first().click();
|
||||
});
|
||||
cy.get('[col-id="asset"]')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('button[type="button"]').click();
|
||||
});
|
||||
// 7004-POSI-008
|
||||
cy.getByTestId(dialogContent).should('be.visible');
|
||||
cy.getByTestId(dialogCloseX).click();
|
||||
@@ -306,7 +310,7 @@ function validatePositionsDisplayed(multiKey = false) {
|
||||
cy.getByTestId('tab-positions').should('be.visible');
|
||||
cy.getByTestId('tab-positions')
|
||||
.get('.ag-center-cols-container .ag-row')
|
||||
.first()
|
||||
.eq(multiKey ? 3 : 1)
|
||||
.within(() => {
|
||||
cy.get('[col-id="marketName"]')
|
||||
.should('be.visible')
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { selectAsset } from '../support/helpers';
|
||||
|
||||
const amountField = 'input[name="amount"]';
|
||||
const includeTransferFeeRadioBtn = 'include-transfer-fee';
|
||||
const manageVegaWallet = 'manage-vega-wallet';
|
||||
const toAddressField = '[name="toAddress"]';
|
||||
const totalTransferfee = 'total-transfer-fee';
|
||||
const transferAmount = 'transfer-amount';
|
||||
const transferForm = 'transfer-form';
|
||||
const transferFee = 'transfer-fee';
|
||||
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();
|
||||
|
||||
cy.visit('/');
|
||||
cy.getByTestId(manageVegaWallet).click();
|
||||
cy.getByTestId(walletTransfer).click();
|
||||
|
||||
cy.wait('@Assets');
|
||||
cy.wait('@Accounts');
|
||||
|
||||
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 });
|
||||
|
||||
/// 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 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');
|
||||
});
|
||||
|
||||
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(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');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -4,118 +4,16 @@ const amountField = 'input[name="amount"]';
|
||||
const transferText = 'transfer-intro-text';
|
||||
const errorText = 'input-error-text';
|
||||
const formFieldError = 'input-error-text';
|
||||
const includeTransferFeeRadioBtn = 'include-transfer-fee';
|
||||
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
|
||||
const manageVegaWallet = 'manage-vega-wallet';
|
||||
const submitTransferBtn = '[type="submit"]';
|
||||
const toAddressField = '[name="toAddress"]';
|
||||
const totalTransferfee = 'total-transfer-fee';
|
||||
const transferAmount = 'transfer-amount';
|
||||
const transferForm = 'transfer-form';
|
||||
const transferFee = 'transfer-fee';
|
||||
const walletTransfer = 'wallet-transfer';
|
||||
|
||||
const ASSET_EURO = 1;
|
||||
const ASSET_SEPOLIA_TBTC = 2;
|
||||
|
||||
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.wait('@Assets');
|
||||
cy.wait('@Accounts');
|
||||
|
||||
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 });
|
||||
|
||||
/// 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 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');
|
||||
});
|
||||
|
||||
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(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');
|
||||
});
|
||||
});
|
||||
|
||||
describe(
|
||||
'transfer form validation',
|
||||
{ tags: '@regression', testIsolation: true },
|
||||
@@ -132,6 +30,8 @@ describe(
|
||||
|
||||
cy.wait('@Accounts');
|
||||
cy.wait('@Assets');
|
||||
|
||||
cy.mockVegaWalletTransaction();
|
||||
});
|
||||
|
||||
it('transfer Text', () => {
|
||||
|
||||
@@ -44,16 +44,16 @@ const MainGrid = memo(
|
||||
|
||||
return (
|
||||
<ResizableGrid vertical onChange={handleOnLayoutChange}>
|
||||
<ResizableGridPanel minSize={75} priority={LayoutPriority.High}>
|
||||
<ResizableGrid
|
||||
proportionalLayout={false}
|
||||
minSize={200}
|
||||
onChange={handleOnMiddleLayoutChange}
|
||||
>
|
||||
<ResizableGridPanel
|
||||
preferredSize={sizes[0]}
|
||||
priority={LayoutPriority.High}
|
||||
minSize={200}
|
||||
>
|
||||
<ResizableGrid onChange={handleOnMiddleLayoutChange}>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.High}
|
||||
minSize={200}
|
||||
preferredSize={sizesMiddle[1] || '50%'}
|
||||
preferredSize={sizesMiddle[0] || '75%'}
|
||||
>
|
||||
<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,27 +1,15 @@
|
||||
import { OrderbookManager } from '@vegaprotocol/market-depth';
|
||||
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
||||
import { ViewType, useSidebar } from '../sidebar';
|
||||
import { useStopOrderFormValues } from '@vegaprotocol/deal-ticket';
|
||||
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
|
||||
|
||||
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
||||
const useOrderStoreRef = useCreateOrderStore();
|
||||
const updateOrder = useOrderStoreRef((store) => store.update);
|
||||
const updateStoredFormValues = useStopOrderFormValues(
|
||||
(state) => state.update
|
||||
);
|
||||
const update = useDealTicketFormValues((state) => state.updateAll);
|
||||
const setView = useSidebar((store) => store.setView);
|
||||
return (
|
||||
<OrderbookManager
|
||||
marketId={marketId}
|
||||
onClick={({ price, size }) => {
|
||||
if (price) {
|
||||
updateOrder(marketId, { price });
|
||||
updateStoredFormValues(marketId, { price });
|
||||
}
|
||||
if (size) {
|
||||
updateOrder(marketId, { size });
|
||||
updateStoredFormValues(marketId, { size });
|
||||
}
|
||||
onClick={(values) => {
|
||||
update(marketId, values);
|
||||
setView({ type: ViewType.Order });
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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,7 +16,6 @@ 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';
|
||||
|
||||
@@ -40,7 +39,7 @@ export const GetStarted = ({ lead }: Props) => {
|
||||
{ 'mt-8': !lead }
|
||||
);
|
||||
|
||||
if (!isBrowserWalletInstalled()) {
|
||||
if (!pubKey && !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', ...(await utils.getProjects(ctx))],
|
||||
['ci', 'docs', 'specs', ...(await utils.getProjects(ctx))],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ const singleRow = {
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'BTCUSD Monthly (30 Jun 2022)',
|
||||
code: 'BTCUSD.MF21',
|
||||
},
|
||||
},
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
@@ -57,7 +58,7 @@ describe('BreakdownTable', () => {
|
||||
});
|
||||
const cells = await screen.findAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'BTCUSD Monthly (30 Jun 2022)',
|
||||
'BTCUSD.MF21',
|
||||
'Margin',
|
||||
'1,256.00 (50%)',
|
||||
'1,256.00',
|
||||
@@ -118,6 +119,7 @@ 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.name',
|
||||
field: 'market.tradableInstrument.instrument.code',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
AccountFields,
|
||||
'market.tradableInstrument.instrument.name'
|
||||
'market.tradableInstrument.instrument.code'
|
||||
>) => {
|
||||
if (!value) return 'None';
|
||||
return value;
|
||||
|
||||
@@ -28,10 +28,10 @@ export const assetProvider = makeDataProvider<
|
||||
getData,
|
||||
});
|
||||
|
||||
export const useAssetDataProvider = (assetId: string) => {
|
||||
export const useAssetDataProvider = (assetId: string, skip?: boolean) => {
|
||||
return useDataProvider({
|
||||
dataProvider: assetProvider,
|
||||
variables: { assetId: assetId || '' },
|
||||
skip: !assetId,
|
||||
skip: !assetId || skip,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,9 +4,25 @@ export default {
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
|
||||
'^.+\\.[tj]sx?$': [
|
||||
'babel-jest',
|
||||
{
|
||||
presets: ['@nx/react/babel'],
|
||||
// required for pennant to work in jest, due to having untranspiled exports
|
||||
plugins: [
|
||||
[
|
||||
'@babel/plugin-proposal-private-methods',
|
||||
{
|
||||
loose: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/candles-chart',
|
||||
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||
// dont ignore pennant from transpilation
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/pennant'],
|
||||
};
|
||||
|
||||
@@ -3,29 +3,25 @@ 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 { OrderObj } from '@vegaprotocol/orders';
|
||||
import type { OrderFormFields } from '../../hooks/use-order-form';
|
||||
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
|
||||
export interface DealTicketAmountProps {
|
||||
control: Control<OrderFormFields>;
|
||||
orderType: Schema.OrderType;
|
||||
control: Control<OrderFormValues>;
|
||||
type: Schema.OrderType;
|
||||
marketData: StaticMarketData;
|
||||
marketPrice?: string;
|
||||
market: Market;
|
||||
sizeError?: string;
|
||||
priceError?: string;
|
||||
update: (obj: Partial<OrderObj>) => void;
|
||||
size: string;
|
||||
price?: string;
|
||||
}
|
||||
|
||||
export const DealTicketAmount = ({
|
||||
orderType,
|
||||
type,
|
||||
marketData,
|
||||
marketPrice,
|
||||
...props
|
||||
}: DealTicketAmountProps) => {
|
||||
switch (orderType) {
|
||||
switch (type) {
|
||||
case Schema.OrderType.TYPE_MARKET:
|
||||
return (
|
||||
<DealTicketMarketAmount
|
||||
@@ -37,7 +33,7 @@ export const DealTicketAmount = ({
|
||||
case Schema.OrderType.TYPE_LIMIT:
|
||||
return <DealTicketLimitAmount {...props} />;
|
||||
default: {
|
||||
throw new Error('Invalid ticket type');
|
||||
throw new Error('Invalid ticket type ' + type);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
DealTicketType,
|
||||
useDealTicketTypeStore,
|
||||
} from '../../hooks/use-type-store';
|
||||
isStopOrderType,
|
||||
useDealTicketFormValues,
|
||||
} from '../../hooks/use-form-values';
|
||||
import { StopOrder } from './deal-ticket-stop-order';
|
||||
import {
|
||||
useStaticMarketData,
|
||||
@@ -25,7 +25,9 @@ export const DealTicketContainer = ({
|
||||
marketId,
|
||||
...props
|
||||
}: DealTicketContainerProps) => {
|
||||
const type = useDealTicketTypeStore((state) => state.type[marketId]);
|
||||
const showStopOrder = useDealTicketFormValues((state) =>
|
||||
isStopOrderType(state.formValues[marketId]?.type)
|
||||
);
|
||||
const {
|
||||
data: market,
|
||||
error: marketError,
|
||||
@@ -48,9 +50,7 @@ export const DealTicketContainer = ({
|
||||
reload={reload}
|
||||
>
|
||||
{market && marketData ? (
|
||||
FLAGS.STOP_ORDERS &&
|
||||
(type === DealTicketType.StopLimit ||
|
||||
type === DealTicketType.StopMarket) ? (
|
||||
FLAGS.STOP_ORDERS && showStopOrder ? (
|
||||
<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<
|
||||
Omit<DealTicketAmountProps, 'marketData'>,
|
||||
'orderType'
|
||||
DealTicketAmountProps,
|
||||
'marketData' | 'type'
|
||||
>;
|
||||
|
||||
export const DealTicketLimitAmount = ({
|
||||
@@ -14,9 +14,6 @@ export const DealTicketLimitAmount = ({
|
||||
market,
|
||||
sizeError,
|
||||
priceError,
|
||||
update,
|
||||
price,
|
||||
size,
|
||||
}: DealTicketLimitAmountProps) => {
|
||||
const priceStep = toDecimal(market?.decimalPlaces);
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
@@ -62,17 +59,16 @@ export const DealTicketLimitAmount = ({
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -95,19 +91,17 @@ 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={() => (
|
||||
render={({ field }) => (
|
||||
<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,10 +10,7 @@ import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export type DealTicketMarketAmountProps = Omit<
|
||||
DealTicketAmountProps,
|
||||
'orderType'
|
||||
>;
|
||||
export type DealTicketMarketAmountProps = Omit<DealTicketAmountProps, 'type'>;
|
||||
|
||||
export const DealTicketMarketAmount = ({
|
||||
control,
|
||||
@@ -21,8 +18,6 @@ export const DealTicketMarketAmount = ({
|
||||
marketData,
|
||||
marketPrice,
|
||||
sizeError,
|
||||
update,
|
||||
size,
|
||||
}: DealTicketMarketAmountProps) => {
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
@@ -50,17 +45,16 @@ export const DealTicketMarketAmount = ({
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<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,7 +1,6 @@
|
||||
import { Controller, type Control } from 'react-hook-form';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { OrderObj } from '@vegaprotocol/orders';
|
||||
import type { OrderFormFields } from '../../hooks/use-order-form';
|
||||
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
@@ -12,25 +11,21 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export interface DealTicketSizeIcebergProps {
|
||||
control: Control<OrderFormFields>;
|
||||
control: Control<OrderFormValues>;
|
||||
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,
|
||||
peakSize,
|
||||
minimumVisibleSize,
|
||||
size,
|
||||
peakSize,
|
||||
}: DealTicketSizeIcebergProps) => {
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
|
||||
@@ -80,7 +75,7 @@ export const DealTicketSizeIceberg = ({
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="icebergOpts.peakSize"
|
||||
name="peakSize"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a peak size'),
|
||||
@@ -97,25 +92,17 @@ export const DealTicketSizeIceberg = ({
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'peakSize'),
|
||||
}}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -144,7 +131,7 @@ export const DealTicketSizeIceberg = ({
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="icebergOpts.minimumVisibleSize"
|
||||
name="minimumVisibleSize"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a minimum visible size'),
|
||||
@@ -154,7 +141,7 @@ export const DealTicketSizeIceberg = ({
|
||||
'Minimum visible size cannot be lower than ' + sizeStep
|
||||
),
|
||||
},
|
||||
max: {
|
||||
max: peakSize && {
|
||||
value: peakSize,
|
||||
message: t(
|
||||
'Minimum visible size cannot be greater than the peak size (%s)',
|
||||
@@ -163,25 +150,17 @@ export const DealTicketSizeIceberg = ({
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'minimumVisibleSize'),
|
||||
}}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<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,8 +6,11 @@ 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-stop-order-form-values';
|
||||
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||
import type { StopOrderFormValues } from '../../hooks/use-form-values';
|
||||
import {
|
||||
DealTicketType,
|
||||
useDealTicketFormValues,
|
||||
} from '../../hooks/use-form-values';
|
||||
import type { FeatureFlags } from '@vegaprotocol/environment';
|
||||
|
||||
jest.mock('zustand');
|
||||
@@ -131,9 +134,11 @@ describe('StopOrder', () => {
|
||||
expiresAt: '2023-07-27T16:43:27.000',
|
||||
};
|
||||
|
||||
useStopOrderFormValues.setState({
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[market.id]: values,
|
||||
[market.id]: {
|
||||
[DealTicketType.StopLimit]: values,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -207,11 +212,13 @@ 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,4 +1,3 @@
|
||||
import type { FormEventHandler } from 'react';
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
|
||||
@@ -8,7 +7,7 @@ import {
|
||||
toDecimal,
|
||||
validateAmount,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { useForm, Controller, useController } from 'react-hook-form';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
Radio,
|
||||
@@ -24,22 +23,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, useOrder } from '@vegaprotocol/orders';
|
||||
import { timeInForceLabel } from '@vegaprotocol/orders';
|
||||
import {
|
||||
NoWalletWarning,
|
||||
REDUCE_ONLY_TOOLTIP,
|
||||
useNotionalSize,
|
||||
stopSubmit,
|
||||
getNotionalSize,
|
||||
} from './deal-ticket';
|
||||
import { TypeToggle } from './type-selector';
|
||||
import {
|
||||
useStopOrderFormValues,
|
||||
type StopOrderFormValues,
|
||||
} from '../../hooks/use-stop-order-form-values';
|
||||
import {
|
||||
useDealTicketFormValues,
|
||||
DealTicketType,
|
||||
useDealTicketTypeStore,
|
||||
} from '../../hooks/use-type-store';
|
||||
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-stop-order-submission';
|
||||
type StopOrderFormValues,
|
||||
dealTicketTypeToOrderType,
|
||||
isStopOrderType,
|
||||
} from '../../hooks/use-form-values';
|
||||
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||
import { validateExpiration } from '../../utils';
|
||||
@@ -50,32 +49,36 @@ export interface StopOrderProps {
|
||||
submit: (order: StopOrdersSubmission) => void;
|
||||
}
|
||||
|
||||
const defaultValues: Partial<StopOrderFormValues> = {
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
const getDefaultValues = (
|
||||
type: Schema.OrderType,
|
||||
storedValues?: Partial<StopOrderFormValues>
|
||||
): StopOrderFormValues => ({
|
||||
type,
|
||||
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',
|
||||
};
|
||||
|
||||
const stopSubmit: FormEventHandler = (e) => e.preventDefault();
|
||||
...storedValues,
|
||||
});
|
||||
|
||||
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
||||
const [, updateOrder] = useOrder(market.id);
|
||||
const updateStoredFormValues = useStopOrderFormValues(
|
||||
(state) => state.update
|
||||
const setType = useDealTicketFormValues((state) => state.setType);
|
||||
const updateStoredFormValues = useDealTicketFormValues(
|
||||
(state) => state.updateStopOrder
|
||||
);
|
||||
const storedFormValues = useStopOrderFormValues(
|
||||
const storedFormValues = useDealTicketFormValues(
|
||||
(state) => state.formValues[market.id]
|
||||
);
|
||||
const { handleSubmit, setValue, watch, control, formState } =
|
||||
const dealTicketType = storedFormValues?.type ?? DealTicketType.StopLimit;
|
||||
const type = dealTicketTypeToOrderType(dealTicketType);
|
||||
const { handleSubmit, setValue, watch, control, formState, reset } =
|
||||
useForm<StopOrderFormValues>({
|
||||
defaultValues: { ...defaultValues, ...storedFormValues },
|
||||
defaultValues: getDefaultValues(type, storedFormValues?.[dealTicketType]),
|
||||
});
|
||||
const { errors } = formState;
|
||||
const lastSubmitTime = useRef(0);
|
||||
@@ -102,16 +105,22 @@ 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');
|
||||
|
||||
if (storedFormValues?.size && rawSize !== storedFormValues?.size) {
|
||||
setValue('size', storedFormValues.size);
|
||||
}
|
||||
if (storedFormValues?.price && rawPrice !== storedFormValues?.price) {
|
||||
setValue('price', storedFormValues.price);
|
||||
}
|
||||
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]);
|
||||
|
||||
const isPriceTrigger = triggerType === 'price';
|
||||
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
|
||||
@@ -127,7 +136,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
: marketPrice
|
||||
);
|
||||
|
||||
const notionalSize = useNotionalSize(
|
||||
const notionalSize = getNotionalSize(
|
||||
price,
|
||||
size,
|
||||
market.decimalPlaces,
|
||||
@@ -153,47 +162,28 @@ 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
|
||||
>
|
||||
<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
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
<TypeToggle
|
||||
value={dealTicketType}
|
||||
onValueChange={(dealTicketType) => {
|
||||
setType(market.id, dealTicketType);
|
||||
if (isStopOrderType(dealTicketType)) {
|
||||
reset(
|
||||
getDefaultValues(
|
||||
dealTicketTypeToOrderType(dealTicketType),
|
||||
storedFormValues?.[dealTicketType]
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{errors.type && (
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
act,
|
||||
render,
|
||||
renderHook,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { generateMarket, generateMarketData } from '../../test-helpers';
|
||||
import { DealTicket } from './deal-ticket';
|
||||
@@ -15,7 +9,10 @@ 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 { useCreateOrderStore } from '@vegaprotocol/orders';
|
||||
import {
|
||||
DealTicketType,
|
||||
useDealTicketFormValues,
|
||||
} from '../../hooks/use-form-values';
|
||||
import * as positionsTools from '@vegaprotocol/positions';
|
||||
import { OrdersDocument } from '@vegaprotocol/orders';
|
||||
|
||||
@@ -50,9 +47,6 @@ function generateJsx(mocks: MockedResponse[] = []) {
|
||||
}
|
||||
|
||||
describe('DealTicket', () => {
|
||||
const { result } = renderHook(() => useCreateOrderStore());
|
||||
const useOrderStore = result.current;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
localStorage.clear();
|
||||
@@ -166,9 +160,11 @@ describe('DealTicket', () => {
|
||||
persist: true,
|
||||
};
|
||||
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[expectedOrder.marketId]: {
|
||||
[DealTicketType.Limit]: expectedOrder,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -204,9 +200,11 @@ describe('DealTicket', () => {
|
||||
reduceOnly: true,
|
||||
postOnly: false,
|
||||
};
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[expectedOrder.marketId]: {
|
||||
[DealTicketType.Limit]: expectedOrder,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -247,9 +245,11 @@ describe('DealTicket', () => {
|
||||
postOnly: true,
|
||||
};
|
||||
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[expectedOrder.marketId]: {
|
||||
[DealTicketType.Limit]: expectedOrder,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -295,9 +295,11 @@ describe('DealTicket', () => {
|
||||
},
|
||||
};
|
||||
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[expectedOrder.marketId]: {
|
||||
[DealTicketType.Limit]: expectedOrder,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -339,9 +341,11 @@ describe('DealTicket', () => {
|
||||
reduceOnly: false,
|
||||
postOnly: false,
|
||||
};
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
useDealTicketFormValues.setState({
|
||||
formValues: {
|
||||
[expectedOrder.marketId]: {
|
||||
[DealTicketType.Limit]: expectedOrder,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -370,6 +374,7 @@ 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,7 +1,8 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import type { FormEventHandler } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { Controller, useController, useForm } from 'react-hook-form';
|
||||
import { DealTicketAmount } from './deal-ticket-amount';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
import {
|
||||
@@ -13,7 +14,8 @@ import { SideSelector } from './side-selector';
|
||||
import { TimeInForceSelector } from './time-in-force-selector';
|
||||
import { TypeSelector } from './type-selector';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { normalizeOrderSubmission, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
|
||||
import {
|
||||
Checkbox,
|
||||
InputError,
|
||||
@@ -51,14 +53,15 @@ import {
|
||||
useAccountBalance,
|
||||
} from '@vegaprotocol/accounts';
|
||||
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import { useOrderForm } from '../../hooks/use-order-form';
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import {
|
||||
DealTicketType,
|
||||
useDealTicketTypeStore,
|
||||
} from '../../hooks/use-type-store';
|
||||
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||
dealTicketTypeToOrderType,
|
||||
isStopOrderType,
|
||||
} from '../../hooks/use-form-values';
|
||||
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
import { useDealTicketFormValues } from '../../hooks/use-form-values';
|
||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||
import noop from 'lodash/noop';
|
||||
|
||||
@@ -75,23 +78,42 @@ export interface DealTicketProps {
|
||||
onDeposit: (assetId: string) => void;
|
||||
}
|
||||
|
||||
export const useNotionalSize = (
|
||||
export const getNotionalSize = (
|
||||
price: string | null | undefined,
|
||||
size: string | undefined,
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number
|
||||
) =>
|
||||
useMemo(() => {
|
||||
if (price && size) {
|
||||
return removeDecimal(
|
||||
toBigNum(size, positionDecimalPlaces).multipliedBy(
|
||||
toBigNum(price, decimalPlaces)
|
||||
),
|
||||
decimalPlaces
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [price, size, decimalPlaces, positionDecimalPlaces]);
|
||||
) => {
|
||||
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,
|
||||
});
|
||||
|
||||
export const DealTicket = ({
|
||||
market,
|
||||
@@ -103,32 +125,29 @@ export const DealTicket = ({
|
||||
onDeposit,
|
||||
}: DealTicketProps) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
||||
const updateStopOrderFormValues = useStopOrderFormValues(
|
||||
(state) => state.update
|
||||
const setType = useDealTicketFormValues((state) => state.setType);
|
||||
const storedFormValues = useDealTicketFormValues(
|
||||
(state) => state.formValues[market.id]
|
||||
);
|
||||
// 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 updateStoredFormValues = useDealTicketFormValues(
|
||||
(state) => state.updateOrder
|
||||
);
|
||||
const dealTicketType = storedFormValues?.type ?? DealTicketType.Limit;
|
||||
const type = dealTicketTypeToOrderType(dealTicketType);
|
||||
|
||||
const {
|
||||
control,
|
||||
errors,
|
||||
order,
|
||||
setError,
|
||||
clearErrors,
|
||||
update,
|
||||
reset,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
} = useOrderForm(market.id);
|
||||
|
||||
setValue,
|
||||
watch,
|
||||
} = useForm<OrderFormValues>({
|
||||
defaultValues: getDefaultValues(type, storedFormValues?.[dealTicketType]),
|
||||
});
|
||||
const lastSubmitTime = useRef(0);
|
||||
|
||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
|
||||
const {
|
||||
accountBalance: marginAccountBalance,
|
||||
loading: loadingMarginAccountBalance,
|
||||
@@ -144,24 +163,54 @@ export const DealTicket = ({
|
||||
).toString();
|
||||
|
||||
const { marketState, marketTradingMode } = marketData;
|
||||
const timeInForce = watch('timeInForce');
|
||||
|
||||
const normalizedOrder =
|
||||
order &&
|
||||
normalizeOrderSubmission(
|
||||
order,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
const side = watch('side');
|
||||
const rawSize = watch('size');
|
||||
const rawPrice = watch('price');
|
||||
const iceberg = watch('iceberg');
|
||||
const peakSize = watch('peakSize');
|
||||
|
||||
const price = useMemo(() => {
|
||||
return (
|
||||
normalizedOrder &&
|
||||
marketPrice &&
|
||||
getDerivedPrice(normalizedOrder, marketPrice)
|
||||
);
|
||||
}, [normalizedOrder, marketPrice]);
|
||||
useEffect(() => {
|
||||
const size = storedFormValues?.[dealTicketType]?.size;
|
||||
if (size && rawSize !== size) {
|
||||
setValue('size', size);
|
||||
}
|
||||
}, [storedFormValues, dealTicketType, rawSize, setValue]);
|
||||
|
||||
const notionalSize = useNotionalSize(
|
||||
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(
|
||||
price,
|
||||
normalizedOrder?.size,
|
||||
market.decimalPlaces,
|
||||
@@ -205,22 +254,20 @@ export const DealTicket = ({
|
||||
const assetSymbol =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
|
||||
useEffect(() => {
|
||||
const summaryError = useMemo(() => {
|
||||
if (!pubKey) {
|
||||
setError('summary', {
|
||||
return {
|
||||
message: t('No public key selected'),
|
||||
type: SummaryValidationType.NoPubKey,
|
||||
});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
const marketStateError = validateMarketState(marketState);
|
||||
if (marketStateError !== true) {
|
||||
setError('summary', {
|
||||
return {
|
||||
message: marketStateError,
|
||||
type: SummaryValidationType.MarketState,
|
||||
});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
const hasNoBalance =
|
||||
@@ -229,24 +276,21 @@ export const DealTicket = ({
|
||||
hasNoBalance &&
|
||||
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
|
||||
) {
|
||||
setError('summary', {
|
||||
return {
|
||||
message: SummaryValidationType.NoCollateral,
|
||||
type: SummaryValidationType.NoCollateral,
|
||||
});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
const marketTradingModeError = validateMarketTradingMode(marketTradingMode);
|
||||
if (marketTradingModeError !== true) {
|
||||
setError('summary', {
|
||||
return {
|
||||
message: marketTradingModeError,
|
||||
type: SummaryValidationType.TradingMode,
|
||||
});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
// No error found above clear the error in case it was active on a previous render
|
||||
clearErrors('summary');
|
||||
return undefined;
|
||||
}, [
|
||||
marketState,
|
||||
marketTradingMode,
|
||||
@@ -255,156 +299,83 @@ export const DealTicket = ({
|
||||
loadingMarginAccountBalance,
|
||||
loadingGeneralAccountBalance,
|
||||
pubKey,
|
||||
setError,
|
||||
clearErrors,
|
||||
]);
|
||||
|
||||
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 disablePostOnlyCheckbox = [
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
].includes(timeInForce);
|
||||
|
||||
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 disableReduceOnlyCheckbox = !disablePostOnlyCheckbox;
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(order: OrderSubmission) => {
|
||||
(formValues: OrderFormValues) => {
|
||||
const now = new Date().getTime();
|
||||
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
|
||||
return;
|
||||
}
|
||||
submit(
|
||||
normalizeOrderSubmission(
|
||||
order,
|
||||
mapFormValuesToOrderSubmission(
|
||||
formValues,
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
);
|
||||
lastSubmitTime.current = now;
|
||||
},
|
||||
[submit, market.decimalPlaces, market.positionDecimalPlaces]
|
||||
[submit, market.decimalPlaces, market.positionDecimalPlaces, market.id]
|
||||
);
|
||||
|
||||
// if an order doesn't exist one will be created by the store immediately
|
||||
if (!order || !normalizedOrder) {
|
||||
return null;
|
||||
}
|
||||
useController({
|
||||
name: 'type',
|
||||
control,
|
||||
rules: {
|
||||
validate: validateType(marketData.marketTradingMode, marketData.trigger),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={isReadOnly ? noop : handleSubmit(onSubmit)}
|
||||
onSubmit={
|
||||
isReadOnly || !pubKey
|
||||
? stopSubmit
|
||||
: handleSubmit(summaryError ? noop : onSubmit)
|
||||
}
|
||||
noValidate
|
||||
data-testid="deal-ticket-form"
|
||||
>
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: validateType(
|
||||
marketData.marketTradingMode,
|
||||
marketData.trigger
|
||||
),
|
||||
<TypeSelector
|
||||
value={dealTicketType}
|
||||
onValueChange={(dealTicketType) => {
|
||||
setType(market.id, dealTicketType);
|
||||
if (!isStopOrderType(dealTicketType)) {
|
||||
reset(
|
||||
getDefaultValues(
|
||||
dealTicketTypeToOrderType(dealTicketType),
|
||||
storedFormValues?.[dealTicketType]
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
errorMessage={errors.type?.message}
|
||||
/>
|
||||
<Controller
|
||||
name="side"
|
||||
control={control}
|
||||
render={() => (
|
||||
<SideSelector
|
||||
value={order.side}
|
||||
onValueChange={(side) => {
|
||||
update({ side });
|
||||
}}
|
||||
/>
|
||||
render={({ field }) => (
|
||||
<SideSelector value={field.value} onValueChange={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
<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"
|
||||
@@ -415,58 +386,29 @@ export const DealTicket = ({
|
||||
marketData.trigger
|
||||
),
|
||||
}}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<TimeInForceSelector
|
||||
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');
|
||||
}}
|
||||
value={field.value}
|
||||
orderType={type}
|
||||
onSelect={field.onChange}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
errorMessage={errors.timeInForce?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{order.type === Schema.OrderType.TYPE_LIMIT &&
|
||||
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
|
||||
{type === Schema.OrderType.TYPE_LIMIT &&
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
|
||||
<Controller
|
||||
name="expiresAt"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: validateExpiration,
|
||||
}}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<ExpirySelector
|
||||
value={order.expiresAt}
|
||||
onSelect={(expiresAt) =>
|
||||
update({
|
||||
expiresAt: expiresAt || undefined,
|
||||
})
|
||||
}
|
||||
value={field.value}
|
||||
onSelect={(expiresAt) => field.onChange(expiresAt)}
|
||||
errorMessage={errors.expiresAt?.message}
|
||||
/>
|
||||
)}
|
||||
@@ -476,13 +418,14 @@ export const DealTicket = ({
|
||||
<Controller
|
||||
name="postOnly"
|
||||
control={control}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
name="post-only"
|
||||
checked={order.postOnly}
|
||||
checked={!disablePostOnlyCheckbox && field.value}
|
||||
disabled={disablePostOnlyCheckbox}
|
||||
onCheckedChange={() => {
|
||||
update({ postOnly: !order.postOnly, reduceOnly: false });
|
||||
onCheckedChange={(postOnly) => {
|
||||
field.onChange(postOnly);
|
||||
setValue('reduceOnly', false);
|
||||
}}
|
||||
label={
|
||||
<Tooltip
|
||||
@@ -507,13 +450,14 @@ export const DealTicket = ({
|
||||
<Controller
|
||||
name="reduceOnly"
|
||||
control={control}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
name="reduce-only"
|
||||
checked={order.reduceOnly}
|
||||
checked={!disableReduceOnlyCheckbox && field.value}
|
||||
disabled={disableReduceOnlyCheckbox}
|
||||
onCheckedChange={() => {
|
||||
update({ postOnly: false, reduceOnly: !order.reduceOnly });
|
||||
onCheckedChange={(reduceOnly) => {
|
||||
field.onChange(reduceOnly);
|
||||
setValue('postOnly', false);
|
||||
}}
|
||||
label={
|
||||
<Tooltip
|
||||
@@ -534,53 +478,49 @@ export const DealTicket = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<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.
|
||||
{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.
|
||||
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>
|
||||
{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 || ''}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<SummaryMessage
|
||||
errorMessage={errors.summary?.message}
|
||||
error={summaryError}
|
||||
asset={asset}
|
||||
marketTradingMode={marketData.marketTradingMode}
|
||||
balance={balance}
|
||||
@@ -593,7 +533,7 @@ export const DealTicket = ({
|
||||
onClickCollateral={onClickCollateral}
|
||||
onDeposit={onDeposit}
|
||||
/>
|
||||
<DealTicketButton side={order.side} />
|
||||
<DealTicketButton side={side} />
|
||||
<DealTicketFeeDetails
|
||||
order={
|
||||
normalizedOrder && { ...normalizedOrder, price: price || undefined }
|
||||
@@ -619,7 +559,7 @@ export const DealTicket = ({
|
||||
* renders warnings about current state of the market
|
||||
*/
|
||||
interface SummaryMessageProps {
|
||||
errorMessage?: string;
|
||||
error?: { message: string; type: string };
|
||||
asset: { id: string; symbol: string; name: string; decimals: number };
|
||||
marketTradingMode: MarketData['marketTradingMode'];
|
||||
balance: string;
|
||||
@@ -649,7 +589,7 @@ export const NoWalletWarning = ({
|
||||
|
||||
const SummaryMessage = memo(
|
||||
({
|
||||
errorMessage,
|
||||
error,
|
||||
asset,
|
||||
marketTradingMode,
|
||||
balance,
|
||||
@@ -665,7 +605,7 @@ const SummaryMessage = memo(
|
||||
return <NoWalletWarning isReadOnly={isReadOnly} />;
|
||||
}
|
||||
|
||||
if (errorMessage === SummaryValidationType.NoCollateral) {
|
||||
if (error?.type === SummaryValidationType.NoCollateral) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<ZeroBalanceError
|
||||
@@ -679,11 +619,11 @@ const SummaryMessage = memo(
|
||||
|
||||
// If we have any other full error which prevents
|
||||
// submission render that first
|
||||
if (errorMessage) {
|
||||
if (error?.message) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<InputError testId="deal-ticket-error-message-summary">
|
||||
{errorMessage}
|
||||
{error?.message}
|
||||
</InputError>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,13 @@ 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,
|
||||
@@ -31,12 +38,8 @@ export const TimeInForceSelector = ({
|
||||
}: TimeInForceSelectorProps) => {
|
||||
const options =
|
||||
orderType === Schema.OrderType.TYPE_LIMIT
|
||||
? Object.entries(Schema.OrderTimeInForce)
|
||||
: Object.entries(Schema.OrderTimeInForce).filter(
|
||||
([_, timeInForce]) =>
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
? typeLimitOptions
|
||||
: typeMarketOptions;
|
||||
|
||||
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-type-store';
|
||||
import { DealTicketType } from '../../hooks/use-form-values';
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||
import classNames from 'classnames';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './__generated__/EstimateOrder';
|
||||
export * from './use-estimate-fees';
|
||||
export * from './use-type-store';
|
||||
export * from './use-stop-order-form-values';
|
||||
export * from './use-form-values';
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
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',
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1,68 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
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',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -1,28 +0,0 @@
|
||||
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',
|
||||
}
|
||||
)
|
||||
);
|
||||
+60
-5
@@ -1,12 +1,64 @@
|
||||
import type {
|
||||
OrderSubmission,
|
||||
StopOrderSetup,
|
||||
StopOrdersSubmission,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
|
||||
import type { StopOrderFormValues } from '../hooks/use-stop-order-form-values';
|
||||
import type {
|
||||
OrderFormValues,
|
||||
StopOrderFormValues,
|
||||
} from '../hooks/use-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,
|
||||
@@ -15,9 +67,8 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
): StopOrdersSubmission => {
|
||||
const submission: StopOrdersSubmission = {};
|
||||
const stopOrderSetup: StopOrderSetup = {
|
||||
orderSubmission: normalizeOrderSubmission(
|
||||
orderSubmission: mapFormValuesToOrderSubmission(
|
||||
{
|
||||
marketId,
|
||||
type: data.type,
|
||||
side: data.side,
|
||||
size: data.size,
|
||||
@@ -25,12 +76,16 @@ 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
|
||||
@@ -0,0 +1,64 @@
|
||||
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: {
|
||||
name: 'test market',
|
||||
code: '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.name || '',
|
||||
buyerFill.market?.tradableInstrument.instrument.code || '',
|
||||
'+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.name || '',
|
||||
buyerFill.market?.tradableInstrument.instrument.code || '',
|
||||
'-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.name || '',
|
||||
buyerFill.market?.tradableInstrument.instrument.code || '',
|
||||
'-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.name',
|
||||
field: 'market.tradableInstrument.instrument.code',
|
||||
cellRenderer: 'MarketNameCell',
|
||||
cellRendererParams: { idPath: 'market.id', onMarketClick },
|
||||
},
|
||||
|
||||
@@ -4,9 +4,25 @@ export default {
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
|
||||
'^.+\\.[tj]sx?$': [
|
||||
'babel-jest',
|
||||
{
|
||||
presets: ['@nx/react/babel'],
|
||||
// required for pennant to work in jest, due to having untranspiled exports
|
||||
plugins: [
|
||||
[
|
||||
'@babel/plugin-proposal-private-methods',
|
||||
{
|
||||
loose: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/market-depth',
|
||||
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||
// dont ignore pennant from transpilation
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/pennant'],
|
||||
};
|
||||
|
||||
@@ -34,5 +34,8 @@ query SuccessorMarket($marketId: ID!) {
|
||||
code
|
||||
}
|
||||
}
|
||||
proposal {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -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 } } } | 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 } }, proposal?: { __typename?: 'Proposal', id?: string | null } | null } | null };
|
||||
|
||||
|
||||
export const SuccessorMarketIdDocument = gql`
|
||||
@@ -153,6 +153,9 @@ export const SuccessorMarketDocument = gql`
|
||||
code
|
||||
}
|
||||
}
|
||||
proposal {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
Intent,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Lozenge,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import BigNumber from 'bignumber.js';
|
||||
@@ -22,9 +24,11 @@ interface RowProps {
|
||||
unformatted?: boolean;
|
||||
assetSymbol?: string;
|
||||
noBorder?: boolean;
|
||||
parentValue?: ReactNode;
|
||||
hasParentData?: boolean;
|
||||
}
|
||||
|
||||
const Row = ({
|
||||
export const Row = ({
|
||||
field,
|
||||
value,
|
||||
decimalPlaces,
|
||||
@@ -32,7 +36,14 @@ 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) => {
|
||||
@@ -55,6 +66,10 @@ const Row = ({
|
||||
const formattedValue = getFormattedValue(value);
|
||||
|
||||
if (!formattedValue) return null;
|
||||
|
||||
const newValueInSuccessorMarket = hasParentData && value && !parentValue;
|
||||
const valueDiffersFromParentMarket = parentValue && parentValue !== value;
|
||||
|
||||
return (
|
||||
<KeyValueTableRow
|
||||
key={field}
|
||||
@@ -63,10 +78,35 @@ const Row = ({
|
||||
dtClassName={className}
|
||||
ddClassName={className}
|
||||
>
|
||||
<Tooltip description={tooltipMapping[field]} align="start">
|
||||
<div tabIndex={-1}>{startCase(t(field))}</div>
|
||||
</Tooltip>
|
||||
<span style={{ wordBreak: 'break-word' }}>{formattedValue}</span>
|
||||
<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>
|
||||
</KeyValueTableRow>
|
||||
);
|
||||
};
|
||||
@@ -79,6 +119,7 @@ export interface MarketInfoTableProps {
|
||||
children?: ReactNode;
|
||||
assetSymbol?: string;
|
||||
noBorder?: boolean;
|
||||
parentData?: Record<string, ReactNode> | null | undefined;
|
||||
}
|
||||
|
||||
export const MarketInfoTable = ({
|
||||
@@ -89,25 +130,33 @@ 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}
|
||||
/>
|
||||
))}
|
||||
<>
|
||||
{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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</KeyValueTable>
|
||||
<div className="flex flex-col gap-2">{children}</div>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { TokenStaticLinks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
FLAGS,
|
||||
TokenStaticLinks,
|
||||
useEnvironment,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
@@ -34,6 +38,7 @@ import {
|
||||
RiskModelInfoPanel,
|
||||
RiskParametersInfoPanel,
|
||||
SettlementAssetInfoPanel,
|
||||
SuccessionLineInfoPanel,
|
||||
} from './market-info-panels';
|
||||
import type { DataSourceDefinition } from '@vegaprotocol/types';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
@@ -291,6 +296,13 @@ export const MarketInfoAccordion = ({
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{FLAGS.SUCCESSOR_MARKETS && (
|
||||
<AccordionItem
|
||||
itemId="succession-line"
|
||||
title={t('Succession line')}
|
||||
content={<SuccessionLineInfoPanel market={market} />}
|
||||
/>
|
||||
)}
|
||||
</Accordion>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
ConditionOperator,
|
||||
ConditionOperatorMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { DataSourceProof } from './market-info-panels';
|
||||
import { DataSourceProof, SuccessionLineInfoPanel } from './market-info-panels';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { SuccessorMarketIdsDocument } from '../../__generated__';
|
||||
|
||||
jest.mock('../../hooks/use-oracle-markets', () => ({
|
||||
useOracleMarkets: () => [],
|
||||
@@ -137,3 +139,93 @@ 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,11 +1,18 @@
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Fragment, 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 } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
ExternalLink,
|
||||
Splash,
|
||||
Tooltip,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
@@ -23,17 +30,32 @@ import BigNumber from 'bignumber.js';
|
||||
import type { DataSourceDefinition, SignerKind } from '@vegaprotocol/types';
|
||||
import { ConditionOperatorMapping } from '@vegaprotocol/types';
|
||||
import { MarketTradingModeMapping } from '@vegaprotocol/types';
|
||||
import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
DApp,
|
||||
FLAGS,
|
||||
TOKEN_PROPOSAL,
|
||||
useEnvironment,
|
||||
useLinks,
|
||||
} 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 } from '../../__generated__';
|
||||
import {
|
||||
useParentMarketIdQuery,
|
||||
useSuccessorMarketIdsQuery,
|
||||
useSuccessorMarketQuery,
|
||||
} 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;
|
||||
};
|
||||
|
||||
@@ -138,21 +160,43 @@ export const InsurancePoolInfoPanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
const { data: parentData } = useParentMarketIdQuery({
|
||||
export const KeyDetailsInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => {
|
||||
const { data: parentMarketIdData } = useParentMarketIdQuery({
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
},
|
||||
skip: !FLAGS.SUCCESSOR_MARKETS,
|
||||
});
|
||||
|
||||
const { data: successor } = useSuccessorMarketProposalDetailsQuery({
|
||||
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({
|
||||
variables: {
|
||||
proposalId: market.proposal?.id || '',
|
||||
marketId: parentMarket?.id || '',
|
||||
},
|
||||
skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id,
|
||||
skip: !parentMarket?.id,
|
||||
});
|
||||
|
||||
const { data: parentSuccessorProposalDetails } =
|
||||
useSuccessorMarketProposalDetailsQuery({
|
||||
variables: {
|
||||
proposalId: parentMarket?.proposal?.id || '',
|
||||
},
|
||||
skip: !parentMarket?.proposal?.id,
|
||||
});
|
||||
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
@@ -163,11 +207,12 @@ export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
? {
|
||||
name: market.tradableInstrument.instrument.name,
|
||||
marketID: market.id,
|
||||
parentMarketID: parentData?.market?.parentMarketID || '-',
|
||||
parentMarketID: parentMarketIdData?.market?.parentMarketID || '-',
|
||||
insurancePoolFraction:
|
||||
(successor?.proposal?.terms.change.__typename === 'NewMarket' &&
|
||||
successor.proposal.terms.change.successorConfiguration
|
||||
?.insurancePoolFraction) ||
|
||||
(successorProposalDetails?.proposal?.terms.change.__typename ===
|
||||
'NewMarket' &&
|
||||
successorProposalDetails.proposal.terms.change
|
||||
.successorConfiguration?.insurancePoolFraction) ||
|
||||
'-',
|
||||
tradingMode:
|
||||
market.tradingMode &&
|
||||
@@ -187,11 +232,156 @@ export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => (
|
||||
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) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
@@ -199,6 +389,16 @@ export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => (
|
||||
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,
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -211,6 +411,7 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
);
|
||||
|
||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
||||
return asset ? (
|
||||
<>
|
||||
@@ -233,54 +434,148 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const MetadataInfoPanel = ({ 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) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
expiryDate: getMarketExpiryDateFormatted(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
),
|
||||
...market.tradableInstrument.instrument.metadata.tags
|
||||
?.map((tag) => {
|
||||
const [key, value] = tag.split(':');
|
||||
return { [key]: value };
|
||||
})
|
||||
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
|
||||
...(getMarketMetadata(market) || {}),
|
||||
}}
|
||||
parentData={
|
||||
parentMarket && {
|
||||
expiryDate: getMarketExpiryDateFormatted(
|
||||
parentMarket.tradableInstrument.instrument.metadata.tags
|
||||
),
|
||||
...(getMarketMetadata(parentMarket) || {}),
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RiskModelInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
export const RiskModelInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => {
|
||||
if (market.tradableInstrument.riskModel.__typename !== 'LogNormalRiskModel') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { tau, riskAversionParameter } = market.tradableInstrument.riskModel;
|
||||
return <MarketInfoTable data={{ tau, riskAversionParameter }} unformatted />;
|
||||
|
||||
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
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RiskParametersInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
if (market.tradableInstrument.riskModel.__typename === 'LogNormalRiskModel') {
|
||||
export const RiskParametersInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => {
|
||||
const marketType = market.tradableInstrument.riskModel.__typename;
|
||||
|
||||
let data, parentData;
|
||||
|
||||
if (marketType === 'LogNormalRiskModel') {
|
||||
const { r, sigma, mu } = market.tradableInstrument.riskModel.params;
|
||||
return <MarketInfoTable data={{ r, sigma, mu }} unformatted />;
|
||||
}
|
||||
if (market.tradableInstrument.riskModel.__typename === 'SimpleRiskModel') {
|
||||
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') {
|
||||
const { factorLong, factorShort } =
|
||||
market.tradableInstrument.riskModel.params;
|
||||
return <MarketInfoTable data={{ factorLong, factorShort }} unformatted />;
|
||||
data = { factorLong, factorShort };
|
||||
|
||||
if (
|
||||
parentMarket?.tradableInstrument?.riskModel.__typename ===
|
||||
'SimpleRiskModel'
|
||||
) {
|
||||
const parentParams = parentMarket.tradableInstrument.riskModel.params;
|
||||
parentData = {
|
||||
factorLong: parentParams.factorLong,
|
||||
factorShort: parentParams.factorShort,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return <MarketInfoTable data={data} parentData={parentData} unformatted />;
|
||||
};
|
||||
|
||||
export const RiskFactorsInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
export const RiskFactorsInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: MarketInfoProps) => {
|
||||
if (!market.riskFactors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { short, long } = market.riskFactors;
|
||||
return <MarketInfoTable data={{ short, long }} unformatted />;
|
||||
|
||||
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
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PriceMonitoringBoundsInfoPanel = ({
|
||||
market,
|
||||
triggerIndex,
|
||||
parentMarket,
|
||||
}: MarketInfoProps & {
|
||||
triggerIndex: number;
|
||||
}) => {
|
||||
@@ -288,11 +583,35 @@ 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})`
|
||||
@@ -319,6 +638,14 @@ export const PriceMonitoringBoundsInfoPanel = ({
|
||||
highestPrice: bounds.maxValidPrice,
|
||||
lowestPrice: bounds.minValidPrice,
|
||||
}}
|
||||
parentData={
|
||||
shouldShowParentData
|
||||
? {
|
||||
highestPrice: parentBounds.maxValidPrice,
|
||||
lowestPrice: parentBounds.minValidPrice,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
assetSymbol={quoteUnit}
|
||||
/>
|
||||
@@ -334,18 +661,31 @@ export const PriceMonitoringBoundsInfoPanel = ({
|
||||
|
||||
export const LiquidityMonitoringParametersInfoPanel = ({
|
||||
market,
|
||||
}: MarketInfoProps) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
|
||||
timeWindow:
|
||||
market.liquidityMonitoringParameters.targetStakeParameters.timeWindow,
|
||||
scalingFactor:
|
||||
market.liquidityMonitoringParameters.targetStakeParameters
|
||||
.scalingFactor,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
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} />;
|
||||
};
|
||||
|
||||
export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
|
||||
const assetDecimals =
|
||||
@@ -372,16 +712,61 @@ export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
export const LiquidityPriceRangeInfoPanel = ({
|
||||
market,
|
||||
parentMarket,
|
||||
}: 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">
|
||||
@@ -414,6 +799,7 @@ export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
parentData={parentData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -422,8 +808,12 @@ export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
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);
|
||||
|
||||
@@ -432,12 +822,33 @@ 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
|
||||
@@ -446,7 +857,14 @@ 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/${
|
||||
@@ -468,14 +886,28 @@ 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} />;
|
||||
@@ -484,7 +916,14 @@ export const DataSourceProof = ({
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{signers.map(({ signer }, i) => {
|
||||
return (
|
||||
const parentSigner = parentSigners?.find(
|
||||
({ signer: ParentSigner }) =>
|
||||
ParentSigner.__typename === signer.__typename
|
||||
)?.signer;
|
||||
|
||||
const isParentSignerEqual = isEqual(signer, parentSigner);
|
||||
|
||||
return isParentSignerEqual ? (
|
||||
<OracleLink
|
||||
key={i}
|
||||
providers={providers}
|
||||
@@ -492,6 +931,16 @@ export const DataSourceProof = ({
|
||||
type={type}
|
||||
dataSourceSpecId={dataSourceSpecId}
|
||||
/>
|
||||
) : (
|
||||
<OracleLink
|
||||
key={i}
|
||||
providers={providers}
|
||||
signer={signer}
|
||||
type={type}
|
||||
dataSourceSpecId={dataSourceSpecId}
|
||||
parentSigner={parentSigner}
|
||||
parentDataSourceSpecId={parentDataSourceSpecId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -525,18 +974,8 @@ export const DataSourceProof = ({
|
||||
return <div>{t('Invalid data source')}</div>;
|
||||
};
|
||||
|
||||
const OracleLink = ({
|
||||
providers,
|
||||
signer,
|
||||
type,
|
||||
dataSourceSpecId,
|
||||
}: {
|
||||
providers: Provider[];
|
||||
signer: SignerKind;
|
||||
type: 'settlementData' | 'termination';
|
||||
dataSourceSpecId: string;
|
||||
}) => {
|
||||
const signerProviders = providers.filter((p) => {
|
||||
const getSignerProviders = (signer: SignerKind, providers: Provider[]) =>
|
||||
providers.filter((p) => {
|
||||
if (signer.__typename === 'PubKey') {
|
||||
if (
|
||||
p.oracle.type === 'public_key' &&
|
||||
@@ -558,19 +997,62 @@ const OracleLink = ({
|
||||
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) => (
|
||||
<OracleProfile
|
||||
key={dataSourceSpecId}
|
||||
provider={provider}
|
||||
dataSourceSpecId={dataSourceSpecId}
|
||||
/>
|
||||
))}
|
||||
{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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -593,13 +1075,18 @@ 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,6 +5,7 @@ import {
|
||||
ExternalLink,
|
||||
Icon,
|
||||
Intent,
|
||||
Lozenge,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
@@ -59,10 +60,12 @@ 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);
|
||||
|
||||
@@ -78,8 +81,14 @@ 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,16 +11,27 @@ 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} />}
|
||||
title={
|
||||
<OracleProfileTitle
|
||||
provider={provider}
|
||||
parentProvider={parentProvider}
|
||||
/>
|
||||
}
|
||||
aria-labelledby="oracle-proof-dialog"
|
||||
open={open}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ExternalLink,
|
||||
Icon,
|
||||
Intent,
|
||||
Lozenge,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
@@ -18,15 +19,30 @@ import type { OracleMarketSpecFieldsFragment } from '../../__generated__/OracleM
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const OracleProfileTitle = ({ provider }: { provider: Provider }) => {
|
||||
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.
|
||||
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,3 +150,9 @@ query StopOrders($partyId: ID!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query StopOrderById($stopOrderId: ID!) {
|
||||
stopOrder(id: $stopOrderId) {
|
||||
...StopOrderFields
|
||||
}
|
||||
}
|
||||
|
||||
+44
-2
@@ -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,6 +43,13 @@ 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
|
||||
@@ -309,4 +316,39 @@ 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 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>;
|
||||
@@ -114,9 +114,11 @@ describe('OrderListTable', () => {
|
||||
it('should apply correct formatting applied for an iceberg order', async () => {
|
||||
const icebergOrder = {
|
||||
...limitOrder,
|
||||
size: '100',
|
||||
remaining: '50',
|
||||
icebergOrder: {
|
||||
__typename: 'IcebergOrder',
|
||||
minimumVisibleSize: '100',
|
||||
minimumVisibleSize: '1',
|
||||
peakSize: '50',
|
||||
reservedRemaining: '50',
|
||||
} as OrderFieldsFragment['icebergOrder'],
|
||||
@@ -127,8 +129,8 @@ describe('OrderListTable', () => {
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues: string[] = [
|
||||
icebergOrder.market?.tradableInstrument.instrument.code || '',
|
||||
'0.05',
|
||||
'0.10',
|
||||
'0.00',
|
||||
'+1.00',
|
||||
Schema.OrderTypeMapping[
|
||||
icebergOrder.type || Schema.OrderType.TYPE_LIMIT
|
||||
] + ' (Iceberg)',
|
||||
|
||||
@@ -86,21 +86,13 @@ export const OrderListTable = memo<
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Order>) => {
|
||||
if (data?.icebergOrder) {
|
||||
return data?.size && data.market
|
||||
? toBigNum(
|
||||
(
|
||||
BigInt(data.size) -
|
||||
BigInt(data.remaining) -
|
||||
BigInt(data.icebergOrder.reservedRemaining)
|
||||
).toString(),
|
||||
data.market.positionDecimalPlaces ?? 0
|
||||
).toNumber()
|
||||
? BigInt(data.size) -
|
||||
BigInt(data.remaining) -
|
||||
BigInt(data.icebergOrder.reservedRemaining)
|
||||
: undefined;
|
||||
}
|
||||
return data?.size && data.market
|
||||
? toBigNum(
|
||||
(BigInt(data.size) - BigInt(data.remaining)).toString(),
|
||||
data.market.positionDecimalPlaces ?? 0
|
||||
).toNumber()
|
||||
? BigInt(data.size) - BigInt(data.remaining)
|
||||
: undefined;
|
||||
},
|
||||
valueFormatter: ({
|
||||
@@ -114,8 +106,8 @@ export const OrderListTable = memo<
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
(BigInt(data.size) - BigInt(data.remaining)).toString(),
|
||||
data.market.positionDecimalPlaces
|
||||
value,
|
||||
data.market.positionDecimalPlaces ?? 0
|
||||
);
|
||||
},
|
||||
minWidth: 50,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getDateTimeFormat,
|
||||
isNumeric,
|
||||
toBigNum,
|
||||
formatTrigger,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@@ -55,29 +56,8 @@ export const StopOrdersTable = memo<
|
||||
sortable: false,
|
||||
valueFormatter: ({
|
||||
data,
|
||||
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 '-';
|
||||
},
|
||||
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string =>
|
||||
data ? formatTrigger(data, data.market.decimalPlaces) : '',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './__generated__/OrdersSubscription';
|
||||
export * from './use-has-amendable-order';
|
||||
export * from './use-order-update';
|
||||
export * from './use-order-store';
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,139 +0,0 @@
|
||||
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,6 +113,7 @@ const marketsData = [
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'AAVEDAI Monthly (30 Jun 2022)',
|
||||
code: 'AAVEDAI.MF21',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
symbol: 'tDAI',
|
||||
@@ -142,6 +143,7 @@ const marketsData = [
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||
code: 'UNIDAI.MF21',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
symbol: 'tDAI',
|
||||
@@ -183,7 +185,7 @@ describe('getMetrics && rejoinPositionData', () => {
|
||||
expect(metrics[0].marketId).toEqual(
|
||||
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
|
||||
);
|
||||
expect(metrics[0].marketName).toEqual('AAVEDAI Monthly (30 Jun 2022)');
|
||||
expect(metrics[0].marketName).toEqual('AAVEDAI.MF21');
|
||||
expect(metrics[0].marketTradingMode).toEqual(
|
||||
'TRADING_MODE_MONITORING_AUCTION'
|
||||
);
|
||||
@@ -208,7 +210,7 @@ describe('getMetrics && rejoinPositionData', () => {
|
||||
expect(metrics[1].marketId).toEqual(
|
||||
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
|
||||
);
|
||||
expect(metrics[1].marketName).toEqual('UNIDAI Monthly (30 Jun 2022)');
|
||||
expect(metrics[1].marketName).toEqual('UNIDAI.MF21');
|
||||
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.name,
|
||||
marketName: market.tradableInstrument.instrument.code,
|
||||
marketTradingMode: market.tradingMode,
|
||||
markPrice: marketData ? marketData.markPrice : undefined,
|
||||
notional: notional
|
||||
|
||||
@@ -41,285 +41,329 @@ const singleRow: Position = {
|
||||
|
||||
const singleRowData = [singleRow];
|
||||
|
||||
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} />);
|
||||
describe('Positions', () => {
|
||||
it('should render successfully', async () => {
|
||||
await act(async () => {
|
||||
const { baseElement } = render(
|
||||
<PositionsTable rowData={[]} isReadOnly={false} />
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
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('render correct columns', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
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: {
|
||||
...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();
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(12);
|
||||
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();
|
||||
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 value if no status is normal', () => {
|
||||
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
|
||||
);
|
||||
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: {
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||
},
|
||||
data: undefined,
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders status with warning tooltip if not normal', async () => {
|
||||
describe('OpenVolumeCell', () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
|
||||
},
|
||||
data: undefined,
|
||||
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();
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -478,6 +478,12 @@ 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')}
|
||||
@@ -499,27 +505,51 @@ 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('Your position was affected by market conditions')}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
'Status: %s',
|
||||
PositionStatusMapping[
|
||||
PositionStatus.POSITION_STATUS_ORDERS_CLOSED
|
||||
]
|
||||
)}
|
||||
{t('Status: %s', PositionStatusMapping[data.status])}
|
||||
</p>
|
||||
{POSITION_RESOLUTION_LINK && (
|
||||
<ExternalLink href={POSITION_RESOLUTION_LINK}>
|
||||
@@ -537,15 +567,17 @@ 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">
|
||||
<Icon name="warning-sign" size={3} />
|
||||
{showIcon && <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 { useCreateOrderStore } from '@vegaprotocol/orders';
|
||||
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
interface TradesContainerProps {
|
||||
@@ -9,8 +9,7 @@ interface TradesContainerProps {
|
||||
}
|
||||
|
||||
export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||
const useOrderStoreRef = useCreateOrderStore();
|
||||
const updateOrder = useOrderStoreRef((store) => store.update);
|
||||
const update = useDealTicketFormValues((state) => state.updateAll);
|
||||
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: tradesWithMarketProvider,
|
||||
@@ -21,9 +20,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||
<TradesTable
|
||||
rowData={data}
|
||||
onClick={(price?: string) => {
|
||||
if (price) {
|
||||
updateOrder(marketId, { price });
|
||||
}
|
||||
update(marketId, { price });
|
||||
}}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
||||
/>
|
||||
|
||||
@@ -136,15 +136,12 @@ export const AnchorButton = forwardRef<HTMLAnchorElement, AnchorButtonProps>(
|
||||
}
|
||||
);
|
||||
|
||||
type ButtonLinkProps = Omit<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'className' | 'style'
|
||||
>;
|
||||
type ButtonLinkProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'style'>;
|
||||
|
||||
export const ButtonLink = forwardRef<HTMLButtonElement, ButtonLinkProps>(
|
||||
({ type = 'button', ...props }, ref) => {
|
||||
const className = classnames('inline underline');
|
||||
return <button ref={ref} className={className} type={type} {...props} />;
|
||||
({ type = 'button', className, ...props }, ref) => {
|
||||
const style = classnames('inline underline', className);
|
||||
return <button ref={ref} className={style} 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-700': intent === Intent.Primary,
|
||||
'bg-vega-blue-300 dark:bg-vega-blue-650': intent === Intent.Primary,
|
||||
'bg-danger': intent === Intent.Danger,
|
||||
'bg-warning': intent === Intent.Warning,
|
||||
// contrast issues with light mode
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './number';
|
||||
export * from './range';
|
||||
export * from './size';
|
||||
export * from './strings';
|
||||
export * from './trigger';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
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,9 +1,4 @@
|
||||
import {
|
||||
determineId,
|
||||
normalizeOrderAmendment,
|
||||
normalizeOrderSubmission,
|
||||
} from './utils';
|
||||
import type { OrderSubmissionBody } from './connectors/vega-connector';
|
||||
import { determineId, normalizeOrderAmendment } from './utils';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
describe('determineId', () => {
|
||||
it('produces a known result for an ID', () => {
|
||||
@@ -16,62 +11,6 @@ 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,15 +1,10 @@
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import type { Market, Order } from '@vegaprotocol/types';
|
||||
import { OrderTimeInForce, OrderType, AccountType } from '@vegaprotocol/types';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { sha3_256 } from 'js-sha3';
|
||||
import type {
|
||||
OrderAmendment,
|
||||
OrderSubmission,
|
||||
Transaction,
|
||||
Transfer,
|
||||
} from './connectors';
|
||||
import type { OrderAmendment, Transaction, Transfer } from './connectors';
|
||||
import type { Exact } from 'type-fest';
|
||||
|
||||
/**
|
||||
@@ -29,36 +24,6 @@ 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,6 +12,7 @@ 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 = {
|
||||
@@ -23,7 +24,7 @@ jest.mock('@vegaprotocol/assets', () => {
|
||||
};
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/assets'),
|
||||
useAssetsDataProvider: jest.fn(() => ({ data: [A1] })),
|
||||
useAssetsMapProvider: jest.fn(() => ({ data: { [A1.id]: A1 } })),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -50,7 +51,7 @@ jest.mock('@vegaprotocol/markets', () => {
|
||||
};
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
useMarketList: jest.fn(() => ({ data: [M1] })),
|
||||
useMarketsMapProvider: jest.fn(() => ({ data: { [M1.id]: M1 } })),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -59,20 +60,64 @@ jest.mock('@vegaprotocol/orders', () => {
|
||||
...jest.requireActual('@vegaprotocol/orders'),
|
||||
useOrderByIdQuery: jest.fn(({ variables: { orderId } }) => {
|
||||
if (orderId === '0') {
|
||||
return {
|
||||
data: {
|
||||
orderByID: {
|
||||
id: '0',
|
||||
side: 'SIDE_BUY',
|
||||
size: '10',
|
||||
timeInForce: 'TIME_IN_FORCE_FOK',
|
||||
type: 'TYPE_MARKET',
|
||||
price: '1234',
|
||||
createdAt: new Date(),
|
||||
status: 'STATUS_ACTIVE',
|
||||
market: { id: 'market-1' },
|
||||
},
|
||||
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',
|
||||
size: '10',
|
||||
side: 'SIDE_SELL',
|
||||
timeInForce: 'TIME_IN_FORCE_FOK',
|
||||
expiresAt: null,
|
||||
type: 'TYPE_MARKET',
|
||||
reference: '',
|
||||
peggedOrder: null,
|
||||
postOnly: false,
|
||||
reduceOnly: true,
|
||||
__typename: 'OrderSubmission',
|
||||
},
|
||||
__typename: 'StopOrder',
|
||||
},
|
||||
} as StopOrderByIdQuery;
|
||||
return {
|
||||
data,
|
||||
};
|
||||
} else {
|
||||
return { data: undefined };
|
||||
@@ -143,6 +188,31 @@ 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(),
|
||||
@@ -208,6 +278,23 @@ 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(),
|
||||
@@ -269,12 +356,20 @@ 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 }) => {
|
||||
@@ -291,12 +386,18 @@ 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,9 +36,10 @@ import {
|
||||
formatNumber,
|
||||
toBigNum,
|
||||
truncateByChars,
|
||||
formatTrigger,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
import { useEthWithdrawApprovalsStore } from './use-ethereum-withdraw-approvals-store';
|
||||
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
|
||||
import {
|
||||
@@ -46,8 +47,9 @@ import {
|
||||
getOrderToastTitle,
|
||||
getRejectionReason,
|
||||
useOrderByIdQuery,
|
||||
useStopOrderByIdQuery,
|
||||
} from '@vegaprotocol/orders';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||
import type { Side } from '@vegaprotocol/types';
|
||||
import { OrderStatusMapping } from '@vegaprotocol/types';
|
||||
import { Size } from '@vegaprotocol/datagrid';
|
||||
@@ -136,8 +138,8 @@ const SubmitOrderDetails = ({
|
||||
data: OrderSubmission;
|
||||
order?: OrderTxUpdateFieldsFragment;
|
||||
}) => {
|
||||
const { data: markets } = useMarketList();
|
||||
const market = markets?.find((m) => m.id === order?.marketId);
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
const market = markets?.[order?.marketId || ''];
|
||||
if (!market) return null;
|
||||
|
||||
const price = order ? order.price : data.price;
|
||||
@@ -172,6 +174,58 @@ 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,
|
||||
@@ -183,13 +237,12 @@ const EditOrderDetails = ({
|
||||
variables: { orderId: data.orderId },
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
const { data: markets } = useMarketList();
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
|
||||
const originalOrder = order || orderById?.orderByID;
|
||||
const marketId = order?.marketId || orderById?.orderByID.market.id;
|
||||
if (!originalOrder) return null;
|
||||
const market = markets?.find((m) => m.id === marketId);
|
||||
if (!market) return null;
|
||||
const market = markets?.[marketId || ''];
|
||||
if (!originalOrder || !market) return null;
|
||||
|
||||
const original = (
|
||||
<SizeAtPrice
|
||||
@@ -245,11 +298,11 @@ const CancelOrderDetails = ({
|
||||
const { data: orderById } = useOrderByIdQuery({
|
||||
variables: { orderId },
|
||||
});
|
||||
const { data: markets } = useMarketList();
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
|
||||
const originalOrder = orderById?.orderByID;
|
||||
if (!originalOrder) return null;
|
||||
const market = markets?.find((m) => m.id === originalOrder.market.id);
|
||||
const market = markets?.[originalOrder.market.id];
|
||||
if (!market) return null;
|
||||
|
||||
const original = (
|
||||
@@ -282,15 +335,52 @@ 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 } = useAssetsDataProvider();
|
||||
const { data: markets } = useMarketList();
|
||||
const { data: assets } = useAssetsMapProvider();
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
|
||||
if (isWithdrawTransaction(tx.body)) {
|
||||
const transactionDetails = tx.body;
|
||||
const asset = assets?.find(
|
||||
(a) => a.id === transactionDetails.withdrawSubmission.asset
|
||||
);
|
||||
const asset = assets?.[transactionDetails.withdrawSubmission.asset];
|
||||
if (asset) {
|
||||
const num = formatNumber(
|
||||
toBigNum(transactionDetails.withdrawSubmission.amount, asset.decimals),
|
||||
@@ -312,15 +402,11 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
);
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
if (isStopOrdersSubmissionTransaction(tx.body)) {
|
||||
return <SubmitStopOrderDetails data={tx.body.stopOrdersSubmission} />;
|
||||
}
|
||||
|
||||
if (isOrderCancellationTransaction(tx.body)) {
|
||||
// CANCEL
|
||||
if (
|
||||
tx.body.orderCancellation.orderId &&
|
||||
@@ -336,22 +422,50 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
|
||||
// CANCEL ALL (from Trading)
|
||||
if (tx.body.orderCancellation.marketId) {
|
||||
const marketName = markets?.find(
|
||||
(m) =>
|
||||
m.id === (tx.body as OrderCancellationBody).orderCancellation.marketId
|
||||
)?.tradableInstrument.instrument.code;
|
||||
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
|
||||
) {
|
||||
return (
|
||||
<Panel>
|
||||
{marketName ? (
|
||||
<>
|
||||
{t('Cancel all orders for')} <strong>{marketName}</strong>
|
||||
</>
|
||||
) : (
|
||||
t('Cancel all orders')
|
||||
)}
|
||||
</Panel>
|
||||
<CancelStopOrderDetails
|
||||
stopOrderId={String(tx.body.stopOrdersCancellation.stopOrderId)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
@@ -368,7 +482,7 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
const marketId = first(
|
||||
transaction.batchMarketInstructions.cancellations
|
||||
)?.marketId;
|
||||
const market = marketId && markets?.find((m) => m.id === marketId);
|
||||
const market = markets?.[marketId || ''];
|
||||
if (market) {
|
||||
return (
|
||||
<Panel>
|
||||
@@ -385,7 +499,7 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
|
||||
if (isTransferTransaction(tx.body)) {
|
||||
const { amount, to, asset } = tx.body.transfer;
|
||||
const transferAsset = assets?.find((a) => a.id === asset);
|
||||
const transferAsset = assets?.[asset];
|
||||
// only render if we have an asset to avoid unformatted amounts showing
|
||||
if (transferAsset) {
|
||||
const value = addDecimalsFormatNumber(amount, transferAsset.decimals);
|
||||
|
||||
+2
-2
@@ -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.18.1",
|
||||
"allotment": "1.19.2",
|
||||
"alpha-lyrae": "vegaprotocol/alpha-lyrae",
|
||||
"apollo": "^2.33.9",
|
||||
"apollo-link-timeout": "^4.0.0",
|
||||
@@ -70,7 +70,7 @@
|
||||
"jsondiffpatch": "^0.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "13.3.0",
|
||||
"pennant": "1.10.0",
|
||||
"pennant": "1.11.1",
|
||||
"react": "18.2.0",
|
||||
"react-copy-to-clipboard": "^5.0.4",
|
||||
"react-dom": "18.2.0",
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# Wallet
|
||||
|
||||
A Vega wallet is required to prepare and submit transaction on Vega (place, cancel, orders etc). See the [wallet docs](https://docs.vega.xyz/docs/mainnet/concepts/vega-wallet) for more on how "crypto" wallets work.
|
||||
|
||||
A wallet can contain many public/private key pairs. The public part of each key pair is known the [Party](../protocol/0017-PART-party.md) sometimes just referred to as a key or public key.
|
||||
|
||||
The primary job(s) of a wallet is to [sign/encrypt transaction](../protocol/0022-AUTH-auth.md) (so the network can be sure they were sent by a given party) and to broadcast these transactions to a node on the network.
|
||||
|
||||
## Set up wallet / Restore wallet
|
||||
|
||||
When opening the wallet for the first time, I...
|
||||
|
||||
- if the wallet sends telemetry/analytics: **must** be prompted to opt into (or stay out of) analytics (<a name="0001-WALL-003" href="#0001-WALL-003">0001-WALL-003</a>)
|
||||
- I can restore a wallet from a seed phrase (<a name="0001-WALL-004" href="#0001-WALL-004">0001-WALL-004</a>)
|
||||
- I can create a new wallet (<a name="0001-WALL-005" href="#0001-WALL-005">0001-WALL-005</a>)
|
||||
- I can view the back up phrase (<a name="0001-WALL-006" href="#0001-WALL-006">0001-WALL-006</a>)
|
||||
- I can see the first key without having to "add key". (i.e. The wallet auto generates the first key from the seed phrase) (<a name="0001-WALL-008" href="#0001-WALL-008">0001-WALL-008</a>)
|
||||
|
||||
...so I can sign transactions
|
||||
|
||||
## Configure network
|
||||
|
||||
When using the wallet on a network, I...
|
||||
|
||||
- I can have Mainnet and Fairground (testnet) pre-configured (with Mainnet being the default network) (<a name="0001-WALL-009" href="#0001-WALL-009">0001-WALL-009</a>)
|
||||
- I can create a new network configuration (<a name="0001-WALL-010" href="#0001-WALL-010">0001-WALL-010</a>)
|
||||
- I can refine the configuration for existing networks (including the ones that come pre-configured) (<a name="0001-WALL-011" href="#0001-WALL-011">0001-WALL-011</a>)
|
||||
- I can remove networks (<a name="0001-WALL-013" href="#0001-WALL-013">0001-WALL-013</a>)
|
||||
|
||||
...so I can broadcast transactions to, and read information from a vega network in my wallet
|
||||
|
||||
## Update wallet
|
||||
|
||||
When using an older version of a Vega wallet than the current official release, I...
|
||||
|
||||
- I am warned if the version I am using is not compatible with the version of Vega on the selected network, and I am given a link to get latest compatible version on github (<a name="0001-WALL-015" href="#0001-WALL-015">0001-WALL-015</a>)
|
||||
|
||||
... so the version of the wallet app I am using works with the network I am using
|
||||
|
||||
## Log in to a wallet
|
||||
|
||||
When using a given wallet, I...
|
||||
|
||||
- I can select a wallet and enter the passphrase only once per "session" (<a name="0001-WALL-016" href="#0001-WALL-016">0001-WALL-016</a>)
|
||||
|
||||
... so that other users of my machine can not use my wallet, and I am not asked to re-enter frequently
|
||||
|
||||
## Connecting to Dapps
|
||||
|
||||
When a dapp requests use of a wallet, I...
|
||||
|
||||
- I am prompted to either select a wallet or dismiss the prompt (<a name="0001-WALL-017" href="#0001-WALL-017">0001-WALL-017</a>)
|
||||
- I can select whole wallet (so that new keys are automatically shared) (<a name="0001-WALL-019" href="#0001-WALL-019">0001-WALL-019</a>)
|
||||
- I can enter wallet passphrase before wallet details are shared (assuming a password has not recently been entered)(<a name="0001-WALL-022" href="#0001-WALL-022">0001-WALL-022</a>)
|
||||
- I can retrospectively revoke Dapp's access to a Wallet (<a name="0001-WALL-023" href="#0001-WALL-023">0001-WALL-023</a>)
|
||||
|
||||
... so that I can control what public keys are shared with a dapp and what dapps can prompt me to sign transactions
|
||||
|
||||
## Approving transactions
|
||||
|
||||
When a dapp sends a transaction to the wallet for signing and broadcast, I...
|
||||
|
||||
- I am prompted to confirm, reject or ignore the transaction (if auto-confirm is not on) (<a name="0001-WALL-024" href="#0001-WALL-024">0001-WALL-024</a>)
|
||||
- I can see the details of the transaction. See [details of transaction](#transaction-detail). (<a name="0001-WALL-025" href="#0001-WALL-025">0001-WALL-025</a>)
|
||||
|
||||
... so I can verify that the transaction being sent is the one I want
|
||||
|
||||
## Transaction log
|
||||
|
||||
When thinking about a recent or specific transaction, I ...
|
||||
|
||||
- I can find a single list of all transactions, completed and ongoing, from all keys and wallets, from my current desktop session and network (<a name="0001-WALL-034" href="#0001-WALL-034">0001-WALL-034</a>)
|
||||
- I can see transactions that were confirmed by the wallet user (me) (<a name="0001-WALL-035" href="#0001-WALL-035">0001-WALL-035</a>)
|
||||
- I can see transactions that were rejected by the wallet user (me) (<a name="0001-WALL-036" href="#0001-WALL-036">0001-WALL-036</a>)
|
||||
- If I switch network, transactions list changes to show the transactions for that network (<a name="0001-WALL-037" href="#0001-WALL-037">0001-WALL-037</a>)
|
||||
- I can click a transaction in the list to see the transaction details (<a name="0001-WALL-038" href="#0001-WALL-038">0001-WALL-038</a>)
|
||||
- I can see empty state when there are no transactions for this session (<a name="0001-WALL-039" href="#0001-WALL-039">0001-WALL-039</a>)
|
||||
|
||||
... so that I can ensure my wallet is being used appropriately and find transaction I made
|
||||
|
||||
## Transaction details
|
||||
|
||||
when looking at a specific transaction...
|
||||
|
||||
- I can see details of specific transactions I opened (<a name="0001-WALL-041" href="#0001-WALL-041">0001-WALL-041</a>)
|
||||
- I can find my way to the transaction on block explorer (<a name="0001-WALL-042" href="#0001-WALL-042">0001-WALL-042</a>)
|
||||
- I can find my way to the complete transaction history for that key on block explorer (<a name="0001-WALL-043" href="#0001-WALL-043">0001-WALL-043</a>)
|
||||
|
||||
- I can see [status of broadcasted transactions](0003-WTXN-submit_vega_transaction.md#track-transaction-on-network)
|
||||
|
||||
.. so I can find all the information about what has happened with mined and un-mined transactions
|
||||
|
||||
## Key management
|
||||
|
||||
When using a Vega wallet, I...
|
||||
|
||||
- I can create new keys (derived from the source of wallet) (<a name="0001-WALL-052" href="#0001-WALL-052">0001-WALL-052</a>)
|
||||
- I can see full public key or be able to copy it to clipboard (<a name="0001-WALL-054" href="#0001-WALL-054">0001-WALL-054</a>)
|
||||
- I can change key name/alias (<a name="0001-WALL-055" href="#0001-WALL-055">0001-WALL-055</a>)
|
||||
|
||||
... so I can manage risk (e.g. isolate margin), mitigate the damage of a key being compromised, or use multiple trading strategies
|
||||
|
||||
## Taint keys
|
||||
|
||||
When protecting myself from use of keys that may be compromised, I..
|
||||
|
||||
- I can select a key I wish to taint (<a name="0001-WALL-057" href="#0001-WALL-057">0001-WALL-057</a>)
|
||||
- I am prompted to enter wallet password to taint key (<a name="0001-WALL-058" href="#0001-WALL-058">0001-WALL-058</a>)
|
||||
- I can see tainted keys flagged as tainted (<a name="0001-WALL-060" href="#0001-WALL-060">0001-WALL-060</a>)
|
||||
|
||||
... so that tainted keys must not be used
|
||||
|
||||
When I have accidentally tainted a key I...
|
||||
|
||||
- I can select a key to un-taint and be required to enter wallet password (<a name="0001-WALL-061" href="#0001-WALL-061">0001-WALL-061</a>)
|
||||
|
||||
...so that I must use the key again
|
||||
|
||||
## Manually sign a message
|
||||
|
||||
When wishing to use my wallet to sign arbitrary messages, I...
|
||||
|
||||
- I can enter content to be signed with key (<a name="0001-WALL-062" href="#0001-WALL-062">0001-WALL-062</a>)
|
||||
- I can submit/sign the content (<a name="0001-WALL-065" href="#0001-WALL-065">0001-WALL-065</a>)
|
||||
- I can [track progress](0003-WTXN-submit_vega_transaction.md#track-transaction-on-network) of broadcast transaction either by being given a hash that I can use in block explorer, or see the transaction status
|
||||
|
||||
.. so I can control of the message being signed, and can use the message elsewhere (for example to prove I own a wallet)
|
||||
|
||||
## Wallet management
|
||||
|
||||
When seeking to reduce risk of compromise I...
|
||||
|
||||
- I can create multiple wallets (<a name="0001-WALL-066" href="#0001-WALL-066">0001-WALL-066</a>)
|
||||
- I can switch between wallets (<a name="0001-WALL-067" href="#0001-WALL-067">0001-WALL-067</a>)
|
||||
- I can remove a wallet (<a name="0001-WALL-068" href="#0001-WALL-068">0001-WALL-068</a>)
|
||||
- I can change wallet name (<a name="0001-WALL-069" href="#0001-WALL-069">0001-WALL-069</a>)
|
||||
|
||||
... so that I must administrate my wallets
|
||||
@@ -0,0 +1,64 @@
|
||||
# Connect Vega wallet & select keys
|
||||
|
||||
## Connect wallet
|
||||
|
||||
When looking to use Vega via a user interface e.g. Dapp (Decentralized web App), I...
|
||||
|
||||
- If the app loads and already has a connection it can restore "eagerly" (without the user having to click connect) it **could** do so
|
||||
- **must** select a connection method / wallet type: (<a name="0002-WCON-002" href="#0002-WCON-002">0002-WCON-002</a>)
|
||||
- if Rest:
|
||||
|
||||
- **must** have the option to input a non-default Wallet location (<a name="0002-WCON-003" href="#0002-WCON-003">0002-WCON-003</a>)
|
||||
- **must** submit attempt to connect to wallet (<a name="0002-WCON-005" href="#0002-WCON-005">0002-WCON-005</a>)
|
||||
|
||||
- if the dapp DOES already have a permission with the wallet: **must** see that wallet is connected (<a name="0002-WCON-007" href="#0002-WCON-007">0002-WCON-007</a>) note: if the user want to connect to a different wallet to the one that they were previously connected with, they will have to hit logout.
|
||||
|
||||
- if the app uses one key at a time: **should** show what key is active (re-use the last active key) (<a name="0002-WCON-008" href="#0002-WCON-008">0002-WCON-008</a>)
|
||||
|
||||
- if the wallet does NOT have an existing permission with the wallet: **must** prompt user to check wallet app to approve the request to connect wallet: See [Connecting to Dapps](0002-WCON-connect_vega_wallet.md#connect-wallet) for what should happen in wallet app (<a name="0002-WCON-009" href="#0002-WCON-009">0002-WCON-009</a>)
|
||||
|
||||
- if new keys are given permission: **must** show the user the keys have been approved (<a name="0002-WCON-010" href="#0002-WCON-010">0002-WCON-010</a>)
|
||||
|
||||
- if the dapp uses one key at a time: **should** prompt me to select key. See [select/switch keys](#select-and-switch-keys). (<a name="0002-WCON-014" href="#0002-WCON-014">0002-WCON-014</a>)
|
||||
|
||||
- if user rejects connection: **must** see a message saying that the request to connect was denied (<a name="0002-WCON-015" href="#0002-WCON-015">0002-WCON-015</a>)
|
||||
|
||||
- if the dapp is unable to connect for technical reason (e.g. CORS): **must** see an explanation of the error, and a method of fixing the issue (<a name="0002-WCON-016" href="#0002-WCON-016">0002-WCON-016</a>)
|
||||
|
||||
- ~~Browser wallet~~ `not available yet`
|
||||
- Fairground hosted wallet
|
||||
- **must** only be be shown this option if the dapp is connected to fairground (<a name="0002-WCON-039" href="#0002-WCON-039">0002-WCON-039</a>)
|
||||
- **must** input a wallet name (<a name="0002-WCON-017" href="#0002-WCON-017">0002-WCON-017</a>)
|
||||
- **must** input a password (<a name="0002-WCON-018" href="#0002-WCON-018">0002-WCON-018</a>)
|
||||
- if success: **must** see that the wallet is connected and details of connected key (<a name="0002-WCON-019" href="#0002-WCON-019">0002-WCON-019</a>)
|
||||
- if failure: **must** see reason for failure (<a name="0002-WCON-020" href="#0002-WCON-020">0002-WCON-020</a>)
|
||||
- _note: the fairground hosted wallet is configured to automatically approve connections from dapps so there is no need for key selection._
|
||||
- **must** have the option to select a different method / wallet type if I change my mind (<a name="0002-WCON-021" href="#0002-WCON-021">0002-WCON-021</a>)
|
||||
|
||||
... so I can use the interface to read data about my key/party or request my wallet to broadcast transactions to a Vega network.
|
||||
|
||||
## Disconnect wallet
|
||||
|
||||
When wishing to disconnect my wallet, I...
|
||||
|
||||
- **must** see an option to disconnect wallet (<a name="0002-WCON-022" href="#0002-WCON-022">0002-WCON-022</a>)
|
||||
- **must** see confirmation that wallet has been disconnected (<a name="0002-WCON-023" href="#0002-WCON-023">0002-WCON-023</a>)
|
||||
|
||||
... so that I can protect my wallet from malicious use or select a different wallet to connect to
|
||||
|
||||
## Select and switch keys
|
||||
|
||||
when looking to do something with a specific key (or set of keys) from my wallet, I...
|
||||
|
||||
- **must** see what key is currently selected (if any) (<a name="0002-WCON-025" href="#0002-WCON-025">0002-WCON-025</a>)
|
||||
- **must** see a list of keys that are approved from the connected wallet (<a name="0002-WCON-026" href="#0002-WCON-026">0002-WCON-026</a>)
|
||||
|
||||
- for each key:
|
||||
|
||||
- **must** see the first and last 6 digits of the [public key](DATA-data_display.md#public-keys). (<a name="0002-WCON-027" href="#0002-WCON-027">0002-WCON-027</a>)
|
||||
- **must** be able to copy to clipboard the whole public key (<a name="0002-WCON-029" href="#0002-WCON-029">0002-WCON-029</a>)
|
||||
- **must** see the key name/alias (meta data) (<a name="0002-WCON-030" href="#0002-WCON-030">0002-WCON-030</a>)
|
||||
|
||||
- **must** see the option to trigger a re-authenticate so I can use newly created keys (<a name="0002-WCON-035" href="#0002-WCON-035">0002-WCON-035</a>)
|
||||
|
||||
...so that I can select the key(s) that I want to use.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Submit Vega transaction
|
||||
|
||||
A dapp sends a transaction to a wallet, that wallet then broadcasts the transaction to a network. Therefore the following is broken up into two steps. The transaction could fail at either. Generally: Once the transaction has gone to the network a user can use block explorer to track the transaction, but some tracking in Dapp or wallet will help.
|
||||
|
||||
When submitting a Vega transaction of any kind, I...
|
||||
|
||||
## Track transaction to wallet
|
||||
|
||||
if not connected to a Vega wallet:
|
||||
|
||||
- **must** be told that I am not connected, so can not submit a transaction, and given the [option to connect](0012-WCON-connect_vega_wallet.md). (Note: this may have happened if the wallet has become disconnected without the user knowing) (<a name="0003-WTXN-001" href="#0003-WTXN-001">0003-WTXN-001</a>)
|
||||
|
||||
if transaction not auto approved by wallet:
|
||||
|
||||
- **must** see a prompt to check connected vega wallet to approve transaction (<a name="0003-WTXN-002" href="#0003-WTXN-002">0003-WTXN-002</a>)
|
||||
- **could** see the transaction details that has been passed to the wallet for broadcast
|
||||
|
||||
if transaction is approved by wallet:
|
||||
|
||||
- **must** see A [transaction hash](DATA-data_display.md#transaction-hash) (<a name="0003-WTXN-003" href="#0003-WTXN-003">0003-WTXN-003</a>)
|
||||
- **must** see the public key that this transaction was submitted for (<a name="0003-WTXN-004" href="#0003-WTXN-004">0003-WTXN-004</a>)
|
||||
- **should** see the alias for the key that submitted this transaction
|
||||
- **could** see a prompt to set this app to [auto approve](0001-WALL-wallet.md#approving-transactions) in wallet app
|
||||
|
||||
if transaction is rejected by wallet:
|
||||
|
||||
- **could** see that the order was rejected by the connected wallet (closing the window automatically may be appropriate) (<a name="0003-WTXN-007" href="#0003-WTXN-007">0003-WTXN-007</a>)
|
||||
|
||||
if the wallet does not respond:
|
||||
|
||||
- **must** not be able prevented from using the app, e.g. you can (<a name="0003-WTXN-008" href="#0003-WTXN-008">0003-WTXN-008</a>)
|
||||
- **would** like to be able to cancel the transaction from the dapp so that the wallet is no longer in the state where it is asking user to confirm
|
||||
|
||||
if the wallet highlights an issue with the transaction:
|
||||
|
||||
- **must** show that the transaction was marked as invalid by the wallet and not broadcast (aka an error was returned from Wallet) (<a name="0003-WTXN-009" href="#0003-WTXN-009">0003-WTXN-009</a>)
|
||||
- **should** see the error returned highlighted in context of the form that submitted the transaction in Dapp
|
||||
- **must** show error returned by wallet (<a name="0003-WTXN-011" href="#0003-WTXN-011">0003-WTXN-011</a>)
|
||||
|
||||
## Track transaction on network
|
||||
|
||||
- **must** see a link to that transaction in a block explorer for the appropriate network (<a name="0003-WTXN-012" href="#0003-WTXN-012">0003-WTXN-012</a>)
|
||||
- **should** see an indication transaction status
|
||||
- **should** see the network the transaction was broadcast to
|
||||
- **should** see the block the transaction was processed in
|
||||
- **should** show the node the transaction was broadcast to
|
||||
- **could** see the validator that processed the block the transaction was processed in
|
||||
|
||||
... so I am aware of the transactions status of the transactions my wallet is sending and that are being processed by the network
|
||||
@@ -0,0 +1,34 @@
|
||||
# Connect Ethereum wallet
|
||||
|
||||
Dapps can connect to an Ethereum wallet to complete Ethereum transactions such as Deposits and withdraws to/from Vega, Association and more.
|
||||
|
||||
## Connecting wallet
|
||||
|
||||
When wanting or needing to write to Ethereum, I...
|
||||
|
||||
- will have seen a link to connect that opens connection options (this always happens in context so should be covered by WITH, DEPO, ASSO ACS)
|
||||
|
||||
- if first time:
|
||||
- **must** select a connection method / wallet type: (e.g. wallet connect, injected / metamask) (<a name="0004-EWAL-001" href="#0004-EWAL-001">0004-EWAL-001</a>)
|
||||
- **must** be prompt to check eth wallet (while the dapp waits for a response) (<a name="0004-EWAL-002" href="#0004-EWAL-002">0004-EWAL-002</a>)
|
||||
- **must** see an option to cancel the attempted connection (if the wallet fails to respond) (<a name="0004-EWAL-003" href="#0004-EWAL-003">0004-EWAL-003</a>)
|
||||
- if the app gets multiple keys: the user:
|
||||
- **should** be shown the keys returned and given a UI to select a key for use (but the pattern is often just to select the first in the array)
|
||||
- **should** be prompted to select one (in many cases Dapps default to key 0 in the array)
|
||||
- after first use (if there is a connection to restore):
|
||||
- **must** prompt wallet to grant access (<a name="0004-EWAL-004" href="#0004-EWAL-004">0004-EWAL-004</a>)
|
||||
- **should** see previous connection has been recovered
|
||||
- **should** see a link to trigger a fresh connection / fetch new keys (in in the case where I now want to use a different wallet to the one I was connected with)
|
||||
- once connected:
|
||||
- **must** see the connected ethereum wallet Public key (<a name="0004-EWAL-005" href="#0004-EWAL-005">0004-EWAL-005</a>)
|
||||
|
||||
... so I can sign and broadcast Ethereum transactions, use a key address as in input, or read data from ethereum via my connected wallet
|
||||
|
||||
## Disconnecting
|
||||
|
||||
When I'm finished using a connected Ethereum wallet I may wish to disconnect...
|
||||
|
||||
- **must** see a link to disconnect (<a name="0004-EWAL-006" href="#0004-EWAL-006">0004-EWAL-006</a>)
|
||||
- **must** destroy dapp -> ETH wallet session so that hitting connect again triggers the modal that asks what method you'd like to use to connect to an ETH wallet, (note: it is not possible to invalidate the permission the metamask wallet has granted the app, therefore users will need to know that if they want to connect to a new ETH key they will have to do so from the wallet) (<a name="0004-EWAL-007" href="#0004-EWAL-007">0004-EWAL-007</a>)
|
||||
|
||||
... so that I can use a different wallet, or ensure may wallet can not be used by other apps
|
||||
@@ -0,0 +1,60 @@
|
||||
# Submit Ethereum transaction
|
||||
|
||||
## Know what transaction I'm signing
|
||||
|
||||
When about to click to prompt Ethereum wallet to sign a transaction, I...
|
||||
|
||||
- **should** see the contract address I am about to interact with
|
||||
- **should** see the function name I am about to interact with
|
||||
|
||||
...so I know what to expect when my wallet asks me to sign
|
||||
|
||||
## Track transactions to wallet
|
||||
|
||||
after clicking to submit an eth transaction to a connected wallet, I...
|
||||
|
||||
- **could** see an estimate for gas prices compared to a recent history
|
||||
- **could** see an estimated gas price for the function in question
|
||||
- **must** see prompt to check Ethereum wallet to approve transactions
|
||||
|
||||
... so I know I need to go to my wallet app to approve the transaction
|
||||
|
||||
## ERC20 approval/permit
|
||||
|
||||
> The approval/permit step is part of the [ERC20 standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/). It's intention is to provide additional security when interacting with smart contracts / using Dapps. It sets a maximum amount that the given eth key can send to a given address. It means that before end "spend" of ERC20 tokens I will have need to submit a "approve" transaction to tell the network how much is approved. An attempt to spend more than the approved amount will fail.
|
||||
> For example: In my approve transaction, I approve 10, I then spend 5. In my next transaction I attempt to spend 6, this will fail as my approve amount was reduced by the first transaction.
|
||||
> It is common for dapps to have a approve button that simply asks to approve a massive amount and leave it up to the wallet UI (e.g. metamask) to ask the user if they would like to change this.
|
||||
> Some ERC20 contracts have an additional function that runs both the approve and deposit function in one, but this is not standard.
|
||||
|
||||
If the transaction in question requires an ERC20 approval, I...
|
||||
|
||||
- if the current approved amount is less than the amount being "spent": **must** see be prompt to approve
|
||||
- **could** see the current approved amount
|
||||
- **must** be able to set the amount to be approved (in case the connected wallet does not handle this) (<a name="0005-ETXN-006" href="#0005-ETXN-006">0005-ETXN-006</a>)
|
||||
- **must** send an approve transaction with either a user specified amount or a very large number (<a name="0005-ETXN-001" href="#0005-ETXN-001">0005-ETXN-001</a>)
|
||||
- **must** see feedback of the state of approve transaction see "tracking ethereum transactions" below. (<a name="0005-ETXN-002" href="#0005-ETXN-002">0005-ETXN-002</a>)
|
||||
|
||||
... so I can control the maximum permitted transfer to the contract in question
|
||||
|
||||
## Tracking Ethereum transactions on network
|
||||
|
||||
After approving a transaction in my wallet app, I...
|
||||
|
||||
- **should** see link to the transaction on etherscan
|
||||
- **must** see the transactions status (Pending, confirmed, etc) on Ethereum by reading Ethereum (via connected wallet or the back up node specified in the app) (<a name="0005-ETXN-003" href="#0005-ETXN-003">0005-ETXN-003</a>)
|
||||
- if failed: **must** see why the transaction failed (e.g. didn't pay enough gas) (<a name="0005-ETXN-004" href="#0005-ETXN-004">0005-ETXN-004</a>)
|
||||
- if success: **should** see how many blocks ago the transaction was confirmed by the eth node being read
|
||||
|
||||
... so I can see the status of the transaction and debug as appropriate
|
||||
|
||||
## Tracking Ethereum transactions having their affect on Vega
|
||||
|
||||
Note: it is common for inter-blockchain applications to wait a certain amount of blocks before crediting money, as this reduces the risk of double spend in the case of forks or chain roll backs. There is a Vega environment variable the defines how long Vega waits.
|
||||
|
||||
If the ethereum transaction I've just submitted changes the state of the Vega network (e.g. a deposit from eth appearing as credited to my vega key on vega), I...
|
||||
|
||||
- **should** see how many Ethereum blocks Vega needs to wait before changing the state of Vega
|
||||
- **should** see how many blocks have passed or remain until the required number has been met
|
||||
- **must** see whether the expect action has taken place on Vega (e.g. credited Vega key) (<a name="0005-ETXN-005" href="#0005-ETXN-005">0005-ETXN-005</a>)
|
||||
|
||||
... so I know vega has been updated as appropriate
|
||||
@@ -0,0 +1,36 @@
|
||||
# Select network and nodes
|
||||
|
||||
## Startup
|
||||
|
||||
- **Must** automatically select a node from the environments network config stored in the [networks repo](https://github.com/vegaprotocol/networks) (<a name="0006-NETW-001" href="#0006-NETW-001">0006-NETW-001</a>)
|
||||
|
||||
## Network switcher
|
||||
|
||||
- **Must** see current network (<a name="0006-NETW-002" href="#0006-NETW-002">0006-NETW-002</a>)
|
||||
- **Must** be able to change network (<a name="0006-NETW-003" href="#0006-NETW-003">0006-NETW-003</a>)
|
||||
|
||||
## Node health
|
||||
|
||||
- **Must** see node status
|
||||
- Operational if node is less than 3 blocks behind (<a name="0006-NETW-004" href="#0006-NETW-004">0006-NETW-004</a>)
|
||||
- Warning if greater than 3 blocks behind (<a name="0006-NETW-005" href="#0006-NETW-005">0006-NETW-005</a>)
|
||||
- Warning if vega time is 3 seconds behind current time (<a name="0006-NETW-006" href="#0006-NETW-006">0006-NETW-006</a>)
|
||||
- Prominent error if vega time is 10 seconds behind current time (<a name="0006-NETW-007" href="#0006-NETW-007">0006-NETW-007</a>)
|
||||
- **Must** see current connected node (<a name="0006-NETW-008" href="#0006-NETW-008">0006-NETW-008</a>)
|
||||
- **Must** see current block height (<a name="0006-NETW-009" href="#0006-NETW-009">0006-NETW-009</a>)
|
||||
- **Must** see block height progressing (<a name="0006-NETW-010" href="#0006-NETW-010">0006-NETW-010</a>)
|
||||
- **Must** see link to status and incidents site (<a name="0006-NETW-011" href="#0006-NETW-011">0006-NETW-011</a>)
|
||||
|
||||
## Node switcher
|
||||
|
||||
- **Must** be able to click on current node to open node switcher dialog (<a name="0006-NETW-012" href="#0006-NETW-012">0006-NETW-012</a>)
|
||||
- In the node dialog
|
||||
- **Must** must see all nodes provided by the [network config](https://github.com/vegaprotocol/networks) (<a name="0006-NETW-013" href="#0006-NETW-013">0006-NETW-013</a>)
|
||||
- For each node
|
||||
- **Must** see the response time of the node (<a name="0006-NETW-014" href="#0006-NETW-014">0006-NETW-014</a>)
|
||||
- **Must** see the current block height (<a name="0006-NETW-015" href="#0006-NETW-015">0006-NETW-015</a>)
|
||||
- **Must** see if subscriptions are working for that node (<a name="0006-NETW-016" href="#0006-NETW-016">0006-NETW-016</a>)
|
||||
- **Must** be able to select and connect to any node, regardless of response time, block height or subscription status (<a name="0006-NETW-017" href="#0006-NETW-017">0006-NETW-017</a>)
|
||||
- **Must** be able to select 'other' to input a node address and connect to it (<a name="0006-NETW-018" href="#0006-NETW-018">0006-NETW-018</a>)
|
||||
- **Must** have disabled connect button if 'other' is selected but no url has been entered (<a name="0006-NETW-019" href="#0006-NETW-019">0006-NETW-019</a>)
|
||||
- **Must** have disabled connect button if selected node is the current node (<a name="0006-NETW-020" href="#0006-NETW-020">0006-NETW-020</a>)
|
||||
@@ -0,0 +1,23 @@
|
||||
# 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>)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Deposit
|
||||
|
||||
The Vega network has no native assets. All settlement assets exist on another chain and are "bridged" to Vega in one way or another.
|
||||
|
||||
In the case of [ERC20 tokens](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) there is a smart contract on the Ethereum network that acts as a vault (aka bridge) for the tokens that are deposited to Vega. The Vega network then reads the information from this vault about what Vega key to credit these tokens to. While in the Vault the Vega key that owns them (and consequently the ethereum key) may change. The vault then manages how much each ethereum key is able to withdraw from the vault given then changes in ownership that may have happened on Vega. The keys to this vault and managed by the different nodes that make up the Vega network. They verify the the appropriate amounts can be withdrawn by each Ethereum key. At time of writing only ERC20 tokens have been implemented but the pattern is likely the same for other assets/networks.
|
||||
|
||||
## ERC20 deposits
|
||||
|
||||
Note: ERC20 assets require an approval transaction to be finalised before funds can be credited to another key. Read more about approvals [link 1](https://medium.com/ethex-market/erc20-approve-allow-explained-88d6de921ce9), [link 2](https://hackernoon.com/erc20-infinite-approval-a-battle-between-convenience-and-security-lk60350r).
|
||||
|
||||
When making to deposit ERC20 assets to an Vega key, I...
|
||||
|
||||
- **should** be able to follow a link from an asset (e.g. on a market page) to the deposit form pre-populated with the given asset
|
||||
- **must** see a link to [connect an ethereum wallet](0004-EWAL-connect_ethereum_wallet.md) that I want to deposit from (<a name="1001-DEPO-001" href="#1001-DEPO-001">1001-DEPO-001</a>)
|
||||
- **must** select the [asset](9001-DATA-data_display.md#asset) that I want to deposit (<a name="1001-DEPO-002" href="#1001-DEPO-002">1001-DEPO-002</a>)
|
||||
- **should** easily see the assets that I have a non-zero balance for (in the connected eth wallet)
|
||||
- **should** see the ERC20 token address of the asset
|
||||
- **should** see the [Vega asset symbol](9001-DATA-data_display.md#asset-symbol)
|
||||
- **should** see the [Vega asset name](9001-DATA-data_display.md#asset-name)
|
||||
- **must** select the [amount of the asset](9001-DATA-data_display.md#asset-balances) that I want to deposit (<a name="1001-DEPO-003" href="#1001-DEPO-003">1001-DEPO-003</a>)
|
||||
- **should** see an ability to populate the input with the full balance in the connected wallet
|
||||
- **must** be warned if the amount being deposited is greater than the balance of the token in the connected Eth wallet (<a name="1001-DEPO-004" href="#1001-DEPO-004">1001-DEPO-004</a>)
|
||||
- **must** select the [Vega key](9001-DATA-data_display.md#public-keys) that I wish to deposit to (<a name="1001-DEPO-005" href="#1001-DEPO-005">1001-DEPO-005</a>)
|
||||
- **should** be able to [connect to a Vega wallet and select a key](0002-WCON-connect_vega_wallet.md#select-and-switch-keys)
|
||||
- **should** be easily (if not automatically) pre-populated with a [currently connected and active Vega key](0002-WCON-connect_vega_wallet.md#select-and-switch-keys)
|
||||
- **should** be able to input a Vega key other than one I am connected with (even without being connected)
|
||||
- if approved amount is less than deposit:
|
||||
- **must** see that an approval is needed and be prompted to approve more (<a name="1001-DEPO-006" href="#1001-DEPO-006">1001-DEPO-006</a>)
|
||||
- **should** see the approved [asset amount](9001-DATA-data_display.md#asset-balances)
|
||||
- **should** be able to input an amount to approve
|
||||
- **must** [submit eth transaction to approve](0005-ETXN-submit_ethereum_transaction.md) (<a name="1001-DEPO-007" href="#1001-DEPO-007">1001-DEPO-007</a>)
|
||||
- **must** see feedback for the approve transaction
|
||||
- if approved amount is more than deposit amount:
|
||||
- **could** see the approved [asset amount](9001-DATA-data_display.md#asset-balances)
|
||||
- **could** set a submit a new [eth transaction to approve more or less](0005-ETXN-submit_ethereum_transaction.md)
|
||||
- **must** submit the Deposit [eth transaction](0005-ETXN-submit_ethereum_transaction.md) (<a name="1001-DEPO-008" href="#1001-DEPO-008">1001-DEPO-008</a>)
|
||||
- **must** see feedback on the deposit [ETH transaction](0003-WTXN-submit_vega_transaction.md) (<a name="1001-DEPO-009" href="#1001-DEPO-009">1001-DEPO-009</a>)
|
||||
- **must** see feedback that the deposit has or has not been credited to the Vega key (<a name="1001-DEPO-010" href="#1001-DEPO-010">1001-DEPO-010</a>)
|
||||
|
||||
...so that my Vega key can use these assets on Vega
|
||||
@@ -0,0 +1,117 @@
|
||||
# Withdraw
|
||||
|
||||
Withdrawing funds is a two step process.
|
||||
|
||||
First the Vega network needs to approve that the funds can be released (not required for margin on open positions or in liquidity bond etc). If they are not, a withdraw is prepared and set aside so that it can not be used for positions etc. This also define what ethereum address will be credited the funds in step 2.
|
||||
|
||||
Second the user will need to run an ethereum function on the bridge contract to release the funds (and pay the gas to do so). They do this using a signature supplied by nodes of the Vega network in Step 1.
|
||||
|
||||
Although this is a two step process technically effort should be put into making it feel like one, then handle exceptions (like delays on withdrawals) as required.
|
||||
|
||||
See [Specs for eth bridge](../protocol/0031-ETHB-ethereum_bridge_spec.md) and [docs](https://docs.vega.xyz/docs/mainnet/concepts/vega-protocol#withdrawals) on withdrawals. See also the [specs on delays to withdrawals](../non-protocol-specs/0003-NP-LIMI-limits_aka_training_wheels.md#withdrawal-limits).
|
||||
|
||||
## Prepare an ERC20 withdraw from Vega
|
||||
|
||||
When wishing to withdraw some of an ERC20 asset from Vega, I...
|
||||
|
||||
- **should** be prompted to complete any existing incomplete withdrawals that exist for connected keys (see [complete withdrawal](#complete-erc20-withdraw-from-ethereum-bridge))
|
||||
|
||||
Note: It is better to encourage the completion of started withdraws as soon as possible after preparing them. This is because the validator set could theoretically change enough to make the node signatures that authorize the withdrawal invalid.
|
||||
|
||||
- **should** be warned that they will need to pay gas on the withdrawal before starting
|
||||
- **could** show the current gas fees BEFORE preparing the withdrawal (note: shows gas estimate is a general should for all [ethereum transactions](0005-ETXN-submit_ethereum_transaction.md) but this is so a user gets to see the gas costs at step 1 assuming they will do step 2 immediately. )
|
||||
|
||||
Note: A user may want to delay preparing a withdrawal if gas fees on the network are particularly high at the time
|
||||
|
||||
- **must** select the asset to withdraw (<a name="1002-WITH-001" href="#1002-WITH-001">1002-WITH-001</a>)
|
||||
|
||||
- **should not** see option to select assets where I a zero [total balance](9001-DATA-data_display.md#asset-balances) (note this should also avoid `Pending` assets from appearing in the list)
|
||||
- **must** see the general balance I have for that asset (<a name="1002-WITH-002" href="#1002-WITH-002">1002-WITH-002</a>)
|
||||
- **should** see balances to the full number of decimal places possible for that asset
|
||||
- **should** see the total balances of the assets I have
|
||||
- **could** see a breakdown of other accounts I have in this asset and their balances
|
||||
|
||||
- **must** select the [amount](9001-DATA-data_display.md#asset-balances) of the asset I wish to withdraw (<a name="1002-WITH-003" href="#1002-WITH-003">1002-WITH-003</a>)
|
||||
- **should** have an easy option (link/button) to input the full amount in general balance
|
||||
- **must** be able to specify as many decimal places as the asset supports (<a name="1002-WITH-004" href="#1002-WITH-004">1002-WITH-004</a>)
|
||||
- **must** be warned if the amount is greater than general balance (including if the general balance amount changes while the user is looking at the form) (<a name="1002-WITH-005" href="#1002-WITH-005">1002-WITH-005</a>)
|
||||
|
||||
- **must** be warned if the amount is lesser than the minimum allowed, where the minimum amount is the selected asset's quantum multiplied by the value of `spam.protection.minimumWithdrawalQuantumMultiple` network parameter (<a name="1002-WITH-026" href="#1002-WITH-026">1002-WITH-026</a>)
|
||||
|
||||
- **should** see a link to a faucet on the selected asset (only if there is one)
|
||||
|
||||
- **must** specify the Ethereum address that can claim the withdrawal (e.g. where you are withdrawing too) (<a name="1002-WITH-006" href="#1002-WITH-006">1002-WITH-006</a>)
|
||||
|
||||
- **should** be able to easily select an Ethereum key the app is already connected to
|
||||
- **should** be able to withdraw to a different Ethereum key to the one the app is connected to
|
||||
- **should** be warned if the input does not look like an ethereum address (wrong number of characters, not starting with 0x etc)
|
||||
|
||||
- if there is a withdraw delay on the selected asset:
|
||||
|
||||
- **should** see how large a withdrawal (or sum of withdrawals) needs to be to hit the `withdraw delay threshold`
|
||||
- **should** see what the withdraw delay is in hours and mins (if hit)
|
||||
- **should** see how much I have withdrawn in the last `withdraw delay period`
|
||||
- **must** be warned if this withdraw will hit a the delay before hitting withdraw (<a name="1002-WITH-007" href="#1002-WITH-007">1002-WITH-007</a>)
|
||||
|
||||
- **must** be warned if there are known reasons that the prepared withdrawal will not work (<a name="1002-WITH-008" href="#1002-WITH-008">1002-WITH-008</a>)
|
||||
- **must** submit a withdraw [vega transaction](0003-WTXN-submit_vega_transaction.md) (<a name="1002-WITH-009" href="#1002-WITH-009">1002-WITH-009</a>)
|
||||
|
||||
- if the preparing the withdraw on Vega fails:
|
||||
|
||||
- **must** be directed back to the withdraw form (containing the submitted values) and see an explanation of why the transaction failed, so I can fix and resubmit (<a name="1002-WITH-010" href="#1002-WITH-010">1002-WITH-010</a>)
|
||||
|
||||
- if the preparing the withdraw on Vega is successful:
|
||||
- **must** see that withdraw is prepared (<a name="1002-WITH-011" href="#1002-WITH-011">1002-WITH-011</a>)
|
||||
- if this withdraw will not hit the withdrawal threshold:
|
||||
- **should** be prompted to complete the transaction on ethereum (see [complete ERC20 withdraw](#complete-erc20-withdraw-from-ethereum-bridge))
|
||||
- **could** be directed to a list of incomplete withdrawals
|
||||
- if this withdraw will hit withdrawal threshold:
|
||||
- **must** see that the withdraw has been complete and is in the list waiting for the delay to pass (<a name="1002-WITH-024" href="#1002-WITH-024">1002-WITH-024</a>)
|
||||
|
||||
...so that I can get the details required to release my funds from the the Ethereum ERC20 bridge.
|
||||
|
||||
## Withdraws list / history
|
||||
|
||||
When looking to either complete a withdraw or view past withdraws, I...
|
||||
|
||||
- **must** be able to navigate to a list prepared withdrawals for the [connected to a vega wallet + key(s)](0002-WCON-connect_vega_wallet.md)
|
||||
|
||||
- for each prepared withdraw:
|
||||
- **must** see the asset being withdrawn (<a name="1002-WITH-012" href="#1002-WITH-012">1002-WITH-012</a>)
|
||||
- **must** see the [amount](9001-DATA-data_display.md#asset-balances) being withdrawn (<a name="1002-WITH-013" href="#1002-WITH-013">1002-WITH-013</a>)
|
||||
- **must** see the destination of the withdrawal (e.g. Recipient Eth address) (<a name="1002-WITH-014" href="#1002-WITH-014">1002-WITH-014</a>)
|
||||
- **should** see the date with withdraw was prepared
|
||||
- **could** see the full signature bundle from Vega node (for use on Ethereum)
|
||||
- for withdraws that are in progress:
|
||||
- **must** see the status of the withdraw (e.g. pending) (<a name="1002-WITH-015" href="#1002-WITH-015">1002-WITH-015</a>)
|
||||
- for completed withdraws:
|
||||
- **could** see when it was completed on native chain (e.g. ethereum)
|
||||
- **must** see a link to the transaction on native block explorer (e.g. etherscan) (<a name="1002-WITH-016" href="#1002-WITH-016">1002-WITH-016</a>)
|
||||
- for withdraws that have not been completed on the external chain, but are not delayed (e.g. Ethereum):
|
||||
- **must** see a link to complete the withdraw. See [complete ERC20 withdrawal](#complete-erc20-withdraw-from-ethereum-bridge) (<a name="1002-WITH-017" href="#1002-WITH-017">1002-WITH-017</a>)
|
||||
- for withdrawals that have a delay in place before the transaction can be completed:
|
||||
- **should** see much of the delay remains before it can be completed
|
||||
- for withdraws that failed to be prepared (e.g. there was not enough in the general account):
|
||||
- **must** show that the withdraw preparation failed (<a name="1002-WITH-025" href="#1002-WITH-025">1002-WITH-025</a>)
|
||||
|
||||
... so I can complete withdrawals or find details of previous ones
|
||||
|
||||
## Complete ERC20 withdraw from Ethereum bridge
|
||||
|
||||
When looking to submit the Ethereum transaction to release funds from the Vega bridge into my Ethereum wallet, I...
|
||||
|
||||
- **must** see a link to [connect an ethereum wallet](0004-EWAL-connect_ethereum_wallet.md) if not already connected (<a name="1002-WITH-018" href="#1002-WITH-018">1002-WITH-018</a>)
|
||||
- **must** see a link to [submit the ethereum transaction to finish withdrawal](0005-ETXN-submit_ethereum_transaction.md) (<a name="1002-WITH-019" href="#1002-WITH-019">1002-WITH-019</a>)
|
||||
- **could** be warned if the connected ethereum wallet is different to the one that the withdraw is going to credit (this is permitted but is a good reminder to the user about what to expect)
|
||||
|
||||
- if successful:
|
||||
- **must** see asset balances have been updated post withdrawal (<a name="1002-WITH-020" href="#1002-WITH-020">1002-WITH-020</a>)
|
||||
- **must** see the list of withdrawals (with updated status) (<a name="1002-WITH-021" href="#1002-WITH-021">1002-WITH-021</a>)
|
||||
- **could** see prompt to start another transaction or complete another incomplete one
|
||||
- if failed:
|
||||
- **must** see a description of why the transaction failed, and advised what to do (e.g. bad signature) (<a name="1002-WITH-022" href="#1002-WITH-022">1002-WITH-022</a>)
|
||||
- **must** be returned to a state where I can correct anything that is wrong, and attempt to submit the transaction again (<a name="1002-WITH-023" href="#1002-WITH-023">1002-WITH-023</a>)
|
||||
- **should** see a link to docs about withdrawals for trouble shooting (e.g. if the signer set has changed significantly since the withdraw was prepared)
|
||||
- **should** see status of incomplete withdrawals (so I can confirm the withdraw I attempted to complete is incomplete)
|
||||
|
||||
... so the funds I withdrew from Vega are credited to my Ethereum key
|
||||
@@ -0,0 +1,59 @@
|
||||
# Transfer
|
||||
|
||||
## Transfer Window
|
||||
|
||||
- **Must** be able to open transfer window through transfer button under key, account history page and collateral options (<a name="1003-TRAN-001" href="#1003-TRAN-001">1003-TRAN-001</a>)
|
||||
|
||||
- **Must** be able to close the window with the x (<a name="1003-TRAN-002" href="#1003-TRAN-002">1003-TRAN-002</a>)
|
||||
|
||||
- **Must** display a message showing obfuscated key that funds will be transferred from (<a name="1003-TRAN-003" href="#1003-TRAN-003">1003-TRAN-003</a>)
|
||||
|
||||
- **Must** each field has their label. Vega key, Asset, Amount (<a name="1003-TRAN-004" href="#1003-TRAN-004">1003-TRAN-004</a>)
|
||||
|
||||
## Vega Key
|
||||
|
||||
- **Must**
|
||||
if the user has multiple keys they must be able to swap between dropdown and manual entry (<a name="1003-TRAN-005" href="#1003-TRAN-005">1003-TRAN-005</a>)
|
||||
|
||||
- **Must**
|
||||
if the user has multiple keys they must be able to select from their list of keys(<a name="1003-TRAN-006" href="#1003-TRAN-006">1003-TRAN-006</a>)
|
||||
|
||||
## Asset
|
||||
|
||||
- **Must** display a drop down with all assets in the portfolio (<a name="1003-TRAN-007" href="#1003-TRAN-007">1003-TRAN-007</a>)
|
||||
|
||||
- **Must** the holdings of each asset is displayed (<a name="1003-TRAN-008" href="#1003-TRAN-008">1003-TRAN-008</a>)
|
||||
|
||||
- **Must** i can select any available assets and selected asset is displayed (<a name="1003-TRAN-009" href="#1003-TRAN-009">1003-TRAN-009</a>)
|
||||
|
||||
- **Must** selected asset shortname is displayed in the amount field (<a name="1003-TRAN-010" href="#1003-TRAN-010">1003-TRAN-010</a>)
|
||||
|
||||
## Validation
|
||||
|
||||
- **Must** cannot choose amount over current collateral. Message is displayed (<a name="1003-TRAN-011" href="#1003-TRAN-011">1003-TRAN-011</a>)
|
||||
|
||||
- **Must** display "required" message on each field if left blank when clicking button "Confirm Transfer" (<a name="1003-TRAN-012" href="#1003-TRAN-012">1003-TRAN-012</a>)
|
||||
|
||||
- **Must** display "Invalid vega key" message on Vega Key field if entered key doesn't pass validation(<a name="1003-TRAN-013" href="#1003-TRAN-013">1003-TRAN-013</a>)
|
||||
|
||||
- **Must** "Value below minimum" message is shown if amount is lower than minimum(<a name="1003-TRAN-014" href="#1003-TRAN-014">1003-TRAN-014</a>)
|
||||
|
||||
## Transfer
|
||||
|
||||
- **Must** can select include transfer fee (<a name="1003-TRAN-015" href="#1003-TRAN-015">1003-TRAN-015</a>)
|
||||
|
||||
- **Must** display tooltip for "Include transfer fee" when hovered over.(<a name="1003-TRAN-016" href="#1003-TRAN-016">1003-TRAN-016</a>)
|
||||
|
||||
- **Must** display tooltip for "Transfer fee when hovered over.(<a name="1003-TRAN-017" href="#1003-TRAN-017">1003-TRAN-017</a>)
|
||||
|
||||
- **Must** display tooltip for "Amount to be transferred" when hovered over.(<a name="1003-TRAN-018" href="#1003-TRAN-018">1003-TRAN-018</a>)
|
||||
|
||||
- **Must** display tooltip for "Total amount (with fee)" when hovered over.(<a name="1003-TRAN-019" href="#1003-TRAN-019">1003-TRAN-019</a>)
|
||||
|
||||
- **Must** amount to be transferred and transfer fee update correctly when include transfer fee is selected (<a name="1003-TRAN-020" href="#1003-TRAN-020">1003-TRAN-020</a>)
|
||||
|
||||
- **Must** total amount with fee is correct with and without "Include transfer fee" selected (<a name="1003-TRAN-021" href="#1003-TRAN-021">1003-TRAN-021</a>)
|
||||
|
||||
- **Must** i cannot select include transfer fee unless amount is entered (<a name="1003-TRAN-022" href="#1003-TRAN-022">1003-TRAN-022</a>)
|
||||
|
||||
- **Must** With all fields entered correctly, clicking "confirm transfer" button will start transaction(<a name="1003-TRAN-023" href="#1003-TRAN-023">1003-TRAN-023</a>)
|
||||
@@ -0,0 +1,70 @@
|
||||
# Associate and disassociate governance tokens with a Vega key
|
||||
|
||||
The Governance token on a Vega network is an ERC20 ethereum token. It has two utilities on Vega,
|
||||
|
||||
- Staking the Proof of stake network,
|
||||
- Participating in Governance.
|
||||
|
||||
To use the Governance token on a Vega network it first needs to be associated with a Vega key/party. This Vega key can then stake, propose and vote.
|
||||
|
||||
The word "associate" is used in some user interfaces, as apposed the word "stake" in function names. Stake can be avoided to prevent users thinking they would get a return only after the staking step. On Vega `Staking = Association + Nomination`, as in you need to run the "stake " function on Ethereum but then the nominate step on Vega before you get staking income. See [Glossary](../glossaries/staking-and-governance.md).
|
||||
|
||||
Associated tokens also count as vote weight in on-Vega governance (new markets etc), A parties vote weight is backed by the number of Governance tokens associated with that Vega key/party.
|
||||
|
||||
Associating tokens to a Vega key is a little like depositing, except, a deposit can only be released by the Vega network, where as an association can be revoked on ethereum by the the eth key that did the association.
|
||||
|
||||
Governance tokens may be held by a [Vesting contract](1005-VEST-vesting.md).
|
||||
|
||||
## Token discovery
|
||||
|
||||
When looking to acquire governance tokens, I...
|
||||
|
||||
- **must** see the contract address for the governance token of the Vega network <a name="1004-ASSO-001" href="#1004-ASSO-001">1004-ASSO-001</a>
|
||||
|
||||
...so I can participate in governance and staking.
|
||||
|
||||
## Associate
|
||||
|
||||
When looking to stake validators or participate in governance, I first need to associate governance tokens with a Vega wallet/key, I...
|
||||
|
||||
- **must** [connect an Ethereum wallet/key](0004-EWAL-connect_ethereum_wallet.md) to see tokens it may have in wallet or attributed to it in the vesting contract <a name="1004-ASSO-002" href="#1004-ASSO-002">1004-ASSO-002</a>
|
||||
- **must** select a Vega key to associate to <a name="1004-ASSO-003" href="#1004-ASSO-003">1004-ASSO-003</a>
|
||||
- **must** be able use a [connected Vega wallet](0002-WCON-connect_vega_wallet.md) as instead of manually inputting a public key <a name="1004-ASSO-004" href="#1004-ASSO-004">1004-ASSO-004</a>
|
||||
- **should** be able to populate field with a string, so I can associate to a wallet I without connecting it
|
||||
- if the connected ethereum wallet has vesting tokens: **must** be able to select to associate from either the vesting contract or the wallet (The default should be wallet and the option to use vesting tokens should only appear if there are tokens in a tranche (associated or not)) <a name="1004-ASSO-006" href="#1004-ASSO-006">1004-ASSO-006</a>
|
||||
- **must** see the number of associated and un-associated tokens in the selected wallet/vesting contract <a name="1004-ASSO-007" href="#1004-ASSO-007">1004-ASSO-007</a>
|
||||
- **must** select the amount of tokens to associate <a name="1004-ASSO-008" href="#1004-ASSO-008">1004-ASSO-008</a>
|
||||
- **must** be able to populate the input with the amount of un-associated tokens for the selected wallet/vesting contract <a name="1004-ASSO-009" href="#1004-ASSO-009">1004-ASSO-009</a>
|
||||
- **must** be warned if the amount being associated is greater than the amount available in the connected ethereum wallet <a name="1004-ASSO-010" href="#1004-ASSO-010">1004-ASSO-010</a>
|
||||
- **must** submit the association on [Ethereum transaction(s) inc ERC20 approval if required](0005-ETXN-submit_ethereum_transaction.md) <a name="1004-ASSO-011" href="#1004-ASSO-011">1004-ASSO-011</a>
|
||||
- **must** see feedback whether my association has been registered on Ethereum <a name="1004-ASSO-012" href="#1004-ASSO-012">1004-ASSO-012</a>
|
||||
- **must** see feedback that the association has been registered by Vega and that it can be used after the number of Ethereum block confirmations required (typically 50, check network param) <a name="1004-ASSO-013" href="#1004-ASSO-013">1004-ASSO-013</a>
|
||||
- **should** be able to see a balance for the number of tokens associated that are ready for use <a name="1004-ASSO-014" href="#1004-ASSO-014">1004-ASSO-014</a>
|
||||
- **should** be able to see a balance for the number of tokens for each pending association <a name="1004-ASSO-015" href="#1004-ASSO-015">1004-ASSO-015</a>
|
||||
- on completion: **should** be prompted to go on to [nominate](2001-STKE-staking.md) and/or participate in [Governance](1004-GOVE-governance_list.md)
|
||||
- **must** see the balances of tokens available in Ethereum wallet updated (both associated and un-associated) <a name="1004-ASSO-030" href="#1004-ASSO-030">1004-ASSO-030</a>
|
||||
- **must** see the balances of tokens available in vesting contract updated (both associated and un-associated) <a name="1004-ASSO-032" href="#1004-ASSO-032">1004-ASSO-032</a>
|
||||
|
||||
...so I can then use the Vega wallet to use my tokens.
|
||||
|
||||
## Disassociate
|
||||
|
||||
When wanting to remove governance tokens, I...
|
||||
|
||||
- **must** [connect an Ethereum wallet/key](0004-EWAL-connect_ethereum_wallet.md) to see tokens it may have in wallet or attributed to it in the vesting contract <a name="1004-ASSO-018" href="#1004-ASSO-018">1004-ASSO-018</a>
|
||||
- **must** see a list Vega keys that the connected Ethereum wallet has associated too <a name="1004-ASSO-019" href="#1004-ASSO-019">1004-ASSO-019</a>
|
||||
- **must** see an amount associated with each key <a name="1004-ASSO-020" href="#1004-ASSO-020">1004-ASSO-020</a>
|
||||
- **must** see the full Vega public key associated too <a name="1004-ASSO-021" href="#1004-ASSO-021">1004-ASSO-021</a>
|
||||
- **must** see the the origin of the association: wallet or vesting contract <a name="1004-ASSO-022" href="#1004-ASSO-022">1004-ASSO-022</a>
|
||||
- **should** be able to select one row (vega key + amount) to populate disassociate form with the key you want to disassociate from and how much <a name="1004-ASSO-023" href="#1004-ASSO-023">1004-ASSO-023</a>
|
||||
- If some of the tokens for the given Eth key are held by the vesting contract: **must** select to return tokens to Vesting contract <a name="1004-ASSO-024" href="#1004-ASSO-024">1004-ASSO-024</a>
|
||||
- **must** select an amount of tokens to disassociate <a name="1004-ASSO-031" href="#1004-ASSO-031">1004-ASSO-031</a>
|
||||
- **must** be able to populate the input with the amount of associated tokens for the selected Vega wallet/contract <a name="1004-ASSO-025" href="#1004-ASSO-025">1004-ASSO-025</a>
|
||||
- **should** be warned that disassociating will forfeit and rewards for the current epoch and reduce the Vote weigh on any open proposals
|
||||
- **must** be warned if the inputs on the form will result in an invalid withdraw, before submitting <a name="1004-ASSO-026" href="#1004-ASSO-026">1004-ASSO-026</a>
|
||||
- **must** action the disassociation [Ethereum transaction](0005-ETXN-submit_ethereum_transaction.md) <a name="1004-ASSO-027" href="#1004-ASSO-027">1004-ASSO-027</a>
|
||||
- **must** see feedback on the progress of the disassociation on ethereum <a name="1004-ASSO-028" href="#1004-ASSO-028">1004-ASSO-028</a>
|
||||
- **must** see new associated balances in Vega (theses should be applied instantly, rather than wait for the 50 eth blocks like associate) <a name="1004-ASSO-029" href="#1004-ASSO-029">1004-ASSO-029</a>
|
||||
- on completion (if tokens were returned to vesting contract): **could** be prompted to go on to [redeem](1001-VEST-vesting.md).
|
||||
|
||||
...so that I can transfer them to another Ethereum wallet (e.g. sell them on an exchange).
|
||||
@@ -0,0 +1,80 @@
|
||||
# Vesting
|
||||
|
||||
Some governance tokens may be held by a Vesting contract. This means that can be "owned" by an Ethereum key but not freely transferred until a vesting terms are complete.
|
||||
|
||||
## list of tranches
|
||||
|
||||
When looking to understand to overall vesting schedule for tokens, I...
|
||||
|
||||
- **must** see a list of tranches <a name="1005-VEST-001" href="#1005-VEST-001">1005-VEST-001</a>
|
||||
- **should** see a visualization of the vesting schedule with a break down of the type of token holders (e.g. team, investors, community) <a name="1005-VEST-002" href="#1005-VEST-002">1005-VEST-002</a>
|
||||
|
||||
For each tranche:
|
||||
|
||||
- **must** see a tranche number <a name="1005-VEST-003" href="#1005-VEST-003">1005-VEST-003</a>
|
||||
- **could** see any annotation of what this tranche is about (e.g. community schedule A)
|
||||
- **must** see a sum of tokens in the tranche <a name="1005-VEST-005" href="#1005-VEST-005">1005-VEST-005</a>
|
||||
- **must** see how many tokens in the tranche are locked <a name="1005-VEST-006" href="#1005-VEST-006">1005-VEST-006</a>
|
||||
- **must** see how how many tokens in the tranche are redeemable <a name="1005-VEST-007" href="#1005-VEST-007">1005-VEST-007</a>
|
||||
- **must** see the vesting terms for each tranche (when unlocking stats and ends) <a name="1005-VEST-008" href="#1005-VEST-008">1005-VEST-008</a>
|
||||
|
||||
... so I can understand how circulating supply could change over time.
|
||||
|
||||
## Details of a tranche
|
||||
|
||||
When looking into a specific tranche, I...
|
||||
|
||||
- **must** see all the same details as the [list of tranches](#details-of-a-tranche)
|
||||
- **should** see a list of ethereum wallets with tokens in this tranche
|
||||
|
||||
for each ethereum wallet:
|
||||
|
||||
- **should** see the full eth address of the wallet
|
||||
- **should** see the total tokens this address holds in this tranche
|
||||
- **should** see how many tokens in the tranche are locked
|
||||
- **should** see how how many tokens in the tranche are redeemable
|
||||
|
||||
... so I can see the details of how tokens are distributed in this tranche
|
||||
|
||||
## See summary for a given Ethereum key
|
||||
|
||||
When looking to see how many tokens I have in total, and how many I might be able to redeem, I...
|
||||
|
||||
- **must** be able to [Connect and ethereum wallet](0004-EWAL-connect_ethereum_wallet.md) <a name="1005-VEST-018" href="#1005-VEST-018">1005-VEST-018</a>
|
||||
- **should** be able input an ethereum address
|
||||
|
||||
for the a given Ethereum wallet/address/key:
|
||||
|
||||
- **must** see a total of tokens across all tranches <a name="1005-VEST-020" href="#1005-VEST-020">1005-VEST-020</a>
|
||||
- **must** see how many tokens across all tranches are locked <a name="1005-VEST-021" href="#1005-VEST-021">1005-VEST-021</a>
|
||||
- **must** see how many tokens across all tranches are redeemable <a name="1005-VEST-022" href="#1005-VEST-022">1005-VEST-022</a>
|
||||
- **must** see a list of tranches this key has tokens in <a name="1005-VEST-023" href="#1005-VEST-023">1005-VEST-023</a>
|
||||
- **must** see a total of tokens in each tranche <a name="1005-VEST-024" href="#1005-VEST-024">1005-VEST-024</a>
|
||||
- **must** see how many tokens in each tranche are locked <a name="1005-VEST-025" href="#1005-VEST-025">1005-VEST-025</a>
|
||||
- **must** see how many tokens in each tranche are redeemable <a name="1005-VEST-026" href="#1005-VEST-026">1005-VEST-026</a>
|
||||
- **must** see an option to redeem from tranche <a name="1005-VEST-027" href="#1005-VEST-027">1005-VEST-027</a>
|
||||
- **must** be warned if amount that can be redeemed from that tranche is greater than the un-associated balance for that Eth key (because this will cause the redeem function to fail) <a name="1005-VEST-028" href="#1005-VEST-028">1005-VEST-028</a>
|
||||
- **should** see how many tokens I'd need to disassociate to be able to run the redeem function (this should be rounded up to avoid the transaction failing due to more tokens having unlocked since the user looked at the form)
|
||||
- **should** see link to [disassociate](1004-ASSO-associate.md)
|
||||
|
||||
... so I can easily see how many tokens I have, and can redeem.
|
||||
|
||||
## Redeem tokens from a tranche
|
||||
|
||||
Note: it is not possible to choose how many tokens you redeem from a tranche, instead you select a tranche and the smart contract will attempt to redeem all. However, it will fail if some of the amount it attempts to redeem have been associated to a Vega key. Therefore the job of this page is to help the user work out how many tokens to disassociate before they can successfully redeem.
|
||||
|
||||
When looking to redeem tokens, I...
|
||||
|
||||
- **must** [connect the ethereum wallet](0004-EWAL-connect_ethereum_wallet.md) that holds tokens <a name="1005-VEST-029" href="#1005-VEST-029">1005-VEST-029</a>
|
||||
- must see see all tranches that you have tokens in (including tranche 0) <a name="1005-VEST-036" href="#1005-VEST-036">1005-VEST-036</a>
|
||||
- must see a total of tokens across all tranches (including tranche 0) <a name="1005-VEST-037" href="#1005-VEST-037">1005-VEST-037</a>
|
||||
- **must** select a tranche to redeem from <a name="1005-VEST-030" href="#1005-VEST-030">1005-VEST-030</a>
|
||||
- **must** see the number of tokens that can be redeemed <a name="1005-VEST-031" href="#1005-VEST-031">1005-VEST-031</a>
|
||||
- **must** be warned if the number of tokens you would be attempting to redeem is greater than you have unassociated <a name="1005-VEST-035" href="#1005-VEST-035">1005-VEST-035</a>
|
||||
- **should** tell you how many tokens to disassociate for the redeem function to work (should round up to create a buffer for the tokens that may unlock between now and when the user gets to the disassociate form)
|
||||
- **should** see a link to disassociate
|
||||
- **must** submit the redeem from tranche [ethereum transaction](0005-ETXN-submit_ethereum_transaction.md) <a name="1005-VEST-032" href="#1005-VEST-032">1005-VEST-032</a>
|
||||
- **must** get feedback on the progress of the Ethereum transaction <a name="1005-VEST-033" href="#1005-VEST-033">1005-VEST-033</a>
|
||||
- **must** see updated balances (in the trance and my eth wallet) after redemption <a name="1005-VEST-034" href="#1005-VEST-034">1005-VEST-034</a>
|
||||
|
||||
... so that I can use this tokens more generally on Ethereum (transfer to another key etc)
|
||||
@@ -0,0 +1,121 @@
|
||||
# Staking
|
||||
|
||||
Staking is the act of securing a Vega network by nominating good validators with the [governance token](../protocol/0071-STAK-erc20_governance_token_staking.md). Staking is rewarded with a share of trading fees (and [treasury rewards](../0056-REWA-rewards_overview.md)). See the [glossary](../glossaries/staking-and-governance.md) and [these specs](../protocol#delegation-staking-and-rewards) for more on staking.
|
||||
|
||||
When staking a user may be motivated to select validators to maximize the rewards they get for the tokens they hold, this means selecting validator(s) who are less likely to be penalized (e.g. over staked, poor performance). Users may wish to stake more than one validator to diversify. Users will want/need to manage their stake over time to ensure they are getting a good return, e.g. move stake between validators. Staking is also important for facilitating protocol upgrades.
|
||||
|
||||
## Understand staking on Vega
|
||||
|
||||
When considering whether to stake on Vega, I...
|
||||
|
||||
- **should** see information to help inform me what return I might expect from staking (other protocols might show a typical APY)
|
||||
- **must** see that the governance token is an ethereum ERC20 token and needs to attributed (or associated) to a Vega wallet for use on Vega <a name="1002-STKE-002" href="#1002-STKE-002">1002-STKE-002</a>
|
||||
- **must** see detailed documentation on how staking works on Vega <a name="1002-STKE-003" href="#1002-STKE-003">1002-STKE-003</a>
|
||||
|
||||
...so I can decide if I want to stake on Vega, and how to go about doing it.
|
||||
|
||||
Note: There are many ways that "understanding the return" can be done, and this does not impose a particular solution. Solutions could...
|
||||
|
||||
- look at previous epochs,
|
||||
- average this over a period,
|
||||
- select a range of validators or just one,
|
||||
- could have a calculator that allows the user to enter some values or just show this on the list of validators
|
||||
|
||||
Note: Income may come in a range of different tokens, as markets can settle in different assets, and there may be rewards paid out by the treasury.
|
||||
|
||||
## Associate tokens
|
||||
|
||||
Before I stake, I need to [Associate tokens](./1000-ASSO-associate.md) with a Vega wallet/key...
|
||||
|
||||
- **must** see link[Associate tokens](./1000-ASSO-associate.md)
|
||||
- **should** see that if no further action is taken, newly associated tokens will be nominated to validators based on existing distribution
|
||||
|
||||
...so that I can nominate validators.
|
||||
|
||||
## Select validator(s)
|
||||
|
||||
When selecting what validators to nominate with my stake, I...
|
||||
|
||||
- **should** be able to select any data that is available on Validators in a table
|
||||
|
||||
- **must** see all validator information without having to connect Vega wallet <a name="1002-STKE-050" href="#1002-STKE-050">1002-STKE-050</a>
|
||||
- see "static" information about the validator (these can change, just not frequently)
|
||||
- **must** see name <a name="1002-STKE-006" href="#1002-STKE-006">1002-STKE-006</a>
|
||||
- **must** see Vega public key <a name="1002-STKE-008" href="#1002-STKE-008">1002-STKE-008</a>
|
||||
- **should** see a URL where to find more information about the validator (if there is one)
|
||||
- **must** see Ethereum address<a name="1002-STKE-010" href="#1002-STKE-010">1002-STKE-010</a>
|
||||
- can see data for the current/next epoch, for each validator
|
||||
- **must** see the current "status" (consensus, Ersatz, New etc) <a name="1002-STKE-011" href="#1002-STKE-011">1002-STKE-011</a>
|
||||
- **must** see a total stake (inc self stake) <a name="1002-STKE-012" href="#1002-STKE-012">1002-STKE-012</a>
|
||||
- **must** see self stake <a name="1002-STKE-013" href="#1002-STKE-013">1002-STKE-013</a>
|
||||
- **must** see nominated stake <a name="1002-STKE-014" href="#1002-STKE-014">1002-STKE-014</a>
|
||||
- **must** see total stake as a % of total staked across all nodes <a name="1002-STKE-051" href="#1002-STKE-051">1002-STKE-051</a>
|
||||
- **must** see total stake change next epoch <a name="1002-STKE-015" href="#1002-STKE-015">1002-STKE-015</a>
|
||||
- **must** see self stake <a name="1002-STKE-016" href="#1002-STKE-016">1002-STKE-016</a>
|
||||
- **must** see nominated stake <a name="1002-STKE-017" href="#1002-STKE-017">1002-STKE-017</a>
|
||||
- **must** see total stake as a % change <a name="1002-STKE-052" href="#1002-STKE-052">1002-STKE-052</a>
|
||||
- **should** see the version of Vega they are currently running
|
||||
- **should** see the version of Vega they propose running
|
||||
- can see data for the previous epoch
|
||||
- **must** see the overall "score" for a validator for the previous epoch <a name="1002-STKE-020" href="#1002-STKE-020">1002-STKE-020</a>
|
||||
- can see all the inputs to that "score"
|
||||
- **must** see Ranking score <a name="1002-STKE-021" href="#1002-STKE-021">1002-STKE-021</a>
|
||||
- **must** see stake score <a name="1002-STKE-022" href="#1002-STKE-022">1002-STKE-022</a>
|
||||
- **must** see performance score <a name="1002-STKE-023" href="#1002-STKE-023">1002-STKE-023</a>
|
||||
- **must** see voting score <a name="1002-STKE-024" href="#1002-STKE-024">1002-STKE-024</a>
|
||||
- can see data for previous epochs
|
||||
- **should** see the the overall "score" for all previous epochs for each validator <a name="1002-STKE-025" href="#1002-STKE-025">1002-STKE-025</a>
|
||||
- can see a breakdown of all the inputs to that "score" for all previous epochs
|
||||
- **should** see Ranking score
|
||||
- **should** see stake score
|
||||
- **should** see performance score
|
||||
- **should** see voting score
|
||||
|
||||
...so I can select validators that should give me the biggest return.
|
||||
|
||||
## Nominate a validator
|
||||
|
||||
Note: User interfaces may use the term "Nominate", technically the function is called "delegate". Delegating tokens to a validator may imply that you also give that validator your vote on proposals, at time of writing, it does not. It only gives them the potential for more "voting power" in the production of blocks.
|
||||
|
||||
Within a staking epoch (typically 24 hours) a user can change their nominations many times, however the changes are only effective at the end of the epoch. You will only get rewards for a full epoch staked.
|
||||
|
||||
When attributing some (or all of my governance tokens to a given validator), I...
|
||||
|
||||
- **must** select a validator I want to nominate <a name="1002-STKE-031" href="#1002-STKE-031">1002-STKE-031</a>
|
||||
- **must** see link to [connect to a Vega wallet/key](0002-WCON-connect_vega_wallet.md) (if not already) that has associated Vega (or Pending association) <a name="1002-STKE-032" href="#1002-STKE-032">1002-STKE-032</a>
|
||||
- **must** select an amount of tokens <a name="1002-STKE-033" href="#1002-STKE-033">1002-STKE-033</a>
|
||||
- **must** be able to populate this with the amount of governance tokens that will be associated but not nominated at the beginning of the next epoch <a name="1002-STKE-034" href="#1002-STKE-034">1002-STKE-034</a>
|
||||
- **must** be warned if the amount I am about to nominate is below a minimum amount (spam protection) <a name="1002-STKE-035" href="#1002-STKE-035">1002-STKE-035</a>
|
||||
- **must** be warned if the amount I am about to nominate is more than I have associated + un-nominated at the end of current epoch/beginning of next <a name="1002-STKE-036" href="#1002-STKE-036">1002-STKE-036</a>
|
||||
- **must** submit the nomination [Vega transactions](0003-WTXN-submit_vega_transaction.md) <a name="1002-STKE-037" href="#1002-STKE-037">1002-STKE-037</a>
|
||||
- **must** see feedback that my nomination has been registered, and will be processed at the next epoch <a name="1002-STKE-038" href="#1002-STKE-038">1002-STKE-038</a>
|
||||
- **must** see all my pending nomination changes for the next epoch <a name="1002-STKE-039" href="#1002-STKE-039">1002-STKE-039</a>
|
||||
|
||||
...so that I am rewarded for a share based on this validators performance.
|
||||
|
||||
## Monitor staking rewards
|
||||
|
||||
When checking if im getting the staking return that I was expecting, I...
|
||||
|
||||
- See [Staking income](2002-SINC-staking-income.md)
|
||||
|
||||
...so that I can make decisions about my staking, e.g. whether to re-distribute my stake.
|
||||
|
||||
## Un-nominate validator
|
||||
|
||||
When removing stake from a validator, I...
|
||||
|
||||
- **must** select a validator I want to un-nominate <a name="1002-STKE-040" href="#1002-STKE-040">1002-STKE-040</a>
|
||||
- **must** [connect to a Vega wallet/key](0002-WCON-connect_vega_wallet.md) with nominated stake, if not already <a name="1002-STKE-041" href="#1002-STKE-041">1002-STKE-041</a>
|
||||
- - **must** have the option of withdrawing nominated amount at the end of the epoch (and maintain the staking income for the current epoch) <a name="1002-STKE-053" href="#1002-STKE-053">1002-STKE-053</a>
|
||||
- **should** have the option of withdrawing nomination amount now immediately (and forfeit the staking income)
|
||||
- **must** set an amount to remove from a validator <a name="1002-STKE-044" href="#1002-STKE-044">1002-STKE-044</a>
|
||||
- **must** be able populate with the total delegated amount at the time where un-nominate will happen <a name="1002-STKE-045" href="#1002-STKE-045">1002-STKE-045</a>
|
||||
- **must** be warned if amount is greater than the amount that will be on that validator at the end of the epoch <a name="1002-STKE-046" href="#1002-STKE-046">1002-STKE-046</a>
|
||||
<a name="1002-STKE-047" href="#1002-STKE-047">1002-STKE-047</a>
|
||||
- **must** submit un-nominate [Vega transaction](0003-WTXN-submit_vega_transaction.md) <a name="1002-STKE-048" href="#1002-STKE-048">1002-STKE-048</a>
|
||||
- **must** see feedback that the un-nomination has been registered, and that the un-nominated amount is now available for re-nomination <a name="1002-STKE-049" href="#1002-STKE-049">1002-STKE-049</a>
|
||||
|
||||
... so that I can use this stake for another validator etc.
|
||||
|
||||
note: if a user just wishes to seel their tokens and not wait for the end of an epoch they could simply [disassociate](1004-ASSO-associate.md#disassociate) (as long as tokens are not held by vesting contract)
|
||||
@@ -0,0 +1,31 @@
|
||||
# Staking income
|
||||
|
||||
## Monitor staking rewards
|
||||
|
||||
When checking the staking rewards, I...
|
||||
|
||||
- must be [connected to a Vega wallet](0002-WCON-connect_vega_wallet.md)
|
||||
|
||||
- **must** see when the current epoch ends <a name="2002-SINC-001" href="#2002-SINC-001">2002-SINC-001</a>
|
||||
- **should** see when the current epoch started
|
||||
- **should** see how much the connected wallet might earn at the end of the epoch (with some assumptions)
|
||||
- **should** see the balance of all infrastructure fee accounts for the current epoch
|
||||
- **should** see real time validator score for the current epoch
|
||||
- **should** see if any validators have had a penalty this epoch
|
||||
- **must** see the sum of Tokens I have nominated to each validator in the current epoch <a name="2002-SINC-007" href="#2002-SINC-007">2002-SINC-007</a>
|
||||
- **should** see what percentage of my Tokens I have nominated to each validator in the current epoch
|
||||
- **must** see the staking income I have received for each epoch previous <a name="2002-SINC-009" href="#2002-SINC-009">2002-SINC-009</a>
|
||||
- **must** see asset name <a name="2002-SINC-010" href="#2002-SINC-010">2002-SINC-010</a>
|
||||
- **must** see balance of asset <a name="2002-SINC-011" href="#2002-SINC-011">2002-SINC-011</a>
|
||||
- **must** see type (organic or treasury) <a name="2002-SINC-012" href="#2002-SINC-012">2002-SINC-012</a>
|
||||
- **should** see the staking income by epoch, broken down by validator
|
||||
- **should** see the staking income by epoch, broken down by market
|
||||
|
||||
- **should** see all income values expressed in a single currency
|
||||
|
||||
- **must** see current asset balances for connected wallets <a name="2002-SINC-016" href="#2002-SINC-016">2002-SINC-016</a>
|
||||
- **must** see link to [withdraw](1002-WITH-withdraw.md) assets <a name="2002-SINC-017" href="#2002-SINC-017">2002-SINC-017</a>
|
||||
|
||||
- **must** see where I can see where I did not receive full income because the validator suffered penalties <a name="2002-SINC-018" href="#2002-SINC-018">2002-SINC-018</a>
|
||||
|
||||
...so that I can make decisions about my staking
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user