Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-16 14:14:19 +05:00
commit da409f11ce
19 changed files with 842 additions and 433 deletions

View file

@ -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
}
}
}

View file

@ -92,6 +92,7 @@ internal sealed class StakingStates {
val feeState: FeeState,
val validatorState: ValidatorState,
val pendingAction: PendingAction?,
val pendingActions: ImmutableList<PendingAction>?,
val notifications: ImmutableList<NotificationUM>,
val footerText: TextReference,
val transactionDoneState: TransactionDoneState,

View file

@ -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
}
}

View file

@ -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<PendingAction>?,
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<PendingAction>?,
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<StakingError, StakingGasEstimate>,
): Either<StakingError, StakingGasEstimate> {
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
}
}

View file

@ -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<StakingTransaction>) -> 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<StakingTransaction>?,
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<StakingTransaction> {
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<FullTransactionData>,
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<String>, transactionHashes: List<String>) {
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
}
}

View file

@ -92,5 +92,6 @@ internal object ConfirmationStatePreviewData {
pendingAction = null,
isApprovalNeeded = false,
reduceAmountBy = null,
pendingActions = null,
)
}

View file

@ -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<PendingAction>?,
) {
}
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<PendingAction>?) {
}
override fun onAmountReduceByClick(
reduceAmountBy: BigDecimal,

View file

@ -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

View file

@ -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<StakingUiState> {
internal object DismissBottomSheetStateTransformer : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(bottomSheetConfig = null)
}

View file

@ -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<PendingAction>?,
) : Transformer<StakingUiState> {
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(),
)
}
}

View file

@ -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<AppCurrency>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val fee: Fee,
private val action: PendingAction?,
private val actions: ImmutableList<PendingAction>?,
) : Transformer<StakingUiState> {
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,
)

View file

@ -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<AppCurrency>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val fee: Fee,
private val txUrl: String,
) : Transformer<StakingUiState> {
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(

View file

@ -38,6 +38,7 @@ internal class SetConfirmationStateLoadingTransformer(
footerText = getFooter(prevState),
transactionDoneState = TransactionDoneState.Empty,
pendingAction = possibleConfirmationState?.pendingAction,
pendingActions = possibleConfirmationState?.pendingActions,
isApprovalNeeded = false,
reduceAmountBy = null,
),

View file

@ -224,6 +224,7 @@ internal class SetInitialDataStateTransformer(
footerText = TextReference.EMPTY,
transactionDoneState = TransactionDoneState.Empty,
pendingAction = null,
pendingActions = null,
isApprovalNeeded = isApprovalNeeded,
reduceAmountBy = null,
)

View file

@ -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<PendingAction>?): Boolean {
val isSolana = isSolana(networkId)
val isWithdraw = pendingActions?.all { it.type == StakingActionType.WITHDRAW } == true
return isSolana && isWithdraw
}

View file

@ -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)

View file

@ -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<PendingAction>? = 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<PendingAction>?)
override fun onAmountNext() = onNextClick(actionTypeToOverwrite = null)

View file

@ -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<PendingAction>?,
) {
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<PendingAction>?) {
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<FullTransactionData>,
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<String>, transactionHashes: List<String>) {
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
}
}

View file

@ -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 ^