Updated on 2026-08-14
This commit is contained in:
commit
69d803c630
7 changed files with 345 additions and 10 deletions
|
|
@ -515,6 +515,13 @@ internal class SendConfirmModel @Inject constructor(
|
|||
feeCryptoCurrencyStatus = getCurrencyStatusForFeePayment(),
|
||||
),
|
||||
)
|
||||
val balance = cryptoCurrencyStatus.value.amount.orZero()
|
||||
val feeValue = confirmData.fee?.amount?.value.orZero()
|
||||
val isTotalSendingMoreThanBalance = confirmData.enteredAmount.orZero() + feeValue > balance
|
||||
val isFeeSubtractedFromAmount = isAmountSubtractAvailable && isTotalSendingMoreThanBalance
|
||||
// Fee alone can't be covered by the balance → nothing can be sent, footer must show $0.
|
||||
val isFeeExceedingBalance = isAmountSubtractAvailable && feeValue > balance
|
||||
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
confirmUM = SendConfirmationNotificationsTransformerV2(
|
||||
|
|
@ -524,6 +531,8 @@ internal class SendConfirmModel @Inject constructor(
|
|||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
isFeeSubtractedFromAmount = isFeeSubtractedFromAmount,
|
||||
isFeeExceedingBalance = isFeeExceedingBalance,
|
||||
).transform(uiState.value.confirmUM),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ import com.tangem.features.send.common.ui.state.ConfirmUM
|
|||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendConfirmationNotificationsTransformerV2(
|
||||
private val feeSelectorUM: FeeSelectorUM,
|
||||
private val amountUM: AmountState,
|
||||
|
|
@ -29,6 +31,8 @@ internal class SendConfirmationNotificationsTransformerV2(
|
|||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val analyticsCategoryName: String,
|
||||
private val isFeeSubtractedFromAmount: Boolean,
|
||||
private val isFeeExceedingBalance: Boolean,
|
||||
) : Transformer<ConfirmUM> {
|
||||
override fun transform(prevState: ConfirmUM): ConfirmUM {
|
||||
val state = prevState as? ConfirmUM.Content ?: return prevState
|
||||
|
|
@ -73,10 +77,14 @@ internal class SendConfirmationNotificationsTransformerV2(
|
|||
|
||||
val isFeeConvertibleToFiat = feeSelectorUM.feeExtraInfo.isFeeConvertibleToFiat
|
||||
|
||||
val fiatSendingValue = if (isFeeConvertibleToFiat) {
|
||||
fiatFeeValue?.let { fiatAmountValue?.plus(it) }
|
||||
} else {
|
||||
fiatAmountValue
|
||||
val fiatSendingValue = when {
|
||||
!isFeeConvertibleToFiat -> fiatAmountValue
|
||||
// Fee alone exceeds the balance → the transaction can't go through, nothing is sent.
|
||||
isFeeExceedingBalance -> BigDecimal.ZERO
|
||||
// When the fee is subtracted from the amount, the entered amount is the gross that already
|
||||
// includes the fee, so it must not be added again — that double-counts it.
|
||||
isFeeSubtractedFromAmount -> fiatAmountValue
|
||||
else -> fiatFeeValue?.let { fiatAmountValue?.plus(it) }
|
||||
}
|
||||
|
||||
val fiatSending = fiatSendingValue.format {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.send.send.confirm.model.transformers
|
||||
package com.tangem.features.send.v2.send.confirm.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Amount
|
||||
|
|
@ -8,6 +8,11 @@ import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
|||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
|
@ -18,7 +23,9 @@ import com.tangem.features.send.api.entity.FeeFiatRateUM
|
|||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.utils.formatFooterFiatFee
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.send.confirm.model.transformers.SendConfirmationNotificationsTransformerV2
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
|
|
@ -77,6 +84,8 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
isFeeSubtractedFromAmount = false,
|
||||
isFeeExceedingBalance = false,
|
||||
)
|
||||
val initialState: ConfirmUM = ConfirmUM.Empty
|
||||
|
||||
|
|
@ -99,6 +108,8 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
isFeeSubtractedFromAmount = false,
|
||||
isFeeExceedingBalance = false,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
|
|
@ -121,6 +132,8 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
isFeeSubtractedFromAmount = false,
|
||||
isFeeExceedingBalance = false,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
|
|
@ -134,6 +147,58 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee subtracted from amount WHEN transform THEN footer sending excludes the fee`() = runTest {
|
||||
// GIVEN: fee is taken out of the entered amount, so the footer must show the amount alone (not amount + fee).
|
||||
val feeSelectorUM = createFiatConvertibleFeeSelectorUM(feeValue = BigDecimal("0.001"))
|
||||
val amountUM = createTestAmountUM()
|
||||
val transformer = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
isFeeSubtractedFromAmount = true,
|
||||
isFeeExceedingBalance = false,
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(createTestConfirmUM())
|
||||
|
||||
// THEN: sending = entered fiat amount (50.00), fee NOT added on top.
|
||||
val content = result as ConfirmUM.Content
|
||||
assertThat(content.sendingFooter).isEqualTo(
|
||||
expectedFiatFooter(sendingValue = BigDecimal("50.00"), feeSelectorUM = feeSelectorUM),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee exceeds balance WHEN transform THEN footer sending is zero`() = runTest {
|
||||
// GIVEN: the fee alone exceeds the balance → nothing can be sent.
|
||||
val feeSelectorUM = createFiatConvertibleFeeSelectorUM(feeValue = BigDecimal("0.001"))
|
||||
val amountUM = createTestAmountUM()
|
||||
val transformer = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
isFeeSubtractedFromAmount = true,
|
||||
isFeeExceedingBalance = true,
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(createTestConfirmUM())
|
||||
|
||||
// THEN: sending = $0.
|
||||
val content = result as ConfirmUM.Content
|
||||
assertThat(content.sendingFooter).isEqualTo(
|
||||
expectedFiatFooter(sendingValue = BigDecimal.ZERO, feeSelectorUM = feeSelectorUM),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest {
|
||||
// GIVEN
|
||||
|
|
@ -146,6 +211,8 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
isFeeSubtractedFromAmount = false,
|
||||
isFeeExceedingBalance = false,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
|
|
@ -171,6 +238,8 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
isFeeSubtractedFromAmount = false,
|
||||
isFeeExceedingBalance = false,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
|
|
@ -197,6 +266,8 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
isFeeSubtractedFromAmount = false,
|
||||
isFeeExceedingBalance = false,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
|
|
@ -264,6 +335,43 @@ class SendConfirmationNotificationsTransformerV2Test {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createFiatConvertibleFeeSelectorUM(feeValue: BigDecimal): FeeSelectorUM.Content {
|
||||
val fee = Fee.Common(amount = Amount(currencySymbol = "SOL", value = feeValue, decimals = 8))
|
||||
return FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
fees = TransactionFee.Single(fee),
|
||||
feeItems = persistentListOf(FeeItem.Market(fee)),
|
||||
selectedFeeItem = FeeItem.Market(fee),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(rate = BigDecimal("50000"), appCurrency = appCurrency),
|
||||
feeNonce = FeeNonce.Nonce(nonce = BigInteger.ZERO, onNonceChange = {}),
|
||||
)
|
||||
}
|
||||
|
||||
/** Builds the expected footer reference for a fiat-convertible fee, mirroring the transformer's formatting. */
|
||||
private fun expectedFiatFooter(sendingValue: BigDecimal, feeSelectorUM: FeeSelectorUM.Content): TextReference {
|
||||
val fee = feeSelectorUM.selectedFeeItem.fee
|
||||
val fiatFeeValue = fee.amount.value?.multiply(feeSelectorUM.feeFiatRateUM!!.rate)
|
||||
val sending = sendingValue.format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
}
|
||||
val feeText = formatFooterFiatFee(
|
||||
amount = fee.amount.copy(value = fiatFeeValue),
|
||||
isFeeConvertibleToFiat = true,
|
||||
isFeeApproximate = feeSelectorUM.feeExtraInfo.isFeeApproximate,
|
||||
appCurrency = appCurrency,
|
||||
)
|
||||
return resourceReference(
|
||||
id = R.string.send_summary_transaction_description,
|
||||
formatArgs = wrappedList(sending, feeText),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNormalFeeSelectorUM(): FeeSelectorUM.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import com.tangem.domain.transaction.models.TransactionFeeExtended
|
|||
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
|
|
@ -69,6 +70,7 @@ class SwapTransferInteractorImpl @Inject constructor(
|
|||
private val getTronFeeNotificationShowCountUseCase: GetTronFeeNotificationShowCountUseCase,
|
||||
private val incrementNotificationsShowCountUseCase: IncrementNotificationsShowCountUseCase,
|
||||
private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase,
|
||||
private val validateTransactionUseCase: ValidateTransactionUseCase,
|
||||
) : SwapTransferInteractor {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -139,6 +141,13 @@ class SwapTransferInteractorImpl @Inject constructor(
|
|||
userWalletId = toSwapCurrencyStatus.userWalletId,
|
||||
currency = toToken,
|
||||
).getOrNull() is AssetRequirementsCondition.RequiredTrustline
|
||||
val validationResult = manageTransactionValidationWarnings(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
destinationAddress = toSwapCurrencyStatus.destinationAddress(),
|
||||
amount = fromTokenInfo.tokenAmount,
|
||||
fee = fee,
|
||||
)
|
||||
val minAdaValue = (fee as? Fee.CardanoToken)?.minAdaValue
|
||||
return SwapState.Transfer(
|
||||
userWallet = userWallet,
|
||||
fromTokenInfo = fromTokenInfo,
|
||||
|
|
@ -154,10 +163,29 @@ class SwapTransferInteractorImpl @Inject constructor(
|
|||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
isSendingAmountLoading = coverageState.isSendingAmountLoading,
|
||||
currencyCheck = currencyCheck,
|
||||
validationResult = validationResult,
|
||||
minAdaValue = minAdaValue,
|
||||
hasRequiredTrustline = hasRequiredTrustline,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun manageTransactionValidationWarnings(
|
||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
destinationAddress: String?,
|
||||
amount: SwapAmount,
|
||||
fee: Fee?,
|
||||
): Throwable? {
|
||||
destinationAddress ?: return null
|
||||
return validateTransactionUseCase(
|
||||
amount = amount.value.convertToSdkAmount(fromSwapCurrencyStatus.status),
|
||||
fee = fee,
|
||||
memo = null,
|
||||
destination = destinationAddress,
|
||||
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||
network = fromSwapCurrencyStatus.currency.network,
|
||||
).leftOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getCryptoCurrencyWarning(
|
||||
feeValue: BigDecimal,
|
||||
userWallet: UserWallet,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import com.tangem.domain.transaction.models.TransactionFeeExtended
|
|||
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
|
||||
import com.tangem.feature.swap.domain.fee.TransactionFeeResult
|
||||
|
|
@ -68,6 +69,7 @@ internal class SwapTransferInteractorImplTest {
|
|||
private val getTronFeeNotificationShowCountUseCase: GetTronFeeNotificationShowCountUseCase = mockk(relaxed = true)
|
||||
private val incrementNotificationsShowCountUseCase: IncrementNotificationsShowCountUseCase = mockk(relaxed = true)
|
||||
private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase = mockk()
|
||||
private val validateTransactionUseCase: ValidateTransactionUseCase = mockk()
|
||||
|
||||
private val sut = SwapTransferInteractorImpl(
|
||||
swapFeatureToggles = swapFeatureToggles,
|
||||
|
|
@ -86,11 +88,13 @@ internal class SwapTransferInteractorImplTest {
|
|||
getTronFeeNotificationShowCountUseCase = getTronFeeNotificationShowCountUseCase,
|
||||
incrementNotificationsShowCountUseCase = incrementNotificationsShowCountUseCase,
|
||||
getAssetRequirementsUseCase = getAssetRequirementsUseCase,
|
||||
validateTransactionUseCase = validateTransactionUseCase,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
coEvery { getAssetRequirementsUseCase(any(), any()) } returns null.right()
|
||||
coEvery { validateTransactionUseCase(any(), any(), any(), any(), any(), any()) } returns Unit.right()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
|
@ -500,6 +504,57 @@ internal class SwapTransferInteractorImplTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Cardano token fee WHEN updateTransfer THEN minAdaValue flows through to Transfer state`() = runTest {
|
||||
// Arrange
|
||||
val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$")
|
||||
val userWallet: UserWallet = mockk(relaxed = true)
|
||||
val expectedMinAdaValue = BigDecimal("1.444443")
|
||||
val fromCurrencyStatus = buildCurrencyStatus(
|
||||
rawCurrencyId = FROM_RAW_CURRENCY_ID,
|
||||
decimals = FROM_DECIMALS,
|
||||
fiatRate = BigDecimal.TEN,
|
||||
amount = BigDecimal("1.6"),
|
||||
userWallet = userWallet,
|
||||
)
|
||||
val toCurrencyStatus = buildCurrencyStatus(
|
||||
rawCurrencyId = TO_RAW_CURRENCY_ID,
|
||||
decimals = TO_DECIMALS,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
val fee: Fee.CardanoToken = mockk(relaxed = true) {
|
||||
every { amount.value } returns BigDecimal("0.2")
|
||||
every { minAdaValue } returns expectedMinAdaValue
|
||||
}
|
||||
every { getSelectedAppCurrencyUseCase() } returns flowOf(appCurrency.right())
|
||||
every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(false)
|
||||
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
|
||||
coEvery {
|
||||
getCurrencyCheckUseCase(
|
||||
userWalletId = any(),
|
||||
currencyStatus = any(),
|
||||
feeCurrencyStatus = any(),
|
||||
amount = any(),
|
||||
fee = any(),
|
||||
feeCurrencyBalanceAfterTransaction = any(),
|
||||
recipientAddress = any(),
|
||||
)
|
||||
} returns buildCurrencyCheck()
|
||||
coEvery { isAmountSubtractAvailableUseCase(any(), any(), any()) } returns false.right()
|
||||
|
||||
// Act
|
||||
val result = sut.updateTransfer(
|
||||
fromSwapCurrencyStatus = fromCurrencyStatus,
|
||||
toSwapCurrencyStatus = toCurrencyStatus,
|
||||
fromTokenAmount = "1,5",
|
||||
feePaidCurrencyStatus = null,
|
||||
fee = fee,
|
||||
) as SwapState.Transfer
|
||||
|
||||
// Assert
|
||||
assertThat(result.minAdaValue).isEqualTo(expectedMinAdaValue)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region loadFee
|
||||
|
|
|
|||
|
|
@ -364,6 +364,8 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
fee = fee,
|
||||
tokenSwapInfo = transferState.fromTokenInfo,
|
||||
appCurrency = transferState.appCurrency,
|
||||
isFeeSubtractedFromAmount = isFeeSubtractedFromAmount(transferState, fee),
|
||||
isFeeExceedingBalance = isFeeExceedingBalance(transferState, fee),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -384,11 +386,34 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun isFeeSubtractedFromAmount(transferState: SwapState.Transfer, fee: Fee?): Boolean {
|
||||
if (!transferState.isAmountSubtractAvailable || fee == null) return false
|
||||
val swapCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus
|
||||
val balance = swapCurrencyStatus.status.value.amount.orZero()
|
||||
val amountValue = transferState.fromTokenInfo.tokenAmount.value
|
||||
return amountValue + fee.amount.value.orZero() > balance
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the fee alone exceeds the balance: nothing can be sent (not even enough to cover the fee), so
|
||||
* the footer must show $0 instead of a positive total. Only meaningful when the fee is paid from the same
|
||||
* balance (subtraction available).
|
||||
*/
|
||||
private fun isFeeExceedingBalance(transferState: SwapState.Transfer, fee: Fee?): Boolean {
|
||||
if (!transferState.isAmountSubtractAvailable || fee == null) return false
|
||||
val swapCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus
|
||||
val balance = swapCurrencyStatus.status.value.amount.orZero()
|
||||
return fee.amount.value.orZero() > balance
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun getSendingFooterText(
|
||||
dataState: SwapProcessDataState,
|
||||
fee: Fee?,
|
||||
tokenSwapInfo: TokenSwapInfo,
|
||||
appCurrency: AppCurrency,
|
||||
isFeeSubtractedFromAmount: Boolean,
|
||||
isFeeExceedingBalance: Boolean,
|
||||
): TextReference? {
|
||||
if (fee == null) return null
|
||||
|
||||
|
|
@ -398,10 +423,13 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
val fiatFeeValue = value?.fiatRate?.multiply(fee.amount.value)
|
||||
val isFeeConvertibleToFiat = status.currency.network.hasFiatFeeRate
|
||||
|
||||
val fiatSendingValue = if (isFeeConvertibleToFiat) {
|
||||
fiatFeeValue?.let { fiatAmountValue.plus(it) }
|
||||
} else {
|
||||
fiatAmountValue
|
||||
val fiatSendingValue = when {
|
||||
!isFeeConvertibleToFiat -> fiatAmountValue
|
||||
// Fee alone exceeds the balance → the transaction can't go through, nothing is sent.
|
||||
isFeeExceedingBalance -> BigDecimal.ZERO
|
||||
// Fee is taken out of the entered amount → it already includes the fee, don't add it again.
|
||||
isFeeSubtractedFromAmount -> fiatAmountValue
|
||||
else -> fiatFeeValue?.let { fiatAmountValue.plus(it) }
|
||||
}
|
||||
|
||||
val fiatSending = fiatSendingValue.format {
|
||||
|
|
|
|||
|
|
@ -590,6 +590,104 @@ internal class SwapTransferStateBuilderTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee subtracted from amount WHEN updateTransferButtonEnableState THEN footer sending excludes the fee`() =
|
||||
runTest {
|
||||
// Arrange: subtraction available + amount + fee exceeds balance (1.0), but fee (0.5) <= balance.
|
||||
// The entered amount is the gross that already includes the fee, so the footer must not add it again.
|
||||
val fromAmount = BigDecimal("1.0")
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = fromAmount,
|
||||
toAmount = fromAmount,
|
||||
isAccountsMode = false,
|
||||
isAmountSubtractAvailable = true,
|
||||
)
|
||||
val feePaidStatus = buildSwapCurrencyStatus(coldWallet)
|
||||
val feePaidRate = feePaidStatus.status.value.fiatRate!!
|
||||
val dataState = SwapProcessDataState(
|
||||
fromSwapCurrencyStatus = buildStatusWithNetwork(hasFiatFeeRate = true),
|
||||
feePaidCryptoCurrency = feePaidStatus.status,
|
||||
)
|
||||
val feeValue = BigDecimal("0.5")
|
||||
val fee = Fee.Common(amount = Amount(currencySymbol = "ETH", value = feeValue, decimals = 18))
|
||||
val appCurrency = transferState.appCurrency
|
||||
// Sending is the entered amount only — the fee is NOT added on top.
|
||||
val expectedFiatSending = (fromAmount * QUOTE).format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
}
|
||||
val expectedFiatFee = feePaidRate.multiply(feeValue).format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
}
|
||||
|
||||
// Act
|
||||
val result = sut.updateTransferButtonEnableState(
|
||||
dataState = dataState,
|
||||
transferState = transferState,
|
||||
actions = actions,
|
||||
uiStateHolder = baseStateHolder(),
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
isTangemPayWithdrawal = false,
|
||||
feeSelectorUM = null,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result.transferFooter).isEqualTo(
|
||||
resourceReference(
|
||||
id = com.tangem.features.send.impl.R.string.send_summary_transaction_description,
|
||||
formatArgs = wrappedList(expectedFiatSending, expectedFiatFee),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee exceeds balance WHEN updateTransferButtonEnableState THEN footer sending is zero`() =
|
||||
runTest {
|
||||
// Arrange: subtraction available + fee (2.0) exceeds balance (1.0) → nothing can be sent.
|
||||
val fromAmount = BigDecimal("1.0")
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = fromAmount,
|
||||
toAmount = fromAmount,
|
||||
isAccountsMode = false,
|
||||
isAmountSubtractAvailable = true,
|
||||
)
|
||||
val feePaidStatus = buildSwapCurrencyStatus(coldWallet)
|
||||
val feePaidRate = feePaidStatus.status.value.fiatRate!!
|
||||
val dataState = SwapProcessDataState(
|
||||
fromSwapCurrencyStatus = buildStatusWithNetwork(hasFiatFeeRate = true),
|
||||
feePaidCryptoCurrency = feePaidStatus.status,
|
||||
)
|
||||
val feeValue = BigDecimal("2.0")
|
||||
val fee = Fee.Common(amount = Amount(currencySymbol = "ETH", value = feeValue, decimals = 18))
|
||||
val appCurrency = transferState.appCurrency
|
||||
val expectedFiatSending = BigDecimal.ZERO.format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
}
|
||||
val expectedFiatFee = feePaidRate.multiply(feeValue).format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
}
|
||||
|
||||
// Act
|
||||
val result = sut.updateTransferButtonEnableState(
|
||||
dataState = dataState,
|
||||
transferState = transferState,
|
||||
actions = actions,
|
||||
uiStateHolder = baseStateHolder(),
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
isTangemPayWithdrawal = false,
|
||||
feeSelectorUM = null,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result.transferFooter).isEqualTo(
|
||||
resourceReference(
|
||||
id = com.tangem.features.send.impl.R.string.send_summary_transaction_description,
|
||||
formatArgs = wrappedList(expectedFiatSending, expectedFiatFee),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-Tron fee and non-fiat-convertible network WHEN updateTransferButtonEnableState THEN transferFooter uses no-fiat-fee description`() =
|
||||
runTest {
|
||||
|
|
@ -919,6 +1017,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
isInsufficientBalance: Boolean = false,
|
||||
isFeeCoverage: Boolean = false,
|
||||
isSendingAmountLoading: Boolean = false,
|
||||
isAmountSubtractAvailable: Boolean = false,
|
||||
): SwapState.Transfer {
|
||||
val fromInfo = TokenSwapInfo(
|
||||
tokenAmount = SwapAmount(value = fromAmount, decimals = fromCurrencyStatus.currency.decimals),
|
||||
|
|
@ -942,7 +1041,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
isFeeCoverage = isFeeCoverage,
|
||||
sendingAmount = toAmount,
|
||||
tronFeeNotificationShowCount = 0,
|
||||
isAmountSubtractAvailable = false,
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
isSendingAmountLoading = isSendingAmountLoading,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue