8000 fix: misc trade fixes 4 by tyleroooo · Pull Request #1685 · dydxprotocol/v4-web · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

fix: misc trade fixes 4 #1685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/bonsai/forms/trade/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
TradeFormType,
} from './types';

export const DEFAULT_TRADE_TYPE = TradeFormType.MARKET;

const DEFAULT_GOOD_TIL_TIME: GoodUntilTime = {
duration: '28',
unit: TimeUnit.DAY,
Expand Down Expand Up @@ -52,7 +54,7 @@ export function getTradeFormFieldStates(
});

const defaults: Required<TradeForm> = {
type: TradeFormType.LIMIT,
type: DEFAULT_TRADE_TYPE,
marketId: '',
side: OrderSide.BUY,
size: OrderSizeInputs.SIZE({ value: '' }),
Expand Down
3 changes: 2 additions & 1 deletion src/bonsai/forms/trade/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createVanillaReducer } from '../../lib/forms';
import { DEFAULT_TRADE_TYPE } from './fields';
import {
ExecutionType,
MarginMode,
Expand All @@ -11,7 +12,7 @@ import {
} from './types';

const getMinimumRequiredFields = (
type: TradeFormType = TradeFormType.LIMIT,
type: TradeFormType = DEFAULT_TRADE_TYPE,
marketId?: string
): TradeForm => {
// Base form only includes type
Expand Down
30 changes: 7 additions & 23 deletions src/bonsai/forms/trade/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ import { AttemptNumber, MAX_INT_ROUGHLY, MustBigNumber } from '@/lib/numbers';
import { isPresent } from '@/lib/typeUtils';

import { PlaceOrderMarketInfo, PlaceOrderPayload } from '../triggers/types';
import { getTradeFormFieldStates, isFieldStateEnabled, isFieldStateRelevant } from './fields';
import {
DEFAULT_TRADE_TYPE,
getTradeFormFieldStates,
isFieldStateEnabled,
isFieldStateRelevant,
} from './fields';
import { calculateTradeInfo } from './tradeInfo';
import {
ExecutionType,
Expand Down Expand Up @@ -264,7 +269,7 @@ export function calculateTradeSummary(
export function getErrorTradeSummary(marketId?: string | undefined): TradeFormSummary {
return {
effectiveTrade: {
type: TradeFormType.LIMIT,
type: DEFAULT_TRADE_TYPE,
marketId,
side: undefined,
size: undefined,
Expand Down Expand Up @@ -564,27 +569,6 @@ export function tradeFormTypeToOrderType(tradeFormType: TradeFormType): OrderTyp
}
}

export function orderTypeToTradeFormType(orderType: OrderType): TradeFormType {
switch (orderType) {
case OrderType.MARKET:
return TradeFormType.MARKET;
case OrderType.LIMIT:
return TradeFormType.LIMIT;
case OrderType.STOP_MARKET:
return TradeFormType.STOP_MARKET;
case OrderType.STOP_LIMIT:
return TradeFormType.STOP_LIMIT;
case OrderType.TAKE_PROFIT_MARKET:
return TradeFormType.TAKE_PROFIT_MARKET;
case OrderType.TAKE_PROFIT_LIMIT:
// Note: Handling the naming difference in reverse
return TradeFormType.TAKE_PROFIT_LIMIT;
default:
assertNever(orderType);
return TradeFormType.MARKET;
}
}

export function getGoodTilInSeconds(goodTil: GoodUntilTime | undefined) {
if (goodTil == null) {
return undefined;
Expand Down
18 changes: 9 additions & 9 deletions src/bonsai/forms/triggers/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { STRING_KEYS } from '@/constants/localization';
import { IndexerPerpetualPositionStatus, IndexerPositionSide } from '@/types/indexer/indexerApiGen';

import { calc, mapIfPresent } from '@/lib/do';
import { isTruthy } from '@/lib/isTruthy';
import { AttemptBigNumber, AttemptNumber, MustBigNumber, MustNumber } from '@/lib/numbers';
import { isPresent } from '@/lib/typeUtils';

Expand Down Expand Up @@ -104,11 +105,7 @@ export function getErrors(
validationErrors.push(...validateCustomSize(state.size.size, inputData));
}

if (
summary.payload == null ||
(summary.payload.cancelOrderPayloads.length === 0 &&
summary.payload.placeOrderPayloads.length === 0)
) {
if (summary.payload == null || summary.payload.payloads.length === 0) {
validationErrors.push(errors.noPayload());
}

Expand Down Expand Up @@ -293,9 +290,12 @@ function validateTriggerOrderPayloadForEquityTiers(
payload: TriggerOrdersPayload,
inputData: TriggerOrderInputData
) {
const deletedOrderIds = new Set(payload.cancelOrderPayloads.map((c) => c.orderId));
const subaccountToUse = payload.placeOrderPayloads[0]?.subaccountNumber;
if (subaccountToUse == null) {
const deletedOrderIds = new Set(
payload.payloads.map((c) => c.cancelPayload?.orderId).filter(isTruthy)
);
const placeOrders = payload.payloads.map((p) => p.placePayload).filter(isPresent);
const subaccountToUse = placeOrders[0]?.subaccountNumber;
if (placeOrders.length === 0 || subaccountToUse == null) {
return [];
}
if (inputData.allOpenOrders == null || inputData.equityTiers == null) {
Expand Down Expand Up @@ -327,7 +327,7 @@ function validateTriggerOrderPayloadForEquityTiers(
return [errors.cantCalculateEquityTier()];
}

if (relevantOpenOrders.length + payload.placeOrderPayloads.length > myEquityTierLimit.maxOrders) {
if (relevantOpenOrders.length + placeOrders.length > myEquityTierLimit.maxOrders) {
return [
errors.equityTierError(
subaccountEquity,
Expand Down
27 changes: 10 additions & 17 deletions src/bonsai/forms/triggers/summary.ts
1E11 F438
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
PlaceOrderMarketInfo,
PlaceOrderPayload,
SummaryData,
TriggerOrderActions,
TriggerOrderDetails,
TriggerOrderInputData,
TriggerOrdersFormState,
Expand Down Expand Up @@ -57,9 +58,7 @@ function calculateTriggerOrderPayload(
return undefined;
}

const placeOrderPayloads: PlaceOrderPayload[] = [];
const cancelOrderPayloads: CancelOrderPayload[] = [];

const payloads: TriggerOrdersPayload['payloads'] = [];
if (
state.stopLossOrder.orderId != null ||
state.stopLossOrder.priceInput != null ||
Expand All @@ -79,9 +78,9 @@ function calculateTriggerOrderPayload(
if (actions === undefined) {
return undefined;
}

if (actions.cancelPayload) cancelOrderPayloads.push(actions.cancelPayload);
if (actions.placePayload) placeOrderPayloads.push(actions.placePayload);
if (actions.cancelPayload != null || actions.placePayload != null) {
payloads.push(actions);
}
}

if (
Expand All @@ -103,27 +102,21 @@ function calculateTriggerOrderPayload(
if (actions === undefined) {
return undefined;
}

if (actions.cancelPayload) cancelOrderPayloads.push(actions.cancelPayload);
if (actions.placePayload) placeOrderPayloads.push(actions.placePayload);
if (actions.cancelPayload != null || actions.placePayload != null) {
payloads.push(actions);
}
}

// Only return a payload if there's at least one action to take
if (placeOrderPayloads.length === 0 && cancelOrderPayloads.length === 0) {
if (payloads.length === 0) {
return undefined;
}

return {
placeOrderPayloads,
cancelOrderPayloads,
payloads,
};
}

interface TriggerOrderActions {
cancelPayload?: CancelOrderPayload;
placePayload?: PlaceOrderPayload;
}

function getTriggerOrderActions(
isStopLoss: boolean,
triggerOrderState: TriggerOrderState,
Expand Down
8 changes: 6 additions & 2 deletions src/bonsai/forms/triggers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@ export interface TriggerOrderInputData {
allOpenOrders?: SubaccountOrder[];
}

export interface TriggerOrderActions {
cancelPayload?: CancelOrderPayload;
placePayload?: PlaceOrderPayload;
}

export interface TriggerOrdersPayload {
placeOrderPayloads: PlaceOrderPayload[];
cancelOrderPayloads: CancelOrderPayload[];
payloads: TriggerOrderActions[];
}

export interface TriggerOrderDetails {
Expand Down
53 changes: 31 additions & 22 deletions src/bonsai/lifecycles/cancelTriggerOrdersLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { IndexerOrderSide, IndexerPositionSide } from '@/types/indexer/indexerAp
import { type RootStore } from '@/state/_store';
import { createAppSelector } from '@/state/appTypes';

import { runFn } from '@/lib/do';
import { calc, runFn } from '@/lib/do';
import { TimeEjectingSet } from '@/lib/timeEjectingSet';
import { isPresent } from '@/lib/typeUtils';

Expand All @@ -16,7 +16,6 @@ import { isOperationFailure } from '../lib/operationResult';
import { logBonsaiError, logBonsaiInfo } from '../logs';
import { BonsaiCore } from '../ontology';
import { createValidatorStoreEffect } from '../rest/lib/indexerQueryStoreEffect';
import { selectParentSubaccountOpenPositions } from '../selectors/account';
import { selectTxAuthorizedAccount } from '../selectors/accountTransaction';
import { OrderFlags, OrderStatus } from '../types/summaryTypes';

Expand All @@ -31,31 +30,41 @@ export function setUpCancelOrphanedTriggerOrdersLifecycle(store: RootStore) {
[
selectTxAuthorizedAccount,
BonsaiCore.account.openOrders.data,
selectParentSubaccountOpenPositions,
BonsaiCore.account.parentSubaccountPositions.data,
BonsaiCore.account.openOrders.loading,
BonsaiCore.account.parentSubaccountPositions.loading,
],
(txAuthorizedAccount, orders, positions) => {
(txAuthorizedAccount, orders, positions, ordersLoading, positionsLoading) => {
if (!txAuthorizedAccount || orders.length === 0) {
return undefined;
}

const groupedPositions = keyBy(positions, (o) => o.uniqueId);

const filteredOrders = orders.filter((o) => {
const isConditionalOrder = o.orderFlags === OrderFlags.CONDITIONAL;
const isReduceOnly = o.reduceOnly;
const isActiveOrder = o.status === OrderStatus.Open || o.status === OrderStatus.Untriggered;
return isConditionalOrder && isReduceOnly && isActiveOrder;
});

// Add orders to cancel if they are orphaned, or if the reduce-only order would increase the position
const ordersToCancel = filteredOrders.filter((o) => {
const position = groupedPositions[o.positionUniqueId];
const isOrphan = position == null;
const hasInvalidReduceOnlyOrder =
(position?.side === IndexerPositionSide.LONG && o.side === IndexerOrderSide.BUY) ||
(position?.side === IndexerPositionSide.SHORT && o.side === IndexerOrderSide.SELL);

return isOrphan || hasInvalidReduceOnlyOrder;
const ordersToCancel = calc(() => {
if (ordersLoading !== 'success' || positionsLoading !== 'success') {
return [];
}
const groupedPositions = keyBy(positions, (o) => o.uniqueId);

const filteredOrders = orders.filter((o) => {
const isConditionalOrder = o.orderFlags === OrderFlags.CONDITIONAL;
const isReduceOnly = o.reduceOnly;
const isActiveOrder =
o.status === OrderStatus.Open || o.status === OrderStatus.Untriggered;
return isConditionalOrder && isReduceOnly && isActiveOrder;
});

// Add orders to cancel if they are orphaned, or if the reduce-only order would increase the position
const cancelOrders = filteredOrders.filter((o) => {
const position = groupedPositions[o.positionUniqueId];
const isOrphan = position == null;
const hasInvalidReduceOnlyOrder =
(position?.side === IndexerPositionSide.LONG && o.side === IndexerOrderSide.BUY) ||
(position?.side === IndexerPositionSide.SHORT && o.side === IndexerOrderSide.SELL);

return isOrphan || hasInvalidReduceOnlyOrder;
});

return cancelOrders;
});

const { localDydxWallet, sourceAccount, parentSubaccountInfo } = txAuthorizedAccount;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,16 @@
BonsaiCore.account.childSubaccountSummaries.data,
BonsaiCore.account.parentSubaccountPositions.data,
selectUserHasUsdcGasForTransaction,
BonsaiCore.account.openOrders.loading,
],
(
authorizedAccount,
openOrders,
localPlaceOrders,
childSubaccountSummaries,
parentSubaccountPositions,
userHasUsdcGasForTransaction
userHasUsdcGasForTransaction,
ordersLoading
) => {
if (
!authorizedAccount ||
Expand Down Expand Up @@ -77,7 +79,7 @@

return {
subaccountNumber,
equity: summary.equity ?? BIG_NUMBERS.ZERO,

Check warning on line 82 in src/bonsai/lifecycles/reclaimChildSubaccountBalancesLifecycle.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined
};
})
.filter(isPresent);
Expand All @@ -96,7 +98,7 @@
submissionStatus === PlaceOrderStatuses.Submitted
);

if (!hasUsdc || !hasNoOrders || hasLocalPlaceOrders) {
if (!hasUsdc || !hasNoOrders || hasLocalPlaceOrders || ordersLoading !== 'success') {
return undefined;
}

Expand Down
45 changes: 23 additions & 22 deletions src/hooks/useSubaccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { accountTransactionManager } from '@/bonsai/AccountTransactionSupervisor
import { SubaccountTransferPayload } from '@/bonsai/forms/adjustIsolatedMargin';
import { TransferPayload, TransferToken } from '@/bonsai/forms/transfers';
import { TriggerOrdersPayload } from '@/bonsai/forms/triggers/types';
import { wrapOperationFailure, wrapOperationSuccess } from '@/bonsai/lib/operationResult';
import {
isOperationFailure,
wrapOperationFailure,
wrapOperationSuccess,
WrappedOperationFailureError,
} from '@/bonsai/lib/operationResult';
import { logBonsaiError, logBonsaiInfo } from '@/bonsai/logs';
import { BonsaiCore } from '@/bonsai/ontology';
import type { EncodeObject } from '@cosmjs/proto-signing';
Expand Down Expand Up @@ -32,7 +37,6 @@ import { assertNever } from '@/lib/assertNever';
import { stringifyTransactionError } from '@/lib/errors';
import { isTruthy } from '@/lib/isTruthy';
import { parseToPrimitives } from '@/lib/parseToPrimitives';
import { SerialTaskExecutor } from '@/lib/serialExecutor';
import { log } from '@/lib/telemetry';

import { useAccounts } from './useAccounts';
Expand All @@ -54,8 +58,6 @@ export const SubaccountProvider = ({ ...props }) => {

export const useSubaccount = () => useContext(SubaccountContext);

const chainTxExecutor = new SerialTaskExecutor();

const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWallet }) => {
const dispatch = useAppDispatch();
const { usdcDenom, usdcDecimals, chainTokenDecimals } = useTokenConfigs();
Expand Down Expand Up @@ -270,25 +272,24 @@ const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWall

// ------ Trigger Orders Methods ------ //
const placeTriggerOrders = useCallback(async (payload: TriggerOrdersPayload) => {
const { placeOrderPayloads, cancelOrderPayloads } = payload;

const cancels = cancelOrderPayloads.map(async (cancelOrderPayload) => {
const res = await chainTxExecutor.enqueue(() =>
accountTransactionManager.cancelOrder({ orderId: cancelOrderPayload.orderId })
);
return res;
});

const places = placeOrderPayloads.map(async (placeOrderPayload) => {
const res = await chainTxExecutor.enqueue(() =>
accountTransactionManager.placeOrder(placeOrderPayload)
);
return res;
// can assume promise fulfills if all operations are successful, otherwise throws
const operations = payload.payloads.map(async (operationPayload) => {
if (operationPayload.cancelPayload?.orderId) {
const res = await accountTransactionManager.cancelOrder({
orderId: operationPayload.cancelPayload.orderId,
});
if (isOperationFailure(res)) {
throw new WrappedOperationFailureError(res);
}
}
if (operationPayload.placePayload != null) {
const res = await accountTransactionManager.placeOrder(operationPayload.placePayload);
if (isOperationFailure(res)) {
throw new WrappedOperationFailureError(res);
}
}
});
// return promises should only resolve when all are done
const cancelResults = await Promise.all([...cancels, ...places]);
const placeResults = await Promise.all([...cancels, ...places]);
return { cancelResults, placeResults };
return operations;
}, []);

// ------ Listing Method ------ //
Expand Down
Loading
0