Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-30 16:06:33 +01:00
parent 50fa269746
commit 4cb147b27e
7 changed files with 345 additions and 10 deletions

View file

@ -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 {

View file

@ -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,
)
}