Skip to content

Commit

Permalink
fix: qa route endpoint
Browse files Browse the repository at this point in the history
feat: enable withdrawals. also small fixes

Bump version

cleanup suppression

remove squid integrator id check from skip method

address detekt complaint

cleanup
  • Loading branch information
yogurtandjam committed Jun 14, 2024
1 parent 6ff73c3 commit 5c6a01e
Show file tree
Hide file tree
Showing 7 changed files with 405 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -131,7 +132,10 @@ internal class SkipProcessor(
payload: Map<String, Any>,
transactionId: String?,
): Map<String, Any>? {
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<String, Any>, selectedChainId: String?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ 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

@Suppress("ForbiddenComment")
// TODO: decide if we 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(
Expand All @@ -25,17 +30,50 @@ internal class SkipRoutePayloadProcessor(parser: ParserProtocol) : BaseProcessor
),
)

enum class TxType {
EVM,
COSMOS
}

// Create custom exceptions for better error handling specificity and expressiveness
@Suppress("TooGenericExceptionThrown")
internal fun getTxType(payload: Map<String, Any>): 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<String, Any>?,
payload: Map<String, Any>
): Map<String, Any> {
val txType = getTxType(payload)
val modified = transform(existing, payload, keyMap)
val data = modified.get("data")
if (data != null) {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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" }
Expand Down Expand Up @@ -582,7 +586,19 @@ internal open class AccountSupervisor(
}
}

@Suppress("ForbiddenComment")
// TODO: 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()
Expand Down Expand Up @@ -638,6 +654,63 @@ 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()
val chainId = helper.environment.dydxChainId
val dydxTokenDemon = helper.environment.tokens["usdc"]?.denom
if (
fromChain != null &&
fromToken != null &&
nobleAddress != null &&
chainId != null &&
dydxTokenDemon != null
) {
val body: Map<String, Any> = 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.toString(),
),
"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
Expand Down
12 changes: 12 additions & 0 deletions src/commonMain/kotlin/exchange.dydx.abacus/utils/Map+Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,15 @@ fun IMap<String, String>.toUrlParams(): String =
entries.joinToString("&") {
it.key + "=" + it.value
}

fun Map<String, Any>.toCamelCaseKeys(): Map<String, Any> {
return this.mapKeys { it.key.toCamelCase() }.mapValues { (_, value) ->
when (value) {
is Map<*, *> -> (value as Map<String, Any>).toCamelCaseKeys()
is List<*> -> value.map {
if (it is Map<*, *>) (it as Map<String, Any>).toCamelCaseKeys() else it
}
else -> value
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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
Expand All @@ -12,7 +13,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() {
Expand All @@ -36,6 +38,96 @@ class SkipRouteProcessorTests {
assertTrue(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<String, Any>(),
"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<String, Any>(),
"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
fun testReceivedError() {
val payload = skipRouteMock.payloadError
Expand Down
Loading

0 comments on commit 5c6a01e

Please sign in to comment.