diff --git a/build.gradle.kts b/build.gradle.kts index 1cf90fe99..a793e446d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,7 +51,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.7.23" +version = "1.7.24" repositories { google() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/Account.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/Account.kt index b2b14c481..7432a2a0e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/Account.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/Account.kt @@ -1354,11 +1354,12 @@ data class Subaccount( */ val marginEnabled = parser.asBool(data["marginEnabled"]) ?: true - return if (existing?.positionId != positionId || - existing?.pnlTotal != pnlTotal || - existing?.pnl24h != pnl24h || - existing?.pnl24hPercent != pnl24hPercent || - existing?.quoteBalance !== quoteBalance || + return if (existing?.subaccountNumber != subaccountNumber || + existing.positionId != positionId || + existing.pnlTotal != pnlTotal || + existing.pnl24h != pnl24h || + existing.pnl24hPercent != pnl24hPercent || + existing.quoteBalance !== quoteBalance || existing.notionalTotal !== notionalTotal || existing.valueTotal !== valueTotal || existing.initialRiskTotal !== initialRiskTotal || @@ -2043,6 +2044,7 @@ data class Account( if (subaccountsData != null) { for ((key, value) in subaccountsData) { val subaccountData = parser.asMap(value) ?: iMapOf() + Subaccount.create( existing?.subaccounts?.get(key), parser, @@ -2062,6 +2064,7 @@ data class Account( if (groupedSubaccountsData != null) { for ((key, value) in groupedSubaccountsData) { val subaccountData = parser.asMap(value) ?: iMapOf() + Subaccount.create( existing?.subaccounts?.get(key), parser, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AccountProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AccountProcessor.kt index 00b1179d2..8bd017a8e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AccountProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AccountProcessor.kt @@ -195,6 +195,9 @@ internal open class SubaccountProcessor(parser: ParserProtocol) : BaseProcessor( "starkKey" to "starkKey", "positionId" to "positionId", ), + "int" to mapOf( + "subaccountNumber" to "subaccountNumber", + ), "double" to mapOf( "pendingDeposits" to "pendingDeposits", "pendingWithdrawals" to "pendingWithdrawals", 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 10ae94c96..20581a270 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 @@ -10,6 +10,7 @@ import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.output.input.TradeInputGoodUntil import exchange.dydx.abacus.output.input.TriggerOrder import exchange.dydx.abacus.protocols.AnalyticsEvent +import exchange.dydx.abacus.protocols.LocalTimerProtocol import exchange.dydx.abacus.protocols.ThreadingType import exchange.dydx.abacus.protocols.TransactionCallback import exchange.dydx.abacus.protocols.TransactionType @@ -294,6 +295,10 @@ internal class SubaccountSupervisor( channel, subaccountChannelParams(accountAddress, subaccountNumber, subscribe), ) + + if (parent) { + pollReclaimUnutilizedFunds() + } } private fun subaccountChannelParams( @@ -1407,6 +1412,91 @@ internal class SubaccountSupervisor( } } + /** + * @description Loop through all subaccounts to find childSubaccounts that have funds but no open positions or orders. Initiate a transfer to parentSubaccount. + */ + private fun reclaimUnutilizedFundsFromChildSubaccounts() { + val subaccounts = stateMachine.state?.account?.subaccounts ?: return + + val subaccountQuoteBalanceMap = subaccounts.mapValues { subaccount -> + // If the subaccount is the parentSubaccount, skip + if (subaccount.value.subaccountNumber == subaccountNumber) { + return@mapValues 0.0 + } + + val openPositions = subaccount.value.openPositions + val openOrders = subaccount.value.orders?.filter { order -> + val status = helper.parser.asString(order.status) + iListOf("open", "pending", "untriggered", "partiallyFilled").contains(status) + } + val quoteBalance = subaccount.value.quoteBalance?.current ?: 0.0 + + // Only return a quoteBalance if the subaccount has no open positions or orders + if (openPositions.isNullOrEmpty() && openOrders.isNullOrEmpty() && quoteBalance > 0.0) { + quoteBalance + } else { + 0.0 + } + }.filter { + it.value > 0.0 + } + + val transferPayloadStrings = iMutableListOf() + + subaccountQuoteBalanceMap.forEach { + val childSubaccountNumber = it.key.toInt() + val amountToTransfer = it.value.toString() + + val transferPayload = HumanReadableSubaccountTransferPayload( + childSubaccountNumber, + amountToTransfer, + accountAddress, + subaccountNumber, + ) + + val transferPayloadString = Json.encodeToString(transferPayload) + transferPayloadStrings.add(transferPayloadString) + } + + recursivelyReclaimChildSubaccountFunds(transferPayloadStrings) + } + + private fun recursivelyReclaimChildSubaccountFunds(transferPayloadStrings: MutableList) { + if (transferPayloadStrings.isNotEmpty()) { + val transferPayloadString = transferPayloadStrings.removeAt(0) + helper.transaction(TransactionType.SubaccountTransfer, transferPayloadString) { response -> + val error = parseTransactionResponse(response) + if (error != null) { + emitError(error) + } else { + recursivelyReclaimChildSubaccountFunds(transferPayloadStrings) + } + } + } + } + + private var reclaimUnutilizedFundsTimer: LocalTimerProtocol? = null + set(value) { + if (field !== value) { + field?.cancel() + field = value + } + } + + private fun pollReclaimUnutilizedFunds() { + reclaimUnutilizedFundsTimer = null + helper.ioImplementations.threading?.async(ThreadingType.abacus) { + this.reclaimUnutilizedFundsTimer = helper.ioImplementations.timer?.schedule( + (10.seconds).inWholeSeconds.toDouble(), + null, + ) { + reclaimUnutilizedFundsFromChildSubaccounts() + pollReclaimUnutilizedFunds() + false + } + } + } + override fun updateNotifications() { val notifications = notificationsProvider.buildNotifications(subaccountNumber) consolidateNotifications(notifications) diff --git a/v4_abacus.podspec b/v4_abacus.podspec index ad6a0a643..4ce4a493c 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.23' + spec.version = '1.7.24' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''