Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-30 18:04:44 +05:00
commit 5da271e45b
26 changed files with 1252 additions and 105 deletions

View file

@ -436,4 +436,10 @@ internal object TokensDomainModule {
fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase {
return CheckHasLinkedTokensUseCase(currenciesRepository)
}
@Provides
@Singleton
fun provideGetCurrencyCheckUseCase(currencyChecksRepository: CurrencyChecksRepository): GetCurrencyCheckUseCase {
return GetCurrencyCheckUseCase(currencyChecksRepository)
}
}

View file

@ -35,6 +35,7 @@ dependencies {
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets.models)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}

View file

@ -0,0 +1,251 @@
package com.tangem.common.ui.notifications
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.ui.R
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import java.math.BigDecimal
sealed class NotificationUM(val config: NotificationConfig) {
open class Error(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.ic_alert_24,
buttonState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : NotificationUM(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonState,
onCloseClick = onCloseClick,
),
) {
data object TotalExceedsBalance : Error(
title = resourceReference(R.string.send_notification_exceed_balance_title),
subtitle = resourceReference(R.string.send_notification_exceed_balance_text),
)
data object InvalidAmount : Error(
title = resourceReference(R.string.send_notification_invalid_amount_title),
subtitle = resourceReference(R.string.send_notification_invalid_amount_text),
)
data class MinimumAmountError(val amount: String) : Error(
title = resourceReference(R.string.send_notification_invalid_amount_title),
subtitle = resourceReference(
R.string.send_notification_invalid_minimum_amount_text,
wrappedList(amount, amount),
),
)
data class TransactionLimitError(
val cryptoCurrency: String,
val utxoLimit: String,
val amountLimit: String,
val onConfirmClick: () -> Unit,
) : Error(
title = resourceReference(R.string.send_notification_transaction_limit_title),
subtitle = resourceReference(
R.string.send_notification_transaction_limit_text,
wrappedList(cryptoCurrency, utxoLimit, amountLimit),
),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)),
onClick = onConfirmClick,
),
)
data class ExceedsBalance(
val networkIconId: Int,
val currencyName: String,
val feeName: String,
val feeSymbol: String,
val networkName: String,
val mergeFeeNetworkName: Boolean = false,
val onClick: (() -> Unit)? = null,
) : Error(
title = resourceReference(
id = R.string.warning_send_blocked_funds_for_fee_title,
wrappedList(feeName),
),
subtitle = resourceReference(
id = R.string.warning_send_blocked_funds_for_fee_message,
formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol),
),
iconResId = networkIconId,
buttonState = onClick?.let {
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(
R.string.common_buy_currency,
wrappedList(
if (mergeFeeNetworkName) {
"$currencyName ($feeSymbol)"
} else {
feeName
},
),
),
onClick = onClick,
)
},
)
data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error(
title = resourceReference(R.string.send_notification_existential_deposit_title),
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)),
onClick = onConfirmClick,
),
)
data class ReserveAmount(val amount: String) : Error(
title = resourceReference(
id = R.string.send_notification_invalid_reserve_amount_title,
wrappedList(amount),
),
subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text),
)
}
open class Warning(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.img_attention_20,
buttonsState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : NotificationUM(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonsState,
onCloseClick = onCloseClick,
),
) {
data class HighFeeError(
val currencyName: String,
val amount: String,
val onConfirmClick: () -> Unit,
val onCloseClick: () -> Unit,
) : Warning(
title = resourceReference(R.string.send_notification_high_fee_title),
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)),
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)),
onClick = onConfirmClick,
),
onCloseClick = onCloseClick,
)
data object FeeTooLow : Warning(
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
)
data class TooHigh(
val value: String,
) : Warning(
title = resourceReference(id = R.string.send_notification_fee_too_high_title),
subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)),
)
data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = onRefresh,
),
)
data class TronAccountNotActivated(val tokenName: String) : Warning(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(
R.string.send_tron_account_activation_error,
wrappedList(tokenName),
),
)
data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning(
title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference(
R.string.common_network_fee_warning_content,
wrappedList(cryptoAmount, fiatAmount),
),
)
}
sealed interface Cardano {
data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning(
title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title),
subtitle = resourceReference(
id = R.string.cardano_coin_will_be_send_with_token_description,
formatArgs = wrappedList(minAdaValue, tokenName),
),
)
data object InsufficientBalanceToTransferCoin : Error(
title = resourceReference(id = R.string.cardano_max_amount_has_token_title),
subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description),
)
data class InsufficientBalanceToTransferToken(val tokenName: String) : Error(
title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title),
subtitle = resourceReference(
id = R.string.cardano_insufficient_balance_to_send_token_description,
formatArgs = wrappedList(tokenName),
),
)
}
sealed interface Koinos {
data class InsufficientRecoverableMana(
val mana: BigDecimal,
val maxMana: BigDecimal,
) : Error(
title = resourceReference(R.string.koinos_insufficient_mana_to_send_koin_title),
subtitle = resourceReference(
R.string.koinos_insufficient_mana_to_send_koin_description,
formatArgs = wrappedList(
BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()),
BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()),
),
),
)
data object InsufficientBalance : Error(
title = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_title),
subtitle = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_description),
)
data class ManaExceedsBalance(
val availableKoinForTransfer: BigDecimal,
val onReduceClick: () -> Unit,
) : Error(
title = resourceReference(R.string.koinos_mana_exceeds_koin_balance_title),
subtitle = resourceReference(
R.string.koinos_mana_exceeds_koin_balance_description,
formatArgs = wrappedList(
BigDecimalFormatter.formatCryptoAmount(
availableKoinForTransfer,
Blockchain.Koinos.currency,
Blockchain.Koinos.decimals(),
),
),
),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_reduce_to, wrappedList(availableKoinForTransfer)),
onClick = onReduceClick,
),
)
}
}

