Skip to content

Commit

Permalink
feat: top level withdrawal methods [OTE-389] [2/n] (#443)
Browse files Browse the repository at this point in the history
Co-authored-by: mobile-build-bot-git <[email protected]>
  • Loading branch information
yogurtandjam and mobile-build-bot authored Jun 19, 2024
1 parent 12b2d06 commit 97487af
Show file tree
Hide file tree
Showing 11 changed files with 418 additions and 37 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ allprojects {
}

group = "exchange.dydx.abacus"
version = "1.7.85"
version = "1.7.86"

repositories {
google()
Expand Down
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,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(
Expand All @@ -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<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) {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,22 @@ 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
// this allows to match the current errors format.
// 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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand All @@ -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"
}

Expand All @@ -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() {
Expand Down
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,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
Expand Down Expand Up @@ -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<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,
),
"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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<String, Any> = mapOf(
"amount_in" to fromAmountString,
"source_asset_denom" to fromToken,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
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("")
}
Loading

0 comments on commit 97487af

Please sign in to comment.