From 675dc70a1c8a191727e856d07af214b5d00d458f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jun 2026 13:01:52 +0300 Subject: [PATCH 1/3] Updated on 2026-08-14 --- .../swap/model/SwapNotificationsFactory.kt | 5 +- .../SwapTransferNotificationsFactory.kt | 5 +- .../model/SwapNotificationsFactoryTest.kt | 192 ++++++++++++++++++ .../SwapTransferNotificationsFactoryTest.kt | 27 +++ 4 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapNotificationsFactoryTest.kt diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 7f9c144c0f..5016539228 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -138,7 +138,10 @@ internal class SwapNotificationsFactory( maybeAddPriceImpactNotification(quoteModel.priceImpact) maybeAddHighNetworkFeeWarning(isHighNetworkFee) } - return warnings.toPersistentList() + // Two independent dust checks (SDK TransactionDustChangeError in addValidateTransactionNotifications + + // manual change-below-dust in addDustWarningNotification) can both add an identical MinimumAmountError. + // distinct() collapses the duplicate "Invalid amount" banner. + return warnings.distinct().toPersistentList() } private fun MutableList.maybeAddHighNetworkFeeWarning(isHighNetworkFee: Boolean) { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt index e863c9ef7b..d1b892da2f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt @@ -69,7 +69,10 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { feeError = getFeeError, actions = actions, ) - }.toPersistentList() + } + // Two independent dust checks can both add an identical MinimumAmountError; collapse the duplicate banner. + .distinct() + .toPersistentList() } private fun MutableList.maybeAddRentExemptionError(state: SwapState.Transfer) { diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapNotificationsFactoryTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapNotificationsFactoryTest.kt new file mode 100644 index 0000000000..14b5aea302 --- /dev/null +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapNotificationsFactoryTest.kt @@ -0,0 +1,192 @@ +package com.tangem.feature.swap.model + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.swap.models.SwapCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork +import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType +import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.feature.swap.domain.models.ui.PriceImpact +import com.tangem.feature.swap.domain.models.ui.SwapState +import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo +import com.tangem.feature.swap.models.UiActions +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.math.BigDecimal + +/** + * Regression coverage for the duplicate "Invalid amount" (MinimumAmountError) banner in the regular + * (quotes-loaded) swap flow. For a BTC dust-change amount, two independent dust checks in + * [SwapNotificationsFactory.getConfirmationStateNotifications] → + * [SwapNotificationsFactory.maybeAddDomainWarnings] both add an identical MinimumAmountError: + * the SDK validation ([BlockchainSdkError.TransactionDustChangeError]) and the manual `checkDustLimits` + * change-below-dust branch. The factory must collapse the duplicate so only one banner is shown. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class SwapNotificationsFactoryTest { + + private val actions: UiActions = mockk(relaxed = true) + private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk(relaxed = true) + private val appRouter: AppRouter = mockk(relaxed = true) + + private val sut = SwapNotificationsFactory( + actions = actions, + isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork, + ) + + private val userWalletId = UserWalletId(stringValue = "deadbeef") + private val coldWallet: UserWallet.Cold = mockk(relaxed = true) { + every { walletId } returns userWalletId + } + + @Test + fun `GIVEN dust-change validation error and change below dust WHEN getConfirmationStateNotifications THEN single MinimumAmountError`() = + runTest { + // Arrange — balance 1.0, sending 0.99, dust 0.02 → leftover change 0.01 (< dust) triggers the + // manual checkDustLimits path, while validationResult = TransactionDustChangeError triggers the SDK + // path. Both add an identical MinimumAmountError; the fix must collapse them into one. + val quoteModel = buildQuotesLoadedState( + balance = BigDecimal("1.0"), + amount = BigDecimal("0.99"), + dustValue = BigDecimal("0.02"), + validationResult = BlockchainSdkError.TransactionDustChangeError, + ) + + // Act + val result = sut.getConfirmationStateNotifications( + quoteModel = quoteModel, + feeCryptoCurrencyStatus = null, + swapFee = null, + feeError = null, + appRouter = appRouter, + ) + + // Assert + assertThat(result.filterIsInstance()).hasSize(1) + } + + @Test + fun `GIVEN manual dust limit only and no validation error WHEN getConfirmationStateNotifications THEN single MinimumAmountError`() = + runTest { + // Arrange — same change-below-dust condition but no SDK validation error: only the manual path fires. + val quoteModel = buildQuotesLoadedState( + balance = BigDecimal("1.0"), + amount = BigDecimal("0.99"), + dustValue = BigDecimal("0.02"), + validationResult = null, + ) + + // Act + val result = sut.getConfirmationStateNotifications( + quoteModel = quoteModel, + feeCryptoCurrencyStatus = null, + swapFee = null, + feeError = null, + appRouter = appRouter, + ) + + // Assert + assertThat(result.filterIsInstance()).hasSize(1) + } + + @Test + fun `GIVEN no dust value and no validation error WHEN getConfirmationStateNotifications THEN no MinimumAmountError`() = + runTest { + // Arrange — comfortable amount, no dust value, no validation error. + val quoteModel = buildQuotesLoadedState( + balance = BigDecimal("1.0"), + amount = BigDecimal("0.5"), + dustValue = null, + validationResult = null, + ) + + // Act + val result = sut.getConfirmationStateNotifications( + quoteModel = quoteModel, + feeCryptoCurrencyStatus = null, + swapFee = null, + feeError = null, + appRouter = appRouter, + ) + + // Assert + assertThat(result.filterIsInstance()).isEmpty() + } + + private fun buildQuotesLoadedState( + balance: BigDecimal, + amount: BigDecimal, + dustValue: BigDecimal?, + validationResult: Throwable?, + ): SwapState.QuotesLoadedState { + val fromStatus = buildCoinStatus(balance = balance) + val toStatus = buildCoinStatus(balance = BigDecimal("1.0")) + return SwapState.QuotesLoadedState( + fromTokenInfo = buildTokenInfo(swapCurrencyStatus = fromStatus, amount = amount), + toTokenInfo = buildTokenInfo(swapCurrencyStatus = toStatus, amount = BigDecimal("1.0")), + swapProvider = buildProvider(), + priceImpact = PriceImpact.Empty, + currencyCheck = buildCurrencyCheck(dustValue = dustValue), + validationResult = validationResult, + minAdaValue = null, + ) + } + + private fun buildTokenInfo(swapCurrencyStatus: SwapCurrencyStatus, amount: BigDecimal): TokenSwapInfo = + TokenSwapInfo( + tokenAmount = SwapAmount(value = amount, decimals = swapCurrencyStatus.currency.decimals), + amountFiat = amount * BigDecimal("2000"), + swapCurrencyStatus = swapCurrencyStatus, + ) + + private fun buildCurrencyCheck(dustValue: BigDecimal?): CryptoCurrencyCheck = CryptoCurrencyCheck( + dustValue = dustValue, + reserveAmount = null, + minimumSendAmount = null, + existentialDeposit = null, + utxoAmountLimit = null, + isAccountFunded = true, + rentWarning = null, + ) + + private fun buildCoinStatus(balance: BigDecimal): SwapCurrencyStatus { + val coin = buildCoin() + val statusValue: CryptoCurrencyStatus.Loaded = mockk(relaxed = true) { + every { amount } returns balance + } + return SwapCurrencyStatus( + userWallet = coldWallet, + status = CryptoCurrencyStatus(currency = coin, value = statusValue), + account = Account.CryptoPortfolio.createMainAccount(userWalletId), + ) + } + + private fun buildCoin(): CryptoCurrency.Coin = mockk(relaxed = true) { + every { id } returns mockk(relaxed = true) + every { network } returns mockk(relaxed = true) { + every { rawId } returns "bitcoin" + every { name } returns "Bitcoin" + every { currencySymbol } returns "BTC" + } + every { name } returns "Bitcoin" + every { symbol } returns "BTC" + every { decimals } returns 8 + } + + private fun buildProvider(type: ExchangeProviderType = ExchangeProviderType.CEX): SwapProvider = + mockk(relaxed = true) { + every { this@mockk.type } returns type + } +} \ No newline at end of file diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt index 0c7767d828..7b450f9570 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.ui.transfer import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.notifications.NotificationUM @@ -121,6 +122,32 @@ internal class SwapTransferNotificationsFactoryTest { assertThat(result.filterIsInstance()).hasSize(1) } + @Test + fun `GIVEN SDK dust-change error and manual dust limit WHEN getNotifications THEN single MinimumAmountError`() = + runTest { + // Both the SDK validation (TransactionDustChangeError) and the manual checkDustLimits change-below-dust + // path add an identical MinimumAmountError; the dedup must collapse them into one banner. + val fromStatus = buildCoinStatus(balance = BigDecimal("1.0")) + val transferState = buildTransferState( + fromTokenInfo = buildTokenInfo( + swapCurrencyStatus = fromStatus, + amount = BigDecimal("0.99"), + ), + currencyCheck = buildCurrencyCheck(dustValue = BigDecimal("0.02")), + validationResult = BlockchainSdkError.TransactionDustChangeError, + sendingAmount = BigDecimal("0.99"), + ) + + val result = sut.getNotifications( + transferState = transferState, + feeSelectorUM = null, + feeCryptoCurrencyStatus = null, + actions = actions, + ) + + assertThat(result.filterIsInstance()).hasSize(1) + } + @Test fun `GIVEN minAdaValue and no validationResult WHEN getNotifications THEN MinAdaValueCharged is added`() = runTest { From e27052c85b3f791ec5e7fd3dd8c11ad86649dc81 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jun 2026 15:27:59 +0300 Subject: [PATCH 2/3] Updated on 2026-08-14 --- .../swap/model/SwapNotificationsFactory.kt | 5 +- .../SwapTransferNotificationsFactory.kt | 5 +- .../model/SwapNotificationsFactoryTest.kt | 192 ------------------ .../SwapTransferNotificationsFactoryTest.kt | 27 --- 4 files changed, 2 insertions(+), 227 deletions(-) delete mode 100644 features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapNotificationsFactoryTest.kt diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 5016539228..7f9c144c0f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -138,10 +138,7 @@ internal class SwapNotificationsFactory( maybeAddPriceImpactNotification(quoteModel.priceImpact) maybeAddHighNetworkFeeWarning(isHighNetworkFee) } - // Two independent dust checks (SDK TransactionDustChangeError in addValidateTransactionNotifications + - // manual change-below-dust in addDustWarningNotification) can both add an identical MinimumAmountError. - // distinct() collapses the duplicate "Invalid amount" banner. - return warnings.distinct().toPersistentList() + return warnings.toPersistentList() } private fun MutableList.maybeAddHighNetworkFeeWarning(isHighNetworkFee: Boolean) { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt index d1b892da2f..e863c9ef7b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt @@ -69,10 +69,7 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { feeError = getFeeError, actions = actions, ) - } - // Two independent dust checks can both add an identical MinimumAmountError; collapse the duplicate banner. - .distinct() - .toPersistentList() + }.toPersistentList() } private fun MutableList.maybeAddRentExemptionError(state: SwapState.Transfer) { diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapNotificationsFactoryTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapNotificationsFactoryTest.kt deleted file mode 100644 index 14b5aea302..0000000000 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapNotificationsFactoryTest.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.tangem.feature.swap.model - -import com.google.common.truth.Truth.assertThat -import com.tangem.blockchain.common.BlockchainSdkError -import com.tangem.common.routing.AppRouter -import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.swap.models.SwapCurrencyStatus -import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck -import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork -import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType -import com.tangem.feature.swap.domain.models.domain.SwapProvider -import com.tangem.feature.swap.domain.models.ui.PriceImpact -import com.tangem.feature.swap.domain.models.ui.SwapState -import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo -import com.tangem.feature.swap.models.UiActions -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import java.math.BigDecimal - -/** - * Regression coverage for the duplicate "Invalid amount" (MinimumAmountError) banner in the regular - * (quotes-loaded) swap flow. For a BTC dust-change amount, two independent dust checks in - * [SwapNotificationsFactory.getConfirmationStateNotifications] → - * [SwapNotificationsFactory.maybeAddDomainWarnings] both add an identical MinimumAmountError: - * the SDK validation ([BlockchainSdkError.TransactionDustChangeError]) and the manual `checkDustLimits` - * change-below-dust branch. The factory must collapse the duplicate so only one banner is shown. - */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class SwapNotificationsFactoryTest { - - private val actions: UiActions = mockk(relaxed = true) - private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk(relaxed = true) - private val appRouter: AppRouter = mockk(relaxed = true) - - private val sut = SwapNotificationsFactory( - actions = actions, - isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork, - ) - - private val userWalletId = UserWalletId(stringValue = "deadbeef") - private val coldWallet: UserWallet.Cold = mockk(relaxed = true) { - every { walletId } returns userWalletId - } - - @Test - fun `GIVEN dust-change validation error and change below dust WHEN getConfirmationStateNotifications THEN single MinimumAmountError`() = - runTest { - // Arrange — balance 1.0, sending 0.99, dust 0.02 → leftover change 0.01 (< dust) triggers the - // manual checkDustLimits path, while validationResult = TransactionDustChangeError triggers the SDK - // path. Both add an identical MinimumAmountError; the fix must collapse them into one. - val quoteModel = buildQuotesLoadedState( - balance = BigDecimal("1.0"), - amount = BigDecimal("0.99"), - dustValue = BigDecimal("0.02"), - validationResult = BlockchainSdkError.TransactionDustChangeError, - ) - - // Act - val result = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = null, - swapFee = null, - feeError = null, - appRouter = appRouter, - ) - - // Assert - assertThat(result.filterIsInstance()).hasSize(1) - } - - @Test - fun `GIVEN manual dust limit only and no validation error WHEN getConfirmationStateNotifications THEN single MinimumAmountError`() = - runTest { - // Arrange — same change-below-dust condition but no SDK validation error: only the manual path fires. - val quoteModel = buildQuotesLoadedState( - balance = BigDecimal("1.0"), - amount = BigDecimal("0.99"), - dustValue = BigDecimal("0.02"), - validationResult = null, - ) - - // Act - val result = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = null, - swapFee = null, - feeError = null, - appRouter = appRouter, - ) - - // Assert - assertThat(result.filterIsInstance()).hasSize(1) - } - - @Test - fun `GIVEN no dust value and no validation error WHEN getConfirmationStateNotifications THEN no MinimumAmountError`() = - runTest { - // Arrange — comfortable amount, no dust value, no validation error. - val quoteModel = buildQuotesLoadedState( - balance = BigDecimal("1.0"), - amount = BigDecimal("0.5"), - dustValue = null, - validationResult = null, - ) - - // Act - val result = sut.getConfirmationStateNotifications( - quoteModel = quoteModel, - feeCryptoCurrencyStatus = null, - swapFee = null, - feeError = null, - appRouter = appRouter, - ) - - // Assert - assertThat(result.filterIsInstance()).isEmpty() - } - - private fun buildQuotesLoadedState( - balance: BigDecimal, - amount: BigDecimal, - dustValue: BigDecimal?, - validationResult: Throwable?, - ): SwapState.QuotesLoadedState { - val fromStatus = buildCoinStatus(balance = balance) - val toStatus = buildCoinStatus(balance = BigDecimal("1.0")) - return SwapState.QuotesLoadedState( - fromTokenInfo = buildTokenInfo(swapCurrencyStatus = fromStatus, amount = amount), - toTokenInfo = buildTokenInfo(swapCurrencyStatus = toStatus, amount = BigDecimal("1.0")), - swapProvider = buildProvider(), - priceImpact = PriceImpact.Empty, - currencyCheck = buildCurrencyCheck(dustValue = dustValue), - validationResult = validationResult, - minAdaValue = null, - ) - } - - private fun buildTokenInfo(swapCurrencyStatus: SwapCurrencyStatus, amount: BigDecimal): TokenSwapInfo = - TokenSwapInfo( - tokenAmount = SwapAmount(value = amount, decimals = swapCurrencyStatus.currency.decimals), - amountFiat = amount * BigDecimal("2000"), - swapCurrencyStatus = swapCurrencyStatus, - ) - - private fun buildCurrencyCheck(dustValue: BigDecimal?): CryptoCurrencyCheck = CryptoCurrencyCheck( - dustValue = dustValue, - reserveAmount = null, - minimumSendAmount = null, - existentialDeposit = null, - utxoAmountLimit = null, - isAccountFunded = true, - rentWarning = null, - ) - - private fun buildCoinStatus(balance: BigDecimal): SwapCurrencyStatus { - val coin = buildCoin() - val statusValue: CryptoCurrencyStatus.Loaded = mockk(relaxed = true) { - every { amount } returns balance - } - return SwapCurrencyStatus( - userWallet = coldWallet, - status = CryptoCurrencyStatus(currency = coin, value = statusValue), - account = Account.CryptoPortfolio.createMainAccount(userWalletId), - ) - } - - private fun buildCoin(): CryptoCurrency.Coin = mockk(relaxed = true) { - every { id } returns mockk(relaxed = true) - every { network } returns mockk(relaxed = true) { - every { rawId } returns "bitcoin" - every { name } returns "Bitcoin" - every { currencySymbol } returns "BTC" - } - every { name } returns "Bitcoin" - every { symbol } returns "BTC" - every { decimals } returns 8 - } - - private fun buildProvider(type: ExchangeProviderType = ExchangeProviderType.CEX): SwapProvider = - mockk(relaxed = true) { - every { this@mockk.type } returns type - } -} \ No newline at end of file diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt index 7b450f9570..0c7767d828 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.ui.transfer import com.google.common.truth.Truth.assertThat -import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.notifications.NotificationUM @@ -122,32 +121,6 @@ internal class SwapTransferNotificationsFactoryTest { assertThat(result.filterIsInstance()).hasSize(1) } - @Test - fun `GIVEN SDK dust-change error and manual dust limit WHEN getNotifications THEN single MinimumAmountError`() = - runTest { - // Both the SDK validation (TransactionDustChangeError) and the manual checkDustLimits change-below-dust - // path add an identical MinimumAmountError; the dedup must collapse them into one banner. - val fromStatus = buildCoinStatus(balance = BigDecimal("1.0")) - val transferState = buildTransferState( - fromTokenInfo = buildTokenInfo( - swapCurrencyStatus = fromStatus, - amount = BigDecimal("0.99"), - ), - currencyCheck = buildCurrencyCheck(dustValue = BigDecimal("0.02")), - validationResult = BlockchainSdkError.TransactionDustChangeError, - sendingAmount = BigDecimal("0.99"), - ) - - val result = sut.getNotifications( - transferState = transferState, - feeSelectorUM = null, - feeCryptoCurrencyStatus = null, - actions = actions, - ) - - assertThat(result.filterIsInstance()).hasSize(1) - } - @Test fun `GIVEN minAdaValue and no validationResult WHEN getNotifications THEN MinAdaValueCharged is added`() = runTest { From 8fc73abaf94e55dcfcf13545b8b8c136d48c2a75 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jun 2026 16:21:53 +0300 Subject: [PATCH 3/3] Updated on 2026-08-14 --- .../com/tangem/feature/swap/model/SwapNotificationsFactory.kt | 3 ++- .../swap/ui/transfer/SwapTransferNotificationsFactory.kt | 3 ++- .../swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 7f9c144c0f..99ecd52206 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -36,6 +36,7 @@ import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.getTezosThreshold import com.tangem.lib.crypto.BlockchainUtils.isTezos import com.tangem.utils.Provider +import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -215,7 +216,7 @@ internal class SwapNotificationsFactory( actions.onReduceToAmount(amount.copy(value = reduceTo)) }, ) - if (!isCardano) { + if (!isCardano && !feeValue.isZero()) { addDustWarningNotification( dustValue = quoteModel.currencyCheck?.dustValue, feeValue = feeValue, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt index e863c9ef7b..778e8eff18 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt @@ -24,6 +24,7 @@ import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.getTezosThreshold import com.tangem.lib.crypto.BlockchainUtils.isTezos import com.tangem.lib.crypto.BlockchainUtils.isTron +import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -112,7 +113,7 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { onReduceToAmount(amount.copy(value = reduceTo)) }, ) - if (!isCardano) { + if (!isCardano && !feeValue.isZero()) { addDustWarningNotification( dustValue = state.currencyCheck?.dustValue, feeValue = feeValue, diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt index 0c7767d828..61eb7d7ac8 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt @@ -113,7 +113,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, - feeSelectorUM = null, + // A loaded (non-zero) fee is required: the manual dust check is now skipped when feeValue is zero + // (fee errored / not yet loaded), so it cannot duplicate the fee-error path's MinimumAmountError. + feeSelectorUM = contentWithFee(feeValue = BigDecimal("0.001")), feeCryptoCurrencyStatus = null, actions = actions, )