diff --git a/src/App.tsx b/src/App.tsx index e1ccfe3..1869616 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,10 +10,10 @@ import { AppRoute, DEFAULT_TRADE_ROUTE } from '@/constants/routes'; import { useBreakpoints, useInitializePage, useShouldShowFooter, useAnalytics } from '@/hooks'; import { DydxProvider } from '@/hooks/useDydxClient'; import { AccountsProvider } from '@/hooks/useAccounts'; -import { SubaccountProvider } from './hooks/useSubaccount'; import { DialogAreaProvider, useDialogArea } from './hooks/useDialogArea'; +import { LocaleProvider } from './hooks/useLocaleSeparators'; import { NotificationsProvider } from './hooks/useNotifications'; - +import { SubaccountProvider } from './hooks/useSubaccount'; import { GuardedMobileRoute } from '@/components/GuardedMobileRoute'; import MarketsPage from '@/pages/markets/Markets'; @@ -61,10 +61,7 @@ const Content = () => { } /> - } - /> + } /> } /> {import.meta.env.MODE !== 'production' && ( @@ -103,17 +100,19 @@ const App = () => ( - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/components/AssetIcon.tsx b/src/components/AssetIcon.tsx index 085376b..d9c1995 100644 --- a/src/components/AssetIcon.tsx +++ b/src/components/AssetIcon.tsx @@ -1,68 +1,63 @@ import styled, { type AnyStyledComponent } from 'styled-components'; +import { Nullable } from '@/constants/abacus'; + export type AssetSymbol = keyof typeof assetIcons; const assetIcons = { '1INCH': '/currencies/1inch.svg', - 'AAVE': '/currencies/aave.svg', - 'ADA': '/currencies/ada.svg', - 'ALGO': '/currencies/algo.svg', - 'ATOM': '/currencies/atom.svg', - 'AVAX': '/currencies/avax.svg', - 'BCH': '/currencies/bch.svg', - 'BTC': '/currencies/btc.svg', - 'CELO': '/currencies/celo.svg', - 'COMP': '/currencies/comp.svg', - 'CRV': '/currencies/crv.svg', - 'DAI': '/currencies/dai.svg', - 'DOGE': '/currencies/doge.svg', - 'DOT': '/currencies/dot.svg', - 'DYDX': '/currencies/dydx.svg', - 'ENJ': '/currencies/enj.svg', - 'EOS': '/currencies/eos.svg', - 'ETC': '/currencies/etc.svg', - 'ETH': '/currencies/eth.svg', - 'FIL': '/currencies/fil.svg', - 'ICP': '/currencies/icp.svg', - 'LINK': '/currencies/link.svg', - 'LTC': '/currencies/ltc.svg', - 'LUNA': '/currencies/luna.svg', - 'MATIC': '/currencies/matic.svg', - 'MKR': '/currencies/mkr.svg', - 'NEAR': '/currencies/near.svg', - 'RUNE': '/currencies/rune.svg', - 'SNX': '/currencies/snx.svg', - 'SOL': '/currencies/sol.svg', - 'SUSHI': '/currencies/sushi.svg', - 'TRX': '/currencies/trx.svg', - 'UMA': '/currencies/uma.svg', - 'UNI': '/currencies/uni.svg', - 'USDC': '/currencies/usdc.svg', - 'USDT': '/currencies/usdt.svg', - 'WBTC': '/currencies/wbtc.svg', - 'WETH': '/currencies/weth.svg', - 'XLM': '/currencies/xlm.svg', - 'XMR': '/currencies/xmr.svg', - 'XTZ': '/currencies/xtz.svg', - 'YFI': '/currencies/yfi.svg', - 'ZEC': '/currencies/zec.svg', - 'ZRX': '/currencies/zrx.svg', + AAVE: '/currencies/aave.svg', + ADA: '/currencies/ada.svg', + ALGO: '/currencies/algo.svg', + ATOM: '/currencies/atom.svg', + AVAX: '/currencies/avax.svg', + BCH: '/currencies/bch.svg', + BTC: '/currencies/btc.svg', + CELO: '/currencies/celo.svg', + COMP: '/currencies/comp.svg', + CRV: '/currencies/crv.svg', + DAI: '/currencies/dai.svg', + DOGE: '/currencies/doge.svg', + DOT: '/currencies/dot.svg', + DYDX: '/currencies/dydx.svg', + ENJ: '/currencies/enj.svg', + EOS: '/currencies/eos.svg', + ETC: '/currencies/etc.svg', + ETH: '/currencies/eth.svg', + FIL: '/currencies/fil.svg', + ICP: '/currencies/icp.svg', + LINK: '/currencies/link.svg', + LTC: '/currencies/ltc.svg', + LUNA: '/currencies/luna.svg', + MATIC: '/currencies/matic.svg', + MKR: '/currencies/mkr.svg', + NEAR: '/currencies/near.svg', + RUNE: '/currencies/rune.svg', + SNX: '/currencies/snx.svg', + SOL: '/currencies/sol.svg', + SUSHI: '/currencies/sushi.svg', + TRX: '/currencies/trx.svg', + UMA: '/currencies/uma.svg', + UNI: '/currencies/uni.svg', + USDC: '/currencies/usdc.svg', + USDT: '/currencies/usdt.svg', + WBTC: '/currencies/wbtc.svg', + WETH: '/currencies/weth.svg', + XLM: '/currencies/xlm.svg', + XMR: '/currencies/xmr.svg', + XTZ: '/currencies/xtz.svg', + YFI: '/currencies/yfi.svg', + ZEC: '/currencies/zec.svg', + ZRX: '/currencies/zrx.svg', } as const; export const AssetIcon = ({ symbol, - className + className, }: { - symbol?: AssetSymbol, - className?: string -}) => ( - symbol ? ( - - ) : null -); + symbol?: Nullable; + className?: string; +}) => (symbol ? : null); const Styled: Record = {}; diff --git a/src/components/SelectMenu.tsx b/src/components/SelectMenu.tsx index a53d600..387be0d 100644 --- a/src/components/SelectMenu.tsx +++ b/src/components/SelectMenu.tsx @@ -91,6 +91,7 @@ Styled.Trigger = styled(Trigger)<{ $withBlur?: boolean }>` Styled.DropdownIcon = styled(SelectIcon)` font-size: 0.675em; color: var(--color-text-1); + margin-left: auto; `; Styled.Content = styled(Content)` diff --git a/src/constants/abacus.ts b/src/constants/abacus.ts index 551274c..a769f95 100644 --- a/src/constants/abacus.ts +++ b/src/constants/abacus.ts @@ -1,6 +1,6 @@ import Abacus, { kollections } from '@dydxprotocol/abacus'; import { OrderSide } from '@dydxprotocol/v4-client'; -import { PositionSide } from './trade'; +import { PositionSide, TradeTypes } from './trade'; import { STRING_KEYS } from './localization'; export type Nullable = T | null | undefined; @@ -99,6 +99,7 @@ export type TradeInputSummary = Abacus.exchange.dydx.abacus.output.input.TradeIn export type TransferInputs = Abacus.exchange.dydx.abacus.output.input.TransferInput; export type InputError = Abacus.exchange.dydx.abacus.output.input.ValidationError; export const ErrorType = Abacus.exchange.dydx.abacus.output.input.ErrorType; +export const InputSelectionOption = Abacus.exchange.dydx.abacus.output.input.SelectionOption; // ------ Wallet ------ // export type Wallet = Abacus.exchange.dydx.abacus.output.Wallet; @@ -217,3 +218,19 @@ export const ORDER_STATUS_STRINGS: Record, + Nullable +> = { + [AbacusOrderType.limit.name]: TradeTypes.LIMIT, + [AbacusOrderType.market.name]: TradeTypes.MARKET, + [AbacusOrderType.stopLimit.name]: TradeTypes.STOP_LIMIT, + [AbacusOrderType.stopMarket.name]: TradeTypes.STOP_MARKET, + [AbacusOrderType.takeProfitLimit.name]: TradeTypes.TAKE_PROFIT, + [AbacusOrderType.takeProfitMarket.name]: TradeTypes.TAKE_PROFIT_MARKET, + + [AbacusOrderType.liquidated.name]: null, + [AbacusOrderType.liquidation.name]: null, + [AbacusOrderType.trailingStop.name]: null, +}; diff --git a/src/constants/analytics.ts b/src/constants/analytics.ts index f68ee0b..bf117f3 100644 --- a/src/constants/analytics.ts +++ b/src/constants/analytics.ts @@ -4,7 +4,7 @@ import type { OnboardingState, OnboardingSteps } from './account'; import type { DydxAddress, WalletType, WalletConnectionType, EthereumAddress } from './wallets'; import type { DialogTypes } from './dialogs'; import type { TradeTypes } from './trade'; -import type { AbacusApiStatus, SubaccountPlaceOrderPayload } from './abacus'; +import type { AbacusApiStatus, HumanReadablePlaceOrderPayload } from './abacus'; // User properties export enum AnalyticsUserProperty { @@ -25,37 +25,28 @@ export enum AnalyticsUserProperty { SubaccountNumber = 'subaccountNumber', } -export type AnalyticsUserPropertyValue = +export type AnalyticsUserPropertyValue = // Environment - T extends AnalyticsUserProperty.Breakpoint ? - | 'MOBILE' - | 'TABLET' - | 'DESKTOP_SMALL' - | 'DESKTOP_MEDIUM' - | 'DESKTOP_LARGE' - | 'UNSUPPORTED' - : T extends AnalyticsUserProperty.Locale ? - SupportedLocales - - // Network - : T extends AnalyticsUserProperty.Network ? - DydxNetwork - - // Wallet - : T extends AnalyticsUserProperty.WalletType ? - WalletType | undefined - : T extends AnalyticsUserProperty.WalletConnectionType ? - WalletConnectionType | undefined - : T extends AnalyticsUserProperty.WalletAddress ? - EthereumAddress | DydxAddress | undefined - - // Account - : T extends AnalyticsUserProperty.DydxAddress ? - DydxAddress | undefined - : T extends AnalyticsUserProperty.SubaccountNumber ? - number | undefined - - : undefined; + T extends AnalyticsUserProperty.Breakpoint + ? 'MOBILE' | 'TABLET' | 'DESKTOP_SMALL' | 'DESKTOP_MEDIUM' | 'DESKTOP_LARGE' | 'UNSUPPORTED' + : T extends AnalyticsUserProperty.Locale + ? SupportedLocales + : // Network + T extends AnalyticsUserProperty.Network + ? DydxNetwork + : // Wallet + T extends AnalyticsUserProperty.WalletType + ? WalletType | undefined + : T extends AnalyticsUserProperty.WalletConnectionType + ? WalletConnectionType | undefined + : T extends AnalyticsUserProperty.WalletAddress + ? EthereumAddress | DydxAddress | undefined + : // Account + T extends AnalyticsUserProperty.DydxAddress + ? DydxAddress | undefined + : T extends AnalyticsUserProperty.SubaccountNumber + ? number | undefined + : undefined; // Events export enum AnalyticsEvent { @@ -91,87 +82,81 @@ export enum AnalyticsEvent { export type AnalyticsEventData = // App - T extends AnalyticsEvent.AppStart ? - {} - : T extends AnalyticsEvent.NetworkStatus ? - { - status: typeof AbacusApiStatus['name']; - /** Last time indexer node was queried successfully */ - lastSuccessfulIndexerRpcQuery?: number; - /** Time elapsed since indexer node was queried successfully */ - elapsedTime?: number; - blockHeight?: number; - indexerBlockHeight?: number; - } - - // Navigation - : T extends AnalyticsEvent.NavigatePage ? - { - path: string; - } - : T extends AnalyticsEvent.NavigateDialog ? - { - type: DialogTypes; - } - : T extends AnalyticsEvent.NavigateDialogClose ? - { - type: DialogTypes; - } - : T extends AnalyticsEvent.NavigateExternal ? - { - link: string; - } - - // Wallet - : T extends AnalyticsEvent.ConnectWallet ? - { - walletType: WalletType; - walletConnectionType: WalletConnectionType; - } - : T extends AnalyticsEvent.DisconnectWallet ? - {} - - // Onboarding - : T extends AnalyticsEvent.OnboardingStepChanged ? - { - state: OnboardingState; - step?: OnboardingSteps; - } - : T extends AnalyticsEvent.OnboardingAccountDerived ? - { - hasPreviousTransactions: boolean; - } - - // Transfers - : T extends AnalyticsEvent.TransferFaucet ? - {} - : T extends AnalyticsEvent.TransferDeposit ? - {} - : T extends AnalyticsEvent.TransferWithdraw ? - {} - - // Trading - : T extends AnalyticsEvent.TradeOrderTypeSelected ? - { - type: TradeTypes; - } - : T extends AnalyticsEvent.TradePlaceOrder ? - SubaccountPlaceOrderPayload & { - isClosePosition: boolean; - } - : T extends AnalyticsEvent.TradePlaceOrderConfirmed ? - { - /** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */ - roundtripMs: number; - /** URL/IP of node the order was sent to */ - validator: string; - } - : T extends AnalyticsEvent.TradeCancelOrderConfirmed ? - { - /** roundtrip time between user canceling an order and confirmation from indexer (client → validator → indexer → client) */ - roundtripMs: number; - /** URL/IP of node the order was sent to */ - validator: string; - } - : - never; + T extends AnalyticsEvent.AppStart + ? {} + : T extends AnalyticsEvent.NetworkStatus + ? { + status: (typeof AbacusApiStatus)['name']; + /** Last time indexer node was queried successfully */ + lastSuccessfulIndexerRpcQuery?: number; + /** Time elapsed since indexer node was queried successfully */ + elapsedTime?: number; + blockHeight?: number; + indexerBlockHeight?: number; + } + : // Navigation + T extends AnalyticsEvent.NavigatePage + ? { + path: string; + } + : T extends AnalyticsEvent.NavigateDialog + ? { + type: DialogTypes; + } + : T extends AnalyticsEvent.NavigateDialogClose + ? { + type: DialogTypes; + } + : T extends AnalyticsEvent.NavigateExternal + ? { + link: string; + } + : // Wallet + T extends AnalyticsEvent.ConnectWallet + ? { + walletType: WalletType; + walletConnectionType: WalletConnectionType; + } + : T extends AnalyticsEvent.DisconnectWallet + ? {} + : // Onboarding + T extends AnalyticsEvent.OnboardingStepChanged + ? { + state: OnboardingState; + step?: OnboardingSteps; + } + : T extends AnalyticsEvent.OnboardingAccountDerived + ? { + hasPreviousTransactions: boolean; + } + : // Transfers + T extends AnalyticsEvent.TransferFaucet + ? {} + : T extends AnalyticsEvent.TransferDeposit + ? {} + : T extends AnalyticsEvent.TransferWithdraw + ? {} + : // Trading + T extends AnalyticsEvent.TradeOrderTypeSelected + ? { + type: TradeTypes; + } + : T extends AnalyticsEvent.TradePlaceOrder + ? HumanReadablePlaceOrderPayload & { + isClosePosition: boolean; + } + : T extends AnalyticsEvent.TradePlaceOrderConfirmed + ? { + /** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */ + roundtripMs: number; + /** URL/IP of node the order was sent to */ + validator: string; + } + : T extends AnalyticsEvent.TradeCancelOrderConfirmed + ? { + /** roundtrip time between user canceling an order and confirmation from indexer (client → validator → indexer → client) */ + roundtripMs: number; + /** URL/IP of node the order was sent to */ + validator: string; + } + : never; diff --git a/src/constants/routes.ts b/src/constants/routes.ts index a7f415d..1aaa95b 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -1,3 +1,5 @@ +import { DEFAULT_MARKETID } from './markets'; + export enum AppRoute { Markets = '/markets', Portfolio = '/portfolio', @@ -31,7 +33,7 @@ export enum MobileSettingsRoute { export const TRADE_ROUTE = `${AppRoute.Trade}/:market`; export const PORTFOLIO_ROUTE = `${AppRoute.Portfolio}/:subroute`; export const HISTORY_ROUTE = `${AppRoute.Portfolio}/${PortfolioRoute.History}/:subroute`; -export const DEFAULT_TRADE_ROUTE = `${AppRoute.Trade}/ETH-USD`; +export const DEFAULT_TRADE_ROUTE = `${AppRoute.Trade}/${DEFAULT_MARKETID}`; export const SETTINGS_ROUTE = `${AppRoute.Settings}/*`; export const DEFAULT_DOCUMENT_TITLE = 'dYdX'; diff --git a/src/constants/trade.ts b/src/constants/trade.ts index bff4237..7444c49 100644 --- a/src/constants/trade.ts +++ b/src/constants/trade.ts @@ -10,7 +10,7 @@ export enum TradeTypes { LIMIT = 'LIMIT', STOP_LIMIT = 'STOP_LIMIT', STOP_MARKET = 'STOP_MARKET', - TAKE_PROFIT_LIMIT = 'TAKE_PROFIT_LIMIT', + TAKE_PROFIT = 'TAKE_PROFIT', TAKE_PROFIT_MARKET = 'TAKE_PROFIT_MARKET', TRAILING_STOP = 'TRAILING_STOP', } @@ -30,9 +30,9 @@ export enum PositionSide { export const UNCOMMITTED_ORDER_TIMEOUT = 10_000; export const ORDER_SIDE_STRINGS = { - [OrderSide.SIDE_BUY]: STRING_KEYS.BUY, - [OrderSide.SIDE_SELL]: STRING_KEYS.SELL, -} + [OrderSide.BUY]: STRING_KEYS.BUY, + [OrderSide.SELL]: STRING_KEYS.SELL, +}; export const POSITION_SIDE_STRINGS: Record = { [PositionSide.None]: STRING_KEYS.NONE, @@ -68,7 +68,7 @@ export const TRADE_TYPE_STRINGS: Record< tradeTypeKey: STRING_KEYS.STOP_MARKET, descriptionKey: STRING_KEYS.STOP_MARKET_DESCRIPTION, }, - [TradeTypes.TAKE_PROFIT_LIMIT]: { + [TradeTypes.TAKE_PROFIT]: { tradeTypeKeyShort: STRING_KEYS.TAKE_PROFIT_LIMIT, tradeTypeKey: STRING_KEYS.TAKE_PROFIT_LIMIT, descriptionKey: STRING_KEYS.TAKE_PROFIT_LIMIT_DESCRIPTION, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 12cfedd..bc0c3cd 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,6 +1,7 @@ import { useApiState } from './useApiState'; import { useBreakpoints } from './useBreakpoints'; import { useCommandMenu } from './useCommandMenu'; +import { useCurrentMarketId } from './useCurrentMarketId'; import { useInterval } from './useInterval'; import { useDocumentTitle } from './useDocumentTitle'; import { useDydxClient } from './useDydxClient'; @@ -23,6 +24,7 @@ export { useApiState, useBreakpoints, useCommandMenu, + useCurrentMarketId, useDocumentTitle, useDydxClient, useAccountBalance, diff --git a/src/hooks/useCurrentMarketId.ts b/src/hooks/useCurrentMarketId.ts new file mode 100644 index 0000000..528364b --- /dev/null +++ b/src/hooks/useCurrentMarketId.ts @@ -0,0 +1,49 @@ +import { useEffect, useMemo } from 'react'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { useMatch, useNavigate } from 'react-router-dom'; + +import { LocalStorageKey } from '@/constants/localStorage'; +import { DEFAULT_MARKETID } from '@/constants/markets'; +import { AppRoute, DEFAULT_TRADE_ROUTE } from '@/constants/routes'; + +import { getSelectedNetwork } from '@/state/appSelectors'; +import { closeDialogInTradeBox } from '@/state/dialogs'; +import { setCurrentMarketId } from '@/state/perpetuals'; + +import abacusStateManager from '@/lib/abacus'; + +import { useLocalStorage } from './useLocalStorage'; +import { getMarketIds } from '@/state/perpetualsSelectors'; + +export const useCurrentMarketId = () => { + const navigate = useNavigate(); + const match = useMatch(`/${AppRoute.Trade}/:marketId`); + const { marketId } = match?.params ?? {}; + const dispatch = useDispatch(); + const selectedNetwork = useSelector(getSelectedNetwork); + const marketIds = useSelector(getMarketIds, shallowEqual); + const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({ + key: LocalStorageKey.LastViewedMarket, + defaultValue: DEFAULT_MARKETID, + }); + + const validId = useMemo(() => { + if (marketIds.length === 0) return marketId ?? lastViewedMarket; + if (!marketIds.includes(marketId)) return DEFAULT_MARKETID; + return marketId ?? lastViewedMarket; + }, [marketIds, marketId]); + + useEffect(() => { + setLastViewedMarket(validId); + dispatch(setCurrentMarketId(validId)); + dispatch(closeDialogInTradeBox()); + + navigate(`${AppRoute.Trade}/${validId}`, { + replace: true, + }); + }, [validId]); + + useEffect(() => { + abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID); + }, [selectedNetwork, marketId]); +}; diff --git a/src/hooks/useLocaleSeparators.ts b/src/hooks/useLocaleSeparators.ts deleted file mode 100644 index 048e393..0000000 --- a/src/hooks/useLocaleSeparators.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { getSeparator } from '@/lib/numbers'; - -export const useLocaleSeparators = () => { - const [locale, setLocale] = useState(navigator.language || 'en-US'); - - useEffect(() => { - const handler = () => setLocale(navigator.language || 'en-US'); - globalThis.addEventListener('languagechange', handler); - return () => globalThis.removeEventListener('languagechange', handler); - }, []); - - return useMemo(() => { - return { - group: getSeparator({ locale, separatorType: 'group' }), - decimal: getSeparator({ locale, separatorType: 'decimal' }), - }; - }, [locale]); -}; diff --git a/src/hooks/useLocaleSeparators.tsx b/src/hooks/useLocaleSeparators.tsx new file mode 100644 index 0000000..870f747 --- /dev/null +++ b/src/hooks/useLocaleSeparators.tsx @@ -0,0 +1,55 @@ +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { SUPPORTED_BASE_TAGS_LOCALE_MAPPING } from '@/constants/localization'; + +import { getSelectedLocale } from '@/state/localizationSelectors'; + +import abacusStateManager from '@/lib/abacus'; +import { getSeparator } from '@/lib/numbers'; + +type LocaleContextType = ReturnType; +const LocaleContext = createContext({} as LocaleContextType); +LocaleContext.displayName = 'Locale'; + +export const LocaleProvider = ({ ...props }) => ( + +); + +const useLocaleContext = () => { + const selectedLocale = useSelector(getSelectedLocale); + const [browserLanguage, setBrowserLanguage] = useState(navigator.language || 'en-US'); + + useEffect(() => { + const handler = () => setBrowserLanguage(navigator.language || 'en-US'); + globalThis.addEventListener('languagechange', handler); + return () => globalThis.removeEventListener('languagechange', handler); + }, []); + + useEffect(() => { + if (selectedLocale) { + const updatedBrowserLanguage = Object.entries(SUPPORTED_BASE_TAGS_LOCALE_MAPPING).find( + ([, value]) => value === selectedLocale + ); + + if (updatedBrowserLanguage) { + setBrowserLanguage(selectedLocale); + } + } + }, [selectedLocale]); + + const separators = useMemo(() => { + return { + group: getSeparator({ browserLanguage, separatorType: 'group' }), + decimal: getSeparator({ browserLanguage, separatorType: 'decimal' }), + }; + }, [browserLanguage]); + + useEffect(() => { + abacusStateManager.setLocaleSeparators(separators); + }, [separators]); + + return separators; +}; + +export const useLocaleSeparators = () => useContext(LocaleContext); diff --git a/src/hooks/useMarketsData.ts b/src/hooks/useMarketsData.ts index 6507e95..af6b545 100644 --- a/src/hooks/useMarketsData.ts +++ b/src/hooks/useMarketsData.ts @@ -29,7 +29,7 @@ export const useMarketsData = ( const markets = useMemo(() => { return Object.values(allPerpetualMarkets) - .filter(Boolean) + .filter(({ assetId }) => allAssets[assetId]) // Filter out markets with no asset information .map((marketData) => ({ asset: allAssets[marketData.assetId], tickSizeDecimals: marketData.configs?.tickSizeDecimals, @@ -56,7 +56,7 @@ export const useMarketsData = ( () => [ MarketFilters.ALL, ...Object.keys(MARKET_FILTER_LABELS).filter((marketFilter) => - markets.some((market) => market.asset.tags?.toArray().some((tag) => tag === marketFilter)) + markets.some((market) => market.asset?.tags?.toArray().some((tag) => tag === marketFilter)) ), ], [markets] diff --git a/src/hooks/usePageTitlePriceUpdates.ts b/src/hooks/usePageTitlePriceUpdates.ts index c8c3f3e..3093974 100644 --- a/src/hooks/usePageTitlePriceUpdates.ts +++ b/src/hooks/usePageTitlePriceUpdates.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { shallowEqual, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { DEFAULT_DOCUMENT_TITLE } from '@/constants/routes'; diff --git a/src/hooks/useSubaccount.tsx b/src/hooks/useSubaccount.tsx index 570878b..2ce92c5 100644 --- a/src/hooks/useSubaccount.tsx +++ b/src/hooks/useSubaccount.tsx @@ -215,6 +215,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo type, side, price, + triggerPrice, size, clientId, timeInForce, @@ -228,6 +229,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo type: OrderType; side: OrderSide; price: number; + triggerPrice: Nullable; size: number; clientId: number; timeInForce: OrderTimeInForce; @@ -250,7 +252,8 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo goodTilTimeInSeconds, execution, postOnly, - reduceOnly + reduceOnly, + triggerPrice ?? undefined ); const endTimestamp = performance.now(); @@ -287,7 +290,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo clobPairId, goodTilBlock, goodTilBlockTime - ) + ); const endTimestamp = performance.now(); @@ -435,6 +438,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo type, side, price, + triggerPrice, size, clientId, timeInForce, @@ -461,6 +465,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo type: type as OrderType, side: side as OrderSide, price, + triggerPrice, size, clientId, timeInForce: timeInForce as OrderTimeInForce, @@ -490,7 +495,10 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo ); } - track(AnalyticsEvent.TradePlaceOrder, { ...orderParams, isClosePosition }); + track(AnalyticsEvent.TradePlaceOrder, { + ...orderParams, + isClosePosition, + } as HumanReadablePlaceOrderPayload & { isClosePosition: boolean }); onSuccess?.(); } catch (error) { const errorStringKey = error?.code && ORDER_ERROR_CODE_MAP[error.code as number]; diff --git a/src/layout/Footer/FooterMobile.tsx b/src/layout/Footer/FooterMobile.tsx index 6b7b9ef..5cac678 100644 --- a/src/layout/Footer/FooterMobile.tsx +++ b/src/layout/Footer/FooterMobile.tsx @@ -1,6 +1,7 @@ import styled, { type AnyStyledComponent } from 'styled-components'; import { useDispatch, useSelector } from 'react-redux'; +import { ButtonAction, ButtonSize } from '@/constants/buttons'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { DEFAULT_MARKETID } from '@/constants/markets'; @@ -15,9 +16,8 @@ import { BellIcon, MarketsIcon, PlayIcon, PortfolioIcon, ProfileIcon, TradeIcon import { IconButton } from '@/components/IconButton'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; -import { getCurrentMarketId } from '@/state/perpetualsSelectors'; import { openDialog } from '@/state/dialogs'; -import { ButtonAction, ButtonSize } from '@/constants/buttons'; +import { getCurrentMarketId } from '@/state/perpetualsSelectors'; export const FooterMobile = () => { const dispatch = useDispatch(); diff --git a/src/lib/abacus/formatter.ts b/src/lib/abacus/formatter.ts index 08f70cb..bebfe0d 100644 --- a/src/lib/abacus/formatter.ts +++ b/src/lib/abacus/formatter.ts @@ -1,12 +1,25 @@ import type { AbacusFormatterProtocol } from '@/constants/abacus'; +import { type LocaleSeparators, MustBigNumber, getFractionDigits } from '../numbers'; + class AbacusFormatter implements AbacusFormatterProtocol { + localeSeparators: LocaleSeparators; + + constructor() { + this.localeSeparators = { group: ',', decimal: '.' }; + } + + setLocaleSeparators({ group, decimal }: LocaleSeparators) { + this.localeSeparators = { group, decimal }; + } + percent(value: number, digits: number): string { - return value.toString(); + return MustBigNumber(value).toFixed(digits); } dollar(value: number, tickSize: string): string { - return value.toString(); + const tickSizeDecimals = getFractionDigits(tickSize); + return MustBigNumber(value).toFixed(tickSizeDecimals); } } diff --git a/src/lib/abacus/index.ts b/src/lib/abacus/index.ts index 80212b6..1d43a86 100644 --- a/src/lib/abacus/index.ts +++ b/src/lib/abacus/index.ts @@ -34,6 +34,7 @@ import AbacusStateNotifier from './stateNotification'; import AbacusLocalizer from './localizer'; import AbacusFormatter from './formatter'; import AbacusThreading from './threading'; +import { LocaleSeparators } from '../numbers'; class AbacusStateManager { private store: RootStore | undefined; @@ -42,12 +43,14 @@ class AbacusStateManager { stateManager: InstanceType; websocket: AbacusWebsocket; stateNotifier: AbacusStateNotifier; + abacusFormatter: AbacusFormatter; constructor() { this.store = undefined; this.currentMarket = undefined; this.stateNotifier = new AbacusStateNotifier(); this.websocket = new AbacusWebsocket(); + this.abacusFormatter = new AbacusFormatter(); const ioImplementations = new IOImplementations( // @ts-ignore @@ -63,7 +66,7 @@ class AbacusStateManager { const uiImplementations = new UIImplementations( // @ts-ignore new AbacusLocalizer(), - new AbacusFormatter() + this.abacusFormatter ); this.stateManager = new AsyncAbacusStateManager( @@ -179,6 +182,10 @@ class AbacusStateManager { this.stateManager.closePosition(value, field); }; + setLocaleSeparators = ({ group, decimal }: LocaleSeparators) => { + this.abacusFormatter.setLocaleSeparators({ group, decimal }); + }; + // ------ Utils ------ // placeOrderPayload = (): Nullable => this.stateManager.placeOrderPayload(); @@ -203,7 +210,7 @@ class AbacusStateManager { }; sendSocketRequest = (requestText: string) => { - this.websocket.sendSocketRequest(requestText); + this.websocket.send(requestText); }; } diff --git a/src/lib/abacus/websocket.ts b/src/lib/abacus/websocket.ts index 450151a..d206418 100644 --- a/src/lib/abacus/websocket.ts +++ b/src/lib/abacus/websocket.ts @@ -46,10 +46,6 @@ class AbacusWebsocket implements Omit { - this.socket?.send(requestText); - }; - handleCandlesSubscription = ({ channelId, subscribe, diff --git a/src/lib/numbers.ts b/src/lib/numbers.ts index 6df2b65..8676d88 100644 --- a/src/lib/numbers.ts +++ b/src/lib/numbers.ts @@ -1,6 +1,7 @@ import { BigNumber } from 'bignumber.js'; export type BigNumberish = BigNumber | string | number; +export type LocaleSeparators = { group?: string; decimal?: string }; export const BIG_NUMBERS = { ZERO: new BigNumber(0), @@ -56,12 +57,12 @@ export const shorternNumberForDisplay = (num?: number) => * @returns separator for the given locale and separator type */ export const getSeparator = ({ - locale = navigator.language || 'en-US', + browserLanguage = navigator.language || 'en-US', separatorType, }: { - locale?: string; + browserLanguage?: string; separatorType: Intl.NumberFormatPartTypes; }) => - Intl.NumberFormat(locale) + Intl.NumberFormat(browserLanguage) .formatToParts(1000.1) .find?.((part) => part.type === separatorType)?.value; diff --git a/src/lib/tradeData.ts b/src/lib/tradeData.ts index 0a790d8..416cc8c 100644 --- a/src/lib/tradeData.ts +++ b/src/lib/tradeData.ts @@ -13,7 +13,7 @@ import { } from '@/constants/abacus'; import { AlertType } from '@/constants/alerts'; -import { PERCENT_DECIMALS } from '@/constants/numbers'; +import { PERCENT_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; import { TRADE_ROUTE } from '@/constants/routes'; import { PositionSide, TradeTypes } from '@/constants/trade'; @@ -65,9 +65,13 @@ export const hasPositionSideChanged = ({ const formatErrorParam = ({ value, format, + stepSizeDecimals, + tickSizeDecimals, }: { value: Nullable; format?: Nullable; + stepSizeDecimals: Nullable; + tickSizeDecimals: Nullable; }) => { switch (format) { case 'percent': { @@ -76,7 +80,11 @@ const formatErrorParam = ({ } case 'size': { const sizeBN = MustBigNumber(value); - return sizeBN.toFixed(0); + return sizeBN.toFixed(stepSizeDecimals ?? 0); + } + case 'price': { + const dollarBN = MustBigNumber(value); + return `$${dollarBN.toFixed(tickSizeDecimals ?? USD_DECIMALS)}`; } default: { return value || ''; @@ -90,9 +98,13 @@ const formatErrorParam = ({ export const getTradeInputAlert = ({ abacusInputErrors, stringGetter, + stepSizeDecimals, + tickSizeDecimals, }: { abacusInputErrors: ValidationError[]; stringGetter: StringGetterFunction; + stepSizeDecimals: Nullable; + tickSizeDecimals: Nullable; }) => { const inputAlerts = abacusInputErrors.map(({ action: errorAction, resources, type }) => { const { action, text } = resources || {}; @@ -104,7 +116,10 @@ export const getTradeInputAlert = ({ Object.fromEntries( stringParams .toArray() - .map(({ key, value, format }) => [key, formatErrorParam({ value, format })]) + .map(({ key, value, format }) => [ + key, + formatErrorParam({ value, format, stepSizeDecimals, tickSizeDecimals }), + ]) ); return { diff --git a/src/main.tsx b/src/main.tsx index a6a9b80..e8a6619 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,5 @@ import './polyfills'; -import { Fragment, StrictMode } from 'react'; +import { StrictMode } from 'react'; import ReactDOM from 'react-dom/client'; import { HashRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; diff --git a/src/pages/trade/HorizontalPanel.tsx b/src/pages/trade/HorizontalPanel.tsx index eed22a2..43be48f 100644 --- a/src/pages/trade/HorizontalPanel.tsx +++ b/src/pages/trade/HorizontalPanel.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { shallowEqual, useSelector } from 'react-redux'; -import styled, { css, type AnyStyledComponent } from 'styled-components'; +import styled, { type AnyStyledComponent } from 'styled-components'; import { STRING_KEYS } from '@/constants/localization'; diff --git a/src/pages/trade/MarketSelectorAndStats.tsx b/src/pages/trade/MarketSelectorAndStats.tsx index 077a402..73138d1 100644 --- a/src/pages/trade/MarketSelectorAndStats.tsx +++ b/src/pages/trade/MarketSelectorAndStats.tsx @@ -1,21 +1,19 @@ import styled, { type AnyStyledComponent } from 'styled-components'; import { shallowEqual, useSelector } from 'react-redux'; -import { DEFAULT_MARKETID } from '@/constants/markets'; import { layoutMixins } from '@/styles/layoutMixins'; import { VerticalSeparator } from '@/components/Separator'; -import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; - -import { getCurrentMarketId } from '@/state/perpetualsSelectors'; - import { MarketsDropdown } from '@/views/MarketsDropdown'; import { MarketStatsDetails } from '@/views/MarketStatsDetails'; +import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; +import { getCurrentMarketId } from '@/state/perpetualsSelectors'; + export const MarketSelectorAndStats = ({ className }: { className?: string }) => { const { symbol = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; - const currentMarketId = useSelector(getCurrentMarketId) ?? DEFAULT_MARKETID; + const currentMarketId = useSelector(getCurrentMarketId); return ( diff --git a/src/pages/trade/Trade.tsx b/src/pages/trade/Trade.tsx index 5035bcd..1f97b66 100644 --- a/src/pages/trade/Trade.tsx +++ b/src/pages/trade/Trade.tsx @@ -1,16 +1,13 @@ -import { useEffect, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import styled, { AnyStyledComponent, css } from 'styled-components'; -import { useLocation } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { TradeLayouts } from '@/constants/layout'; -import { useBreakpoints, usePageTitlePriceUpdates } from '@/hooks'; - -import { setTradeLocation } from '@/state/navigation'; +import { useBreakpoints, useCurrentMarketId, usePageTitlePriceUpdates } from '@/hooks'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; import { getSelectedTradeLayout } from '@/state/layoutSelectors'; @@ -32,18 +29,12 @@ import { TradeBox } from '@/views/TradeBox'; const TradePage = () => { const tradePageRef = useRef(null); - const location = useLocation(); - const dispatch = useDispatch(); + useCurrentMarketId(); const { isTablet } = useBreakpoints(); const tradeLayout = useSelector(getSelectedTradeLayout); - - const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true); - const canAccountTrade = useSelector(calculateCanAccountTrade); - useEffect(() => { - dispatch(setTradeLocation(location)); - }, [location.pathname]); + const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true); usePageTitlePriceUpdates(); diff --git a/src/pages/trade/VerticalPanel.tsx b/src/pages/trade/VerticalPanel.tsx index d180f81..d35848c 100644 --- a/src/pages/trade/VerticalPanel.tsx +++ b/src/pages/trade/VerticalPanel.tsx @@ -7,13 +7,13 @@ import { STRING_KEYS } from '@/constants/localization'; import { useStringGetter } from '@/hooks'; -import { getCurrentMarketId } from '@/state/perpetualsSelectors'; - import { Tabs } from '@/components/Tabs'; import { Orderbook, orderbookMixins, OrderbookScrollBehavior } from '@/views/tables/Orderbook'; import { LiveTrades } from '@/views/tables/LiveTrades'; +import { getCurrentMarketId } from '@/state/perpetualsSelectors'; + enum Tab { Orderbook = 'Orderbook', Trades = 'Trades', diff --git a/src/state/perpetuals.ts b/src/state/perpetuals.ts index 629103c..848b3d8 100644 --- a/src/state/perpetuals.ts +++ b/src/state/perpetuals.ts @@ -10,6 +10,10 @@ import type { } from '@/constants/abacus'; import { Candle, RESOLUTION_MAP } from '@/constants/candles'; +import { LocalStorageKey } from '@/constants/localStorage'; +import { DEFAULT_MARKETID } from '@/constants/markets'; + +import { getLocalStorage } from '@/lib/localStorage'; interface CandleDataByMarket { data: Record; @@ -17,6 +21,7 @@ interface CandleDataByMarket { } export interface PerpetualsState { + currentMarketId?: string; candles: Record; liveTrades?: Record; markets?: Record; @@ -25,6 +30,7 @@ export interface PerpetualsState { } const initialState: PerpetualsState = { + currentMarketId: undefined, candles: {}, liveTrades: {}, markets: undefined, @@ -38,6 +44,9 @@ export const perpetualsSlice = createSlice({ name: 'Perpetuals', initialState, reducers: { + setCurrentMarketId: (state: PerpetualsState, action: PayloadAction) => { + state.currentMarketId = action.payload; + }, setCandles: ( state: PerpetualsState, action: PayloadAction<{ candles: Candle[]; marketId: string; resolution: string }> @@ -116,11 +125,17 @@ export const perpetualsSlice = createSlice({ ) => { state.historicalFundings[action.payload.marketId] = action.payload.historicalFundings; }, - resetPerpetualsState: () => initialState, + resetPerpetualsState: () => + ({ + ...initialState, + currentMarketId: + getLocalStorage({ key: LocalStorageKey.LastViewedMarket }) ?? DEFAULT_MARKETID, + } as PerpetualsState), }, }); export const { + setCurrentMarketId, setCandles, setLiveTrades, setMarkets, diff --git a/src/state/perpetualsSelectors.ts b/src/state/perpetualsSelectors.ts index ee0f353..17a6d33 100644 --- a/src/state/perpetualsSelectors.ts +++ b/src/state/perpetualsSelectors.ts @@ -11,8 +11,7 @@ import { mapCandle } from '@/lib/tradingView/utils'; * @param state * @returns marketId of the market the user is currently viewing */ -export const getCurrentMarketId = (state: RootState) => - matchPath(TRADE_ROUTE, state.navigation.tradeLocation.pathname)?.params.market; +export const getCurrentMarketId = (state: RootState) => state.perpetuals.currentMarketId; /** * @returns assetId of the currentMarket diff --git a/src/views/MarketsDropdown.tsx b/src/views/MarketsDropdown.tsx index af46c44..ea5d0b0 100644 --- a/src/views/MarketsDropdown.tsx +++ b/src/views/MarketsDropdown.tsx @@ -15,7 +15,7 @@ import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType } from '@/components/Output'; import { Popover, TriggerType } from '@/components/Popover'; -import { Tag, TagSize } from '@/components/Tag'; +import { Tag } from '@/components/Tag'; import { Toolbar } from '@/components/Toolbar'; import { ColumnDef, Table } from '@/components/Table'; @@ -152,7 +152,7 @@ const MarketsDropdownContent = ({ onRowAction }: { onRowAction?: (market: string ); }; -export const MarketsDropdown: React.FC<{ currentMarketId: string; symbol: string | null }> = memo( +export const MarketsDropdown: React.FC<{ currentMarketId?: string; symbol: string | null }> = memo( ({ currentMarketId, symbol = '' }) => { const [isOpen, setIsOpen] = useState(false); const stringGetter = useStringGetter(); diff --git a/src/views/TradeBoxOrderView.tsx b/src/views/TradeBoxOrderView.tsx index fbb9f6d..6036781 100644 --- a/src/views/TradeBoxOrderView.tsx +++ b/src/views/TradeBoxOrderView.tsx @@ -1,58 +1,71 @@ +import { useCallback } from 'react'; import styled, { AnyStyledComponent } from 'styled-components'; import { shallowEqual, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; import { TradeInputField } from '@/constants/abacus'; -import { TradeTypes, TRADE_TYPE_STRINGS } from '@/constants/trade'; +import { TradeTypes } from '@/constants/trade'; import { STRING_KEYS } from '@/constants/localization'; import { useStringGetter } from '@/hooks'; import { layoutMixins } from '@/styles/layoutMixins'; import { Tabs } from '@/components/Tabs'; -import { getInputTradeData } from '@/state/inputsSelectors'; +import { getInputTradeData, getInputTradeOptions } from '@/state/inputsSelectors'; import abacusStateManager from '@/lib/abacus'; -import { getSelectedTradeType } from '@/lib/tradeData'; +import { isTruthy } from '@/lib/isTruthy'; import { TradeForm } from './forms/TradeForm'; -export const TradeBoxOrderView = () => { +const useTradeTypeOptions = () => { const stringGetter = useStringGetter(); - const currentTradeData = useSelector(getInputTradeData, shallowEqual); - const { type } = currentTradeData || {}; - const selectedTradeType = getSelectedTradeType(type); + const selectedTradeType = useSelector( + createSelector( + [getInputTradeData], + (currentTradeData) => (currentTradeData?.type?.rawValue as TradeTypes) ?? TradeTypes.LIMIT + ) + ); + const { typeOptions } = useSelector(getInputTradeOptions, shallowEqual) ?? {}; + const allTradeTypeItems = typeOptions?.toArray()?.map(({ type, stringKey }) => ({ + value: type, + label: stringGetter({ key: stringKey }), + })); - const onTradeTypeChange = (tradeType?: TradeTypes) => { + return { + selectedTradeType, + tradeTypeItems: allTradeTypeItems + ? [ + allTradeTypeItems?.shift(), // Limit order is always first + allTradeTypeItems?.shift(), // Market order is always second + // All conditional orders labeled under "Stop Order" + allTradeTypeItems?.length && { + label: stringGetter({ key: STRING_KEYS.STOP_ORDER_SHORT }), + subitems: allTradeTypeItems + ?.map( + ({ value, label }) => + value && { + value: value as TradeTypes, + label, + disabled: import.meta.env.MODE === 'production', // TODO: Remove this when stop orders are supported on testnet + } + ) + .filter(isTruthy), + }, + ].filter(isTruthy) + : [], + }; +}; + +export const TradeBoxOrderView = () => { + const onTradeTypeChange = useCallback((tradeType?: TradeTypes) => { if (tradeType) { abacusStateManager.clearTradeInputValues(); abacusStateManager.setTradeValue({ value: tradeType, field: TradeInputField.type }); } - }; + }, []); - const tradeTypeItems = [ - { - value: TradeTypes.LIMIT, - label: stringGetter({ key: TRADE_TYPE_STRINGS[TradeTypes.LIMIT].tradeTypeKeyShort }), - }, - { - value: TradeTypes.MARKET, - label: stringGetter({ key: TRADE_TYPE_STRINGS[TradeTypes.MARKET].tradeTypeKeyShort }), - }, - { - label: stringGetter({ key: STRING_KEYS.STOP_ORDER_SHORT }), - subitems: [ - TradeTypes.STOP_LIMIT, - TradeTypes.STOP_MARKET, - TradeTypes.TRAILING_STOP, - TradeTypes.TAKE_PROFIT_LIMIT, - TradeTypes.TAKE_PROFIT_MARKET, - ].map((tradeType) => ({ - value: tradeType, - label: stringGetter({ key: TRADE_TYPE_STRINGS[tradeType].tradeTypeKeyShort }), - disabled: true, - })), - }, - ]; + const { selectedTradeType, tradeTypeItems } = useTradeTypeOptions(); return ( void; + setIsOpen: (open: boolean) => void; }; export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { diff --git a/src/views/forms/TradeForm.tsx b/src/views/forms/TradeForm.tsx index 013936d..476986d 100644 --- a/src/views/forms/TradeForm.tsx +++ b/src/views/forms/TradeForm.tsx @@ -99,7 +99,8 @@ export const TradeForm = ({ const { limitPrice, triggerPrice, trailingPercent } = price || {}; const hasUncommittedOrders = useSelector(calculateHasUncommittedOrders); const currentInput = useSelector(getCurrentInput); - const { tickSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {}; + const { tickSizeDecimals, stepSizeDecimals } = + useSelector(getCurrentMarketConfig, shallowEqual) || {}; const needsAdvancedOptions = needsGoodUntil || timeInForceOptions || executionOptions || needsPostOnly || needsReduceOnly; @@ -120,6 +121,8 @@ export const TradeForm = ({ const inputAlert = getTradeInputAlert({ abacusInputErrors: tradeErrors ?? [], stringGetter, + stepSizeDecimals, + tickSizeDecimals, }); if (placeOrderError) { diff --git a/src/views/forms/TradeForm/AdvancedTradeOptions.tsx b/src/views/forms/TradeForm/AdvancedTradeOptions.tsx index 477f947..5fa2ef3 100644 --- a/src/views/forms/TradeForm/AdvancedTradeOptions.tsx +++ b/src/views/forms/TradeForm/AdvancedTradeOptions.tsx @@ -109,6 +109,7 @@ export const AdvancedTradeOptions = () => { {executionOptions && ( abacusStateManager.setTradeValue({ value: selectedTimeInForceOption, diff --git a/src/views/tables/Orderbook.tsx b/src/views/tables/Orderbook.tsx index 1ab0ff7..0cdc8c0 100644 --- a/src/views/tables/Orderbook.tsx +++ b/src/views/tables/Orderbook.tsx @@ -44,7 +44,7 @@ type StyleProps = { type RowData = OrderbookLine & { side: 'bid' | 'ask'; mine?: number }; -const calculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => { +const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => { const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual); const openOrdersBySideAndPrice = @@ -96,7 +96,8 @@ const calculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) } } - const spread = MustBigNumber(asks[0]?.price ?? 0).minus(bids[0]?.price ?? 0); + const spread = + asks[0] && bids[0] ? MustBigNumber(asks[0]?.price ?? 0).minus(bids[0]?.price ?? 0) : null; const spreadPercent = orderbook?.spreadPercent; @@ -233,7 +234,7 @@ export const Orderbook = ({ const showMineColumn = useSelector(calculateCanViewAccount) && !isTablet; const { asks, bids, spread, spreadPercent, histogramRange, hasOrderbook } = - calculateOrderbookData({ + useCalculateOrderbookData({ maxRowsPerSide, }); diff --git a/src/views/tables/OrdersTable.tsx b/src/views/tables/OrdersTable.tsx index 4910fba..755f8ec 100644 --- a/src/views/tables/OrdersTable.tsx +++ b/src/views/tables/OrdersTable.tsx @@ -153,8 +153,16 @@ const getOrdersTableColumnDef = ({ tag: symbol, renderCell: ({ size, totalFilled, stepSizeDecimals }) => ( - - + + ), },