Merge pull request #5 from dydxprotocol/conditional-orders
Conditional orders
This commit is contained in:
+16
-17
@@ -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 = () => {
|
||||
<Routes>
|
||||
<Route path={AppRoute.Trade}>
|
||||
<Route path=":market" element={<TradePage />} />
|
||||
<Route
|
||||
path={AppRoute.Trade}
|
||||
element={<Navigate to={DEFAULT_TRADE_ROUTE} replace />}
|
||||
/>
|
||||
<Route path={AppRoute.Trade} element={<TradePage />} />
|
||||
</Route>
|
||||
<Route path={AppRoute.Markets} element={<MarketsPage />} />
|
||||
{import.meta.env.MODE !== 'production' && (
|
||||
@@ -103,17 +100,19 @@ const App = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GrazProvider>
|
||||
<WagmiConfig config={config}>
|
||||
<DydxProvider>
|
||||
<AccountsProvider>
|
||||
<SubaccountProvider>
|
||||
<NotificationsProvider>
|
||||
<DialogAreaProvider>
|
||||
<Content />
|
||||
</DialogAreaProvider>
|
||||
</NotificationsProvider>
|
||||
</SubaccountProvider>
|
||||
</AccountsProvider>
|
||||
</DydxProvider>
|
||||
<LocaleProvider>
|
||||
<DydxProvider>
|
||||
<AccountsProvider>
|
||||
<SubaccountProvider>
|
||||
<NotificationsProvider>
|
||||
<DialogAreaProvider>
|
||||
<Content />
|
||||
</DialogAreaProvider>
|
||||
</NotificationsProvider>
|
||||
</SubaccountProvider>
|
||||
</AccountsProvider>
|
||||
</DydxProvider>
|
||||
</LocaleProvider>
|
||||
</WagmiConfig>
|
||||
</GrazProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -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 ? (
|
||||
<Styled.Img
|
||||
src={assetIcons[symbol]}
|
||||
className={className}
|
||||
/>
|
||||
) : null
|
||||
);
|
||||
symbol?: Nullable<AssetSymbol>;
|
||||
className?: string;
|
||||
}) => (symbol ? <Styled.Img src={assetIcons[symbol]} className={className} /> : null);
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
|
||||
@@ -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)`
|
||||
|
||||
+18
-1
@@ -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> = 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<KotlinIrEnumValues<typeof AbacusOrderS
|
||||
[AbacusOrderStatus.pending.name]: STRING_KEYS.PENDING,
|
||||
[AbacusOrderStatus.untriggered.name]: STRING_KEYS.UNTRIGGERED,
|
||||
};
|
||||
|
||||
export const TRADE_TYPES: Record<
|
||||
KotlinIrEnumValues<typeof AbacusOrderType>,
|
||||
Nullable<TradeTypes>
|
||||
> = {
|
||||
[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,
|
||||
};
|
||||
|
||||
+100
-115
@@ -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<T extends AnalyticsUserProperty> =
|
||||
export type AnalyticsUserPropertyValue<T extends AnalyticsUserProperty> =
|
||||
// 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<T extends AnalyticsEvent> =
|
||||
// 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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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, string> = {
|
||||
[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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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<typeof useLocaleContext>;
|
||||
const LocaleContext = createContext<LocaleContextType>({} as LocaleContextType);
|
||||
LocaleContext.displayName = 'Locale';
|
||||
|
||||
export const LocaleProvider = ({ ...props }) => (
|
||||
<LocaleContext.Provider value={useLocaleContext()} {...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);
|
||||
@@ -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]
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<number>;
|
||||
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];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<typeof AsyncAbacusStateManager>;
|
||||
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<HumanReadablePlaceOrderPayload> =>
|
||||
this.stateManager.placeOrderPayload();
|
||||
@@ -203,7 +210,7 @@ class AbacusStateManager {
|
||||
};
|
||||
|
||||
sendSocketRequest = (requestText: string) => {
|
||||
this.websocket.sendSocketRequest(requestText);
|
||||
this.websocket.send(requestText);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -46,10 +46,6 @@ class AbacusWebsocket implements Omit<AbacusWebsocketProtocol, '__doNotUseOrImpl
|
||||
}
|
||||
}
|
||||
|
||||
sendSocketRequest = (requestText: string) => {
|
||||
this.socket?.send(requestText);
|
||||
};
|
||||
|
||||
handleCandlesSubscription = ({
|
||||
channelId,
|
||||
subscribe,
|
||||
|
||||
+4
-3
@@ -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;
|
||||
|
||||
+18
-3
@@ -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<string>;
|
||||
format?: Nullable<string>;
|
||||
stepSizeDecimals: Nullable<number>;
|
||||
tickSizeDecimals: Nullable<number>;
|
||||
}) => {
|
||||
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<number>;
|
||||
tickSizeDecimals: Nullable<number>;
|
||||
}) => {
|
||||
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 {
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Styled.Container className={className}>
|
||||
|
||||
@@ -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<HTMLDivElement>(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();
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
+16
-1
@@ -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<string, Candle[]>;
|
||||
@@ -17,6 +21,7 @@ interface CandleDataByMarket {
|
||||
}
|
||||
|
||||
export interface PerpetualsState {
|
||||
currentMarketId?: string;
|
||||
candles: Record<string, CandleDataByMarket>;
|
||||
liveTrades?: Record<string, MarketTrade[]>;
|
||||
markets?: Record<string, PerpetualMarket>;
|
||||
@@ -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<string>) => {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 (
|
||||
<Styled.Tabs
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
|
||||
type ElementProps = {
|
||||
orderId: string;
|
||||
setIsOpen?: (open: boolean) => void;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -109,6 +109,7 @@ export const AdvancedTradeOptions = () => {
|
||||
{executionOptions && (
|
||||
<Styled.SelectMenu
|
||||
value={execution}
|
||||
label={stringGetter({ key: STRING_KEYS.EXECUTION })}
|
||||
onValueChange={(selectedTimeInForceOption: string) =>
|
||||
abacusStateManager.setTradeValue({
|
||||
value: selectedTimeInForceOption,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -153,8 +153,16 @@ const getOrdersTableColumnDef = ({
|
||||
tag: symbol,
|
||||
renderCell: ({ size, totalFilled, stepSizeDecimals }) => (
|
||||
<TableCell stacked>
|
||||
<Output type={OutputType.Asset} value={size} fractionDigits={stepSizeDecimals} />
|
||||
<Output type={OutputType.Asset} value={totalFilled} fractionDigits={stepSizeDecimals} />
|
||||
<Output
|
||||
type={OutputType.Asset}
|
||||
value={size}
|
||||
fractionDigits={stepSizeDecimals < TOKEN_DECIMALS ? TOKEN_DECIMALS : stepSizeDecimals}
|
||||
/>
|
||||
<Output
|
||||
type={OutputType.Asset}
|
||||
value={totalFilled}
|
||||
fractionDigits={stepSizeDecimals < TOKEN_DECIMALS ? TOKEN_DECIMALS : stepSizeDecimals}
|
||||
/>
|
||||
</TableCell>
|
||||
),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user