View file

@ -0,0 +1,362 @@
package com.tangem.common.ui.notifications
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.transaction.error.GetFeeError
import java.math.BigDecimal
@Suppress("LargeClass")
object NotificationsFactory {
fun MutableList<NotificationUM>.addFeeUnreachableNotification(
feeError: GetFeeError,
tokenName: String,
onReload: () -> Unit,
) {
when (feeError) {
is GetFeeError.BlockchainErrors.TronActivationError -> add(
NotificationUM.Warning.TronAccountNotActivated(tokenName),
)
is GetFeeError.DataError,
is GetFeeError.UnknownError,
-> add(
NotificationUM.Warning.NetworkFeeUnreachable(onReload),
)
else -> {
/* do nothing */
}
}
}
fun MutableList<NotificationUM>.addExceedBalanceNotification(
feeAmount: BigDecimal,
sendingAmount: BigDecimal,
isSubtractionAvailable: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
) {
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
if (!isSubtractionAvailable) return
val showNotification = sendingAmount + feeAmount > balance
if (showNotification) {
add(NotificationUM.Error.TotalExceedsBalance)
}
}
fun MutableList<NotificationUM>.addReserveAmountErrorNotification(
reserveAmount: BigDecimal?,
sendingAmount: BigDecimal,
cryptoCurrency: CryptoCurrency,
isAccountFunded: Boolean,
) {
if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) {
add(
NotificationUM.Error.ReserveAmount(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = sendingAmount,
cryptoCurrency = cryptoCurrency,
),
),
)
}
}
fun MutableList<NotificationUM>.addTransactionLimitErrorNotification(
utxoLimit: UtxoAmountLimit?,
cryptoCurrency: CryptoCurrency,
onReduceClick: (
reduceAmountTo: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
if (utxoLimit != null) {
add(
NotificationUM.Error.TransactionLimitError(
cryptoCurrency = cryptoCurrency.name,
utxoLimit = utxoLimit.maxLimit.toPlainString(),
amountLimit = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = utxoLimit.maxAmount,
cryptoCurrency = cryptoCurrency,
),
onConfirmClick = {
onReduceClick(
utxoLimit.maxAmount,
NotificationUM.Error.TransactionLimitError::class.java,
)
},
),
)
}
}
fun MutableList<NotificationUM>.addExistentialWarningNotification(
existentialDeposit: BigDecimal?,
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,
onReduceClick: (
reduceAmountBy: BigDecimal,
reduceAmountByDiff: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
val cryptoCurrency = cryptoCurrencyStatus.currency
val balance = cryptoCurrencyStatus.value.amount ?: return
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
feeAmount
} else {
receivedAmount
}
val diff = balance.minus(spendingAmount)
if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) {
add(
NotificationUM.Error.ExistentialDeposit(
deposit = BigDecimalFormatter.formatCryptoAmountUncapped(
cryptoAmount = existentialDeposit,
cryptoCurrency = cryptoCurrency,
),
onConfirmClick = {
onReduceClick(
existentialDeposit,
existentialDeposit.minus(diff),
NotificationUM.Error.ExistentialDeposit::class.java,
)
},
),
)
}
}
fun MutableList<NotificationUM>.addFeeCoverageNotification(
isFeeCoverage: Boolean,
amountField: AmountFieldModel,
sendingValue: BigDecimal,
appCurrency: AppCurrency,
cryptoCurrencyStatus: CryptoCurrencyStatus,
) {
val cryptoCurrency = cryptoCurrencyStatus.currency
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val amountValue = amountField.cryptoAmount.value ?: return
val cryptoDiff = amountValue.minus(sendingValue)
if (isFeeCoverage) {
add(
NotificationUM.Warning.FeeCoverageNotification(
cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped(
cryptoAmount = cryptoDiff,
cryptoCurrency = cryptoCurrency,
),
fiatAmount = getFiatString(
value = cryptoDiff,
rate = fiatRate,
appCurrency = appCurrency,
),
),
)
}
}
fun MutableList<NotificationUM>.addDustWarningNotification(
dustValue: BigDecimal?,
feeValue: BigDecimal,
sendingAmount: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus?,
) {
if (dustValue == null) return
val isExceedsLimit = checkDustLimits(
feeAmount = feeValue,
receivedAmount = sendingAmount,
dustValue = dustValue,
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCurrencyStatus,
)
if (isExceedsLimit) {
add(
NotificationUM.Error.MinimumAmountError(
amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals),
),
)
}
}
fun MutableList<NotificationUM>.addExceedsBalanceNotification(
cryptoCurrencyWarning: CryptoCurrencyWarning?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
shouldMergeFeeNetworkName: Boolean,
onClick: (CryptoCurrency) -> Unit,
onAnalyticsEvent: (CryptoCurrency) -> Unit,
) {
when (cryptoCurrencyWarning) {
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
add(
NotificationUM.Error.ExceedsBalance(
networkIconId = cryptoCurrencyWarning.coinCurrency.networkIconResId,
networkName = cryptoCurrencyWarning.coinCurrency.name,
currencyName = cryptoCurrencyStatus.currency.name,
feeName = cryptoCurrencyWarning.coinCurrency.name,
feeSymbol = cryptoCurrencyWarning.coinCurrency.symbol,
mergeFeeNetworkName = shouldMergeFeeNetworkName,
onClick = {
onClick(cryptoCurrencyWarning.coinCurrency)
},
),
)
onAnalyticsEvent(cryptoCurrencyStatus.currency)
}
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
val currency = cryptoCurrencyWarning.feeCurrency
add(
NotificationUM.Error.ExceedsBalance(
networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24,
currencyName = cryptoCurrencyWarning.currency.name,
feeName = cryptoCurrencyWarning.feeCurrencyName,
feeSymbol = cryptoCurrencyWarning.feeCurrencySymbol,
networkName = cryptoCurrencyWarning.networkName,
mergeFeeNetworkName = shouldMergeFeeNetworkName,
onClick = {
currency?.let {
onClick(currency)
}
},
),
)
onAnalyticsEvent(cryptoCurrencyWarning.currency)
}
else -> Unit
}
}
fun MutableList<NotificationUM>.addValidateTransactionNotifications(
dustValue: BigDecimal,
fee: Fee?,
validationError: Throwable?,
cryptoCurrency: CryptoCurrency,
onReduceClick: (
reduceAmountTo: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
when (validationError) {
is BlockchainSdkError.Cardano -> addCardanoTransactionValidationError(
error = validationError,
sendingCurrency = cryptoCurrency,
dustValue = dustValue,
)
is BlockchainSdkError.Koinos -> addKoinosTransactionValidationError(
error = validationError,
onReduceClick = onReduceClick,
)
null -> (fee as? Fee.CardanoToken)?.let {
add(
NotificationUM.Cardano.MinAdaValueCharged(
tokenName = cryptoCurrency.name,
minAdaValue = it.minAdaValue.parseBigDecimal(cryptoCurrency.decimals),
),
)
}
else -> return
}
}
private fun MutableList<NotificationUM>.addCardanoTransactionValidationError(
error: BlockchainSdkError.Cardano,
sendingCurrency: CryptoCurrency,
dustValue: BigDecimal?,
) {
when (error) {
BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> {
add(NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name))
}
BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> {
when (sendingCurrency) {
is CryptoCurrency.Coin -> NotificationUM.Cardano.InsufficientBalanceToTransferCoin
is CryptoCurrency.Token -> {
NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)
}
}.let(::add)
}
BlockchainSdkError.Cardano.InsufficientRemainingBalance,
BlockchainSdkError.Cardano.InsufficientSendingAdaAmount,
-> {
dustValue?.let {
add(
NotificationUM.Error.MinimumAmountError(
amount = it.parseBigDecimal(sendingCurrency.decimals),
),
)
}
}
}
}
private fun MutableList<NotificationUM>.addKoinosTransactionValidationError(
error: BlockchainSdkError.Koinos,
onReduceClick: (
reduceAmountTo: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
when (error) {
is BlockchainSdkError.Koinos.InsufficientBalance -> {
add(NotificationUM.Koinos.InsufficientBalance)
}
is BlockchainSdkError.Koinos.InsufficientMana -> {
add(
NotificationUM.Koinos.InsufficientRecoverableMana(
mana = error.manaBalance ?: BigDecimal.ZERO,
maxMana = error.maxMana ?: BigDecimal.ZERO,
),
)
}
is BlockchainSdkError.Koinos.ManaFeeExceedsBalance -> {
add(
NotificationUM.Koinos.ManaExceedsBalance(
availableKoinForTransfer = error.availableKoinForTransfer,
onReduceClick = {
onReduceClick(
error.availableKoinForTransfer,
NotificationUM.Koinos.InsufficientRecoverableMana::class.java,
)
},
),
)
}
else -> {}
}
}
private fun checkDustLimits(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
dustValue: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus?,
): Boolean {
val change = when (cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> {
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
balance - (feeAmount + receivedAmount)
}
is CryptoCurrency.Token -> {
val balance = feeCurrencyStatus?.value?.amount ?: BigDecimal.ZERO
balance - feeAmount
}
}
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
return receivedAmount < dustValue || isChangeLowerThanDust
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.tokens.model.warnings
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import java.math.BigDecimal
data class CryptoCurrencyCheck(
val dustValue: BigDecimal?,
val reserveAmount: BigDecimal?,
val existentialDeposit: BigDecimal?,
val utxoAmountLimit: UtxoAmountLimit?,
)

View file

@ -0,0 +1,41 @@
package com.tangem.domain.tokens
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.wallets.models.UserWalletId
import java.math.BigDecimal
class GetCurrencyCheckUseCase(
private val currencyChecksRepository: CurrencyChecksRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
amount: BigDecimal?,
fee: BigDecimal?,
): CryptoCurrencyCheck {
val network = currencyStatus.currency.network
val dustValue = currencyChecksRepository.getDustValue(userWalletId, network)
val reserveAmount = currencyChecksRepository.getReserveAmount(userWalletId, network)
val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network)
val utxoAmountLimit = if (amount != null && fee != null) {
currencyChecksRepository.checkUtxoAmountLimit(
userWalletId = userWalletId,
network = network,
amount = amount,
fee = fee,
)
} else {
null
}
return CryptoCurrencyCheck(
dustValue = dustValue,
reserveAmount = reserveAmount,
existentialDeposit = existentialDeposit,
utxoAmountLimit = utxoAmountLimit,
)
}
}

