Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-12 22:01:26 +05:00
parent f748b8539d
commit f1238f5ef8
20 changed files with 528 additions and 88 deletions

View file

@ -0,0 +1,45 @@
package com.tangem.features.staking.impl.presentation.state.transformers.approval
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
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
internal class SetConfirmationStateAssentApprovalTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val fee: TransactionFee,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
confirmationState = prevState.confirmationState.copyWrapped(),
bottomSheetConfig = null,
)
}
private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState {
return if (this is StakingStates.ConfirmationState.Data) {
copy(
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Content(
fee = fee.normal,
rate = cryptoCurrencyStatusProvider().value.fiatRate,
isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate,
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),
validatorState = validatorState.copySealed(isClickable = true),
isPrimaryButtonEnabled = true,
isApprovalNeeded = true,
)
} else {
this
}
}
}

View file

@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.common.extensions.hexToBytes
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.ui.amountScreen.models.AmountState
@ -18,15 +18,20 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.staking.*
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.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.models.UserWallet
@ -40,6 +45,7 @@ import com.tangem.features.staking.impl.presentation.state.transformers.amount.A
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer
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.utils.Provider
@ -51,6 +57,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
import kotlin.properties.Delegates
@ -64,6 +71,7 @@ internal class StakingViewModel @Inject constructor(
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 getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
@ -71,6 +79,9 @@ internal class StakingViewModel @Inject constructor(
private val submitHashUseCase: SubmitHashUseCase,
private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase,
private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase,
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
private val getAllowanceUseCase: GetAllowanceUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val isApproveNeededUseCase: IsApproveNeededUseCase,
private val clipboardManager: ClipboardManager,
private val vibratorHapticManager: VibratorHapticManager,
@ -101,7 +112,7 @@ internal class StakingViewModel @Inject constructor(
private var userWallet: UserWallet by Delegates.notNull()
private var appCurrency: AppCurrency by Delegates.notNull()
private var isApprovalNeeded: Boolean = false
private var stakingApproval: StakingApproval = StakingApproval.Empty
private var approvalJobHolder: JobHolder = JobHolder()
@ -121,7 +132,7 @@ internal class StakingViewModel @Inject constructor(
}
stakingStateRouter.onNextClick()
if (isAssentState()) {
estimateGas(pendingActions)
getFee(pendingActions)
}
}
@ -139,6 +150,9 @@ internal class StakingViewModel @Inject constructor(
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 amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value")
val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided")
val stakingTransaction = getStakingTransactionUseCase(
userWalletId = userWalletId,
@ -146,8 +160,7 @@ internal class StakingViewModel @Inject constructor(
params = ActionParams(
actionCommonType = value.actionType,
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
amount = amountValue,
address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
?: error("No available address"),
validatorAddress = validatorState.chosenValidator.address,
@ -159,19 +172,27 @@ internal class StakingViewModel @Inject constructor(
error(it)
}
stakingTransaction.unsignedTransaction?.let {
sendStakingTransaction(
transactionId = stakingTransaction.id,
gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"),
txData = TransactionData.Compiled(value = it.hexToBytes()),
pendingActionList = confirmationState.pendingActions,
)
} ?: error("No unsigned transaction available")
stakingTransaction
.filterNot { it.type == StakingTransactionType.APPROVAL }
.forEach { transaction ->
val (constructedTransaction, transactionData) = getConstructedStakingTransactionUseCase(
networkId = cryptoCurrencyStatus.currency.network.id.value,
fee = fee,
transactionId = transaction.id,
).getOrNull() ?: error("No constructed transaction")
sendStakingTransaction(
transactionId = constructedTransaction.id,
gasEstimate = constructedTransaction.gasEstimate ?: error("No gas estimate available"),
txData = transactionData,
pendingActionList = confirmationState.pendingActions,
)
}
}
}
}
private fun estimateGas(pendingActions: ImmutableList<PendingAction>) {
private fun getFee(pendingActions: ImmutableList<PendingAction>) {
viewModelScope.launch {
stateController.update(
SetConfirmationStateLoadingTransformer(
@ -184,38 +205,99 @@ internal class StakingViewModel @Inject constructor(
val validatorState = confirmationState.validatorState as? ValidatorState.Content
?: error("No validator provided")
val pendingAction = pendingActions.firstOrNull()
val stakingGasEstimate = estimateGasUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = value.actionType,
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
address = cryptoCurrencyValue.networkAddress?.defaultAddress?.value
?: error("No available address"),
validatorAddress = validatorState.chosenValidator.address,
token = yield.token,
passthrough = pendingAction?.passthrough,
type = pendingAction?.type,
),
).getOrElse {
stateController.update(AddStakingErrorTransformer(it))
return@launch
}
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
stateController.update(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = stakingGasEstimate,
pendingActionList = pendingActions,
),
)
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,
)
} else {
estimateGas(
pendingActions = pendingActions,
amount = amount,
sourceAddress = sourceAddress,
validatorAddress = validatorAddress,
)
}
} else {
estimateGas(
pendingActions = pendingActions,
amount = amount,
sourceAddress = sourceAddress,
validatorAddress = validatorAddress,
)
}
}
}
private suspend fun estimateGas(
pendingActions: ImmutableList<PendingAction>,
amount: BigDecimal,
sourceAddress: String,
validatorAddress: String,
) {
val pendingAction = pendingActions.firstOrNull()
val stakingGasEstimate = 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 {
stateController.update(AddStakingErrorTransformer(it))
return
}
stateController.update(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = stakingGasEstimate,
pendingActionList = pendingActions,
),
)
}
private suspend fun getApproveFee(amount: BigDecimal, validatorAddress: String) {
val approvalFee = getFeeUseCase(
amount = amount,
destination = validatorAddress,
userWallet = userWallet,
cryptoCurrency = cryptoCurrencyStatus.currency,
).getOrElse {
// TODO staking error
return
}
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
fee = approvalFee,
),
)
}
override fun onPrevClick() {
stakingStateRouter.onPrevClick()
}
@ -286,7 +368,60 @@ internal class StakingViewModel @Inject constructor(
},
)
// todo staking polygon
val tokenCryptoCurrency =
cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: error("No token currency")
val amountValue = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided")
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
?: error("No confirmation state")
val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided")
val approval = stakingApproval as? StakingApproval.Needed ?: error("No staking approve spender address")
val approvalTransaction = createApprovalTransactionUseCase(
amount = amountValue,
contractAddress = tokenCryptoCurrency.contractAddress,
spenderAddress = approval.spenderAddress,
fee = fee,
cryptoCurrency = tokenCryptoCurrency,
userWalletId = userWalletId,
).fold(
ifLeft = { error ->
Timber.e(error.toString())
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
fee = TransactionFee.Single(fee),
),
)
// TODO staking error
return@launch
},
ifRight = { it },
)
sendTransactionUseCase(
txData = approvalTransaction,
userWallet = userWallet,
network = tokenCryptoCurrency.network,
).fold(
ifLeft = { error ->
Timber.e(error.toString())
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
fee = TransactionFee.Single(fee),
),
)
// TODO staking error
},
ifRight = {
getFee(confirmationState.pendingActions)
stateController.update(DismissBottomSheetStateTransformer())
},
)
}.saveIn(approvalJobHolder)
}
@ -319,7 +454,7 @@ internal class StakingViewModel @Inject constructor(
}
private fun setupApprovalNeeded() {
isApprovalNeeded = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { false }
stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { StakingApproval.Empty }
}
private fun subscribeOnCurrencyStatusUpdates() {

View file

@ -7,7 +7,6 @@ import arrow.core.raise.either
import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.datasource.api.common.response.ApiResponse
@ -362,10 +361,10 @@ internal class DefaultSwapRepository @Inject constructor(
),
) ?: error("Cannot cast to Approver")
return when (result) {
is Result.Success -> result.data
is Result.Failure -> error(result.error)
}
return result.fold(
onSuccess = { it },
onFailure = { error(it) },
)
}
override suspend fun getApproveData(