From 5ed1516233a523b11b3b1f569d3476a72265bbaa Mon Sep 17 00:00:00 2001 From: Prashan Dharmasena <163016611+prashanDYDX@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:36:24 -0400 Subject: [PATCH] Convert AsyncStep to suspending functions instead of Flows (#215) Since AsyncSteps are meant to be executed only once and we do not seem to use the `Progress` state for anything, it is far simpler / readable to express them as suspending functions. This PR also adds some logging to AsyncStep and removes EthGetNonceStep (as discussed in t-clients) Tested working with both MetaMask and Trust. --- .../DydxTransferDepositCtaButtonModel.kt | 76 +++++----- .../deposit/DydxTransferDepositStep.kt | 96 +++++-------- .../transfer/deposit/ERC20ApprovalStep.kt | 111 +++++---------- .../transfer/deposit/EnableERC20TokenStep.kt | 69 ++++----- .../feature/transfer/steps/DydxScreenStep.kt | 89 ++++++------ .../DydxTransferOutCtaButtonModel.kt | 131 +++++++++--------- .../transferout/DydxTransferOutDYDXStep.kt | 64 ++++----- .../transferout/DydxTransferOutUSDCStep.kt | 63 ++++----- .../DydxTransferWithdrawalCtaButtonModel.kt | 105 +++++++------- .../withdrawal/DydxWithdrawToIBCStep.kt | 73 +++++----- .../steps/WalletSendTransactionStep.kt | 51 ++++--- .../dydx/web3/steps/EthEstimateGasStep.kt | 30 ---- .../web3/steps/EthGetERC20AllowanceStep.kt | 16 +-- .../dydx/web3/steps/EthGetGasPriceStep.kt | 28 ---- .../dydx/web3/steps/EthGetNonceStep.kt | 32 ----- .../dydx/utilities/utils/AsyncStep.kt | 31 ++--- 16 files changed, 421 insertions(+), 644 deletions(-) delete mode 100644 v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthEstimateGasStep.kt delete mode 100644 v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetGasPriceStep.kt delete mode 100644 v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetNonceStep.kt diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositCtaButtonModel.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositCtaButtonModel.kt index 309dc93c..116bedf6 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositCtaButtonModel.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositCtaButtonModel.kt @@ -15,6 +15,7 @@ import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.dydxstatemanager.clientState.wallets.DydxWalletInstance import exchange.dydx.dydxstatemanager.localizedString import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.di.CoroutineScopes import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.OnboardingRoutes import exchange.dydx.trading.common.navigation.TransferRoutes @@ -25,7 +26,8 @@ import exchange.dydx.trading.feature.transfer.DydxTransferError import exchange.dydx.trading.feature.transfer.utils.DydxTransferInstanceStoring import exchange.dydx.trading.feature.transfer.utils.chainName import exchange.dydx.trading.feature.transfer.utils.networkName -import exchange.dydx.utilities.utils.AsyncEvent +import exchange.dydx.utilities.utils.runWithLogs +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine @@ -33,6 +35,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -46,6 +49,7 @@ class DydxTransferDepositCtaButtonModel @Inject constructor( private val errorFlow: MutableStateFlow<@JvmSuppressWildcards DydxTransferError?>, private val onboardingAnalytics: OnboardingAnalytics, private val transferAnalytics: TransferAnalytics, + @CoroutineScopes.App private val appScope: CoroutineScope, ) : ViewModel(), DydxViewModel { private val carteraProvider: CarteraProvider = CarteraProvider(context) private val isSubmittingFlow: MutableStateFlow = MutableStateFlow(false) @@ -141,43 +145,41 @@ class DydxTransferDepositCtaButtonModel @Inject constructor( val chainRpc = transferInput.resources?.chainResources?.get(chain)?.rpc ?: return val tokenAddress = transferInput.resources?.tokenResources?.get(token)?.address ?: return - DydxTransferDepositStep( - transferInput = transferInput, - provider = carteraProvider, - walletAddress = walletAddress, - walletId = wallet.walletId, - chainRpc = chainRpc, - tokenAddress = tokenAddress, - context = context, - ) - .run() - .onEach { event -> - val eventResult = event as? AsyncEvent.Result ?: return@onEach - isSubmittingFlow.value = false - val hash = eventResult.result - val error = eventResult.error - if (hash != null) { - sendOnboardingAnalytics() - transferAnalytics.logDeposit(transferInput) - abacusStateManager.resetTransferInputFields() - transferInstanceStore.addTransferHash( - hash = hash, - fromChainName = transferInput.chainName ?: transferInput.networkName, - toChainName = abacusStateManager.environment?.chainName, - transferInput = transferInput, - ) - router.navigateBack() - router.navigateTo( - route = TransferRoutes.transfer_status + "/$hash", - presentation = DydxRouter.Presentation.Modal, - ) - } else if (error != null) { - errorFlow.value = DydxTransferError( - message = error.localizedMessage ?: "", - ) - } + appScope.launch { + val event = + DydxTransferDepositStep( + transferInput = transferInput, + provider = carteraProvider, + walletAddress = walletAddress, + walletId = wallet.walletId, + chainRpc = chainRpc, + tokenAddress = tokenAddress, + context = context, + ).runWithLogs() + + isSubmittingFlow.value = false + val hash = event.getOrNull() + if (hash != null) { + sendOnboardingAnalytics() + transferAnalytics.logDeposit(transferInput) + abacusStateManager.resetTransferInputFields() + transferInstanceStore.addTransferHash( + hash = hash, + fromChainName = transferInput.chainName ?: transferInput.networkName, + toChainName = abacusStateManager.environment?.chainName, + transferInput = transferInput, + ) + router.navigateBack() + router.navigateTo( + route = TransferRoutes.transfer_status + "/$hash", + presentation = DydxRouter.Presentation.Modal, + ) + } else { + errorFlow.value = DydxTransferError( + message = event.exceptionOrNull()?.message ?: "Transfer error", + ) } - .launchIn(viewModelScope) + } } private fun sendOnboardingAnalytics() { diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositStep.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositStep.kt index 0417e693..8affdaf0 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositStep.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/DydxTransferDepositStep.kt @@ -5,14 +5,8 @@ import exchange.dydx.abacus.output.input.TransferInput import exchange.dydx.cartera.CarteraProvider import exchange.dydx.cartera.walletprovider.EthereumTransactionRequest import exchange.dydx.dydxCartera.steps.WalletSendTransactionStep -import exchange.dydx.utilities.utils.AsyncEvent import exchange.dydx.utilities.utils.AsyncStep -import exchange.dydx.web3.EthereumInteractor -import exchange.dydx.web3.steps.EthGetNonceStep -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf +import exchange.dydx.utilities.utils.runWithLogs import java.math.BigInteger import kotlin.math.pow @@ -24,18 +18,17 @@ class DydxTransferDepositStep( private val chainRpc: String, private val tokenAddress: String, private val context: Context, -) : AsyncStep { +) : AsyncStep { - @OptIn(ExperimentalCoroutinesApi::class) - override fun run(): Flow> { - val requestPayload = transferInput.requestPayload ?: return flowOf(invalidInputEvent) - val targetAddress = requestPayload.targetAddress ?: return flowOf(invalidInputEvent) - val tokenSize = transferInput.tokenSize ?: return flowOf(invalidInputEvent) - val walletId = walletId ?: return flowOf(invalidInputEvent) - val chainId = transferInput.chain ?: return flowOf(invalidInputEvent) - val value = requestPayload.value ?: return flowOf(invalidInputEvent) + override suspend fun run(): Result { + val requestPayload = transferInput.requestPayload ?: return invalidInputEvent + val targetAddress = requestPayload.targetAddress ?: return invalidInputEvent + val tokenSize = transferInput.tokenSize ?: return invalidInputEvent + val walletId = walletId ?: return invalidInputEvent + val chainId = transferInput.chain ?: return invalidInputEvent + val value = requestPayload.value ?: return invalidInputEvent - return EnableERC20TokenStep( + val approveERC20Result = EnableERC20TokenStep( chainRpc = chainRpc, tokenAddress = tokenAddress, ethereumAddress = walletAddress, @@ -45,51 +38,34 @@ class DydxTransferDepositStep( chainId = chainId, provider = provider, context = context, - ).run() - .flatMapLatest { event -> - val eventResult = - event as? AsyncEvent.Result ?: return@flatMapLatest flowOf() - val approved = eventResult.result - val error = eventResult.error - if (error != null || approved == false) { - return@flatMapLatest flowOf(errorEvent(error?.message ?: "Token not enabled")) - } + ).runWithLogs() - EthGetNonceStep( - address = walletAddress, - ethereumInteractor = EthereumInteractor(chainRpc), - ).run() - } - .flatMapLatest { event -> - val eventResult = - event as? AsyncEvent.Result ?: return@flatMapLatest flowOf() - val nonce = eventResult.result as? BigInteger - val error = eventResult.error - if (error != null || nonce == null) { - return@flatMapLatest flowOf(errorEvent(error?.message ?: "Invalid Nonce")) - } + val approved = approveERC20Result.getOrNull() + if (approveERC20Result.isFailure || approved == false) { + return errorEvent(approveERC20Result.exceptionOrNull()?.message ?: "Token not enabled") + } + + val transaction = EthereumTransactionRequest( + fromAddress = walletAddress, + toAddress = targetAddress, + weiValue = value.toBigInteger(), + data = requestPayload.data ?: "0x0", + nonce = null, + gasPriceInWei = requestPayload.gasPrice?.toBigInteger(), + maxFeePerGas = requestPayload.maxFeePerGas?.toBigInteger(), + maxPriorityFeePerGas = requestPayload.maxPriorityFeePerGas?.toBigInteger(), + gasLimit = requestPayload.gasLimit?.toBigInteger(), + chainId = chainId, + ) - val transaction = EthereumTransactionRequest( - fromAddress = walletAddress, - toAddress = targetAddress, - weiValue = value.toBigInteger(), - data = requestPayload.data ?: "0x0", - nonce = nonce?.toInt(), - gasPriceInWei = requestPayload.gasPrice?.toBigInteger(), - maxFeePerGas = requestPayload.maxFeePerGas?.toBigInteger(), - maxPriorityFeePerGas = requestPayload.maxPriorityFeePerGas?.toBigInteger(), - gasLimit = requestPayload.gasLimit?.toBigInteger(), - chainId = chainId, - ) - WalletSendTransactionStep( - transaction = transaction, - chainId = chainId, - walletAddress = walletAddress, - walletId = walletId, - context = context, - provider = provider, - ).run() - } + return WalletSendTransactionStep( + transaction = transaction, + chainId = chainId, + walletAddress = walletAddress, + walletId = walletId, + context = context, + provider = provider, + ).runWithLogs() } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/ERC20ApprovalStep.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/ERC20ApprovalStep.kt index b5c02c84..36b774fe 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/ERC20ApprovalStep.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/ERC20ApprovalStep.kt @@ -4,22 +4,12 @@ import android.content.Context import exchange.dydx.cartera.CarteraProvider import exchange.dydx.cartera.walletprovider.EthereumTransactionRequest import exchange.dydx.dydxCartera.steps.WalletSendTransactionStep -import exchange.dydx.utilities.utils.AsyncEvent import exchange.dydx.utilities.utils.AsyncStep +import exchange.dydx.utilities.utils.runWithLogs import exchange.dydx.web3.ABIEncoder -import exchange.dydx.web3.EthereumInteractor -import exchange.dydx.web3.steps.EthEstimateGasStep -import exchange.dydx.web3.steps.EthGetGasPriceStep -import exchange.dydx.web3.steps.EthGetNonceStep -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import java.math.BigInteger class ERC20ApprovalStep( - private val chainRpc: String, private val tokenAddress: String, private val ethereumAddress: String, private val spenderAddress: String, @@ -28,77 +18,40 @@ class ERC20ApprovalStep( private val chainId: String, private val provider: CarteraProvider, private val context: Context, -) : AsyncStep { +) : AsyncStep { - private val ethereumInteractor = EthereumInteractor(chainRpc) - - override fun run(): Flow> { - return combine( - EthGetGasPriceStep( - ethereumInteractor = ethereumInteractor, - ).run().filter { it.isResult }, - EthEstimateGasStep( - ethereumInteractor = ethereumInteractor, - ).run().filter { it.isResult }, - EthGetNonceStep( - address = ethereumAddress, - ethereumInteractor = ethereumInteractor, - ).run().filter { it.isResult }, - ) { gasPriceEvent, gasEstimateEvent, nonceEvent -> - val gasPrice = (gasPriceEvent as? AsyncEvent.Result)?.result - val gasEstimate = (gasEstimateEvent as? AsyncEvent.Result)?.result - val nonce = (nonceEvent as? AsyncEvent.Result)?.result - if (gasPrice != null && nonce != null) { - return@combine Triple(gasPrice, gasEstimate, nonce) - } else { - return@combine null - } + override suspend fun run(): Result { + val function = if (desiredAmount != null) { + ABIEncoder.encodeERC20ApproveFunction( + spenderAddress = spenderAddress, + desiredAmount = desiredAmount, + ) + } else { + ABIEncoder.encodeERC20ApproveFunction( + spenderAddress = spenderAddress, + ) } - .filter { it != null } - .flatMapLatest { triple -> - val (gasPrice, gasEstimate, nonce) = triple ?: return@flatMapLatest flowOf() - val function = if (desiredAmount != null) { - ABIEncoder.encodeERC20ApproveFunction( - spenderAddress = spenderAddress, - desiredAmount = desiredAmount, - ) - } else { - ABIEncoder.encodeERC20ApproveFunction( - spenderAddress = spenderAddress, - ) - } + val transaction = EthereumTransactionRequest( + fromAddress = ethereumAddress, + toAddress = tokenAddress, + weiValue = BigInteger.valueOf(0), + data = function, + nonce = null, + gasPriceInWei = null, + maxFeePerGas = null, + maxPriorityFeePerGas = null, + gasLimit = null, + chainId = chainId, + ) - val transaction = EthereumTransactionRequest( - fromAddress = ethereumAddress, - toAddress = tokenAddress, - weiValue = BigInteger.valueOf(0), - data = function, - nonce = nonce.toInt(), - gasPriceInWei = gasPrice, - maxFeePerGas = null, - maxPriorityFeePerGas = null, - gasLimit = gasEstimate, - chainId = chainId, - ) - WalletSendTransactionStep( - transaction = transaction, - chainId = chainId, - walletAddress = ethereumAddress, - walletId = walletId, - context = context, - provider = provider, - ).run() - } - .flatMapLatest { event -> - val eventResult = event as? AsyncEvent.Result ?: return@flatMapLatest flowOf() - val result = eventResult.result - val error = eventResult.error - if (result != null) { - return@flatMapLatest flowOf(AsyncEvent.Result(result = true, error = null)) - } else { - return@flatMapLatest flowOf(AsyncEvent.Result(result = false, error = error)) - } - } + return WalletSendTransactionStep( + transaction = transaction, + chainId = chainId, + walletAddress = ethereumAddress, + walletId = walletId, + context = context, + provider = provider, + ).runWithLogs().map { true } } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/EnableERC20TokenStep.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/EnableERC20TokenStep.kt index 80c0237e..31da0534 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/EnableERC20TokenStep.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/EnableERC20TokenStep.kt @@ -2,14 +2,10 @@ package exchange.dydx.trading.feature.transfer.deposit import android.content.Context import exchange.dydx.cartera.CarteraProvider -import exchange.dydx.utilities.utils.AsyncEvent import exchange.dydx.utilities.utils.AsyncStep +import exchange.dydx.utilities.utils.runWithLogs import exchange.dydx.web3.EthereumInteractor import exchange.dydx.web3.steps.EthGetERC20AllowanceStep -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import java.math.BigInteger class EnableERC20TokenStep( @@ -22,53 +18,36 @@ class EnableERC20TokenStep( private val chainId: String, private val provider: CarteraProvider, private val context: Context, -) : AsyncStep { +) : AsyncStep { - override fun run(): Flow> { + override suspend fun run(): Result { if (tokenAddress == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE") { - return flowOf(AsyncEvent.Result(result = true, error = null)) + return Result.success(true) } - return EthGetERC20AllowanceStep( + val event = EthGetERC20AllowanceStep( ethereumInteractor = EthereumInteractor(chainRpc), tokenAddress = tokenAddress, ethereumAddress = ethereumAddress, spenderAddress = spenderAddress, - ).run() - .filter { it.isResult } - .flatMapLatest { event -> - val eventResult = event as? AsyncEvent.Result ?: return@flatMapLatest flowOf() - val allowance = eventResult.result - val error = eventResult.error - if (allowance != null) { - if (allowance >= desiredAmount) { - return@flatMapLatest flowOf( - AsyncEvent.Result( - result = true, - error = null, - ), - ) - } else { - ERC20ApprovalStep( - chainRpc = chainRpc, - tokenAddress = tokenAddress, - ethereumAddress = ethereumAddress, - spenderAddress = spenderAddress, - desiredAmount = desiredAmount, - walletId = walletId, - chainId = chainId, - provider = provider, - context = context, - ).run() - } - } else { - return@flatMapLatest flowOf( - AsyncEvent.Result( - result = false, - error = error, - ), - ) - } - } + ).runWithLogs() + + if (event.isFailure) return errorEvent(event.exceptionOrNull()?.message ?: "unknown error") + + val allowance = event.getOrThrow() + if (allowance >= desiredAmount) { + return Result.success(true) + } else { + return ERC20ApprovalStep( + tokenAddress = tokenAddress, + ethereumAddress = ethereumAddress, + spenderAddress = spenderAddress, + desiredAmount = desiredAmount, + walletId = walletId, + chainId = chainId, + provider = provider, + context = context, + ).runWithLogs() + } } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/steps/DydxScreenStep.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/steps/DydxScreenStep.kt index c3e6a235..97a3567f 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/steps/DydxScreenStep.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/steps/DydxScreenStep.kt @@ -4,29 +4,22 @@ import exchange.dydx.abacus.output.Restriction import exchange.dydx.abacus.output.input.TransferInput import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.trading.feature.shared.DydxScreenResult -import exchange.dydx.utilities.utils.AsyncEvent import exchange.dydx.utilities.utils.AsyncStep -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filter +import exchange.dydx.utilities.utils.runWithLogs +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class DydxScreenStep( val address: String, val abacusStateManager: AbacusStateManagerProtocol, -) : AsyncStep { +) : AsyncStep { - private val eventFlow: MutableStateFlow> = MutableStateFlow(AsyncEvent.Progress(Unit)) - - override fun run(): Flow> { + override suspend fun run(): Result = suspendCoroutine { continuation -> abacusStateManager.screen(address = address) { - eventFlow.value = AsyncEvent.Result( - result = it, - error = null, - ) + continuation.resume(Result.success(it)) } - - return eventFlow } } @@ -35,46 +28,50 @@ class DydxTransferScreenStep( val destinationAddress: String, val transferInput: TransferInput, val abacusStateManager: AbacusStateManagerProtocol, -) : AsyncStep { +) : AsyncStep { + + override suspend fun run(): Result = + coroutineScope { + val originationScreenAsync = async { + DydxScreenStep( + address = originationAddress, + abacusStateManager = abacusStateManager, + ).runWithLogs() + } - override fun run(): Flow> { - return combine( - DydxScreenStep( - address = originationAddress, - abacusStateManager = abacusStateManager, - ).run().filter { it.isResult }, - DydxScreenStep( - address = destinationAddress, - abacusStateManager = abacusStateManager, - ).run().filter { it.isResult }, - ) { originationScreen, destinationScreen -> - if (originationScreen is AsyncEvent.Result && originationScreen.result == Restriction.USER_RESTRICTED) { - return@combine AsyncEvent.Result( - result = DydxScreenResult.SourceRestriction, - error = null, + val destinationScreenAsync = async { + DydxScreenStep( + address = destinationAddress, + abacusStateManager = abacusStateManager, + ).runWithLogs() + } + + val originationScreen = originationScreenAsync.await() + val destinationScreen = destinationScreenAsync.await() + + if (originationScreen.isSuccess && originationScreen.getOrThrow() == Restriction.USER_RESTRICTED) { + return@coroutineScope Result.success( + DydxScreenResult.SourceRestriction, ) } - if (destinationScreen is AsyncEvent.Result && destinationScreen.result == Restriction.USER_RESTRICTED) { - return@combine AsyncEvent.Result( - result = DydxScreenResult.DestinationRestriction, - error = null, + if (destinationScreen.isSuccess && destinationScreen.getOrThrow() == Restriction.USER_RESTRICTED) { + return@coroutineScope Result.success( + DydxScreenResult.DestinationRestriction, ) } listOf(originationScreen, destinationScreen).forEach { - if (it is AsyncEvent.Result) { - when (it.result) { + if (it.isSuccess) { + when (it.getOrThrow()) { Restriction.GEO_RESTRICTED -> { - return@combine AsyncEvent.Result( - result = DydxScreenResult.GeoRestriction, - error = null, + return@coroutineScope Result.success( + DydxScreenResult.GeoRestriction, ) } Restriction.USER_RESTRICTION_UNKNOWN -> { - return@combine AsyncEvent.Result( - result = DydxScreenResult.UnknownRestriction, - error = null, + return@coroutineScope Result.success( + DydxScreenResult.UnknownRestriction, ) } @@ -85,10 +82,8 @@ class DydxTransferScreenStep( } } - AsyncEvent.Result( - result = DydxScreenResult.NoRestriction, - error = null, + return@coroutineScope Result.success( + DydxScreenResult.NoRestriction, ) } - } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutCtaButtonModel.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutCtaButtonModel.kt index 7cb53330..05aea588 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutCtaButtonModel.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutCtaButtonModel.kt @@ -17,6 +17,7 @@ import exchange.dydx.dydxstatemanager.nativeTokenKey import exchange.dydx.dydxstatemanager.usdcTokenDenom import exchange.dydx.dydxstatemanager.usdcTokenKey import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.di.CoroutineScopes import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.OnboardingRoutes import exchange.dydx.trading.common.navigation.TransferRoutes @@ -28,17 +29,17 @@ import exchange.dydx.trading.feature.transfer.utils.DydxTransferInstanceStoring import exchange.dydx.trading.feature.transfer.utils.chainName import exchange.dydx.trading.feature.transfer.utils.networkName import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol -import exchange.dydx.utilities.utils.AsyncEvent +import exchange.dydx.utilities.utils.runWithLogs +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch import org.web3j.tuples.generated.Tuple4 import javax.inject.Inject @@ -52,6 +53,7 @@ class DydxTransferOutCtaButtonModel @Inject constructor( private val cosmosClient: CosmosV4WebviewClientProtocol, private val errorFlow: MutableStateFlow<@JvmSuppressWildcards DydxTransferError?>, private val transferInstanceStore: DydxTransferInstanceStoring, + @CoroutineScopes.App private val appScope: CoroutineScope, ) : ViewModel(), DydxViewModel { private val isSubmittingFlow: MutableStateFlow = MutableStateFlow(false) @@ -160,79 +162,72 @@ class DydxTransferOutCtaButtonModel @Inject constructor( val destinationAddress = transferInput.address ?: return val originationAddress = wallet.cosmoAddress ?: return - DydxTransferScreenStep( - originationAddress = originationAddress, - destinationAddress = destinationAddress, - transferInput = transferInput, - abacusStateManager = abacusStateManager, - ).run() - .take(1) - .flatMapLatest { event -> - val emptyFlow = flowOf>>() - val eventResult = event as? AsyncEvent.Result ?: return@flatMapLatest emptyFlow - val result = eventResult.result - screenResultFlow.value = result - when (result) { - DydxScreenResult.NoRestriction -> { - when (transferInput.token) { - abacusStateManager.usdcTokenKey -> - DydxTransferOutUSDCStep( - transferInput = transferInput, - selectedSubaccount = selectedSubaccount, - usdcTokenAmount = usdcTokenAmount, - cosmosClient = cosmosClient, - parser = parser, - localizer = localizer, - ).run() + appScope.launch { + val eventResult = DydxTransferScreenStep( + originationAddress = originationAddress, + destinationAddress = destinationAddress, + transferInput = transferInput, + abacusStateManager = abacusStateManager, + ).runWithLogs() - abacusStateManager.nativeTokenKey -> - DydxTransferOutDYDXStep( - transferInput = transferInput, - nativeTokenAmount = nativeTokenAmount, - cosmosClient = cosmosClient, - parser = parser, - localizer = localizer, - ).run() + val result = eventResult.getOrNull() + screenResultFlow.value = result + val transferResult = when (result) { + DydxScreenResult.NoRestriction -> { + when (transferInput.token) { + abacusStateManager.usdcTokenKey -> + DydxTransferOutUSDCStep( + transferInput = transferInput, + selectedSubaccount = selectedSubaccount, + usdcTokenAmount = usdcTokenAmount, + cosmosClient = cosmosClient, + parser = parser, + localizer = localizer, + ).runWithLogs() - else -> { - isSubmittingFlow.value = false - emptyFlow - } - } - } + abacusStateManager.nativeTokenKey -> + DydxTransferOutDYDXStep( + transferInput = transferInput, + nativeTokenAmount = nativeTokenAmount, + cosmosClient = cosmosClient, + parser = parser, + localizer = localizer, + ).runWithLogs() - else -> { - isSubmittingFlow.value = false - emptyFlow + else -> { + isSubmittingFlow.value = false + return@launch + } } } - } - .onEach { event -> - val event = event as? AsyncEvent.Result ?: return@onEach - val hash = event.result - val error = event.error - if (hash != null) { - abacusStateManager.resetTransferInputFields() - transferInstanceStore.addTransferHash( - hash = hash, - fromChainName = abacusStateManager.environment?.chainName, - toChainName = transferInput.chainName ?: transferInput.networkName, - transferInput = transferInput, - ) - router.navigateBack() - router.navigateTo( - route = TransferRoutes.transfer_status + "/$hash", - presentation = DydxRouter.Presentation.Modal, - ) - } else if (error != null) { - errorFlow.value = DydxTransferError( - message = error.localizedMessage ?: "", - ) + else -> { + isSubmittingFlow.value = false + return@launch } + } - isSubmittingFlow.value = false + val hash = transferResult.getOrNull() + if (hash != null) { + abacusStateManager.resetTransferInputFields() + transferInstanceStore.addTransferHash( + hash = hash, + fromChainName = abacusStateManager.environment?.chainName, + toChainName = transferInput.chainName ?: transferInput.networkName, + transferInput = transferInput, + ) + router.navigateBack() + router.navigateTo( + route = TransferRoutes.transfer_status + "/$hash", + presentation = DydxRouter.Presentation.Modal, + ) + } else { + errorFlow.value = DydxTransferError( + message = transferResult.exceptionOrNull()?.localizedMessage ?: "", + ) } - .launchIn(viewModelScope) + + isSubmittingFlow.value = false + } } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutDYDXStep.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutDYDXStep.kt index 88a224b2..4f15df84 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutDYDXStep.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutDYDXStep.kt @@ -5,12 +5,10 @@ import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.utils.toJson import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol -import exchange.dydx.utilities.utils.AsyncEvent import exchange.dydx.utilities.utils.AsyncStep import exchange.dydx.utilities.utils.jsonStringToMap -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flowOf +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class DydxTransferOutDYDXStep( private val transferInput: TransferInput, @@ -18,20 +16,17 @@ class DydxTransferOutDYDXStep( private val cosmosClient: CosmosV4WebviewClientProtocol, private val parser: ParserProtocol, private val localizer: LocalizerProtocol -) : AsyncStep { +) : AsyncStep { - private val eventFlow: MutableStateFlow> = - MutableStateFlow(AsyncEvent.Progress(Unit)) - - override fun run(): Flow> { - val amount = transferInput.size?.size ?: return flowOf(invalidInputEvent) + override suspend fun run(): Result { + val amount = transferInput.size?.size ?: return invalidInputEvent val amountDecimal = parser.asDouble(amount) ?: 0.0 if (amountDecimal <= 0.0) { - return flowOf(invalidInputEvent) + return invalidInputEvent } val gasFee = transferInput.summary?.gasFee ?: 0.0 val nativeTokenBalanceInWallet = nativeTokenAmount ?: 0.0 - val recipient = transferInput.address ?: return flowOf(invalidInputEvent) + val recipient = transferInput.address ?: return invalidInputEvent if (amountDecimal + gasFee <= nativeTokenBalanceInWallet) { val payload: Map = mapOf( @@ -39,30 +34,29 @@ class DydxTransferOutDYDXStep( "recipient" to recipient, ) val paramsInJson = payload.toJson() - cosmosClient.call( - functionName = "transferNativeToken", - paramsInJson = paramsInJson, - completion = { response -> - val result = response?.jsonStringToMap() ?: return@call - val error = result["error"] as? Map - val transactionHash = parser.asString(result["transactionHash"]) - val hash = parser.asString(result["hash"]) - if (error != null) { - eventFlow.value = errorEvent(error["message"] as? String ?: "Unknown error") - } else if (transactionHash != null) { - eventFlow.value = - AsyncEvent.Result(result = "0x$transactionHash", error = null) - } else if (hash != null) { - eventFlow.value = AsyncEvent.Result(result = "0x$hash", error = null) - } else { - eventFlow.value = errorEvent(localizer.localize("APP.V4.NO_HASH")) - } - }, - ) + return suspendCoroutine { continuation -> + cosmosClient.call( + functionName = "transferNativeToken", + paramsInJson = paramsInJson, + completion = { response -> + val result = response?.jsonStringToMap() ?: return@call + val error = result["error"] as? Map + val transactionHash = parser.asString(result["transactionHash"]) + val hash = parser.asString(result["hash"]) + if (error != null) { + continuation.resume(errorEvent(error["message"] as? String ?: "Unknown error")) + } else if (transactionHash != null) { + continuation.resume(Result.success("0x$transactionHash")) + } else if (hash != null) { + continuation.resume(Result.success("0x$hash")) + } else { + continuation.resume(errorEvent(localizer.localize("APP.V4.NO_HASH"))) + } + }, + ) + } } else { - eventFlow.value = errorEvent(localizer.localize("APP.V4.NO_GAS_BODY")) + return errorEvent(localizer.localize("APP.V4.NO_GAS_BODY")) } - - return eventFlow } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutUSDCStep.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutUSDCStep.kt index 77b273af..1a6a5503 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutUSDCStep.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/transferout/DydxTransferOutUSDCStep.kt @@ -6,12 +6,10 @@ import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.utils.toJson import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol -import exchange.dydx.utilities.utils.AsyncEvent import exchange.dydx.utilities.utils.AsyncStep import exchange.dydx.utilities.utils.jsonStringToMap -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flowOf +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class DydxTransferOutUSDCStep( private val transferInput: TransferInput, @@ -20,21 +18,18 @@ class DydxTransferOutUSDCStep( private val cosmosClient: CosmosV4WebviewClientProtocol, private val parser: ParserProtocol, private val localizer: LocalizerProtocol -) : AsyncStep { +) : AsyncStep { - private val eventFlow: MutableStateFlow> = - MutableStateFlow(AsyncEvent.Progress(Unit)) - - override fun run(): Flow> { - val amount = transferInput.size?.usdcSize ?: return flowOf(invalidInputEvent) + override suspend fun run(): Result { + val amount = transferInput.size?.usdcSize ?: return invalidInputEvent val amountDecimal = parser.asDouble(amount) ?: 0.0 if (amountDecimal <= 0.0) { - return flowOf(invalidInputEvent) + return invalidInputEvent } val subaccountNumber = selectedSubaccount.subaccountNumber val gasFee = transferInput.summary?.gasFee ?: 0.0 val usdcBalanceInWallet = usdcTokenAmount ?: 0.0 - val recipient = transferInput.address ?: return flowOf(invalidInputEvent) + val recipient = transferInput.address ?: return invalidInputEvent if (usdcBalanceInWallet > gasFee) { val payload: Map = mapOf( @@ -43,29 +38,29 @@ class DydxTransferOutUSDCStep( "recipient" to recipient, ) val paramsInJson = payload.toJson() - cosmosClient.call( - functionName = "withdraw", - paramsInJson = paramsInJson, - completion = { response -> - val result = response?.jsonStringToMap() ?: return@call - val error = result["error"] as? Map - val transactionHash = parser.asString(result["transactionHash"]) - val hash = parser.asString(result["hash"]) - if (error != null) { - eventFlow.value = errorEvent(error["message"] as? String ?: "Unknown error") - } else if (transactionHash != null) { - eventFlow.value = AsyncEvent.Result(result = "0x$transactionHash", error = null) - } else if (hash != null) { - eventFlow.value = AsyncEvent.Result(result = "0x$hash", error = null) - } else { - eventFlow.value = errorEvent(localizer.localize("APP.V4.NO_HASH")) - } - }, - ) + return suspendCoroutine { continuation -> + cosmosClient.call( + functionName = "withdraw", + paramsInJson = paramsInJson, + completion = { response -> + val result = response?.jsonStringToMap() ?: return@call + val error = result["error"] as? Map + val transactionHash = parser.asString(result["transactionHash"]) + val hash = parser.asString(result["hash"]) + if (error != null) { + continuation.resume(errorEvent(error["message"] as? String ?: "Unknown error")) + } else if (transactionHash != null) { + continuation.resume(Result.success("0x$transactionHash")) + } else if (hash != null) { + continuation.resume(Result.success("0x$hash")) + } else { + continuation.resume(errorEvent(localizer.localize("APP.V4.NO_HASH"))) + } + }, + ) + } } else { - eventFlow.value = errorEvent(localizer.localize("APP.V4.NO_GAS_BODY")) + return errorEvent(localizer.localize("APP.V4.NO_GAS_BODY")) } - - return eventFlow } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxTransferWithdrawalCtaButtonModel.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxTransferWithdrawalCtaButtonModel.kt index c730145b..87f53477 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxTransferWithdrawalCtaButtonModel.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxTransferWithdrawalCtaButtonModel.kt @@ -13,6 +13,7 @@ import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.dydxstatemanager.clientState.wallets.DydxWalletInstance import exchange.dydx.dydxstatemanager.localizedString import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.di.CoroutineScopes import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.OnboardingRoutes import exchange.dydx.trading.common.navigation.TransferRoutes @@ -25,17 +26,17 @@ import exchange.dydx.trading.feature.transfer.utils.DydxTransferInstanceStoring import exchange.dydx.trading.feature.transfer.utils.chainName import exchange.dydx.trading.feature.transfer.utils.networkName import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol -import exchange.dydx.utilities.utils.AsyncEvent +import exchange.dydx.utilities.utils.runWithLogs +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -49,6 +50,7 @@ class DydxTransferWithdrawalCtaButtonModel @Inject constructor( private val errorFlow: MutableStateFlow<@JvmSuppressWildcards DydxTransferError?>, private val transferInstanceStore: DydxTransferInstanceStoring, private val transferAnalytics: TransferAnalytics, + @CoroutineScopes.App private val appScope: CoroutineScope, ) : ViewModel(), DydxViewModel { private val isSubmittingFlow: MutableStateFlow = MutableStateFlow(false) @@ -154,62 +156,55 @@ class DydxTransferWithdrawalCtaButtonModel @Inject constructor( val destinationAddress = transferInput.address ?: return val originationAddress = wallet?.cosmoAddress ?: return - DydxTransferScreenStep( - originationAddress = originationAddress, - destinationAddress = destinationAddress, - transferInput = transferInput, - abacusStateManager = abacusStateManager, - ).run() - .take(1) - .flatMapLatest { event -> - val eventResult = event as? AsyncEvent.Result ?: return@flatMapLatest flowOf() - val result = eventResult.result - screenResultFlow.value = result - when (result) { - DydxScreenResult.NoRestriction -> { - DydxWithdrawToIBCStep( - transferInput = transferInput, - selectedSubaccount = selectedSubaccount, - cosmosClient = cosmosClient, - parser = parser, - localizer = localizer, - abacusStateManager = abacusStateManager, - ).run() - } - - else -> { - isSubmittingFlow.value = false - flowOf() - } + appScope.launch { + val transferScreenResult = DydxTransferScreenStep( + originationAddress = originationAddress, + destinationAddress = destinationAddress, + transferInput = transferInput, + abacusStateManager = abacusStateManager, + ).runWithLogs() + val result = transferScreenResult.getOrNull() + screenResultFlow.value = result + val withdrawResult = when (result) { + DydxScreenResult.NoRestriction -> { + DydxWithdrawToIBCStep( + transferInput = transferInput, + selectedSubaccount = selectedSubaccount, + cosmosClient = cosmosClient, + parser = parser, + localizer = localizer, + abacusStateManager = abacusStateManager, + ).runWithLogs() } - } - .onEach { event -> - val eventResult = event as? AsyncEvent.Result ?: return@onEach - val hash = eventResult.result - val error = eventResult.error - if (hash != null) { - transferAnalytics.logWithdrawal(transferInput) - transferInstanceStore.addTransferHash( - hash = hash, - fromChainName = abacusStateManager.environment?.chainName, - toChainName = transferInput.chainName ?: transferInput.networkName, - transferInput = transferInput, - ) - abacusStateManager.resetTransferInputFields() - router.navigateBack() - router.navigateTo( - route = TransferRoutes.transfer_status + "/$hash", - presentation = DydxRouter.Presentation.Modal, - ) - } else if (error != null) { - errorFlow.value = DydxTransferError( - message = error.localizedMessage ?: "", - ) + else -> { + isSubmittingFlow.value = false + return@launch } + } - isSubmittingFlow.value = false + val hash = withdrawResult.getOrNull() + if (hash != null) { + transferAnalytics.logWithdrawal(transferInput) + transferInstanceStore.addTransferHash( + hash = hash, + fromChainName = abacusStateManager.environment?.chainName, + toChainName = transferInput.chainName ?: transferInput.networkName, + transferInput = transferInput, + ) + abacusStateManager.resetTransferInputFields() + router.navigateBack() + router.navigateTo( + route = TransferRoutes.transfer_status + "/$hash", + presentation = DydxRouter.Presentation.Modal, + ) + } else { + errorFlow.value = DydxTransferError( + message = withdrawResult.exceptionOrNull()?.localizedMessage ?: "", + ) } - .launchIn(viewModelScope) + + isSubmittingFlow.value = false + } } } diff --git a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxWithdrawToIBCStep.kt b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxWithdrawToIBCStep.kt index 84897a6c..86d12c74 100644 --- a/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxWithdrawToIBCStep.kt +++ b/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/withdrawal/DydxWithdrawToIBCStep.kt @@ -6,12 +6,11 @@ import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.trading.integration.cosmos.CosmosV4WebviewClientProtocol -import exchange.dydx.utilities.utils.AsyncEvent import exchange.dydx.utilities.utils.AsyncStep import exchange.dydx.utilities.utils.jsonStringToMap -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flowOf +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class DydxWithdrawToIBCStep( private val transferInput: TransferInput, @@ -20,50 +19,49 @@ class DydxWithdrawToIBCStep( private val parser: ParserProtocol, private val localizer: LocalizerProtocol, private val abacusStateManager: AbacusStateManagerProtocol, -) : AsyncStep { +) : AsyncStep { - private val eventFlow: MutableStateFlow> = - MutableStateFlow(AsyncEvent.Progress(Unit)) - - override fun run(): Flow> { - val amount = transferInput.size?.usdcSize ?: return flowOf(invalidInputEvent) + override suspend fun run(): Result { + val amount = transferInput.size?.usdcSize ?: return invalidInputEvent val amountDecimal = parser.asDouble(amount) ?: 0.0 if (amountDecimal <= 0.0) { - return flowOf(invalidInputEvent) + return invalidInputEvent } - val data = transferInput.requestPayload?.data ?: return flowOf(invalidInputEvent) + val data = transferInput.requestPayload?.data ?: return invalidInputEvent - if (transferInput.isCctp) { - abacusStateManager.commitCCTPWithdraw { success, parsingError, data -> - if (success) { - val response = data as? String - if (response != null) { - postTransaction(response) + return if (transferInput.isCctp) { + suspendCoroutine { continuation -> + abacusStateManager.commitCCTPWithdraw { success, parsingError, data -> + if (success) { + val response = data as? String + if (response != null) { + postTransaction(response, continuation) + } else { + continuation.resume(errorEvent(localizer.localize("APP.GENERAL.UNKNOWN_ERROR"))) + } } else { - eventFlow.value = errorEvent(localizer.localize("APP.GENERAL.UNKNOWN_ERROR")) + continuation.resume(errorEvent(parsingError?.message ?: localizer.localize("APP.GENERAL.UNKNOWN_ERROR"))) } - } else { - eventFlow.value = errorEvent(parsingError?.message ?: localizer.localize("APP.GENERAL.UNKNOWN_ERROR")) } } } else { - cosmosClient.withdrawToIBC( - subaccount = selectedSubaccount.subaccountNumber, - amount = amount, - payload = data, - completion = { response -> - postTransaction(response) - }, - ) + suspendCoroutine { continuation -> + cosmosClient.withdrawToIBC( + subaccount = selectedSubaccount.subaccountNumber, + amount = amount, + payload = data, + completion = { response -> + postTransaction(response, continuation) + }, + ) + } } - - return eventFlow } - private fun postTransaction(response: String?) { + private fun postTransaction(response: String?, continuation: Continuation>) { val result = response?.jsonStringToMap() if (result == null) { - eventFlow.value = errorEvent(localizer.localize("APP.GENERAL.UNKNOWN_ERROR")) + continuation.resume(errorEvent(localizer.localize("APP.GENERAL.UNKNOWN_ERROR"))) return } @@ -71,14 +69,13 @@ class DydxWithdrawToIBCStep( val transactionHash = parser.asString(result["transactionHash"]) val hash = parser.asString(result["hash"]) if (error != null) { - eventFlow.value = errorEvent(error["message"] as? String ?: "Unknown error") + continuation.resume(errorEvent(error["message"] as? String ?: "Unknown error")) } else if (transactionHash != null) { - eventFlow.value = - AsyncEvent.Result(result = "0x$transactionHash", error = null) + continuation.resume(Result.success("0x$transactionHash")) } else if (hash != null) { - eventFlow.value = AsyncEvent.Result(result = "0x$hash", error = null) + continuation.resume(Result.success("0x$hash")) } else { - eventFlow.value = errorEvent(localizer.localize("APP.V4.NO_HASH")) + continuation.resume(errorEvent(localizer.localize("APP.V4.NO_HASH"))) } } } diff --git a/v4/integration/dydxCartera/src/main/java/exchange/dydx/dydxCartera/steps/WalletSendTransactionStep.kt b/v4/integration/dydxCartera/src/main/java/exchange/dydx/dydxCartera/steps/WalletSendTransactionStep.kt index ae3d63f1..e2c8466b 100644 --- a/v4/integration/dydxCartera/src/main/java/exchange/dydx/dydxCartera/steps/WalletSendTransactionStep.kt +++ b/v4/integration/dydxCartera/src/main/java/exchange/dydx/dydxCartera/steps/WalletSendTransactionStep.kt @@ -1,16 +1,15 @@ package exchange.dydx.dydxCartera.steps import android.content.Context +import android.util.Log import exchange.dydx.cartera.CarteraConfig import exchange.dydx.cartera.CarteraProvider import exchange.dydx.cartera.walletprovider.EthereumTransactionRequest import exchange.dydx.cartera.walletprovider.WalletRequest import exchange.dydx.cartera.walletprovider.WalletTransactionRequest -import exchange.dydx.utilities.utils.AsyncEvent import exchange.dydx.utilities.utils.AsyncStep -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flowOf +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class WalletSendTransactionStep( private val transaction: EthereumTransactionRequest, @@ -19,39 +18,39 @@ class WalletSendTransactionStep( private val walletId: String?, private val context: Context, private val provider: CarteraProvider, -) : AsyncStep { +) : AsyncStep { - private val eventFlow: MutableStateFlow> = MutableStateFlow(AsyncEvent.Progress(Unit)) + override suspend fun run(): Result { + val wallet = CarteraConfig.shared?.wallets?.firstOrNull { it.id == walletId } ?: CarteraConfig.shared?.wallets?.firstOrNull() ?: return invalidInputEvent - override fun run(): Flow> { - val wallet = CarteraConfig.shared?.wallets?.firstOrNull { it.id == walletId } ?: CarteraConfig.shared?.wallets?.firstOrNull() ?: return flowOf(invalidInputEvent) val walletRequest = WalletRequest( wallet = wallet, address = walletAddress, chainId = chainId, context = context, ) - val transaction = WalletTransactionRequest( + val transactionRequest = WalletTransactionRequest( walletRequest = walletRequest, ethereum = transaction, ) - provider.send( - request = transaction, - connected = { info -> - if (info == null) { - eventFlow.value = errorEvent("Wallet not connected") - } - }, - completion = { signed, error -> - if (signed != null) { - eventFlow.value = AsyncEvent.Result(result = signed, error = null) - } else { - eventFlow.value = errorEvent(error?.message ?: "Unknown error") - } - }, - ) - - return eventFlow + return suspendCoroutine { continuation -> + Log.d("AsyncStep", "Sending $transaction") + provider.send( + request = transactionRequest, + connected = { info -> + if (info == null) { + continuation.resume(errorEvent("Wallet not connected")) + } + }, + completion = { signed, error -> + if (signed != null) { + continuation.resume(Result.success(signed)) + } else { + continuation.resume(errorEvent(error?.message ?: "Unknown error")) + } + }, + ) + } } } diff --git a/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthEstimateGasStep.kt b/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthEstimateGasStep.kt deleted file mode 100644 index fd2232c0..00000000 --- a/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthEstimateGasStep.kt +++ /dev/null @@ -1,30 +0,0 @@ -package exchange.dydx.web3.steps - -import exchange.dydx.utilities.utils.AsyncEvent -import exchange.dydx.utilities.utils.AsyncStep -import exchange.dydx.web3.EthereumInteractor -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import java.math.BigInteger - -class EthEstimateGasStep( - private val ethereumInteractor: EthereumInteractor, -) : AsyncStep { - private val eventFlow: MutableStateFlow> = MutableStateFlow( - AsyncEvent.Progress(Unit), - ) - - override fun run(): Flow> { - ethereumInteractor.ethEstimateGas( - completion = { error, gasPrice -> - if (gasPrice != null) { - eventFlow.value = AsyncEvent.Result(result = gasPrice, error = null) - } else { - eventFlow.value = errorEvent(error?.message ?: "Unknown error") - } - }, - ) - - return eventFlow - } -} diff --git a/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetERC20AllowanceStep.kt b/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetERC20AllowanceStep.kt index a760b1a3..e2ab5a8d 100644 --- a/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetERC20AllowanceStep.kt +++ b/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetERC20AllowanceStep.kt @@ -1,34 +1,30 @@ package exchange.dydx.web3.steps -import exchange.dydx.utilities.utils.AsyncEvent import exchange.dydx.utilities.utils.AsyncStep import exchange.dydx.web3.EthereumInteractor -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import java.math.BigInteger +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class EthGetERC20AllowanceStep( private val ethereumInteractor: EthereumInteractor, private val tokenAddress: String, private val ethereumAddress: String, private val spenderAddress: String, -) : AsyncStep { - private val eventFlow: MutableStateFlow> = MutableStateFlow(AsyncEvent.Progress(Unit)) +) : AsyncStep { - override fun run(): Flow> { + override suspend fun run(): Result = suspendCoroutine { continuation -> ethereumInteractor.erc20GetAllowance( tokenAddress = tokenAddress, ownerAddress = ethereumAddress, spenderAddress = spenderAddress, completion = { error, allowance -> if (allowance != null) { - eventFlow.value = AsyncEvent.Result(result = allowance, error = null) + continuation.resume(Result.success(allowance)) } else { - eventFlow.value = errorEvent(error?.message ?: "Unknown error") + continuation.resume(errorEvent(error?.message ?: "Unknown error")) } }, ) - - return eventFlow } } diff --git a/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetGasPriceStep.kt b/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetGasPriceStep.kt deleted file mode 100644 index 16322a1f..00000000 --- a/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetGasPriceStep.kt +++ /dev/null @@ -1,28 +0,0 @@ -package exchange.dydx.web3.steps - -import exchange.dydx.utilities.utils.AsyncEvent -import exchange.dydx.utilities.utils.AsyncStep -import exchange.dydx.web3.EthereumInteractor -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import java.math.BigInteger - -class EthGetGasPriceStep( - private val ethereumInteractor: EthereumInteractor, -) : AsyncStep { - private val eventFlow: MutableStateFlow> = MutableStateFlow(AsyncEvent.Progress(Unit)) - - override fun run(): Flow> { - ethereumInteractor.ethGasPrice( - completion = { error, gasPrice -> - if (gasPrice != null) { - eventFlow.value = AsyncEvent.Result(result = gasPrice, error = null) - } else { - eventFlow.value = errorEvent(error?.message ?: "Unknown error") - } - }, - ) - - return eventFlow - } -} diff --git a/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetNonceStep.kt b/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetNonceStep.kt deleted file mode 100644 index 3028a397..00000000 --- a/v4/integration/web3/src/main/java/exchange/dydx/web3/steps/EthGetNonceStep.kt +++ /dev/null @@ -1,32 +0,0 @@ -package exchange.dydx.web3.steps - -import exchange.dydx.utilities.utils.AsyncEvent -import exchange.dydx.utilities.utils.AsyncStep -import exchange.dydx.web3.EthereumInteractor -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import java.math.BigInteger - -class EthGetNonceStep( - private val ethereumInteractor: EthereumInteractor, - private val address: String, -) : AsyncStep { - private val eventFlow: MutableStateFlow> = MutableStateFlow( - AsyncEvent.Progress(Unit), - ) - - override fun run(): Flow> { - ethereumInteractor.ethGetTransactionCount( - address = address, - completion = { error, count -> - if (count != null) { - eventFlow.value = AsyncEvent.Result(result = count, error = null) - } else { - eventFlow.value = errorEvent(error?.message ?: "Unknown error") - } - }, - ) - - return eventFlow - } -} diff --git a/v4/utilities/src/main/java/exchange/dydx/utilities/utils/AsyncStep.kt b/v4/utilities/src/main/java/exchange/dydx/utilities/utils/AsyncStep.kt index 5d30126a..791c96c4 100644 --- a/v4/utilities/src/main/java/exchange/dydx/utilities/utils/AsyncStep.kt +++ b/v4/utilities/src/main/java/exchange/dydx/utilities/utils/AsyncStep.kt @@ -1,28 +1,19 @@ package exchange.dydx.utilities.utils -import kotlinx.coroutines.flow.Flow - -// Sealed class to represent async events -sealed class AsyncEvent { - data class Progress(val progress: ProgressType) : AsyncEvent() - data class Result(val result: ResultType?, val error: Throwable?) : AsyncEvent() - - val isProgress: Boolean - get() = this is Progress - - val isResult: Boolean - get() = this is Result -} +import android.util.Log // Interface representing an asynchronous step -interface AsyncStep { - fun run(): Flow> +interface AsyncStep { + suspend fun run(): Result - val invalidInputEvent: AsyncEvent.Result + val invalidInputEvent: Result get() = errorEvent("Invalid input") - fun errorEvent(error: String): AsyncEvent.Result = AsyncEvent.Result( - result = null, - error = Throwable(error), - ) + fun errorEvent(error: String) = Result.failure(Throwable(error)) +} + +suspend inline fun > A.runWithLogs(): Result { + val name = A::class.java.simpleName + Log.d("AsyncStep", "Starting $name") + return run().also { Log.d("AsyncStep", "Got result for $name: $it") } }