fix(trading): order store connection to deal ticket (#3100)
Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
This commit is contained in:
parent
46513685c8
commit
8f5a2276de
@ -669,7 +669,7 @@ describe('account validation', { tags: '@regression' }, () => {
|
|||||||
'have.text',
|
'have.text',
|
||||||
'You need ' +
|
'You need ' +
|
||||||
'tDAI' +
|
'tDAI' +
|
||||||
' in your wallet to trade in this market. See all your collateral.Make a deposit'
|
' in your wallet to trade in this market.See all your collateral.Make a deposit'
|
||||||
);
|
);
|
||||||
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
|
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
|
||||||
});
|
});
|
||||||
|
21
libs/deal-ticket/__mocks__/zustand.ts
Normal file
21
libs/deal-ticket/__mocks__/zustand.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { StateCreator } from 'zustand';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
const { create: actualCreate } = jest.requireActual('zustand'); // if using jest
|
||||||
|
|
||||||
|
// a variable to hold reset functions for all stores declared in the app
|
||||||
|
const storeResetFns = new Set<() => void>();
|
||||||
|
|
||||||
|
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||||
|
export const create =
|
||||||
|
() =>
|
||||||
|
<S>(createState: StateCreator<S>) => {
|
||||||
|
const store = actualCreate(createState);
|
||||||
|
const initialState = store.getState();
|
||||||
|
storeResetFns.add(() => store.setState(initialState, true));
|
||||||
|
return store;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset all stores after each test run
|
||||||
|
beforeEach(() => {
|
||||||
|
act(() => storeResetFns.forEach((resetFn) => resetFn()));
|
||||||
|
});
|
@ -7,7 +7,7 @@ interface ZeroBalanceErrorProps {
|
|||||||
id: string;
|
id: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
};
|
};
|
||||||
onClickCollateral: () => void;
|
onClickCollateral?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ZeroBalanceError = ({
|
export const ZeroBalanceError = ({
|
||||||
@ -21,8 +21,12 @@ export const ZeroBalanceError = ({
|
|||||||
testId="dealticket-error-message-zero-balance"
|
testId="dealticket-error-message-zero-balance"
|
||||||
message={
|
message={
|
||||||
<>
|
<>
|
||||||
You need {asset.symbol} in your wallet to trade in this market. See
|
You need {asset.symbol} in your wallet to trade in this market.
|
||||||
all your <Link onClick={onClickCollateral}>collateral</Link>.
|
{onClickCollateral && (
|
||||||
|
<>
|
||||||
|
See all your <Link onClick={onClickCollateral}>collateral</Link>.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
buttonProps={{
|
buttonProps={{
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
import type { UseFormRegister } from 'react-hook-form';
|
import type { Control } from 'react-hook-form';
|
||||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||||
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 { DealTicketFormFields } from './deal-ticket';
|
import type { OrderObj } from '@vegaprotocol/orders';
|
||||||
|
import type { OrderFormFields } from '../../hooks/use-order-form';
|
||||||
|
|
||||||
export interface DealTicketAmountProps {
|
export interface DealTicketAmountProps {
|
||||||
|
control: Control<OrderFormFields>;
|
||||||
orderType: Schema.OrderType;
|
orderType: Schema.OrderType;
|
||||||
marketData: MarketData;
|
marketData: MarketData;
|
||||||
market: Market;
|
market: Market;
|
||||||
register: UseFormRegister<DealTicketFormFields>;
|
|
||||||
sizeError?: string;
|
sizeError?: string;
|
||||||
priceError?: string;
|
priceError?: string;
|
||||||
|
update: (obj: Partial<OrderObj>) => void;
|
||||||
|
size: string;
|
||||||
|
price?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DealTicketAmount = ({
|
export const DealTicketAmount = ({
|
||||||
|
@ -47,7 +47,7 @@ export const DealTicketContainer = ({
|
|||||||
market={market}
|
market={market}
|
||||||
marketData={marketData}
|
marketData={marketData}
|
||||||
submit={(orderSubmission) => create({ orderSubmission })}
|
submit={(orderSubmission) => create({ orderSubmission })}
|
||||||
onClickCollateral={onClickCollateral || (() => null)}
|
onClickCollateral={onClickCollateral}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Splash>
|
<Splash>
|
||||||
|
@ -2,6 +2,7 @@ import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
|||||||
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
|
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
export type DealTicketLimitAmountProps = Omit<
|
export type DealTicketLimitAmountProps = Omit<
|
||||||
Omit<DealTicketAmountProps, 'marketData'>,
|
Omit<DealTicketAmountProps, 'marketData'>,
|
||||||
@ -9,10 +10,13 @@ export type DealTicketLimitAmountProps = Omit<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export const DealTicketLimitAmount = ({
|
export const DealTicketLimitAmount = ({
|
||||||
register,
|
control,
|
||||||
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);
|
||||||
@ -47,22 +51,30 @@ export const DealTicketLimitAmount = ({
|
|||||||
labelFor="input-order-size-limit"
|
labelFor="input-order-size-limit"
|
||||||
className="!mb-1"
|
className="!mb-1"
|
||||||
>
|
>
|
||||||
<Input
|
<Controller
|
||||||
id="input-order-size-limit"
|
name="size"
|
||||||
className="w-full"
|
control={control}
|
||||||
type="number"
|
rules={{
|
||||||
step={sizeStep}
|
|
||||||
min={sizeStep}
|
|
||||||
data-testid="order-size"
|
|
||||||
onWheel={(e) => e.currentTarget.blur()}
|
|
||||||
{...register('size', {
|
|
||||||
required: t('You need to provide a size'),
|
required: t('You need to provide a size'),
|
||||||
min: {
|
min: {
|
||||||
value: sizeStep,
|
value: sizeStep,
|
||||||
message: t('Size cannot be lower than ' + sizeStep),
|
message: t('Size cannot be lower than ' + sizeStep),
|
||||||
},
|
},
|
||||||
validate: validateAmount(sizeStep, 'Size'),
|
validate: validateAmount(sizeStep, 'Size'),
|
||||||
})}
|
}}
|
||||||
|
render={() => (
|
||||||
|
<Input
|
||||||
|
id="input-order-size-limit"
|
||||||
|
className="w-full"
|
||||||
|
type="number"
|
||||||
|
value={size}
|
||||||
|
onChange={(e) => update({ size: e.target.value })}
|
||||||
|
step={sizeStep}
|
||||||
|
min={sizeStep}
|
||||||
|
data-testid="order-size"
|
||||||
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
@ -77,14 +89,10 @@ export const DealTicketLimitAmount = ({
|
|||||||
labelAlign="right"
|
labelAlign="right"
|
||||||
className="!mb-1"
|
className="!mb-1"
|
||||||
>
|
>
|
||||||
<Input
|
<Controller
|
||||||
id="input-price-quote"
|
name="price"
|
||||||
className="w-full"
|
control={control}
|
||||||
type="number"
|
rules={{
|
||||||
step={priceStep}
|
|
||||||
data-testid="order-price"
|
|
||||||
onWheel={(e) => e.currentTarget.blur()}
|
|
||||||
{...register('price', {
|
|
||||||
required: t('You need provide a price'),
|
required: t('You need provide a price'),
|
||||||
min: {
|
min: {
|
||||||
value: priceStep,
|
value: priceStep,
|
||||||
@ -92,7 +100,19 @@ export const DealTicketLimitAmount = ({
|
|||||||
},
|
},
|
||||||
// @ts-ignore this fulfills the interface but still errors
|
// @ts-ignore this fulfills the interface but still errors
|
||||||
validate: validateAmount(priceStep, 'Price'),
|
validate: validateAmount(priceStep, 'Price'),
|
||||||
})}
|
}}
|
||||||
|
render={() => (
|
||||||
|
<Input
|
||||||
|
id="input-price-quote"
|
||||||
|
className="w-full"
|
||||||
|
type="number"
|
||||||
|
value={price}
|
||||||
|
onChange={(e) => update({ price: e.target.value })}
|
||||||
|
step={priceStep}
|
||||||
|
data-testid="order-price"
|
||||||
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,6 +8,7 @@ import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
|
|||||||
import { isMarketInAuction } from '../../utils';
|
import { isMarketInAuction } from '../../utils';
|
||||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||||
import { getMarketPrice } from '../../utils/get-price';
|
import { getMarketPrice } from '../../utils/get-price';
|
||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
|
||||||
export type DealTicketMarketAmountProps = Omit<
|
export type DealTicketMarketAmountProps = Omit<
|
||||||
DealTicketAmountProps,
|
DealTicketAmountProps,
|
||||||
@ -15,10 +16,12 @@ export type DealTicketMarketAmountProps = Omit<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export const DealTicketMarketAmount = ({
|
export const DealTicketMarketAmount = ({
|
||||||
register,
|
control,
|
||||||
market,
|
market,
|
||||||
marketData,
|
marketData,
|
||||||
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);
|
||||||
@ -47,22 +50,30 @@ export const DealTicketMarketAmount = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Input
|
<Controller
|
||||||
id="input-order-size-market"
|
name="size"
|
||||||
className="w-full"
|
control={control}
|
||||||
type="number"
|
rules={{
|
||||||
step={sizeStep}
|
|
||||||
min={sizeStep}
|
|
||||||
onWheel={(e) => e.currentTarget.blur()}
|
|
||||||
data-testid="order-size"
|
|
||||||
{...register('size', {
|
|
||||||
required: t('You need to provide a size'),
|
required: t('You need to provide a size'),
|
||||||
min: {
|
min: {
|
||||||
value: sizeStep,
|
value: sizeStep,
|
||||||
message: t('Size cannot be lower than ' + sizeStep),
|
message: t('Size cannot be lower than ' + sizeStep),
|
||||||
},
|
},
|
||||||
validate: validateAmount(sizeStep, 'Size'),
|
validate: validateAmount(sizeStep, 'Size'),
|
||||||
})}
|
}}
|
||||||
|
render={() => (
|
||||||
|
<Input
|
||||||
|
id="input-order-size-market"
|
||||||
|
className="w-full"
|
||||||
|
type="number"
|
||||||
|
value={size}
|
||||||
|
onChange={(e) => update({ size: e.target.value })}
|
||||||
|
step={sizeStep}
|
||||||
|
min={sizeStep}
|
||||||
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
data-testid="order-size"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>@</div>
|
<div>@</div>
|
||||||
|
@ -1,38 +1,28 @@
|
|||||||
/* 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 { fireEvent, render, screen, act } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
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';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import type { ChainIdQuery } from '@vegaprotocol/react-helpers';
|
|
||||||
import { ChainIdDocument } from '@vegaprotocol/react-helpers';
|
|
||||||
import { addDecimal } from '@vegaprotocol/utils';
|
import { addDecimal } from '@vegaprotocol/utils';
|
||||||
|
import { useOrderStore } from '@vegaprotocol/orders';
|
||||||
|
|
||||||
|
jest.mock('zustand');
|
||||||
|
jest.mock('./deal-ticket-fee-details', () => ({
|
||||||
|
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const pubKey = 'pubKey';
|
||||||
const market = generateMarket();
|
const market = generateMarket();
|
||||||
const marketData = generateMarketData();
|
const marketData = generateMarketData();
|
||||||
const submit = jest.fn();
|
const submit = jest.fn();
|
||||||
|
|
||||||
const mockChainId = 'chain-id';
|
function generateJsx() {
|
||||||
|
|
||||||
function generateJsx(order?: OrderSubmissionBody['orderSubmission']) {
|
|
||||||
const chainIdMock: MockedResponse<ChainIdQuery> = {
|
|
||||||
request: {
|
|
||||||
query: ChainIdDocument,
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
statistics: {
|
|
||||||
chainId: mockChainId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<MockedProvider mocks={[chainIdMock]}>
|
<MockedProvider>
|
||||||
<VegaWalletContext.Provider value={{ pubKey: mockChainId } as any}>
|
<VegaWalletContext.Provider value={{ pubKey, isReadOnly: false } as any}>
|
||||||
<DealTicket market={market} marketData={marketData} submit={submit} />
|
<DealTicket market={market} marketData={marketData} submit={submit} />
|
||||||
</VegaWalletContext.Provider>
|
</VegaWalletContext.Provider>
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
@ -41,10 +31,11 @@ function generateJsx(order?: OrderSubmissionBody['orderSubmission']) {
|
|||||||
|
|
||||||
describe('DealTicket', () => {
|
describe('DealTicket', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
window.localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
window.localStorage.clear();
|
localStorage.clear();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -61,9 +52,7 @@ describe('DealTicket', () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||||
).not.toBeChecked();
|
).not.toBeChecked();
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue('0');
|
||||||
String(1 / Math.pow(10, market.positionDecimalPlaces))
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||||
);
|
);
|
||||||
@ -76,7 +65,49 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles TIF select box dependent on order type', () => {
|
it('should use local storage state for initial values', () => {
|
||||||
|
const expectedOrder = {
|
||||||
|
marketId: market.id,
|
||||||
|
type: Schema.OrderType.TYPE_LIMIT,
|
||||||
|
side: Schema.Side.SIDE_SELL,
|
||||||
|
size: '0.1',
|
||||||
|
price: '300.22',
|
||||||
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||||
|
persist: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
useOrderStore.setState({
|
||||||
|
orders: {
|
||||||
|
[expectedOrder.marketId]: expectedOrder,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(generateJsx());
|
||||||
|
|
||||||
|
// Assert correct defaults are used from store
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
||||||
|
.querySelector('input')
|
||||||
|
).toBeChecked();
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||||
|
).toBeChecked();
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||||
|
).not.toBeChecked();
|
||||||
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
|
expectedOrder.size
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
|
expectedOrder.timeInForce
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('order-price')).toHaveDisplayValue(
|
||||||
|
expectedOrder.price
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles TIF select box dependent on order type', async () => {
|
||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Only FOK and IOC should be present by default (type market order)
|
// Only FOK and IOC should be present by default (type market order)
|
||||||
@ -86,50 +117,72 @@ describe('DealTicket', () => {
|
|||||||
)
|
)
|
||||||
).toEqual(['Fill or Kill (FOK)', 'Immediate or Cancel (IOC)']);
|
).toEqual(['Fill or Kill (FOK)', 'Immediate or Cancel (IOC)']);
|
||||||
|
|
||||||
|
// IOC should be default
|
||||||
|
expect(screen.getByTestId('order-tif')).toHaveDisplayValue(
|
||||||
|
'Immediate or Cancel (IOC)'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Select FOK - FOK should be selected
|
||||||
|
await userEvent.selectOptions(
|
||||||
|
screen.getByTestId('order-tif'),
|
||||||
|
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('order-tif')).toHaveDisplayValue(
|
||||||
|
'Fill or Kill (FOK)'
|
||||||
|
);
|
||||||
|
|
||||||
// Switch to type limit order -> all TIF options should be shown
|
// Switch to type limit order -> all TIF options should be shown
|
||||||
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||||
Object.keys(Schema.OrderTimeInForce).length
|
Object.keys(Schema.OrderTimeInForce).length
|
||||||
);
|
);
|
||||||
|
|
||||||
// Select GTC -> GTC should be selected
|
// expect GTC as LIMIT default
|
||||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
|
||||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC },
|
|
||||||
});
|
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
|
||||||
);
|
|
||||||
|
|
||||||
// Switch to type market order -> IOC should be selected (default)
|
|
||||||
fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
|
||||||
);
|
|
||||||
|
|
||||||
// Select IOC -> IOC should be selected
|
|
||||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
|
||||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC },
|
|
||||||
});
|
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
|
||||||
);
|
|
||||||
|
|
||||||
// Switch to type limit order -> GTC should be selected
|
|
||||||
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
||||||
);
|
);
|
||||||
|
|
||||||
// Select GTT -> GTT should be selected
|
// Select GTT -> GTT should be selected
|
||||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
await userEvent.selectOptions(
|
||||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT },
|
screen.getByTestId('order-tif'),
|
||||||
});
|
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||||
|
);
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||||
);
|
);
|
||||||
|
|
||||||
// Switch to type market order -> IOC should be selected
|
// Switch back to type market order -> FOK should be preserved from previous selection
|
||||||
fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||||
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
|
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||||
|
);
|
||||||
|
|
||||||
|
// Select IOC -> IOC should be selected
|
||||||
|
await userEvent.selectOptions(
|
||||||
|
screen.getByTestId('order-tif'),
|
||||||
|
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
|
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||||
|
);
|
||||||
|
|
||||||
|
// Switch back type limit order -> GTT should be preserved
|
||||||
|
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||||
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
|
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||||
|
);
|
||||||
|
|
||||||
|
// Select GFN -> GFN should be selected
|
||||||
|
await userEvent.selectOptions(
|
||||||
|
screen.getByTestId('order-tif'),
|
||||||
|
Schema.OrderTimeInForce.TIME_IN_FORCE_GFN
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
|
Schema.OrderTimeInForce.TIME_IN_FORCE_GFN
|
||||||
|
);
|
||||||
|
|
||||||
|
// Switch to type market order -> IOC should be preserved
|
||||||
|
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||||
);
|
);
|
||||||
@ -143,23 +196,20 @@ describe('DealTicket', () => {
|
|||||||
screen.getByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
screen.getByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||||
).toBeChecked();
|
).toBeChecked();
|
||||||
|
|
||||||
await act(async () => {
|
await userEvent.type(screen.getByTestId('order-size'), '200');
|
||||||
fireEvent.change(screen.getByTestId('order-size'), {
|
|
||||||
target: { value: '200' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue('200');
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue('200');
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
await userEvent.selectOptions(
|
||||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC },
|
screen.getByTestId('order-tif'),
|
||||||
});
|
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||||
|
);
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||||
);
|
);
|
||||||
|
|
||||||
// Switch to limit order
|
// Switch to limit order
|
||||||
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||||
|
|
||||||
// Check all TIF options shown
|
// Check all TIF options shown
|
||||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 } from 'react';
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller } 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 { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||||
@ -9,10 +9,12 @@ import { ExpirySelector } from './expiry-selector';
|
|||||||
import { SideSelector } from './side-selector';
|
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 { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||||
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
import {
|
||||||
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
|
normalizeOrderSubmission,
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
useVegaWallet,
|
||||||
|
useVegaWalletDialogStore,
|
||||||
|
} from '@vegaprotocol/wallet';
|
||||||
import {
|
import {
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
InputError,
|
InputError,
|
||||||
@ -22,7 +24,7 @@ import {
|
|||||||
import { useOrderMarginValidation } from '../../hooks/use-order-margin-validation';
|
import { useOrderMarginValidation } from '../../hooks/use-order-margin-validation';
|
||||||
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
||||||
import {
|
import {
|
||||||
getDefaultOrder,
|
validateExpiration,
|
||||||
validateMarketState,
|
validateMarketState,
|
||||||
validateMarketTradingMode,
|
validateMarketTradingMode,
|
||||||
validateTimeInForce,
|
validateTimeInForce,
|
||||||
@ -32,27 +34,17 @@ import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
|
|||||||
import { SummaryValidationType } from '../../constants';
|
import { SummaryValidationType } from '../../constants';
|
||||||
import { useHasNoBalance } from '../../hooks/use-has-no-balance';
|
import { useHasNoBalance } from '../../hooks/use-has-no-balance';
|
||||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||||
import {
|
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||||
usePersistedOrderStore,
|
import { useOrderForm } from '../../hooks/use-order-form';
|
||||||
usePersistedOrderStoreSubscription,
|
import type { OrderObj } from '@vegaprotocol/orders';
|
||||||
} from '@vegaprotocol/orders';
|
|
||||||
import { OrderType } from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
export type TransactionStatus = 'default' | 'pending';
|
|
||||||
|
|
||||||
export interface DealTicketProps {
|
export interface DealTicketProps {
|
||||||
market: Market;
|
market: Market;
|
||||||
marketData: MarketData;
|
marketData: MarketData;
|
||||||
submit: (order: OrderSubmissionBody['orderSubmission']) => void;
|
submit: (order: OrderSubmission) => void;
|
||||||
onClickCollateral?: () => void;
|
onClickCollateral?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
|
|
||||||
// This is not a field used in the form but allows us to set a
|
|
||||||
// summary error message
|
|
||||||
summary: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DealTicket = ({
|
export const DealTicket = ({
|
||||||
market,
|
market,
|
||||||
marketData,
|
marketData,
|
||||||
@ -60,44 +52,21 @@ export const DealTicket = ({
|
|||||||
onClickCollateral,
|
onClickCollateral,
|
||||||
}: DealTicketProps) => {
|
}: DealTicketProps) => {
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
const { getPersistedOrder, setPersistedOrder } = usePersistedOrderStore(
|
// store last used tif for market so that when changing OrderType the previous TIF
|
||||||
(store) => ({
|
// selection for that type is used when switching back
|
||||||
getPersistedOrder: store.getOrder,
|
const [lastTIF, setLastTIF] = useState({
|
||||||
setPersistedOrder: store.setOrder,
|
[OrderType.TYPE_MARKET]: OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||||
})
|
[OrderType.TYPE_LIMIT]: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||||
);
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
errors,
|
||||||
watch,
|
order,
|
||||||
setError,
|
setError,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
formState: { errors },
|
update,
|
||||||
setValue,
|
handleSubmit,
|
||||||
} = useForm<DealTicketFormFields>({
|
} = useOrderForm(market.id);
|
||||||
defaultValues: getPersistedOrder(market.id) || getDefaultOrder(market),
|
|
||||||
});
|
|
||||||
|
|
||||||
const order = watch();
|
|
||||||
|
|
||||||
watch((orderData) => {
|
|
||||||
const persistable = !(
|
|
||||||
orderData.type === OrderType.TYPE_LIMIT && orderData.price === ''
|
|
||||||
);
|
|
||||||
if (persistable) {
|
|
||||||
setPersistedOrder(orderData as DealTicketFormFields);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
usePersistedOrderStoreSubscription(market.id, (storedOrder) => {
|
|
||||||
if (order.price !== storedOrder.price) {
|
|
||||||
clearErrors('price');
|
|
||||||
setValue('price', storedOrder.price);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const marketStateError = validateMarketState(marketData.marketState);
|
const marketStateError = validateMarketState(marketData.marketState);
|
||||||
const hasNoBalance = useHasNoBalance(
|
const hasNoBalance = useHasNoBalance(
|
||||||
market.tradableInstrument.instrument.product.settlementAsset.id
|
market.tradableInstrument.instrument.product.settlementAsset.id
|
||||||
@ -166,7 +135,7 @@ export const DealTicket = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(order: OrderSubmissionBody['orderSubmission']) => {
|
(order: OrderSubmission) => {
|
||||||
checkForErrors();
|
checkForErrors();
|
||||||
submit(
|
submit(
|
||||||
normalizeOrderSubmission(
|
normalizeOrderSubmission(
|
||||||
@ -179,9 +148,12 @@ export const DealTicket = ({
|
|||||||
[checkForErrors, submit, market.decimalPlaces, market.positionDecimalPlaces]
|
[checkForErrors, submit, market.decimalPlaces, market.positionDecimalPlaces]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// if an order doesn't exist one will be created by the store immediately
|
||||||
|
if (!order) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={isReadOnly ? () => null : handleSubmit(onSubmit)}
|
onSubmit={isReadOnly ? undefined : handleSubmit(onSubmit)}
|
||||||
className="p-4"
|
className="p-4"
|
||||||
noValidate
|
noValidate
|
||||||
>
|
>
|
||||||
@ -194,10 +166,17 @@ export const DealTicket = ({
|
|||||||
marketData.trigger
|
marketData.trigger
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
render={({ field }) => (
|
render={() => (
|
||||||
<TypeSelector
|
<TypeSelector
|
||||||
value={field.value}
|
value={order.type}
|
||||||
onSelect={field.onChange}
|
onSelect={(type) => {
|
||||||
|
if (type === OrderType.TYPE_NETWORK) return;
|
||||||
|
update({
|
||||||
|
type,
|
||||||
|
// when changing type also update the tif to what was last used of new type
|
||||||
|
timeInForce: lastTIF[type] || order.timeInForce,
|
||||||
|
});
|
||||||
|
}}
|
||||||
market={market}
|
market={market}
|
||||||
marketData={marketData}
|
marketData={marketData}
|
||||||
errorMessage={errors.type?.message}
|
errorMessage={errors.type?.message}
|
||||||
@ -207,17 +186,25 @@ export const DealTicket = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name="side"
|
name="side"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={() => (
|
||||||
<SideSelector value={field.value} onSelect={field.onChange} />
|
<SideSelector
|
||||||
|
value={order.side}
|
||||||
|
onSelect={(side) => {
|
||||||
|
update({ side });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DealTicketAmount
|
<DealTicketAmount
|
||||||
|
control={control}
|
||||||
orderType={order.type}
|
orderType={order.type}
|
||||||
market={market}
|
market={market}
|
||||||
marketData={marketData}
|
marketData={marketData}
|
||||||
register={register}
|
|
||||||
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"
|
||||||
@ -228,11 +215,16 @@ export const DealTicket = ({
|
|||||||
marketData.trigger
|
marketData.trigger
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
render={({ field }) => (
|
render={() => (
|
||||||
<TimeInForceSelector
|
<TimeInForceSelector
|
||||||
value={field.value}
|
value={order.timeInForce}
|
||||||
orderType={order.type}
|
orderType={order.type}
|
||||||
onSelect={field.onChange}
|
onSelect={(timeInForce) => {
|
||||||
|
update({ timeInForce });
|
||||||
|
// 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 }));
|
||||||
|
}}
|
||||||
market={market}
|
market={market}
|
||||||
marketData={marketData}
|
marketData={marketData}
|
||||||
errorMessage={errors.timeInForce?.message}
|
errorMessage={errors.timeInForce?.message}
|
||||||
@ -244,12 +236,18 @@ export const DealTicket = ({
|
|||||||
<Controller
|
<Controller
|
||||||
name="expiresAt"
|
name="expiresAt"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
rules={{
|
||||||
|
validate: validateExpiration,
|
||||||
|
}}
|
||||||
|
render={() => (
|
||||||
<ExpirySelector
|
<ExpirySelector
|
||||||
value={field.value}
|
value={order.expiresAt}
|
||||||
onSelect={field.onChange}
|
onSelect={(expiresAt) =>
|
||||||
|
update({
|
||||||
|
expiresAt: expiresAt || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
errorMessage={errors.expiresAt?.message}
|
errorMessage={errors.expiresAt?.message}
|
||||||
register={register}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -261,7 +259,7 @@ export const DealTicket = ({
|
|||||||
order={order}
|
order={order}
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
pubKey={pubKey}
|
pubKey={pubKey}
|
||||||
onClickCollateral={onClickCollateral || (() => null)}
|
onClickCollateral={onClickCollateral}
|
||||||
/>
|
/>
|
||||||
<DealTicketButton
|
<DealTicketButton
|
||||||
disabled={Object.keys(errors).length >= 1 || isReadOnly}
|
disabled={Object.keys(errors).length >= 1 || isReadOnly}
|
||||||
@ -284,10 +282,10 @@ interface SummaryMessageProps {
|
|||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
market: Market;
|
market: Market;
|
||||||
marketData: MarketData;
|
marketData: MarketData;
|
||||||
order: OrderSubmissionBody['orderSubmission'];
|
order: OrderObj;
|
||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
pubKey: string | null;
|
pubKey: string | null;
|
||||||
onClickCollateral: () => void;
|
onClickCollateral?: () => void;
|
||||||
}
|
}
|
||||||
const SummaryMessage = memo(
|
const SummaryMessage = memo(
|
||||||
({
|
({
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||||
import { formatForInput } from '@vegaprotocol/utils';
|
import { formatForInput } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import type { UseFormRegister } from 'react-hook-form';
|
|
||||||
import { validateExpiration } from '../../utils/validate-expiration';
|
|
||||||
import type { DealTicketFormFields } from '.';
|
|
||||||
|
|
||||||
interface ExpirySelectorProps {
|
interface ExpirySelectorProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
onSelect: (expiration: string | null) => void;
|
onSelect: (expiration: string | null) => void;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
register?: UseFormRegister<DealTicketFormFields>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExpirySelector = ({
|
export const ExpirySelector = ({
|
||||||
value,
|
value,
|
||||||
onSelect,
|
onSelect,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
register,
|
|
||||||
}: ExpirySelectorProps) => {
|
}: ExpirySelectorProps) => {
|
||||||
const date = value ? new Date(value) : new Date();
|
const date = value ? new Date(value) : new Date();
|
||||||
const dateFormatted = formatForInput(date);
|
const dateFormatted = formatForInput(date);
|
||||||
@ -34,9 +29,6 @@ export const ExpirySelector = ({
|
|||||||
value={dateFormatted}
|
value={dateFormatted}
|
||||||
onChange={(e) => onSelect(e.target.value)}
|
onChange={(e) => onSelect(e.target.value)}
|
||||||
min={minDate}
|
min={minDate}
|
||||||
{...register?.('expiresAt', {
|
|
||||||
validate: validateExpiration,
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<InputError testId="dealticket-error-message-expiry">
|
<InputError testId="dealticket-error-message-expiry">
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import {
|
import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
InputError,
|
InputError,
|
||||||
@ -22,15 +21,6 @@ interface TimeInForceSelectorProps {
|
|||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderType = Schema.OrderType.TYPE_MARKET | Schema.OrderType.TYPE_LIMIT;
|
|
||||||
type PreviousTimeInForce = {
|
|
||||||
[key in OrderType]: Schema.OrderTimeInForce;
|
|
||||||
};
|
|
||||||
const DEFAULT_TIME_IN_FORCE: PreviousTimeInForce = {
|
|
||||||
[Schema.OrderType.TYPE_MARKET]: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
|
||||||
[Schema.OrderType.TYPE_LIMIT]: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TimeInForceSelector = ({
|
export const TimeInForceSelector = ({
|
||||||
value,
|
value,
|
||||||
orderType,
|
orderType,
|
||||||
@ -47,28 +37,6 @@ export const TimeInForceSelector = ({
|
|||||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
|
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
|
||||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||||
);
|
);
|
||||||
const [previousOrderType, setPreviousOrderType] = useState(
|
|
||||||
Schema.OrderType.TYPE_MARKET
|
|
||||||
);
|
|
||||||
const [previousTimeInForce, setPreviousTimeInForce] =
|
|
||||||
useState<PreviousTimeInForce>({
|
|
||||||
...DEFAULT_TIME_IN_FORCE,
|
|
||||||
[orderType]: value,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (previousOrderType !== orderType) {
|
|
||||||
setPreviousOrderType(orderType);
|
|
||||||
const prev = previousTimeInForce[orderType as OrderType];
|
|
||||||
onSelect(prev);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
onSelect,
|
|
||||||
orderType,
|
|
||||||
previousTimeInForce,
|
|
||||||
previousOrderType,
|
|
||||||
setPreviousOrderType,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const renderError = (errorType: string) => {
|
const renderError = (errorType: string) => {
|
||||||
if (errorType === MarketModeValidationType.Auction) {
|
if (errorType === MarketModeValidationType.Auction) {
|
||||||
@ -128,10 +96,16 @@ export const TimeInForceSelector = ({
|
|||||||
id="select-time-in-force"
|
id="select-time-in-force"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setPreviousTimeInForce({
|
// setPreviousTimeInForce({
|
||||||
...previousTimeInForce,
|
// ...previousTimeInForce,
|
||||||
[orderType]: e.target.value,
|
// [orderType]: e.target.value,
|
||||||
});
|
// });
|
||||||
|
|
||||||
|
// if (previousOrderType !== orderType) {
|
||||||
|
// setPreviousOrderType(orderType);
|
||||||
|
// const prev = previousTimeInForce[orderType as OrderType];
|
||||||
|
// onSelect(prev);
|
||||||
|
// }
|
||||||
onSelect(e.target.value as Schema.OrderTimeInForce);
|
onSelect(e.target.value as Schema.OrderTimeInForce);
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
66
libs/deal-ticket/src/hooks/use-order-form.spec.ts
Normal file
66
libs/deal-ticket/src/hooks/use-order-form.spec.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import omit from 'lodash/omit';
|
||||||
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import { getDefaultOrder, useOrderStore } 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));
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
74
libs/deal-ticket/src/hooks/use-order-form.ts
Normal file
74
libs/deal-ticket/src/hooks/use-order-form.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
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';
|
||||||
|
import type { Exact } from 'type-fest';
|
||||||
|
|
||||||
|
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: <T>(o: Exact<OrderSubmission, T>) => void
|
||||||
|
) => {
|
||||||
|
return handleSubmit(() => {
|
||||||
|
// remove the persist key from the order in the store, the wallet will reject
|
||||||
|
// an order that contains unrecognized additional keys
|
||||||
|
cb(omit(order, 'persist'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
order,
|
||||||
|
update,
|
||||||
|
control,
|
||||||
|
errors,
|
||||||
|
setError,
|
||||||
|
clearErrors,
|
||||||
|
getValues, // returned for test purposes only
|
||||||
|
handleSubmit: handleSubmitWrapper,
|
||||||
|
};
|
||||||
|
};
|
@ -17,7 +17,8 @@
|
|||||||
"**/*.test.js",
|
"**/*.test.js",
|
||||||
"**/*.spec.jsx",
|
"**/*.spec.jsx",
|
||||||
"**/*.test.jsx",
|
"**/*.test.jsx",
|
||||||
"jest.config.ts"
|
"jest.config.ts",
|
||||||
|
"__mocks__"
|
||||||
],
|
],
|
||||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
"**/*.test.jsx",
|
"**/*.test.jsx",
|
||||||
"**/*.spec.jsx",
|
"**/*.spec.jsx",
|
||||||
"**/*.d.ts",
|
"**/*.d.ts",
|
||||||
"**/__mocks__/*.tsx",
|
|
||||||
"jest.config.ts"
|
"jest.config.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
getPriceLevel,
|
getPriceLevel,
|
||||||
} from './orderbook-data';
|
} from './orderbook-data';
|
||||||
import type { OrderbookData } from './orderbook-data';
|
import type { OrderbookData } from './orderbook-data';
|
||||||
import { usePersistedOrderStore } from '@vegaprotocol/orders';
|
import { useOrderStore } from '@vegaprotocol/orders';
|
||||||
|
|
||||||
interface OrderbookManagerProps {
|
interface OrderbookManagerProps {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
@ -172,7 +172,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
|||||||
flush();
|
flush();
|
||||||
}, [resolution, flush]);
|
}, [resolution, flush]);
|
||||||
|
|
||||||
const updatePrice = usePersistedOrderStore((store) => store.updatePrice);
|
const updateOrder = useOrderStore((store) => store.update);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
@ -190,7 +190,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
|||||||
onClick={(price?: string | number) => {
|
onClick={(price?: string | number) => {
|
||||||
if (price) {
|
if (price) {
|
||||||
const priceValue = addDecimal(price, market?.decimalPlaces ?? 0);
|
const priceValue = addDecimal(price, market?.decimalPlaces ?? 0);
|
||||||
updatePrice(marketId, priceValue);
|
updateOrder(marketId, { price: priceValue });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
21
libs/orders/__mocks__/zustand.ts
Normal file
21
libs/orders/__mocks__/zustand.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { StateCreator } from 'zustand';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
const { create: actualCreate } = jest.requireActual('zustand'); // if using jest
|
||||||
|
|
||||||
|
// a variable to hold reset functions for all stores declared in the app
|
||||||
|
const storeResetFns = new Set<() => void>();
|
||||||
|
|
||||||
|
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||||
|
export const create =
|
||||||
|
() =>
|
||||||
|
<S>(createState: StateCreator<S>) => {
|
||||||
|
const store = actualCreate(createState);
|
||||||
|
const initialState = store.getState();
|
||||||
|
storeResetFns.add(() => store.setState(initialState, true));
|
||||||
|
return store;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset all stores after each test run
|
||||||
|
beforeEach(() => {
|
||||||
|
act(() => storeResetFns.forEach((resetFn) => resetFn()));
|
||||||
|
});
|
@ -8,6 +8,10 @@ import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
|||||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
|
// @ts-ignore OrderList is read only but we need to override with the forwardRef to
|
||||||
|
// avoid warnings about padding refs
|
||||||
|
orderListMock.OrderListTable = forwardRef(() => <div>OrderList</div>);
|
||||||
|
|
||||||
const generateJsx = () => {
|
const generateJsx = () => {
|
||||||
const pubKey = '0x123';
|
const pubKey = '0x123';
|
||||||
return (
|
return (
|
||||||
@ -56,9 +60,6 @@ describe('OrderListManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render the order list if orders provided', async () => {
|
it('should render the order list if orders provided', async () => {
|
||||||
// @ts-ignore OrderList is read only but we need to override with the forwardRef to
|
|
||||||
// avoid warnings about padding refs
|
|
||||||
orderListMock.OrderListTable = forwardRef(() => <div>OrderList</div>);
|
|
||||||
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||||
data: [{ id: '1' } as OrderFieldsFragment],
|
data: [{ id: '1' } as OrderFieldsFragment],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -6,7 +6,6 @@ import type { PartialDeep } from 'type-fest';
|
|||||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
import type { OrderListTableProps } from '../';
|
import type { OrderListTableProps } from '../';
|
||||||
import { OrderListTable } from '../';
|
import { OrderListTable } from '../';
|
||||||
import {
|
import {
|
||||||
@ -15,6 +14,15 @@ import {
|
|||||||
marketOrder,
|
marketOrder,
|
||||||
} from '../mocks/generate-orders';
|
} from '../mocks/generate-orders';
|
||||||
|
|
||||||
|
// Mock theme switcher to get around inconsistent mocking of zustand
|
||||||
|
// stores
|
||||||
|
jest.mock('@vegaprotocol/react-helpers', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/react-helpers'),
|
||||||
|
useThemeSwitcher: () => ({
|
||||||
|
theme: 'light',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const defaultProps: OrderListTableProps = {
|
const defaultProps: OrderListTableProps = {
|
||||||
rowData: [],
|
rowData: [],
|
||||||
setEditOrder: jest.fn(),
|
setEditOrder: jest.fn(),
|
||||||
|
@ -4,4 +4,4 @@ export * from './use-order-cancel';
|
|||||||
export * from './use-order-submit';
|
export * from './use-order-submit';
|
||||||
export * from './use-order-edit';
|
export * from './use-order-edit';
|
||||||
export * from './use-order-update';
|
export * from './use-order-update';
|
||||||
export * from './use-persisted-order';
|
export * from './use-order-store';
|
||||||
|
121
libs/orders/src/lib/order-hooks/use-order-store.spec.ts
Normal file
121
libs/orders/src/lib/order-hooks/use-order-store.spec.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import {
|
||||||
|
getDefaultOrder,
|
||||||
|
STORAGE_KEY,
|
||||||
|
useOrder,
|
||||||
|
useOrderStore,
|
||||||
|
} from './use-order-store';
|
||||||
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import { OrderType } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
jest.mock('zustand');
|
||||||
|
|
||||||
|
describe('useOrderStore', () => {
|
||||||
|
const setup = () => {
|
||||||
|
return renderHook(() => useOrderStore());
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
117
libs/orders/src/lib/order-hooks/use-order-store.ts
Normal file
117
libs/orders/src/lib/order-hooks/use-order-store.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { OrderTimeInForce, Side } from '@vegaprotocol/types';
|
||||||
|
import { OrderType } from '@vegaprotocol/types';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
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
|
||||||
|
};
|
||||||
|
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';
|
||||||
|
|
||||||
|
export const useOrderStore = create<Store>()(
|
||||||
|
persist(
|
||||||
|
subscribeWithSelector((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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an order from the store for a market and
|
||||||
|
* creates one if it doesn't already exist
|
||||||
|
*/
|
||||||
|
export const useOrder = (marketId: string) => {
|
||||||
|
const [order, _update] = useOrderStore((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 doesnt exist, but don't
|
||||||
|
// persist until user has edited
|
||||||
|
useEffect(() => {
|
||||||
|
if (!order) {
|
||||||
|
update(
|
||||||
|
getDefaultOrder(marketId),
|
||||||
|
false // dont 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_MARKET,
|
||||||
|
side: Side.SIDE_BUY,
|
||||||
|
timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||||
|
size: '0',
|
||||||
|
price: '0',
|
||||||
|
expiresAt: undefined,
|
||||||
|
persist: false,
|
||||||
|
});
|
@ -1,78 +0,0 @@
|
|||||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
|
||||||
import produce from 'immer';
|
|
||||||
import { create } from 'zustand';
|
|
||||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
|
||||||
import isEqual from 'lodash/isEqual';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
type OrderData = OrderSubmissionBody['orderSubmission'] | null;
|
|
||||||
|
|
||||||
type PersistedOrderStore = {
|
|
||||||
orders: OrderData[];
|
|
||||||
getOrder: (marketId: string) => OrderData | undefined;
|
|
||||||
setOrder: (order: OrderData) => void;
|
|
||||||
clear: () => void;
|
|
||||||
updatePrice: (marketId: string, price: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const usePersistedOrderStore = create<PersistedOrderStore>()(
|
|
||||||
persist(
|
|
||||||
subscribeWithSelector((set, get) => ({
|
|
||||||
orders: [],
|
|
||||||
getOrder: (marketId: string) => {
|
|
||||||
const current = get() as PersistedOrderStore;
|
|
||||||
const persisted = current.orders.find((o) => o?.marketId === marketId);
|
|
||||||
return persisted;
|
|
||||||
},
|
|
||||||
setOrder: (order: OrderData) => {
|
|
||||||
set(
|
|
||||||
produce((store: PersistedOrderStore) => {
|
|
||||||
const persisted = store.orders.find(
|
|
||||||
(o) => o?.marketId === order?.marketId
|
|
||||||
);
|
|
||||||
if (persisted) {
|
|
||||||
if (!isEqual(persisted, order)) {
|
|
||||||
Object.assign(persisted, order);
|
|
||||||
} else {
|
|
||||||
// NOOP
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
store.orders.push(order);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
clear: () => set({ orders: [] }),
|
|
||||||
updatePrice: (marketId: string, price: string) =>
|
|
||||||
set(
|
|
||||||
produce((store: PersistedOrderStore) => {
|
|
||||||
const persisted = store.orders.find(
|
|
||||||
(o) => o?.marketId === marketId
|
|
||||||
);
|
|
||||||
if (persisted) {
|
|
||||||
persisted.price = price;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
name: 'VEGA_DEAL_TICKET_ORDER_STORE',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const usePersistedOrderStoreSubscription = (
|
|
||||||
marketId: string,
|
|
||||||
onOrderChange: (order: NonNullable<OrderData>) => void
|
|
||||||
) => {
|
|
||||||
const selector = (state: PersistedOrderStore) =>
|
|
||||||
state.orders.find((o) => o?.marketId === marketId);
|
|
||||||
const action = (storedOrder: OrderData | undefined) => {
|
|
||||||
if (storedOrder) {
|
|
||||||
onOrderChange(storedOrder);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const unsubscribe = usePersistedOrderStore.subscribe(selector, action);
|
|
||||||
useEffect(() => () => unsubscribe(), [unsubscribe]);
|
|
||||||
};
|
|
@ -21,7 +21,8 @@
|
|||||||
"**/*.stories.js",
|
"**/*.stories.js",
|
||||||
"**/*.stories.jsx",
|
"**/*.stories.jsx",
|
||||||
"**/*.stories.tsx",
|
"**/*.stories.tsx",
|
||||||
"jest.config.ts"
|
"jest.config.ts",
|
||||||
|
"__mocks__"
|
||||||
],
|
],
|
||||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { MAX_TRADES, tradesWithMarketProvider } from './trades-data-provider';
|
|||||||
import { TradesTable } from './trades-table';
|
import { TradesTable } from './trades-table';
|
||||||
import type { Trade, TradeEdge } from './trades-data-provider';
|
import type { Trade, TradeEdge } from './trades-data-provider';
|
||||||
import type { TradesQueryVariables } from './__generated__/Trades';
|
import type { TradesQueryVariables } from './__generated__/Trades';
|
||||||
import { usePersistedOrderStore } from '@vegaprotocol/orders';
|
import { useOrderStore } from '@vegaprotocol/orders';
|
||||||
|
|
||||||
interface TradesContainerProps {
|
interface TradesContainerProps {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
@ -20,7 +20,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
|||||||
const totalCountRef = useRef<number | undefined>(undefined);
|
const totalCountRef = useRef<number | undefined>(undefined);
|
||||||
const newRows = useRef(0);
|
const newRows = useRef(0);
|
||||||
const scrolledToTop = useRef(true);
|
const scrolledToTop = useRef(true);
|
||||||
const updatePrice = usePersistedOrderStore((store) => store.updatePrice);
|
const updateOrder = useOrderStore((store) => store.update);
|
||||||
|
|
||||||
const variables = useMemo<TradesQueryVariables>(
|
const variables = useMemo<TradesQueryVariables>(
|
||||||
() => ({ marketId, maxTrades: MAX_TRADES }),
|
() => ({ marketId, maxTrades: MAX_TRADES }),
|
||||||
@ -115,7 +115,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
|||||||
onBodyScroll={onBodyScroll}
|
onBodyScroll={onBodyScroll}
|
||||||
onClick={(price?: string) => {
|
onClick={(price?: string) => {
|
||||||
if (price) {
|
if (price) {
|
||||||
updatePrice(marketId, price);
|
updateOrder(marketId, { price });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -89,6 +89,7 @@ export const Notification = ({
|
|||||||
onClick={buttonProps.action}
|
onClick={buttonProps.action}
|
||||||
className={classNames(buttonProps.className)}
|
className={classNames(buttonProps.className)}
|
||||||
data-testid={buttonProps.dataTestId}
|
data-testid={buttonProps.dataTestId}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
{buttonProps.text}
|
{buttonProps.text}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -5,11 +5,12 @@ 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 {
|
||||||
OrderAmendmentBody,
|
OrderAmendment,
|
||||||
OrderSubmissionBody,
|
OrderSubmission,
|
||||||
Transaction,
|
Transaction,
|
||||||
Transfer,
|
Transfer,
|
||||||
} from './connectors';
|
} from './connectors';
|
||||||
|
import type { Exact } from 'type-fest';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an ID in the same way that core does on the backend. This way we
|
* Creates an ID in the same way that core does on the backend. This way we
|
||||||
@ -28,11 +29,11 @@ export const encodeTransaction = (tx: Transaction): string => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalizeOrderSubmission = (
|
export const normalizeOrderSubmission = <T extends Exact<OrderSubmission, T>>(
|
||||||
order: OrderSubmissionBody['orderSubmission'],
|
order: T,
|
||||||
decimalPlaces: number,
|
decimalPlaces: number,
|
||||||
positionDecimalPlaces: number
|
positionDecimalPlaces: number
|
||||||
): OrderSubmissionBody['orderSubmission'] => ({
|
): OrderSubmission => ({
|
||||||
...order,
|
...order,
|
||||||
price:
|
price:
|
||||||
order.type === OrderType.TYPE_LIMIT && order.price
|
order.type === OrderType.TYPE_LIMIT && order.price
|
||||||
@ -45,12 +46,12 @@ export const normalizeOrderSubmission = (
|
|||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const normalizeOrderAmendment = (
|
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'>,
|
||||||
price: string,
|
price: string,
|
||||||
size: string
|
size: string
|
||||||
): OrderAmendmentBody['orderAmendment'] => ({
|
): OrderAmendment => ({
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
marketId: market.id,
|
marketId: market.id,
|
||||||
price: removeDecimal(price, market.decimalPlaces),
|
price: removeDecimal(price, market.decimalPlaces),
|
||||||
@ -65,7 +66,7 @@ export const normalizeOrderAmendment = (
|
|||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const normalizeTransfer = (
|
export const normalizeTransfer = <T extends Exact<Transfer, T>>(
|
||||||
address: string,
|
address: string,
|
||||||
amount: string,
|
amount: string,
|
||||||
asset: {
|
asset: {
|
||||||
|
Loading…
Reference in New Issue
Block a user