diff --git a/build.gradle.kts b/build.gradle.kts index ad5134c4e..40bcab85f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,7 +49,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.7.4" +version = "1.7.5" repositories { google() diff --git a/detekt.yml b/detekt.yml index 47438651d..33960f594 100644 --- a/detekt.yml +++ b/detekt.yml @@ -4,6 +4,9 @@ naming: # /exchange -> /dydx -> /abacus # didn't seem worth the potential thrash in PRs to fix (feel free to fix if you feel differently) active: false + MatchingDeclarationName: + # Affects a lot of the TradingStateMachine+_.kt files + active: false complexity: CognitiveComplexMethod: diff --git a/docs/API/Actions.md b/docs/API/Actions.md index 55d3b00ff..34ac44059 100644 --- a/docs/API/Actions.md +++ b/docs/API/Actions.md @@ -240,4 +240,30 @@ Take profit order trigger price's percentage difference from the position's aver ### takeProfitUsdcDiff -Take profit order trigger price's usdc difference from the position's average entry price \ No newline at end of file +Take profit order trigger price's usdc difference from the position's average entry price + +# AdjustIsolatedMargin + + fun adjustIsolatedMargin(data: String?, type: AdjustIsolatedMarginInputField?): AppStateResponse + +The input state is in `response.state.input.adjustIsolatedMargin` as a [AdjustIsolatedMarginInput](../Input/AdjustIsolatedMarginInput.md). + +### data + +Data input in string format + +## AdjustIsolatedMarginInputField + +### Type + +IsolatedMarginAdjustmentType +Add - Add margin to the child's isolated margin account from the parent's cross margin account +Remove - Remove margin from the child's isolated margin account to the parent's cross margin account + +### Amount + +Amount of USDC to remove or add + +### ChildSubaccountNumber + +Subaccount number for the child whose margin is to be adjusted diff --git a/docs/Input/AdjustIsolatedMarginInput.md b/docs/Input/AdjustIsolatedMarginInput.md new file mode 100644 index 000000000..07719558a --- /dev/null +++ b/docs/Input/AdjustIsolatedMarginInput.md @@ -0,0 +1,21 @@ +# AdjustIsolatedMarginInput + +data class AdjustIsolatedMarginInput( + val type: String?, // "ADD" or "REMOVE" + val amount: Double?, + val childSubaccountNumber: Int?, +) + +## type + +ADD - Add margin to the child's isolated margin account from the parent's cross margin account +REMOVE - Remove margin from the child's isolated margin account to the parent's cross margin account + +## amount + +Amount of USDC to remove or add + +## childSubaccountNumber + +Subaccount number for the child whose margin is to be adjusted + diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AdjustIsolatedMarginInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AdjustIsolatedMarginInputCalculator.kt new file mode 100644 index 000000000..113feec6a --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AdjustIsolatedMarginInputCalculator.kt @@ -0,0 +1,136 @@ +package exchange.dydx.abacus.calculator + +import exchange.dydx.abacus.output.input.IsolatedMarginAdjustmentType +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.utils.Numeric +import exchange.dydx.abacus.utils.mutable +import exchange.dydx.abacus.utils.safeSet + +@Suppress("UNCHECKED_CAST") +internal class AdjustIsolatedMarginInputCalculator(val parser: ParserProtocol) { + private val subaccountTransformer = SubaccountTransformer() + + internal fun calculate( + state: Map, + parentSubaccountNumber: Int?, + ): Map { + val wallet = parser.asNativeMap(state["wallet"]) + val isolatedMarginAdjustment = parser.asNativeMap(state["adjustIsolatedMargin"]) + val childSubaccountNumber = parser.asInt(isolatedMarginAdjustment?.get("ChildSubaccountNumber")) + val type = parser.asString(isolatedMarginAdjustment?.get("Type"))?.let { + IsolatedMarginAdjustmentType.valueOf(it) + } ?: IsolatedMarginAdjustmentType.Add + + return if (wallet != null && isolatedMarginAdjustment != null && type != null) { + val modified = state.mutable() + val parentTransferDelta = getModifiedTransferDelta(isolatedMarginAdjustment, true) + val childTransferDelta = getModifiedTransferDelta(isolatedMarginAdjustment, false) + + val walletPostParentSubaccountTransfer = + subaccountTransformer.applyIsolatedMarginAdjustmentToWallet( + wallet, + subaccountNumber = parentSubaccountNumber, + parentTransferDelta, + parser, + "postOrder", + ) + + val walletPostChildSubaccountTransfer = + subaccountTransformer.applyIsolatedMarginAdjustmentToWallet( + wallet = walletPostParentSubaccountTransfer, + subaccountNumber = childSubaccountNumber, + childTransferDelta, + parser, + "postOrder", + ) + + val modifiedParentSubaccount = parser.asNativeMap(parser.value(walletPostChildSubaccountTransfer, "accounts.subaccounts.$parentSubaccountNumber")) + val modifiedChildSubaccount = parser.asNativeMap(parser.value(walletPostChildSubaccountTransfer, "accounts.subaccounts.$childSubaccountNumber")) + val modifiedIsolatedMarginAdjustment = finalize(isolatedMarginAdjustment, modifiedParentSubaccount, modifiedChildSubaccount, type) + + modified["adjustIsolatedMargin"] = modifiedIsolatedMarginAdjustment + modified["wallet"] = walletPostChildSubaccountTransfer + modified + } else { + state + } + } + + private fun getModifiedTransferDelta( + isolatedMarginAdjustment: Map, + isParentSubaccount: Boolean, + ): Map { + val type = parser.asString(isolatedMarginAdjustment["Type"])?.let { + IsolatedMarginAdjustmentType.valueOf(it) + } ?: IsolatedMarginAdjustmentType.Add + val amount = parser.asDouble(isolatedMarginAdjustment["Amount"]) + + when (type) { + IsolatedMarginAdjustmentType.Add -> { + val multiplier = + if (isParentSubaccount) Numeric.double.NEGATIVE else Numeric.double.POSITIVE + val usdcSize = (amount ?: Numeric.double.ZERO) * multiplier + + return mapOf( + "usdcSize" to usdcSize, + ) + } + + IsolatedMarginAdjustmentType.Remove -> { + val multiplier = + if (isParentSubaccount) Numeric.double.POSITIVE else Numeric.double.NEGATIVE + val usdcSize = (amount ?: Numeric.double.ZERO) * multiplier + + return mapOf( + "usdcSize" to usdcSize, + ) + } + } + } + + private fun summaryForType( + parentSubaccount: Map?, + childSubaccount: Map?, + type: IsolatedMarginAdjustmentType, + ): Map { + val summary = mutableMapOf() + val crossCollateral = parser.asDouble(parser.value(parentSubaccount, "freeCollateral.postOrder")) + val crossMarginUsage = parser.asDouble(parser.value(parentSubaccount, "marginUsage.postOrder")) + val openPositions = parser.asNativeMap(childSubaccount?.get("openPositions")) + val marketId = openPositions?.keys?.firstOrNull() + val positionMargin = parser.asDouble(parser.value(childSubaccount, "freeCollateral.postOrder")) + val positionLeverage = parser.asDouble(parser.value(childSubaccount, "openPositions.$marketId.leverage.postOrder")) + val liquidationPrice = parser.asDouble(parser.value(childSubaccount, "openPositions.$marketId.liquidationPrice.postOrder")) + + when (type) { + IsolatedMarginAdjustmentType.Add -> { + summary.safeSet("crossFreeCollateral", crossCollateral) + summary.safeSet("crossMarginUsage", crossMarginUsage) + summary.safeSet("positionMargin", positionMargin) + summary.safeSet("positionLeverage", positionLeverage) + summary.safeSet("liquidationPrice", liquidationPrice) + } + + IsolatedMarginAdjustmentType.Remove -> { + summary.safeSet("crossFreeCollateral", crossCollateral) + summary.safeSet("crossMarginUsage", crossMarginUsage) + summary.safeSet("positionMargin", positionMargin) + summary.safeSet("positionLeverage", positionLeverage) + summary.safeSet("liquidationPrice", liquidationPrice) + } + } + + return summary + } + + private fun finalize( + isolatedMarginAdjustment: Map, + parentSubaccount: Map?, + childSubaccount: Map?, + type: IsolatedMarginAdjustmentType, + ): Map { + val modified = isolatedMarginAdjustment.mutable() + modified.safeSet("summary", summaryForType(parentSubaccount, childSubaccount, type)) + return modified + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/SubaccountTransformer.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/SubaccountTransformer.kt index ab48261c1..6d61ad710 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/SubaccountTransformer.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/SubaccountTransformer.kt @@ -199,6 +199,26 @@ internal class SubaccountTransformer { } } + internal fun applyIsolatedMarginAdjustmentToWallet( + wallet: Map, + subaccountNumber: Int?, + delta: Map, + parser: ParserProtocol, + period: String + ): Map { + val key = "account.subaccounts.$subaccountNumber" + val subaccount = parser.asNativeMap(parser.value(wallet, key)) + + if (subaccount != null) { + val modifiedSubaccount = applyDeltaToSubaccount(subaccount, delta, parser, period) + val modifiedWallet = wallet.mutable() + modifiedWallet.safeSet(key, modifiedSubaccount) + return modifiedWallet + } + + return wallet + } + internal fun applyTradeToSubaccount( subaccount: Map?, trade: Map, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/AdjustIsolatedMarginInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/AdjustIsolatedMarginInput.kt new file mode 100644 index 000000000..c6dd2941a --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/AdjustIsolatedMarginInput.kt @@ -0,0 +1,170 @@ +package exchange.dydx.abacus.output.input + +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.utils.Logger +import kollections.JsExport +import kotlinx.serialization.Serializable + +@JsExport +@Serializable +data class AdjustIsolatedMarginInputOptions( + val needsSize: Boolean, +) { + companion object { + internal fun create( + existing: AdjustIsolatedMarginInputOptions?, + parser: ParserProtocol, + data: Map<*, *>? + ): AdjustIsolatedMarginInputOptions? { + Logger.d { "creating Adjust Isolated Margin Input Options\n" } + + data?.let { + val needsSize = parser.asBool(data["needsSize"]) ?: false + + return if (existing?.needsSize != needsSize + ) { + AdjustIsolatedMarginInputOptions(needsSize) + } else { + existing + } + } + Logger.d { "Adjust Isolated Margin Input Options not valid" } + return null + } + } +} + +@JsExport +@Serializable +data class AdjustIsolatedMarginInputSummary( + val crossFreeCollateral: Double?, + val crossMarginUsage: Double?, + val positionMargin: Double?, + val positionLeverage: Double?, + val liquidationPrice: Double?, +) { + companion object { + internal fun create( + existing: AdjustIsolatedMarginInputSummary?, + parser: ParserProtocol, + data: Map<*, *>? + ): AdjustIsolatedMarginInputSummary? { + Logger.d { "creating Adjust Isolated Margin Input Summary\n" } + + data?.let { + val crossFreeCollateral = parser.asDouble(data["crossFreeCollateral"]) + val crossMarginUsage = parser.asDouble(data["crossMarginUsage"]) + val positionMargin = parser.asDouble(data["positionMargin"]) + val positionLeverage = parser.asDouble(data["positionLeverage"]) + val liquidationPrice = parser.asDouble(data["liquidationPrice"]) + + return if ( + existing?.crossFreeCollateral != crossFreeCollateral || + existing?.crossMarginUsage != crossMarginUsage || + existing?.positionMargin != positionMargin || + existing?.positionLeverage != positionLeverage || + existing?.liquidationPrice != liquidationPrice + ) { + AdjustIsolatedMarginInputSummary( + crossFreeCollateral, + crossMarginUsage, + positionMargin, + positionLeverage, + liquidationPrice, + ) + } else { + existing + } + } + Logger.d { "Adjust Isolated Margin Input Summary not valid" } + return null + } + } +} + +@JsExport +@Serializable +enum class IsolatedMarginAdjustmentType { + Add, + Remove +} + +@JsExport +@Serializable +data class AdjustIsolatedMarginInput( + val type: IsolatedMarginAdjustmentType, + val amount: String?, + val childSubaccountNumber: Int?, + val adjustIsolatedMarginInputOptions: AdjustIsolatedMarginInputOptions?, + val summary: AdjustIsolatedMarginInputSummary?, + val errors: String?, + val errorMessage: String?, + val fee: Double?, +) { + companion object { + internal fun create( + existing: AdjustIsolatedMarginInput?, + parser: ParserProtocol, + data: Map<*, *>?, + ): AdjustIsolatedMarginInput? { + Logger.d { "creating Adjust Isolated Margin Input\n" } + + data?.let { + val type = parser.asString(data["Type"])?.let { + IsolatedMarginAdjustmentType.valueOf(it) + } ?: IsolatedMarginAdjustmentType.Add + + val childSubaccountNumber = parser.asInt(data["ChildSubaccountNumber"]) + val amount = parser.asString(data["Amount"]) + val fee = parser.asDouble(data["fee"]) + val adjustIsolatedMarginInputOptions = AdjustIsolatedMarginInputOptions.create( + existing?.adjustIsolatedMarginInputOptions, + parser, + parser.asMap(data["adjustIsolatedMarginInputOptions"]), + ) + val summary = AdjustIsolatedMarginInputSummary.create( + existing?.summary, + parser, + parser.asMap(data["summary"]), + ) + + val errors = parser.asString(data["errors"]) + + val errorMessage: String? = + if (errors != null) { + val errorArray = parser.decodeJsonArray(errors) + val firstError = parser.asMap(errorArray?.first()) + parser.asString(firstError?.get("message")) + } else { + null + } + + return if ( + existing?.type != type || + existing.amount != amount || + existing.childSubaccountNumber != childSubaccountNumber || + existing.adjustIsolatedMarginInputOptions != adjustIsolatedMarginInputOptions || + existing.summary !== summary || + existing.errors !== errors || + existing.errorMessage != errorMessage || + existing.fee != fee + ) { + AdjustIsolatedMarginInput( + type, + amount, + childSubaccountNumber, + adjustIsolatedMarginInputOptions, + summary, + errors, + errorMessage, + fee, + ) + } else { + existing + } + } + Logger.d { "Adjust Isolated Margin Input not valid" } + return null + } + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt index 4cdaa29eb..c3695b813 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt @@ -13,7 +13,8 @@ enum class InputType(val rawValue: String) { TRADE("trade"), CLOSE_POSITION("closePosition"), TRANSFER("transfer"), - TRIGGER_ORDERS("triggerOrders"); + TRIGGER_ORDERS("triggerOrders"), + ADJUST_ISOLATED_MARGIN("adjustIsolatedMargin"); companion object { operator fun invoke(rawValue: String?) = @@ -29,6 +30,7 @@ data class Input( val closePosition: ClosePositionInput?, val transfer: TransferInput?, val triggerOrders: TriggerOrdersInput?, + val adjustIsolatedMargin: AdjustIsolatedMarginInput?, val receiptLines: IList?, val errors: IList? ) { @@ -51,6 +53,8 @@ data class Input( TransferInput.create(existing?.transfer, parser, parser.asMap(data["transfer"]), environment) val triggerOrders = TriggerOrdersInput.create(existing?.triggerOrders, parser, parser.asMap(data["triggerOrders"])) + val adjustIsolatedMargin = + AdjustIsolatedMarginInput.create(existing?.adjustIsolatedMargin, parser, parser.asMap(data["adjustIsolatedMargin"])) val errors = ValidationError.create(existing?.errors, parser, parser.asList(data["errors"])) val receiptLines = ReceiptLine.create(parser, parser.asList(data["receiptLines"])) @@ -59,6 +63,7 @@ data class Input( existing?.closePosition !== closePosition || existing?.transfer !== transfer || existing?.triggerOrders !== triggerOrders || + existing?.adjustIsolatedMargin !== adjustIsolatedMargin || existing?.receiptLines != receiptLines || existing?.errors != errors ) { @@ -68,6 +73,7 @@ data class Input( closePosition, transfer, triggerOrders, + adjustIsolatedMargin, receiptLines, errors, ) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt index a2aec8a8a..abdc8cafc 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -728,20 +728,25 @@ enum class OrderTimeInForce(val rawValue: String) { @JsExport @Serializable enum class ReceiptLine(val rawValue: String) { - equity("EQUITY"), - buyingPower("BUYING_POWER"), - marginUsage("MARGIN_USAGE"), - expectedPrice("EXPECTED_PRICE"), - fee("FEE"), - total("TOTAL"), - walletBalance("WALLET_BALANCE"), - bridgeFee("BRIDGE_FEE"), - exchangeRate("EXCHANGE_RATE"), - exchangeReceived("EXCHANGE_RECEIVED"), - slippage("SLIPPAGE"), - gasFee("GAS_FEES"), - reward("REWARD"), - transferRouteEstimatedDuration("TRANSFER_ROUTE_ESTIMATE_DURATION"); + Equity("EQUITY"), + BuyingPower("BUYING_POWER"), + MarginUsage("MARGIN_USAGE"), + ExpectedPrice("EXPECTED_PRICE"), + Fee("FEE"), + Total("TOTAL"), + WalletBalance("WALLET_BALANCE"), + BridgeFee("BRIDGE_FEE"), + ExchangeRate("EXCHANGE_RATE"), + ExchangeReceived("EXCHANGE_RECEIVED"), + Slippage("SLIPPAGE"), + GasFee("GAS_FEES"), + Reward("REWARD"), + TransferRouteEstimatedDuration("TRANSFER_ROUTE_ESTIMATE_DURATION"), + CrossFreeCollateral("CROSS_FREE_COLLATERAL"), + CrossMarginUsage("CROSS_MARGIN_USAGE"), + PositionMargin("POSITION_MARGIN"), + PositionLeverage("POSITION_LEVERAGE"), + LiquidationPrice("LIQUIDATION_PRICE"); companion object { operator fun invoke(rawValue: String) = @@ -773,7 +778,7 @@ data class TradeInput( val reduceOnly: Boolean, val postOnly: Boolean, val fee: Double?, - val marginMode: MarginMode?, + val marginMode: MarginMode, val targetLeverage: Double?, val bracket: TradeInputBracket?, val marketOrder: TradeInputMarketOrder?, @@ -811,7 +816,7 @@ data class TradeInput( val marginMode = parser.asString(data["marginMode"])?.let { MarginMode.invoke(it) - } + } ?: MarginMode.cross val targetLeverage = parser.asDouble(data["targetLeverage"]) @@ -853,7 +858,7 @@ data class TradeInput( existing?.reduceOnly != reduceOnly || existing.postOnly != postOnly || existing.fee != fee || - existing?.marginMode != marginMode || + existing.marginMode != marginMode || existing?.targetLeverage != targetLeverage || existing.bracket != bracket || existing.marketOrder != marketOrder || diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManager.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManager.kt index 06fa57b2d..7647ad1a8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManager.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManager.kt @@ -14,6 +14,7 @@ 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.configs.V4StateManagerConfigs +import exchange.dydx.abacus.state.model.AdjustIsolatedMarginInputField import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.TransferInputField @@ -404,6 +405,10 @@ class AsyncAbacusStateManager( adaptor?.triggerOrders(data, type) } + override fun adjustIsolatedMargin(data: String?, type: AdjustIsolatedMarginInputField?) { + adaptor?.adjustIsolatedMargin(data, type) + } + override fun isMarketValid(marketId: String?): Boolean { return if (marketId == null) { true @@ -445,6 +450,10 @@ class AsyncAbacusStateManager( return adaptor?.withdrawPayload() } + override fun adjustIsolatedMarginPayload(): HumanReadableSubaccountTransferPayload? { + return adaptor?.adjustIsolatedMarginPayload() + } + override fun subaccountTransferPayload(): HumanReadableSubaccountTransferPayload? { return adaptor?.subaccountTransferPayload() } @@ -479,6 +488,16 @@ class AsyncAbacusStateManager( } } + override fun commitAdjustIsolatedMargin(callback: TransactionCallback): HumanReadableSubaccountTransferPayload? { + return try { + adaptor?.commitAdjustIsolatedMargin(callback) + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + callback(false, error, null) + null + } + } + override fun stopWatchingLastOrder() { adaptor?.stopWatchingLastOrder() } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt index 581093cfc..75b656adc 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt @@ -5,6 +5,7 @@ import exchange.dydx.abacus.output.Documentation import exchange.dydx.abacus.output.Restriction import exchange.dydx.abacus.output.input.SelectionOption import exchange.dydx.abacus.protocols.TransactionCallback +import exchange.dydx.abacus.state.model.AdjustIsolatedMarginInputField import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.TransferInputField @@ -38,6 +39,7 @@ interface AsyncAbacusStateManagerProtocol { fun closePosition(data: String?, type: ClosePositionInputField) fun transfer(data: String?, type: TransferInputField?) fun triggerOrders(data: String?, type: TriggerOrdersInputField?) + fun adjustIsolatedMargin(data: String?, type: AdjustIsolatedMarginInputField?) // helper functions fun isMarketValid(marketId: String?): Boolean @@ -61,11 +63,13 @@ interface AsyncAbacusStateManagerProtocol { fun depositPayload(): HumanReadableDepositPayload? fun withdrawPayload(): HumanReadableWithdrawPayload? fun subaccountTransferPayload(): HumanReadableSubaccountTransferPayload? + fun adjustIsolatedMarginPayload(): HumanReadableSubaccountTransferPayload? // Commit changes with input objects fun commitPlaceOrder(callback: TransactionCallback): HumanReadablePlaceOrderPayload? fun commitClosePosition(callback: TransactionCallback): HumanReadablePlaceOrderPayload? fun commitTriggerOrders(callback: TransactionCallback): HumanReadableTriggerOrdersPayload? + fun commitAdjustIsolatedMargin(callback: TransactionCallback): HumanReadableSubaccountTransferPayload? fun stopWatchingLastOrder() fun commitTransfer(callback: TransactionCallback) fun commitCCTPWithdraw(callback: TransactionCallback) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/StateManagerAdaptor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/StateManagerAdaptor.kt index 1795a65f0..25c55ce71 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/StateManagerAdaptor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/StateManagerAdaptor.kt @@ -33,6 +33,7 @@ import exchange.dydx.abacus.state.manager.configs.StateManagerConfigs import exchange.dydx.abacus.state.manager.utils.Address import exchange.dydx.abacus.state.manager.utils.DydxAddress import exchange.dydx.abacus.state.manager.utils.EvmAddress +import exchange.dydx.abacus.state.model.AdjustIsolatedMarginInputField import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.PerpTradingStateMachine import exchange.dydx.abacus.state.model.TradeInputField @@ -40,6 +41,7 @@ import exchange.dydx.abacus.state.model.TradingStateMachine import exchange.dydx.abacus.state.model.TransferInputField import exchange.dydx.abacus.state.model.TriggerOrdersInputField import exchange.dydx.abacus.state.model.account +import exchange.dydx.abacus.state.model.adjustIsolatedMargin import exchange.dydx.abacus.state.model.candles import exchange.dydx.abacus.state.model.closePosition import exchange.dydx.abacus.state.model.findOrder @@ -1774,6 +1776,21 @@ open class StateManagerAdaptor( } } + fun adjustIsolatedMargin( + data: String?, + type: AdjustIsolatedMarginInputField?, + ) { + ioImplementations.threading?.async(ThreadingType.abacus) { + val stateResponse = stateMachine.adjustIsolatedMargin(data, type, subaccountNumber) + ioImplementations.threading?.async(ThreadingType.main) { + stateNotification?.stateChanged( + stateResponse.state, + stateResponse.changes, + ) + } + } + } + internal open fun commitPlaceOrder(callback: TransactionCallback): HumanReadablePlaceOrderPayload? { callback(false, V4TransactionErrors.error(null, "Not implemented"), null) return null @@ -1784,6 +1801,11 @@ open class StateManagerAdaptor( return null } + internal open fun commitAdjustIsolatedMargin(callback: TransactionCallback): HumanReadableSubaccountTransferPayload? { + callback(false, V4TransactionErrors.error(null, "Not implemented"), null) + return null + } + internal open fun commitClosePosition(callback: TransactionCallback): HumanReadablePlaceOrderPayload? { callback(false, V4TransactionErrors.error(null, "Not implemented"), null) return null @@ -2206,6 +2228,27 @@ open class StateManagerAdaptor( } } + @Throws(Exception::class) + fun adjustIsolatedMarginPayload(): HumanReadableSubaccountTransferPayload { + val subaccount = stateMachine.state?.subaccount(subaccountNumber) + ?: error("subaccount is null") + val parentSubaccountNumber = subaccount.subaccountNumber + val wallet = stateMachine.state?.wallet ?: error("wallet is null") + val walletAddress = wallet.walletAddress ?: error("walletAddress is null") + val isolatedMarginAdjustment = stateMachine.state?.input?.adjustIsolatedMargin + ?: error("isolatedMarginAdjustment is null") + val amount = isolatedMarginAdjustment.amount ?: error("amount is null") + val childSubaccountNumber = isolatedMarginAdjustment.childSubaccountNumber + ?: error("childSubaccountNumber is null") + + return HumanReadableSubaccountTransferPayload( + parentSubaccountNumber, + amount, + walletAddress, + childSubaccountNumber, + ) + } + private fun updateTracking(changes: StateChanges) { if (changes.changes.contains(Changes.transfers)) { parseTransfersToMatchFaucetRecords() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt new file mode 100644 index 000000000..a902e6358 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt @@ -0,0 +1,141 @@ +package exchange.dydx.abacus.state.model + +import exchange.dydx.abacus.calculator.AdjustIsolatedMarginInputCalculator +import exchange.dydx.abacus.output.input.IsolatedMarginAdjustmentType +import exchange.dydx.abacus.responses.ParsingError +import exchange.dydx.abacus.responses.StateResponse +import exchange.dydx.abacus.state.changes.Changes +import exchange.dydx.abacus.state.changes.StateChanges +import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS +import exchange.dydx.abacus.utils.mutable +import exchange.dydx.abacus.utils.mutableMapOf +import exchange.dydx.abacus.utils.safeSet +import kollections.JsExport +import kollections.iListOf +import kotlinx.serialization.Serializable + +@JsExport +@Serializable +enum class AdjustIsolatedMarginInputField { + Type, + Amount, + ChildSubaccountNumber, +} + +fun TradingStateMachine.adjustIsolatedMargin( + data: String?, + type: AdjustIsolatedMarginInputField?, + parentSubaccountNumber: Int, +): StateResponse { + var changes: StateChanges? = null + var error: ParsingError? = null + + val input = this.input?.mutable() ?: mutableMapOf() + input["current"] = "adjustIsolatedMargin" + + val adjustIsolatedMargin = + parser.asMap(input["adjustIsolatedMargin"])?.mutable() + ?: kotlin.run { + val adjustIsolatedMargin = mutableMapOf() + val calculator = AdjustIsolatedMarginInputCalculator(parser) + val params = mutableMapOf() + params.safeSet("adjustIsolatedMargin", adjustIsolatedMargin) + params.safeSet("account", account) + params.safeSet("wallet", wallet) + + val modified = calculator.calculate( + state = params, + parentSubaccountNumber = parentSubaccountNumber, + ) + + parser.asMap(modified["adjustIsolatedMargin"])?.mutable() ?: adjustIsolatedMargin + } + val childSubaccountNumber = parser.asInt(adjustIsolatedMargin["ChildSubaccountNumber"]) + val subaccountNumbers = if (childSubaccountNumber != null) { + iListOf(parentSubaccountNumber, childSubaccountNumber) + } else { + iListOf(parentSubaccountNumber) + } + + if (type != null) { + if (validAdjustIsolatedMarginInput(adjustIsolatedMargin, parentSubaccountNumber, type.name)) { + when (type) { + AdjustIsolatedMarginInputField.Type -> { + if (adjustIsolatedMargin["Type"] != parser.asString(data)) { + adjustIsolatedMargin.safeSet(type.name, parser.asString(data)) + adjustIsolatedMargin.safeSet("Amount", null) + } + changes = StateChanges( + iListOf(Changes.wallet, Changes.subaccount, Changes.input), + null, + subaccountNumbers, + ) + } + AdjustIsolatedMarginInputField.Amount -> { + val amount = parser.asString(data) + adjustIsolatedMargin.safeSet(type.name, amount) + changes = StateChanges( + iListOf(Changes.wallet, Changes.subaccount, Changes.input), + null, + subaccountNumbers, + ) + } + AdjustIsolatedMarginInputField.ChildSubaccountNumber -> { + val childSubaccountNumber = parser.asInt(data) + adjustIsolatedMargin.safeSet(type.name, childSubaccountNumber) + val subaccountNumbers = if (childSubaccountNumber != null) { + iListOf(parentSubaccountNumber, childSubaccountNumber) + } else { + iListOf(parentSubaccountNumber) + } + changes = StateChanges( + iListOf(Changes.wallet, Changes.subaccount, Changes.input), + null, + subaccountNumbers, + ) + } + else -> {} + } + } else { + error = cannotModify(type.name) + } + } else { + changes = StateChanges( + iListOf(Changes.wallet, Changes.subaccount, Changes.input), + null, + subaccountNumbers, + ) + } + + input["adjustIsolatedMargin"] = adjustIsolatedMargin + this.input = input + changes?.let { update(it) } + return StateResponse(state, changes, if (error != null) iListOf(error) else null) +} + +fun TradingStateMachine.validAdjustIsolatedMarginInput( + adjustIsolatedMargin: Map, + parentSubaccountNumber: Int?, + typeText: String?, +): Boolean { + if (typeText == null) return false + + when (typeText) { + AdjustIsolatedMarginInputField.Type.name -> { + val typeString = parser.asString(adjustIsolatedMargin["Type"]) ?: return true + val type = IsolatedMarginAdjustmentType.valueOf(typeString) + return type == IsolatedMarginAdjustmentType.Add || type == IsolatedMarginAdjustmentType.Remove + } + AdjustIsolatedMarginInputField.Amount.name -> { + val amount = parser.asDouble(adjustIsolatedMargin["Amount"]) + return amount == null || amount > 0 + } + AdjustIsolatedMarginInputField.ChildSubaccountNumber.name -> { + val childSubaccountNumber = parser.asInt(adjustIsolatedMargin["ChildSubaccountNumber"]) + return childSubaccountNumber == null || childSubaccountNumber % NUM_PARENT_SUBACCOUNTS == parentSubaccountNumber + } + else -> { + return false + } + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 9dff7c88e..ac6af5afc 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -1,6 +1,7 @@ package exchange.dydx.abacus.state.model import exchange.dydx.abacus.calculator.AccountCalculator +import exchange.dydx.abacus.calculator.AdjustIsolatedMarginInputCalculator import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarketCalculator import exchange.dydx.abacus.calculator.TradeCalculation @@ -599,6 +600,10 @@ open class TradingStateMachine( calculateTriggerOrders(subaccountNumber) } + "adjustIsolatedMargin" -> { + calculateAdjustIsolatedMargin(subaccountNumber) + } + else -> {} } } @@ -714,6 +719,25 @@ open class TradingStateMachine( this.input = input } + private fun calculateAdjustIsolatedMargin(subaccountNumber: Int?) { + val input = this.input?.mutable() + val adjustIsolatedMargin = parser.asNativeMap(input?.get("adjustIsolatedMargin")) + val calculator = AdjustIsolatedMarginInputCalculator(parser) + val params = mutableMapOf() + params.safeSet("wallet", wallet) + params.safeSet("account", account) + params.safeSet("user", user) + params.safeSet("markets", parser.asNativeMap(marketsSummary?.get("markets"))) + params.safeSet("adjustIsolatedMargin", adjustIsolatedMargin) + + val modified = calculator.calculate(params, subaccountNumber) + this.setMarkets(parser.asNativeMap(modified["markets"])) + this.wallet = parser.asNativeMap(modified["wallet"]) + input?.safeSet("adjustIsolatedMargin", parser.asNativeMap(modified["adjustIsolatedMargin"])) + + this.input = input + } + private fun subaccount(subaccountNumber: Int): Map? { return parser.asNativeMap(parser.value(account, "subaccounts.$subaccountNumber")) } @@ -885,20 +909,20 @@ open class TradingStateMachine( return when (type) { "MARKET", "STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP" -> { listOf( - ReceiptLine.buyingPower.rawValue, - ReceiptLine.marginUsage.rawValue, - ReceiptLine.expectedPrice.rawValue, - ReceiptLine.fee.rawValue, - ReceiptLine.reward.rawValue, + ReceiptLine.BuyingPower.rawValue, + ReceiptLine.MarginUsage.rawValue, + ReceiptLine.ExpectedPrice.rawValue, + ReceiptLine.Fee.rawValue, + ReceiptLine.Reward.rawValue, ) } else -> { listOf( - ReceiptLine.buyingPower.rawValue, - ReceiptLine.marginUsage.rawValue, - ReceiptLine.fee.rawValue, - ReceiptLine.reward.rawValue, + ReceiptLine.BuyingPower.rawValue, + ReceiptLine.MarginUsage.rawValue, + ReceiptLine.Fee.rawValue, + ReceiptLine.Reward.rawValue, ) } } @@ -906,11 +930,11 @@ open class TradingStateMachine( "closePosition" -> { listOf( - ReceiptLine.buyingPower.rawValue, - ReceiptLine.marginUsage.rawValue, - ReceiptLine.expectedPrice.rawValue, - ReceiptLine.fee.rawValue, - ReceiptLine.reward.rawValue, + ReceiptLine.BuyingPower.rawValue, + ReceiptLine.MarginUsage.rawValue, + ReceiptLine.ExpectedPrice.rawValue, + ReceiptLine.Fee.rawValue, + ReceiptLine.Reward.rawValue, ) } @@ -920,22 +944,53 @@ open class TradingStateMachine( return when (type) { "DEPOSIT", "WITHDRAWAL" -> { listOf( - ReceiptLine.equity.rawValue, - ReceiptLine.buyingPower.rawValue, - ReceiptLine.exchangeRate.rawValue, - ReceiptLine.exchangeReceived.rawValue, - ReceiptLine.bridgeFee.rawValue, - ReceiptLine.fee.rawValue, - ReceiptLine.slippage.rawValue, - ReceiptLine.transferRouteEstimatedDuration.rawValue, + ReceiptLine.Equity.rawValue, + ReceiptLine.BuyingPower.rawValue, + ReceiptLine.ExchangeRate.rawValue, + ReceiptLine.ExchangeReceived.rawValue, + ReceiptLine.BridgeFee.rawValue, + ReceiptLine.Fee.rawValue, + ReceiptLine.Slippage.rawValue, + ReceiptLine.TransferRouteEstimatedDuration.rawValue, ) } "TRANSFER_OUT" -> { listOf( - ReceiptLine.equity.rawValue, - ReceiptLine.marginUsage.rawValue, - ReceiptLine.fee.rawValue, + ReceiptLine.Equity.rawValue, + ReceiptLine.MarginUsage.rawValue, + ReceiptLine.Fee.rawValue, + ) + } + + else -> { + listOf() + } + } + } + + "adjustIsolatedMargin" -> { + val adjustIsolatedMargin = parser.asNativeMap(input["adjustIsolatedMargin"]) ?: return null + val type = parser.asString(adjustIsolatedMargin["Type"]) ?: return null + + when (type) { + "ADD" -> { + listOf( + ReceiptLine.CrossFreeCollateral.rawValue, + ReceiptLine.CrossMarginUsage.rawValue, + ReceiptLine.PositionLeverage.rawValue, + ReceiptLine.PositionMargin.rawValue, + ReceiptLine.LiquidationPrice.rawValue, + ) + } + + "REMOVE" -> { + listOf( + ReceiptLine.CrossFreeCollateral.rawValue, + ReceiptLine.CrossMarginUsage.rawValue, + ReceiptLine.PositionLeverage.rawValue, + ReceiptLine.PositionMargin.rawValue, + ReceiptLine.LiquidationPrice.rawValue, ) } @@ -1150,7 +1205,7 @@ open class TradingStateMachine( val subaccountHistoricalPnlData = (subaccountHistoricalPnl(subaccountNumber) as? IList>)?.mutable() ?: mutableListOf() - val equity = parser.asDouble(parser.value(subaccount, "equity.current")) + if (subaccountHistoricalPnl?.size == 1) { // Check if the PNL was generated from equity val first = subaccountHistoricalPnl.firstOrNull() 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 bac742fe5..0dd6597a3 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 @@ -28,6 +28,7 @@ import exchange.dydx.abacus.state.manager.OrderbookGrouping import exchange.dydx.abacus.state.manager.SingletonAsyncAbacusStateManagerProtocol import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.state.manager.configs.V4StateManagerConfigs +import exchange.dydx.abacus.state.model.AdjustIsolatedMarginInputField import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.TransferInputField @@ -421,6 +422,10 @@ class AsyncAbacusStateManagerV2( adaptor?.triggerOrders(data, type) } + override fun adjustIsolatedMargin(data: String?, type: AdjustIsolatedMarginInputField?) { + adaptor?.adjustIsolatedMargin(data, type) + } + override fun isMarketValid(marketId: String?): Boolean { return if (marketId == null) { true @@ -460,6 +465,10 @@ class AsyncAbacusStateManagerV2( return adaptor?.triggerOrdersPayload() } + override fun adjustIsolatedMarginPayload(): HumanReadableSubaccountTransferPayload? { + return adaptor?.adjustIsolatedMarginPayload() + } + override fun depositPayload(): HumanReadableDepositPayload? { return adaptor?.depositPayload() } @@ -492,6 +501,16 @@ class AsyncAbacusStateManagerV2( } } + override fun commitAdjustIsolatedMargin(callback: TransactionCallback): HumanReadableSubaccountTransferPayload? { + return try { + adaptor?.commitAdjustIsolatedMargin(callback) + } catch (e: Exception) { + val error = V4TransactionErrors.error(null, e.toString()) + callback(false, error, null) + null + } + } + override fun commitClosePosition(callback: TransactionCallback): HumanReadablePlaceOrderPayload? { return try { adaptor?.commitClosePosition(callback) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt index bfeccf111..0d3192e8e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt @@ -35,6 +35,7 @@ import exchange.dydx.abacus.state.manager.NetworkState import exchange.dydx.abacus.state.manager.OrderbookGrouping import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.state.manager.configs.V4StateManagerConfigs +import exchange.dydx.abacus.state.model.AdjustIsolatedMarginInputField import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.PerpTradingStateMachine import exchange.dydx.abacus.state.model.TradeInputField @@ -52,10 +53,13 @@ import exchange.dydx.abacus.state.v2.supervisor.OnboardingSupervisor import exchange.dydx.abacus.state.v2.supervisor.SystemSupervisor import exchange.dydx.abacus.state.v2.supervisor.accountAddress import exchange.dydx.abacus.state.v2.supervisor.addressRestriction +import exchange.dydx.abacus.state.v2.supervisor.adjustIsolatedMargin +import exchange.dydx.abacus.state.v2.supervisor.adjustIsolatedMarginPayload import exchange.dydx.abacus.state.v2.supervisor.cancelOrder import exchange.dydx.abacus.state.v2.supervisor.cancelOrderPayload import exchange.dydx.abacus.state.v2.supervisor.closePosition import exchange.dydx.abacus.state.v2.supervisor.closePositionPayload +import exchange.dydx.abacus.state.v2.supervisor.commitAdjustIsolatedMargin import exchange.dydx.abacus.state.v2.supervisor.commitClosePosition import exchange.dydx.abacus.state.v2.supervisor.commitPlaceOrder import exchange.dydx.abacus.state.v2.supervisor.commitTriggerOrders @@ -481,6 +485,10 @@ internal class StateManagerAdaptorV2( accounts.triggerOrders(data, type) } + internal fun adjustIsolatedMargin(data: String?, type: AdjustIsolatedMarginInputField?) { + accounts.adjustIsolatedMargin(data, type) + } + internal fun placeOrderPayload(): HumanReadablePlaceOrderPayload? { return accounts.placeOrderPayload(currentHeight) } @@ -535,6 +543,14 @@ internal class StateManagerAdaptorV2( return accounts.commitClosePosition(currentHeight, callback) } + internal fun commitAdjustIsolatedMargin(callback: TransactionCallback): HumanReadableSubaccountTransferPayload? { + return accounts.commitAdjustIsolatedMargin(callback) + } + + internal fun adjustIsolatedMarginPayload(): HumanReadableSubaccountTransferPayload? { + return accounts.adjustIsolatedMarginPayload() + } + internal fun stopWatchingLastOrder() { accounts.stopWatchingLastOrder() } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt index 79c41df89..45a33d319 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt @@ -31,6 +31,7 @@ import exchange.dydx.abacus.state.manager.processingCctpWithdraw import exchange.dydx.abacus.state.manager.utils.Address import exchange.dydx.abacus.state.manager.utils.DydxAddress import exchange.dydx.abacus.state.manager.utils.EvmAddress +import exchange.dydx.abacus.state.model.AdjustIsolatedMarginInputField import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.TradingStateMachine @@ -959,6 +960,13 @@ internal fun AccountSupervisor.triggerOrders(data: String?, type: TriggerOrdersI subaccount?.triggerOrders(data, type) } +internal fun AccountSupervisor.adjustIsolatedMargin( + data: String?, + type: AdjustIsolatedMarginInputField?, +) { + subaccount?.adjustIsolatedMargin(data, type) +} + internal fun AccountSupervisor.placeOrderPayload(currentHeight: Int?): HumanReadablePlaceOrderPayload? { return subaccount?.placeOrderPayload(currentHeight) } @@ -971,6 +979,10 @@ internal fun AccountSupervisor.triggerOrdersPayload(currentHeight: Int?): HumanR return subaccount?.triggerOrdersPayload(currentHeight) } +internal fun AccountSupervisor.adjustIsolatedMarginPayload(): HumanReadableSubaccountTransferPayload? { + return subaccount?.adjustIsolatedMarginPayload() +} + internal fun AccountSupervisor.cancelOrderPayload(orderId: String): HumanReadableCancelOrderPayload? { return subaccount?.cancelOrderPayload(orderId) } @@ -1001,6 +1013,12 @@ internal fun AccountSupervisor.commitTriggerOrders( return subaccount?.commitTriggerOrders(currentHeight, callback) } +internal fun AccountSupervisor.commitAdjustIsolatedMargin( + callback: TransactionCallback +): HumanReadableSubaccountTransferPayload? { + return subaccount?.commitAdjustIsolatedMargin(callback) +} + internal fun AccountSupervisor.commitClosePosition( currentHeight: Int?, callback: TransactionCallback diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountsSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountsSupervisor.kt index b00133f9e..8bbbf7ba9 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountsSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountsSupervisor.kt @@ -21,6 +21,7 @@ import exchange.dydx.abacus.state.manager.HumanReadablePlaceOrderPayload import exchange.dydx.abacus.state.manager.HumanReadableSubaccountTransferPayload import exchange.dydx.abacus.state.manager.HumanReadableTriggerOrdersPayload import exchange.dydx.abacus.state.manager.HumanReadableWithdrawPayload +import exchange.dydx.abacus.state.model.AdjustIsolatedMarginInputField import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.TradingStateMachine @@ -292,6 +293,23 @@ internal fun AccountsSupervisor.commitTriggerOrders( return account?.commitTriggerOrders(currentHeight, callback) } +internal fun AccountsSupervisor.adjustIsolatedMargin( + data: String?, + type: AdjustIsolatedMarginInputField? +) { + account?.adjustIsolatedMargin(data, type) +} + +internal fun AccountsSupervisor.commitAdjustIsolatedMargin( + callback: TransactionCallback +): HumanReadableSubaccountTransferPayload? { + return account?.commitAdjustIsolatedMargin(callback) +} + +internal fun AccountsSupervisor.adjustIsolatedMarginPayload(): HumanReadableSubaccountTransferPayload? { + return account?.adjustIsolatedMarginPayload() +} + internal fun AccountsSupervisor.commitClosePosition( currentHeight: Int?, callback: TransactionCallback diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountSupervisor.kt index 2f4696195..27d997cba 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountSupervisor.kt @@ -34,10 +34,12 @@ import exchange.dydx.abacus.state.manager.PlaceOrderMarketInfo import exchange.dydx.abacus.state.manager.PlaceOrderRecord import exchange.dydx.abacus.state.manager.TransactionParams import exchange.dydx.abacus.state.manager.TransactionQueue +import exchange.dydx.abacus.state.model.AdjustIsolatedMarginInputField import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.state.model.TradeInputField import exchange.dydx.abacus.state.model.TradingStateMachine import exchange.dydx.abacus.state.model.TriggerOrdersInputField +import exchange.dydx.abacus.state.model.adjustIsolatedMargin import exchange.dydx.abacus.state.model.closePosition import exchange.dydx.abacus.state.model.findOrder import exchange.dydx.abacus.state.model.historicalPnl @@ -447,6 +449,21 @@ internal class SubaccountSupervisor( } } + fun adjustIsolatedMargin( + data: String?, + type: AdjustIsolatedMarginInputField?, + ) { + helper.ioImplementations.threading?.async(ThreadingType.abacus) { + val stateResponse = stateMachine.adjustIsolatedMargin(data, type, subaccountNumber) + helper.ioImplementations.threading?.async(ThreadingType.main) { + helper.stateNotification?.stateChanged( + stateResponse.state, + stateResponse.changes, + ) + } + } + } + /** * @description Get the childSubaccount number that is available for the given marketId * @param marketId @@ -862,6 +879,30 @@ internal class SubaccountSupervisor( return payload } + internal fun commitAdjustIsolatedMargin( + callback: TransactionCallback + ): HumanReadableSubaccountTransferPayload { + val payload = adjustIsolatedMarginPayload() + val transferPayloadString = Json.encodeToString(payload) + + submitTransaction( + TransactionType.SubaccountTransfer, + transferPayloadString, + null, + transactionCallback = { response: String? -> + val error = parseTransactionResponse(response) + helper.send( + error, + callback, + payload, + ) + }, + false, + ) + + return payload + } + internal fun stopWatchingLastOrder() { lastOrderClientId = null } @@ -1194,6 +1235,20 @@ internal class SubaccountSupervisor( ) } + @Throws(Exception::class) + fun adjustIsolatedMarginPayload(): HumanReadableSubaccountTransferPayload { + val isolatedMarginAdjustment = stateMachine.state?.input?.adjustIsolatedMargin ?: error("AdjustIsolatedMarginInput is null") + val amount = isolatedMarginAdjustment.amount ?: error("amount is null") + val childSubaccountNumber = isolatedMarginAdjustment.childSubaccountNumber ?: error("childSubaccountNumber is null") + + return HumanReadableSubaccountTransferPayload( + subaccountNumber, + amount, + accountAddress, + childSubaccountNumber, + ) + } + private fun faucetBody(amount: Double): String? { return if (accountAddress != null) { val params = iMapOf( diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/AdjustIsolatedMarginInputTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/AdjustIsolatedMarginInputTests.kt new file mode 100644 index 000000000..e60d945bc --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/AdjustIsolatedMarginInputTests.kt @@ -0,0 +1,206 @@ +package exchange.dydx.abacus.payload.v4 + +import exchange.dydx.abacus.output.input.IsolatedMarginAdjustmentType +import exchange.dydx.abacus.responses.StateResponse +import exchange.dydx.abacus.state.model.AdjustIsolatedMarginInputField +import exchange.dydx.abacus.state.model.adjustIsolatedMargin +import kotlin.test.Test + +class AdjustIsolatedMarginInputTests : V4BaseTests() { + + override fun loadSubaccounts(): StateResponse { + return perp.socket(testWsUrl, "", 0, null) + } + + fun loadSubaccountWithoutChildren(): StateResponse { + return perp.socket( + testWsUrl, + mock.accountsChannel.v4_subscribed_for_calculation, + 0, + null, + ) + } + + fun loadSubaccountsWithChildren(): StateResponse { + return perp.socket(testWsUrl, mock.parentSubaccountsChannel.subscribed, 0, null) + } + +// @Test +// fun testInputsWithoutChildren() { +// setup() +// loadSubaccountWithoutChildren() +// +// testChildSubaccountNumberInput() +// testMarginAddition() +// testMarginRemoval() +// testZeroAmount() +// } + + @Test + fun testInputs() { + setup() + loadSubaccountsWithChildren() + + testChildSubaccountNumberInput() + testMarginAddition() + testMarginRemoval() + testZeroAmount() + } + + private fun testChildSubaccountNumberInput() { + test( + { + perp.adjustIsolatedMargin("128", AdjustIsolatedMarginInputField.ChildSubaccountNumber, 0) + }, + """ + { + "input": { + "current": "adjustIsolatedMargin", + "adjustIsolatedMargin": { + "ChildSubaccountNumber": "128" + } + } + } + """.trimIndent(), + ) + } + + private fun testZeroAmount() { + test( + { + perp.adjustIsolatedMargin("0", AdjustIsolatedMarginInputField.Amount, 0) + }, + """ + { + "input": { + "current": "adjustIsolatedMargin", + "adjustIsolatedMargin": { + "Amount": "0" + } + }, + "wallet": { + "account": { + "subaccounts": { + "0": { + "quoteBalance": { + "current": "100000", + "postOrder": null + } + }, + "128": { + "quoteBalance": { + "current": "500.000000", + "postOrder": null + } + } + } + } + } + } + """.trimIndent(), + ) + } + + private fun testMarginAddition() { + test( + { + perp.adjustIsolatedMargin(IsolatedMarginAdjustmentType.Add.name, AdjustIsolatedMarginInputField.Type, 0) + }, + """ + { + "input": { + "current": "adjustIsolatedMargin", + "adjustIsolatedMargin": { + "Type": "Add" + } + } + } + """.trimIndent(), + ) + + test( + { + perp.adjustIsolatedMargin("92.49", AdjustIsolatedMarginInputField.Amount, 0) + }, + """ + { + "input": { + "current": "adjustIsolatedMargin", + "adjustIsolatedMargin": { + "Amount": "92.49" + } + } + } + """.trimIndent(), + ) + } + + private fun testMarginRemoval() { + test( + { + perp.adjustIsolatedMargin(IsolatedMarginAdjustmentType.Remove.name, AdjustIsolatedMarginInputField.Type, 0) + }, + """ + { + "input": { + "current": "adjustIsolatedMargin", + "adjustIsolatedMargin": { + "Type": "Remove" + } + }, + "wallet": { + "account": { + "subaccounts": { + "0": { + "quoteBalance": { + "current": "100000" + } + }, + "128": { + "quoteBalance": { + "current": "500.000000" + } + } + } + } + } + } + """.trimIndent(), + ) + + test( + { + perp.adjustIsolatedMargin("20", AdjustIsolatedMarginInputField.Amount, 0) + }, + """ + { + "input": { + "current": "adjustIsolatedMargin", + "adjustIsolatedMargin": { + "Type": "Remove", + "Amount": "20" + } + }, + "wallet": { + "account": { + "subaccounts": { + "0": { + "quoteBalance": { + "current": "100000", + "postOrder": "100020" + } + }, + "128": { + "quoteBalance": { + "current": "500.000000", + "postOrder": "480" + } + } + } + } + } + } + """.trimIndent(), + ) + } +} diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 3b739e729..73604a297 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.7.4' + spec.version = '1.7.5' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''