Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-27 15:23:46 +05:00
parent d0d9fcfef8
commit d772beda45
11 changed files with 90 additions and 41 deletions

View file

@ -113,10 +113,19 @@ object NotificationsFactory {
}
}
/**
* Adds Existential Warning
*
* @param existentialDeposit existential deposit of blockchain
* @param feeAmount amount of fee spending for transaction
* @param sendingAmount amount sending by user (excluding fee for coins)
* @param cryptoCurrencyStatus blockchain currency status
* @param onReduceClick action to leave existential amount in balance after transaction
*/
fun MutableList<NotificationUM>.addExistentialWarningNotification(
existentialDeposit: BigDecimal?,
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
sendingAmount: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,
onReduceClick: (
reduceAmountBy: BigDecimal,
@ -129,7 +138,7 @@ object NotificationsFactory {
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
feeAmount
} else {
receivedAmount + feeAmount
sendingAmount
}
val diff = balance.minus(spendingAmount)
if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) {

View file

@ -25,11 +25,12 @@ class Debouncer {
coroutineScope: CoroutineScope,
waitMs: Long = 300L,
context: CoroutineContext = EmptyCoroutineContext,
forceUpdate: Boolean = false,
destinationFunction: suspend () -> Unit,
) {
debounceJob?.cancel()
debounceJob = coroutineScope.launch(context) {
delay(waitMs)
if (!forceUpdate) delay(waitMs)
destinationFunction.invoke()
}
}

View file

@ -234,7 +234,7 @@ internal class SendNotificationFactory(
addExistentialWarningNotification(
existentialDeposit = currencyCheck.existentialDeposit,
feeAmount = feeState.fee?.amount?.value.orZero(),
receivedAmount = sendingAmount,
sendingAmount = sendingAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus,
onReduceClick = clickIntents::onAmountReduceByClick,
)

View file

@ -198,7 +198,7 @@ internal class AddStakingNotificationsTransformer(
addExistentialWarningNotification(
existentialDeposit = currencyCheck.existentialDeposit,
feeAmount = feeState?.fee?.amount?.value.orZero(),
receivedAmount = sendingAmount,
sendingAmount = sendingAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus,
onReduceClick = prevState.clickIntents::onAmountReduceByClick,
)

View file

@ -9,6 +9,7 @@ import com.tangem.feature.swap.domain.models.domain.PermissionOptions
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.ui.*
import java.math.BigDecimal
interface SwapInteractor {
@ -39,6 +40,7 @@ interface SwapInteractor {
toToken: CryptoCurrencyStatus,
providers: List<SwapProvider>,
amountToSwap: String,
reduceBalanceBy: BigDecimal,
selectedFee: FeeType = FeeType.NORMAL,
): Map<SwapProvider, SwapState>
@ -70,6 +72,7 @@ interface SwapInteractor {
selectedFee: FeeType,
fromToken: CryptoCurrencyStatus,
amountToSwap: String,
reduceBalanceBy: BigDecimal,
): SwapState.QuotesLoadedState
/**

View file

@ -4,8 +4,8 @@ import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Blockchain.*
import com.tangem.blockchain.common.TransactionExtras
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
@ -251,6 +251,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
toToken: CryptoCurrencyStatus,
providers: List<SwapProvider>,
amountToSwap: String,
reduceBalanceBy: BigDecimal,
selectedFee: FeeType,
): Map<SwapProvider, SwapState> {
Timber.i(
@ -291,6 +292,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
toToken = toToken,
provider = provider,
amount = amount,
reduceBalanceBy = reduceBalanceBy,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
)
}
@ -369,11 +371,13 @@ internal class SwapInteractorImpl @AssistedInject constructor(
toToken: CryptoCurrencyStatus,
provider: SwapProvider,
amount: SwapAmount,
reduceBalanceBy: BigDecimal,
isBalanceWithoutFeeEnough: Boolean,
): Pair<SwapProvider, SwapState> {
return provider to loadCexQuoteData(
networkId = networkId,
amount = amount,
reduceBalanceBy = reduceBalanceBy,
fromTokenStatus = fromToken,
toTokenStatus = toToken,
isAllowedToSpend = true,
@ -392,12 +396,15 @@ internal class SwapInteractorImpl @AssistedInject constructor(
is TxFeeState.MultipleFeeState -> feeState.priorityFee.feeValue
is TxFeeState.SingleFeeState -> feeState.fee.feeValue
}
val statusValue = fromTokenStatus.value as? CryptoCurrencyStatus.Loaded
val balanceAfterTransaction = statusValue?.let { it.amount - amount.value - fee }
val currencyCheck = getCurrencyCheckUseCase(
userWalletId = userWalletId,
currencyStatus = fromTokenStatus,
amount = amount.value,
fee = fee,
balanceAfterTransaction = balanceAfterTransaction,
)
return currencyCheck
@ -494,6 +501,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
selectedFee: FeeType,
fromToken: CryptoCurrencyStatus,
amountToSwap: String,
reduceBalanceBy: BigDecimal,
): SwapState.QuotesLoadedState {
val amountDecimal = toBigDecimalOrNull(amountToSwap)
if (amountDecimal == null || amountDecimal.signum() == 0) {
@ -504,6 +512,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
networkId = fromToken.currency.network.backendId,
txFee = state.txFee,
amount = amount,
reduceBalanceBy = reduceBalanceBy,
fromToken = fromToken.currency,
)
val fee = when (val txFee = state.txFee) {
@ -1023,6 +1032,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
private suspend fun loadCexQuoteData(
networkId: String,
amount: SwapAmount,
reduceBalanceBy: BigDecimal,
fromTokenStatus: CryptoCurrencyStatus,
toTokenStatus: CryptoCurrencyStatus,
provider: SwapProvider,
@ -1048,6 +1058,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
networkId = networkId,
txFee = txFee,
amount = amount,
reduceBalanceBy = reduceBalanceBy,
fromToken = fromToken,
)
val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) {
@ -1200,6 +1211,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
networkId: String,
txFee: TxFeeState,
amount: SwapAmount,
reduceBalanceBy: BigDecimal,
fromToken: CryptoCurrency,
): IncludeFeeInAmount {
val feeValue = when (txFee) {
@ -1219,13 +1231,20 @@ internal class SwapInteractorImpl @AssistedInject constructor(
IncludeFeeInAmount.BalanceNotEnough
}
}
else -> getIncludeFeeAmountForCoinFee(networkId, amount, feeValue, fromToken)
else -> getIncludeFeeAmountForCoinFee(
networkId = networkId,
amount = amount,
reduceBalanceBy = reduceBalanceBy,
feeValue = feeValue,
fromToken = fromToken,
)
}
}
private suspend fun getIncludeFeeAmountForCoinFee(
networkId: String,
amount: SwapAmount,
reduceBalanceBy: BigDecimal,
feeValue: BigDecimal,
fromToken: CryptoCurrency,
): IncludeFeeInAmount {
@ -1234,26 +1253,27 @@ internal class SwapInteractorImpl @AssistedInject constructor(
networkId,
fromToken.network.derivationPath.value,
) ?: ProxyAmount.empty()
val reducedBalance = tokenForFeeBalance.value - reduceBalanceBy
val amountWithFee = amount.value + feeValue
return when {
fromToken is CryptoCurrency.Token -> {
if (feeValue > tokenForFeeBalance.value || tokenForFeeBalance.value.signum() == 0) {
if (feeValue > reducedBalance || reducedBalance.signum() == 0) {
IncludeFeeInAmount.BalanceNotEnough
} else {
IncludeFeeInAmount.Excluded
}
}
amount.value > tokenForFeeBalance.value -> {
amount.value > reducedBalance -> {
IncludeFeeInAmount.BalanceNotEnough
}
amountWithFee < tokenForFeeBalance.value -> {
amountWithFee < reducedBalance -> {
IncludeFeeInAmount.Excluded
}
else -> {
if (feeValue < amount.value) {
IncludeFeeInAmount.Included(
SwapAmount(
tokenForFeeBalance.value - feeValue,
reducedBalance - feeValue,
getNativeToken(fromToken.network.backendId).decimals,
),
)

View file

@ -4,6 +4,7 @@ import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.ui.TxFee
import java.math.BigDecimal
data class UiActions(
val onSearchEntered: (String) -> Unit,
@ -15,8 +16,8 @@ data class UiActions(
val onChangeCardsClicked: () -> Unit,
val onBackClicked: () -> Unit,
val onMaxAmountSelected: () -> Unit,
val onReduceAmount: (SwapAmount) -> Unit,
val onLeaveExistentialDeposit: (SwapAmount) -> Unit,
val onReduceToAmount: (SwapAmount) -> Unit,
val onReduceByAmount: (SwapAmount, reduceBy: BigDecimal) -> Unit,
val openPermissionBottomSheet: () -> Unit,
val onChangeApproveType: (ApproveType) -> Unit,
// region new actions

View file

@ -134,10 +134,11 @@ internal class SwapNotificationsFactory(
) {
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val includeFeeInAmount = quoteModel.preparedSwapConfigState.includeFeeInAmount
val amount = quoteModel.fromTokenInfo.tokenAmount
val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) {
includeFeeInAmount.amountSubtractFee
} else {
quoteModel.fromTokenInfo.tokenAmount
amount
}
val fee = when (val feeState = quoteModel.txFee) {
TxFeeState.Empty -> null
@ -154,13 +155,12 @@ internal class SwapNotificationsFactory(
addExistentialWarningNotification(
existentialDeposit = quoteModel.currencyCheck?.existentialDeposit,
feeAmount = fee?.feeValue.orZero(),
receivedAmount = amountToRequest.value,
sendingAmount = amount.value,
cryptoCurrencyStatus = fromCurrencyStatus,
onReduceClick = { _, reduceByDiff, _ ->
actions.onLeaveExistentialDeposit(
amountToRequest.copy(
value = amountToRequest.value.minus(reduceByDiff).minus(fee?.feeValue.orZero()),
),
onReduceClick = { reduceBy, reduceByDiff, _ ->
actions.onReduceByAmount(
amount.copy(value = amount.value.minus(reduceByDiff)),
reduceBy,
)
},
)
@ -170,7 +170,7 @@ internal class SwapNotificationsFactory(
cryptoCurrency = fromCurrencyStatus.currency,
minAdaValue = quoteModel.minAdaValue,
onReduceClick = { reduceTo, _ ->
actions.onLeaveExistentialDeposit(amountToRequest.copy(value = reduceTo))
actions.onReduceToAmount(amount.copy(value = reduceTo))
},
)
if (!isCardano) {
@ -191,13 +191,13 @@ internal class SwapNotificationsFactory(
addReduceAmountNotification(
cryptoCurrencyStatus = fromCurrencyStatus,
fromAmount = quoteModel.fromTokenInfo.tokenAmount,
onReduceAmount = actions.onReduceAmount,
onReduceByAmount = actions.onReduceByAmount,
)
addTransactionLimitErrorNotification(
utxoLimit = quoteModel.currencyCheck?.utxoAmountLimit,
cryptoCurrency = fromCurrencyStatus.currency,
onReduceClick = { reduceTo, _ ->
actions.onReduceAmount(amountToRequest.copy(value = reduceTo))
actions.onReduceToAmount(amountToRequest.copy(value = reduceTo))
},
)
}
@ -294,7 +294,7 @@ internal class SwapNotificationsFactory(
private fun MutableList<NotificationUM>.addReduceAmountNotification(
cryptoCurrencyStatus: CryptoCurrencyStatus,
fromAmount: SwapAmount,
onReduceAmount: (SwapAmount) -> Unit,
onReduceByAmount: (SwapAmount, BigDecimal) -> Unit,
) {
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value)
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
@ -309,7 +309,7 @@ internal class SwapNotificationsFactory(
val patchedAmount = fromAmount.copy(
value = fromAmount.value - threshold,
)
onReduceAmount(patchedAmount)
onReduceByAmount(patchedAmount, threshold)
},
),
)

View file

@ -7,6 +7,7 @@ import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
import com.tangem.feature.swap.domain.models.ui.TxFee
import java.math.BigDecimal
data class SwapProcessDataState(
// Initial network id
@ -15,6 +16,7 @@ data class SwapProcessDataState(
val feePaidCryptoCurrency: CryptoCurrencyStatus? = null,
// Amount from input
val amount: String? = null,
val reduceBalanceBy: BigDecimal = BigDecimal.ZERO,
val approveDataModel: RequestApproveStateData? = null,
val swapDataModel: SwapDataModel? = null,
val selectedFee: TxFee? = null,

View file

@ -25,11 +25,7 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
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.tokens.model.Network
@ -144,6 +140,7 @@ internal class SwapViewModel @Inject constructor(
// shows currency order (direct - swap initial to selected, reversed = selected to initial)
private var isOrderReversed = false
private val lastAmount = mutableStateOf(INITIAL_AMOUNT)
private val lastReducedBalanceBy = mutableStateOf(BigDecimal.ZERO)
private var swapRouter: SwapRouter by Delegates.notNull()
private val isUserResolvableError: (SwapState) -> Boolean = {
@ -304,6 +301,7 @@ internal class SwapViewModel @Inject constructor(
fromToken = fromCurrencyStatus,
toToken = toCurrencyStatus,
amount = lastAmount.value,
reduceBalanceBy = lastReducedBalanceBy.value,
toProvidersList = findSwapProviders(fromCurrencyStatus, toCurrencyStatus),
)
}
@ -321,6 +319,7 @@ internal class SwapViewModel @Inject constructor(
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
amount: String,
reduceBalanceBy: BigDecimal,
toProvidersList: List<SwapProvider>,
isSilent: Boolean = false,
) {
@ -339,6 +338,7 @@ internal class SwapViewModel @Inject constructor(
fromToken = fromToken,
toToken = toToken,
amount = amount,
reduceBalanceBy = reduceBalanceBy,
toProvidersList = toProvidersList,
),
)
@ -354,6 +354,7 @@ internal class SwapViewModel @Inject constructor(
toToken = toCurrency,
amount = amount,
isSilent = isSilent,
reduceBalanceBy = dataState.reduceBalanceBy,
toProvidersList = findSwapProviders(fromCurrency, toCurrency),
)
}
@ -363,6 +364,7 @@ internal class SwapViewModel @Inject constructor(
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
amount: String,
reduceBalanceBy: BigDecimal,
toProvidersList: List<SwapProvider>,
): PeriodicTask<Map<SwapProvider, SwapState>> {
return PeriodicTask(
@ -372,6 +374,7 @@ internal class SwapViewModel @Inject constructor(
runCatching(dispatchers.io) {
dataState = dataState.copy(
amount = amount,
reduceBalanceBy = reduceBalanceBy,
swapDataModel = null,
approveDataModel = null,
)
@ -380,6 +383,7 @@ internal class SwapViewModel @Inject constructor(
toToken = toToken,
providers = toProvidersList,
amountToSwap = amount,
reduceBalanceBy = reduceBalanceBy,
selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL,
)
}
@ -817,6 +821,7 @@ internal class SwapViewModel @Inject constructor(
fromToken = fromToken,
toToken = toToken,
amount = lastAmount.value,
reduceBalanceBy = lastReducedBalanceBy.value,
toProvidersList = findSwapProviders(fromToken, toToken),
)
swapRouter.openScreen(SwapNavScreen.Main)
@ -886,6 +891,7 @@ internal class SwapViewModel @Inject constructor(
).getOrNull()
val decimals = newFromToken.currency.decimals
lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value)
lastReducedBalanceBy.value = BigDecimal.ZERO
uiState = stateBuilder.updateSwapAmount(
uiState = uiState,
amountFormatted = inputNumberFormatter.formatWithThousands(lastAmount.value, decimals),
@ -897,13 +903,18 @@ internal class SwapViewModel @Inject constructor(
fromToken = newFromToken,
toToken = newToToken,
amount = lastAmount.value,
reduceBalanceBy = lastReducedBalanceBy.value,
toProvidersList = findSwapProviders(newFromToken, newToToken),
)
}
}
}
private fun onAmountChanged(value: String) {
private fun onAmountChanged(
value: String,
forceQuotesUpdate: Boolean = false,
reduceBalanceBy: BigDecimal = BigDecimal.ZERO,
) {
viewModelScope.launch {
val fromToken = dataState.fromCryptoCurrency
val toToken = dataState.toCryptoCurrency
@ -915,6 +926,7 @@ internal class SwapViewModel @Inject constructor(
fromToken,
).getOrNull()
lastAmount.value = cutValue
lastReducedBalanceBy.value = reduceBalanceBy
uiState = stateBuilder.updateSwapAmount(
uiState = uiState,
amountFormatted = inputNumberFormatter.formatWithThousands(cutValue, decimals),
@ -928,11 +940,12 @@ internal class SwapViewModel @Inject constructor(
isAmountChangedByUser = true
}
amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) {
amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY, forceUpdate = forceQuotesUpdate) {
startLoadingQuotes(
fromToken = fromToken,
toToken = toToken,
amount = lastAmount.value,
reduceBalanceBy = lastReducedBalanceBy.value,
toProvidersList = findSwapProviders(fromToken, toToken),
)
}
@ -948,12 +961,12 @@ internal class SwapViewModel @Inject constructor(
}
}
private fun onReduceAmountClicked(newAmount: SwapAmount) {
onAmountChanged(newAmount.formatToUIRepresentation())
}
private fun onLeaveExistentialDepositClicked(newAmount: SwapAmount) {
onAmountChanged(newAmount.formatToUIRepresentation())
private fun onReduceAmountClicked(newAmount: SwapAmount, reduceBalanceBy: BigDecimal = BigDecimal.ZERO) {
onAmountChanged(
value = newAmount.formatToUIRepresentation(),
forceQuotesUpdate = true,
reduceBalanceBy = reduceBalanceBy,
)
}
private fun onAmountSelected(selected: Boolean) {
@ -1014,8 +1027,8 @@ internal class SwapViewModel @Inject constructor(
onSearchEntered("")
},
onMaxAmountSelected = ::onMaxAmountClicked,
onReduceAmount = ::onReduceAmountClicked,
onLeaveExistentialDeposit = ::onLeaveExistentialDepositClicked,
onReduceToAmount = ::onReduceAmountClicked,
onReduceByAmount = ::onReduceAmountClicked,
openPermissionBottomSheet = {
singleTaskScheduler.cancelTask()
analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked)
@ -1054,6 +1067,7 @@ internal class SwapViewModel @Inject constructor(
selectedFee = it.feeType,
fromToken = fromToken,
amountToSwap = amountToSwap,
reduceBalanceBy = dataState.reduceBalanceBy,
)
setupLoadedState(selectedProvider, updatedState, fromToken)
}

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.compatibility.l2BlockchainsList
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.isSupportedInApp
import com.tangem.blockchainsdk.utils.minimalAmount
import com.tangem.lib.crypto.converter.XrpTaggedAddressConverter
import com.tangem.lib.crypto.models.XrpTaggedAddress