Skip to content
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: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
"@cosmjs/stargate": "^0.32.1",
"@cosmjs/tendermint-rpc": "^0.32.1",
"@datadog/browser-logs": "^5.23.3",
"@dydxprotocol/v4-client-js": "3.5.0",
"@dydxprotocol/v4-localization": "1.1.394",
"@dydxprotocol/v4-client-js": "3.6.0",
"@dydxprotocol/v4-localization": "1.1.396",
"@dydxprotocol/v4-proto": "^7.0.0-dev.0",
"@emotion/is-prop-valid": "^1.3.0",
"@hugocxl/react-to-image": "^0.0.9",
Expand Down
28 changes: 14 additions & 14 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 69 additions & 24 deletions src/bonsai/AccountTransactionSupervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
OrderType,
SubaccountClient,
} from '@dydxprotocol/v4-client-js';
import { isEmpty } from 'lodash';

import {
AnalyticsEvents,
Expand Down Expand Up @@ -1076,13 +1077,17 @@ export class AccountTransactionSupervisor {
// if order is not compound, just do placeOrder so metrics are clean
order.orderPayload != null &&
(order.orderPayload.transferToSubaccountAmount ?? 0) <= 0 &&
(order.triggersPayloads ?? []).length === 0
(order.triggersPayloads ?? []).length === 0 &&
isEmpty(order.scaleOrderPayloads)
) {
return this.placeOrder(order.orderPayload, source);
}

// Handle stateful main order + trigger orders together in bulk
const hasStatefulOperations = isMainOrderStateful || (order.triggersPayloads?.length ?? 0) > 0;
// Handle stateful main order + trigger orders + scale orders together in bulk
const hasStatefulOperations =
isMainOrderStateful ||
(order.triggersPayloads?.length ?? 0) > 0 ||
!isEmpty(order.scaleOrderPayloads);

if (hasStatefulOperations) {
const maybeDydxLocalWallet = await this.getCosmosLocalWallet();
Expand All @@ -1092,6 +1097,7 @@ export class AccountTransactionSupervisor {
payload: {
mainOrderPayload: isMainOrderStateful ? order.orderPayload : undefined,
triggersPayloads: order.triggersPayloads ?? [],
scaleOrderPayloads: order.scaleOrderPayloads ?? [],
source,
},
})
Expand Down Expand Up @@ -1138,39 +1144,52 @@ export class AccountTransactionSupervisor {
if (context.payload.mainOrderPayload) {
const mainPayload = context.payload.mainOrderPayload;

const sourceSubaccount = getSubaccountId(this.store.getState());
const sourceAddress = getUserWalletAddress(this.store.getState());
const mainTransfer = getIsolatedMarginTransfer(
mainPayload,
sourceSubaccount,
sourceAddress
);
// Check if we need a transfer for isolated margin
if (
mainPayload.transferToSubaccountAmount != null &&
mainPayload.transferToSubaccountAmount > 0
mainPayload.transferToSubaccountAmount > 0 &&
mainTransfer == null
) {
const sourceSubaccount = getSubaccountId(this.store.getState());
const sourceAddress = getUserWalletAddress(this.store.getState());

if (sourceSubaccount == null || sourceAddress == null) {
return createMiddlewareFailureResult(
wrapSimpleError(
context.fnName,
'unknown parent subaccount number or address',
STRING_KEYS.SOMETHING_WENT_WRONG
),
context
);
}

transferPayload = {
fromSubaccount: sourceSubaccount,
toSubaccount: mainPayload.subaccountNumber,
amount: mainPayload.transferToSubaccountAmount,
address: sourceAddress,
};
return createMiddlewareFailureResult(
wrapSimpleError(
context.fnName,
'unknown parent subaccount number or address',
STRING_KEYS.SOMETHING_WENT_WRONG
),
context
);
}
transferPayload = mainTransfer ?? transferPayload;

placePayloads.push({
...mainPayload,
currentHeight,
});
}

// Process scale order payloads
context.payload.scaleOrderPayloads.forEach((scalePayload) => {
if (transferPayload == null) {
transferPayload = getIsolatedMarginTransfer(
scalePayload,
getSubaccountId(this.store.getState()),
getUserWalletAddress(this.store.getState())
);
}

placePayloads.push({
...scalePayload,
currentHeight,
});
});

// Process trigger payloads
context.payload.triggersPayloads.forEach((operationPayload) => {
if (operationPayload.cancelPayload?.orderId) {
Expand Down Expand Up @@ -1675,6 +1694,32 @@ class SimpleEvent<T> {
}
}

type IsolatedMarginTransferPayload = {
fromSubaccount: number;
toSubaccount: number;
amount: number;
address: string;
};

function getIsolatedMarginTransfer(
payload: PlaceOrderPayload,
sourceSubaccount: number | undefined,
sourceAddress: string | undefined
): IsolatedMarginTransferPayload | undefined {
if (payload.transferToSubaccountAmount == null || payload.transferToSubaccountAmount <= 0) {
return undefined;
}
if (sourceSubaccount == null || sourceAddress == null) {
return undefined;
}
return {
fromSubaccount: sourceSubaccount,
toSubaccount: payload.subaccountNumber,
amount: payload.transferToSubaccountAmount,
address: sourceAddress,
};
}

function isShortTermOrderPayload(payload: PlaceOrderPayload) {
if (payload.type === OrderType.MARKET) {
return true;
Expand Down
89 changes: 86 additions & 3 deletions src/bonsai/forms/trade/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import {
} from '@/bonsai/lib/validationErrors';
import { OrderFlags, OrderStatus } from '@/bonsai/types/summaryTypes';
import { OrderType } from '@dydxprotocol/v4-client-js';
import { isEmpty } from 'lodash';

import { STRING_KEYS } from '@/constants/localization';
import { timeUnits } from '@/constants/time';
import {
MAX_SCALE_ORDERS,
MAX_SCALE_SKEW,
MIN_SCALE_ORDERS,
MIN_SCALE_SKEW,
} from '@/constants/trade';
import {
IndexerOrderSide,
IndexerOrderType,
Expand Down Expand Up @@ -280,6 +287,75 @@ function validateFieldsBasic(
}
}

if (options.needsScaleStartPrice) {
const startPrice = AttemptNumber(state.scaleStartPrice) ?? 0;
if (startPrice <= 0) {
errors.push(
simpleValidationError({
code: 'REQUIRED_SCALE_START_PRICE',
type: ErrorType.error,
fields: ['scaleStartPrice'],
titleKey: STRING_KEYS.ENTER_LIMIT_PRICE,
})
);
}
}

if (options.needsScaleEndPrice) {
const endPrice = AttemptNumber(state.scaleEndPrice) ?? 0;
if (endPrice <= 0) {
errors.push(
simpleValidationError({
code: 'REQUIRED_SCALE_END_PRICE',
type: ErrorType.error,
fields: ['scaleEndPrice'],
titleKey: STRING_KEYS.ENTER_LIMIT_PRICE,
})
);
}
}

if (options.needsScaleTotalOrders) {
const totalOrders = AttemptNumber(state.scaleTotalOrders) ?? 0;
if (
totalOrders < MIN_SCALE_ORDERS ||
totalOrders > MAX_SCALE_ORDERS ||
!Number.isInteger(totalOrders)
) {
errors.push(
simpleValidationError({
code: 'REQUIRED_SCALE_TOTAL_ORDERS',
type: ErrorType.error,
fields: ['scaleTotalOrders'],
titleKey: STRING_KEYS.TOTAL_ORDERS,
textKey: STRING_KEYS.TOTAL_ORDERS_MUST_BE_WITHIN_RANGE,
textParams: {
MIN: { value: MIN_SCALE_ORDERS },
MAX: { value: MAX_SCALE_ORDERS },
},
})
);
}
}

if (options.needsScaleSkew) {
const skew = AttemptNumber(state.scaleSkew) ?? 0;
if (skew < MIN_SCALE_SKEW || skew > MAX_SCALE_SKEW) {
errors.push(
simpleValidationError({
code: 'REQUIRED_SCALE_SKEW',
type: ErrorType.error,
fields: ['scaleSkew'],
titleKey: STRING_KEYS.SKEW,
textParams: {
MIN: { value: MIN_SCALE_SKEW },
MAX: { value: MAX_SCALE_SKEW },
},
})
);
}
}

if (state.side == null) {
errors.push(
simpleValidationError({
Expand Down Expand Up @@ -374,6 +450,7 @@ function validateAdvancedTradeConditions(
// For limit orders, validate isolated margin requirements
} else if (
state.type === TradeFormType.LIMIT ||
state.type === TradeFormType.SCALE ||
state.type === TradeFormType.TRIGGER_LIMIT ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
state.type === TradeFormType.TRIGGER_MARKET
Expand Down Expand Up @@ -945,7 +1022,11 @@ function validateTradeFormSummaryFields(summary: TradeFormSummary): ValidationEr
errors.push(simpleValidationError({ code: 'MISSING_TRADE_PAYLOAD' }));
}

if (summary.tradeInfo.inputSummary.size?.size == null || summary.tradeInfo.payloadPrice == null) {
const hasPriceInfo = !isEmpty(summary.tradePayload?.scaleOrderPayloads)
? summary.tradeInfo.startPrice != null && summary.tradeInfo.endPrice != null
: summary.tradeInfo.payloadPrice != null;

if (summary.tradeInfo.inputSummary.size?.size == null || !hasPriceInfo) {
errors.push(simpleValidationError({ code: 'MISSING__METRICS' }));
}

Expand Down Expand Up @@ -997,8 +1078,10 @@ function validateEquityTiers(inputData: TradeFormInputData, summary: TradeFormSu
summary.effectiveTrade.timeInForce === TimeInForce.IOC);

const ordersToOpen =
(isShortTerm ? 0 : 1) +
(summary.tradePayload?.triggersPayloads?.filter((t) => t.placePayload != null).length ?? 0);
summary.effectiveTrade.type === TradeFormType.SCALE // Scale order types will only contain scaleOrderPayloads in the payload
? (summary.tradePayload?.scaleOrderPayloads?.length ?? 0)
: (isShortTerm ? 0 : 1) +
(summary.tradePayload?.triggersPayloads?.filter((t) => t.placePayload != null).length ?? 0);

if (ordersToOpen <= 0) {
return [];
Expand Down
30 changes: 30 additions & 0 deletions src/bonsai/forms/trade/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export function getTradeFormFieldStates(
goodTil: DEFAULT_GOOD_TIL_TIME,
stopLossOrder: undefined,
takeProfitOrder: undefined,
scaleStartPrice: '',
scaleEndPrice: '',
scaleTotalOrders: '5',
scaleSkew: '1',
};

// Initialize all fields as not visible
Expand Down Expand Up @@ -215,6 +219,32 @@ export function getTradeFormFieldStates(

// Execution is fixed for stop market
forceValueAndDisable(result.execution, ExecutionType.IOC);
return result;
case TradeFormType.SCALE:
makeVisible(result, [
'marketId',
'side',
'size',
'marginMode',
'scaleStartPrice',
'scaleEndPrice',
'scaleTotalOrders',
'scaleSkew',
'timeInForce',
'postOnly',
'reduceOnly',
]);
defaultSizeIfSizeInputIsInvalid(result);
setMarginMode(result);

// goodTil is only visible for GTT
if (result.timeInForce.effectiveValue === TimeInForce.GTT) {
makeVisible(result, ['goodTil']);
forceValueAndDisable(result.reduceOnly, false);
} else if (result.timeInForce.effectiveValue === TimeInForce.IOC) {
forceValueAndDisable(result.postOnly, false);
}

return result;
default:
assertNever(type);
Expand Down
Loading
Loading