diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/IRouterProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/IRouterProcessor.kt index 010188635..3bfe6c026 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/IRouterProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/IRouterProcessor.kt @@ -42,6 +42,7 @@ interface IRouterProcessor { transactionId: String?, ): Map? + fun getTokenByDenomAndChainId(tokenDenom: String?, chainId: String?): Map? fun updateTokensDefaults(modified: MutableMap, selectedChainId: String?) fun defaultChainId(): String? fun selectedTokenSymbol(tokenAddress: String?, selectedChainId: String?): String? 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 c3978e996..857074614 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 @@ -151,16 +151,21 @@ internal class SkipProcessor( return parser.asString(selectedChain?.get("chain_id")) } - override fun selectedTokenSymbol(tokenAddress: String?, selectedChainId: String?): String? { - val tokensList = filteredTokens(selectedChainId) + override fun getTokenByDenomAndChainId(tokenDenom: String?, chainId: String?): Map? { + val tokensList = filteredTokens(chainId) tokensList?.find { - parser.asString(parser.asNativeMap(it)?.get("denom")) == tokenAddress + parser.asString(parser.asNativeMap(it)?.get("denom")) == tokenDenom }?.let { - return parser.asString(parser.asNativeMap(it)?.get("symbol")) + return parser.asNativeMap(it) } return null } + override fun selectedTokenSymbol(tokenAddress: String?, selectedChainId: String?): String? { + val token = getTokenByDenomAndChainId(tokenAddress, selectedChainId) ?: return null + return parser.asString(token.get("symbol")) + } + override fun selectedTokenDecimals(tokenAddress: String?, selectedChainId: String?): String? { val tokensList = filteredTokens(selectedChainId) tokensList?.find { @@ -174,7 +179,31 @@ internal class SkipProcessor( override fun filteredTokens(chainId: String?): List? { val chainIdToUse = chainId ?: defaultChainId() val assetsMapForChainId = parser.asNativeMap(this.skipTokens?.get(chainIdToUse)) - return parser.asNativeList(assetsMapForChainId?.get("assets")) + val assetsForChainId = parser.asNativeList(assetsMapForChainId?.get("assets")) +// coinbase exchange chainId is noble-1. we only allow usdc withdrawals from it + if (chainId === "noble-1") { + return assetsForChainId?.filter { + parser.asString(parser.asNativeMap(it)?.get("denom")) == "uusdc" + } + } + + val filteredTokens = mutableListOf>() +// we have to replace skip's {chain-name}-native naming bc it doesn't play well with +// any of our SDKs. +// however, their {chain-name}-native denom naming is required for their API +// so we need to store both values + assetsForChainId?.forEach { + val token = parser.asNativeMap(it)?.toMutableMap() + if (token != null) { + val denom = parser.asString(token["denom"]) + if (denom?.endsWith("native") == true) { + token["skipDenom"] = denom + token["denom"] = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + } + filteredTokens.add(token.toMap()) + } + } + return filteredTokens } override fun defaultTokenAddress(chainId: String?): String? { 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 4c4bbf6e7..779205bbb 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 @@ -12,7 +12,7 @@ internal class SkipRouteProcessor(internal val parser: ParserProtocol) { "string" to mapOf( "route.usd_amount_out" to "toAmountUSD", "route.estimated_amount_out" to "toAmount", - "swap_price_impact_percent" to "aggregatePriceImpact", + "route.swap_price_impact_percent" to "aggregatePriceImpact", // SQUID PARAMS THAT ARE NOW DEPRECATED: // "route.estimate.gasCosts.0.amountUSD" to "gasFee", @@ -23,6 +23,12 @@ internal class SkipRouteProcessor(internal val parser: ParserProtocol) { ), ) + private val transactionTypes = listOf( + "transfer", + "axelar_transfer", + "hyperlane_transfer", + ) + private fun findFee(payload: Map, key: String): Double? { val estimatedFees = parser.asList(parser.value(payload, "route.estimated_fees")) val foundFeeObj = estimatedFees?.find { @@ -32,6 +38,29 @@ internal class SkipRouteProcessor(internal val parser: ParserProtocol) { return feeInUSD } + private fun calculateFeesFromSwaps(payload: Map): Double { + val operations = parser.asList(parser.value(payload, "route.operations")) ?: return 0.0 + var total = 0.0 + operations.forEach { operation -> + val fee = getFeeFromOperation(parser.asNativeMap(operation)) + total += fee + } + return total + } + + private fun getFeeFromOperation(operationPayload: Map?): Double { + if (operationPayload == null) return 0.0 + var fee = 0.0 + transactionTypes.forEach { transactionType -> + val transaction = parser.asNativeMap(operationPayload.get(transactionType)) + if (transaction != null) { + val usdFeeAmount = parser.asDouble(transaction.get("usd_fee_amount")) ?: 0.0 + fee += usdFeeAmount + } + } + return fee + } + fun received( existing: Map?, payload: Map, @@ -39,19 +68,17 @@ internal class SkipRouteProcessor(internal val parser: ParserProtocol) { ): Map { val modified = BaseProcessor(parser).transform(existing, payload, keyMap) - var bridgeFees = findFee(payload, "BRIDGE") + var bridgeFees = findFee(payload, "BRIDGE") ?: 0.0 // TODO: update web UI to show smart relay fees // For now we're just bundling it with the bridge fees - val smartRelayFees = findFee(payload, "SMART_RELAY") - if (bridgeFees == null) { - bridgeFees = smartRelayFees - } else if (smartRelayFees != null) { - bridgeFees += smartRelayFees - } + val smartRelayFees = findFee(payload, "SMART_RELAY") ?: 0.0 + bridgeFees += smartRelayFees + bridgeFees += calculateFeesFromSwaps(payload) + val gasFees = findFee(payload, "GAS") - modified.safeSet("gasFees", gasFees) - modified.safeSet("bridgeFees", bridgeFees) + modified.safeSet("gasFee", gasFees) + modified.safeSet("bridgeFee", bridgeFees) val toAmount = parser.asLong(parser.value(payload, "route.estimated_amount_out")) if (toAmount != null && decimals != null) { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/squid/SquidProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/squid/SquidProcessor.kt index c0362a6be..0e2c551a5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/squid/SquidProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/squid/SquidProcessor.kt @@ -184,6 +184,16 @@ internal class SquidProcessor( return parser.asString(selectedChain?.get("chainId")) } + override fun getTokenByDenomAndChainId(tokenDenom: String?, chainId: String?): Map? { + val tokensList = filteredTokens(chainId) + tokensList?.find { + parser.asString(parser.asNativeMap(it)?.get("address")) == tokenDenom + }?.let { + return parser.asNativeMap(it) + } + return null + } + override fun selectedTokenSymbol(tokenAddress: String?, selectedChainId: String?): String? { this.tokens?.find { parser.asString(parser.asNativeMap(it)?.get("address")) == tokenAddress 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 23e73dcb5..9936755c0 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 @@ -103,6 +103,10 @@ class V4StateManagerConfigs( return if (environment.isMainNet) "noble-1" else "grand-1" } + fun osmosisChainId(): String? { + return if (environment.isMainNet) "osmosis-1" else "osmosis-5" + } + fun skipV1Chains(): String { return "$skipHost/v1/info/chains?include_evm=true" } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt index e9391f86e..61e0ec1b1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt @@ -5,6 +5,7 @@ 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.Logger import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.mutableMapOf import exchange.dydx.abacus.utils.safeSet @@ -245,7 +246,9 @@ private fun TradingStateMachine.updateTransferToChainType(transfer: MutableMap, exchange: String) { val exchangeDestinationChainId = routerProcessor.exchangeDestinationChainId + Logger.e({ "exchangedestinationchainid:$exchangeDestinationChainId" }) val tokenOptions = routerProcessor.tokenOptions(exchangeDestinationChainId) + Logger.e({ "tokenOptions:$tokenOptions" }) if (transfer["type"] != "TRANSFER_OUT") { internalState.transfer.tokens = tokenOptions transfer.safeSet("token", routerProcessor.defaultTokenAddress(exchangeDestinationChainId)) 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 69e4a72d2..a7e9735d6 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 @@ -43,7 +43,9 @@ import exchange.dydx.abacus.utils.isAddressValid import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet import exchange.dydx.abacus.utils.toJsonPrettyPrint +import exchange.dydx.abacus.utils.toNeutronAddress import exchange.dydx.abacus.utils.toNobleAddress +import exchange.dydx.abacus.utils.toOsmosisAddress import io.ktor.util.encodeBase64 import kollections.iListOf import kotlinx.serialization.encodeToString @@ -210,48 +212,51 @@ internal class OnboardingSupervisor( } } - @Suppress("UnusedPrivateMember", "ForbiddenComment") private fun retrieveSkipDepositRouteNonCCTP( state: PerpetualState?, accountAddress: String, sourceAddress: String, subaccountNumber: Int?, ) { -// We have a lot of duplicate code for these deposit/withdrawal route calls -// It's easier to dedupe now that he url is the same and only the args differ -// TODO: Consider creating generateArgs fun to reduce code duplication - val fromChain = state?.input?.transfer?.chain - val fromToken = state?.input?.transfer?.token - val fromAmount = helper.parser.asDecimal(state?.input?.transfer?.size?.size)?.let { + val fromChain = state?.input?.transfer?.chain ?: return + val fromTokenDenom = state.input.transfer.token ?: return + val fromTokenSkipDenom = stateMachine.routerProcessor.getTokenByDenomAndChainId( + tokenDenom = fromTokenDenom, + chainId = fromChain, + )?.get("skipDenom") +// Denoms for tokens on their native chains are returned from the skip API in an incompatible +// format for our frontend SDKs but are required by the skip API for other API calls. +// So we prefer the skimDenom and default to the regular denom for API calls. + val fromTokenDenomForAPIUse = fromTokenSkipDenom ?: fromTokenDenom + val fromAmount = helper.parser.asDecimal(state.input.transfer.size?.size)?.let { val decimals = - helper.parser.asInt(stateMachine.routerProcessor.selectedTokenDecimals(tokenAddress = fromToken, selectedChainId = fromChain)) + helper.parser.asInt(stateMachine.routerProcessor.selectedTokenDecimals(tokenAddress = fromTokenDenom, selectedChainId = fromChain)) if (decimals != null) { (it * Numeric.decimal.TEN.pow(decimals)).toBigInteger() } else { null } } - val chainId = helper.environment.dydxChainId - val nativeChainUSDCDenom = helper.environment.tokens["usdc"]?.denom - val fromAmountString = helper.parser.asString(fromAmount) + val osmosisAddress = accountAddress.toOsmosisAddress() ?: return + val nobleAddress = accountAddress.toNobleAddress() ?: return + val osmosisChainId = helper.configs.osmosisChainId() ?: return + val nobleChainId = helper.configs.nobleChainId() + val chainId = helper.environment.dydxChainId ?: return + val nativeChainUSDCDenom = helper.environment.tokens["usdc"]?.denom ?: return + val fromAmountString = helper.parser.asString(fromAmount) ?: return val url = helper.configs.skipV2MsgsDirect() - if (fromChain != null && - fromToken != null && - fromAmount != null && fromAmount > 0 && - fromAmountString != null && - chainId != null && - nativeChainUSDCDenom != null && - url != null - ) { + if (fromAmount != null && fromAmount > 0) { val body: Map = mapOf( "amount_in" to fromAmountString, - "source_asset_denom" to fromToken, + "source_asset_denom" to fromTokenDenomForAPIUse, "source_asset_chain_id" to fromChain, "dest_asset_denom" to nativeChainUSDCDenom, "dest_asset_chain_id" to chainId, "chain_ids_to_addresses" to mapOf( - "fromChain" to sourceAddress, - "toChain" to accountAddress, + fromChain to sourceAddress, + osmosisChainId to osmosisAddress, + nobleChainId to nobleAddress, + chainId to accountAddress, ), "slippage_tolerance_percent" to SLIPPAGE_PERCENT, ) @@ -995,7 +1000,17 @@ internal class OnboardingSupervisor( subaccountNumber: Int?, ) { val toChain = state?.input?.transfer?.chain ?: return - val toToken = state?.input?.transfer?.token ?: return + val toAddress = state?.input?.transfer?.address + val toTokenDenom = state.input.transfer.token ?: return + val toTokenSkipDenom = stateMachine.routerProcessor.getTokenByDenomAndChainId( + tokenDenom = toTokenDenom, + chainId = toChain, + )?.get("skipDenom") +// Denoms for tokens on their native chains are returned from the skip API in an incompatible +// format for our frontend SDKs but are required by the skip API for other API calls. +// So we prefer the skimDenom and default to the regular denom for API calls. + val toTokenDenomForAPIUse = toTokenSkipDenom ?: toTokenDenom + val usdcSize = helper.parser.asDecimal(state?.input?.transfer?.size?.usdcSize) ?: return val fromAmount = if (usdcSize > gas) { ((usdcSize - gas) * Numeric.decimal.TEN.pow(decimals)).toBigInteger() @@ -1003,6 +1018,10 @@ internal class OnboardingSupervisor( return } if (fromAmount <= 0) return + val osmosisAddress = accountAddress.toOsmosisAddress() ?: return + val nobleAddress = accountAddress.toNobleAddress() ?: return + val osmosisChainId = helper.configs.osmosisChainId() ?: return + val nobleChainId = helper.configs.nobleChainId() val fromChain = helper.environment.dydxChainId ?: return val fromToken = helper.environment.tokens["usdc"]?.denom ?: return val fromAmountString = helper.parser.asString(fromAmount) ?: return @@ -1011,12 +1030,17 @@ internal class OnboardingSupervisor( "amount_in" to fromAmountString, "source_asset_denom" to fromToken, "source_asset_chain_id" to fromChain, - "dest_asset_denom" to toToken, + "dest_asset_denom" to toTokenDenomForAPIUse, "dest_asset_chain_id" to toChain, "chain_ids_to_addresses" to mapOf( - fromChain to sourceAddress, - toChain to accountAddress, + fromChain to accountAddress, + osmosisChainId to osmosisAddress, + nobleChainId to nobleAddress, + "neutron-1" to accountAddress.toNeutronAddress(), + toChain to toAddress, ), + "allow_multi_tx" to true, + "allow_unsafe" to true, "slippage_tolerance_percent" to SLIPPAGE_PERCENT, ) val header = iMapOf( 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 ac468c258..92cec88a7 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/utils/String+Utils.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/utils/String+Utils.kt @@ -24,6 +24,29 @@ fun String.toNobleAddress(): String? { } } +fun String.toOsmosisAddress(): String? { + try { + val (humanReadablePart, data) = Bech32.decode(this) + if (humanReadablePart != "dydx") { + return null + } + return Bech32.encode("osmo", data) + } catch (e: Exception) { + return null + } +} +fun String.toNeutronAddress(): String? { + try { + val (humanReadablePart, data) = Bech32.decode(this) + if (humanReadablePart != "dydx") { + return null + } + return Bech32.encode("neutron1", data) + } catch (e: Exception) { + return null + } +} + fun String.toDydxAddress(): String? { try { val (humanReadablePart, data) = Bech32.decode(this) 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 9c4f1ff0d..e44bf143c 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 @@ -43,7 +43,7 @@ class SkipRouteProcessorTests { * This processes a Dydx -> Noble CCTP transaction */ @Test - fun testReceivedCCTPDydxToNoble() { + fun testReceivedCCTPDydxToNobleWithdrawal() { val payload = skipRouteMock.payloadCCTPDydxToNoble val result = skipRouteProcessor.received(existing = mapOf(), payload = templateToJson(payload), decimals = 6.0) val jsonEncoder = JsonEncoder() @@ -126,7 +126,54 @@ class SkipRouteProcessorTests { ) assertEquals(expected, result) } - + + /** + * Tests a Non-CCTP withdrawal from Dydx to Ethereum + */ + @Test + fun testReceivedNonCCTPDydxToEthWithdrawal() { + val payload = skipRouteMock.payloadDydxToEth + val result = skipRouteProcessor.received(existing = mapOf(), payload = templateToJson(payload), decimals = 18.0) + val jsonEncoder = JsonEncoder() + val expectedMsg = mapOf( + "sourcePort" to "transfer", + "sourceChannel" to "channel-0", + "token" to mapOf( + "denom" to "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", + "amount" to "129996028", + ), + "sender" to "dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s", + "receiver" to "noble1nhzuazjhyfu474er6v4ey8zn6wa5fy6gthndxf", + "timeoutHeight" to mapOf(), + "timeoutTimestamp" to 1718399715601228463, + "memo" to "{\"forward\":{\"channel\":\"channel-1\",\"next\":{\"wasm\":{\"contract\":\"osmo1vkdakqqg5htq5c3wy2kj2geq536q665xdexrtjuwqckpads2c2nsvhhcyv\",\"msg\":{\"swap_and_action\":{\"affiliates\":[],\"min_asset\":{\"native\":{\"amount\":\"37656643372307734\",\"denom\":\"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5\"}},\"post_swap_action\":{\"ibc_transfer\":{\"ibc_info\":{\"memo\":\"{\\\"destination_chain\\\":\\\"Ethereum\\\",\\\"destination_address\\\":\\\"0xD397883c12b71ea39e0d9f6755030205f31A1c96\\\",\\\"payload\\\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,15,120,51,119,123,252,158,247,45,139,118,174,149,77,56,73,221,113,248,41],\\\"type\\\":2,\\\"fee\\\":{\\\"amount\\\":\\\"7692677672185391\\\",\\\"recipient\\\":\\\"axelar1aythygn6z5thymj6tmzfwekzh05ewg3l7d6y89\\\"}}\",\"receiver\":\"axelar1dv4u5k73pzqrxlzujxg3qp8kvc3pje7jtdvu72npnt5zhq05ejcsn5qme5\",\"recover_address\":\"osmo1nhzuazjhyfu474er6v4ey8zn6wa5fy6gt044g4\",\"source_channel\":\"channel-208\"}}},\"timeout_timestamp\":1718399715601274600,\"user_swap\":{\"swap_exact_asset_in\":{\"operations\":[{\"denom_in\":\"ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4\",\"denom_out\":\"factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc\",\"pool\":\"1437\"},{\"denom_in\":\"factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc\",\"denom_out\":\"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5\",\"pool\":\"1441\"}],\"swap_venue_name\":\"osmosis-poolmanager\"}}}}}},\"port\":\"transfer\",\"receiver\":\"osmo1vkdakqqg5htq5c3wy2kj2geq536q665xdexrtjuwqckpads2c2nsvhhcyv\",\"retries\":2,\"timeout\":1718399715601230847}}", + ) + 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 103.17, + "toAmount" to 0.03034433583519616, + "aggregatePriceImpact" to "0.2607", + "bridgeFees" to 26.15, + "slippage" to "1", + "requestPayload" to mapOf( + "fromChainId" to "dydx-mainnet-1", + "fromAddress" to "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", + "toChainId" to "1", + "toAddress" to "ethereum-native", + "data" to expectedData, + ), + ) + + assertEquals(expected, result) + } + @Test fun testReceivedError() { val payload = skipRouteMock.payloadError 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 3ea3ec290..c3f88148c 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/SkipRouteMock.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/SkipRouteMock.kt @@ -284,6 +284,237 @@ internal class SkipRouteMock { } } """.trimIndent() + + internal val payloadDydxToEth = """ + { + "msgs": [ + { + "multi_chain_msg": { + "chain_id": "dydx-mainnet-1", + "path": [ + "dydx-mainnet-1", + "noble-1", + "osmosis-1", + "1" + ], + "msg": "{\"source_port\":\"transfer\",\"source_channel\":\"channel-0\",\"token\":{\"denom\":\"ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5\",\"amount\":\"129996028\"},\"sender\":\"dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s\",\"receiver\":\"noble1nhzuazjhyfu474er6v4ey8zn6wa5fy6gthndxf\",\"timeout_height\":{},\"timeout_timestamp\":1718399715601228463,\"memo\":\"{\\\"forward\\\":{\\\"channel\\\":\\\"channel-1\\\",\\\"next\\\":{\\\"wasm\\\":{\\\"contract\\\":\\\"osmo1vkdakqqg5htq5c3wy2kj2geq536q665xdexrtjuwqckpads2c2nsvhhcyv\\\",\\\"msg\\\":{\\\"swap_and_action\\\":{\\\"affiliates\\\":[],\\\"min_asset\\\":{\\\"native\\\":{\\\"amount\\\":\\\"37656643372307734\\\",\\\"denom\\\":\\\"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5\\\"}},\\\"post_swap_action\\\":{\\\"ibc_transfer\\\":{\\\"ibc_info\\\":{\\\"memo\\\":\\\"{\\\\\\\"destination_chain\\\\\\\":\\\\\\\"Ethereum\\\\\\\",\\\\\\\"destination_address\\\\\\\":\\\\\\\"0xD397883c12b71ea39e0d9f6755030205f31A1c96\\\\\\\",\\\\\\\"payload\\\\\\\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,15,120,51,119,123,252,158,247,45,139,118,174,149,77,56,73,221,113,248,41],\\\\\\\"type\\\\\\\":2,\\\\\\\"fee\\\\\\\":{\\\\\\\"amount\\\\\\\":\\\\\\\"7692677672185391\\\\\\\",\\\\\\\"recipient\\\\\\\":\\\\\\\"axelar1aythygn6z5thymj6tmzfwekzh05ewg3l7d6y89\\\\\\\"}}\\\",\\\"receiver\\\":\\\"axelar1dv4u5k73pzqrxlzujxg3qp8kvc3pje7jtdvu72npnt5zhq05ejcsn5qme5\\\",\\\"recover_address\\\":\\\"osmo1nhzuazjhyfu474er6v4ey8zn6wa5fy6gt044g4\\\",\\\"source_channel\\\":\\\"channel-208\\\"}}},\\\"timeout_timestamp\\\":1718399715601274600,\\\"user_swap\\\":{\\\"swap_exact_asset_in\\\":{\\\"operations\\\":[{\\\"denom_in\\\":\\\"ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4\\\",\\\"denom_out\\\":\\\"factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc\\\",\\\"pool\\\":\\\"1437\\\"},{\\\"denom_in\\\":\\\"factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc\\\",\\\"denom_out\\\":\\\"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5\\\",\\\"pool\\\":\\\"1441\\\"}],\\\"swap_venue_name\\\":\\\"osmosis-poolmanager\\\"}}}}}},\\\"port\\\":\\\"transfer\\\",\\\"receiver\\\":\\\"osmo1vkdakqqg5htq5c3wy2kj2geq536q665xdexrtjuwqckpads2c2nsvhhcyv\\\",\\\"retries\\\":2,\\\"timeout\\\":1718399715601230847}}\"}", + "msg_type_url": "/ibc.applications.transfer.v1.MsgTransfer" + } + } + ], + "txs": [ + { + "cosmos_tx": { + "chain_id": "dydx-mainnet-1", + "path": [ + "dydx-mainnet-1", + "noble-1", + "osmosis-1", + "1" + ], + "msgs": [ + { + "msg": "{\"source_port\":\"transfer\",\"source_channel\":\"channel-0\",\"token\":{\"denom\":\"ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5\",\"amount\":\"129996028\"},\"sender\":\"dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s\",\"receiver\":\"noble1nhzuazjhyfu474er6v4ey8zn6wa5fy6gthndxf\",\"timeout_height\":{},\"timeout_timestamp\":1718399715601228463,\"memo\":\"{\\\"forward\\\":{\\\"channel\\\":\\\"channel-1\\\",\\\"next\\\":{\\\"wasm\\\":{\\\"contract\\\":\\\"osmo1vkdakqqg5htq5c3wy2kj2geq536q665xdexrtjuwqckpads2c2nsvhhcyv\\\",\\\"msg\\\":{\\\"swap_and_action\\\":{\\\"affiliates\\\":[],\\\"min_asset\\\":{\\\"native\\\":{\\\"amount\\\":\\\"37656643372307734\\\",\\\"denom\\\":\\\"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5\\\"}},\\\"post_swap_action\\\":{\\\"ibc_transfer\\\":{\\\"ibc_info\\\":{\\\"memo\\\":\\\"{\\\\\\\"destination_chain\\\\\\\":\\\\\\\"Ethereum\\\\\\\",\\\\\\\"destination_address\\\\\\\":\\\\\\\"0xD397883c12b71ea39e0d9f6755030205f31A1c96\\\\\\\",\\\\\\\"payload\\\\\\\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,15,120,51,119,123,252,158,247,45,139,118,174,149,77,56,73,221,113,248,41],\\\\\\\"type\\\\\\\":2,\\\\\\\"fee\\\\\\\":{\\\\\\\"amount\\\\\\\":\\\\\\\"7692677672185391\\\\\\\",\\\\\\\"recipient\\\\\\\":\\\\\\\"axelar1aythygn6z5thymj6tmzfwekzh05ewg3l7d6y89\\\\\\\"}}\\\",\\\"receiver\\\":\\\"axelar1dv4u5k73pzqrxlzujxg3qp8kvc3pje7jtdvu72npnt5zhq05ejcsn5qme5\\\",\\\"recover_address\\\":\\\"osmo1nhzuazjhyfu474er6v4ey8zn6wa5fy6gt044g4\\\",\\\"source_channel\\\":\\\"channel-208\\\"}}},\\\"timeout_timestamp\\\":1718399715601274600,\\\"user_swap\\\":{\\\"swap_exact_asset_in\\\":{\\\"operations\\\":[{\\\"denom_in\\\":\\\"ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4\\\",\\\"denom_out\\\":\\\"factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc\\\",\\\"pool\\\":\\\"1437\\\"},{\\\"denom_in\\\":\\\"factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc\\\",\\\"denom_out\\\":\\\"ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5\\\",\\\"pool\\\":\\\"1441\\\"}],\\\"swap_venue_name\\\":\\\"osmosis-poolmanager\\\"}}}}}},\\\"port\\\":\\\"transfer\\\",\\\"receiver\\\":\\\"osmo1vkdakqqg5htq5c3wy2kj2geq536q665xdexrtjuwqckpads2c2nsvhhcyv\\\",\\\"retries\\\":2,\\\"timeout\\\":1718399715601230847}}\"}", + "msg_type_url": "/ibc.applications.transfer.v1.MsgTransfer" + } + ], + "signer_address": "dydx1nhzuazjhyfu474er6v4ey8zn6wa5fy6g2dgp7s" + }, + "operations_indices": [ + 0, + 1, + 2, + 3 + ] + } + ], + "route": { + "source_asset_denom": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", + "source_asset_chain_id": "dydx-mainnet-1", + "dest_asset_denom": "ethereum-native", + "dest_asset_chain_id": "1", + "amount_in": "129996028", + "amount_out": "30344335835196158", + "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": "129996028", + "amount_out": "129996028" + }, + { + "transfer": { + "port": "transfer", + "channel": "channel-1", + "from_chain_id": "noble-1", + "to_chain_id": "osmosis-1", + "pfm_enabled": true, + "supports_memo": true, + "denom_in": "uusdc", + "denom_out": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + "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/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4" + }, + "tx_index": 0, + "amount_in": "129996028", + "amount_out": "129996028" + }, + { + "swap": { + "swap_in": { + "swap_venue": { + "name": "osmosis-poolmanager", + "chain_id": "osmosis-1", + "logo_uri": "https://raw.githubusercontent.com/skip-mev/skip-api-registry/main/swap-venues/osmosis/logo.png" + }, + "swap_operations": [ + { + "pool": "1437", + "denom_in": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + "denom_out": "factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc" + }, + { + "pool": "1441", + "denom_in": "factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc", + "denom_out": "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5" + } + ], + "swap_amount_in": "129996028", + "price_impact_percent": "0.2607" + }, + "estimated_affiliate_fee": "0ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + "from_chain_id": "osmosis-1", + "chain_id": "osmosis-1", + "denom_in": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + "denom_out": "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + "swap_venues": [ + { + "name": "osmosis-poolmanager", + "chain_id": "osmosis-1", + "logo_uri": "https://raw.githubusercontent.com/skip-mev/skip-api-registry/main/swap-venues/osmosis/logo.png" + } + ] + }, + "tx_index": 0, + "amount_in": "129996028", + "amount_out": "38037013507381549" + }, + { + "axelar_transfer": { + "from_chain": "osmosis", + "from_chain_id": "osmosis-1", + "to_chain": "Ethereum", + "to_chain_id": "1", + "asset": "weth-wei", + "should_unwrap": true, + "denom_in": "weth-wei", + "denom_out": "ethereum-native", + "fee_amount": "7692677672185391", + "usd_fee_amount": "26.15", + "fee_asset": { + "denom": "ethereum-native", + "chain_id": "1", + "origin_denom": "", + "origin_chain_id": "", + "trace": "", + "is_cw20": false, + "is_evm": true, + "is_svm": false, + "symbol": "ETH", + "name": "Ethereum", + "logo_uri": "https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/eth-blue.svg", + "decimals": 18, + "token_contract": "" + }, + "is_testnet": false, + "ibc_transfer_to_axelar": { + "port": "transfer", + "channel": "channel-208", + "from_chain_id": "osmosis-1", + "to_chain_id": "axelar-dojo-1", + "pfm_enabled": true, + "supports_memo": true, + "denom_in": "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", + "denom_out": "weth-wei", + "bridge_id": "IBC", + "smart_relay": false, + "chain_id": "osmosis-1", + "dest_denom": "weth-wei" + }, + "bridge_id": "AXELAR", + "smart_relay": false + }, + "tx_index": 0, + "amount_in": "38037013507381549", + "amount_out": "30344335835196158" + } + ], + "chain_ids": [ + "dydx-mainnet-1", + "noble-1", + "osmosis-1", + "1" + ], + "does_swap": true, + "estimated_amount_out": "30344335835196158", + "swap_venues": [ + { + "name": "osmosis-poolmanager", + "chain_id": "osmosis-1", + "logo_uri": "https://raw.githubusercontent.com/skip-mev/skip-api-registry/main/swap-venues/osmosis/logo.png" + } + ], + "txs_required": 1, + "usd_amount_in": "130.13", + "usd_amount_out": "103.17", + "swap_price_impact_percent": "0.2607", + "warning": { + "type": "BAD_PRICE_WARNING", + "message": "Difference in USD value of route input and output is large. Input USD value: 130.13 Output USD value: 103.17" + }, + "estimated_fees": [], + "required_chain_addresses": [ + "dydx-mainnet-1", + "noble-1", + "osmosis-1", + "1" + ], + "swap_venue": { + "name": "osmosis-poolmanager", + "chain_id": "osmosis-1", + "logo_uri": "https://raw.githubusercontent.com/skip-mev/skip-api-registry/main/swap-venues/osmosis/logo.png" + } + } + } + """.trimIndent() + internal val payloadError = """ { "code": 3,