Updated on 2026-08-14
This commit is contained in:
parent
34344f71b0
commit
178ae9c928
6 changed files with 122 additions and 33 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue