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 d8d6560555..0c1371e0ad 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 @@ -114,7 +114,12 @@ class SwapTransferInteractorImpl @Inject constructor( feeCurrencyStatus = feePaidCurrencyStatus, amount = fromTokenAmountValue, fee = warningsFee, - feeCurrencyBalanceAfterTransaction = null, + feeCurrencyBalanceAfterTransaction = getFeeCurrencyBalanceAfterTx( + fromSwapCurrencyStatus = fromSwapCurrencyStatus, + feePaidCurrencyStatus = feePaidCurrencyStatus, + sendingAmount = fromTokenAmountValue, + feeValue = fee?.amount?.value, + ), recipientAddress = toSwapCurrencyStatus.destinationAddress(), ) val isAmountSubtractAvailable = isAmountSubtractAvailable( @@ -169,6 +174,22 @@ class SwapTransferInteractorImpl @Inject constructor( ) } + private fun getFeeCurrencyBalanceAfterTx( + fromSwapCurrencyStatus: SwapCurrencyStatus, + feePaidCurrencyStatus: CryptoCurrencyStatus?, + sendingAmount: BigDecimal, + feeValue: BigDecimal?, + ): BigDecimal? { + val feeCurrencyBalance = feePaidCurrencyStatus?.value as? CryptoCurrencyStatus.Loaded ?: return null + if (feeValue == null) return null + val isFeeInFromToken = feePaidCurrencyStatus.currency.id == fromSwapCurrencyStatus.currency.id + return if (isFeeInFromToken) { + feeCurrencyBalance.amount - sendingAmount - feeValue + } else { + feeCurrencyBalance.amount - feeValue + } + } + private suspend fun manageTransactionValidationWarnings( fromSwapCurrencyStatus: SwapCurrencyStatus, destinationAddress: String?, 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 b01d73b13d..d5c42f5ef9 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 @@ -555,6 +555,175 @@ internal class SwapTransferInteractorImplTest { assertThat(result.minAdaValue).isEqualTo(expectedMinAdaValue) } + @Test + fun `GIVEN fee paid in the from-currency WHEN updateTransfer THEN feeCurrencyBalanceAfterTx is balance minus amount minus fee`() = + runTest { + // Arrange + val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$") + val userWallet: UserWallet = mockk(relaxed = true) + val feeBalance = BigDecimal("2.0") + val feeValue = BigDecimal("0.1") + val fromCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = FROM_RAW_CURRENCY_ID, + decimals = FROM_DECIMALS, + fiatRate = BigDecimal.TEN, + amount = feeBalance, + userWallet = userWallet, + ) + val toCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = TO_RAW_CURRENCY_ID, + decimals = TO_DECIMALS, + userWallet = userWallet, + ) + // fee is paid in the same currency as the from-token → identical currency id → the sending + // amount is deducted from the fee balance too. + val feePaidCurrencyStatus = buildFeeCurrencyStatus( + currency = fromCurrencyStatus.currency, + amount = feeBalance, + ) + val fee: Fee = mockk(relaxed = true) { every { amount.value } returns feeValue } + stubBaseFlows(appCurrency) + coEvery { isAmountSubtractAvailableUseCase(any(), any(), any()) } returns false.right() + coEvery { getBalanceNotEnoughForFeeWarningUseCase(any(), any(), any(), any()) } returns null.right() + val feeBalances = mutableListOf() + stubGetCurrencyCheckCapturingFeeBalance(feeBalances) + + // Act + sut.updateTransfer( + fromSwapCurrencyStatus = fromCurrencyStatus, + toSwapCurrencyStatus = toCurrencyStatus, + fromTokenAmount = "1.5", + feePaidCurrencyStatus = feePaidCurrencyStatus, + fee = fee, + ) + + // Assert + assertThat(feeBalances.single()).isEqualTo(feeBalance - BigDecimal("1.5") - feeValue) + } + + @Test + fun `GIVEN fee paid in a different currency WHEN updateTransfer THEN feeCurrencyBalanceAfterTx is fee balance minus fee only`() = + runTest { + // Arrange + val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$") + val userWallet: UserWallet = mockk(relaxed = true) + val feeBalance = BigDecimal("2.0") + val feeValue = BigDecimal("0.1") + val fromCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = FROM_RAW_CURRENCY_ID, + decimals = FROM_DECIMALS, + fiatRate = BigDecimal.TEN, + amount = BigDecimal("5.0"), + userWallet = userWallet, + ) + val toCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = TO_RAW_CURRENCY_ID, + decimals = TO_DECIMALS, + userWallet = userWallet, + ) + // fee is paid in a separate currency (distinct id) → the sending amount must NOT be + // deducted from the fee balance. + val feePaidCurrencyStatus = buildFeeCurrencyStatus( + currency = buildDistinctCoin(), + amount = feeBalance, + ) + val fee: Fee = mockk(relaxed = true) { every { amount.value } returns feeValue } + stubBaseFlows(appCurrency) + coEvery { isAmountSubtractAvailableUseCase(any(), any(), any()) } returns false.right() + coEvery { getBalanceNotEnoughForFeeWarningUseCase(any(), any(), any(), any()) } returns null.right() + val feeBalances = mutableListOf() + stubGetCurrencyCheckCapturingFeeBalance(feeBalances) + + // Act + sut.updateTransfer( + fromSwapCurrencyStatus = fromCurrencyStatus, + toSwapCurrencyStatus = toCurrencyStatus, + fromTokenAmount = "1.5", + feePaidCurrencyStatus = feePaidCurrencyStatus, + fee = fee, + ) + + // Assert + assertThat(feeBalances.single()).isEqualTo(feeBalance - feeValue) + } + + @Test + fun `GIVEN no fee currency status WHEN updateTransfer THEN feeCurrencyBalanceAfterTx is null`() = runTest { + // Arrange + val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$") + val userWallet: UserWallet = mockk(relaxed = true) + val fromCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = FROM_RAW_CURRENCY_ID, + decimals = FROM_DECIMALS, + fiatRate = BigDecimal.TEN, + amount = BigDecimal("2.0"), + userWallet = userWallet, + ) + val toCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = TO_RAW_CURRENCY_ID, + decimals = TO_DECIMALS, + userWallet = userWallet, + ) + val fee: Fee = mockk(relaxed = true) { every { amount.value } returns BigDecimal("0.1") } + stubBaseFlows(appCurrency) + coEvery { isAmountSubtractAvailableUseCase(any(), any(), any()) } returns false.right() + val feeBalances = mutableListOf() + stubGetCurrencyCheckCapturingFeeBalance(feeBalances) + + // Act + sut.updateTransfer( + fromSwapCurrencyStatus = fromCurrencyStatus, + toSwapCurrencyStatus = toCurrencyStatus, + fromTokenAmount = "1.5", + feePaidCurrencyStatus = null, + fee = fee, + ) + + // Assert + assertThat(feeBalances.single()).isNull() + } + + @Test + fun `GIVEN fee currency present but fee not loaded WHEN updateTransfer THEN feeCurrencyBalanceAfterTx is null`() = + runTest { + // Arrange + val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$") + val userWallet: UserWallet = mockk(relaxed = true) + val fromCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = FROM_RAW_CURRENCY_ID, + decimals = FROM_DECIMALS, + fiatRate = BigDecimal.TEN, + amount = BigDecimal("2.0"), + userWallet = userWallet, + ) + val toCurrencyStatus = buildCurrencyStatus( + rawCurrencyId = TO_RAW_CURRENCY_ID, + decimals = TO_DECIMALS, + userWallet = userWallet, + ) + val feePaidCurrencyStatus = buildFeeCurrencyStatus( + currency = buildDistinctCoin(), + amount = BigDecimal("2.0"), + ) + stubBaseFlows(appCurrency) + coEvery { isAmountSubtractAvailableUseCase(any(), any(), any()) } returns false.right() + coEvery { getBalanceNotEnoughForFeeWarningUseCase(any(), any(), any(), any()) } returns null.right() + val feeBalances = mutableListOf() + stubGetCurrencyCheckCapturingFeeBalance(feeBalances) + + // Act: fee not loaded yet → feeValue is null → balance-after-tx cannot be computed + sut.updateTransfer( + fromSwapCurrencyStatus = fromCurrencyStatus, + toSwapCurrencyStatus = toCurrencyStatus, + fromTokenAmount = "1.5", + feePaidCurrencyStatus = feePaidCurrencyStatus, + fee = null, + ) + + // Assert + assertThat(feeBalances.single()).isNull() + } + // endregion // region loadFee @@ -1123,6 +1292,44 @@ internal class SwapTransferInteractorImplTest { } } + private fun stubBaseFlows(appCurrency: AppCurrency) { + every { getSelectedAppCurrencyUseCase() } returns flowOf(appCurrency.right()) + every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(false) + coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false + } + + private fun stubGetCurrencyCheckCapturingFeeBalance(feeBalances: MutableList) { + coEvery { + getCurrencyCheckUseCase( + userWalletId = any(), + currencyStatus = any(), + feeCurrencyStatus = any(), + amount = any(), + fee = any(), + feeCurrencyBalanceAfterTransaction = captureNullable(feeBalances), + recipientAddress = any(), + ) + } returns buildCurrencyCheck() + } + + private fun buildFeeCurrencyStatus(currency: CryptoCurrency, amount: BigDecimal): CryptoCurrencyStatus { + val loadedValue: CryptoCurrencyStatus.Loaded = mockk { + every { this@mockk.amount } returns amount + } + return mockk { + every { this@mockk.value } returns loadedValue + every { this@mockk.currency } returns currency + } + } + + private fun buildDistinctCoin(): CryptoCurrency.Coin { + val currencyId: CryptoCurrency.ID = mockk() + return mockk { + every { this@mockk.id } returns currencyId + every { this@mockk.network } returns mockk() + } + } + private fun buildCurrencyCheck( existentialDeposit: BigDecimal? = null, dustValue: BigDecimal? = null,