diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts index 83536a1c8..79efb2358 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts @@ -669,7 +669,7 @@ describe('account validation', { tags: '@regression' }, () => { 'have.text', 'You need ' + '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'); }); diff --git a/libs/deal-ticket/__mocks__/zustand.ts b/libs/deal-ticket/__mocks__/zustand.ts new file mode 100644 index 000000000..b42750b88 --- /dev/null +++ b/libs/deal-ticket/__mocks__/zustand.ts @@ -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 = + () => + (createState: StateCreator) => { + 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())); +}); diff --git a/libs/deal-ticket/src/components/deal-ticket-validation/zero-balance-error.tsx b/libs/deal-ticket/src/components/deal-ticket-validation/zero-balance-error.tsx index 6a4c83eec..aca37b255 100644 --- a/libs/deal-ticket/src/components/deal-ticket-validation/zero-balance-error.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-validation/zero-balance-error.tsx @@ -7,7 +7,7 @@ interface ZeroBalanceErrorProps { id: string; symbol: string; }; - onClickCollateral: () => void; + onClickCollateral?: () => void; } export const ZeroBalanceError = ({ @@ -21,8 +21,12 @@ export const ZeroBalanceError = ({ testId="dealticket-error-message-zero-balance" message={ <> - You need {asset.symbol} in your wallet to trade in this market. See - all your collateral. + You need {asset.symbol} in your wallet to trade in this market. + {onClickCollateral && ( + <> + See all your collateral. + + )} } buttonProps={{ diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-amount.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-amount.tsx index 1ba5906a0..c2ac8fa61 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-amount.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-amount.tsx @@ -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 { DealTicketMarketAmount } from './deal-ticket-market-amount'; import { DealTicketLimitAmount } from './deal-ticket-limit-amount'; 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 { + control: Control; orderType: Schema.OrderType; marketData: MarketData; market: Market; - register: UseFormRegister; sizeError?: string; priceError?: string; + update: (obj: Partial) => void; + size: string; + price?: string; } export const DealTicketAmount = ({ diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx index c7db7eca1..93d00d3c3 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx @@ -47,7 +47,7 @@ export const DealTicketContainer = ({ market={market} marketData={marketData} submit={(orderSubmission) => create({ orderSubmission })} - onClickCollateral={onClickCollateral || (() => null)} + onClickCollateral={onClickCollateral} /> ) : ( diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx index a2946dc2f..dcbd52274 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx @@ -2,6 +2,7 @@ import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit'; import { toDecimal, validateAmount } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import type { DealTicketAmountProps } from './deal-ticket-amount'; +import { Controller } from 'react-hook-form'; export type DealTicketLimitAmountProps = Omit< Omit, @@ -9,10 +10,13 @@ export type DealTicketLimitAmountProps = Omit< >; export const DealTicketLimitAmount = ({ - register, + control, market, sizeError, priceError, + update, + price, + size, }: DealTicketLimitAmountProps) => { const priceStep = toDecimal(market?.decimalPlaces); const sizeStep = toDecimal(market?.positionDecimalPlaces); @@ -47,22 +51,30 @@ export const DealTicketLimitAmount = ({ labelFor="input-order-size-limit" className="!mb-1" > - e.currentTarget.blur()} - {...register('size', { + ( + update({ size: e.target.value })} + step={sizeStep} + min={sizeStep} + data-testid="order-size" + onWheel={(e) => e.currentTarget.blur()} + /> + )} /> @@ -77,14 +89,10 @@ export const DealTicketLimitAmount = ({ labelAlign="right" className="!mb-1" > - e.currentTarget.blur()} - {...register('price', { + ( + update({ price: e.target.value })} + step={priceStep} + data-testid="order-price" + onWheel={(e) => e.currentTarget.blur()} + /> + )} /> diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx index 86ca473e0..c55c7fbe8 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx @@ -8,6 +8,7 @@ import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit'; import { isMarketInAuction } from '../../utils'; import type { DealTicketAmountProps } from './deal-ticket-amount'; import { getMarketPrice } from '../../utils/get-price'; +import { Controller } from 'react-hook-form'; export type DealTicketMarketAmountProps = Omit< DealTicketAmountProps, @@ -15,10 +16,12 @@ export type DealTicketMarketAmountProps = Omit< >; export const DealTicketMarketAmount = ({ - register, + control, market, marketData, sizeError, + update, + size, }: DealTicketMarketAmountProps) => { const quoteName = market.tradableInstrument.instrument.product.quoteName; const sizeStep = toDecimal(market?.positionDecimalPlaces); @@ -47,22 +50,30 @@ export const DealTicketMarketAmount = ({
- e.currentTarget.blur()} - data-testid="order-size" - {...register('size', { + ( + update({ size: e.target.value })} + step={sizeStep} + min={sizeStep} + onWheel={(e) => e.currentTarget.blur()} + data-testid="order-size" + /> + )} />
@
diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx index 6ef4e3eb4..71b99cf2b 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx @@ -1,38 +1,28 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 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 { DealTicket } from './deal-ticket'; 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 type { ChainIdQuery } from '@vegaprotocol/react-helpers'; -import { ChainIdDocument } from '@vegaprotocol/react-helpers'; import { addDecimal } from '@vegaprotocol/utils'; +import { useOrderStore } from '@vegaprotocol/orders'; +jest.mock('zustand'); +jest.mock('./deal-ticket-fee-details', () => ({ + DealTicketFeeDetails: () =>
, +})); + +const pubKey = 'pubKey'; const market = generateMarket(); const marketData = generateMarketData(); const submit = jest.fn(); -const mockChainId = 'chain-id'; - -function generateJsx(order?: OrderSubmissionBody['orderSubmission']) { - const chainIdMock: MockedResponse = { - request: { - query: ChainIdDocument, - }, - result: { - data: { - statistics: { - chainId: mockChainId, - }, - }, - }, - }; +function generateJsx() { return ( - - + + @@ -41,10 +31,11 @@ function generateJsx(order?: OrderSubmissionBody['orderSubmission']) { describe('DealTicket', () => { beforeEach(() => { - window.localStorage.clear(); + localStorage.clear(); }); + afterEach(() => { - window.localStorage.clear(); + localStorage.clear(); jest.clearAllMocks(); }); @@ -61,9 +52,7 @@ describe('DealTicket', () => { expect( screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input') ).not.toBeChecked(); - expect(screen.getByTestId('order-size')).toHaveDisplayValue( - String(1 / Math.pow(10, market.positionDecimalPlaces)) - ); + expect(screen.getByTestId('order-size')).toHaveDisplayValue('0'); expect(screen.getByTestId('order-tif')).toHaveValue( 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()); // 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)']); + // 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 - fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT')); + await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT')); expect(screen.getByTestId('order-tif').children).toHaveLength( Object.keys(Schema.OrderTimeInForce).length ); - // Select GTC -> GTC should be selected - 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 GTC as LIMIT default expect(screen.getByTestId('order-tif')).toHaveValue( Schema.OrderTimeInForce.TIME_IN_FORCE_GTC ); // Select GTT -> GTT should be selected - fireEvent.change(screen.getByTestId('order-tif'), { - target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT }, - }); + await userEvent.selectOptions( + screen.getByTestId('order-tif'), + Schema.OrderTimeInForce.TIME_IN_FORCE_GTT + ); expect(screen.getByTestId('order-tif')).toHaveValue( Schema.OrderTimeInForce.TIME_IN_FORCE_GTT ); - // Switch to type market order -> IOC should be selected - fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET')); + // Switch back to type market order -> FOK should be preserved from previous selection + 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( Schema.OrderTimeInForce.TIME_IN_FORCE_IOC ); @@ -143,23 +196,20 @@ describe('DealTicket', () => { screen.getByTestId('order-side-SIDE_BUY')?.querySelector('input') ).toBeChecked(); - await act(async () => { - fireEvent.change(screen.getByTestId('order-size'), { - target: { value: '200' }, - }); - }); + await userEvent.type(screen.getByTestId('order-size'), '200'); expect(screen.getByTestId('order-size')).toHaveDisplayValue('200'); - fireEvent.change(screen.getByTestId('order-tif'), { - target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC }, - }); + 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 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 expect(screen.getByTestId('order-tif').children).toHaveLength( diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index 6f2d39d66..b4de0f9f0 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -1,7 +1,7 @@ import { t } from '@vegaprotocol/i18n'; import * as Schema from '@vegaprotocol/types'; -import { memo, useCallback, useEffect } from 'react'; -import { Controller, useForm } from 'react-hook-form'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { Controller } from 'react-hook-form'; import { DealTicketAmount } from './deal-ticket-amount'; import { DealTicketButton } from './deal-ticket-button'; import { DealTicketFeeDetails } from './deal-ticket-fee-details'; @@ -9,10 +9,12 @@ import { ExpirySelector } from './expiry-selector'; import { SideSelector } from './side-selector'; import { TimeInForceSelector } from './time-in-force-selector'; import { TypeSelector } from './type-selector'; -import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; -import { useVegaWalletDialogStore } from '@vegaprotocol/wallet'; -import { normalizeOrderSubmission } from '@vegaprotocol/wallet'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import type { OrderSubmission } from '@vegaprotocol/wallet'; +import { + normalizeOrderSubmission, + useVegaWallet, + useVegaWalletDialogStore, +} from '@vegaprotocol/wallet'; import { ExternalLink, InputError, @@ -22,7 +24,7 @@ import { import { useOrderMarginValidation } from '../../hooks/use-order-margin-validation'; import { MarginWarning } from '../deal-ticket-validation/margin-warning'; import { - getDefaultOrder, + validateExpiration, validateMarketState, validateMarketTradingMode, validateTimeInForce, @@ -32,27 +34,17 @@ import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error'; import { SummaryValidationType } from '../../constants'; import { useHasNoBalance } from '../../hooks/use-has-no-balance'; import type { Market, MarketData } from '@vegaprotocol/market-list'; -import { - usePersistedOrderStore, - usePersistedOrderStoreSubscription, -} from '@vegaprotocol/orders'; -import { OrderType } from '@vegaprotocol/types'; - -export type TransactionStatus = 'default' | 'pending'; +import { OrderTimeInForce, OrderType } from '@vegaprotocol/types'; +import { useOrderForm } from '../../hooks/use-order-form'; +import type { OrderObj } from '@vegaprotocol/orders'; export interface DealTicketProps { market: Market; marketData: MarketData; - submit: (order: OrderSubmissionBody['orderSubmission']) => void; + submit: (order: OrderSubmission) => 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 = ({ market, marketData, @@ -60,44 +52,21 @@ export const DealTicket = ({ onClickCollateral, }: DealTicketProps) => { const { pubKey, isReadOnly } = useVegaWallet(); - const { getPersistedOrder, setPersistedOrder } = usePersistedOrderStore( - (store) => ({ - getPersistedOrder: store.getOrder, - setPersistedOrder: store.setOrder, - }) - ); - + // store last used tif for market so that when changing OrderType the previous TIF + // selection for that type is used when switching back + const [lastTIF, setLastTIF] = useState({ + [OrderType.TYPE_MARKET]: OrderTimeInForce.TIME_IN_FORCE_IOC, + [OrderType.TYPE_LIMIT]: OrderTimeInForce.TIME_IN_FORCE_GTC, + }); const { - register, control, - handleSubmit, - watch, + errors, + order, setError, clearErrors, - formState: { errors }, - setValue, - } = useForm({ - 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); - } - }); - + update, + handleSubmit, + } = useOrderForm(market.id); const marketStateError = validateMarketState(marketData.marketState); const hasNoBalance = useHasNoBalance( market.tradableInstrument.instrument.product.settlementAsset.id @@ -166,7 +135,7 @@ export const DealTicket = ({ ]); const onSubmit = useCallback( - (order: OrderSubmissionBody['orderSubmission']) => { + (order: OrderSubmission) => { checkForErrors(); submit( normalizeOrderSubmission( @@ -179,9 +148,12 @@ export const DealTicket = ({ [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 (
null : handleSubmit(onSubmit)} + onSubmit={isReadOnly ? undefined : handleSubmit(onSubmit)} className="p-4" noValidate > @@ -194,10 +166,17 @@ export const DealTicket = ({ marketData.trigger ), }} - render={({ field }) => ( + render={() => ( { + 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} marketData={marketData} errorMessage={errors.type?.message} @@ -207,17 +186,25 @@ export const DealTicket = ({ ( - + render={() => ( + { + update({ side }); + }} + /> )} /> ( + render={() => ( { + 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} marketData={marketData} errorMessage={errors.timeInForce?.message} @@ -244,12 +236,18 @@ export const DealTicket = ({ ( + rules={{ + validate: validateExpiration, + }} + render={() => ( + update({ + expiresAt: expiresAt || undefined, + }) + } errorMessage={errors.expiresAt?.message} - register={register} /> )} /> @@ -261,7 +259,7 @@ export const DealTicket = ({ order={order} isReadOnly={isReadOnly} pubKey={pubKey} - onClickCollateral={onClickCollateral || (() => null)} + onClickCollateral={onClickCollateral} /> = 1 || isReadOnly} @@ -284,10 +282,10 @@ interface SummaryMessageProps { errorMessage?: string; market: Market; marketData: MarketData; - order: OrderSubmissionBody['orderSubmission']; + order: OrderObj; isReadOnly: boolean; pubKey: string | null; - onClickCollateral: () => void; + onClickCollateral?: () => void; } const SummaryMessage = memo( ({ diff --git a/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx b/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx index b4abb8975..09767826d 100644 --- a/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/expiry-selector.tsx @@ -1,22 +1,17 @@ import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit'; import { formatForInput } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; -import type { UseFormRegister } from 'react-hook-form'; -import { validateExpiration } from '../../utils/validate-expiration'; -import type { DealTicketFormFields } from '.'; interface ExpirySelectorProps { value?: string; onSelect: (expiration: string | null) => void; errorMessage?: string; - register?: UseFormRegister; } export const ExpirySelector = ({ value, onSelect, errorMessage, - register, }: ExpirySelectorProps) => { const date = value ? new Date(value) : new Date(); const dateFormatted = formatForInput(date); @@ -34,9 +29,6 @@ export const ExpirySelector = ({ value={dateFormatted} onChange={(e) => onSelect(e.target.value)} min={minDate} - {...register?.('expiresAt', { - validate: validateExpiration, - })} /> {errorMessage && ( diff --git a/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx b/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx index 5909a6e45..28a114afe 100644 --- a/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from 'react'; import { FormGroup, InputError, @@ -22,15 +21,6 @@ interface TimeInForceSelectorProps { 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 = ({ value, orderType, @@ -47,28 +37,6 @@ export const TimeInForceSelector = ({ timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK || timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC ); - const [previousOrderType, setPreviousOrderType] = useState( - Schema.OrderType.TYPE_MARKET - ); - const [previousTimeInForce, setPreviousTimeInForce] = - useState({ - ...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) => { if (errorType === MarketModeValidationType.Auction) { @@ -128,10 +96,16 @@ export const TimeInForceSelector = ({ id="select-time-in-force" value={value} onChange={(e) => { - setPreviousTimeInForce({ - ...previousTimeInForce, - [orderType]: e.target.value, - }); + // setPreviousTimeInForce({ + // ...previousTimeInForce, + // [orderType]: e.target.value, + // }); + + // if (previousOrderType !== orderType) { + // setPreviousOrderType(orderType); + // const prev = previousTimeInForce[orderType as OrderType]; + // onSelect(prev); + // } onSelect(e.target.value as Schema.OrderTimeInForce); }} className="w-full" diff --git a/libs/deal-ticket/src/hooks/use-order-form.spec.ts b/libs/deal-ticket/src/hooks/use-order-form.spec.ts new file mode 100644 index 000000000..093146c05 --- /dev/null +++ b/libs/deal-ticket/src/hooks/use-order-form.spec.ts @@ -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(); + }); +}); diff --git a/libs/deal-ticket/src/hooks/use-order-form.ts b/libs/deal-ticket/src/hooks/use-order-form.ts new file mode 100644 index 000000000..dfdc4dc2c --- /dev/null +++ b/libs/deal-ticket/src/hooks/use-order-form.ts @@ -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({ + // 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: Exact) => 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, + }; +}; diff --git a/libs/deal-ticket/tsconfig.lib.json b/libs/deal-ticket/tsconfig.lib.json index b2447df1e..c04a92da1 100644 --- a/libs/deal-ticket/tsconfig.lib.json +++ b/libs/deal-ticket/tsconfig.lib.json @@ -17,7 +17,8 @@ "**/*.test.js", "**/*.spec.jsx", "**/*.test.jsx", - "jest.config.ts" + "jest.config.ts", + "__mocks__" ], "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] } diff --git a/libs/environment/tsconfig.spec.json b/libs/environment/tsconfig.spec.json index a717b47cb..3da863401 100644 --- a/libs/environment/tsconfig.spec.json +++ b/libs/environment/tsconfig.spec.json @@ -15,7 +15,6 @@ "**/*.test.jsx", "**/*.spec.jsx", "**/*.d.ts", - "**/__mocks__/*.tsx", "jest.config.ts" ] } diff --git a/libs/market-depth/src/lib/orderbook-manager.tsx b/libs/market-depth/src/lib/orderbook-manager.tsx index 79b855139..20140fc7c 100644 --- a/libs/market-depth/src/lib/orderbook-manager.tsx +++ b/libs/market-depth/src/lib/orderbook-manager.tsx @@ -20,7 +20,7 @@ import { getPriceLevel, } from './orderbook-data'; import type { OrderbookData } from './orderbook-data'; -import { usePersistedOrderStore } from '@vegaprotocol/orders'; +import { useOrderStore } from '@vegaprotocol/orders'; interface OrderbookManagerProps { marketId: string; @@ -172,7 +172,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => { flush(); }, [resolution, flush]); - const updatePrice = usePersistedOrderStore((store) => store.updatePrice); + const updateOrder = useOrderStore((store) => store.update); return ( { onClick={(price?: string | number) => { if (price) { const priceValue = addDecimal(price, market?.decimalPlaces ?? 0); - updatePrice(marketId, priceValue); + updateOrder(marketId, { price: priceValue }); } }} /> diff --git a/libs/orders/__mocks__/zustand.ts b/libs/orders/__mocks__/zustand.ts new file mode 100644 index 000000000..b42750b88 --- /dev/null +++ b/libs/orders/__mocks__/zustand.ts @@ -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 = + () => + (createState: StateCreator) => { + 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())); +}); diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx index d1adb8a5e..46f2fd2e7 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx @@ -8,6 +8,10 @@ import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet'; 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(() =>
OrderList
); + const generateJsx = () => { const pubKey = '0x123'; return ( @@ -56,9 +60,6 @@ describe('OrderListManager', () => { }); 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(() =>
OrderList
); jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ data: [{ id: '1' } as OrderFieldsFragment], loading: false, diff --git a/libs/orders/src/lib/components/order-list/order-list.spec.tsx b/libs/orders/src/lib/components/order-list/order-list.spec.tsx index de97833d0..2e5d59099 100644 --- a/libs/orders/src/lib/components/order-list/order-list.spec.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.spec.tsx @@ -6,7 +6,6 @@ import type { PartialDeep } from 'type-fest'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet'; import { MockedProvider } from '@apollo/client/testing'; - import type { OrderListTableProps } from '../'; import { OrderListTable } from '../'; import { @@ -15,6 +14,15 @@ import { marketOrder, } 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 = { rowData: [], setEditOrder: jest.fn(), diff --git a/libs/orders/src/lib/order-hooks/index.ts b/libs/orders/src/lib/order-hooks/index.ts index f8fe8b26a..793eb4fa5 100644 --- a/libs/orders/src/lib/order-hooks/index.ts +++ b/libs/orders/src/lib/order-hooks/index.ts @@ -4,4 +4,4 @@ export * from './use-order-cancel'; export * from './use-order-submit'; export * from './use-order-edit'; export * from './use-order-update'; -export * from './use-persisted-order'; +export * from './use-order-store'; diff --git a/libs/orders/src/lib/order-hooks/use-order-store.spec.ts b/libs/orders/src/lib/order-hooks/use-order-store.spec.ts new file mode 100644 index 000000000..9a7fb8674 --- /dev/null +++ b/libs/orders/src/lib/order-hooks/use-order-store.spec.ts @@ -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, + }); + }); +}); diff --git a/libs/orders/src/lib/order-hooks/use-order-store.ts b/libs/orders/src/lib/order-hooks/use-order-store.ts new file mode 100644 index 000000000..c79c29d31 --- /dev/null +++ b/libs/orders/src/lib/order-hooks/use-order-store.ts @@ -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, + persist?: boolean +) => void; + +interface Store { + orders: OrderMap; + update: UpdateOrder; +} + +export const STORAGE_KEY = 'vega_order_store'; + +export const useOrderStore = create()( + 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, 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, +}); diff --git a/libs/orders/src/lib/order-hooks/use-persisted-order.ts b/libs/orders/src/lib/order-hooks/use-persisted-order.ts deleted file mode 100644 index d80b13441..000000000 --- a/libs/orders/src/lib/order-hooks/use-persisted-order.ts +++ /dev/null @@ -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()( - 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) => 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]); -}; diff --git a/libs/orders/tsconfig.lib.json b/libs/orders/tsconfig.lib.json index 6a440c7bd..7a5f5ce37 100644 --- a/libs/orders/tsconfig.lib.json +++ b/libs/orders/tsconfig.lib.json @@ -21,7 +21,8 @@ "**/*.stories.js", "**/*.stories.jsx", "**/*.stories.tsx", - "jest.config.ts" + "jest.config.ts", + "__mocks__" ], "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] } diff --git a/libs/trades/src/lib/trades-container.tsx b/libs/trades/src/lib/trades-container.tsx index 1c364c031..98faac955 100644 --- a/libs/trades/src/lib/trades-container.tsx +++ b/libs/trades/src/lib/trades-container.tsx @@ -8,7 +8,7 @@ import { MAX_TRADES, tradesWithMarketProvider } from './trades-data-provider'; import { TradesTable } from './trades-table'; import type { Trade, TradeEdge } from './trades-data-provider'; import type { TradesQueryVariables } from './__generated__/Trades'; -import { usePersistedOrderStore } from '@vegaprotocol/orders'; +import { useOrderStore } from '@vegaprotocol/orders'; interface TradesContainerProps { marketId: string; @@ -20,7 +20,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => { const totalCountRef = useRef(undefined); const newRows = useRef(0); const scrolledToTop = useRef(true); - const updatePrice = usePersistedOrderStore((store) => store.updatePrice); + const updateOrder = useOrderStore((store) => store.update); const variables = useMemo( () => ({ marketId, maxTrades: MAX_TRADES }), @@ -115,7 +115,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => { onBodyScroll={onBodyScroll} onClick={(price?: string) => { if (price) { - updatePrice(marketId, price); + updateOrder(marketId, { price }); } }} /> diff --git a/libs/ui-toolkit/src/components/notification/notification.tsx b/libs/ui-toolkit/src/components/notification/notification.tsx index c0beeefd3..3672ba576 100644 --- a/libs/ui-toolkit/src/components/notification/notification.tsx +++ b/libs/ui-toolkit/src/components/notification/notification.tsx @@ -89,6 +89,7 @@ export const Notification = ({ onClick={buttonProps.action} className={classNames(buttonProps.className)} data-testid={buttonProps.dataTestId} + type="button" > {buttonProps.text} diff --git a/libs/wallet/src/utils.ts b/libs/wallet/src/utils.ts index ff96d1710..273275cb7 100644 --- a/libs/wallet/src/utils.ts +++ b/libs/wallet/src/utils.ts @@ -5,11 +5,12 @@ import BigNumber from 'bignumber.js'; import { ethers } from 'ethers'; import { sha3_256 } from 'js-sha3'; import type { - OrderAmendmentBody, - OrderSubmissionBody, + OrderAmendment, + OrderSubmission, Transaction, Transfer, } from './connectors'; +import type { Exact } from 'type-fest'; /** * 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 = ( - order: OrderSubmissionBody['orderSubmission'], +export const normalizeOrderSubmission = >( + order: T, decimalPlaces: number, positionDecimalPlaces: number -): OrderSubmissionBody['orderSubmission'] => ({ +): OrderSubmission => ({ ...order, price: order.type === OrderType.TYPE_LIMIT && order.price @@ -45,12 +46,12 @@ export const normalizeOrderSubmission = ( : undefined, }); -export const normalizeOrderAmendment = ( +export const normalizeOrderAmendment = >( order: Pick, market: Pick, price: string, size: string -): OrderAmendmentBody['orderAmendment'] => ({ +): OrderAmendment => ({ orderId: order.id, marketId: market.id, price: removeDecimal(price, market.decimalPlaces), @@ -65,7 +66,7 @@ export const normalizeOrderAmendment = ( : undefined, }); -export const normalizeTransfer = ( +export const normalizeTransfer = >( address: string, amount: string, asset: {