View file

@ -1,8 +1,9 @@
package com.tangem.features.staking.impl.di
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.features.staking.impl.navigation.DefaultStakingRouter
import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.staking.impl.navigation.DefaultStakingRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -18,9 +19,10 @@ internal object StakingRouterModule {
@Provides
@ActivityScoped
fun provideStakingRouter(urlOpener: UrlOpener): StakingRouter {
fun provideStakingRouter(urlOpener: UrlOpener, router: AppRouter): StakingRouter {
return DefaultStakingRouter(
urlOpener = urlOpener,
router = router,
)
}
}

View file

@ -1,15 +1,33 @@
package com.tangem.features.staking.impl.navigation
import androidx.fragment.app.Fragment
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.staking.impl.presentation.StakingFragment
internal class DefaultStakingRouter(
private val urlOpener: UrlOpener,
private val router: AppRouter,
) : InnerStakingRouter {
override fun getEntryFragment(): Fragment = StakingFragment.create()
override fun openUrl(url: String) {
urlOpener.openUrl(url)
}
override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) {
router.pop { isSuccess ->
if (isSuccess) {
router.push(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = currency,
),
)
}
}
}
}

View file

@ -1,8 +1,12 @@
package com.tangem.features.staking.impl.navigation
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.staking.api.navigation.StakingRouter
interface InnerStakingRouter : StakingRouter {
fun openUrl(url: String)
fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency)
}

