Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-09 17:52:46 +05:00
parent fa478206b5
commit a0cc4794f8
2 changed files with 79 additions and 5 deletions

View file

@ -1581,6 +1581,7 @@ internal class SwapInteractorImpl @Inject constructor(
feeValue = nativeFee,
selectedFeeToken = fee.selectedFeeToken,
provider = state.swapProvider,
txType = state.txType,
)
val currencyCheck = manageWarnings(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
@ -1621,6 +1622,7 @@ internal class SwapInteractorImpl @Inject constructor(
* from-currencies but "fee > native balance" for Token from-currencies is resolved here
* by consulting `isBalanceEnough` (amount-alone check) directly.
*/
@Suppress("LongParameterList")
private suspend fun computeBalanceStatus(
fromSwapCurrencyStatus: SwapCurrencyStatus,
amount: SwapAmount,
@ -1628,9 +1630,10 @@ internal class SwapInteractorImpl @Inject constructor(
feeValue: BigDecimal,
selectedFeeToken: CryptoCurrencyStatus?,
provider: SwapProvider,
txType: ExpressTxType?,
): SwapBalanceStatus {
when (provider.type) {
ExchangeProviderType.CEX -> {
when (resolveQuoteFlow(provider, txType)) {
ResolvedFlow.CexLike -> {
val includeStatus = getIncludeFeeInAmountInternal(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
amount = amount,
@ -1642,9 +1645,7 @@ internal class SwapInteractorImpl @Inject constructor(
return SwapBalanceStatus.FeeAdjustedAmount(adjustedAmount = includeStatus.amountSubtractFee)
}
}
ExchangeProviderType.DEX,
ExchangeProviderType.DEX_BRIDGE,
-> Unit
ResolvedFlow.DexLike -> Unit
}
val isAmountAlone = isBalanceEnough(fromSwapCurrencyStatus, amount, fee = feeValue)

View file

@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.feature.swap.domain.fee.TransactionFeeResult
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.ExpressTxType
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
import com.tangem.feature.swap.domain.models.domain.SwapBalanceStatus
import com.tangem.feature.swap.domain.models.ui.*
@ -214,6 +215,76 @@ internal class SwapInteractorImplApplySwapFeeMatrixTest : SwapInteractorImplTest
}
}
// =========================================================================
// Section A2: DEX provider re-routed to the CEX-like flow via txType=SEND ([REDACTED_TASK_KEY])
// =========================================================================
@Nested
inner class `DEX provider with SEND txType follows CEX semantics` {
/**
* [REDACTED_TASK_KEY]: a DEX-typed provider (e.g. Moonpay trade) whose quote returned txType=SEND
* executes as a plain transfer built by the app, so the fee must be folded into the amount
* exactly like for a CEX provider.
*
* GIVEN ExchangeProviderType.DEX, txType = SEND
* fromToken is Coin, amount = full native balance (max amount), fee = 0.01
* WHEN applySwapFee runs
* THEN balanceStatus == FeeAdjustedAmount with adjustedAmount = balance - fee
* (NOT InsufficientAmount the pre-fix behavior that showed "Insufficient funds")
*/
@Test
fun `applySwapFee DEX with SEND txType — max amount returns FeeAdjustedAmount like CEX`() = runTest {
coEvery { currenciesRepository.getFeePaidCurrency(any(), any()) } returns FeePaidCurrency.Coin
coEvery {
walletManagersFacade.getNativeTokenBalance(any(), any(), any())
} returns BigDecimal("1.0")
val state = buildQuotesLoadedState(
providerType = ExchangeProviderType.DEX,
fromAmount = SwapAmount(BigDecimal("1.0"), 18),
isCoin = true,
fromBalance = BigDecimal("1.0"),
txType = ExpressTxType.SEND,
)
val fee = buildSwapFeeWithCoinToken(feeValue = BigDecimal("0.01"))
val result = sut.applySwapFee(state, fee, lastReducedBalanceBy)
val balanceStatus = result.preparedSwapConfigState.balanceStatus
assertThat(balanceStatus).isInstanceOf(SwapBalanceStatus.FeeAdjustedAmount::class.java)
assertThat((balanceStatus as SwapBalanceStatus.FeeAdjustedAmount).adjustedAmount.value)
.isEqualTo(BigDecimal("0.99"))
}
/**
* Twin guard: the same max-amount scenario with txType = SWAP keeps the DEX invariant
* the fee is never deducted from the amount, and the amount alone exceeding
* balance-with-fee yields InsufficientAmount.
*/
@Test
fun `applySwapFee DEX with SWAP txType — max amount keeps DEX semantics without fee deduction`() = runTest {
coEvery { currenciesRepository.getFeePaidCurrency(any(), any()) } returns FeePaidCurrency.Coin
coEvery {
walletManagersFacade.getNativeTokenBalance(any(), any(), any())
} returns BigDecimal("1.0")
val state = buildQuotesLoadedState(
providerType = ExchangeProviderType.DEX,
fromAmount = SwapAmount(BigDecimal("1.0"), 18),
isCoin = true,
fromBalance = BigDecimal("1.0"),
txType = ExpressTxType.SWAP,
)
val fee = buildSwapFeeWithCoinToken(feeValue = BigDecimal("0.01"))
val result = sut.applySwapFee(state, fee, lastReducedBalanceBy)
assertThat(result.preparedSwapConfigState.balanceStatus)
.isInstanceOf(SwapBalanceStatus.InsufficientAmount::class.java)
}
}
// =========================================================================
// Section B: FeePaidCurrency.Token (gasless-token) paths
// =========================================================================
@ -749,6 +820,7 @@ internal class SwapInteractorImplApplySwapFeeMatrixTest : SwapInteractorImplTest
fromAmount: SwapAmount,
isCoin: Boolean,
fromBalance: BigDecimal,
txType: ExpressTxType? = null,
): SwapState.QuotesLoadedState {
val from = buildSwapCurrencyStatus(
networkRawId = ethNetwork,
@ -778,6 +850,7 @@ internal class SwapInteractorImplApplySwapFeeMatrixTest : SwapInteractorImplTest
validationResult = null,
minAdaValue = null,
swapProvider = buildSwapProvider(providerType),
txType = txType,
)
}