diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index 42dc192fbb..013bf2e49f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -42,13 +42,7 @@ internal class StakingAnalyticSender( StakingAnalyticsEvents.ConfirmationScreenOpened( token = value.cryptoCurrencySymbol, validator = validatorName, - action = when (value.actionType) { - StakingActionCommonType.ENTER -> StakingActionType.STAKE - StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE - StakingActionCommonType.PENDING_REWARDS, - StakingActionCommonType.PENDING_OTHER, - -> confirmationState.pendingAction?.type ?: StakingActionType.UNKNOWN - }, + action = getStakingActionType(value), ), ) } @@ -83,12 +77,24 @@ internal class StakingAnalyticSender( ) } - fun sendTransactionStakingAnalytics(cryptoCurrency: CryptoCurrency, validator: String) { + fun sendTransactionStakingAnalytics(value: StakingUiState) { + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorName = validatorState?.chosenValidator?.name ?: return + + analyticsEventHandler.send( + StakingAnalyticsEvents.ButtonAction( + action = getStakingActionType(value).name, + token = value.cryptoCurrencyName, + validator = validatorName, + ), + ) + analyticsEventHandler.send( Basic.TransactionSent( sentFrom = AnalyticsParam.TxSentFrom.Staking( - blockchain = cryptoCurrency.network.name, - token = cryptoCurrency.symbol, + blockchain = value.cryptoCurrencyName, + token = value.cryptoCurrencySymbol, feeType = AnalyticsParam.FeeType.Normal, ), memoType = Basic.TransactionSent.MemoType.Null, @@ -96,9 +102,21 @@ internal class StakingAnalyticSender( ) analyticsEventHandler.send( StakingAnalyticsEvents.StakeInProgressScreenOpened( - validator = validator, - token = cryptoCurrency.symbol, + validator = validatorName, + token = value.cryptoCurrencySymbol, ), ) } + + private fun getStakingActionType(value: StakingUiState): StakingActionType { + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + + return when (value.actionType) { + StakingActionCommonType.ENTER -> StakingActionType.STAKE + StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE + StakingActionCommonType.PENDING_REWARDS, + StakingActionCommonType.PENDING_OTHER, + -> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 202219cb3b..9e4db06b4e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -92,6 +92,7 @@ internal sealed class StakingStates { val feeState: FeeState, val validatorState: ValidatorState, val pendingAction: PendingAction?, + val pendingActions: ImmutableList?, val notifications: ImmutableList, val footerText: TextReference, val transactionDoneState: TransactionDoneState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt new file mode 100644 index 0000000000..55446f2303 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -0,0 +1,102 @@ +package com.tangem.features.staking.impl.presentation.state.helpers + +import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase +import com.tangem.domain.tokens.FetchPendingTransactionsUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.utils.coroutines.DelayedWork +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.* + +@Suppress("LongParameterList") +internal class StakingBalanceUpdater @AssistedInject constructor( + private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, + private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, + @DelayedWork private val coroutineScope: CoroutineScope, + @Assisted private val userWallet: UserWallet, + @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, +) { + fun scheduleUpdates() { + coroutineScope.launch { + listOf( + // we should update network to find pending tx after 1 sec + async { + fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrencyStatus.currency.network)) + }, + // we should update tx history and network for new balances + async { + updateStakeBalance() + }, + async { + updateTxHistory() + }, + async { + updateNetworkStatuses() + }, + ).awaitAll() + } + } + + suspend fun instantUpdate() { + coroutineScope { + listOf( + async { + updateStakeBalance() + }, + async { + updateNetworkStatuses(delay = 0) + }, + ).awaitAll() + } + } + + private suspend fun updateNetworkStatuses(delay: Long = BALANCE_UPDATE_DELAY) { + updateDelayedNetworkStatusUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrencyStatus.currency.network, + delayMillis = delay, + refresh = true, + ) + } + + private suspend fun updateStakeBalance() { + stakingYieldBalanceUseCase( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + refresh = true, + ) + } + + private suspend fun updateTxHistory() { + delay(BALANCE_UPDATE_DELAY) + val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase( + userWalletId = userWallet.walletId, + currency = cryptoCurrencyStatus.currency, + ) + + txHistoryItemsCountEither.onRight { + getTxHistoryItemsUseCase( + userWalletId = userWallet.walletId, + currency = cryptoCurrencyStatus.currency, + refresh = true, + ) + } + } + + @AssistedFactory + interface Factory { + fun create(cryptoCurrencyStatus: CryptoCurrencyStatus, userWallet: UserWallet): StakingBalanceUpdater + } + + private companion object { + const val BALANCE_UPDATE_DELAY = 11_000L + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt new file mode 100644 index 0000000000..0fdda4ab88 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -0,0 +1,238 @@ +package com.tangem.features.staking.impl.presentation.state.helpers + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.extensions.isZero +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.domain.staking.EstimateGasUseCase +import com.tangem.domain.staking.model.StakingApproval +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.transaction.ActionParams +import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.staking.impl.presentation.state.StakingStateController +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw +import com.tangem.utils.extensions.orZero +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class StakingFeeTransactionLoader @AssistedInject constructor( + private val stateController: StakingStateController, + private val getAllowanceUseCase: GetAllowanceUseCase, + private val getFeeUseCase: GetFeeUseCase, + private val estimateGasUseCase: EstimateGasUseCase, + @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, + @Assisted private val userWallet: UserWallet, + @Assisted private val yield: Yield, + @Assisted private val stakingApproval: StakingApproval, +) { + + suspend fun getFee( + pendingAction: PendingAction?, + pendingActions: ImmutableList?, + onFeeError: (GetFeeError) -> Unit, + onStakingFee: (Fee) -> Unit, + onApprovalFee: (TransactionFee) -> Unit, + ) { + val state = stateController.value + val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data + ?: error("No confirmation state") + val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("No validator provided") + + val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided") + + val validatorAddress = validatorState.chosenValidator.address + + val approval = stakingApproval as? StakingApproval.Needed + if (approval != null) { + val allowance = getAllowanceUseCase( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + + if (allowance < amount) { + getApproveFee( + amount = amount, + validatorAddress = validatorAddress, + onApprovalFee = onApprovalFee, + onApprovalFeeError = onFeeError, + ) + } else { + estimateGas( + pendingAction = pendingAction, + pendingActions = pendingActions, + amount = amount, + validatorAddress = validatorAddress, + onFeeError = onFeeError, + onStakingFee = onStakingFee, + ) + } + } else { + estimateGas( + pendingAction = pendingAction, + pendingActions = pendingActions, + amount = amount, + validatorAddress = validatorAddress, + onFeeError = onFeeError, + onStakingFee = onStakingFee, + ) + } + } + + private suspend fun estimateGas( + pendingAction: PendingAction?, + pendingActions: ImmutableList?, + amount: BigDecimal, + validatorAddress: String, + onFeeError: (GetFeeError) -> Unit, + onStakingFee: (Fee) -> Unit, + ) { + val sourceAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address") + + val gasEstimate = if (isSolanaWithdraw(cryptoCurrencyStatus.currency.network.id.value, pendingActions)) { + val result = coroutineScope { + pendingActions?.map { action -> + async { + // Simultaneous or quick api calls can sometimes return ZERO fee + estimateFeeRetry { + estimateFee( + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + action = action, + ) + }.getOrElse { + onFeeError(GetFeeError.DataError(Throwable(it.toString()))) + null + } + } + }?.awaitAll()?.filterNotNull() + } + + if (result.isNullOrEmpty()) { + onFeeError(GetFeeError.UnknownError) + return + } + + val totalAmount = result.sumOf { it.amount } + val totalGasLimit = result.sumOf { it.gasLimit?.toBigDecimalOrNull().orZero() } + StakingGasEstimate( + amount = totalAmount, + token = result.first().token, + gasLimit = totalGasLimit.toPlainString().orEmpty(), + ) + } else { + estimateFee( + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + action = pendingAction, + ).getOrElse { + onFeeError(GetFeeError.DataError(Throwable(it.toString()))) + return + } + } + + onStakingFee( + Fee.Common( + Amount( + currencySymbol = gasEstimate.token.symbol, + value = gasEstimate.amount, + decimals = gasEstimate.token.decimals, + ), + ), + ) + } + + private suspend fun estimateFee( + amount: BigDecimal, + sourceAddress: String, + validatorAddress: String, + action: PendingAction?, + ) = estimateGasUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrencyStatus.currency.network, + params = ActionParams( + actionCommonType = stateController.value.actionType, + integrationId = yield.id, + amount = amount, + address = sourceAddress, + validatorAddress = validatorAddress, + token = yield.token, + passthrough = action?.passthrough, + type = action?.type, + ), + ) + + private suspend fun getApproveFee( + amount: BigDecimal, + validatorAddress: String, + onApprovalFee: (TransactionFee) -> Unit, + onApprovalFeeError: (GetFeeError) -> Unit, + ) { + getFeeUseCase( + amount = amount, + destination = validatorAddress, + userWallet = userWallet, + cryptoCurrency = cryptoCurrencyStatus.currency, + ).fold( + ifRight = { fee -> + onApprovalFee(fee) + }, + ifLeft = { error -> + onApprovalFeeError(error) + }, + ) + } + + private suspend fun estimateFeeRetry( + times: Int = 3, + delay: Long = 1000, + block: suspend () -> Either, + ): Either { + repeat(times - 1) { + val feeResult = block() + feeResult.fold( + ifLeft = { return feeResult }, + ifRight = { + if (!it.amount.isZero()) return feeResult + }, + ) + delay(delay) + } + return block() + } + + @AssistedFactory + interface Factory { + fun create( + cryptoCurrencyStatus: CryptoCurrencyStatus, + userWallet: UserWallet, + yield: Yield, + stakingApproval: StakingApproval, + ): StakingFeeTransactionLoader + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt new file mode 100644 index 0000000000..e6e0b2b6e0 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -0,0 +1,271 @@ +package com.tangem.features.staking.impl.presentation.state.helpers + +import arrow.core.getOrElse +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase +import com.tangem.domain.staking.GetStakingTransactionUseCase +import com.tangem.domain.staking.SaveUnsubmittedHashUseCase +import com.tangem.domain.staking.SubmitHashUseCase +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.transaction.ActionParams +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.transaction.usecase.SendMultipleTransactionUseCase +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount +import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw +import com.tangem.utils.extensions.orZero +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import timber.log.Timber +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class StakingTransactionSender @AssistedInject constructor( + private val stateController: StakingStateController, + private val stakingBalanceUpdater: StakingBalanceUpdater.Factory, + private val getStakingTransactionUseCase: GetStakingTransactionUseCase, + private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase, + private val sendMultipleTransactionUseCase: SendMultipleTransactionUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val submitHashUseCase: SubmitHashUseCase, + private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, + @Assisted private val userWallet: UserWallet, + @Assisted private val yield: Yield, + @Assisted private val isAmountSubtractAvailable: Boolean, +) { + + private val balanceUpdater: StakingBalanceUpdater + get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet) + + suspend fun constructAndSendTransactions( + onConstructSuccess: (List) -> Unit, + onConstructError: (StakingError) -> Unit, + onSendSuccess: (String) -> Unit, + onSendError: (SendTransactionError?) -> Unit, + ) { + val state = stateController.value + + val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data + ?: error("No confirmation state") + val fee = (confirmationState.feeState as? FeeState.Content)?.fee + ?: error("No fee provided") + + val stakingTransactions = getStakingTransactions( + state = state, + confirmationState = confirmationState, + onConstructError = onConstructError, + ) + + val fullTransactionsData = getConstructedTransactions( + stakingTransactions = stakingTransactions, + fee = fee, + onConstructError = onConstructError, + ) + + if (fullTransactionsData.isNullOrEmpty()) { + onConstructError(StakingError.UnknownError) + return + } + + onConstructSuccess(fullTransactionsData.map { it.stakeKitTransaction }) + + sendStakingTransaction( + fullTransactionsData = fullTransactionsData, + onSendSuccess = onSendSuccess, + onSendError = onSendError, + ) + } + + private suspend fun getStakingTransactions( + state: StakingUiState, + confirmationState: StakingStates.ConfirmationState.Data, + onConstructError: (StakingError) -> Unit, + ) = coroutineScope { + val isAllWithdrawAction = isSolanaWithdraw( + cryptoCurrencyStatus.currency.network.id.value, + confirmationState.pendingActions, + ) + if (isAllWithdrawAction) { + confirmationState.pendingActions?.map { action -> + async { + getStakingTransaction( + state = state, + action = action, + confirmationState = confirmationState, + onConstructError = onConstructError, + ) + } + }?.awaitAll()?.flatten() + } else { + getStakingTransaction( + state = state, + confirmationState = confirmationState, + onConstructError = onConstructError, + ) + } + } + + private suspend fun getConstructedTransactions( + stakingTransactions: List?, + fee: Fee, + onConstructError: (StakingError) -> Unit, + ) = coroutineScope { + stakingTransactions?.filterNot { it.type == StakingTransactionType.APPROVAL } + ?.map { transaction -> + async { + getConstructedStakingTransactionUseCase( + networkId = cryptoCurrencyStatus.currency.network.id.value, + fee = fee, + transactionId = transaction.id, + ).fold( + ifRight = { (constructedTransaction, transactionData) -> + FullTransactionData( + stakeKitTransaction = constructedTransaction, + tangemTransaction = transactionData, + ) + }, + ifLeft = { + onConstructError(it) + null + }, + ) + } + }?.awaitAll()?.filterNotNull() + } + + private suspend fun getStakingTransaction( + state: StakingUiState, + confirmationState: StakingStates.ConfirmationState.Data, + action: PendingAction? = confirmationState.pendingAction, + onConstructError: (StakingError) -> Unit, + ): List { + val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("No validator provided") + val fee = (confirmationState.feeState as? FeeState.Content)?.fee + ?: error("No fee provided") + val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address") + val amountState = state.amountState as? AmountState.Data + ?: error("No amount provided") + + val validatorAddress = validatorState.chosenValidator.address + val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy) + + return getStakingTransactionUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrencyStatus.currency.network, + params = ActionParams( + actionCommonType = state.actionType, + integrationId = yield.id, + amount = amount, + address = defaultAddress, + validatorAddress = validatorAddress, + token = yield.token, + passthrough = action?.passthrough, + type = action?.type, + ), + ).getOrElse { + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(state.cryptoCurrencyName)) + onConstructError(it) + return emptyList() + } + } + + private suspend fun sendStakingTransaction( + fullTransactionsData: List, + onSendSuccess: (txUrl: String) -> Unit, + onSendError: (SendTransactionError?) -> Unit, + ) { + sendMultipleTransactionUseCase( + txsData = fullTransactionsData.map { it.tangemTransaction }, + userWallet = userWallet, + network = cryptoCurrencyStatus.currency.network, + ).fold( + ifLeft = { error -> + onSendError(error) + }, + ifRight = { transactionHashes -> + submitHash( + transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id }, + transactionHashes = transactionHashes, + ) + val txUrl = getExplorerTransactionUrlUseCase( + txHash = transactionHashes.last(), + networkId = cryptoCurrencyStatus.currency.network.id, + ).getOrElse { "" } + + balanceUpdater.scheduleUpdates() + onSendSuccess(txUrl) + }, + ) + } + + private suspend fun submitHash(transactionIds: List, transactionHashes: List) { + transactionIds + .zip(transactionHashes) + .forEach { (transactionId, transactionHash) -> + submitHashUseCase.submitHash( + transactionId = transactionId, + transactionHash = transactionHash, + ) + .onLeft { + analyticsEventHandler.send( + StakingAnalyticsEvents.StakingError(stateController.value.cryptoCurrencyName), + ) + saveUnsubmittedHashUseCase.invoke( + transactionId = transactionId, + transactionHash = transactionHash, + ) + }.onRight { + Timber.d("Successful hash submission") + } + } + } + + private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal { + val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") + val feeValue = fee.amount.value ?: error("No fee value") + val isEnterAction = stateController.value.actionType == StakingActionCommonType.ENTER + + return checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy.orZero(), + ) + } + + private data class FullTransactionData( + val stakeKitTransaction: StakingTransaction, + val tangemTransaction: TransactionData.Compiled, + ) + + @AssistedFactory + interface Factory { + fun create( + cryptoCurrencyStatus: CryptoCurrencyStatus, + userWallet: UserWallet, + yield: Yield, + isAmountSubtractAvailable: Boolean, + ): StakingTransactionSender + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index 5e63fa4e32..b5b6e9061f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -92,5 +92,6 @@ internal object ConfirmationStatePreviewData { pendingAction = null, isApprovalNeeded = false, reduceAmountBy = null, + pendingActions = null, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 4cc05fa7f2..b4c0e2a299 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -8,6 +8,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @Suppress("TooManyFunctions") @@ -15,7 +16,12 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} - override fun onNextClick(actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?) {} + override fun onNextClick( + actionTypeToOverwrite: StakingActionCommonType?, + pendingAction: PendingAction?, + pendingActions: ImmutableList?, + ) { + } override fun onActionClick() {} @@ -55,7 +61,8 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onActiveStake(activeStake: BalanceState) {} - override fun getFee(pendingAction: PendingAction?) {} + override fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) { + } override fun onAmountReduceByClick( reduceAmountBy: BigDecimal, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index d9f2c9725f..35e1219e3f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -91,7 +91,12 @@ internal class AddStakingNotificationsTransformer( prevState = prevState, feeError = feeError, sendingAmount = sendingAmount, - onReload = { prevState.clickIntents.getFee(confirmationState.pendingAction) }, + onReload = { + prevState.clickIntents.getFee( + confirmationState.pendingAction, + confirmationState.pendingActions, + ) + }, feeValue = feeValue, ) // warnings diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt index 251b237e9d..fe77e4e0c2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt @@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer -internal class DismissBottomSheetStateTransformer : Transformer { +internal object DismissBottomSheetStateTransformer : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy(bottomSheetConfig = null) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt new file mode 100644 index 0000000000..e3adab9f50 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt @@ -0,0 +1,25 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList + +internal class SetActionToExecuteTransformer( + private val actionTypeToOverwrite: StakingActionCommonType, + private val pendingAction: PendingAction?, + private val pendingActions: ImmutableList?, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + return prevState.copy( + actionType = actionTypeToOverwrite, + confirmationState = confirmationState?.copy( + pendingAction = pendingAction, + pendingActions = pendingActions, + ) ?: StakingStates.ConfirmationState.Empty(), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index ee19ad787a..196784785f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -10,12 +10,14 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val fee: Fee, private val action: PendingAction?, + private val actions: ImmutableList?, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -38,6 +40,7 @@ internal class SetConfirmationStateAssentTransformer( ), validatorState = validatorState.copySealed(isClickable = true), pendingAction = action, + pendingActions = actions, isPrimaryButtonEnabled = true, isApprovalNeeded = false, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 32936b8d7e..db27e0c2d9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -1,43 +1,26 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.blockchain.common.transaction.Fee import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf internal class SetConfirmationStateCompletedTransformer( - private val appCurrencyProvider: Provider, - private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, - private val fee: Fee, private val txUrl: String, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - confirmationState = prevState.confirmationState.copyWrapped(fee), + confirmationState = prevState.confirmationState.copyWrapped(), ) } - private fun StakingStates.ConfirmationState.copyWrapped(fee: Fee): StakingStates.ConfirmationState { + private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState { if (this is StakingStates.ConfirmationState.Data) { - val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true return copy( isPrimaryButtonEnabled = true, innerState = InnerConfirmationStakingState.COMPLETED, - feeState = FeeState.Content( - fee = fee, - rate = feeCryptoCurrencyStatus?.value?.fiatRate, - isFeeConvertibleToFiat = isFeeConvertibleToFiat, - appCurrency = appCurrencyProvider(), - isFeeApproximate = false, - ), - validatorState = validatorState.copySealed( - isClickable = false, - ), + validatorState = validatorState.copySealed(isClickable = false), footerText = TextReference.EMPTY, notifications = persistentListOf(), transactionDoneState = TransactionDoneState.Content( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 076c5577a7..bd88fa6ffc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -38,6 +38,7 @@ internal class SetConfirmationStateLoadingTransformer( footerText = getFooter(prevState), transactionDoneState = TransactionDoneState.Empty, pendingAction = possibleConfirmationState?.pendingAction, + pendingActions = possibleConfirmationState?.pendingActions, isApprovalNeeded = false, reduceAmountBy = null, ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 8779bf824e..58d6743dbd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -224,6 +224,7 @@ internal class SetInitialDataStateTransformer( footerText = TextReference.EMPTY, transactionDoneState = TransactionDoneState.Empty, pendingAction = null, + pendingActions = null, isApprovalNeeded = isApprovalNeeded, reduceAmountBy = null, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt index 0116e0d6d5..2822f01758 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -3,7 +3,10 @@ package com.tangem.features.staking.impl.presentation.state.utils import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.lib.crypto.BlockchainUtils.isSolana +import kotlinx.collections.immutable.ImmutableList @Suppress("CyclomaticComplexMethod") internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (this) { @@ -24,4 +27,10 @@ internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (t StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake) StakingActionType.UNKNOWN -> TextReference.EMPTY null -> TextReference.EMPTY +} + +internal fun isSolanaWithdraw(networkId: String, pendingActions: ImmutableList?): Boolean { + val isSolana = isSolana(networkId) + val isWithdraw = pendingActions?.all { it.type == StakingActionType.WITHDRAW } == true + return isSolana && isWithdraw } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 1a001e874d..e666de9908 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -30,17 +30,16 @@ import java.math.BigDecimal @Composable internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { + val backgroundColor = if (isTransactionSent) { + TangemTheme.colors.background.action + } else { + TangemTheme.colors.button.disabled + } Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) - .background( - if (isTransactionSent) { - TangemTheme.colors.background.action - } else { - TangemTheme.colors.button.disabled - }, - ) + .background(backgroundColor) .padding(TangemTheme.dimens.spacing12), ) { Text( @@ -49,9 +48,7 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { color = TangemTheme.colors.text.tertiary, ) - Box( - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), - ) { + Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) { when (feeState) { is FeeState.Content -> { val feeAmount = feeState.fee?.amount @@ -82,6 +79,7 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { SelectorRowItem( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, + isSelected = true, paddingValues = PaddingValues(), showDivider = false, ) @@ -91,6 +89,7 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { SelectorRowItem( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, + isSelected = true, paddingValues = PaddingValues(), ) FeeError(feeState) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index ed0cfbffc4..c6fe6235d4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -8,6 +8,7 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType +import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @Suppress("TooManyFunctions") @@ -15,7 +16,11 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() - fun onNextClick(actionTypeToOverwrite: StakingActionCommonType? = null, pendingAction: PendingAction? = null) + fun onNextClick( + actionTypeToOverwrite: StakingActionCommonType? = null, + pendingAction: PendingAction? = null, + pendingActions: ImmutableList? = null, + ) fun onActionClick() @@ -27,7 +32,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onInfoClick(infoType: InfoType) - fun getFee(pendingAction: PendingAction?) + fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) override fun onAmountNext() = onNextClick(actionTypeToOverwrite = null) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index b283511af9..e23576a2b7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -6,9 +6,6 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle @@ -26,24 +23,22 @@ import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.staking.* +import com.tangem.domain.staking.IsAnyTokenStakedUseCase +import com.tangem.domain.staking.IsApproveNeededUseCase import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction -import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.usecase.* -import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase +import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -55,6 +50,9 @@ import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory +import com.tangem.features.staking.impl.presentation.state.helpers.StakingBalanceUpdater +import com.tangem.features.staking.impl.presentation.state.helpers.StakingFeeTransactionLoader +import com.tangem.features.staking.impl.presentation.state.helpers.StakingTransactionSender import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.* import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer @@ -62,14 +60,14 @@ import com.tangem.features.staking.impl.presentation.state.transformers.approval import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer -import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount +import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem -import com.tangem.utils.extensions.orZero import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import java.util.concurrent.CopyOnWriteArrayList @@ -86,22 +84,9 @@ internal class StakingViewModel @Inject constructor( private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, - private val getStakingTransactionUseCase: GetStakingTransactionUseCase, - private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase, - private val estimateGasUseCase: EstimateGasUseCase, private val sendTransactionUseCase: SendTransactionUseCase, - private val sendMultipleTransactionUseCase: SendMultipleTransactionUseCase, - private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, - private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, - private val submitHashUseCase: SubmitHashUseCase, - private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, - private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase, - private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, - private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val getAllowanceUseCase: GetAllowanceUseCase, - private val getFeeUseCase: GetFeeUseCase, private val isApproveNeededUseCase: IsApproveNeededUseCase, private val vibratorHapticManager: VibratorHapticManager, private val feedbackManager: FeedbackManager, @@ -112,8 +97,10 @@ internal class StakingViewModel @Inject constructor( private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase, + private val stakingTransactionLoader: StakingTransactionSender.Factory, + private val stakingFeeTransactionLoader: StakingFeeTransactionLoader.Factory, + private val stakingBalanceUpdater: StakingBalanceUpdater.Factory, private val analyticsEventHandler: AnalyticsEventHandler, - @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -144,6 +131,31 @@ internal class StakingViewModel @Inject constructor( private var isInitialInfoAnalyticSent: Boolean = false + private val balanceUpdater by lazy(LazyThreadSafetyMode.NONE) { + stakingBalanceUpdater.create( + cryptoCurrencyStatus, + userWallet, + ) + } + + private val feeLoader by lazy(LazyThreadSafetyMode.NONE) { + stakingFeeTransactionLoader.create( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWallet = userWallet, + yield = yield, + stakingApproval = stakingApproval, + ) + } + + private val transactionSender by lazy(LazyThreadSafetyMode.NONE) { + stakingTransactionLoader.create( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWallet = userWallet, + yield = yield, + isAmountSubtractAvailable = isAmountSubtractAvailable, + ) + } + private val stakingEventFactory: StakingEventFactory get() = StakingEventFactory( stateController = stateController, @@ -180,16 +192,13 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } - override fun onNextClick(actionTypeToOverwrite: StakingActionCommonType?, pendingAction: PendingAction?) { + override fun onNextClick( + actionTypeToOverwrite: StakingActionCommonType?, + pendingAction: PendingAction?, + pendingActions: ImmutableList?, + ) { if (actionTypeToOverwrite != null) { - stateController.update { it.copy(actionType = actionTypeToOverwrite) } - stateController.update { - val confirmationState = it.confirmationState as? StakingStates.ConfirmationState.Data - it.copy( - confirmationState = confirmationState?.copy(pendingAction = pendingAction) - ?: StakingStates.ConfirmationState.Empty(), - ) - } + stateController.update(SetActionToExecuteTransformer(actionTypeToOverwrite, pendingAction, pendingActions)) } stakingStateRouter.onNextClick() when { @@ -198,7 +207,7 @@ internal class StakingViewModel @Inject constructor( onRefreshSwipe(isRefreshing = false) } isAssentState() -> { - getFee(pendingAction) + getFee(pendingAction, pendingActions) val amountState = value.amountState as? AmountState.Data if (amountState?.amountTextField?.isWarning == true) { stateController.update( @@ -215,50 +224,40 @@ internal class StakingViewModel @Inject constructor( handleOnNextConfirmationClick() } - override fun getFee(pendingAction: PendingAction?) { + override fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) { + stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency)) viewModelScope.launch { - stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency)) - val cryptoCurrencyValue = cryptoCurrencyStatus.value - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - ?: error("No confirmation state") - val validatorState = confirmationState.validatorState as? ValidatorState.Content - ?: error("No validator provided") - - val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - ?: error("No amount provided") - val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value - ?: error("No available address") - val validatorAddress = validatorState.chosenValidator.address - - val approval = stakingApproval as? StakingApproval.Needed - if (approval != null) { - val allowance = getAllowanceUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - spenderAddress = approval.spenderAddress, - ).getOrElse { BigDecimal.ZERO } - - if (allowance < amount) { - getApproveFee( - amount = amount, - validatorAddress = validatorAddress, + feeLoader.getFee( + pendingAction = pendingAction, + pendingActions = pendingActions, + onStakingFee = { gasEstimate -> + stateController.update( + SetConfirmationStateAssentTransformer( + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fee = gasEstimate, + action = pendingAction, + actions = pendingActions, + ), ) - } else { - estimateGas( - pendingAction = pendingAction, - amount = amount, - sourceAddress = sourceAddress, - validatorAddress = validatorAddress, + updateNotifications() + }, + onFeeError = { error -> + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) + stateController.update(AddStakingErrorTransformer()) + updateNotifications(error) + }, + onApprovalFee = { fee -> + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fee = fee, + ), ) - } - } else { - estimateGas( - pendingAction = pendingAction, - amount = amount, - sourceAddress = sourceAddress, - validatorAddress = validatorAddress, - ) - } + updateNotifications() + }, + ) } } @@ -266,139 +265,32 @@ internal class StakingViewModel @Inject constructor( if (isAssentState()) { viewModelScope.launch { stateController.update(SetConfirmationStateInProgressTransformer()) - - val confirmationState = - value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") - val validatorState = confirmationState.validatorState as? ValidatorState.Content - ?: error("No validator provided") - val amountState = value.amountState as? AmountState.Data ?: error("No amount provided") - - val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") - val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - ?: error("No available address") - val pendingAction = confirmationState.pendingAction - - val stakingTransactions = getStakingTransactionUseCase( - userWalletId = userWalletId, - network = cryptoCurrencyStatus.currency.network, - params = ActionParams( - actionCommonType = value.actionType, - integrationId = yield.id, - amount = getAmount(amountState, fee, confirmationState.reduceAmountBy), - address = defaultAddress, - validatorAddress = validatorState.chosenValidator.address, - token = yield.token, - passthrough = pendingAction?.passthrough, - type = pendingAction?.type, - ), - ).getOrElse { - Timber.e(it.toString()) - analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) - stakingEventFactory.createStakingErrorAlert(it) - stateController.update(SetConfirmationStateResetAssentTransformer) - return@launch - } - - val fullTransactionsData = stakingTransactions - .filterNot { it.type == StakingTransactionType.APPROVAL } - .map { transaction -> - val (constructedTransaction, transactionData) = getConstructedStakingTransactionUseCase( - networkId = cryptoCurrencyStatus.currency.network.id.value, - fee = fee, - transactionId = transaction.id, - ).getOrElse { - Timber.e(it.toString()) - analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) - stakingEventFactory.createStakingErrorAlert(it) - stateController.update(SetConfirmationStateResetAssentTransformer) - return@launch - } - - FullTransactionData( - stakeKitTransaction = constructedTransaction, - tangemTransaction = transactionData, - ) - } - - transactionsInProgress.addAll(fullTransactionsData.map { it.stakeKitTransaction }) - - sendStakingTransaction( - fullTransactionsData = fullTransactionsData, - fee = fee, - pendingAction = confirmationState.pendingAction, - validator = validatorState.chosenValidator.name, + transactionSender.constructAndSendTransactions( + onConstructSuccess = { constructedTransactions -> + transactionsInProgress.addAll(constructedTransactions) + }, + onConstructError = { error -> + Timber.e(error.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) + stakingEventFactory.createStakingErrorAlert(error) + stateController.update(SetConfirmationStateResetAssentTransformer) + }, + onSendSuccess = { txUrl -> + stakingAnalyticSender.sendTransactionStakingAnalytics(stateController.value) + transactionsInProgress.clear() + stateController.update(SetConfirmationStateCompletedTransformer(txUrl)) + }, + onSendError = { error -> + Timber.e(error.toString()) + analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) + stakingEventFactory.createSendTransactionErrorAlert(error) + stateController.update(SetConfirmationStateResetAssentTransformer) + }, ) } } } - private suspend fun estimateGas( - pendingAction: PendingAction?, - amount: BigDecimal, - sourceAddress: String, - validatorAddress: String, - ) { - val gasEstimate = estimateGasUseCase( - userWalletId = userWalletId, - network = cryptoCurrencyStatus.currency.network, - params = ActionParams( - actionCommonType = value.actionType, - integrationId = yield.id, - amount = amount, - address = sourceAddress, - validatorAddress = validatorAddress, - token = yield.token, - passthrough = pendingAction?.passthrough, - type = pendingAction?.type, - ), - ).getOrElse { - analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) - stateController.update(AddStakingErrorTransformer()) - updateNotifications(GetFeeError.UnknownError) - return - } - stateController.update( - SetConfirmationStateAssentTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = Fee.Common( - Amount( - currencySymbol = gasEstimate.token.symbol, - value = gasEstimate.amount, - decimals = gasEstimate.token.decimals, - ), - ), - action = pendingAction, - ), - ) - updateNotifications() - } - - private suspend fun getApproveFee(amount: BigDecimal, validatorAddress: String) { - getFeeUseCase( - amount = amount, - destination = validatorAddress, - userWallet = userWallet, - cryptoCurrency = cryptoCurrencyStatus.currency, - ).fold( - ifRight = { fee -> - stateController.update( - SetConfirmationStateAssentApprovalTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = fee, - ), - ) - updateNotifications() - }, - ifLeft = { - analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName)) - stateController.update(AddStakingErrorTransformer()) - updateNotifications(it) - }, - ) - } - override fun onPrevClick() { if (value.currentStep == StakingStep.Confirmation) { when ((value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState) { @@ -422,10 +314,7 @@ internal class StakingViewModel @Inject constructor( override fun onRefreshSwipe(isRefreshing: Boolean) { stateController.update(SetInitialLoadingStateTransformer(isRefreshing)) viewModelScope.launch { - val balancesUpdate = async { updateStakeBalance() } - val networkUpdate = async { updateNetworkStatuses(0) } - - awaitAll(balancesUpdate, networkUpdate) + balanceUpdater.instantUpdate() }.saveIn(updateBalancesJobHolder) .invokeOnCompletion { stateController.update(SetInitialLoadingStateTransformer(false)) @@ -440,7 +329,7 @@ internal class StakingViewModel @Inject constructor( override fun onInfoClick(infoType: InfoType) { stateController.update( ShowInfoBottomSheetStateTransformer(infoType) { - stateController.update(DismissBottomSheetStateTransformer()) + stateController.update(DismissBottomSheetStateTransformer) }, ) } @@ -500,11 +389,16 @@ internal class StakingViewModel @Inject constructor( } override fun onActiveStake(activeStake: BalanceState) { - if (activeStake.pendingActions.size > 1) { + val isAllWithdrawActions = isSolanaWithdraw( + cryptoCurrencyStatus.currency.network.id.value, + activeStake.pendingActions, + ) + val isMultiplePendingActions = activeStake.pendingActions.isSingleItem().not() + if (isMultiplePendingActions && !isAllWithdrawActions) { stateController.update( ShowActionSelectorBottomSheetTransformer( pendingActions = activeStake.pendingActions, - onActionSelect = { + onActionSelect = { action -> stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) stateController.update( AmountChangeStateTransformer( @@ -513,19 +407,25 @@ internal class StakingViewModel @Inject constructor( yield, ), ) - onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER, pendingAction = it) - stateController.update(DismissBottomSheetStateTransformer()) - }, - onDismiss = { - stateController.update(DismissBottomSheetStateTransformer()) + onNextClick( + actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER, + pendingAction = action, + ) + stateController.update(DismissBottomSheetStateTransformer) }, + onDismiss = { stateController.update(DismissBottomSheetStateTransformer) }, ), ) } else { stateController.update(ActionTypeActiveStakeTransformer(cryptoCurrencyStatus, activeStake)) stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) - onNextClick(null, activeStake.pendingActions.firstOrNull()) + + onNextClick( + actionTypeToOverwrite = null, + pendingAction = activeStake.pendingActions.firstOrNull(), + pendingActions = activeStake.pendingActions.takeIf { isAllWithdrawActions }, + ) } } @@ -545,7 +445,7 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) { - stateController.update(DismissBottomSheetStateTransformer()) + stateController.update(DismissBottomSheetStateTransformer) }, ) } @@ -554,7 +454,7 @@ internal class StakingViewModel @Inject constructor( viewModelScope.launch { stateController.update( SetApprovalBottomSheetInProgressTransformer { - stateController.update(DismissBottomSheetStateTransformer()) + stateController.update(DismissBottomSheetStateTransformer) }, ) @@ -614,7 +514,7 @@ internal class StakingViewModel @Inject constructor( ifRight = { stakingAnalyticSender.sendTransactionApprovalAnalytics(tokenCryptoCurrency) stateController.update(SetApprovalInProgressTransformer) - stateController.update(DismissBottomSheetStateTransformer()) + stateController.update(DismissBottomSheetStateTransformer) awaitForAllowance(confirmationState.pendingAction) }, ) @@ -720,7 +620,7 @@ internal class StakingViewModel @Inject constructor( val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided") if (allowance >= amount) { - getFee(pendingAction) + getFee(pendingAction = pendingAction, pendingActions = null) allowanceTaskScheduler.cancelTask() } }, @@ -884,146 +784,6 @@ internal class StakingViewModel @Inject constructor( .launchIn(viewModelScope) } - private suspend fun sendStakingTransaction( - fullTransactionsData: List, - fee: Fee, - pendingAction: PendingAction?, - validator: String, - ) { - val cryptoCurrency = cryptoCurrencyStatus.currency - val action = when (value.actionType) { - StakingActionCommonType.ENTER -> StakingActionType.STAKE - StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE - StakingActionCommonType.PENDING_REWARDS, - StakingActionCommonType.PENDING_OTHER, - -> pendingAction?.type - } - action?.let { - analyticsEventHandler.send( - StakingAnalyticsEvents.ButtonAction( - action = it.name, - token = value.cryptoCurrencyName, - validator = validator, - ), - ) - } - sendMultipleTransactionUseCase( - txsData = fullTransactionsData.map { it.tangemTransaction }, - userWallet = userWallet, - network = cryptoCurrency.network, - ).fold( - ifLeft = { error -> - Timber.e(error.toString()) - analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) - stateController.update( - SetConfirmationStateAssentTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = fee, - action = pendingAction, - ), - ) - stakingEventFactory.createSendTransactionErrorAlert(error) - stateController.update(SetConfirmationStateResetAssentTransformer) - }, - ifRight = { transactionHashes -> - stakingAnalyticSender.sendTransactionStakingAnalytics(cryptoCurrency, validator) - transactionsInProgress.clear() - submitHash( - transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id }, - transactionHashes = transactionHashes, - ) - scheduleUpdates() - val txUrl = getExplorerTransactionUrlUseCase( - txHash = transactionHashes.last(), - networkId = cryptoCurrency.network.id, - ).getOrElse { "" } - stateController.update( - SetConfirmationStateCompletedTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = fee, - txUrl = txUrl, - ), - ) - }, - ) - } - - private suspend fun submitHash(transactionIds: List, transactionHashes: List) { - transactionIds - .zip(transactionHashes) - .forEach { (transactionId, transactionHash) -> - submitHashUseCase.submitHash( - transactionId = transactionId, - transactionHash = transactionHash, - ) - .onLeft { - analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName)) - saveUnsubmittedHashUseCase.invoke( - transactionId = transactionId, - transactionHash = transactionHash, - ) - }.onRight { - Timber.d("Successful hash submission") - } - } - } - - private fun scheduleUpdates() { - coroutineScope.launch { - listOf( - // we should update network to find pending tx after 1 sec - async { - fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrencyStatus.currency.network)) - }, - // we should update tx history and network for new balances - async { - updateStakeBalance() - }, - async { - updateTxHistory() - }, - async { - updateNetworkStatuses() - }, - ).awaitAll() - } - } - - private suspend fun updateNetworkStatuses(delay: Long = BALANCE_UPDATE_DELAY) { - updateDelayedNetworkStatusUseCase( - userWalletId = userWalletId, - network = cryptoCurrencyStatus.currency.network, - delayMillis = delay, - refresh = true, - ) - } - - private suspend fun updateStakeBalance() { - stakingYieldBalanceUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - refresh = true, - ) - } - - private suspend fun updateTxHistory() { - delay(BALANCE_UPDATE_DELAY) - val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase( - userWalletId = userWalletId, - currency = cryptoCurrencyStatus.currency, - ) - - txHistoryItemsCountEither.onRight { - getTxHistoryItemsUseCase( - userWalletId = userWalletId, - currency = cryptoCurrencyStatus.currency, - refresh = true, - ) - } - } - private fun isAssentState(): Boolean { return value.currentStep == StakingStep.Confirmation && (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == @@ -1039,28 +799,8 @@ internal class StakingViewModel @Inject constructor( .getOrElse { false } } - private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal { - val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") - val feeValue = fee.amount.value ?: error("No fee value") - val isEnterAction = stateController.value.actionType == StakingActionCommonType.ENTER - - return checkAndCalculateSubtractedAmount( - isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction, - cryptoCurrencyStatus = cryptoCurrencyStatus, - amountValue = amountValue, - feeValue = feeValue, - reduceAmountBy = reduceAmountBy.orZero(), - ) - } - - private data class FullTransactionData( - val stakeKitTransaction: StakingTransaction, - val tangemTransaction: TransactionData.Compiled, - ) - private companion object { const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/" const val ALLOWANCE_UPDATE_DELAY = 10_000L - const val BALANCE_UPDATE_DELAY = 11_000L } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7dcb445a2f..e6b1b027d1 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,7 +89,7 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-769" +tangemBlockchainSdk = "develop-770" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-381" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^