Skip to content

Commit

Permalink
Convert AsyncStep to suspending functions instead of Flows (#215)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
prashanDYDX authored Aug 14, 2024
1 parent 76663bb commit 5ed1516
Show file tree
Hide file tree
Showing 16 changed files with 421 additions and 644 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,14 +26,16 @@ 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
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
Expand All @@ -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<Boolean> = MutableStateFlow(false)
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -24,18 +18,17 @@ class DydxTransferDepositStep(
private val chainRpc: String,
private val tokenAddress: String,
private val context: Context,
) : AsyncStep<Unit, String> {
) : AsyncStep<String> {

@OptIn(ExperimentalCoroutinesApi::class)
override fun run(): Flow<AsyncEvent<Unit, String>> {
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<String> {
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,
Expand All @@ -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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,77 +18,40 @@ class ERC20ApprovalStep(
private val chainId: String,
private val provider: CarteraProvider,
private val context: Context,
) : AsyncStep<Unit, Boolean> {
) : AsyncStep<Boolean> {

private val ethereumInteractor = EthereumInteractor(chainRpc)

override fun run(): Flow<AsyncEvent<Unit, Boolean>> {
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<Boolean> {
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 }
}
}
Loading

0 comments on commit 5ed1516

Please sign in to comment.