Merge pull request #5 from dydxprotocol/conditional-orders

Conditional orders
This commit is contained in:
Jared Vu
2023-08-28 05:49:27 -10:00
committed by GitHub
34 changed files with 458 additions and 303 deletions
+16 -17
View File
@@ -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>
+49 -54
View File
@@ -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> = {};
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+3 -1
View File
@@ -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';
+5 -5
View File
@@ -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,
+2
View File
@@ -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,
+49
View File
@@ -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]);
};
-19
View File
@@ -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]);
};
+55
View File
@@ -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);
+2 -2
View File
@@ -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 -1
View File
@@ -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';
+11 -3
View File
@@ -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];
+2 -2
View File
@@ -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();
+15 -2
View File
@@ -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);
}
}
+9 -2
View File
@@ -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);
};
}
-4
View File
@@ -46,10 +46,6 @@ class AbacusWebsocket implements Omit<AbacusWebsocketProtocol, '__doNotUseOrImpl
}
}
sendSocketRequest = (requestText: string) => {
this.socket?.send(requestText);
};
handleCandlesSubscription = ({
channelId,
subscribe,
+4 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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';
+4 -6
View File
@@ -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}>
+5 -14
View File
@@ -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();
+2 -2
View File
@@ -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
View File
@@ -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,
+1 -2
View File
@@ -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
+2 -2
View File
@@ -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();
+46 -33
View File
@@ -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) => {
+4 -1
View File
@@ -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,
+4 -3
View File
@@ -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,
});
+10 -2
View File
@@ -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>
),
},