diff --git a/src/constants/trade.ts b/src/constants/trade.ts index a6fcd37..ff042cf 100644 --- a/src/constants/trade.ts +++ b/src/constants/trade.ts @@ -116,3 +116,15 @@ export enum MobilePlaceOrderSteps { PlacingOrder = 'PlacingOrder', Confirmation = 'Confirmation', } + +export const CLEARED_TRADE_INPUTS = { + limitPriceInput: '', + triggerPriceInput: '', + trailingPercentInput: '', +}; + +export const CLEARED_SIZE_INPUTS = { + amountInput: '', + usdAmountInput: '', + leverageInput: '', +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index c7cc2f0..1d2ed1e 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -22,6 +22,7 @@ import { useShouldShowFooter } from './useShouldShowFooter'; import { useSelectedNetwork } from './useSelectedNetwork'; import { useStringGetter } from './useStringGetter'; import { useSubaccount } from './useSubaccount'; +import { useTradeFormInputs } from './useTradeFormInputs'; import { useURLConfigs } from './useURLConfigs'; export { @@ -49,5 +50,6 @@ export { useSelectedNetwork, useStringGetter, useSubaccount, + useTradeFormInputs, useURLConfigs, }; diff --git a/src/hooks/useCurrentMarketId.ts b/src/hooks/useCurrentMarketId.ts index bba116f..7530a2c 100644 --- a/src/hooks/useCurrentMarketId.ts +++ b/src/hooks/useCurrentMarketId.ts @@ -23,6 +23,7 @@ export const useCurrentMarketId = () => { const selectedNetwork = useSelector(getSelectedNetwork); const marketIds = useSelector(getMarketIds, shallowEqual); const hasMarketIds = marketIds.length > 0; + const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({ key: LocalStorageKey.LastViewedMarket, defaultValue: DEFAULT_MARKETID, diff --git a/src/hooks/useTradeFormInputs.ts b/src/hooks/useTradeFormInputs.ts new file mode 100644 index 0000000..39ff5b5 --- /dev/null +++ b/src/hooks/useTradeFormInputs.ts @@ -0,0 +1,33 @@ +import { getTradeFormInputs } from '@/state/inputsSelectors'; +import { useEffect } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; + +import { TradeInputField } from '@/constants/abacus'; + +import abacusStateManager from '@/lib/abacus'; + +export const useTradeFormInputs = () => { + const tradeFormInputValues = useSelector(getTradeFormInputs, shallowEqual); + const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues; + + useEffect(() => { + const floatValue = parseFloat(triggerPriceInput); + abacusStateManager.setTradeValue({ + value: floatValue, + field: TradeInputField.triggerPrice, + }); + }, [triggerPriceInput]); + + useEffect(() => { + const floatValue = parseFloat(limitPriceInput); + abacusStateManager.setTradeValue({ value: floatValue, field: TradeInputField.limitPrice }); + }, [limitPriceInput]); + + useEffect(() => { + const floatValue = parseFloat(trailingPercentInput); + abacusStateManager.setTradeValue({ + value: floatValue, + field: TradeInputField.trailingPercent, + }); + }, [trailingPercentInput]); +}; diff --git a/src/lib/abacus/index.ts b/src/lib/abacus/index.ts index b3d74b6..c643662 100644 --- a/src/lib/abacus/index.ts +++ b/src/lib/abacus/index.ts @@ -27,11 +27,11 @@ import { import { DEFAULT_MARKETID } from '@/constants/markets'; import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork } from '@/constants/networks'; +import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade'; import type { RootStore } from '@/state/_store'; - -import { getInputTradeOptions } from '@/state/inputsSelectors'; -import { getTransferInputs } from '@/state/inputsSelectors'; +import { setTradeFormInputs } from '@/state/inputs'; +import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors'; import AbacusRest from './rest'; import AbacusAnalytics from './analytics'; @@ -43,6 +43,7 @@ import AbacusFormatter from './formatter'; import AbacusThreading from './threading'; import AbacusFileSystem from './filesystem'; import { LocaleSeparators } from '../numbers'; + class AbacusStateManager { private store: RootStore | undefined; private currentMarket: string | undefined; @@ -132,6 +133,8 @@ class AbacusStateManager { this.setTradeValue({ value: null, field: TradeInputField.limitPrice }); } + this.store?.dispatch(setTradeFormInputs(CLEARED_TRADE_INPUTS)); + if (shouldResetSize) { this.setTradeValue({ value: null, field: TradeInputField.size }); this.setTradeValue({ value: null, field: TradeInputField.usdcSize }); @@ -139,6 +142,8 @@ class AbacusStateManager { if (needsLeverage) { this.setTradeValue({ value: null, field: TradeInputField.leverage }); } + + this.store?.dispatch(setTradeFormInputs(CLEARED_SIZE_INPUTS)); } }; diff --git a/src/pages/trade/Trade.tsx b/src/pages/trade/Trade.tsx index 1f97b66..96ca268 100644 --- a/src/pages/trade/Trade.tsx +++ b/src/pages/trade/Trade.tsx @@ -7,7 +7,12 @@ import { layoutMixins } from '@/styles/layoutMixins'; import { TradeLayouts } from '@/constants/layout'; -import { useBreakpoints, useCurrentMarketId, usePageTitlePriceUpdates } from '@/hooks'; +import { + useBreakpoints, + useCurrentMarketId, + usePageTitlePriceUpdates, + useTradeFormInputs, +} from '@/hooks'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; import { getSelectedTradeLayout } from '@/state/layoutSelectors'; @@ -37,6 +42,7 @@ const TradePage = () => { const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true); usePageTitlePriceUpdates(); + useTradeFormInputs(); return isTablet ? ( diff --git a/src/state/inputs.ts b/src/state/inputs.ts index 2b21d50..e8f31ef 100644 --- a/src/state/inputs.ts +++ b/src/state/inputs.ts @@ -1,4 +1,5 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import assign from 'lodash/assign'; import type { InputError, @@ -9,9 +10,14 @@ import type { TransferInputs, } from '@/constants/abacus'; +import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade'; + +type TradeFormInputs = typeof CLEARED_TRADE_INPUTS & typeof CLEARED_SIZE_INPUTS; + export interface InputsState { current?: Nullable; inputErrors?: Nullable; + tradeFormInputs: TradeFormInputs; tradeInputs?: Nullable; closePositionInputs?: Nullable; transferInputs?: Nullable; @@ -20,6 +26,10 @@ export interface InputsState { const initialState: InputsState = { current: undefined, inputErrors: undefined, + tradeFormInputs: { + ...CLEARED_TRADE_INPUTS, + ...CLEARED_SIZE_INPUTS, + }, tradeInputs: undefined, transferInputs: undefined, }; @@ -40,7 +50,11 @@ export const inputsSlice = createSlice({ transferInputs: transfer, }; }, + + setTradeFormInputs: (state, action: PayloadAction>) => { + state.tradeFormInputs = assign({}, state.tradeFormInputs, action.payload); + }, }, }); -export const { setInputs } = inputsSlice.actions; +export const { setInputs, setTradeFormInputs } = inputsSlice.actions; diff --git a/src/state/inputsSelectors.ts b/src/state/inputsSelectors.ts index cb79990..e9d5e3c 100644 --- a/src/state/inputsSelectors.ts +++ b/src/state/inputsSelectors.ts @@ -120,3 +120,8 @@ export const useTradeFormData = () => { shallowEqual ); }; + +/** + * @returns Tradeform Input states for display. Abacus inputs should track these values. + */ +export const getTradeFormInputs = (state: RootState) => state.inputs.tradeFormInputs; diff --git a/src/views/TradeBoxOrderView.tsx b/src/views/TradeBoxOrderView.tsx index 762f489..be0deea 100644 --- a/src/views/TradeBoxOrderView.tsx +++ b/src/views/TradeBoxOrderView.tsx @@ -5,7 +5,7 @@ import { createSelector } from 'reselect'; import { TradeInputField } from '@/constants/abacus'; import { TradeTypes } from '@/constants/trade'; -import { STRING_KEYS } from '@/constants/localization'; +import { STRING_KEYS, StringKey } from '@/constants/localization'; import { useStringGetter } from '@/hooks'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -30,7 +30,8 @@ const useTradeTypeOptions = () => { const allTradeTypeItems = typeOptions?.toArray()?.map(({ type, stringKey }) => ({ value: type, label: stringGetter({ - key: type === TradeTypes.TAKE_PROFIT ? STRING_KEYS.TAKE_PROFIT_LIMIT : stringKey, + key: + type === TradeTypes.TAKE_PROFIT ? STRING_KEYS.TAKE_PROFIT_LIMIT : (stringKey as StringKey), }), })); diff --git a/src/views/forms/TradeForm.tsx b/src/views/forms/TradeForm.tsx index 56fd561..68a96b3 100644 --- a/src/views/forms/TradeForm.tsx +++ b/src/views/forms/TradeForm.tsx @@ -1,6 +1,6 @@ -import { type FormEvent, useState, Ref, useCallback } from 'react'; +import { type FormEvent, useState, Ref, useCallback, useEffect } from 'react'; import styled, { AnyStyledComponent, css } from 'styled-components'; -import { shallowEqual, useSelector } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import type { NumberFormatValues, SourceInfo } from 'react-number-format'; import { AlertType } from '@/constants/alerts'; @@ -37,7 +37,8 @@ import { WithTooltip } from '@/components/WithTooltip'; import { Orderbook } from '@/views/tables/Orderbook'; -import { getCurrentInput, useTradeFormData } from '@/state/inputsSelectors'; +import { setTradeFormInputs } from '@/state/inputs'; +import { getCurrentInput, getTradeFormInputs, useTradeFormData } from '@/state/inputsSelectors'; import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; import abacusStateManager from '@/lib/abacus'; @@ -80,6 +81,7 @@ export const TradeForm = ({ const [placeOrderError, setPlaceOrderError] = useState(); const [showOrderbook, setShowOrderbook] = useState(false); + const dispatch = useDispatch(); const stringGetter = useStringGetter(); const { placeOrder } = useSubaccount(); @@ -98,18 +100,21 @@ export const TradeForm = ({ tradeErrors, } = useTradeFormData(); - const { limitPrice, triggerPrice, trailingPercent } = price || {}; const currentInput = useSelector(getCurrentInput); const { tickSizeDecimals, stepSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {}; + const tradeFormInputValues = useSelector(getTradeFormInputs, shallowEqual); + const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues; + const needsAdvancedOptions = needsGoodUntil || timeInForceOptions || executionOptions || needsPostOnly || needsReduceOnly; const tradeFormInputs: TradeBoxInputConfig[] = []; const isInputFilled = - Object.values(price || {}).some((val) => val != null) || + Object.values(tradeFormInputValues).some((val) => val !== '') || + Object.values(price || {}).some((val) => !!val) || [size?.size, size?.usdcSize, size?.leverage].some((val) => val != null); const hasInputErrors = @@ -197,13 +202,10 @@ export const TradeForm = ({ USD ), - onChange: ({ floatValue }: NumberFormatValues) => { - abacusStateManager.setTradeValue({ - value: floatValue, - field: TradeInputField.triggerPrice, - }); + onChange: ({ value }: NumberFormatValues) => { + dispatch(setTradeFormInputs({ triggerPriceInput: value })); }, - value: triggerPrice ?? '', + value: triggerPriceInput ?? '', decimals: tickSizeDecimals || USD_DECIMALS, }); } @@ -220,10 +222,10 @@ export const TradeForm = ({ USD ), - onChange: ({ floatValue }: NumberFormatValues) => { - abacusStateManager.setTradeValue({ value: floatValue, field: TradeInputField.limitPrice }); + onChange: ({ value }: NumberFormatValues) => { + dispatch(setTradeFormInputs({ limitPriceInput: value })); }, - value: limitPrice ?? '', + value: limitPriceInput, decimals: tickSizeDecimals || USD_DECIMALS, }); } @@ -237,13 +239,10 @@ export const TradeForm = ({ {stringGetter({ key: STRING_KEYS.TRAILING_PERCENT })} ), - onChange: ({ floatValue }: NumberFormatValues) => { - abacusStateManager.setTradeValue({ - value: floatValue, - field: TradeInputField.trailingPercent, - }); + onChange: ({ value }: NumberFormatValues) => { + dispatch(setTradeFormInputs({ trailingPercentInput: value })); }, - value: trailingPercent ?? '', + value: trailingPercentInput ?? '', }); } diff --git a/src/views/forms/TradeForm/MarketLeverageInput.tsx b/src/views/forms/TradeForm/MarketLeverageInput.tsx index 4404331..aba36e9 100644 --- a/src/views/forms/TradeForm/MarketLeverageInput.tsx +++ b/src/views/forms/TradeForm/MarketLeverageInput.tsx @@ -30,8 +30,8 @@ import { getSelectedOrderSide, hasPositionSideChanged } from '@/lib/tradeData'; import { LeverageSlider } from './LeverageSlider'; type ElementProps = { - leverageInputValue: Nullable; - setLeverageInputValue: Dispatch>>; + leverageInputValue: string; + setLeverageInputValue: (value: string) => void; }; export const MarketLeverageInput = ({ @@ -66,7 +66,7 @@ export const MarketLeverageInput = ({ const newLeverage = MustBigNumber(floatValue).toFixed(); if (value === '' || newLeverage === 'NaN' || !floatValue) { - setLeverageInputValue(null); + setLeverageInputValue(''); abacusStateManager.setTradeValue({ value: null, field: TradeInputField.leverage, @@ -84,7 +84,7 @@ export const MarketLeverageInput = ({ ? newLeverageBN.abs().negated() : newLeverageBN.abs(); - setLeverageInputValue(newLeverageSignedBN.toNumber()); + setLeverageInputValue(newLeverageSignedBN.toString()); abacusStateManager.setTradeValue({ value: newLeverageSignedBN.toFixed(LEVERAGE_DECIMALS), @@ -98,11 +98,11 @@ export const MarketLeverageInput = ({ if (leveragePosition === PositionSide.None) return; const inputValue = leverageInputValue || currentLeverage; - const newInputValue = MustBigNumber(inputValue).negated().toNumber(); + const newInputValue = MustBigNumber(inputValue).negated().toFixed(LEVERAGE_DECIMALS); setLeverageInputValue(newInputValue); abacusStateManager.setTradeValue({ - value: newInputValue.toFixed(LEVERAGE_DECIMALS), + value: newInputValue, field: TradeInputField.leverage, }); }; diff --git a/src/views/forms/TradeForm/TradeSizeInputs.tsx b/src/views/forms/TradeForm/TradeSizeInputs.tsx index f2ae66a..51120df 100644 --- a/src/views/forms/TradeForm/TradeSizeInputs.tsx +++ b/src/views/forms/TradeForm/TradeSizeInputs.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import styled, { AnyStyledComponent } from 'styled-components'; import { TradeInputField } from '@/constants/abacus'; @@ -20,8 +20,13 @@ import { Icon, IconName } from '@/components/Icon'; import { ToggleButton } from '@/components/ToggleButton'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; +import { setTradeFormInputs } from '@/state/inputs'; -import { getInputTradeSizeData, getInputTradeOptions } from '@/state/inputsSelectors'; +import { + getInputTradeSizeData, + getInputTradeOptions, + getTradeFormInputs, +} from '@/state/inputsSelectors'; import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; @@ -31,12 +36,8 @@ import { MustBigNumber } from '@/lib/numbers'; import { MarketLeverageInput } from './MarketLeverageInput'; export const TradeSizeInputs = () => { - const [sizeInputValue, setSizeInputValue] = useState(); - const [usdcInputValue, setUsdcInputValue] = useState(); - const [leverageInputValue, setLeverageInputValue] = useState(); - const [showUSDCInputOnTablet, setShowUSDCInputOnTablet] = useState(false); - + const dispatch = useDispatch(); const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); @@ -50,24 +51,27 @@ export const TradeSizeInputs = () => { const { needsLeverage } = currentTradeInputOptions || {}; const decimals = stepSizeDecimals || TOKEN_DECIMALS; + const { amountInput, usdAmountInput, leverageInput } = useSelector( + getTradeFormInputs, + shallowEqual + ); + // Update State variables if their inputs are not being source of calculations // Or if they have been reset to null useEffect(() => { if (lastEditedInput !== TradeSizeInput.Size || size == null) { - // Abacus size already respects step size - // using .toFixed to prevent trailing zeros and exponential notation - setSizeInputValue(size); + dispatch(setTradeFormInputs({ amountInput: size ? size.toString() : '' })); } if (lastEditedInput !== TradeSizeInput.Usdc || usdcSize == null) { - setUsdcInputValue(usdcSize); + dispatch(setTradeFormInputs({ usdAmountInput: usdcSize ? usdcSize.toString() : '' })); } if (lastEditedInput !== TradeSizeInput.Leverage || leverage == null) { - setLeverageInputValue(leverage); + dispatch(setTradeFormInputs({ leverageInput: leverage ? leverage.toString() : '' })); } }, [size, usdcSize, leverage, lastEditedInput]); const onSizeInput = ({ value, floatValue }: { value: string; floatValue?: number }) => { - setSizeInputValue(floatValue); + dispatch(setTradeFormInputs({ amountInput: value })); const newAmount = MustBigNumber(floatValue).toFixed(decimals); abacusStateManager.setTradeValue({ @@ -77,7 +81,7 @@ export const TradeSizeInputs = () => { }; const onUSDCInput = ({ value, floatValue }: { value: string; floatValue?: number }) => { - setUsdcInputValue(floatValue); + dispatch(setTradeFormInputs({ usdAmountInput: value })); const newUsdcAmount = MustBigNumber(floatValue).toFixed(); abacusStateManager.setTradeValue({ @@ -113,7 +117,7 @@ export const TradeSizeInputs = () => { } slotRight={isTablet && inputToggleButton} type={InputType.Number} - value={sizeInputValue || ''} + value={amountInput || ''} /> ); @@ -122,7 +126,7 @@ export const TradeSizeInputs = () => { id="trade-usdc" onInput={onUSDCInput} type={InputType.Currency} - value={usdcInputValue || ''} + value={usdAmountInput || ''} decimals={tickSizeDecimals || USD_DECIMALS} label={ <> @@ -153,8 +157,10 @@ export const TradeSizeInputs = () => { {needsLeverage && ( + dispatch(setTradeFormInputs({ leverageInput: value })) + } /> )} diff --git a/src/views/tables/Orderbook.tsx b/src/views/tables/Orderbook.tsx index 0741c07..816aa56 100644 --- a/src/views/tables/Orderbook.tsx +++ b/src/views/tables/Orderbook.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components'; import { OrderSide } from '@dydxprotocol/v4-client-js'; @@ -9,13 +9,13 @@ import { STRING_KEYS } from '@/constants/localization'; import { useBreakpoints, useStringGetter } from '@/hooks'; import { calculateCanViewAccount } from '@/state/accountCalculators'; +import { setTradeFormInputs } from '@/state/inputs'; import { getSubaccountOpenOrdersBySideAndPrice } from '@/state/accountSelectors'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { getCurrentMarketConfig, getCurrentMarketOrderbook } from '@/state/perpetualsSelectors'; import { getCurrentInput } from '@/state/inputsSelectors'; -import abacusStateManager from '@/lib/abacus'; import { MustBigNumber } from '@/lib/numbers'; import { Details } from '@/components/Details'; @@ -208,7 +208,6 @@ const OrderbookTable = ({ // Style props histogramSide={histogramSide} hideHeader={hideHeader} - withFocusStickyRows style={{ '--histogram-range': histogramRange, }} @@ -223,11 +222,12 @@ export const Orderbook = ({ maxRowsPerSide = ORDERBOOK_MAX_ROWS_PER_SIDE, hideHeader = false, }: ElementProps & StyleProps) => { + const dispatch = useDispatch(); const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); const currentInput = useSelector(getCurrentInput); - const { symbol = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; + const { id = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; const { stepSizeDecimals, tickSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) ?? {}; @@ -274,18 +274,16 @@ export const Orderbook = ({ const onRowAction = useCallback( (key: string, row: RowData) => { - if (currentInput === 'trade' && key !== 'spread' && row?.price) - abacusStateManager.setTradeValue({ - value: row?.price, - field: TradeInputField.limitPrice, - }); + if (currentInput === 'trade' && key !== 'spread' && row?.price) { + dispatch(setTradeFormInputs({ limitPriceInput: row?.price?.toString() })); + } }, [currentInput] ); const orderbookTableProps = { showMineColumn, - symbol, + symbol: id, stepSizeDecimals, tickSizeDecimals, histogramRange, @@ -510,17 +508,21 @@ Styled.OrderbookTable = styled(OrderbookTradesTable)` content: ''; } - &:not(:active):is(:focus-visible, :focus-within) { - ${orderbookMixins.scrollSnapItem} - z-index: 2; + ${({ withFocusStickyRows }) => + withFocusStickyRows && + css` + &:not(:active):is(:focus-visible, :focus-within) { + ${orderbookMixins.scrollSnapItem} + z-index: 2; - &[data-side='bid'] { - top: calc(var(--stickyArea-totalInsetTop) + var(--orderbook-spreadRowHeight)); - } - &[data-side='ask'] { - bottom: calc(var(--stickyArea-totalInsetBottom) + var(--orderbook-spreadRowHeight)); - } - } + &[data-side='bid'] { + top: calc(var(--stickyArea-totalInsetTop) + var(--orderbook-spreadRowHeight)); + } + &[data-side='ask'] { + bottom: calc(var(--stickyArea-totalInsetBottom) + var(--orderbook-spreadRowHeight)); + } + } + `} } ${Styled.HorizontalLayout} & {