Abacus Transactions Protocol (#38)

* Merge main

* MarketLinks: add key to map

* Merge stashed changes

* fix callback usage to fulfill success/error cbs

* Add Abacus analytics class

* Bump Abacus 0.4.27, Update cancelOrder to use Abacus

* Cancel order loading states

* Use latestOrderChanged to determine whether an order has been committed

* nits

* restore uncommittedOrderClientIds

* bump abacus@0.5.0

* fix for handling null testMarket
This commit is contained in:
Jared Vu 2023-09-21 09:19:01 -07:00 committed by GitHub
parent 51906f1096
commit f7d052f52e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 487 additions and 423 deletions

View File

@ -37,9 +37,9 @@
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^0.4.28",
"@dydxprotocol/v4-abacus": "^0.5.0",
"@dydxprotocol/v4-client-js": "^0.36.1",
"@dydxprotocol/v4-localization": "^0.1.8",
"@dydxprotocol/v4-localization": "^0.1.11",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-collapsible": "^1.0.3",

16
pnpm-lock.yaml generated
View File

@ -27,14 +27,14 @@ dependencies:
specifier: ^0.31.0
version: 0.31.0
'@dydxprotocol/v4-abacus':
specifier: ^0.4.28
version: 0.4.28
specifier: ^0.5.0
version: 0.5.0
'@dydxprotocol/v4-client-js':
specifier: ^0.36.1
version: 0.36.1
'@dydxprotocol/v4-localization':
specifier: ^0.1.8
version: 0.1.8
specifier: ^0.1.11
version: 0.1.11
'@ethersproject/providers':
specifier: ^5.7.2
version: 5.7.2
@ -979,8 +979,8 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/v4-abacus@0.4.28:
resolution: {integrity: sha512-RQGTXI7q4HAXDmlUpDhpJDLN8C0dFMgKHzFITK1Sz3KnLb3mkkpqEU1D8VIn/hB7ngt+equMsLFSkUwzXCzgdA==}
/@dydxprotocol/v4-abacus@0.5.0:
resolution: {integrity: sha512-K/aLJeOWzmToCn6p9vjBqfwn79/nuKxjZLSzaj1mKNxw25KBFh2q93njNbONFRA72zBElD0K+gw8J/skTpGg1Q==}
dev: false
/@dydxprotocol/v4-client-js@0.36.1:
@ -1010,8 +1010,8 @@ packages:
- utf-8-validate
dev: false
/@dydxprotocol/v4-localization@0.1.8:
resolution: {integrity: sha512-ZuM/V2tLVSWyi9pDZLOu8h6HScrzKlFftQ/1iBPNIOU4bBMx83vYgokm0X9hzueWzz0PJ2pzT+EoN81sPlN2bg==}
/@dydxprotocol/v4-localization@0.1.11:
resolution: {integrity: sha512-eW88t8KJuDT0fNEgaw615+n3gEYxDZYPeHhdBYquVDXjnkhLNznaH8ftmlLPVPyfu7o5aFeU/cw8ZiqRrf/Stw==}
dev: false
/@dydxprotocol/v4-proto@0.2.1:

View File

@ -40,6 +40,10 @@ export type AbacusFileSystemProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.FileSystemProtocol,
'__doNotUseOrImplementIt'
>;
export type AbacusTrackingProtocol = Omit<
Abacus.exchange.dydx.abacus.protocols.TrackingProtocol,
'__doNotUseOrImplementIt'
>;
export type FileLocation = Abacus.exchange.dydx.abacus.protocols.FileLocation;
export type ThreadingType = Abacus.exchange.dydx.abacus.protocols.ThreadingType;

View File

@ -71,6 +71,7 @@ export enum AnalyticsEvent {
// Transfers
TransferFaucet = 'TransferFaucet',
TransferFaucetConfirmed = 'TransferFaucetConfirmed',
TransferDeposit = 'TransferDeposit',
TransferWithdraw = 'TransferWithdraw',
@ -78,6 +79,7 @@ export enum AnalyticsEvent {
TradeOrderTypeSelected = 'TradeOrderTypeSelected',
TradePlaceOrder = 'TradePlaceOrder',
TradePlaceOrderConfirmed = 'TradePlaceOrderConfirmed',
TradeCancelOrder = 'TradeCancelOrder',
TradeCancelOrderConfirmed = 'TradeCancelOrderConfirmed',
}
@ -133,6 +135,13 @@ export type AnalyticsEventData<T extends AnalyticsEvent> =
: // Transfers
T extends AnalyticsEvent.TransferFaucet
? {}
: T extends AnalyticsEvent.TransferFaucetConfirmed
? {
/** 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 */
validatorUrl: string;
}
: T extends AnalyticsEvent.TransferDeposit
? {}
: T extends AnalyticsEvent.TransferWithdraw
@ -151,13 +160,15 @@ export type AnalyticsEventData<T extends AnalyticsEvent> =
/** 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;
validatorUrl: string;
}
: T extends AnalyticsEvent.TradeCancelOrder
? {}
: 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;
validatorUrl: string;
}
: never;

View File

@ -88,19 +88,3 @@ export type TooltipStrings = {
learnMoreLink?: string;
};
};
export const ORDER_ERROR_CODE_MAP: { [key: number]: string } = {
2000: STRING_KEYS['2000_FILL_OR_KILL_ORDER_COULD_NOT_BE_FULLY_FILLED'],
2001: STRING_KEYS['2001_REDUCE_ONLY_WOULD_INCREASE_POSITION_SIZE'],
2002: STRING_KEYS['2002_REDUCE_ONLY_WOULD_CHANGE_POSITION_SIDE'],
2003: STRING_KEYS['2003_POST_ONLY_WOULD_CROSS_MAKER_ORDER'],
3000: STRING_KEYS['3000_INVALID_ORDER_FLAGS'],
3001: STRING_KEYS['3001_INVALID_STATEFUL_ORDER_GOOD_TIL_BLOCK_TIME'],
3002: STRING_KEYS['3002_STATEFUL_ORDERS_CANNOT_REQUIRE_IMMEDIATE_EXECUTION'],
3003: STRING_KEYS['3003_TIME_EXCEEDS_GOOD_TIL_BLOCK_TIME'],
3004: STRING_KEYS['3004_GOOD_TIL_BLOCK_TIME_EXCEEDS_STATEFUL_ORDER_TIME_WINDOW'],
3005: STRING_KEYS['3005_STATEFUL_ORDER_ALREADY_EXISTS'],
3006: STRING_KEYS['3006_STATEFUL_ORDER_DOES_NOT_EXIST'],
3007: STRING_KEYS['3007_STATEFUL_ORDER_COLLATERALIZATION_CHECK_FAILED'],
3008: STRING_KEYS['3008_STATEFUL_ORDER_PREVIOUSLY_CANCELLED'],
};

View File

@ -27,7 +27,7 @@ export enum PositionSide {
Short = 'SHORT',
}
export const UNCOMMITTED_ORDER_TIMEOUT = 10_000;
export const UNCOMMITTED_ORDER_TIMEOUT_MS = 10_000;
export const ORDER_SIDE_STRINGS = {
[OrderSide.BUY]: STRING_KEYS.BUY,

View File

@ -8,10 +8,7 @@ import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/co
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EvmAddress, PrivateInformation } from '@/constants/wallets';
import {
setOnboardingState,
setOnboardingGuard,
} from '@/state/account';
import { setOnboardingState, setOnboardingGuard } from '@/state/account';
import abacusStateManager from '@/lib/abacus';
import { log } from '@/lib/telemetry';
@ -222,9 +219,9 @@ const useAccountsContext = () => {
// abacus
// TODO: useAbacus({ dydxAddress })
useEffect(() => {
if (dydxAddress) abacusStateManager.setAccount(dydxAddress);
if (dydxAddress) abacusStateManager.setAccount(localDydxWallet);
else abacusStateManager.attemptDisconnectAccount();
}, [dydxAddress]);
}, [localDydxWallet]);
// clear subaccounts when no dydxAddress is set
useEffect(() => {

View File

@ -29,7 +29,7 @@ export const useMarketsData = (
const markets = useMemo(() => {
return Object.values(allPerpetualMarkets)
.filter(({ assetId }) => allAssets[assetId]) // Filter out markets with no asset information
.filter((market) => allAssets[market?.assetId]) // Filter out markets with no asset information
.map((marketData) => ({
asset: allAssets[marketData.assetId],
tickSizeDecimals: marketData.configs?.tickSizeDecimals,

View File

@ -0,0 +1,26 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { getLatestOrderClientId } from '@/state/accountSelectors';
/**
* @description This hook will fire a callback when the latest order has been updated to a non-pending state from the Indexer.
* @param { callback: () => void }
*/
export const useOnLastOrderIndexed = ({ callback }: { callback: () => void }) => {
const [unIndexedClientId, setUnIndexedClientId] = useState<number | undefined>();
const latestOrderClientId = useSelector(getLatestOrderClientId);
useEffect(() => {
if (unIndexedClientId) {
if (unIndexedClientId === latestOrderClientId) {
callback();
setUnIndexedClientId(undefined);
}
}
}, [callback, latestOrderClientId, unIndexedClientId]);
return {
setUnIndexedClientId,
};
};

View File

@ -7,12 +7,7 @@ import type { EncodeObject, Coin } from '@cosmjs/proto-signing';
import { Method } from '@cosmjs/tendermint-rpc';
import {
LocalWallet,
OrderExecution,
OrderFlags,
OrderSide,
OrderTimeInForce,
OrderType,
type LocalWallet,
SubaccountClient,
DYDX_DENOM,
USDC_DENOM,
@ -20,28 +15,20 @@ import {
} from '@dydxprotocol/v4-client-js';
import type {
HumanReadableCancelOrderPayload,
HumanReadablePlaceOrderPayload,
ParsingError,
SubAccountHistoricalPNLs,
} from '@/constants/abacus';
import { AMOUNT_RESERVED_FOR_GAS_USDC } from '@/constants/account';
import { AnalyticsEvent } from '@/constants/analytics';
import { ORDER_ERROR_CODE_MAP } from '@/constants/localization';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { UNCOMMITTED_ORDER_TIMEOUT } from '@/constants/trade';
import { DydxAddress } from '@/constants/wallets';
import {
addUncommittedOrderClientId,
removeUncommittedOrderClientId,
setSubaccount,
setHistoricalPnl,
} from '@/state/account';
import { setSubaccount, setHistoricalPnl, removeUncommittedOrderClientId } from '@/state/account';
import abacusStateManager from '@/lib/abacus';
import { track } from '@/lib/analytics';
import { StatefulOrderError } from '@/lib/errors';
import { MustBigNumber } from '@/lib/numbers';
import { log } from '@/lib/telemetry';
@ -87,8 +74,6 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
transferFromSubaccountToAddress,
transferNativeToken,
simulateTransferNativeToken,
placeOrderForSubaccount,
cancelOrderForSubaccount,
sendSquidWithdrawFromSubaccount,
} = useMemo(
() => ({
@ -210,100 +195,6 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
GAS_PRICE_DYDX_DENOM,
undefined
),
placeOrderForSubaccount: async ({
subaccount,
marketId,
type,
side,
price,
triggerPrice,
size,
clientId,
timeInForce,
goodTilTimeInSeconds,
execution,
postOnly,
reduceOnly,
}: {
subaccount: SubaccountClient;
marketId: string;
type: OrderType;
side: OrderSide;
price: number;
triggerPrice: Nullable<number>;
size: number;
clientId: number;
timeInForce: OrderTimeInForce;
goodTilTimeInSeconds: number;
execution: OrderExecution;
postOnly: boolean;
reduceOnly: boolean;
}) => {
const startTimestamp = performance.now();
const result = await compositeClient?.placeOrder(
subaccount,
marketId,
type,
side,
price,
size,
clientId,
timeInForce,
goodTilTimeInSeconds,
execution,
postOnly,
reduceOnly,
triggerPrice ?? undefined
);
const endTimestamp = performance.now();
track(AnalyticsEvent.TradePlaceOrderConfirmed, {
roundtripMs: endTimestamp - startTimestamp,
validator: compositeClient!.validatorClient.config.restEndpoint,
});
return result;
},
cancelOrderForSubaccount: async ({
subaccount,
clientId,
clobPairId,
orderFlags,
goodTilBlock,
goodTilBlockTime,
}: {
subaccount: SubaccountClient;
clientId: number;
orderFlags: OrderFlags;
clobPairId: number;
goodTilBlock?: number;
goodTilBlockTime?: number;
}) => {
const startTimestamp = performance.now();
const result = await compositeClient?.cancelOrder(
subaccount,
clientId,
orderFlags,
clobPairId,
goodTilBlock,
goodTilBlockTime
);
const endTimestamp = performance.now();
track(AnalyticsEvent.TradeCancelOrderConfirmed, {
roundtripMs: endTimestamp - startTimestamp,
validator: compositeClient!.validatorClient.config.restEndpoint,
});
return result;
},
sendSquidWithdrawFromSubaccount: async ({
subaccountClient,
amount,
@ -467,101 +358,41 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
onSuccess,
}: {
isClosePosition?: boolean;
onError?: (onErrorParams?: { errorStringKey?: string }) => void;
onSuccess?: () => void;
onError?: (onErrorParams?: { errorStringKey?: Nullable<string> }) => void;
onSuccess?: (placeOrderPayload: Nullable<HumanReadablePlaceOrderPayload>) => void;
}) => {
let orderParams: Nullable<HumanReadablePlaceOrderPayload>;
const callback = (
success: boolean,
parsingError?: Nullable<ParsingError>,
data?: Nullable<HumanReadablePlaceOrderPayload>
) => {
if (success) {
onSuccess?.(data);
} else {
onError?.({ errorStringKey: parsingError?.stringKey });
if (!subaccountClient) return;
try {
orderParams = isClosePosition
? abacusStateManager.closePositionPayload()
: abacusStateManager.placeOrderPayload();
if (!orderParams) {
throw new Error('Missing order params');
if (data?.clientId !== undefined) {
dispatch(removeUncommittedOrderClientId(data.clientId));
}
}
};
const {
marketId,
type,
side,
price,
triggerPrice,
size,
clientId,
timeInForce,
goodTilTimeInSeconds,
execution,
postOnly,
reduceOnly,
} = orderParams;
let placeOrderParams;
dispatch(addUncommittedOrderClientId(clientId));
// Remove uncommitted order after timeout if it hasn't already been removed
setTimeout(() => {
dispatch(removeUncommittedOrderClientId(clientId));
}, UNCOMMITTED_ORDER_TIMEOUT);
console.log('useSubaccount/placeOrder', {
...orderParams,
});
const response = await placeOrderForSubaccount({
subaccount: subaccountClient,
marketId,
type: type as OrderType,
side: side as OrderSide,
price,
triggerPrice,
size,
clientId,
timeInForce: timeInForce as OrderTimeInForce,
goodTilTimeInSeconds: goodTilTimeInSeconds ?? 0,
execution: execution as OrderExecution,
postOnly,
reduceOnly,
});
// Handle Stateful orders
if ((response as IndexedTx)?.code !== 0) {
throw new StatefulOrderError('Stateful order has failed to commit.', response);
}
if (orderParams?.clientId) {
dispatch(removeUncommittedOrderClientId(orderParams.clientId));
}
if (response?.hash) {
console.log(
isClosePosition
? 'useSubaccount/closePosition'
: 'useSubaccount/placeOrderForSubaccount',
{
txHash: Buffer.from(response.hash).toString('hex').toUpperCase(),
}
);
}
track(AnalyticsEvent.TradePlaceOrder, {
...orderParams,
isClosePosition,
} as HumanReadablePlaceOrderPayload & { isClosePosition: boolean });
onSuccess?.();
} catch (error) {
const errorCode: number | undefined = error?.code;
const errorStringKey = errorCode ? ORDER_ERROR_CODE_MAP[errorCode] : undefined;
onError?.({ errorStringKey });
log('useSubaccount/placeOrder', error, {
orderParams,
isClosePosition,
});
if (isClosePosition) {
placeOrderParams = abacusStateManager.closePosition(callback);
} else {
placeOrderParams = abacusStateManager.placeOrder(callback);
}
track(AnalyticsEvent.TradePlaceOrder, {
...placeOrderParams,
isClosePosition,
} as HumanReadablePlaceOrderPayload & { isClosePosition: boolean });
return placeOrderParams;
},
[subaccountClient, placeOrderForSubaccount]
[subaccountClient]
);
const closePosition = useCallback(
@ -569,8 +400,8 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
onError,
onSuccess,
}: {
onError: (onErrorParams?: { errorStringKey?: string }) => void;
onSuccess?: () => void;
onError: (onErrorParams?: { errorStringKey?: Nullable<string> }) => void;
onSuccess?: (placeOrderPayload: Nullable<HumanReadablePlaceOrderPayload>) => void;
}) => await placeOrder({ isClosePosition: true, onError, onSuccess }),
[placeOrder]
);
@ -582,53 +413,21 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
onSuccess,
}: {
orderId: string;
onError?: ({ errorMsg }?: { errorMsg?: string }) => void;
onError?: ({ errorStringKey }?: { errorStringKey?: Nullable<string> }) => void;
onSuccess?: () => void;
}) => {
let cancelOrderParams: Nullable<HumanReadableCancelOrderPayload>;
if (!subaccountClient) return;
try {
cancelOrderParams = abacusStateManager.cancelOrderPayload(orderId);
if (!cancelOrderParams) {
throw new Error('Missing cancel order params');
const callback = (success: boolean, parsingError?: Nullable<ParsingError>) => {
if (success) {
track(AnalyticsEvent.TradeCancelOrder);
onSuccess?.();
} else {
onError?.({ errorStringKey: parsingError?.stringKey });
}
};
const { clientId, clobPairId, goodTilBlock, goodTilBlockTime, orderFlags } =
cancelOrderParams;
// Keep for debugging
console.log('useSubaccount/cancelOrder', cancelOrderParams);
const response = await cancelOrderForSubaccount({
subaccount: subaccountClient,
clientId,
orderFlags,
clobPairId,
goodTilBlock: goodTilBlock || undefined,
goodTilBlockTime: goodTilBlockTime || undefined,
});
// Handle Stateful orders
if ((response as IndexedTx)?.code !== 0) {
throw new StatefulOrderError('Stateful cancel has failed to commit.', response);
}
if (response?.hash) {
console.log('useSubaccount/cancelOrderForSubaccount', {
txHash: Buffer.from(response.hash).toString('hex').toUpperCase(),
});
}
onSuccess?.();
} catch (error) {
onError?.();
log('useSubaccount/cancelOrder', error, cancelOrderParams);
}
abacusStateManager.cancelOrder(orderId, callback);
},
[subaccountClient, cancelOrderForSubaccount]
[subaccountClient]
);
return {

View File

@ -0,0 +1,18 @@
import type { AbacusTrackingProtocol, Nullable } from '@/constants/abacus';
import type { AnalyticsEvent } from '@/constants/analytics';
import { track } from '../analytics';
import { log as telemetryLog } from '../telemetry';
class AbacusAnalytics implements AbacusTrackingProtocol {
log(event: string, data: Nullable<string>) {
try {
const parsedData = data ? JSON.parse(data) : {};
track(event as AnalyticsEvent, parsedData);
} catch (error) {
telemetryLog('AbacusAnalytics/log', error);
}
}
}
export default AbacusAnalytics;

View File

@ -1,30 +1,46 @@
import Abacus, { Nullable } from '@dydxprotocol/v4-abacus';
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus';
import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import {
CompositeClient,
IndexerConfig,
type LocalWallet,
Network,
NetworkOptimizer,
SubaccountClient,
ValidatorConfig,
OrderType,
OrderSide,
OrderTimeInForce,
OrderExecution,
} from '@dydxprotocol/v4-client-js';
import {
type AbacusDYDXChainTransactionsProtocol,
QueryType,
type QueryTypes,
TransactionType,
type TransactionTypes,
type HumanReadablePlaceOrderPayload,
type HumanReadableCancelOrderPayload,
} from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { UNCOMMITTED_ORDER_TIMEOUT_MS } from '@/constants/trade';
import { RootStore } from '@/state/_store';
import { addUncommittedOrderClientId, removeUncommittedOrderClientId } from '@/state/account';
import { openDialog } from '@/state/dialogs';
import { StatefulOrderError } from '../errors';
import { bytesToBigInt } from '../numbers';
import { log } from '../telemetry';
class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
private compositeClient: CompositeClient | undefined;
private store: RootStore | undefined;
private localWallet: LocalWallet | undefined;
constructor() {
this.compositeClient = undefined;
@ -35,6 +51,10 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
this.store = store;
}
setLocalWallet(localWallet: LocalWallet) {
this.localWallet = localWallet;
}
async connectNetwork(
indexerUrl: string,
indexerSocketUrl: string,
@ -93,6 +113,10 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
return x.toString() as T;
}
if (x instanceof Uint8Array) {
return bytesToBigInt(x).toString() as T;
}
if (typeof x === 'object') {
const parsedObj: { [key: string]: any } = {};
for (const key in x) {
@ -106,13 +130,138 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
throw new Error(`Unsupported data type: ${typeof x}`);
}
async placeOrderTransaction(params: HumanReadablePlaceOrderPayload): Promise<string> {
if (!this.compositeClient || !this.localWallet)
throw new Error('Missing compositeClient or localWallet');
try {
const {
subaccountNumber,
marketId,
type,
side,
price,
size,
clientId,
timeInForce,
goodTilTimeInSeconds,
execution,
postOnly,
reduceOnly,
triggerPrice,
} = params || {};
// Observe uncommitted order
this.store?.dispatch(addUncommittedOrderClientId(clientId));
setTimeout(() => {
this.store?.dispatch(removeUncommittedOrderClientId(clientId));
}, UNCOMMITTED_ORDER_TIMEOUT_MS);
// Place order
const tx = await this.compositeClient?.placeOrder(
new SubaccountClient(this.localWallet, subaccountNumber),
marketId,
type as OrderType,
side as OrderSide,
price,
size,
clientId,
timeInForce as OrderTimeInForce,
goodTilTimeInSeconds ?? 0,
execution as OrderExecution,
postOnly,
reduceOnly,
triggerPrice ?? undefined
);
// Handle stateful orders
if ((tx as IndexedTx)?.code !== 0) {
throw new StatefulOrderError('Stateful order has failed to commit.', tx);
}
const parsedTx = this.parseToPrimitives(tx);
const hash = parsedTx?.hash;
if (import.meta.env.MODE === 'production') {
console.log(`https://testnet.mintscan.io/dydx-testnet/txs/${hash}`);
} else console.log(`txHash: ${hash}`);
return JSON.stringify(parsedTx);
} catch (error) {
if (error?.name !== 'BroadcastError') {
log('DydxChainTransactions/placeOrderTransaction', error);
}
return JSON.stringify({
error,
});
}
}
async cancelOrderTransaction(params: HumanReadableCancelOrderPayload): Promise<string> {
if (!this.compositeClient || !this.localWallet) {
throw new Error('Missing compositeClient or localWallet');
}
const { subaccountNumber, clientId, orderFlags, clobPairId, goodTilBlock, goodTilBlockTime } =
params ?? {};
try {
const tx = await this.compositeClient?.cancelOrder(
new SubaccountClient(this.localWallet, subaccountNumber),
clientId,
orderFlags,
clobPairId,
goodTilBlock ?? undefined,
goodTilBlockTime ?? undefined
);
const parsedTx = this.parseToPrimitives(tx);
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/cancelOrderTransaction', error);
return JSON.stringify({
error,
});
}
}
async transaction(
type: TransactionTypes,
paramsInJson: Abacus.Nullable<string>,
callback: (p0: Abacus.Nullable<string>) => void
): Promise<void> {
// To be implemented
return;
try {
const params = paramsInJson ? JSON.parse(paramsInJson) : undefined;
switch (type) {
case TransactionType.PlaceOrder: {
const result = await this.placeOrderTransaction(params);
callback(result);
break;
}
case TransactionType.CancelOrder: {
const result = await this.cancelOrderTransaction(params);
callback(result);
break;
}
default: {
break;
}
}
} catch (error) {
try {
const serializedError = JSON.stringify(error);
callback(serializedError);
} catch (parseError) {
log('DydxChainTransactions/transaction', parseError);
}
log('DydxChainTransactions/transaction', error);
}
}
async get(
@ -136,6 +285,12 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
);
callback(JSON.stringify({ url: optimalNode }));
break;
case QueryType.EquityTiers:
const equityTiers =
await this.compositeClient?.validatorClient.get.getEquityTierLimitConfiguration();
const parsedEquityTiers = this.parseToPrimitives(equityTiers);
callback(JSON.stringify(parsedEquityTiers));
break;
case QueryType.FeeTiers:
const feeTiers = await this.compositeClient?.validatorClient.get.getFeeTiers();
const parsedFeeTiers = this.parseToPrimitives(feeTiers);

View File

@ -1,3 +1,5 @@
import type { LocalWallet } from '@dydxprotocol/v4-client-js';
import type {
ClosePositionInputFields,
Nullable,
@ -6,6 +8,7 @@ import type {
TradeInputFields,
TransferInputFields,
HistoricalPnlPeriods,
ParsingError,
} from '@/constants/abacus';
import {
@ -30,6 +33,7 @@ import { getInputTradeOptions } from '@/state/inputsSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import AbacusRest from './rest';
import AbacusAnalytics from './analytics';
import AbacusWebsocket from './websocket';
import AbacusChainTransaction from './dydxChainTransactions';
import AbacusStateNotifier from './stateNotification';
@ -45,23 +49,25 @@ class AbacusStateManager {
stateManager: InstanceType<typeof AsyncAbacusStateManager>;
websocket: AbacusWebsocket;
stateNotifier: AbacusStateNotifier;
analytics: AbacusAnalytics;
abacusFormatter: AbacusFormatter;
chainTransactionClient: AbacusChainTransaction;
chainTransactions: AbacusChainTransaction;
constructor() {
this.store = undefined;
this.currentMarket = undefined;
this.stateNotifier = new AbacusStateNotifier();
this.analytics = new AbacusAnalytics();
this.websocket = new AbacusWebsocket();
this.abacusFormatter = new AbacusFormatter();
this.chainTransactionClient = new AbacusChainTransaction();
this.chainTransactions = new AbacusChainTransaction();
const ioImplementations = new IOImplementations(
// @ts-ignore
new AbacusRest(),
this.websocket,
this.chainTransactionClient,
null,
this.chainTransactions,
this.analytics,
new AbacusThreading(),
new CoroutineTimer(),
new AbacusFileSystem()
@ -157,11 +163,14 @@ class AbacusStateManager {
setStore = (store: RootStore) => {
this.store = store;
this.stateNotifier.setStore(store);
this.chainTransactionClient.setStore(store);
this.chainTransactions.setStore(store);
};
setAccount = (walletAddress: string) => {
this.stateManager.accountAddress = walletAddress;
setAccount = (localWallet?: LocalWallet) => {
if (localWallet) {
this.stateManager.accountAddress = localWallet.address;
this.chainTransactions.setLocalWallet(localWallet);
}
};
setTransfersSourceAddress = (evmAddress: string) => {
@ -219,16 +228,34 @@ class AbacusStateManager {
this.stateManager.transferStatus(hash, fromChainId, toChainId);
};
// ------ Transactions ------ //
placeOrder = (
callback: (
success: boolean,
parsingError: Nullable<ParsingError>,
data: Nullable<HumanReadablePlaceOrderPayload>
) => void
): Nullable<HumanReadablePlaceOrderPayload> => this.stateManager.commitPlaceOrder(callback);
closePosition = (
callback: (
success: boolean,
parsingError: Nullable<ParsingError>,
data: Nullable<HumanReadablePlaceOrderPayload>
) => void
): Nullable<HumanReadablePlaceOrderPayload> => this.stateManager.commitClosePosition(callback);
cancelOrder = (
orderId: string,
callback: (
success: boolean,
parsingError: Nullable<ParsingError>,
data: Nullable<HumanReadableCancelOrderPayload>
) => void
) => this.stateManager.cancelOrder(orderId, callback);
// ------ Utils ------ //
placeOrderPayload = (): Nullable<HumanReadablePlaceOrderPayload> =>
this.stateManager.placeOrderPayload();
closePositionPayload = (): Nullable<HumanReadablePlaceOrderPayload> =>
this.stateManager.closePositionPayload();
cancelOrderPayload = (orderId: string): Nullable<HumanReadableCancelOrderPayload> =>
this.stateManager.cancelOrderPayload(orderId);
getHistoricalPnlPeriod = (): Nullable<HistoricalPnlPeriods> =>
this.stateManager.historicalPnlPeriod;

View File

@ -21,6 +21,7 @@ import {
setFills,
setFundingPayments,
setHistoricalPnl,
setLatestOrder,
setSubaccount,
setTransfers,
setWallet,
@ -155,7 +156,9 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
}
}
lastOrderChanged(order: SubaccountOrder) {}
lastOrderChanged(order: SubaccountOrder) {
this.store?.dispatch(setLatestOrder(order));
}
errorsEmitted(errors: ParsingErrors) {
console.error('parse errors', errors.toArray());

View File

@ -5,10 +5,15 @@ import type {
AnalyticsEventData,
} from '@/constants/analytics';
const DEBUG_ANALYTICS = false;
export const identify = <T extends AnalyticsUserProperty>(
property: T,
propertyValue: AnalyticsUserPropertyValue<T>
) => {
if (DEBUG_ANALYTICS) {
console.log(`[Analytics:Identify] ${property}`, propertyValue);
}
const customEvent = new CustomEvent('dydx:identify', {
detail: { property, propertyValue },
});
@ -20,6 +25,9 @@ export const track = <T extends AnalyticsEvent>(
eventType: T,
eventData?: AnalyticsEventData<T>
) => {
if (DEBUG_ANALYTICS) {
console.log(`[Analytics] ${eventType}`, eventData);
}
const customEvent = new CustomEvent('dydx:track', {
detail: { eventType, eventData },
});

View File

@ -66,3 +66,17 @@ export const getSeparator = ({
Intl.NumberFormat(browserLanguage)
.formatToParts(1000.1)
.find?.((part) => part.type === separatorType)?.value;
/**
* Converts a byte array (representing an arbitrary-size signed integer) into a bigint.
* @param u Array of bytes represented as a Uint8Array.
*/
export function bytesToBigInt(u: Uint8Array): bigint {
if (u.length <= 1) {
return BigInt(0);
}
const negated: boolean = (u[0] & 1) === 1;
const hex: string = Buffer.from(u.slice(1)).toString('hex');
const abs: bigint = BigInt(`0x${hex}`);
return negated ? -abs : abs;
}

View File

@ -58,6 +58,12 @@ export const getStatusIconInfo = ({
statusIconColor: `var(--color-negative)`,
};
}
case AbacusOrderStatus.untriggered: {
return {
statusIcon: IconName.OrderUntriggered,
statusIconColor: `var(--color-text-2)`,
};
}
case AbacusOrderStatus.pending:
default: {
return {

View File

@ -2,6 +2,7 @@ import { useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { AbacusOrderStatus } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
@ -18,13 +19,16 @@ import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { OrdersTable, OrdersTableColumnKey } from '@/views/tables/OrdersTable';
import { PositionsTable, PositionsTableColumnKey } from '@/views/tables/PositionsTable';
import { calculateIsAccountViewOnly } from '@/state/accountCalculators';
import {
calculateHasUncommittedOrders,
calculateIsAccountViewOnly,
} from '@/state/accountCalculators';
import {
getCurrentMarketTradeInfoNumbers,
getHasUnseenFillUpdates,
getHasUnseenOrderUpdates,
getLatestOrderStatus,
getTradeInfoNumbers,
} from '@/state/accountSelectors';
@ -69,8 +73,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
const hasUnseenOrderUpdates = useSelector(getHasUnseenOrderUpdates);
const hasUnseenFillUpdates = useSelector(getHasUnseenFillUpdates);
const isAccountViewOnly = useSelector(calculateIsAccountViewOnly);
const hasUncommittedOrders = useSelector(calculateHasUncommittedOrders);
const isWaitingForOrderToIndex = useSelector(calculateHasUncommittedOrders);
const showCurrentMarket = isTablet || view === PanelView.CurrentMarket;
const fillsTagNumber = shortenNumberForDisplay(showCurrentMarket ? numFills : numTotalFills);
@ -118,7 +121,7 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
value: InfoSection.Orders,
label: stringGetter({ key: STRING_KEYS.ORDERS }),
slotRight: hasUncommittedOrders ? (
slotRight: isWaitingForOrderToIndex ? (
<Styled.LoadingSpinner />
) : (
ordersTagNumber && (

View File

@ -4,7 +4,6 @@ import abacusStateManager from '@/lib/abacus';
import appMiddleware from './appMiddleware';
import localizationMiddleware from './localizationMiddleware';
import routerMiddleware from './routerMiddleware';
import tradeMiddleware from './tradeMiddleware';
import { accountSlice } from './account';
import { appSlice } from './app';
@ -40,7 +39,6 @@ export const store = configureStore({
appMiddleware,
localizationMiddleware,
routerMiddleware,
tradeMiddleware,
],
devTools: process.env.NODE_ENV !== 'production',

View File

@ -24,13 +24,14 @@ export type AccountState = {
fundingPayments?: SubaccountFundingPayments;
transfers?: SubaccountTransfers;
clearedOrderIds?: string[];
uncommittedOrderClientIds?: number[];
hasUnseenFillUpdates: boolean;
hasUnseenOrderUpdates: boolean;
historicalPnl?: SubAccountHistoricalPNLs;
latestOrder?: Nullable<SubaccountOrder>;
onboardingGuards: Record<OnboardingGuard, boolean | undefined>;
onboardingState: OnboardingState;
subaccount?: Nullable<Subaccount>;
uncommittedOrderClientIds: number[];
wallet?: Nullable<Wallet>;
walletType?: WalletType;
historicalPnlPeriod?: HistoricalPnlPeriods;
@ -41,10 +42,10 @@ const initialState: AccountState = {
fundingPayments: undefined,
transfers: undefined,
clearedOrderIds: undefined,
uncommittedOrderClientIds: undefined,
hasUnseenFillUpdates: false,
hasUnseenOrderUpdates: false,
historicalPnl: undefined,
latestOrder: undefined,
onboardingGuards: {
[OnboardingGuard.hasAcknowledgedTerms]: Boolean(
getLocalStorage<boolean>({
@ -56,6 +57,7 @@ const initialState: AccountState = {
},
onboardingState: OnboardingState.Disconnected,
subaccount: undefined,
uncommittedOrderClientIds: [],
wallet: undefined,
walletType: getLocalStorage<WalletType>({
key: LocalStorageKey.OnboardingSelectedWalletType,
@ -86,6 +88,16 @@ export const accountSlice = createSlice({
setTransfers: (state, action: PayloadAction<any>) => {
state.transfers = action.payload;
},
setLatestOrder: (state, action: PayloadAction<Nullable<SubaccountOrder>>) => {
const { clientId } = action.payload ?? {};
state.latestOrder = action.payload;
if (clientId) {
state.uncommittedOrderClientIds = state.uncommittedOrderClientIds.filter(
(id) => id !== clientId
);
}
},
clearOrder: (state, action: PayloadAction<string>) => ({
...state,
clearedOrderIds: [...(state.clearedOrderIds || []), action.payload],
@ -131,22 +143,20 @@ export const accountSlice = createSlice({
...state,
wallet: action.payload,
}),
addUncommittedOrderClientId: (state, action: PayloadAction<number>) => {
state.uncommittedOrderClientIds = state.uncommittedOrderClientIds
? [...state.uncommittedOrderClientIds, action.payload]
: [action.payload];
},
removeUncommittedOrderClientId: (state, action: PayloadAction<number>) => {
state.uncommittedOrderClientIds = state.uncommittedOrderClientIds?.filter(
(clientId) => clientId !== action.payload
);
},
viewedFills: (state) => {
state.hasUnseenFillUpdates = false;
},
viewedOrders: (state) => {
state.hasUnseenOrderUpdates = false;
},
addUncommittedOrderClientId: (state, action: PayloadAction<number>) => {
state.uncommittedOrderClientIds.push(action.payload);
},
removeUncommittedOrderClientId: (state, action: PayloadAction<number>) => {
state.uncommittedOrderClientIds = state.uncommittedOrderClientIds.filter(
(id) => id !== action.payload
);
},
},
});
@ -154,14 +164,15 @@ export const {
setFills,
setFundingPayments,
setTransfers,
setLatestOrder,
clearOrder,
setOnboardingGuard,
setOnboardingState,
setHistoricalPnl,
setSubaccount,
setWallet,
addUncommittedOrderClientId,
removeUncommittedOrderClientId,
viewedFills,
viewedOrders,
addUncommittedOrderClientId,
removeUncommittedOrderClientId,
} = accountSlice.actions;

View File

@ -9,6 +9,7 @@ import {
getOnboardingGuards,
getOnboardingState,
getSubaccountId,
getUncommittedOrderClientIds,
} from '@/state/accountSelectors';
import { getSelectedNetwork } from '@/state/appSelectors';
@ -69,6 +70,11 @@ export const calculateHasOpenPositions = createSelector(
(openPositions?: SubaccountPosition[]) => (openPositions?.length || 0) > 0
);
export const calculateHasUncommittedOrders = createSelector(
[getUncommittedOrderClientIds],
(uncommittedOrderClientIds: number[]) => uncommittedOrderClientIds.length > 0
);
/**
* @description calculate whether the client is loading info.
* (account is connected but subaccountId is till missing)

View File

@ -78,6 +78,12 @@ export const getCurrentMarketPositionData = (state: RootState) => {
export const getSubaccountOrders = (state: RootState) =>
state.account.subaccount?.orders?.toArray();
/**
* @param state
* @returns latestOrder of the currently connected subaccount throughout this session
*/
export const getLatestOrder = (state: RootState) => state.account?.latestOrder;
/**
* @param state
* @returns list of order ids that user has cleared and should be hidden
@ -115,7 +121,26 @@ export const getSubaccountOpenOrdersBySideAndPrice = createSelector(
);
/**
* @param state
* @returns the clientId of the latest order
*/
export const getLatestOrderClientId = createSelector([getLatestOrder], (order) => order?.clientId);
/**
* @returns the rawValue status of the latest order
*/
export const getLatestOrderStatus = createSelector(
[getLatestOrder],
(order) => order?.status.rawValue
);
/**
* @returns a list of clientIds belonging to uncommmited orders
*/
export const getUncommittedOrderClientIds = (state: RootState) =>
state.account.uncommittedOrderClientIds;
/**
* @param orderId
* @returns order details with the given orderId
*/
export const getOrderDetails = (orderId: string) =>
@ -283,21 +308,6 @@ export const getIsAccountConnected = (state: RootState) =>
*/
export const getOnboardingGuards = (state: RootState) => state.account.onboardingGuards;
/**
* @param state
* @returns a boolean indicating whether the user has uncommitted orders
*/
export const calculateHasUncommittedOrders = (state: RootState) => {
return !!state.account.uncommittedOrderClientIds?.length;
};
/**
* @param state
* @returns List of uncommitted order clientIds
*/
export const getUncommittedOrderClientIds = (state: RootState) =>
state.account.uncommittedOrderClientIds;
/**
* @param state
* @returns Whether there are unseen fill updates

View File

@ -1,39 +0,0 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import type { SubaccountOrder } from '@/constants/abacus';
import { removeUncommittedOrderClientId, setSubaccount } from '@/state/account';
import { getSubaccountOrders, getUncommittedOrderClientIds } from '@/state/accountSelectors';
export default (store: any) => (next: any) => async (action: PayloadAction<any>) => {
const { type, payload } = action;
const state = store.getState();
next(action);
switch (type) {
case setSubaccount.type: {
const updatedOrders = payload?.orders?.toArray() || [];
const orders = getSubaccountOrders(state);
const uncommittedOrderClientIds = getUncommittedOrderClientIds(state);
if (uncommittedOrderClientIds?.length && updatedOrders?.length !== orders?.length) {
const updatedOrdersClientIds = Object.fromEntries(
updatedOrders.map((order: SubaccountOrder) => [order.clientId, order])
);
const receivedClientId = uncommittedOrderClientIds.find(
(clientId) => updatedOrdersClientIds[clientId]
);
if (receivedClientId) {
store.dispatch(removeUncommittedOrderClientId(receivedClientId));
}
}
break;
}
default: {
break;
}
}
};

View File

@ -62,8 +62,10 @@ export const AccountInfoConnectedState = () => {
const { buyingPower, equity, marginUsage, leverage } = subAccount || {};
const hasDiff =
marginUsage?.postOrder != null &&
!MustBigNumber(marginUsage?.postOrder).eq(MustBigNumber(marginUsage?.current));
(marginUsage?.postOrder !== null &&
!MustBigNumber(marginUsage?.postOrder).eq(MustBigNumber(marginUsage?.current))) ||
(buyingPower?.postOrder !== null &&
!MustBigNumber(buyingPower?.postOrder).eq(MustBigNumber(buyingPower?.current)));
const showHeader = !hasDiff && !isTablet;

View File

@ -15,14 +15,17 @@ export const MarketLinks = () => {
const linkItems = [
{
key: 'coinmarketcap',
href: coinMarketCapsLink,
icon: IconName.CoinMarketCap,
},
{
key: 'whitepaper',
href: whitepaperLink,
icon: IconName.Whitepaper,
},
{
key: 'project-website',
href: websiteLink,
icon: IconName.Website,
},
@ -31,8 +34,8 @@ export const MarketLinks = () => {
return (
<Styled.MarketLinks>
{linkItems.map(
({ href, icon }) =>
href && <IconButton key={href} href={href} iconName={icon} type={ButtonType.Link} />
({ key, href, icon }) =>
href && <IconButton key={key} href={href} iconName={icon} type={ButtonType.Link} />
)}
</Styled.MarketLinks>
);

View File

@ -7,7 +7,7 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { AbacusOrderStatus, AbacusOrderTypes, type Nullable } from '@/constants/abacus';
import { ButtonAction } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { STRING_KEYS, type StringKey } from '@/constants/localization';
import { AssetIcon } from '@/components/AssetIcon';
import { Button } from '@/components/Button';
@ -47,7 +47,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
const { cancelOrder } = useSubaccount();
const {
asset = {},
asset,
cancelReason,
createdAtMilliseconds,
expiresAtMilliseconds,
@ -57,7 +57,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
price,
reduceOnly,
totalFilled,
resources = {},
resources,
size,
status,
stepSizeDecimals,
@ -121,7 +121,9 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
{
key: 'cancel-reason',
label: stringGetter({ key: STRING_KEYS.CANCEL_REASON }),
value: cancelReason ? stringGetter({ key: STRING_KEYS[cancelReason] }) : undefined,
value: cancelReason
? stringGetter({ key: STRING_KEYS[cancelReason as StringKey] })
: undefined,
},
{
key: 'amount',
@ -182,8 +184,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => {
const onCancelClick = async () => {
setIsPlacingCancel(true);
await cancelOrder({ orderId });
setIsPlacingCancel(false);
await cancelOrder({ orderId, onError: () => setIsPlacingCancel(false) });
};
const onClearClick = () => {

View File

@ -17,7 +17,7 @@ import { Ring } from '@/components/Ring';
import { ToggleGroup } from '@/components/ToggleGroup';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getInputTradeData } from '@/state/inputsSelectors';
import { getInputTradeData, getInputTradeOptions } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { getSelectedTradeType } from '@/lib/tradeData';
@ -35,6 +35,15 @@ export const TradeDialog = ({ isOpen, setIsOpen, slotTrigger }: ElementProps) =>
const currentTradeData = useSelector(getInputTradeData, shallowEqual);
const { type } = currentTradeData || {};
const selectedTradeType = getSelectedTradeType(type);
const { typeOptions } = useSelector(getInputTradeOptions, shallowEqual) ?? {};
const allTradeTypeItems = typeOptions?.toArray()?.map(({ type, stringKey }) => ({
value: type,
label: stringGetter({
key: stringKey,
}),
slotBefore: <AssetIcon symbol={symbol} />,
}));
const [currentStep, setCurrentStep] = useState<MobilePlaceOrderSteps>(
MobilePlaceOrderSteps.EditOrder
@ -62,11 +71,7 @@ export const TradeDialog = ({ isOpen, setIsOpen, slotTrigger }: ElementProps) =>
[MobilePlaceOrderSteps.EditOrder]: {
title: (
<Styled.ToggleGroup
items={[TradeTypes.LIMIT, TradeTypes.MARKET].map((tradeType: TradeTypes) => ({
value: tradeType,
label: stringGetter({ key: TRADE_TYPE_STRINGS[tradeType].tradeTypeKey }),
slotBefore: <AssetIcon symbol={symbol} />,
}))}
items={allTradeTypeItems}
value={selectedTradeType}
onValueChange={(tradeType: TradeTypes) =>
onTradeTypeChange(tradeType || selectedTradeType)
@ -126,6 +131,8 @@ Styled.Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>`
`;
Styled.ToggleGroup = styled(ToggleGroup)`
overflow-x: auto;
button[data-state='off'] {
gap: 0;

View File

@ -1,14 +1,19 @@
import { type FormEvent, useEffect, useState } from 'react';
import { type FormEvent, useCallback, useEffect, useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { ClosePositionInputField } from '@/constants/abacus';
import {
ClosePositionInputField,
type HumanReadablePlaceOrderPayload,
type Nullable,
} from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { TOKEN_DECIMALS } from '@/constants/numbers';
import { STRING_KEYS } from '@/constants/localization';
import { MobilePlaceOrderSteps } from '@/constants/trade';
import { useBreakpoints, useIsFirstRender, useStringGetter, useSubaccount } from '@/hooks';
import { useOnLastOrderIndexed } from '@/hooks/useOnLastOrderIndexed';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
@ -27,10 +32,7 @@ import { Orderbook, orderbookMixins, type OrderbookScrollBehavior } from '@/view
import { PositionPreview } from '@/views/forms/TradeForm/PositionPreview';
import {
calculateHasUncommittedOrders,
getCurrentMarketPositionData,
} from '@/state/accountSelectors';
import { getCurrentMarketPositionData } from '@/state/accountSelectors';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getInputClosePositionData } from '@/state/inputsSelectors';
@ -82,9 +84,7 @@ export const ClosePositionForm = ({
const { stepSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {};
const { size: sizeData, summary } = useSelector(getInputClosePositionData, shallowEqual) || {};
const { size, percent } = sizeData || {};
const hasUncommittedOrders = useSelector(calculateHasUncommittedOrders);
const currentInput = useSelector(getCurrentInput);
const currentPositionData = useSelector(getCurrentMarketPositionData, shallowEqual);
const { size: currentPositionSize } = currentPositionData || {};
const { current: currentSize } = currentPositionSize || {};
@ -106,17 +106,22 @@ export const ClosePositionForm = ({
}
}, [currentInput, market, currentStep]);
useEffect(() => {
// close has been placed
if (!isFirstRender && !hasUncommittedOrders) {
const onLastOrderIndexed = useCallback(() => {
if (!isFirstRender) {
abacusStateManager.clearClosePositionInputValues({ shouldFocusOnTradeInput: true });
onClosePositionSuccess?.();
if (currentStep === MobilePlaceOrderSteps.PlacingOrder) {
setCurrentStep?.(MobilePlaceOrderSteps.Confirmation);
}
setIsClosingPosition(false);
}
}, [hasUncommittedOrders]);
}, [currentStep, isFirstRender]);
const { setUnIndexedClientId } = useOnLastOrderIndexed({
callback: onLastOrderIndexed,
});
const onAmountInput = ({ floatValue }: { floatValue?: number }) => {
if (currentSize == null) return;
@ -165,14 +170,16 @@ export const ClosePositionForm = ({
setIsClosingPosition(true);
await closePosition({
onError: (errorParams?: { errorStringKey?: string }) => {
onError: (errorParams?: { errorStringKey?: Nullable<string> }) => {
setClosePositionError(
stringGetter({ key: errorParams?.errorStringKey || STRING_KEYS.SOMETHING_WENT_WRONG })
);
setIsClosingPosition(false);
},
onSuccess: (placeOrderPayload: Nullable<HumanReadablePlaceOrderPayload>) => {
setUnIndexedClientId(placeOrderPayload?.clientId);
},
});
setIsClosingPosition(false);
};
const alertMessage = closePositionError && (

View File

@ -1,4 +1,4 @@
import { type FormEvent, useState, useEffect, Ref } from 'react';
import { type FormEvent, useState, Ref, useCallback } from 'react';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import type { NumberFormatValues, SourceInfo } from 'react-number-format';
@ -7,6 +7,8 @@ import { AlertType } from '@/constants/alerts';
import {
ErrorType,
type HumanReadablePlaceOrderPayload,
type Nullable,
TradeInputErrorAction,
TradeInputField,
ValidationError,
@ -19,6 +21,7 @@ import { InputErrorData, TradeBoxKeys, MobilePlaceOrderSteps } from '@/constants
import { breakpoints } from '@/styles';
import { useStringGetter, useSubaccount } from '@/hooks';
import { useOnLastOrderIndexed } from '@/hooks/useOnLastOrderIndexed';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
@ -34,7 +37,6 @@ import { WithTooltip } from '@/components/WithTooltip';
import { Orderbook } from '@/views/tables/Orderbook';
import { calculateHasUncommittedOrders } from '@/state/accountSelectors';
import { getCurrentInput, useTradeFormData } from '@/state/inputsSelectors';
import { getCurrentMarketConfig } from '@/state/perpetualsSelectors';
@ -97,7 +99,6 @@ export const TradeForm = ({
} = useTradeFormData();
const { limitPrice, triggerPrice, trailingPercent } = price || {};
const hasUncommittedOrders = useSelector(calculateHasUncommittedOrders);
const currentInput = useSelector(getCurrentInput);
const { tickSizeDecimals, stepSizeDecimals } =
useSelector(getCurrentMarketConfig, shallowEqual) || {};
@ -154,30 +155,33 @@ export const TradeForm = ({
}
};
useEffect(() => {
// order has been placed
if (
(!currentStep || currentStep === MobilePlaceOrderSteps.PlacingOrder) &&
!hasUncommittedOrders
) {
const onLastOrderIndexed = useCallback(() => {
if (!currentStep || currentStep === MobilePlaceOrderSteps.PlacingOrder) {
setIsPlacingOrder(false);
abacusStateManager.clearTradeInputValues({ shouldResetSize: true });
setCurrentStep?.(MobilePlaceOrderSteps.Confirmation);
}
}, [currentStep, hasUncommittedOrders]);
}, [currentStep]);
const { setUnIndexedClientId } = useOnLastOrderIndexed({
callback: onLastOrderIndexed,
});
const onPlaceOrder = async () => {
setPlaceOrderError(undefined);
setIsPlacingOrder(true);
await placeOrder({
onError: (errorParams?: { errorStringKey?: string }) => {
onError: (errorParams?: { errorStringKey?: Nullable<string> }) => {
setPlaceOrderError(
stringGetter({ key: errorParams?.errorStringKey || STRING_KEYS.SOMETHING_WENT_WRONG })
);
setIsPlacingOrder(false);
},
onSuccess: (placeOrderPayload?: Nullable<HumanReadablePlaceOrderPayload>) => {
setUnIndexedClientId(placeOrderPayload?.clientId);
},
});
};

View File

@ -136,7 +136,7 @@ export const PlaceOrderButtonAndReceipt = ({
const buttonState = currentStep
? buttonStatesPerStep[currentStep].buttonState
: { isDisabled: !shouldEnableTrade, isLoading };
: { isDisabled: !shouldEnableTrade || isLoading, isLoading };
return (
<WithDetailsReceipt detailItems={items}>

View File

@ -1,5 +1,5 @@
import { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useCallback, useEffect, useState } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import styled, { AnyStyledComponent } from 'styled-components';
import { AbacusOrderStatus, type OrderStatus } from '@/constants/abacus';
@ -29,8 +29,7 @@ export const OrderActionsCell = ({ orderId, status, isDisabled }: ElementProps)
const onCancel = useCallback(async () => {
setIsCanceling(true);
await cancelOrder({ orderId });
setIsCanceling(false);
await cancelOrder({ orderId, onError: () => setIsCanceling(false) });
}, []);
return (