From 4cb147b27e66bfc39372dd3d486f654404617cf9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jun 2026 16:06:33 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../send/confirm/model/SendConfirmModel.kt | 9 ++ ...dConfirmationNotificationsTransformerV2.kt | 16 ++- ...firmationNotificationsTransformerV2Test.kt | 110 +++++++++++++++++- .../transfer/SwapTransferInteractorImpl.kt | 28 +++++ .../SwapTransferInteractorImplTest.kt | 55 +++++++++ .../ui/transfer/SwapTransferStateBuilder.kt | 36 +++++- .../transfer/SwapTransferStateBuilderTest.kt | 101 +++++++++++++++- 7 files changed, 345 insertions(+), 10 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/SendConfirmModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/SendConfirmModel.kt index 90b2c60464..eb9c469636 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/SendConfirmModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/SendConfirmModel.kt @@ -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), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt b/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt index 141bcab743..8b1cf5dff5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt @@ -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 { 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 { diff --git a/features/send/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt index 9694974592..c7a9edc59d 100644 --- a/features/send/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -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( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt index 2e2b19ef89..d8d6560555 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt @@ -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, diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt index 61779c4f4f..b01d73b13d 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt @@ -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 diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt index e292f5585f..1897a8d200 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt @@ -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 { diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt index 9b0c42f5dd..930fa03b39 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt @@ -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, ) }