From 97487af78cfb34b4246aaf1e0dc02930ef087fee Mon Sep 17 00:00:00 2001 From: Jeremy Lee <37092291+yogurtandjam@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:57:48 -0400 Subject: [PATCH] feat: top level withdrawal methods [OTE-389] [2/n] (#443) Co-authored-by: mobile-build-bot-git --- build.gradle.kts | 2 +- .../processor/router/skip/SkipProcessor.kt | 6 +- .../router/skip/SkipRoutePayloadProcessor.kt | 42 ++++- .../router/skip/SkipRouteProcessor.kt | 6 +- .../manager/configs/V4StateManagerConfigs.kt | 18 +- .../state/v2/supervisor/AccountSupervisor.kt | 68 ++++++- .../v2/supervisor/OnboardingSupervisor.kt | 18 +- .../exchange.dydx.abacus/utils/Map+Utils.kt | 12 ++ .../utils/String+Utils.kt | 6 + .../router/skip/SkipRouteProcessorTests.kt | 100 +++++++++- .../tests/payloads/SkipRouteMock.kt | 177 ++++++++++++++++++ 11 files changed, 418 insertions(+), 37 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f5aef2d4f..dda2446f3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,7 +51,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.7.85" +version = "1.7.86" repositories { google() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipProcessor.kt index 167391b35..c3978e996 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipProcessor.kt @@ -6,6 +6,7 @@ import exchange.dydx.abacus.output.input.TransferInputTokenResource import exchange.dydx.abacus.processor.base.BaseProcessor import exchange.dydx.abacus.processor.router.IRouterProcessor import exchange.dydx.abacus.processor.router.SharedRouterProcessor +import exchange.dydx.abacus.processor.router.squid.SquidStatusProcessor import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalTransferInputState import exchange.dydx.abacus.state.manager.CctpConfig.cctpChainIds @@ -131,7 +132,10 @@ internal class SkipProcessor( payload: Map, transactionId: String?, ): Map? { - throw NotImplementedError("receivedStatus is not implemented in SkipProcessor!") +// using squid status processor until we implement it +// this lets us track our tx so we can more easily tell if tx are succeeding or not in QA + val processor = SquidStatusProcessor(parser, transactionId) + return processor.received(existing, payload) } override fun updateTokensDefaults(modified: MutableMap, selectedChainId: String?) { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRoutePayloadProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRoutePayloadProcessor.kt index d0777267f..08c0c56e5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRoutePayloadProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRoutePayloadProcessor.kt @@ -2,8 +2,12 @@ package exchange.dydx.abacus.processor.router.skip import exchange.dydx.abacus.processor.base.BaseProcessor import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.utils.JsonEncoder import exchange.dydx.abacus.utils.safeSet +import exchange.dydx.abacus.utils.toCamelCaseKeys +// We may later want to split this into one processor per network +// For now we're not since it's just two, but at 3 we will internal class SkipRoutePayloadProcessor(parser: ParserProtocol) : BaseProcessor(parser) { private val keyMap = mapOf( "string" to mapOf( @@ -25,17 +29,51 @@ internal class SkipRoutePayloadProcessor(parser: ParserProtocol) : BaseProcessor ), ) + enum class TxType { + EVM, + COSMOS + } + +// DO-LATER: https://linear.app/dydx/issue/OTE-350/%5Babacus%5D-cleanup +// Create custom exceptions for better error handling specificity and expressiveness + @Suppress("TooGenericExceptionThrown") + internal fun getTxType(payload: Map): TxType { + val evm = parser.value(payload, "txs.0.evm_tx") + val cosmos = parser.value(payload, "txs.0.cosmos_tx") + if (evm != null) return TxType.EVM + if (cosmos != null) return TxType.COSMOS + throw Error("SkipRoutePayloadProcessor: txType is not evm or cosmos") + } + override fun received( existing: Map?, payload: Map ): Map { + val txType = getTxType(payload) val modified = transform(existing, payload, keyMap) - val data = modified.get("data") - if (data != null) { + val data = modified["data"] + if (data != null && txType == TxType.EVM) { // skip does not provide the 0x prefix. it's not required but is good for clarity // and keeps our typing honest (we typecast this value to evmAddress in web) modified.safeSet("data", "0x$data") } + if (txType == TxType.COSMOS) { + val jsonEncoder = JsonEncoder() + val msg = parser.asString(parser.value(payload, "txs.0.cosmos_tx.msgs.0.msg")) + val msgMap = parser.decodeJsonObject(msg) +// tendermint client rejects msgs that aren't camelcased + val camelCasedMsgMap = msgMap?.toCamelCaseKeys() + val msgTypeUrl = parser.value(payload, "txs.0.cosmos_tx.msgs.0.msg_type_url") + val fullMessage = mapOf( + "msg" to camelCasedMsgMap, +// Squid returns the msg payload under the "value" key for noble transfers + "value" to camelCasedMsgMap, + "msgTypeUrl" to msgTypeUrl, +// Squid sometimes returns typeUrl or msgTypeUrl depending on the route version + "typeUrl" to msgTypeUrl, + ) + modified.safeSet("data", jsonEncoder.encode(fullMessage)) + } return modified } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessor.kt index 9230be899..4c4bbf6e7 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessor.kt @@ -62,13 +62,11 @@ internal class SkipRouteProcessor(internal val parser: ParserProtocol) { modified.safeSet("toAmountUSD", toAmountUSD) } - val payloadProcessor = SkipRoutePayloadProcessor(parser) // TODO: Remove slippage. // This is just hard coded in our params so we're keeping it to be at parity for now // Fast follow squid -> skip migration project to removing max slippage // because we already show the actual price impact. modified.safeSet("slippage", SLIPPAGE_PERCENT) - modified.safeSet("requestPayload", payloadProcessor.received(null, payload)) val errorCode = parser.value(payload, "code") // if we have an error code, add the payload as a list of errors @@ -76,6 +74,10 @@ internal class SkipRouteProcessor(internal val parser: ParserProtocol) { // TODO: replace errors with errorMessage once we finish migration if (errorCode != null) { modified.safeSet("errors", parser.asString(listOf(payload))) + } else { +// Only bother processing payload if there's no error + val payloadProcessor = SkipRoutePayloadProcessor(parser) + modified.safeSet("requestPayload", payloadProcessor.received(null, payload)) } return modified } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/configs/V4StateManagerConfigs.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/configs/V4StateManagerConfigs.kt index deb84fd05..23e73dcb5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/configs/V4StateManagerConfigs.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/configs/V4StateManagerConfigs.kt @@ -91,18 +91,6 @@ class V4StateManagerConfigs( return "$squid$path" } - fun squidChains(): String? { - val squid = environment.endpoints.squid ?: return null - val path = parser.asString(parser.value(configs, "paths.0xsquid.chains")) - return "$squid$path" - } - - fun squidToken(): String? { - val squid = environment.endpoints.squid ?: return null - val path = parser.asString(parser.value(configs, "paths.0xsquid.tokens")) - return "$squid$path" - } - fun squidV2Assets(): String? { return "$squidV2Host/v2/sdk-info" } @@ -111,7 +99,7 @@ class V4StateManagerConfigs( return "$squidV2Host/v2/route" } - fun nobleChainId(): String? { + fun nobleChainId(): String { return if (environment.isMainNet) "noble-1" else "grand-1" } @@ -127,9 +115,7 @@ class V4StateManagerConfigs( return "$skipHost/v2/fungible/msgs_direct" } - fun nobleDenom(): String? { - return "uusdc" - } + val nobleDenom = "uusdc" private val skipHost: String get() { 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 fd7e25745..60f909c2c 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 @@ -8,6 +8,7 @@ import exchange.dydx.abacus.output.Notification import exchange.dydx.abacus.output.PerpetualState import exchange.dydx.abacus.output.Restriction import exchange.dydx.abacus.output.UsageRestriction +import exchange.dydx.abacus.processor.router.skip.SkipRoutePayloadProcessor import exchange.dydx.abacus.protocols.LocalTimerProtocol import exchange.dydx.abacus.protocols.QueryType import exchange.dydx.abacus.protocols.ThreadingType @@ -49,6 +50,7 @@ import exchange.dydx.abacus.utils.AnalyticsUtils import exchange.dydx.abacus.utils.CoroutineTimer import exchange.dydx.abacus.utils.IMap import exchange.dydx.abacus.utils.Logger +import exchange.dydx.abacus.utils.SLIPPAGE_PERCENT import exchange.dydx.abacus.utils.iMapOf import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.toJsonPrettyPrint @@ -459,6 +461,7 @@ internal open class AccountSupervisor( if (processingCctpWithdraw) { return@getOnChain } +// if pending withdrawal, perform CCTP Withdrawal pendingCctpWithdraw?.let { walletState -> processingCctpWithdraw = true val callback = walletState.callback @@ -477,6 +480,7 @@ internal open class AccountSupervisor( processingCctpWithdraw = false } } +// else, transfer noble balance back to dydx ?: run { transferNobleBalance(amount) } } else if (balance["error"] != null) { Logger.e { "Error checking noble balance: $response" } @@ -582,10 +586,22 @@ internal open class AccountSupervisor( } } +// DO-LATER: https://linear.app/dydx/issue/OTE-350/%5Babacus%5D-cleanup +// rename to transferNobleToDydx or something more descriptive. +// This function is a unidirection transfer function that sweeps funds +// from a noble account into a user's given subaccount private fun transferNobleBalance(amount: BigDecimal) { + if (stateMachine.useSkip) { + transferNobleBalanceSkip(amount = amount) + } else { + transferNobleBalanceSquid(amount = amount) + } + } + + private fun transferNobleBalanceSquid(amount: BigDecimal) { val url = helper.configs.squidRoute() val fromChain = helper.configs.nobleChainId() - val fromToken = helper.configs.nobleDenom() + val fromToken = helper.configs.nobleDenom val nobleAddress = accountAddress.toNobleAddress() val chainId = helper.environment.dydxChainId val squidIntegratorId = helper.environment.squidIntegratorId @@ -639,6 +655,56 @@ internal open class AccountSupervisor( } } + private fun transferNobleBalanceSkip(amount: BigDecimal) { + val url = helper.configs.skipV2MsgsDirect() + val fromChain = helper.configs.nobleChainId() + val fromToken = helper.configs.nobleDenom + val nobleAddress = accountAddress.toNobleAddress() ?: return + val chainId = helper.environment.dydxChainId ?: return + val dydxTokenDemon = helper.environment.tokens["usdc"]?.denom ?: return + val body: Map = mapOf( + "amount_in" to amount.toPlainString(), +// from noble denom and chain + "source_asset_denom" to fromToken, + "source_asset_chain_id" to fromChain, +// to dydx denom and chain + "dest_asset_denom" to dydxTokenDemon, + "dest_asset_chain_id" to chainId, + "chain_ids_to_addresses" to mapOf( + fromChain to nobleAddress, + chainId to accountAddress, + ), + "slippage_tolerance_percent" to SLIPPAGE_PERCENT, + ) + val header = + iMapOf( + "Content-Type" to "application/json", + ) + helper.post(url, header, body.toJsonPrettyPrint()) { _, response, code, _ -> + if (response == null) { + val json = helper.parser.decodeJsonObject(response) + if (json != null) { + val skipRoutePayloadProcessor = SkipRoutePayloadProcessor(parser = helper.parser) + val processedPayload = skipRoutePayloadProcessor.received(existing = mapOf(), payload = json) + val ibcPayload = + helper.parser.asString( + processedPayload.get("data"), + ) + if (ibcPayload != null) { + helper.transaction(TransactionType.SendNobleIBC, ibcPayload) { + val error = helper.parseTransactionResponse(it) + if (error != null) { + Logger.e { "transferNobleBalanceSkip error: $error" } + } + } + } + } + } else { + Logger.e { "transferNobleBalanceSkip error, code: $code" } + } + } + } + private fun handleComplianceResponse(response: String?, httpCode: Int): ComplianceStatus { var complianceStatus = ComplianceStatus.UNKNOWN var updatedAt: String? = null diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/OnboardingSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/OnboardingSupervisor.kt index b1729d85c..69e4a72d2 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/OnboardingSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/OnboardingSupervisor.kt @@ -299,7 +299,7 @@ internal class OnboardingSupervisor( val nobleAddress = accountAddress.toNobleAddress() val url = helper.configs.skipV2MsgsDirect() val toChain = helper.configs.nobleChainId() - val toToken = helper.configs.nobleDenom() + val toToken = helper.configs.nobleDenom if (fromChain != null && fromToken != null && fromAmount != null && fromAmount > 0 && @@ -428,7 +428,7 @@ internal class OnboardingSupervisor( val nobleAddress = accountAddress.toNobleAddress() val url = helper.configs.squidV2Route() val toChain = helper.configs.nobleChainId() - val toToken = helper.configs.nobleDenom() + val toToken = helper.configs.nobleDenom if (fromChain != null && fromToken != null && fromAmount != null && fromAmount > 0 && @@ -751,7 +751,7 @@ internal class OnboardingSupervisor( subaccountNumber: Int?, ) { val nobleChain = helper.configs.nobleChainId() - val nobleToken = helper.configs.nobleDenom() + val nobleToken = helper.configs.nobleDenom val toAddress = state?.input?.transfer?.address val usdcSize = helper.parser.asDecimal(state?.input?.transfer?.size?.usdcSize) val fromAmount = if (usdcSize != null && usdcSize > gas) { @@ -885,7 +885,7 @@ internal class OnboardingSupervisor( val url = helper.configs.squidV2Route() val fromAddress = accountAddress.toNobleAddress() val fromChain = helper.configs.nobleChainId() - val fromToken = helper.configs.nobleDenom() + val fromToken = helper.configs.nobleDenom if (toChain != null && toToken != null && toAddress != null && @@ -946,7 +946,7 @@ internal class OnboardingSupervisor( subaccountNumber: Int?, ) { val toChain = helper.configs.nobleChainId() ?: return - val toToken = helper.configs.nobleDenom() ?: return + val toToken = helper.configs.nobleDenom ?: return val toAddress = state?.input?.transfer?.address ?: return val usdcSize = helper.parser.asDecimal(state?.input?.transfer?.size?.usdcSize) ?: return val fromAmount = if (usdcSize > gas) { @@ -1055,7 +1055,7 @@ internal class OnboardingSupervisor( val fromAddress = accountAddress.toNobleAddress() ?: return val fromChain = helper.configs.nobleChainId() ?: return - val fromToken = helper.configs.nobleDenom() ?: return + val fromToken = helper.configs.nobleDenom ?: return val body: Map = mapOf( "amount_in" to fromAmountString, "source_asset_denom" to fromToken, @@ -1129,7 +1129,7 @@ internal class OnboardingSupervisor( private fun transferNobleBalance(accountAddress: String, amount: BigDecimal) { val url = helper.configs.squidRoute() val fromChain = helper.configs.nobleChainId() - val fromToken = helper.configs.nobleDenom() + val fromToken = helper.configs.nobleDenom val nobleAddress = accountAddress.toNobleAddress() val chainId = helper.environment.dydxChainId val squidIntegratorId = helper.environment.squidIntegratorId @@ -1382,7 +1382,7 @@ internal class OnboardingSupervisor( ) { val url = helper.configs.squidRoute() val nobleChain = helper.configs.nobleChainId() - val nobleToken = helper.configs.nobleDenom() + val nobleToken = helper.configs.nobleDenom val nobleAddress = accountAddress.toNobleAddress() val chainId = helper.environment.dydxChainId val squidIntegratorId = helper.environment.squidIntegratorId @@ -1490,7 +1490,7 @@ internal class OnboardingSupervisor( // DO-LATER: https://linear.app/dydx/issue/OTE-350/%5Babacus%5D-cleanup val url = helper.configs.skipV2MsgsDirect() val nobleChain = helper.configs.nobleChainId() - val nobleToken = helper.configs.nobleDenom() + val nobleToken = helper.configs.nobleDenom val nobleAddress = accountAddress.toNobleAddress() val chainId = helper.environment.dydxChainId val nativeChainUSDCDenom = helper.environment.tokens["usdc"]?.denom diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/utils/Map+Utils.kt b/src/commonMain/kotlin/exchange.dydx.abacus/utils/Map+Utils.kt index 132d5fdae..8075df3b8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/utils/Map+Utils.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/utils/Map+Utils.kt @@ -141,3 +141,15 @@ fun IMap.toUrlParams(): String = entries.joinToString("&") { it.key + "=" + it.value } + +fun Map.toCamelCaseKeys(): Map { + return this.mapKeys { it.key.toCamelCase() }.mapValues { (_, value) -> + when (value) { + is Map<*, *> -> (value as Map).toCamelCaseKeys() + is List<*> -> value.map { + if (it is Map<*, *>) (it as Map).toCamelCaseKeys() else it + } + else -> value + } + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/utils/String+Utils.kt b/src/commonMain/kotlin/exchange.dydx.abacus/utils/String+Utils.kt index 0ff749000..ac468c258 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/utils/String+Utils.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/utils/String+Utils.kt @@ -35,3 +35,9 @@ fun String.toDydxAddress(): String? { return null } } + +fun String.toCamelCase(): String { + return this.split("_", "-") + .mapIndexed { index, s -> if (index == 0) s.lowercase() else s.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } } + .joinToString("") +} diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessorTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessorTests.kt index 9e4b9c757..c81e3f08c 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessorTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessorTests.kt @@ -1,10 +1,10 @@ package exchange.dydx.abacus.processor.router.skip import exchange.dydx.abacus.tests.payloads.SkipRouteMock +import exchange.dydx.abacus.utils.JsonEncoder import exchange.dydx.abacus.utils.Parser import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue class SkipRouteProcessorTests { val parser = Parser() @@ -12,7 +12,8 @@ class SkipRouteProcessorTests { internal val skipRouteProcessor = SkipRouteProcessor(parser = parser) /** - * Tests a CCTP deposit. + * Tests an EVM CCTP deposit. + * This processes an EVM -> Noble USDC transaction (we only support deposits from EVM chains) */ @Test fun testReceivedCCTPDeposit() { @@ -33,7 +34,97 @@ class SkipRouteProcessorTests { "toAddress" to "uusdc", ), ) - assertTrue(expected == result) + assertEquals(expected, result) + } + + /** + * Tests a CCTP withdrawal initiated from the cctpToNobleSkip method + * This payload is used by the chain transaction method WithdrawToNobleIBC + * This processes a Dydx -> Noble CCTP transaction + */ + @Test + fun testReceivedCCTPDydxToNoble() { + val payload = skipRouteMock.payloadCCTPDydxToNoble + val result = skipRouteProcessor.received(existing = mapOf(), payload = templateToJson(payload), decimals = 6.0) + val jsonEncoder = JsonEncoder() + val expectedMsg = mapOf( + "sourcePort" to "transfer", + "sourceChannel" to "channel-0", + "token" to mapOf( + "denom" to "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", + "amount" to "10996029", + ), + "sender" to "dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s", + "receiver" to "noble1nhzuazjhyfu474er6v4ey8zn6wa5fy6gthndxf", + "timeoutHeight" to mapOf(), + "timeoutTimestamp" to 1718308711061386287, + ) + val expectedData = jsonEncoder.encode( + mapOf( + "msg" to expectedMsg, + "value" to expectedMsg, + "msgTypeUrl" to "/ibc.applications.transfer.v1.MsgTransfer", + "typeUrl" to "/ibc.applications.transfer.v1.MsgTransfer", + ), + ) + val expected = mapOf( + "toAmountUSD" to 11.01, + "toAmount" to 10.996029, + "slippage" to "1", + "requestPayload" to mapOf( + "fromChainId" to "dydx-mainnet-1", + "fromAddress" to "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", + "toChainId" to "noble-1", + "toAddress" to "uusdc", + "data" to expectedData, + ), + ) + assertEquals(expected, result) + } + + /** + * Tests a CCTP autosweep from the transferNobleBalance method + * This payload is used by the chain transaction method sendNobleIBC + * This processes a Noble -> Dydx CCTP transaction + */ + @Test + fun testReceivedCCTPNobleToDydx() { + val payload = skipRouteMock.payloadCCTPNobleToDydx + val result = skipRouteProcessor.received(existing = mapOf(), payload = templateToJson(payload), decimals = 6.0) + val jsonEncoder = JsonEncoder() + val expectedMsg = mapOf( + "sourcePort" to "transfer", + "sourceChannel" to "channel-33", + "token" to mapOf( + "denom" to "uusdc", + "amount" to "5884", + ), + "sender" to "noble1nhzuazjhyfu474er6v4ey8zn6wa5fy6gthndxf", + "receiver" to "dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s", + "timeoutHeight" to mapOf(), + "timeoutTimestamp" to 1718318348813666048, + ) + val expectedData = jsonEncoder.encode( + mapOf( + "msg" to expectedMsg, + "value" to expectedMsg, + "msgTypeUrl" to "/ibc.applications.transfer.v1.MsgTransfer", + "typeUrl" to "/ibc.applications.transfer.v1.MsgTransfer", + ), + ) + val expected = mapOf( + "toAmountUSD" to 0.01, + "toAmount" to 0.005884, + "slippage" to "1", + "requestPayload" to mapOf( + "fromChainId" to "noble-1", + "fromAddress" to "uusdc", + "toChainId" to "dydx-mainnet-1", + "toAddress" to "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", + "data" to expectedData, + ), + ) + assertEquals(expected, result) } @Test @@ -42,9 +133,8 @@ class SkipRouteProcessorTests { val result = skipRouteProcessor.received(existing = mapOf(), payload = templateToJson(payload), decimals = 6.0) val expected = mapOf( "slippage" to "1", - "requestPayload" to emptyMap(), "errors" to "[{code=3, message=\"difference in usd value of route input and output is too large. input usd value: 100000.00 output usd value: 98811.81\", details=[{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"BAD_PRICE_ERROR\",\"domain\":\"skip.money\",\"metadata\":{}}]}]", ) - assertEquals(expected.toString(), result.toString()) + assertEquals(expected, result) } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/SkipRouteMock.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/SkipRouteMock.kt index 9b834ec81..3ea3ec290 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/SkipRouteMock.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/SkipRouteMock.kt @@ -107,6 +107,183 @@ internal class SkipRouteMock { ] } }""" + internal val payloadCCTPDydxToNoble = """ + { + "msgs": [ + { + "multi_chain_msg": { + "chain_id": "dydx-mainnet-1", + "path": [ + "dydx-mainnet-1", + "noble-1" + ], + "msg": "{\"source_port\":\"transfer\",\"source_channel\":\"channel-0\",\"token\":{\"denom\":\"ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5\",\"amount\":\"10996029\"},\"sender\":\"dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s\",\"receiver\":\"noble1nhzuazjhyfu474er6v4ey8zn6wa5fy6gthndxf\",\"timeout_height\":{},\"timeout_timestamp\":1718308711061386287}", + "msg_type_url": "/ibc.applications.transfer.v1.MsgTransfer" + } + } + ], + "txs": [ + { + "cosmos_tx": { + "chain_id": "dydx-mainnet-1", + "path": [ + "dydx-mainnet-1", + "noble-1" + ], + "msgs": [ + { + "msg": "{\"source_port\":\"transfer\",\"source_channel\":\"channel-0\",\"token\":{\"denom\":\"ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5\",\"amount\":\"10996029\"},\"sender\":\"dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s\",\"receiver\":\"noble1nhzuazjhyfu474er6v4ey8zn6wa5fy6gthndxf\",\"timeout_height\":{},\"timeout_timestamp\":1718308711061386287}", + "msg_type_url": "/ibc.applications.transfer.v1.MsgTransfer" + } + ], + "signer_address": "dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s" + }, + "operations_indices": [ + 0 + ] + } + ], + "route": { + "source_asset_denom": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", + "source_asset_chain_id": "dydx-mainnet-1", + "dest_asset_denom": "uusdc", + "dest_asset_chain_id": "noble-1", + "amount_in": "10996029", + "amount_out": "10996029", + "operations": [ + { + "transfer": { + "port": "transfer", + "channel": "channel-0", + "from_chain_id": "dydx-mainnet-1", + "to_chain_id": "noble-1", + "pfm_enabled": false, + "supports_memo": true, + "denom_in": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", + "denom_out": "uusdc", + "bridge_id": "IBC", + "smart_relay": false, + "chain_id": "dydx-mainnet-1", + "dest_denom": "uusdc" + }, + "tx_index": 0, + "amount_in": "10996029", + "amount_out": "10996029" + } + ], + "chain_ids": [ + "dydx-mainnet-1", + "noble-1" + ], + "does_swap": false, + "estimated_amount_out": "10996029", + "swap_venues": [], + "txs_required": 1, + "usd_amount_in": "11.01", + "usd_amount_out": "11.01", + "estimated_fees": [], + "required_chain_addresses": [ + "dydx-mainnet-1", + "noble-1" + ] + } +} + """.trimIndent() + + internal val payloadCCTPNobleToDydx = """ + { + "msgs": [ + { + "multi_chain_msg": { + "chain_id": "noble-1", + "path": [ + "noble-1", + "dydx-mainnet-1" + ], + "msg": "{\"source_port\":\"transfer\",\"source_channel\":\"channel-33\",\"token\":{\"denom\":\"uusdc\",\"amount\":\"5884\"},\"sender\":\"noble1nhzuazjhyfu474er6v4ey8zn6wa5fy6gthndxf\",\"receiver\":\"dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s\",\"timeout_height\":{},\"timeout_timestamp\":1718318348813666048}", + "msg_type_url": "/ibc.applications.transfer.v1.MsgTransfer" + } + } + ], + "txs": [ + { + "cosmos_tx": { + "chain_id": "noble-1", + "path": [ + "noble-1", + "dydx-mainnet-1" + ], + "msgs": [ + { + "msg": "{\"source_port\":\"transfer\",\"source_channel\":\"channel-33\",\"token\":{\"denom\":\"uusdc\",\"amount\":\"5884\"},\"sender\":\"noble1nhzuazjhyfu474er6v4ey8zn6wa5fy6gthndxf\",\"receiver\":\"dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s\",\"timeout_height\":{},\"timeout_timestamp\":1718318348813666048}", + "msg_type_url": "/ibc.applications.transfer.v1.MsgTransfer" + } + ], + "signer_address": "noble1nhzuazjhyfu474er6v4ey8zn6wa5fy6gthndxf" + }, + "operations_indices": [ + 0 + ] + } + ], + "route": { + "source_asset_denom": "uusdc", + "source_asset_chain_id": "noble-1", + "dest_asset_denom": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", + "dest_asset_chain_id": "dydx-mainnet-1", + "amount_in": "5884", + "amount_out": "5884", + "operations": [ + { + "transfer": { + "port": "transfer", + "channel": "channel-33", + "from_chain_id": "noble-1", + "to_chain_id": "dydx-mainnet-1", + "pfm_enabled": true, + "supports_memo": true, + "denom_in": "uusdc", + "denom_out": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", + "fee_amount": "0", + "usd_fee_amount": "0.0000", + "fee_asset": { + "denom": "uusdc", + "chain_id": "noble-1", + "origin_denom": "uusdc", + "origin_chain_id": "noble-1", + "trace": "", + "is_cw20": false, + "is_evm": false, + "is_svm": false + }, + "bridge_id": "IBC", + "smart_relay": false, + "chain_id": "noble-1", + "dest_denom": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5" + }, + "tx_index": 0, + "amount_in": "5884", + "amount_out": "5884" + } + ], + "chain_ids": [ + "noble-1", + "dydx-mainnet-1" + ], + "does_swap": false, + "estimated_amount_out": "5884", + "swap_venues": [], + "txs_required": 1, + "usd_amount_in": "0.01", + "usd_amount_out": "0.01", + "estimated_fees": [], + "required_chain_addresses": [ + "noble-1", + "dydx-mainnet-1" + ] + } + } + """.trimIndent() internal val payloadError = """ { "code": 3,