Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-01 11:49:36 +01:00
parent 69d803c630
commit d11e99d01e
2 changed files with 229 additions and 1 deletions

View file

@ -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?,

View file

@ -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<BigDecimal?>()
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<BigDecimal?>()
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<BigDecimal?>()
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<BigDecimal?>()
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<BigDecimal?>) {
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,