Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-22 20:49:09 +05:00
parent 34344f71b0
commit 178ae9c928
6 changed files with 122 additions and 33 deletions

View file

@ -103,6 +103,10 @@ internal sealed class SendAnalyticEvents(
params = mapOf(TOKEN to token),
)
data object NoticeFeeCoverage : SendAnalyticEvents(
event = "Notice - Network Fee Coverage",
)
/** If error occurs during send transactions */
data class TransactionError(val token: String) : SendAnalyticEvents(
event = "Error - Transaction Rejected",

View file

@ -42,11 +42,6 @@ internal sealed class SendNotification(val config: NotificationConfig) {
),
)
data class ReserveAmountError(val amount: String) : Error(
title = resourceReference(R.string.send_notification_invalid_reserve_amount_title, wrappedList(amount)),
subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text),
)
data class TransactionLimitError(
val cryptoCurrency: String,
val utxoLimit: String,
@ -153,5 +148,10 @@ internal sealed class SendNotification(val config: NotificationConfig) {
onClick = onRefresh,
),
)
data object FeeCoverageNotification : Warning(
title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference(R.string.swapping_network_fee_warning_content),
)
}
}

View file

@ -18,11 +18,10 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.fee.checkIfFeeTooHigh
import com.tangem.features.send.impl.presentation.state.fee.*
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.lib.crypto.BlockchainUtils.isDogecoin
import com.tangem.lib.crypto.BlockchainUtils.isTezos
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -36,11 +35,11 @@ import java.math.BigDecimal
internal class SendNotificationFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feePaidCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val currentStateProvider: Provider<SendUiState>,
private val userWalletProvider: Provider<UserWallet>,
private val currencyChecksRepository: CurrencyChecksRepository,
private val stateRouterProvider: Provider<StateRouter>,
private val isSubtractAvailableProvider: Provider<Boolean>,
private val clickIntents: SendClickIntents,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
@ -52,19 +51,33 @@ internal class SendNotificationFactory(
val state = currentStateProvider()
val sendState = state.sendState ?: return@map persistentListOf()
val feeState = state.feeState ?: return@map persistentListOf()
val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isSubtractAvailableProvider(),
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
)
val sendingAmount = calculateSubtractedAmount(
isFeeCoverage = isFeeCoverage,
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
amountValue = amountValue,
feeValue = feeValue,
)
buildList {
// errors
addFeeUnreachableNotification(feeState.feeSelectorState)
addExceedBalanceNotification(feeAmount, amountValue)
addExceedBalanceNotification(feeValue, sendingAmount)
addExceedsBalanceNotification(feeState.fee)
addMinimumAmountErrorNotification(feeAmount, amountValue)
addDustWarningNotification(feeAmount, amountValue)
addTransactionLimitErrorNotification(feeAmount, amountValue)
addMinimumAmountErrorNotification(feeValue, sendingAmount)
addDustWarningNotification(feeValue, sendingAmount)
addTransactionLimitErrorNotification(feeValue, sendingAmount)
// warnings
addExistentialWarningNotification(feeAmount, amountValue)
addHighFeeWarningNotification(feeAmount, amountValue, sendState.ignoreAmountReduce)
addExistentialWarningNotification(feeValue, amountValue)
addFeeCoverageNotification(isFeeCoverage)
addHighFeeWarningNotification(amountValue, sendState.ignoreAmountReduce)
addTooHighNotification(feeState.feeSelectorState)
addTooLowNotification(feeState)
}.toImmutableList()
@ -95,13 +108,11 @@ internal class SendNotificationFactory(
receivedAmount: BigDecimal,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatusProvider()
val cryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val isCurrentNotFeePaidCurrency = cryptoCurrencyStatus.currency.id != feePaidCryptoCurrencyStatus?.currency?.id
if (isCurrentNotFeePaidCurrency) return
if (!isSubtractAvailableProvider()) return
val showNotification = receivedAmount + feeAmount > cryptoAmount
val showNotification = receivedAmount + feeAmount > balance
if (showNotification) {
add(SendNotification.Error.TotalExceedsBalance)
}
@ -192,16 +203,22 @@ internal class SendNotificationFactory(
}
}
private fun MutableList<SendNotification>.addFeeCoverageNotification(sendingAmount: Boolean) {
if (sendingAmount) {
analyticsEventHandler.send(SendAnalyticEvents.NoticeFeeCoverage)
add(SendNotification.Warning.FeeCoverageNotification)
}
}
private fun MutableList<SendNotification>.addHighFeeWarningNotification(
feeAmount: BigDecimal,
sendAmount: BigDecimal,
ignoreAmountReduce: Boolean,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value)
val threshold = Blockchain.Tezos.minimalAmount()
val isTotalBalance = feeAmount.plus(sendAmount) >= balance && balance > threshold
val isTotalBalance = sendAmount >= balance && balance > threshold
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
add(
SendNotification.Warning.HighFeeError(
@ -218,6 +235,7 @@ internal class SendNotificationFactory(
}
}
// todo remove in [REDACTED_TASK_KEY]
private fun MutableList<SendNotification>.addMinimumAmountErrorNotification(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,

View file

@ -1,21 +1,73 @@
package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.common.extensions.minimalAmount
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.lib.crypto.BlockchainUtils
import java.math.BigDecimal
import java.math.RoundingMode
/**
* Check if sending amount with fee is greater than balance
* Check and calculates subtracted amount
*/
internal fun checkFeeCoverage(state: SendUiState, cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean {
val balance = cryptoCurrencyStatus.value.amount ?: return false
val fee = state.feeState?.fee?.amount?.value ?: return false
val amount = state.amountState?.amountTextField?.cryptoAmount?.value ?: return false
return balance < amount + fee && balance > fee
internal fun checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isAmountSubtractAvailable,
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
)
return calculateSubtractedAmount(
isFeeCoverage = isFeeCoverage,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
)
}
/**
* Checks if sending amount with fee is greater than balance
*/
internal fun checkFeeCoverage(
isSubtractAvailable: Boolean,
balance: BigDecimal,
amountValue: BigDecimal,
feeValue: BigDecimal,
): Boolean {
if (!isSubtractAvailable) return false
return balance < amountValue + feeValue && balance > feeValue
}
/**
* Calculates subtracted amount
*/
internal fun calculateSubtractedAmount(
isFeeCoverage: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
return if (isFeeCoverage) {
var subtractedValue = minOf(amountValue, balance.minus(feeValue))
if (BlockchainUtils.isTezos(cryptoCurrencyStatus.currency.network.id.value)) {
val threshold = Blockchain.Tezos.minimalAmount()
subtractedValue = -threshold
}
subtractedValue
} else {
amountValue
}
}
/**

View file

@ -157,10 +157,10 @@ internal class SendViewModel @Inject constructor(
private val sendNotificationFactory = SendNotificationFactory(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
feePaidCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
currentStateProvider = Provider { uiState },
userWalletProvider = Provider { userWallet },
stateRouterProvider = Provider { stateRouter },
isSubtractAvailableProvider = Provider { isAmountSubtractAvailable },
currencyChecksRepository = currencyChecksRepository,
clickIntents = this,
analyticsEventHandler = analyticsEventHandler,
@ -757,7 +757,7 @@ internal class SendViewModel @Inject constructor(
override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class<out SendNotification>) {
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount.parseBigDecimal(cryptoCurrency.decimals))
uiState = sendNotificationFactory.dismissNotificationState(clazz)
updateNotifications()
feeReload()
}
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
@ -770,10 +770,18 @@ internal class SendViewModel @Inject constructor(
val fee = feeState.fee ?: return
val memo = uiState.recipientState?.memoTextField?.value
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
val feeValue = fee.amount.value ?: return
val receivingAmount = checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
)
viewModelScope.launch(dispatchers.main) {
createTransactionUseCase(
amount = amountValue.convertToAmount(cryptoCurrency),
amount = receivingAmount.convertToAmount(cryptoCurrency),
fee = fee,
memo = memo,
destination = recipient,

View file

@ -33,8 +33,15 @@ object BlockchainUtils {
return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
}
/** If current [networkId] is Dogecoin */
fun isDogecoin(networkId: String): Boolean {
val blockchain = Blockchain.fromId(networkId)
return blockchain == Blockchain.Dogecoin
}
/** If current [networkId] is Tezos */
fun isTezos(networkId: String): Boolean {
val blockchain = Blockchain.fromId(networkId)
return blockchain == Blockchain.Tezos
}
}