feat(trading): merge deal ticket stores (#4494)
This commit is contained in:
parent
aac0c25d09
commit
9e4ba9f275
@ -63,6 +63,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
|||||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
|
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
|
||||||
cy.getByTestId(orderSizeField).clear().type('1');
|
cy.getByTestId(orderSizeField).clear().type('1');
|
||||||
cy.getByTestId(orderPriceField).clear().type('1.123456');
|
cy.getByTestId(orderPriceField).clear().type('1.123456');
|
||||||
|
cy.getByTestId(placeOrderBtn).click();
|
||||||
cy.getByTestId('deal-ticket-error-message-price-limit').should(
|
cy.getByTestId('deal-ticket-error-message-price-limit').should(
|
||||||
'have.text',
|
'have.text',
|
||||||
'Price accepts up to 5 decimal places'
|
'Price accepts up to 5 decimal places'
|
||||||
@ -73,6 +74,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
|||||||
describe('market order', () => {
|
describe('market order', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.getByTestId(toggleMarket).click();
|
cy.getByTestId(toggleMarket).click();
|
||||||
|
cy.getByTestId(placeOrderBtn).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('must not see the price unit', function () {
|
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(orderPriceField).clear().type('0.1');
|
||||||
cy.getByTestId(orderSizeField).clear().type('1');
|
cy.getByTestId(orderSizeField).clear().type('1');
|
||||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||||
|
cy.getByTestId(placeOrderBtn).click();
|
||||||
cy.getByTestId('deal-ticket-warning-auction').should(
|
cy.getByTestId('deal-ticket-warning-auction').should(
|
||||||
'have.text',
|
'have.text',
|
||||||
'Any orders placed now will not trade until the auction ends'
|
'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
|
TIFlist.filter((item) => item.code === 'FOK')[0].value
|
||||||
);
|
);
|
||||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||||
|
cy.getByTestId(placeOrderBtn).click();
|
||||||
cy.getByTestId('deal-ticket-error-message-tif').should(
|
cy.getByTestId('deal-ticket-error-message-tif').should(
|
||||||
'have.text',
|
'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'
|
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
|
||||||
|
@ -1,27 +1,15 @@
|
|||||||
import { OrderbookManager } from '@vegaprotocol/market-depth';
|
import { OrderbookManager } from '@vegaprotocol/market-depth';
|
||||||
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
|
||||||
import { ViewType, useSidebar } from '../sidebar';
|
import { ViewType, useSidebar } from '../sidebar';
|
||||||
import { useStopOrderFormValues } from '@vegaprotocol/deal-ticket';
|
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
|
||||||
|
|
||||||
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
||||||
const useOrderStoreRef = useCreateOrderStore();
|
const update = useDealTicketFormValues((state) => state.updateAll);
|
||||||
const updateOrder = useOrderStoreRef((store) => store.update);
|
|
||||||
const updateStoredFormValues = useStopOrderFormValues(
|
|
||||||
(state) => state.update
|
|
||||||
);
|
|
||||||
const setView = useSidebar((store) => store.setView);
|
const setView = useSidebar((store) => store.setView);
|
||||||
return (
|
return (
|
||||||
<OrderbookManager
|
<OrderbookManager
|
||||||
marketId={marketId}
|
marketId={marketId}
|
||||||
onClick={({ price, size }) => {
|
onClick={(values) => {
|
||||||
if (price) {
|
update(marketId, values);
|
||||||
updateOrder(marketId, { price });
|
|
||||||
updateStoredFormValues(marketId, { price });
|
|
||||||
}
|
|
||||||
if (size) {
|
|
||||||
updateOrder(marketId, { size });
|
|
||||||
updateStoredFormValues(marketId, { size });
|
|
||||||
}
|
|
||||||
setView({ type: ViewType.Order });
|
setView({ type: ViewType.Order });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -3,29 +3,25 @@ import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
|||||||
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
||||||
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import type { OrderObj } from '@vegaprotocol/orders';
|
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||||
import type { OrderFormFields } from '../../hooks/use-order-form';
|
|
||||||
|
|
||||||
export interface DealTicketAmountProps {
|
export interface DealTicketAmountProps {
|
||||||
control: Control<OrderFormFields>;
|
control: Control<OrderFormValues>;
|
||||||
orderType: Schema.OrderType;
|
type: Schema.OrderType;
|
||||||
marketData: StaticMarketData;
|
marketData: StaticMarketData;
|
||||||
marketPrice?: string;
|
marketPrice?: string;
|
||||||
market: Market;
|
market: Market;
|
||||||
sizeError?: string;
|
sizeError?: string;
|
||||||
priceError?: string;
|
priceError?: string;
|
||||||
update: (obj: Partial<OrderObj>) => void;
|
|
||||||
size: string;
|
|
||||||
price?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DealTicketAmount = ({
|
export const DealTicketAmount = ({
|
||||||
orderType,
|
type,
|
||||||
marketData,
|
marketData,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
...props
|
...props
|
||||||
}: DealTicketAmountProps) => {
|
}: DealTicketAmountProps) => {
|
||||||
switch (orderType) {
|
switch (type) {
|
||||||
case Schema.OrderType.TYPE_MARKET:
|
case Schema.OrderType.TYPE_MARKET:
|
||||||
return (
|
return (
|
||||||
<DealTicketMarketAmount
|
<DealTicketMarketAmount
|
||||||
@ -37,7 +33,7 @@ export const DealTicketAmount = ({
|
|||||||
case Schema.OrderType.TYPE_LIMIT:
|
case Schema.OrderType.TYPE_LIMIT:
|
||||||
return <DealTicketLimitAmount {...props} />;
|
return <DealTicketLimitAmount {...props} />;
|
||||||
default: {
|
default: {
|
||||||
throw new Error('Invalid ticket type');
|
throw new Error('Invalid ticket type ' + type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||||
import {
|
import {
|
||||||
DealTicketType,
|
isStopOrderType,
|
||||||
useDealTicketTypeStore,
|
useDealTicketFormValues,
|
||||||
} from '../../hooks/use-type-store';
|
} from '../../hooks/use-form-values';
|
||||||
import { StopOrder } from './deal-ticket-stop-order';
|
import { StopOrder } from './deal-ticket-stop-order';
|
||||||
import {
|
import {
|
||||||
useStaticMarketData,
|
useStaticMarketData,
|
||||||
@ -25,7 +25,9 @@ export const DealTicketContainer = ({
|
|||||||
marketId,
|
marketId,
|
||||||
...props
|
...props
|
||||||
}: DealTicketContainerProps) => {
|
}: DealTicketContainerProps) => {
|
||||||
const type = useDealTicketTypeStore((state) => state.type[marketId]);
|
const showStopOrder = useDealTicketFormValues((state) =>
|
||||||
|
isStopOrderType(state.formValues[marketId]?.type)
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
data: market,
|
data: market,
|
||||||
error: marketError,
|
error: marketError,
|
||||||
@ -48,9 +50,7 @@ export const DealTicketContainer = ({
|
|||||||
reload={reload}
|
reload={reload}
|
||||||
>
|
>
|
||||||
{market && marketData ? (
|
{market && marketData ? (
|
||||||
FLAGS.STOP_ORDERS &&
|
FLAGS.STOP_ORDERS && showStopOrder ? (
|
||||||
(type === DealTicketType.StopLimit ||
|
|
||||||
type === DealTicketType.StopMarket) ? (
|
|
||||||
<StopOrder
|
<StopOrder
|
||||||
market={market}
|
market={market}
|
||||||
marketPrice={marketPrice}
|
marketPrice={marketPrice}
|
||||||
|
@ -5,8 +5,8 @@ import type { DealTicketAmountProps } from './deal-ticket-amount';
|
|||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
export type DealTicketLimitAmountProps = Omit<
|
export type DealTicketLimitAmountProps = Omit<
|
||||||
Omit<DealTicketAmountProps, 'marketData'>,
|
DealTicketAmountProps,
|
||||||
'orderType'
|
'marketData' | 'type'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const DealTicketLimitAmount = ({
|
export const DealTicketLimitAmount = ({
|
||||||
@ -14,9 +14,6 @@ export const DealTicketLimitAmount = ({
|
|||||||
market,
|
market,
|
||||||
sizeError,
|
sizeError,
|
||||||
priceError,
|
priceError,
|
||||||
update,
|
|
||||||
price,
|
|
||||||
size,
|
|
||||||
}: DealTicketLimitAmountProps) => {
|
}: DealTicketLimitAmountProps) => {
|
||||||
const priceStep = toDecimal(market?.decimalPlaces);
|
const priceStep = toDecimal(market?.decimalPlaces);
|
||||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||||
@ -62,17 +59,16 @@ export const DealTicketLimitAmount = ({
|
|||||||
},
|
},
|
||||||
validate: validateAmount(sizeStep, 'Size'),
|
validate: validateAmount(sizeStep, 'Size'),
|
||||||
}}
|
}}
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
id="input-order-size-limit"
|
id="input-order-size-limit"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
value={size}
|
|
||||||
onChange={(e) => update({ size: e.target.value })}
|
|
||||||
step={sizeStep}
|
step={sizeStep}
|
||||||
min={sizeStep}
|
min={sizeStep}
|
||||||
data-testid="order-size"
|
data-testid="order-size"
|
||||||
onWheel={(e) => e.currentTarget.blur()}
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -95,19 +91,17 @@ export const DealTicketLimitAmount = ({
|
|||||||
value: priceStep,
|
value: priceStep,
|
||||||
message: t('Price cannot be lower than ' + priceStep),
|
message: t('Price cannot be lower than ' + priceStep),
|
||||||
},
|
},
|
||||||
// @ts-ignore this fulfills the interface but still errors
|
|
||||||
validate: validateAmount(priceStep, 'Price'),
|
validate: validateAmount(priceStep, 'Price'),
|
||||||
}}
|
}}
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
id="input-price-quote"
|
id="input-price-quote"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
value={price}
|
|
||||||
onChange={(e) => update({ price: e.target.value })}
|
|
||||||
step={priceStep}
|
step={priceStep}
|
||||||
data-testid="order-price"
|
data-testid="order-price"
|
||||||
onWheel={(e) => e.currentTarget.blur()}
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -10,10 +10,7 @@ import type { DealTicketAmountProps } from './deal-ticket-amount';
|
|||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export type DealTicketMarketAmountProps = Omit<
|
export type DealTicketMarketAmountProps = Omit<DealTicketAmountProps, 'type'>;
|
||||||
DealTicketAmountProps,
|
|
||||||
'orderType'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const DealTicketMarketAmount = ({
|
export const DealTicketMarketAmount = ({
|
||||||
control,
|
control,
|
||||||
@ -21,8 +18,6 @@ export const DealTicketMarketAmount = ({
|
|||||||
marketData,
|
marketData,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
sizeError,
|
sizeError,
|
||||||
update,
|
|
||||||
size,
|
|
||||||
}: DealTicketMarketAmountProps) => {
|
}: DealTicketMarketAmountProps) => {
|
||||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||||
@ -50,17 +45,16 @@ export const DealTicketMarketAmount = ({
|
|||||||
},
|
},
|
||||||
validate: validateAmount(sizeStep, 'Size'),
|
validate: validateAmount(sizeStep, 'Size'),
|
||||||
}}
|
}}
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
id="input-order-size-market"
|
id="input-order-size-market"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
value={size}
|
|
||||||
onChange={(e) => update({ size: e.target.value })}
|
|
||||||
step={sizeStep}
|
step={sizeStep}
|
||||||
min={sizeStep}
|
min={sizeStep}
|
||||||
onWheel={(e) => e.currentTarget.blur()}
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
data-testid="order-size"
|
data-testid="order-size"
|
||||||
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Controller, type Control } from 'react-hook-form';
|
import { Controller, type Control } from 'react-hook-form';
|
||||||
import type { Market } from '@vegaprotocol/markets';
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
import type { OrderObj } from '@vegaprotocol/orders';
|
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||||
import type { OrderFormFields } from '../../hooks/use-order-form';
|
|
||||||
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
|
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import {
|
import {
|
||||||
@ -12,25 +11,21 @@ import {
|
|||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
export interface DealTicketSizeIcebergProps {
|
export interface DealTicketSizeIcebergProps {
|
||||||
control: Control<OrderFormFields>;
|
control: Control<OrderFormValues>;
|
||||||
market: Market;
|
market: Market;
|
||||||
peakSizeError?: string;
|
peakSizeError?: string;
|
||||||
minimumVisibleSizeError?: string;
|
minimumVisibleSizeError?: string;
|
||||||
update: (obj: Partial<OrderObj>) => void;
|
|
||||||
peakSize: string;
|
|
||||||
minimumVisibleSize: string;
|
|
||||||
size: string;
|
size: string;
|
||||||
|
peakSize?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DealTicketSizeIceberg = ({
|
export const DealTicketSizeIceberg = ({
|
||||||
control,
|
control,
|
||||||
market,
|
market,
|
||||||
update,
|
|
||||||
peakSizeError,
|
peakSizeError,
|
||||||
minimumVisibleSizeError,
|
minimumVisibleSizeError,
|
||||||
peakSize,
|
|
||||||
minimumVisibleSize,
|
|
||||||
size,
|
size,
|
||||||
|
peakSize,
|
||||||
}: DealTicketSizeIcebergProps) => {
|
}: DealTicketSizeIcebergProps) => {
|
||||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||||
|
|
||||||
@ -80,7 +75,7 @@ export const DealTicketSizeIceberg = ({
|
|||||||
className="!mb-1"
|
className="!mb-1"
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="icebergOpts.peakSize"
|
name="peakSize"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
required: t('You need to provide a peak size'),
|
required: t('You need to provide a peak size'),
|
||||||
@ -97,25 +92,17 @@ export const DealTicketSizeIceberg = ({
|
|||||||
},
|
},
|
||||||
validate: validateAmount(sizeStep, 'peakSize'),
|
validate: validateAmount(sizeStep, 'peakSize'),
|
||||||
}}
|
}}
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
id="input-order-peak-size"
|
id="input-order-peak-size"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
value={peakSize}
|
|
||||||
onChange={(e) =>
|
|
||||||
update({
|
|
||||||
icebergOpts: {
|
|
||||||
peakSize: e.target.value,
|
|
||||||
minimumVisibleSize,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
step={sizeStep}
|
step={sizeStep}
|
||||||
min={sizeStep}
|
min={sizeStep}
|
||||||
max={size}
|
max={size}
|
||||||
data-testid="order-peak-size"
|
data-testid="order-peak-size"
|
||||||
onWheel={(e) => e.currentTarget.blur()}
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -144,7 +131,7 @@ export const DealTicketSizeIceberg = ({
|
|||||||
className="!mb-1"
|
className="!mb-1"
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="icebergOpts.minimumVisibleSize"
|
name="minimumVisibleSize"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
required: t('You need to provide a minimum visible size'),
|
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
|
'Minimum visible size cannot be lower than ' + sizeStep
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
max: {
|
max: peakSize && {
|
||||||
value: peakSize,
|
value: peakSize,
|
||||||
message: t(
|
message: t(
|
||||||
'Minimum visible size cannot be greater than the peak size (%s)',
|
'Minimum visible size cannot be greater than the peak size (%s)',
|
||||||
@ -163,25 +150,17 @@ export const DealTicketSizeIceberg = ({
|
|||||||
},
|
},
|
||||||
validate: validateAmount(sizeStep, 'minimumVisibleSize'),
|
validate: validateAmount(sizeStep, 'minimumVisibleSize'),
|
||||||
}}
|
}}
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
id="input-order-minimum-size"
|
id="input-order-minimum-size"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="number"
|
type="number"
|
||||||
value={minimumVisibleSize}
|
|
||||||
onChange={(e) =>
|
|
||||||
update({
|
|
||||||
icebergOpts: {
|
|
||||||
peakSize,
|
|
||||||
minimumVisibleSize: e.target.value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
step={sizeStep}
|
step={sizeStep}
|
||||||
min={sizeStep}
|
min={sizeStep}
|
||||||
max={peakSize}
|
max={peakSize}
|
||||||
data-testid="order-minimum-size"
|
data-testid="order-minimum-size"
|
||||||
onWheel={(e) => e.currentTarget.blur()}
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -6,8 +6,11 @@ import { generateMarket } from '../../test-helpers';
|
|||||||
import { StopOrder } from './deal-ticket-stop-order';
|
import { StopOrder } from './deal-ticket-stop-order';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import type { StopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
import type { StopOrderFormValues } from '../../hooks/use-form-values';
|
||||||
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
import {
|
||||||
|
DealTicketType,
|
||||||
|
useDealTicketFormValues,
|
||||||
|
} from '../../hooks/use-form-values';
|
||||||
import type { FeatureFlags } from '@vegaprotocol/environment';
|
import type { FeatureFlags } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
jest.mock('zustand');
|
jest.mock('zustand');
|
||||||
@ -131,9 +134,11 @@ describe('StopOrder', () => {
|
|||||||
expiresAt: '2023-07-27T16:43:27.000',
|
expiresAt: '2023-07-27T16:43:27.000',
|
||||||
};
|
};
|
||||||
|
|
||||||
useStopOrderFormValues.setState({
|
useDealTicketFormValues.setState({
|
||||||
formValues: {
|
formValues: {
|
||||||
[market.id]: values,
|
[market.id]: {
|
||||||
|
[DealTicketType.StopLimit]: values,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,11 +212,13 @@ describe('StopOrder', () => {
|
|||||||
// switch to market order type error should disappear
|
// switch to market order type error should disappear
|
||||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||||
|
|
||||||
// switch back to limit type
|
// switch back to limit type
|
||||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
await userEvent.click(screen.getByTestId(orderTypeLimit));
|
await userEvent.click(screen.getByTestId(orderTypeLimit));
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
// to small value should be invalid
|
// to small value should be invalid
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { FormEventHandler } from 'react';
|
|
||||||
import { useRef, useCallback, useEffect } from 'react';
|
import { useRef, useCallback, useEffect } from 'react';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
|
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
|
||||||
@ -8,7 +7,7 @@ import {
|
|||||||
toDecimal,
|
toDecimal,
|
||||||
validateAmount,
|
validateAmount,
|
||||||
} from '@vegaprotocol/utils';
|
} 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 * as Schema from '@vegaprotocol/types';
|
||||||
import {
|
import {
|
||||||
Radio,
|
Radio,
|
||||||
@ -24,22 +23,22 @@ import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { ExpirySelector } from './expiry-selector';
|
import { ExpirySelector } from './expiry-selector';
|
||||||
import { SideSelector } from './side-selector';
|
import { SideSelector } from './side-selector';
|
||||||
import { timeInForceLabel, useOrder } from '@vegaprotocol/orders';
|
import { timeInForceLabel } from '@vegaprotocol/orders';
|
||||||
import {
|
import {
|
||||||
NoWalletWarning,
|
NoWalletWarning,
|
||||||
REDUCE_ONLY_TOOLTIP,
|
REDUCE_ONLY_TOOLTIP,
|
||||||
useNotionalSize,
|
stopSubmit,
|
||||||
|
getNotionalSize,
|
||||||
} from './deal-ticket';
|
} from './deal-ticket';
|
||||||
import { TypeToggle } from './type-selector';
|
import { TypeToggle } from './type-selector';
|
||||||
import {
|
import {
|
||||||
useStopOrderFormValues,
|
useDealTicketFormValues,
|
||||||
type StopOrderFormValues,
|
|
||||||
} from '../../hooks/use-stop-order-form-values';
|
|
||||||
import {
|
|
||||||
DealTicketType,
|
DealTicketType,
|
||||||
useDealTicketTypeStore,
|
type StopOrderFormValues,
|
||||||
} from '../../hooks/use-type-store';
|
dealTicketTypeToOrderType,
|
||||||
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-stop-order-submission';
|
isStopOrderType,
|
||||||
|
} from '../../hooks/use-form-values';
|
||||||
|
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission';
|
||||||
import { DealTicketButton } from './deal-ticket-button';
|
import { DealTicketButton } from './deal-ticket-button';
|
||||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||||
import { validateExpiration } from '../../utils';
|
import { validateExpiration } from '../../utils';
|
||||||
@ -50,32 +49,36 @@ export interface StopOrderProps {
|
|||||||
submit: (order: StopOrdersSubmission) => void;
|
submit: (order: StopOrdersSubmission) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultValues: Partial<StopOrderFormValues> = {
|
const getDefaultValues = (
|
||||||
type: Schema.OrderType.TYPE_LIMIT,
|
type: Schema.OrderType,
|
||||||
|
storedValues?: Partial<StopOrderFormValues>
|
||||||
|
): StopOrderFormValues => ({
|
||||||
|
type,
|
||||||
side: Schema.Side.SIDE_BUY,
|
side: Schema.Side.SIDE_BUY,
|
||||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||||
triggerType: 'price',
|
triggerType: 'price',
|
||||||
triggerDirection:
|
triggerDirection:
|
||||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||||
|
expire: false,
|
||||||
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
|
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
|
||||||
size: '0',
|
size: '0',
|
||||||
};
|
...storedValues,
|
||||||
|
});
|
||||||
const stopSubmit: FormEventHandler = (e) => e.preventDefault();
|
|
||||||
|
|
||||||
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
const setType = useDealTicketFormValues((state) => state.setType);
|
||||||
const [, updateOrder] = useOrder(market.id);
|
const updateStoredFormValues = useDealTicketFormValues(
|
||||||
const updateStoredFormValues = useStopOrderFormValues(
|
(state) => state.updateStopOrder
|
||||||
(state) => state.update
|
|
||||||
);
|
);
|
||||||
const storedFormValues = useStopOrderFormValues(
|
const storedFormValues = useDealTicketFormValues(
|
||||||
(state) => state.formValues[market.id]
|
(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>({
|
useForm<StopOrderFormValues>({
|
||||||
defaultValues: { ...defaultValues, ...storedFormValues },
|
defaultValues: getDefaultValues(type, storedFormValues?.[dealTicketType]),
|
||||||
});
|
});
|
||||||
const { errors } = formState;
|
const { errors } = formState;
|
||||||
const lastSubmitTime = useRef(0);
|
const lastSubmitTime = useRef(0);
|
||||||
@ -102,16 +105,22 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
const triggerType = watch('triggerType');
|
const triggerType = watch('triggerType');
|
||||||
const triggerPrice = watch('triggerPrice');
|
const triggerPrice = watch('triggerPrice');
|
||||||
const timeInForce = watch('timeInForce');
|
const timeInForce = watch('timeInForce');
|
||||||
const type = watch('type');
|
|
||||||
const rawPrice = watch('price');
|
const rawPrice = watch('price');
|
||||||
const rawSize = watch('size');
|
const rawSize = watch('size');
|
||||||
|
|
||||||
if (storedFormValues?.size && rawSize !== storedFormValues?.size) {
|
useEffect(() => {
|
||||||
setValue('size', storedFormValues.size);
|
const size = storedFormValues?.[dealTicketType]?.size;
|
||||||
}
|
if (size && rawSize !== size) {
|
||||||
if (storedFormValues?.price && rawPrice !== storedFormValues?.price) {
|
setValue('size', size);
|
||||||
setValue('price', storedFormValues.price);
|
}
|
||||||
}
|
}, [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 isPriceTrigger = triggerType === 'price';
|
||||||
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
|
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
|
||||||
@ -127,7 +136,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
: marketPrice
|
: marketPrice
|
||||||
);
|
);
|
||||||
|
|
||||||
const notionalSize = useNotionalSize(
|
const notionalSize = getNotionalSize(
|
||||||
price,
|
price,
|
||||||
size,
|
size,
|
||||||
market.decimalPlaces,
|
market.decimalPlaces,
|
||||||
@ -153,47 +162,28 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
? formatNumber(triggerPrice, market.decimalPlaces)
|
? formatNumber(triggerPrice, market.decimalPlaces)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
useController({
|
||||||
|
name: 'type',
|
||||||
|
control,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
|
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
|
||||||
noValidate
|
noValidate
|
||||||
>
|
>
|
||||||
<Controller
|
<TypeToggle
|
||||||
name="type"
|
value={dealTicketType}
|
||||||
control={control}
|
onValueChange={(dealTicketType) => {
|
||||||
render={({ field }) => {
|
setType(market.id, dealTicketType);
|
||||||
const { value } = field;
|
if (isStopOrderType(dealTicketType)) {
|
||||||
return (
|
reset(
|
||||||
<TypeToggle
|
getDefaultValues(
|
||||||
value={
|
dealTicketTypeToOrderType(dealTicketType),
|
||||||
value === Schema.OrderType.TYPE_LIMIT
|
storedFormValues?.[dealTicketType]
|
||||||
? DealTicketType.StopLimit
|
)
|
||||||
: DealTicketType.StopMarket
|
);
|
||||||
}
|
}
|
||||||
onValueChange={(value) => {
|
|
||||||
const type = value as DealTicketType;
|
|
||||||
setDealTicketType(market.id, type);
|
|
||||||
if (
|
|
||||||
type === DealTicketType.Limit ||
|
|
||||||
type === DealTicketType.Market
|
|
||||||
) {
|
|
||||||
updateOrder({
|
|
||||||
type:
|
|
||||||
type === DealTicketType.Limit
|
|
||||||
? Schema.OrderType.TYPE_LIMIT
|
|
||||||
: Schema.OrderType.TYPE_MARKET,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setValue(
|
|
||||||
'type',
|
|
||||||
type === DealTicketType.StopLimit
|
|
||||||
? Schema.OrderType.TYPE_LIMIT
|
|
||||||
: Schema.OrderType.TYPE_MARKET
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{errors.type && (
|
{errors.type && (
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||||
import {
|
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||||
act,
|
|
||||||
render,
|
|
||||||
renderHook,
|
|
||||||
screen,
|
|
||||||
waitFor,
|
|
||||||
} from '@testing-library/react';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { generateMarket, generateMarketData } from '../../test-helpers';
|
import { generateMarket, generateMarketData } from '../../test-helpers';
|
||||||
import { DealTicket } from './deal-ticket';
|
import { DealTicket } from './deal-ticket';
|
||||||
@ -15,7 +9,10 @@ import type { MockedResponse } from '@apollo/client/testing';
|
|||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import { addDecimal } from '@vegaprotocol/utils';
|
import { addDecimal } from '@vegaprotocol/utils';
|
||||||
import type { OrdersQuery } from '@vegaprotocol/orders';
|
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 * as positionsTools from '@vegaprotocol/positions';
|
||||||
import { OrdersDocument } from '@vegaprotocol/orders';
|
import { OrdersDocument } from '@vegaprotocol/orders';
|
||||||
|
|
||||||
@ -50,9 +47,6 @@ function generateJsx(mocks: MockedResponse[] = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('DealTicket', () => {
|
describe('DealTicket', () => {
|
||||||
const { result } = renderHook(() => useCreateOrderStore());
|
|
||||||
const useOrderStore = result.current;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
@ -166,9 +160,11 @@ describe('DealTicket', () => {
|
|||||||
persist: true,
|
persist: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
useOrderStore.setState({
|
useDealTicketFormValues.setState({
|
||||||
orders: {
|
formValues: {
|
||||||
[expectedOrder.marketId]: expectedOrder,
|
[expectedOrder.marketId]: {
|
||||||
|
[DealTicketType.Limit]: expectedOrder,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,9 +200,11 @@ describe('DealTicket', () => {
|
|||||||
reduceOnly: true,
|
reduceOnly: true,
|
||||||
postOnly: false,
|
postOnly: false,
|
||||||
};
|
};
|
||||||
useOrderStore.setState({
|
useDealTicketFormValues.setState({
|
||||||
orders: {
|
formValues: {
|
||||||
[expectedOrder.marketId]: expectedOrder,
|
[expectedOrder.marketId]: {
|
||||||
|
[DealTicketType.Limit]: expectedOrder,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -247,9 +245,11 @@ describe('DealTicket', () => {
|
|||||||
postOnly: true,
|
postOnly: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
useOrderStore.setState({
|
useDealTicketFormValues.setState({
|
||||||
orders: {
|
formValues: {
|
||||||
[expectedOrder.marketId]: expectedOrder,
|
[expectedOrder.marketId]: {
|
||||||
|
[DealTicketType.Limit]: expectedOrder,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -295,9 +295,11 @@ describe('DealTicket', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
useOrderStore.setState({
|
useDealTicketFormValues.setState({
|
||||||
orders: {
|
formValues: {
|
||||||
[expectedOrder.marketId]: expectedOrder,
|
[expectedOrder.marketId]: {
|
||||||
|
[DealTicketType.Limit]: expectedOrder,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -339,9 +341,11 @@ describe('DealTicket', () => {
|
|||||||
reduceOnly: false,
|
reduceOnly: false,
|
||||||
postOnly: false,
|
postOnly: false,
|
||||||
};
|
};
|
||||||
useOrderStore.setState({
|
useDealTicketFormValues.setState({
|
||||||
orders: {
|
formValues: {
|
||||||
[expectedOrder.marketId]: expectedOrder,
|
[expectedOrder.marketId]: {
|
||||||
|
[DealTicketType.Limit]: expectedOrder,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -370,6 +374,7 @@ describe('DealTicket', () => {
|
|||||||
expect(screen.getByTestId('iceberg')).not.toBeChecked();
|
expect(screen.getByTestId('iceberg')).not.toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line jest/no-disabled-tests
|
||||||
it('handles TIF select box dependent on order type', async () => {
|
it('handles TIF select box dependent on order type', async () => {
|
||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
import type { FormEventHandler } from 'react';
|
||||||
import { Controller } from 'react-hook-form';
|
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||||
|
import { Controller, useController, useForm } from 'react-hook-form';
|
||||||
import { DealTicketAmount } from './deal-ticket-amount';
|
import { DealTicketAmount } from './deal-ticket-amount';
|
||||||
import { DealTicketButton } from './deal-ticket-button';
|
import { DealTicketButton } from './deal-ticket-button';
|
||||||
import {
|
import {
|
||||||
@ -13,7 +14,8 @@ import { SideSelector } from './side-selector';
|
|||||||
import { TimeInForceSelector } from './time-in-force-selector';
|
import { TimeInForceSelector } from './time-in-force-selector';
|
||||||
import { TypeSelector } from './type-selector';
|
import { TypeSelector } from './type-selector';
|
||||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
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 {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
InputError,
|
InputError,
|
||||||
@ -51,14 +53,15 @@ import {
|
|||||||
useAccountBalance,
|
useAccountBalance,
|
||||||
} from '@vegaprotocol/accounts';
|
} from '@vegaprotocol/accounts';
|
||||||
|
|
||||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
import { OrderType } from '@vegaprotocol/types';
|
||||||
import { useOrderForm } from '../../hooks/use-order-form';
|
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import {
|
import {
|
||||||
DealTicketType,
|
DealTicketType,
|
||||||
useDealTicketTypeStore,
|
dealTicketTypeToOrderType,
|
||||||
} from '../../hooks/use-type-store';
|
isStopOrderType,
|
||||||
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
} 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 { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||||
import noop from 'lodash/noop';
|
import noop from 'lodash/noop';
|
||||||
|
|
||||||
@ -75,23 +78,42 @@ export interface DealTicketProps {
|
|||||||
onDeposit: (assetId: string) => void;
|
onDeposit: (assetId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotionalSize = (
|
export const getNotionalSize = (
|
||||||
price: string | null | undefined,
|
price: string | null | undefined,
|
||||||
size: string | undefined,
|
size: string | undefined,
|
||||||
decimalPlaces: number,
|
decimalPlaces: number,
|
||||||
positionDecimalPlaces: number
|
positionDecimalPlaces: number
|
||||||
) =>
|
) => {
|
||||||
useMemo(() => {
|
if (price && size) {
|
||||||
if (price && size) {
|
return removeDecimal(
|
||||||
return removeDecimal(
|
toBigNum(size, positionDecimalPlaces).multipliedBy(
|
||||||
toBigNum(size, positionDecimalPlaces).multipliedBy(
|
toBigNum(price, decimalPlaces)
|
||||||
toBigNum(price, decimalPlaces)
|
),
|
||||||
),
|
decimalPlaces
|
||||||
decimalPlaces
|
);
|
||||||
);
|
}
|
||||||
}
|
return null;
|
||||||
return null;
|
};
|
||||||
}, [price, size, decimalPlaces, positionDecimalPlaces]);
|
|
||||||
|
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 = ({
|
export const DealTicket = ({
|
||||||
market,
|
market,
|
||||||
@ -103,32 +125,29 @@ export const DealTicket = ({
|
|||||||
onDeposit,
|
onDeposit,
|
||||||
}: DealTicketProps) => {
|
}: DealTicketProps) => {
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
const setType = useDealTicketFormValues((state) => state.setType);
|
||||||
const updateStopOrderFormValues = useStopOrderFormValues(
|
const storedFormValues = useDealTicketFormValues(
|
||||||
(state) => state.update
|
(state) => state.formValues[market.id]
|
||||||
);
|
);
|
||||||
// store last used tif for market so that when changing OrderType the previous TIF
|
const updateStoredFormValues = useDealTicketFormValues(
|
||||||
// selection for that type is used when switching back
|
(state) => state.updateOrder
|
||||||
|
);
|
||||||
const [lastTIF, setLastTIF] = useState({
|
const dealTicketType = storedFormValues?.type ?? DealTicketType.Limit;
|
||||||
[OrderType.TYPE_MARKET]: OrderTimeInForce.TIME_IN_FORCE_IOC,
|
const type = dealTicketTypeToOrderType(dealTicketType);
|
||||||
[OrderType.TYPE_LIMIT]: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
errors,
|
reset,
|
||||||
order,
|
formState: { errors },
|
||||||
setError,
|
|
||||||
clearErrors,
|
|
||||||
update,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
} = useOrderForm(market.id);
|
setValue,
|
||||||
|
watch,
|
||||||
|
} = useForm<OrderFormValues>({
|
||||||
|
defaultValues: getDefaultValues(type, storedFormValues?.[dealTicketType]),
|
||||||
|
});
|
||||||
const lastSubmitTime = useRef(0);
|
const lastSubmitTime = useRef(0);
|
||||||
|
|
||||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
accountBalance: marginAccountBalance,
|
accountBalance: marginAccountBalance,
|
||||||
loading: loadingMarginAccountBalance,
|
loading: loadingMarginAccountBalance,
|
||||||
@ -144,24 +163,54 @@ export const DealTicket = ({
|
|||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
const { marketState, marketTradingMode } = marketData;
|
const { marketState, marketTradingMode } = marketData;
|
||||||
|
const timeInForce = watch('timeInForce');
|
||||||
|
|
||||||
const normalizedOrder =
|
const side = watch('side');
|
||||||
order &&
|
const rawSize = watch('size');
|
||||||
normalizeOrderSubmission(
|
const rawPrice = watch('price');
|
||||||
order,
|
const iceberg = watch('iceberg');
|
||||||
market.decimalPlaces,
|
const peakSize = watch('peakSize');
|
||||||
market.positionDecimalPlaces
|
|
||||||
);
|
|
||||||
|
|
||||||
const price = useMemo(() => {
|
useEffect(() => {
|
||||||
return (
|
const size = storedFormValues?.[dealTicketType]?.size;
|
||||||
normalizedOrder &&
|
if (size && rawSize !== size) {
|
||||||
marketPrice &&
|
setValue('size', size);
|
||||||
getDerivedPrice(normalizedOrder, marketPrice)
|
}
|
||||||
);
|
}, [storedFormValues, dealTicketType, rawSize, setValue]);
|
||||||
}, [normalizedOrder, marketPrice]);
|
|
||||||
|
|
||||||
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,
|
price,
|
||||||
normalizedOrder?.size,
|
normalizedOrder?.size,
|
||||||
market.decimalPlaces,
|
market.decimalPlaces,
|
||||||
@ -205,22 +254,20 @@ export const DealTicket = ({
|
|||||||
const assetSymbol =
|
const assetSymbol =
|
||||||
market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||||
|
|
||||||
useEffect(() => {
|
const summaryError = useMemo(() => {
|
||||||
if (!pubKey) {
|
if (!pubKey) {
|
||||||
setError('summary', {
|
return {
|
||||||
message: t('No public key selected'),
|
message: t('No public key selected'),
|
||||||
type: SummaryValidationType.NoPubKey,
|
type: SummaryValidationType.NoPubKey,
|
||||||
});
|
};
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const marketStateError = validateMarketState(marketState);
|
const marketStateError = validateMarketState(marketState);
|
||||||
if (marketStateError !== true) {
|
if (marketStateError !== true) {
|
||||||
setError('summary', {
|
return {
|
||||||
message: marketStateError,
|
message: marketStateError,
|
||||||
type: SummaryValidationType.MarketState,
|
type: SummaryValidationType.MarketState,
|
||||||
});
|
};
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNoBalance =
|
const hasNoBalance =
|
||||||
@ -229,24 +276,21 @@ export const DealTicket = ({
|
|||||||
hasNoBalance &&
|
hasNoBalance &&
|
||||||
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
|
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
|
||||||
) {
|
) {
|
||||||
setError('summary', {
|
return {
|
||||||
message: SummaryValidationType.NoCollateral,
|
message: SummaryValidationType.NoCollateral,
|
||||||
type: SummaryValidationType.NoCollateral,
|
type: SummaryValidationType.NoCollateral,
|
||||||
});
|
};
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const marketTradingModeError = validateMarketTradingMode(marketTradingMode);
|
const marketTradingModeError = validateMarketTradingMode(marketTradingMode);
|
||||||
if (marketTradingModeError !== true) {
|
if (marketTradingModeError !== true) {
|
||||||
setError('summary', {
|
return {
|
||||||
message: marketTradingModeError,
|
message: marketTradingModeError,
|
||||||
type: SummaryValidationType.TradingMode,
|
type: SummaryValidationType.TradingMode,
|
||||||
});
|
};
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No error found above clear the error in case it was active on a previous render
|
return undefined;
|
||||||
clearErrors('summary');
|
|
||||||
}, [
|
}, [
|
||||||
marketState,
|
marketState,
|
||||||
marketTradingMode,
|
marketTradingMode,
|
||||||
@ -255,156 +299,83 @@ export const DealTicket = ({
|
|||||||
loadingMarginAccountBalance,
|
loadingMarginAccountBalance,
|
||||||
loadingGeneralAccountBalance,
|
loadingGeneralAccountBalance,
|
||||||
pubKey,
|
pubKey,
|
||||||
setError,
|
|
||||||
clearErrors,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const disablePostOnlyCheckbox = useMemo(() => {
|
const disablePostOnlyCheckbox = [
|
||||||
const disabled = order
|
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||||
? [
|
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
].includes(timeInForce);
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
|
||||||
].includes(order.timeInForce)
|
|
||||||
: true;
|
|
||||||
return disabled;
|
|
||||||
}, [order]);
|
|
||||||
|
|
||||||
const disableReduceOnlyCheckbox = useMemo(() => {
|
const disableReduceOnlyCheckbox = !disablePostOnlyCheckbox;
|
||||||
const disabled = order
|
|
||||||
? ![
|
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
|
||||||
].includes(order.timeInForce)
|
|
||||||
: true;
|
|
||||||
return disabled;
|
|
||||||
}, [order]);
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(order: OrderSubmission) => {
|
(formValues: OrderFormValues) => {
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
|
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
submit(
|
submit(
|
||||||
normalizeOrderSubmission(
|
mapFormValuesToOrderSubmission(
|
||||||
order,
|
formValues,
|
||||||
|
market.id,
|
||||||
market.decimalPlaces,
|
market.decimalPlaces,
|
||||||
market.positionDecimalPlaces
|
market.positionDecimalPlaces
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
lastSubmitTime.current = now;
|
lastSubmitTime.current = now;
|
||||||
},
|
},
|
||||||
[submit, market.decimalPlaces, market.positionDecimalPlaces]
|
[submit, market.decimalPlaces, market.positionDecimalPlaces, market.id]
|
||||||
);
|
);
|
||||||
|
useController({
|
||||||
// if an order doesn't exist one will be created by the store immediately
|
name: 'type',
|
||||||
if (!order || !normalizedOrder) {
|
control,
|
||||||
return null;
|
rules: {
|
||||||
}
|
validate: validateType(marketData.marketTradingMode, marketData.trigger),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={isReadOnly ? noop : handleSubmit(onSubmit)}
|
onSubmit={
|
||||||
|
isReadOnly || !pubKey
|
||||||
|
? stopSubmit
|
||||||
|
: handleSubmit(summaryError ? noop : onSubmit)
|
||||||
|
}
|
||||||
noValidate
|
noValidate
|
||||||
data-testid="deal-ticket-form"
|
data-testid="deal-ticket-form"
|
||||||
>
|
>
|
||||||
<Controller
|
<TypeSelector
|
||||||
name="type"
|
value={dealTicketType}
|
||||||
control={control}
|
onValueChange={(dealTicketType) => {
|
||||||
rules={{
|
setType(market.id, dealTicketType);
|
||||||
validate: validateType(
|
if (!isStopOrderType(dealTicketType)) {
|
||||||
marketData.marketTradingMode,
|
reset(
|
||||||
marketData.trigger
|
getDefaultValues(
|
||||||
),
|
dealTicketTypeToOrderType(dealTicketType),
|
||||||
|
storedFormValues?.[dealTicketType]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
render={() => (
|
market={market}
|
||||||
<TypeSelector
|
marketData={marketData}
|
||||||
value={
|
errorMessage={errors.type?.message}
|
||||||
order.type === OrderType.TYPE_LIMIT
|
|
||||||
? DealTicketType.Limit
|
|
||||||
: DealTicketType.Market
|
|
||||||
}
|
|
||||||
onValueChange={(dealTicketType) => {
|
|
||||||
setDealTicketType(market.id, dealTicketType);
|
|
||||||
if (
|
|
||||||
dealTicketType !== DealTicketType.Limit &&
|
|
||||||
dealTicketType !== DealTicketType.Market
|
|
||||||
) {
|
|
||||||
updateStopOrderFormValues(market.id, {
|
|
||||||
type:
|
|
||||||
dealTicketType === DealTicketType.StopLimit
|
|
||||||
? OrderType.TYPE_LIMIT
|
|
||||||
: OrderType.TYPE_MARKET,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const type =
|
|
||||||
dealTicketType === DealTicketType.Limit
|
|
||||||
? OrderType.TYPE_LIMIT
|
|
||||||
: OrderType.TYPE_MARKET;
|
|
||||||
update({
|
|
||||||
type,
|
|
||||||
// when changing type also update the TIF to what was last used of new type
|
|
||||||
timeInForce: lastTIF[type] || order.timeInForce,
|
|
||||||
postOnly:
|
|
||||||
type === OrderType.TYPE_MARKET ? false : order.postOnly,
|
|
||||||
iceberg:
|
|
||||||
type === OrderType.TYPE_MARKET ||
|
|
||||||
[
|
|
||||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
|
||||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
|
||||||
].includes(lastTIF[type] || order.timeInForce)
|
|
||||||
? false
|
|
||||||
: order.iceberg,
|
|
||||||
icebergOpts:
|
|
||||||
type === OrderType.TYPE_MARKET ||
|
|
||||||
[
|
|
||||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
|
||||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
|
||||||
].includes(lastTIF[type] || order.timeInForce)
|
|
||||||
? undefined
|
|
||||||
: order.icebergOpts,
|
|
||||||
reduceOnly:
|
|
||||||
type === OrderType.TYPE_LIMIT &&
|
|
||||||
![
|
|
||||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
|
||||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
|
||||||
].includes(lastTIF[type] || order.timeInForce)
|
|
||||||
? false
|
|
||||||
: order.postOnly,
|
|
||||||
expiresAt: undefined,
|
|
||||||
});
|
|
||||||
clearErrors(['expiresAt', 'price']);
|
|
||||||
}}
|
|
||||||
market={market}
|
|
||||||
marketData={marketData}
|
|
||||||
errorMessage={errors.type?.message}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="side"
|
name="side"
|
||||||
control={control}
|
control={control}
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<SideSelector
|
<SideSelector value={field.value} onValueChange={field.onChange} />
|
||||||
value={order.side}
|
|
||||||
onValueChange={(side) => {
|
|
||||||
update({ side });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DealTicketAmount
|
<DealTicketAmount
|
||||||
|
type={type}
|
||||||
control={control}
|
control={control}
|
||||||
orderType={order.type}
|
|
||||||
market={market}
|
market={market}
|
||||||
marketData={marketData}
|
marketData={marketData}
|
||||||
marketPrice={marketPrice || undefined}
|
marketPrice={marketPrice || undefined}
|
||||||
sizeError={errors.size?.message}
|
sizeError={errors.size?.message}
|
||||||
priceError={errors.price?.message}
|
priceError={errors.price?.message}
|
||||||
update={update}
|
|
||||||
size={order.size}
|
|
||||||
price={order.price}
|
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="timeInForce"
|
name="timeInForce"
|
||||||
@ -415,58 +386,29 @@ export const DealTicket = ({
|
|||||||
marketData.trigger
|
marketData.trigger
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<TimeInForceSelector
|
<TimeInForceSelector
|
||||||
value={order.timeInForce}
|
value={field.value}
|
||||||
orderType={order.type}
|
orderType={type}
|
||||||
onSelect={(timeInForce) => {
|
onSelect={field.onChange}
|
||||||
// Reset post only and reduce only when changing TIF
|
|
||||||
update({
|
|
||||||
timeInForce,
|
|
||||||
postOnly: [
|
|
||||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
|
||||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
|
||||||
].includes(timeInForce)
|
|
||||||
? false
|
|
||||||
: order.postOnly,
|
|
||||||
reduceOnly: ![
|
|
||||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
|
||||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
|
||||||
].includes(timeInForce)
|
|
||||||
? false
|
|
||||||
: order.reduceOnly,
|
|
||||||
});
|
|
||||||
// Set TIF value for the given order type, so that when switching
|
|
||||||
// types we know the last used TIF for the given order type
|
|
||||||
setLastTIF((curr) => ({
|
|
||||||
...curr,
|
|
||||||
[order.type]: timeInForce,
|
|
||||||
expiresAt: undefined,
|
|
||||||
}));
|
|
||||||
clearErrors('expiresAt');
|
|
||||||
}}
|
|
||||||
market={market}
|
market={market}
|
||||||
marketData={marketData}
|
marketData={marketData}
|
||||||
errorMessage={errors.timeInForce?.message}
|
errorMessage={errors.timeInForce?.message}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{order.type === Schema.OrderType.TYPE_LIMIT &&
|
{type === Schema.OrderType.TYPE_LIMIT &&
|
||||||
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
|
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
|
||||||
<Controller
|
<Controller
|
||||||
name="expiresAt"
|
name="expiresAt"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
validate: validateExpiration,
|
validate: validateExpiration,
|
||||||
}}
|
}}
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<ExpirySelector
|
<ExpirySelector
|
||||||
value={order.expiresAt}
|
value={field.value}
|
||||||
onSelect={(expiresAt) =>
|
onSelect={(expiresAt) => field.onChange(expiresAt)}
|
||||||
update({
|
|
||||||
expiresAt: expiresAt || undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
errorMessage={errors.expiresAt?.message}
|
errorMessage={errors.expiresAt?.message}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -476,13 +418,14 @@ export const DealTicket = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name="postOnly"
|
name="postOnly"
|
||||||
control={control}
|
control={control}
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="post-only"
|
name="post-only"
|
||||||
checked={order.postOnly}
|
checked={!disablePostOnlyCheckbox && field.value}
|
||||||
disabled={disablePostOnlyCheckbox}
|
disabled={disablePostOnlyCheckbox}
|
||||||
onCheckedChange={() => {
|
onCheckedChange={(postOnly) => {
|
||||||
update({ postOnly: !order.postOnly, reduceOnly: false });
|
field.onChange(postOnly);
|
||||||
|
setValue('reduceOnly', false);
|
||||||
}}
|
}}
|
||||||
label={
|
label={
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -507,13 +450,14 @@ export const DealTicket = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name="reduceOnly"
|
name="reduceOnly"
|
||||||
control={control}
|
control={control}
|
||||||
render={() => (
|
render={({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="reduce-only"
|
name="reduce-only"
|
||||||
checked={order.reduceOnly}
|
checked={!disableReduceOnlyCheckbox && field.value}
|
||||||
disabled={disableReduceOnlyCheckbox}
|
disabled={disableReduceOnlyCheckbox}
|
||||||
onCheckedChange={() => {
|
onCheckedChange={(reduceOnly) => {
|
||||||
update({ postOnly: false, reduceOnly: !order.reduceOnly });
|
field.onChange(reduceOnly);
|
||||||
|
setValue('postOnly', false);
|
||||||
}}
|
}}
|
||||||
label={
|
label={
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -534,53 +478,49 @@ export const DealTicket = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 pb-2 justify-between">
|
{type === Schema.OrderType.TYPE_LIMIT && (
|
||||||
{order.type === Schema.OrderType.TYPE_LIMIT && (
|
<>
|
||||||
<Controller
|
<div className="flex gap-2 pb-2 justify-between">
|
||||||
name="iceberg"
|
<Controller
|
||||||
control={control}
|
name="iceberg"
|
||||||
render={() => (
|
control={control}
|
||||||
<Checkbox
|
render={({ field }) => (
|
||||||
name="iceberg"
|
<Checkbox
|
||||||
checked={order.iceberg}
|
name="iceberg"
|
||||||
onCheckedChange={() => {
|
checked={field.value}
|
||||||
update({ iceberg: !order.iceberg, icebergOpts: undefined });
|
onCheckedChange={field.onChange}
|
||||||
}}
|
label={
|
||||||
label={
|
<Tooltip
|
||||||
<Tooltip
|
description={
|
||||||
description={
|
<p>
|
||||||
<p>
|
{t(`Trade only a fraction of the order size at once.
|
||||||
{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.
|
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.
|
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.`)}
|
Note that the full volume of the order is not hidden and is still reflected in the order book.`)}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="text-xs">{t('Iceberg')}</span>
|
<span className="text-xs">{t('Iceberg')}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
{iceberg && (
|
||||||
{order.iceberg && (
|
<DealTicketSizeIceberg
|
||||||
<DealTicketSizeIceberg
|
market={market}
|
||||||
update={update}
|
peakSizeError={errors.peakSize?.message}
|
||||||
market={market}
|
minimumVisibleSizeError={errors.minimumVisibleSize?.message}
|
||||||
peakSizeError={errors.icebergOpts?.peakSize?.message}
|
control={control}
|
||||||
minimumVisibleSizeError={
|
size={rawSize}
|
||||||
errors.icebergOpts?.minimumVisibleSize?.message
|
peakSize={peakSize}
|
||||||
}
|
/>
|
||||||
control={control}
|
)}
|
||||||
size={order.size}
|
</>
|
||||||
peakSize={order.icebergOpts?.peakSize || ''}
|
|
||||||
minimumVisibleSize={order.icebergOpts?.minimumVisibleSize || ''}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<SummaryMessage
|
<SummaryMessage
|
||||||
errorMessage={errors.summary?.message}
|
error={summaryError}
|
||||||
asset={asset}
|
asset={asset}
|
||||||
marketTradingMode={marketData.marketTradingMode}
|
marketTradingMode={marketData.marketTradingMode}
|
||||||
balance={balance}
|
balance={balance}
|
||||||
@ -593,7 +533,7 @@ export const DealTicket = ({
|
|||||||
onClickCollateral={onClickCollateral}
|
onClickCollateral={onClickCollateral}
|
||||||
onDeposit={onDeposit}
|
onDeposit={onDeposit}
|
||||||
/>
|
/>
|
||||||
<DealTicketButton side={order.side} />
|
<DealTicketButton side={side} />
|
||||||
<DealTicketFeeDetails
|
<DealTicketFeeDetails
|
||||||
order={
|
order={
|
||||||
normalizedOrder && { ...normalizedOrder, price: price || undefined }
|
normalizedOrder && { ...normalizedOrder, price: price || undefined }
|
||||||
@ -619,7 +559,7 @@ export const DealTicket = ({
|
|||||||
* renders warnings about current state of the market
|
* renders warnings about current state of the market
|
||||||
*/
|
*/
|
||||||
interface SummaryMessageProps {
|
interface SummaryMessageProps {
|
||||||
errorMessage?: string;
|
error?: { message: string; type: string };
|
||||||
asset: { id: string; symbol: string; name: string; decimals: number };
|
asset: { id: string; symbol: string; name: string; decimals: number };
|
||||||
marketTradingMode: MarketData['marketTradingMode'];
|
marketTradingMode: MarketData['marketTradingMode'];
|
||||||
balance: string;
|
balance: string;
|
||||||
@ -649,7 +589,7 @@ export const NoWalletWarning = ({
|
|||||||
|
|
||||||
const SummaryMessage = memo(
|
const SummaryMessage = memo(
|
||||||
({
|
({
|
||||||
errorMessage,
|
error,
|
||||||
asset,
|
asset,
|
||||||
marketTradingMode,
|
marketTradingMode,
|
||||||
balance,
|
balance,
|
||||||
@ -665,7 +605,7 @@ const SummaryMessage = memo(
|
|||||||
return <NoWalletWarning isReadOnly={isReadOnly} />;
|
return <NoWalletWarning isReadOnly={isReadOnly} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorMessage === SummaryValidationType.NoCollateral) {
|
if (error?.type === SummaryValidationType.NoCollateral) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<ZeroBalanceError
|
<ZeroBalanceError
|
||||||
@ -679,11 +619,11 @@ const SummaryMessage = memo(
|
|||||||
|
|
||||||
// If we have any other full error which prevents
|
// If we have any other full error which prevents
|
||||||
// submission render that first
|
// submission render that first
|
||||||
if (errorMessage) {
|
if (error?.message) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<InputError testId="deal-ticket-error-message-summary">
|
<InputError testId="deal-ticket-error-message-summary">
|
||||||
{errorMessage}
|
{error?.message}
|
||||||
</InputError>
|
</InputError>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -21,6 +21,13 @@ interface TimeInForceSelectorProps {
|
|||||||
errorMessage?: string;
|
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 = ({
|
export const TimeInForceSelector = ({
|
||||||
value,
|
value,
|
||||||
orderType,
|
orderType,
|
||||||
@ -31,12 +38,8 @@ export const TimeInForceSelector = ({
|
|||||||
}: TimeInForceSelectorProps) => {
|
}: TimeInForceSelectorProps) => {
|
||||||
const options =
|
const options =
|
||||||
orderType === Schema.OrderType.TYPE_LIMIT
|
orderType === Schema.OrderType.TYPE_LIMIT
|
||||||
? Object.entries(Schema.OrderTimeInForce)
|
? typeLimitOptions
|
||||||
: Object.entries(Schema.OrderTimeInForce).filter(
|
: typeMarketOptions;
|
||||||
([_, timeInForce]) =>
|
|
||||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
|
|
||||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderError = (errorType: string) => {
|
const renderError = (errorType: string) => {
|
||||||
if (errorType === MarketModeValidationType.Auction) {
|
if (errorType === MarketModeValidationType.Auction) {
|
||||||
|
@ -16,7 +16,7 @@ import { t } from '@vegaprotocol/i18n';
|
|||||||
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||||
import { compileGridData } from '../trading-mode-tooltip';
|
import { compileGridData } from '../trading-mode-tooltip';
|
||||||
import { MarketModeValidationType } from '../../constants';
|
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 * as RadioGroup from '@radix-ui/react-radio-group';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { FLAGS } from '@vegaprotocol/environment';
|
import { FLAGS } from '@vegaprotocol/environment';
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
export * from './__generated__/EstimateOrder';
|
export * from './__generated__/EstimateOrder';
|
||||||
export * from './use-estimate-fees';
|
export * from './use-estimate-fees';
|
||||||
export * from './use-type-store';
|
export * from './use-form-values';
|
||||||
export * from './use-stop-order-form-values';
|
|
||||||
|
144
libs/deal-ticket/src/hooks/use-form-values.ts
Normal file
144
libs/deal-ticket/src/hooks/use-form-values.ts
Normal file
@ -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',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
@ -1,12 +1,64 @@
|
|||||||
import type {
|
import type {
|
||||||
|
OrderSubmission,
|
||||||
StopOrderSetup,
|
StopOrderSetup,
|
||||||
StopOrdersSubmission,
|
StopOrdersSubmission,
|
||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
|
import type {
|
||||||
import type { StopOrderFormValues } from '../hooks/use-stop-order-form-values';
|
OrderFormValues,
|
||||||
|
StopOrderFormValues,
|
||||||
|
} from '../hooks/use-form-values';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
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 = (
|
export const mapFormValuesToStopOrdersSubmission = (
|
||||||
data: StopOrderFormValues,
|
data: StopOrderFormValues,
|
||||||
marketId: string,
|
marketId: string,
|
||||||
@ -15,9 +67,8 @@ export const mapFormValuesToStopOrdersSubmission = (
|
|||||||
): StopOrdersSubmission => {
|
): StopOrdersSubmission => {
|
||||||
const submission: StopOrdersSubmission = {};
|
const submission: StopOrdersSubmission = {};
|
||||||
const stopOrderSetup: StopOrderSetup = {
|
const stopOrderSetup: StopOrderSetup = {
|
||||||
orderSubmission: normalizeOrderSubmission(
|
orderSubmission: mapFormValuesToOrderSubmission(
|
||||||
{
|
{
|
||||||
marketId,
|
|
||||||
type: data.type,
|
type: data.type,
|
||||||
side: data.side,
|
side: data.side,
|
||||||
size: data.size,
|
size: data.size,
|
||||||
@ -25,12 +76,16 @@ export const mapFormValuesToStopOrdersSubmission = (
|
|||||||
price: data.price,
|
price: data.price,
|
||||||
reduceOnly: true,
|
reduceOnly: true,
|
||||||
},
|
},
|
||||||
|
marketId,
|
||||||
decimalPlaces,
|
decimalPlaces,
|
||||||
positionDecimalPlaces
|
positionDecimalPlaces
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
if (data.triggerType === 'price') {
|
if (data.triggerType === 'price') {
|
||||||
stopOrderSetup.price = removeDecimal(data.triggerPrice, decimalPlaces);
|
stopOrderSetup.price = removeDecimal(
|
||||||
|
data.triggerPrice ?? '',
|
||||||
|
decimalPlaces
|
||||||
|
);
|
||||||
} else if (data.triggerType === 'trailingPercentOffset') {
|
} else if (data.triggerType === 'trailingPercentOffset') {
|
||||||
stopOrderSetup.trailingPercentOffset = (
|
stopOrderSetup.trailingPercentOffset = (
|
||||||
Number(data.triggerTrailingPercentOffset) / 100
|
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');
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,3 @@
|
|||||||
export * from './__generated__/OrdersSubscription';
|
export * from './__generated__/OrdersSubscription';
|
||||||
export * from './use-has-amendable-order';
|
export * from './use-has-amendable-order';
|
||||||
export * from './use-order-update';
|
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,
|
|
||||||
});
|
|
@ -1,7 +1,7 @@
|
|||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import { tradesWithMarketProvider } from './trades-data-provider';
|
import { tradesWithMarketProvider } from './trades-data-provider';
|
||||||
import { TradesTable } from './trades-table';
|
import { TradesTable } from './trades-table';
|
||||||
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
interface TradesContainerProps {
|
interface TradesContainerProps {
|
||||||
@ -9,8 +9,7 @@ interface TradesContainerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||||
const useOrderStoreRef = useCreateOrderStore();
|
const update = useDealTicketFormValues((state) => state.updateAll);
|
||||||
const updateOrder = useOrderStoreRef((store) => store.update);
|
|
||||||
|
|
||||||
const { data, error } = useDataProvider({
|
const { data, error } = useDataProvider({
|
||||||
dataProvider: tradesWithMarketProvider,
|
dataProvider: tradesWithMarketProvider,
|
||||||
@ -21,9 +20,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
|||||||
<TradesTable
|
<TradesTable
|
||||||
rowData={data}
|
rowData={data}
|
||||||
onClick={(price?: string) => {
|
onClick={(price?: string) => {
|
||||||
if (price) {
|
update(marketId, { price });
|
||||||
updateOrder(marketId, { price });
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
||||||
/>
|
/>
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { determineId, normalizeOrderAmendment } from './utils';
|
||||||
determineId,
|
|
||||||
normalizeOrderAmendment,
|
|
||||||
normalizeOrderSubmission,
|
|
||||||
} from './utils';
|
|
||||||
import type { OrderSubmissionBody } from './connectors/vega-connector';
|
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
describe('determineId', () => {
|
describe('determineId', () => {
|
||||||
it('produces a known result for an ID', () => {
|
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', () => {
|
describe('normalizeOrderAmendment', () => {
|
||||||
type Order = Parameters<typeof normalizeOrderAmendment>[0];
|
type Order = Parameters<typeof normalizeOrderAmendment>[0];
|
||||||
type Market = Parameters<typeof normalizeOrderAmendment>[1];
|
type Market = Parameters<typeof normalizeOrderAmendment>[1];
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||||
import type { Market, Order } from '@vegaprotocol/types';
|
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 BigNumber from 'bignumber.js';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { sha3_256 } from 'js-sha3';
|
import { sha3_256 } from 'js-sha3';
|
||||||
import type {
|
import type { OrderAmendment, Transaction, Transfer } from './connectors';
|
||||||
OrderAmendment,
|
|
||||||
OrderSubmission,
|
|
||||||
Transaction,
|
|
||||||
Transfer,
|
|
||||||
} from './connectors';
|
|
||||||
import type { Exact } from 'type-fest';
|
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>>(
|
export const normalizeOrderAmendment = <T extends Exact<OrderAmendment, T>>(
|
||||||
order: Pick<Order, 'id' | 'timeInForce' | 'size' | 'expiresAt'>,
|
order: Pick<Order, 'id' | 'timeInForce' | 'size' | 'expiresAt'>,
|
||||||
market: Pick<Market, 'id' | 'decimalPlaces' | 'positionDecimalPlaces'>,
|
market: Pick<Market, 'id' | 'decimalPlaces' | 'positionDecimalPlaces'>,
|
||||||
|
Loading…
Reference in New Issue
Block a user