View file

@ -1,61 +1,56 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.*
import com.tangem.features.staking.impl.R
internal sealed class StakingNotification(val config: NotificationConfig) {
internal object StakingNotification {
sealed class Error(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.ic_alert_24,
buttonState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : StakingNotification(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonState,
onCloseClick = onCloseClick,
),
) : NotificationUM.Error(
title = title,
subtitle = subtitle,
iconResId = R.drawable.ic_alert_24,
buttonState = buttonState,
onCloseClick = onCloseClick,
) {
data class StakedPositionNotFoundError(val message: String) : Error(
data class StakedPositionNotFoundError(val message: String) : StakingNotification.Error(
title = stringReference(message),
subtitle = stringReference(message),
)
data class Common(val subtitle: TextReference) : Error(
data class Common(val subtitle: TextReference) : StakingNotification.Error(
title = resourceReference(R.string.common_error),
subtitle = subtitle,
)
}
sealed class Warning(
title: TextReference,
subtitle: TextReference,
buttonsState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : StakingNotification(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = R.drawable.ic_alert_circle_24,
buttonsState = buttonsState,
onCloseClick = onCloseClick,
),
) : NotificationUM.Warning(
title = title,
subtitle = subtitle,
iconResId = R.drawable.ic_alert_circle_24,
buttonsState = buttonsState,
onCloseClick = onCloseClick,
) {
data class EarnRewards(
val subtitleText: TextReference,
) : Warning(
) : StakingNotification.Warning(
title = resourceReference(R.string.staking_notification_earn_rewards_title),
subtitle = subtitleText,
)
data class Unstake(
val cooldownPeriodDays: Int,
) : Warning(
) : StakingNotification.Warning(
title = resourceReference(R.string.common_unstake),
subtitle = resourceReference(
R.string.staking_notification_unstake_text,
@ -72,6 +67,6 @@ internal sealed class StakingNotification(val config: NotificationConfig) {
data class TransactionInProgress(
val title: TextReference,
val description: TextReference,
) : Warning(title = title, subtitle = description)
) : StakingNotification.Warning(title = title, subtitle = description)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.event.StateEvent
@ -13,6 +14,7 @@ import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
/**
* Ui states of the staking screen
@ -84,10 +86,11 @@ internal sealed class StakingStates {
val feeState: FeeState,
val validatorState: ValidatorState,
val pendingAction: PendingAction?,
val notifications: ImmutableList<StakingNotification>,
val notifications: ImmutableList<NotificationUM>,
val footerText: String,
val transactionDoneState: TransactionDoneState,
val isApprovalNeeded: Boolean,
val reduceAmountBy: BigDecimal?,
) : ConfirmationState()
data class Empty(

View file

@ -89,5 +89,6 @@ internal object ConfirmationStatePreviewData {
transactionDoneState = TransactionDoneState.Empty,
pendingAction = null,
isApprovalNeeded = false,
reduceAmountBy = null,
)
}

View file

@ -1,11 +1,14 @@
package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.common.ui.notifications.NotificationUM
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.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 java.math.BigDecimal
@Suppress("TooManyFunctions")
internal object StakingClickIntentsStub : StakingClickIntents {
@ -49,4 +52,18 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun onFailedTxEmailClick(errorMessage: String) {}
override fun onActiveStake(activeStake: BalanceState) {}
override fun getFee(pendingAction: PendingAction?) {}
override fun onAmountReduceByClick(
reduceAmountBy: BigDecimal,
reduceAmountByDiff: BigDecimal,
notification: Class<out NotificationUM>,
) {}
override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class<out NotificationUM>) {}
override fun onNotificationCancel(notification: Class<out NotificationUM>) {}
override fun openTokenDetails(cryptoCurrency: CryptoCurrency) {}
}

View file

@ -1,28 +1,37 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.StakingNotification
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.toPersistentList
internal class AddStakingErrorTransformer(
private val error: StakingError,
private val error: StakingError? = null,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmationState =
prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
val notifications = buildList {
addAll(confirmationState.notifications)
error?.let { add(convertToNotification(it)) }
}.toPersistentList()
return prevState.copy(
confirmationState = confirmationState.copy(
notifications = (confirmationState.notifications + convertToNotification(error)).toPersistentList(),
notifications = notifications,
feeState = FeeState.Error,
),
)
}
private fun convertToNotification(error: StakingError): StakingNotification {
private fun convertToNotification(error: StakingError): NotificationUM {
return when (error) {
is StakingError.StakedPositionNotFoundError -> StakingNotification.Error.StakedPositionNotFoundError(
message = error.toString(),

View file

@ -0,0 +1,189 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.StakingNotification
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
import com.tangem.features.staking.impl.presentation.state.utils.checkFeeCoverage
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@Suppress("LongParameterList")
internal class AddStakingNotificationsTransformer(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val currencyWarning: CryptoCurrencyWarning?,
private val validatorError: Throwable?,
private val feeError: GetFeeError?,
private val currencyCheck: CryptoCurrencyCheck,
private val isSubtractAvailable: Boolean,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val balance = cryptoCurrencyStatus.value.amount.orZero()
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
val amountState = prevState.amountState as? AmountState.Data ?: return prevState
val feeState = confirmationState.feeState as? FeeState.Content
val amountValue = amountState.amountTextField.cryptoAmount.value.orZero()
val feeValue = feeState?.fee?.amount?.value.orZero()
val reduceAmountBy = confirmationState.reduceAmountBy.orZero()
val isEnterAction = prevState.actionType == StakingActionCommonType.ENTER
val isFeeCoverage = checkFeeCoverage(
amountValue = amountValue,
feeValue = feeValue,
balance = balance,
isSubtractAvailable = isSubtractAvailable,
reduceAmountBy = reduceAmountBy,
)
val sendingAmount = checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
val notifications = buildList {
// errors
addErrorNotifications(
prevState = prevState,
feeError = feeError,
sendingAmount = sendingAmount,
onReload = { prevState.clickIntents.getFee(confirmationState.pendingAction) },
feeValue = feeValue,
)
// warnings
addWarningNotifications(
prevState = prevState,
amountState = amountState,
feeState = feeState,
sendingAmount = sendingAmount,
isFeeCoverage = isFeeCoverage && isEnterAction,
)
addAll(confirmationState.notifications)
}.toImmutableList()
return prevState.copy(
confirmationState = confirmationState.copy(
notifications = notifications.toImmutableList(),
isPrimaryButtonEnabled = !notifications.any { it is StakingNotification.Error },
),
)
}
private fun MutableList<NotificationUM>.addErrorNotifications(
prevState: StakingUiState,
onReload: () -> Unit,
feeError: GetFeeError?,
sendingAmount: BigDecimal,
feeValue: BigDecimal,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
val network = cryptoCurrency.network
if (feeError != null) {
addFeeUnreachableNotification(
feeError = feeError,
tokenName = cryptoCurrencyStatusProvider().currency.name,
onReload = onReload,
)
}
addExceedBalanceNotification(
feeAmount = feeValue,
sendingAmount = sendingAmount,
isSubtractionAvailable = isSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
addExceedsBalanceNotification(
cryptoCurrencyWarning = currencyWarning,
cryptoCurrencyStatus = cryptoCurrencyStatus,
shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId),
onClick = prevState.clickIntents::openTokenDetails,
onAnalyticsEvent = { /* [REDACTED_TODO_COMMENT] */ },
)
if (!BlockchainUtils.isCardano(network.id.value)) {
addDustWarningNotification(
dustValue = currencyCheck.dustValue,
feeValue = feeValue,
sendingAmount = sendingAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCryptoCurrencyStatus,
)
}
addTransactionLimitErrorNotification(
utxoLimit = currencyCheck.utxoAmountLimit,
cryptoCurrency = cryptoCurrency,
onReduceClick = prevState.clickIntents::onAmountReduceToClick,
)
addReserveAmountErrorNotification(
reserveAmount = currencyCheck.reserveAmount,
sendingAmount = sendingAmount,
cryptoCurrency = cryptoCurrency,
isAccountFunded = false,
)
}
private fun MutableList<NotificationUM>.addWarningNotifications(
prevState: StakingUiState,
amountState: AmountState.Data,
feeState: FeeState.Content?,
sendingAmount: BigDecimal,
isFeeCoverage: Boolean,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
addExistentialWarningNotification(
existentialDeposit = currencyCheck.existentialDeposit,
feeAmount = feeState?.fee?.amount?.value.orZero(),
receivedAmount = sendingAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus,
onReduceClick = prevState.clickIntents::onAmountReduceByClick,
)
addFeeCoverageNotification(
isFeeCoverage = isFeeCoverage,
amountField = amountState.amountTextField,
sendingValue = sendingAmount,
appCurrency = appCurrency,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
// blockchain specific
addValidateTransactionNotifications(
dustValue = currencyCheck.dustValue.orZero(),
fee = feeState?.fee,
validationError = validatorError,
cryptoCurrency = cryptoCurrency,
onReduceClick = prevState.clickIntents::onAmountReduceToClick,
)
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.common.ui.notifications.NotificationUM
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.persistentListOf
import kotlinx.collections.immutable.toPersistentList
internal class DismissStakingNotificationsStateTransformer(
private val notification: Class<out NotificationUM>,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val updatedNotifications = confirmationState?.notifications
?.filterNot { it::class == notification }?.toPersistentList()
?: persistentListOf()
return prevState.copy(
confirmationState = confirmationState?.copy(
notifications = updatedNotifications,
) ?: StakingStates.ConfirmationState.Empty(),
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.staking.model.stakekit.Yield
@ -34,11 +35,12 @@ internal class SetConfirmationStateLoadingTransformer(
transactionDoneState = TransactionDoneState.Empty,
pendingAction = null,
isApprovalNeeded = false,
reduceAmountBy = null,
),
)
}
private fun getNotifications(prevState: StakingUiState): ImmutableList<StakingNotification> {
private fun getNotifications(prevState: StakingUiState): ImmutableList<NotificationUM> {
return persistentListOf(
if (prevState.actionType == StakingActionCommonType.EXIT) {
StakingNotification.Warning.Unstake(

View file

@ -214,6 +214,7 @@ internal class SetInitialDataStateTransformer(
transactionDoneState = TransactionDoneState.Empty,
pendingAction = null,
isApprovalNeeded = isApprovalNeeded,
reduceAmountBy = null,
)
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.staking.impl.presentation.state.transformers.amount
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class AmountReduceByStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val value: ReduceByData,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
amountState = AmountReduceByTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState),
)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.staking.impl.presentation.state.transformers.amount
import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal
internal class AmountReduceToStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val value: BigDecimal,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
amountState = AmountReduceToTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState),
)
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.features.staking.impl.presentation.state.utils
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import java.math.BigDecimal
/**
* Check and calculates subtracted amount
*/
internal fun checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
reduceAmountBy: BigDecimal,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isAmountSubtractAvailable,
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
return if (isFeeCoverage) {
balance.minus(reduceAmountBy).minus(feeValue)
} else {
amountValue
}
}
/**
* Checks if sending amount with fee is greater than balance
*/
internal fun checkFeeCoverage(
isSubtractAvailable: Boolean,
balance: BigDecimal,
amountValue: BigDecimal,
feeValue: BigDecimal,
reduceAmountBy: BigDecimal?,
): Boolean {
if (!isSubtractAvailable) return false
val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO)
return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue
}

View file

@ -5,6 +5,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.CardWithIcon
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.resolveReference
@ -13,7 +14,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingNotification
import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun NotificationsBlock(notifications: ImmutableList<StakingNotification>) {
internal fun NotificationsBlock(notifications: ImmutableList<NotificationUM>) {
notifications.forEach { notification ->
key(notification) {
if (notification is StakingNotification.Warning.TransactionInProgress) {
@ -32,8 +33,15 @@ internal fun NotificationsBlock(notifications: ImmutableList<StakingNotification
Notification(
config = notification.config,
iconTint = when (notification) {
is StakingNotification.Error -> TangemTheme.colors.icon.warning
is StakingNotification.Warning -> TangemTheme.colors.icon.accent
is NotificationUM.Error.ExceedsBalance,
is NotificationUM.Warning,
-> null
is StakingNotification.Error,
is NotificationUM.Error,
-> TangemTheme.colors.icon.warning
},
)
}

View file

@ -1,12 +1,16 @@
package com.tangem.features.staking.impl.presentation.viewmodel
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.notifications.NotificationUM
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.tokens.model.CryptoCurrency
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import java.math.BigDecimal
@Suppress("TooManyFunctions")
internal interface StakingClickIntents : AmountScreenClickIntents {
fun onBackClick()
@ -21,6 +25,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun onInfoClick(infoType: InfoType)
fun getFee(pendingAction: PendingAction?)
override fun onAmountNext() = onNextClick(actionType = null)
fun openValidators()
@ -35,9 +41,21 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun onApprovalClick()
fun onAmountReduceByClick(
reduceAmountBy: BigDecimal,
reduceAmountByDiff: BigDecimal,
notification: Class<out NotificationUM>,
)
fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class<out NotificationUM>)
fun onNotificationCancel(notification: Class<out NotificationUM>)
fun onExploreClick()
fun onShareClick()
fun onFailedTxEmailClick(errorMessage: String)
fun openTokenDetails(cryptoCurrency: CryptoCurrency)
}

View file

@ -12,7 +12,9 @@ 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
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
@ -32,16 +34,15 @@ 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.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
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.utils.convertToSdkAmount
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -50,10 +51,7 @@ 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.transformers.*
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer
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.amount.*
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalInProgressTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer
@ -103,6 +101,10 @@ internal class StakingViewModel @Inject constructor(
private val feedbackManager: FeedbackManager,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
private val validateTransactionUseCase: ValidateTransactionUseCase,
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
@DelayedWork private val coroutineScope: CoroutineScope,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
@ -140,6 +142,7 @@ internal class StakingViewModel @Inject constructor(
)
private var stakingApproval: StakingApproval = StakingApproval.Empty
private var isAmountSubtractAvailable: Boolean = false
private val allowanceTaskScheduler = SingleTaskScheduler<BigDecimal>()
private val transactionsInProgress: CopyOnWriteArrayList<StakingTransaction> = CopyOnWriteArrayList()
@ -171,6 +174,57 @@ internal class StakingViewModel @Inject constructor(
stakingStateRouter.onNextClick()
}
override fun getFee(pendingAction: PendingAction?) {
viewModelScope.launch {
stateController.update(
SetConfirmationStateLoadingTransformer(
yield = yield,
),
)
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,
)
} else {
estimateGas(
pendingAction = pendingAction,
amount = amount,
sourceAddress = sourceAddress,
validatorAddress = validatorAddress,
)
}
} else {
estimateGas(
pendingAction = pendingAction,
amount = amount,
sourceAddress = sourceAddress,
validatorAddress = validatorAddress,
)
}
}
}
private fun handleOnNextConfirmationClick() {
if (isAssentState()) {
viewModelScope.launch {
@ -236,57 +290,6 @@ internal class StakingViewModel @Inject constructor(
}
}
private fun getFee(pendingAction: PendingAction?) {
viewModelScope.launch {
stateController.update(
SetConfirmationStateLoadingTransformer(
yield = yield,
),
)
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,
)
} else {
estimateGas(
pendingAction = pendingAction,
amount = amount,
sourceAddress = sourceAddress,
validatorAddress = validatorAddress,
)
}
} else {
estimateGas(
pendingAction = pendingAction,
amount = amount,
sourceAddress = sourceAddress,
validatorAddress = validatorAddress,
)
}
}
}
private suspend fun estimateGas(
pendingAction: PendingAction?,
amount: BigDecimal,
@ -310,7 +313,6 @@ internal class StakingViewModel @Inject constructor(
stateController.update(AddStakingErrorTransformer(it))
return
}
stateController.update(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
@ -325,24 +327,30 @@ internal class StakingViewModel @Inject constructor(
action = pendingAction,
),
)
updateNotifications()
}
private suspend fun getApproveFee(amount: BigDecimal, validatorAddress: String) {
val approvalFee = getFeeUseCase(
getFeeUseCase(
amount = amount,
destination = validatorAddress,
userWallet = userWallet,
cryptoCurrency = cryptoCurrencyStatus.currency,
).getOrElse {
return stakingEventFactory.createGenericErrorAlert(it.toString())
}
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = approvalFee,
),
).fold(
ifRight = { fee ->
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = fee,
),
)
updateNotifications()
},
ifLeft = {
stateController.update(AddStakingErrorTransformer())
updateNotifications(it)
},
)
}
@ -499,6 +507,85 @@ internal class StakingViewModel @Inject constructor(
}.saveIn(approvalJobHolder)
}
private fun updateNotifications(feeError: GetFeeError? = null) {
viewModelScope.launch {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
val feeState = confirmationState?.feeState as? FeeState.Content
val amountState = value.amountState as? AmountState.Data
val amount = amountState?.amountTextField?.cryptoAmount?.value
val fee = feeState?.fee?.amount?.value
val currencyWarning = if (feeCryptoCurrencyStatus != null && fee != null) {
getBalanceNotEnoughForFeeWarningUseCase(
fee = fee,
userWalletId = userWalletId,
tokenStatus = cryptoCurrencyStatus,
coinStatus = feeCryptoCurrencyStatus ?: cryptoCurrencyStatus,
).getOrNull()
} else {
null
}
val validation = amount?.let {
validateTransactionUseCase(
userWalletId = userWalletId,
amount = amount.convertToSdkAmount(cryptoCurrencyStatus.currency),
fee = feeState?.fee,
memo = null,
destination = "",
network = cryptoCurrencyStatus.currency.network,
).leftOrNull()
}
val currencyStatus = getCurrencyCheckUseCase(
userWalletId = userWalletId,
currencyStatus = cryptoCurrencyStatus,
amount = amount,
fee = fee,
)
stateController.update(
AddStakingNotificationsTransformer(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
currencyWarning = currencyWarning,
validatorError = validation,
currencyCheck = currencyStatus,
isSubtractAvailable = isAmountSubtractAvailable,
feeError = feeError,
),
)
}
}
override fun onAmountReduceByClick(
reduceAmountBy: BigDecimal,
reduceAmountByDiff: BigDecimal,
notification: Class<out NotificationUM>,
) {
AmountReduceByStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
value = AmountReduceByTransformer.ReduceByData(
reduceAmountBy = reduceAmountBy,
reduceAmountByDiff = reduceAmountByDiff,
),
)
onNotificationCancel(notification)
}
override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class<out NotificationUM>) {
stateController.update(
AmountReduceToStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
value = reduceAmountTo,
),
)
onNotificationCancel(notification)
}
override fun onNotificationCancel(notification: Class<out NotificationUM>) {
stateController.update(DismissStakingNotificationsStateTransformer(notification))
}
private fun awaitForAllowance(pendingAction: PendingAction?) {
val approval = stakingApproval as? StakingApproval.Needed ?: return
allowanceTaskScheduler.scheduleTask(
@ -586,6 +673,10 @@ internal class StakingViewModel @Inject constructor(
}
}
override fun openTokenDetails(cryptoCurrency: CryptoCurrency) {
innerRouter.openTokenDetails(userWalletId, cryptoCurrency)
}
fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) {
innerRouter = router
this.stakingStateRouter = stateRouter
@ -612,6 +703,7 @@ internal class StakingViewModel @Inject constructor(
cryptoCurrencyStatus = it
setupApprovalNeeded()
checkIfSubtractAvailable()
stateController.update(
transformer = SetInitialDataStateTransformer(
@ -779,6 +871,11 @@ internal class StakingViewModel @Inject constructor(
InnerConfirmationStakingState.ASSENT
}
private suspend fun checkIfSubtractAvailable() {
isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrencyStatus.currency)
.getOrElse { false }
}
private data class FullTransactionData(
val stakeKitTransaction: StakingTransaction,
val tangemTransaction: TransactionData.Compiled,

View file

@ -56,6 +56,11 @@ object BlockchainUtils {
return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false
}
fun isArbitrum(networkId: String): Boolean {
val blockchain = Blockchain.fromId(networkId)
return blockchain == Blockchain.Arbitrum
}
data class BlockchainInfo(
val blockchainId: String,
val name: String,