From a953d7eb5bbd6787574a138b7d7a9eefa5801059 Mon Sep 17 00:00:00 2001 From: Rui Date: Mon, 2 Dec 2024 13:03:10 -0800 Subject: [PATCH] Fix exception when closing a position of > 100% margin usage --- build.gradle.kts | 2 +- .../TradeInputMarketOrderCalculator.kt | 40 ++++++- .../state/app/adaptors/V4TransactionErrors.kt | 28 +++-- .../v2/manager/AsyncAbacusStateManagerV2.kt | 112 ++++++++++++++++-- v4_abacus.podspec | 2 +- 5 files changed, 157 insertions(+), 27 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7a79915ea..87ce6351f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.13.35" +version = "1.13.36" repositories { google() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt index 380be1dbd..b0365f4aa 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt @@ -109,7 +109,12 @@ internal class TradeInputMarketOrderCalculator() { val tradeSize = trade.size val freeCollateral = subaccount?.calculated?.get(CalculationPeriod.current)?.freeCollateral - if (tradeSize != null && tradeSide != null && freeCollateral != null && freeCollateral > Numeric.double.ZERO) { + val freeCollateralCondition = if (trade.reduceOnly) { + true + } else { + freeCollateral != null && freeCollateral > Numeric.double.ZERO + } + if (tradeSize != null && tradeSide != null && freeCollateral != null && freeCollateralCondition) { val maxMarketLeverage = market?.perpetualMarket?.configs?.maxMarketLeverage ?: Numeric.double.ONE val targetLeverage = trade.targetLeverage val marginMode = trade.marginMode ?: MarginMode.Cross @@ -269,7 +274,14 @@ internal class TradeInputMarketOrderCalculator() { if (marginMode == MarginMode.Isolated && !isTradeSameSide) { // For isolated margin orders where the user is trading on the opposite side of their currentPosition, the balancePercent represents a percentage of their current position rather than freeCollateral val desiredSize = existingPositionSize.abs() * balancePercent - return createMarketOrderFromSize(size = desiredSize, existingPositionNotionalSize = existingPositionNotionalSize, isTradeSameSide = isTradeSameSide, freeCollateral = freeCollateral, tradeLeverage = tradeLeverage, orderbook = orderbook) + return createMarketOrderFromSize( + size = desiredSize, + existingPositionNotionalSize = existingPositionNotionalSize, + isTradeSameSide = isTradeSameSide, + freeCollateral = freeCollateral, + tradeLeverage = tradeLeverage, + orderbook = orderbook, + ) } val cappedPercent = min(balancePercent, MAX_FREE_COLLATERAL_BUFFER_PERCENT) @@ -398,7 +410,13 @@ internal class TradeInputMarketOrderCalculator() { break@orderbookLoop } } - val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = existingPositionNotionalSize, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) + val balancePercentTotal = calculateBalancePercentFromUsdcSize( + usdcSize = usdcSizeTotal, + freeCollateral = freeCollateral, + positionSize = existingPositionNotionalSize, + tradeLeverage = tradeLeverage, + isTradeSameSide = isTradeSameSide, + ) createMarketOrderWith( orderbook = marketOrderOrderBook, size = sizeTotal, @@ -471,7 +489,13 @@ internal class TradeInputMarketOrderCalculator() { } } } - val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = existingPositionNotionalSize, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) + val balancePercentTotal = calculateBalancePercentFromUsdcSize( + usdcSize = usdcSizeTotal, + freeCollateral = freeCollateral, + positionSize = existingPositionNotionalSize, + tradeLeverage = tradeLeverage, + isTradeSameSide = isTradeSameSide, + ) createMarketOrderWith( orderbook = marketOrderOrderBook, size = sizeTotal, @@ -657,7 +681,13 @@ internal class TradeInputMarketOrderCalculator() { break@orderbookLoop } } - val balancePercentTotal = calculateBalancePercentFromUsdcSize(usdcSize = usdcSizeTotal, freeCollateral = freeCollateral, positionSize = positionSizeNotional, tradeLeverage = tradeLeverage, isTradeSameSide = isTradeSameSide) + val balancePercentTotal = calculateBalancePercentFromUsdcSize( + usdcSize = usdcSizeTotal, + freeCollateral = freeCollateral, + positionSize = positionSizeNotional, + tradeLeverage = tradeLeverage, + isTradeSameSide = isTradeSameSide, + ) return createMarketOrderWith( orderbook = marketOrderOrderBook, size = sizeTotal, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/app/adaptors/V4TransactionErrors.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/app/adaptors/V4TransactionErrors.kt index bcfc3fa4c..3f7a881a6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/app/adaptors/V4TransactionErrors.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/app/adaptors/V4TransactionErrors.kt @@ -13,9 +13,9 @@ class V4TransactionErrors { return if (code != null) { if (code != 0 && codespace != null) { ParsingError( - ParsingErrorType.BackendError, - message ?: "Unknown error", - "ERRORS.BROADCAST_ERROR_${codespace.uppercase()}_$code", + type = ParsingErrorType.BackendError, + message = message ?: "Unknown error", + stringKey = "ERRORS.BROADCAST_ERROR_${codespace.uppercase()}_$code", ) } else { null @@ -23,14 +23,22 @@ class V4TransactionErrors { } else if (message?.startsWith(QUERY_RESULT_ERROR_PREFIX) == true) { parseQueryResultErrorFromMessage(message) } else { - ParsingError(ParsingErrorType.BackendError, message ?: "Unknown error", null) + ParsingError( + type = ParsingErrorType.BackendError, + message = message ?: "Unknown error", + stringKey = null, + ) } } private fun parseQueryResultErrorFromMessage(message: String): ParsingError { // Workaround: Regex match different query results until protocol can return codespace/code parseSubaccountUpdateError(message)?.let { return it } - return ParsingError(ParsingErrorType.BackendError, "Unknown query result error", null) + return ParsingError( + type = ParsingErrorType.BackendError, + message = "Unknown query result error", + stringKey = null, + ) } private fun parseSubaccountUpdateError(message: String): ParsingError? { @@ -38,9 +46,9 @@ class V4TransactionErrors { return matchResult?.groups?.get(1)?.value?.let { val matchedUpdateResult = SubaccountUpdateFailedResult.invoke(it) ParsingError( - ParsingErrorType.BackendError, - "Subaccount update error: $it", - if (matchedUpdateResult != null) "ERRORS.QUERY_ERROR_SUBACCOUNTS_${matchedUpdateResult.toString().uppercase()}" else null, + type = ParsingErrorType.BackendError, + message = "Subaccount update error: $it", + stringKey = if (matchedUpdateResult != null) "ERRORS.QUERY_ERROR_SUBACCOUNTS_${matchedUpdateResult.toString().uppercase()}" else null, ) } } @@ -48,8 +56,8 @@ class V4TransactionErrors { fun parseErrorFromRawLog(rawLog: String): ParsingError? { return if (rawLog.startsWith(OUT_OF_GAS_ERROR_RAW_LOG_PREFIX)) { return ParsingError( - ParsingErrorType.BackendError, - "Out of gas: inaccurate gas estimation for transaction", + type = ParsingErrorType.BackendError, + message = "Out of gas: inaccurate gas estimation for transaction", ) } else { null diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt index 2b7ecfea4..bee8d3c1b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt @@ -16,6 +16,7 @@ import exchange.dydx.abacus.protocols.ThreadingType import exchange.dydx.abacus.protocols.TransactionCallback import exchange.dydx.abacus.protocols.asTypedObject import exchange.dydx.abacus.protocols.readCachedTextFile +import exchange.dydx.abacus.responses.ParsingError import exchange.dydx.abacus.state.app.adaptors.V4TransactionErrors import exchange.dydx.abacus.state.app.helper.DynamicLocalizer import exchange.dydx.abacus.state.manager.ApiData @@ -49,6 +50,7 @@ import exchange.dydx.abacus.utils.DummyFormatter import exchange.dydx.abacus.utils.DummyLocalizer import exchange.dydx.abacus.utils.IList import exchange.dydx.abacus.utils.IOImplementations +import exchange.dydx.abacus.utils.JsonEncoder import exchange.dydx.abacus.utils.Logger import exchange.dydx.abacus.utils.Parser import exchange.dydx.abacus.utils.ProtocolNativeImpFactory @@ -506,43 +508,103 @@ class AsyncAbacusStateManagerV2( } override fun placeOrderPayload(): HumanReadablePlaceOrderPayload? { - return adaptor?.placeOrderPayload() + try { + return adaptor?.placeOrderPayload() + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("placeOrderPayload", error) + throw e + } } override fun closePositionPayload(): HumanReadablePlaceOrderPayload? { - return adaptor?.closePositionPayload() + try { + return adaptor?.closePositionPayload() + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("closePositionPayload", error) + throw e + } } override fun cancelOrderPayload(orderId: String): HumanReadableCancelOrderPayload? { - return adaptor?.cancelOrderPayload(orderId) + try { + return adaptor?.cancelOrderPayload(orderId) + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("cancelOrderPayload", error) + throw e + } } override fun cancelAllOrdersPayload(marketId: String?): HumanReadableCancelAllOrdersPayload? { - return adaptor?.cancelAllOrdersPayload(marketId) + try { + return adaptor?.cancelAllOrdersPayload(marketId) + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("cancelAllOrdersPayload", error) + throw e + } } override fun closeAllPositionsPayload(): HumanReadableCloseAllPositionsPayload? { - return adaptor?.closeAllPositionsPayload() + try { + return adaptor?.closeAllPositionsPayload() + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("closeAllPositionsPayload", error) + throw e + } } override fun triggerOrdersPayload(): HumanReadableTriggerOrdersPayload? { - return adaptor?.triggerOrdersPayload() + try { + return adaptor?.triggerOrdersPayload() + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("triggerOrdersPayload", error) + throw e + } } override fun adjustIsolatedMarginPayload(): HumanReadableSubaccountTransferPayload? { - return adaptor?.adjustIsolatedMarginPayload() + try { + return adaptor?.adjustIsolatedMarginPayload() + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("adjustIsolatedMarginPayload", error) + throw e + } } override fun depositPayload(): HumanReadableDepositPayload? { - return adaptor?.depositPayload() + try { + return adaptor?.depositPayload() + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("depositPayload", error) + throw e + } } override fun withdrawPayload(): HumanReadableWithdrawPayload? { - return adaptor?.withdrawPayload() + try { + return adaptor?.withdrawPayload() + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("withdrawPayload", error) + throw e + } } override fun subaccountTransferPayload(): HumanReadableSubaccountTransferPayload? { - return adaptor?.subaccountTransferPayload() + try { + return adaptor?.subaccountTransferPayload() + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("subaccountTransferPayload", error) + throw e + } } override fun commitPlaceOrder(callback: TransactionCallback): HumanReadablePlaceOrderPayload? { @@ -550,6 +612,7 @@ class AsyncAbacusStateManagerV2( adaptor?.commitPlaceOrder(callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("commitPlaceOrder", error) callback(false, error, null) null } @@ -560,6 +623,7 @@ class AsyncAbacusStateManagerV2( adaptor?.commitTriggerOrders(callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("commitTriggerOrders", error) callback(false, error, null) null } @@ -570,6 +634,7 @@ class AsyncAbacusStateManagerV2( adaptor?.commitAdjustIsolatedMargin(callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("commitAdjustIsolatedMargin", error) callback(false, error, null) null } @@ -580,6 +645,7 @@ class AsyncAbacusStateManagerV2( adaptor?.commitClosePosition(callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("commitClosePosition", error) callback(false, error, null) null } @@ -594,6 +660,7 @@ class AsyncAbacusStateManagerV2( adaptor?.commitTransfer(callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("commitTransfer", error) callback(false, error, null) } } @@ -603,6 +670,7 @@ class AsyncAbacusStateManagerV2( adaptor?.commitCCTPWithdraw(callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("commitCCTPWithdraw", error) callback(false, error, null) } } @@ -612,6 +680,7 @@ class AsyncAbacusStateManagerV2( adaptor?.faucet(amount, callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("faucet", error) callback(false, error, null) } } @@ -621,6 +690,7 @@ class AsyncAbacusStateManagerV2( adaptor?.cancelOrder(orderId, callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("cancelOrder", error) callback(false, error, null) } } @@ -630,6 +700,7 @@ class AsyncAbacusStateManagerV2( adaptor?.cancelAllOrders(marketId, callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("cancelAllOrders", error) callback(false, error, null) } } @@ -639,6 +710,7 @@ class AsyncAbacusStateManagerV2( adaptor?.closeAllPositions(callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("closeAllPositions", error) callback(false, error, null) null } @@ -649,10 +721,30 @@ class AsyncAbacusStateManagerV2( adaptor?.triggerCompliance(action, callback) } catch (e: Exception) { val error = V4TransactionErrors.error(null, e.toString()) + trackTransactionError("triggerCompliance", error) callback(false, error, null) } } + private fun trackTransactionError(functionName: String, error: ParsingError?) { + if (error == null) { + return + } + + val params = mapOf( + "functionName" to functionName, + "errorType" to error.type.rawValue, + "errorMessage" to error.message, + "stackTrace" to error.stackTrace, + ) + ioImplementations.threading?.async(ThreadingType.main) { + ioImplementations.tracking?.log( + event = "ClientTransactionError", + data = JsonEncoder().encode(params), + ) + } + } + // Bridge functions. // If client is not using cancelOrder function, it should call orderCanceled function with // payload from v4-client to process state diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 29e1387b5..6652cdeb9 100644 --- a/v4_abacus.podspec +++ b/v4_abacus.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'v4_abacus' - spec.version = '1.13.35' + spec.version = '1.13.36' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''