Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-01 21:57:56 +05:00
parent 438f502a4c
commit 0fb88d97c9
6 changed files with 103 additions and 62 deletions

View file

@ -129,5 +129,6 @@ interface SwapInteractor {
amount: SwapAmount,
swapData: SwapDataModel?,
selectedFeeToken: CryptoCurrencyStatus?,
isGasless: Boolean,
): Either<GetFeeError, SwapFee>
}

View file

@ -966,6 +966,7 @@ internal class SwapInteractorImpl @Inject constructor(
amount: SwapAmount,
swapData: SwapDataModel?,
selectedFeeToken: CryptoCurrencyStatus?,
isGasless: Boolean,
): Either<GetFeeError, SwapFee> = either {
if (amount.value.signum() == 0) {
raise(GetFeeError.UnknownError)
@ -982,6 +983,7 @@ internal class SwapInteractorImpl @Inject constructor(
fromStatus = fromStatus,
amount = amount,
selectedFeeToken = selectedFeeToken,
isGasless = isGasless,
)
}
}
@ -1031,12 +1033,14 @@ internal class SwapInteractorImpl @Inject constructor(
fromStatus: SwapCurrencyStatus,
amount: SwapAmount,
selectedFeeToken: CryptoCurrencyStatus?,
isGasless: Boolean,
): Either<GetFeeError, SwapFee> {
return cexSwapFeeCalculator.calculate(
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = amount.value,
selectedFeeToken = selectedFeeToken,
isGasless = isGasless,
).fold(
ifLeft = { it.left() },
ifRight = { cexFeeResult ->

View file

@ -45,40 +45,51 @@ class CexSwapFeeCalculator(
fromSwapCurrencyStatus: SwapCurrencyStatus,
amount: BigDecimal,
selectedFeeToken: CryptoCurrencyStatus?,
isGasless: Boolean,
): Either<GetFeeError, CexFeeResult> = either {
if (amount.signum() == 0) {
raise(GetFeeError.UnknownError)
}
val transactionFeeResult: TransactionFeeResult = when {
selectedFeeToken == null -> {
// Gasless path — overload 1 in SwapInteractorImpl. No gas-limit bump.
val feeExtended = estimateFeeForGaslessTxUseCase(
amount = amount,
userWallet = userWallet,
sendingTokenCurrencyStatus = fromSwapCurrencyStatus.status,
).bind()
TransactionFeeResult.LoadedExtended(feeExtended)
}
selectedFeeToken.currency is CryptoCurrency.Token -> {
// Explicit gasless-token path — overload 1 in SwapInteractorImpl. No gas-limit bump.
val feeExtended = estimateFeeForTokenUseCase(
userWallet = userWallet,
feeTokenCurrencyStatus = selectedFeeToken,
sendingTokenCurrencyStatus = fromSwapCurrencyStatus.status,
amount = amount,
).bind()
TransactionFeeResult.LoadedExtended(feeExtended)
}
else -> {
// Explicit native fee path — overload 2 in SwapInteractorImpl. Apply 5% bump.
val fee = estimateFeeUseCase(
amount = amount,
userWallet = userWallet,
cryptoCurrencyStatus = fromSwapCurrencyStatus.status,
).bind()
TransactionFeeResult.Loaded(patchEthGasLimitForSwap(fee))
val transactionFeeResult: TransactionFeeResult = if (isGasless) {
when {
selectedFeeToken == null -> {
// Gasless path — overload 1 in SwapInteractorImpl. No gas-limit bump.
val feeExtended = estimateFeeForGaslessTxUseCase(
amount = amount,
userWallet = userWallet,
sendingTokenCurrencyStatus = fromSwapCurrencyStatus.status,
).bind()
TransactionFeeResult.LoadedExtended(feeExtended)
}
selectedFeeToken.currency is CryptoCurrency.Token -> {
// Explicit gasless-token path — overload 1 in SwapInteractorImpl. No gas-limit bump.
val feeExtended = estimateFeeForTokenUseCase(
userWallet = userWallet,
feeTokenCurrencyStatus = selectedFeeToken,
sendingTokenCurrencyStatus = fromSwapCurrencyStatus.status,
amount = amount,
).bind()
TransactionFeeResult.LoadedExtended(feeExtended)
}
else -> {
// Explicit native fee path — overload 2 in SwapInteractorImpl. Apply 5% bump.
val fee = estimateFeeUseCase(
amount = amount,
userWallet = userWallet,
cryptoCurrencyStatus = fromSwapCurrencyStatus.status,
).bind()
TransactionFeeResult.Loaded(patchEthGasLimitForSwap(fee))
}
}
} else {
// Explicit native fee path — overload 2 in SwapInteractorImpl. Apply 5% bump.
val fee = estimateFeeUseCase(
amount = amount,
userWallet = userWallet,
cryptoCurrencyStatus = fromSwapCurrencyStatus.status,
).bind()
TransactionFeeResult.Loaded(patchEthGasLimitForSwap(fee))
}
CexFeeResult(transactionFee = transactionFeeResult)

View file

@ -95,6 +95,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = swapData,
selectedFeeToken = null,
isGasless = false,
)
assertThat(result.isRight()).isTrue()
@ -138,6 +139,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
amount = SwapAmount(BigDecimal.ONE, 9),
swapData = swapData,
selectedFeeToken = null,
isGasless = false,
)
assertThat(result.isRight()).isTrue()
@ -173,6 +175,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = swapData,
selectedFeeToken = null,
isGasless = false,
)
assertThat(result.isRight()).isTrue()
@ -193,6 +196,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = null,
selectedFeeToken = null,
isGasless = false,
)
assertThat(result.isLeft()).isTrue()
@ -214,7 +218,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = null,
selectedFeeToken = null,
)
isGasless = false,
)
assertThat(result.isLeft()).isTrue()
result.onLeft { error ->
@ -240,8 +246,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
toStatus = toStatus,
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = swapData,
selectedFeeToken = null,
)
selectedFeeToken = null, isGasless = false,
)
assertThat(result.isLeft()).isTrue()
result.onLeft { error ->
@ -265,7 +272,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
)
}
coEvery {
cexSwapFeeCalculator.calculate(any(), any(), any(), any())
cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any())
} returns CexFeeResult(
transactionFee = TransactionFeeResult.LoadedExtended(extendedFee),
).right()
@ -277,6 +284,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = null,
selectedFeeToken = null,
isGasless = true,
)
assertThat(result.isRight()).isTrue()
@ -291,7 +299,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal.ONE,
selectedFeeToken = null,
)
isGasless = true,
)
}
}
@ -307,7 +317,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
val toStatus = buildSwapCurrencyStatus(networkRawId = ethNetwork, isCoin = true)
val extendedFee = mockk<TransactionFeeExtended>(relaxed = true)
coEvery {
cexSwapFeeCalculator.calculate(any(), any(), any(), any())
cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any())
} returns CexFeeResult(
transactionFee = TransactionFeeResult.LoadedExtended(extendedFee),
).right()
@ -318,8 +328,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
toStatus = toStatus,
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = null,
selectedFeeToken = null,
)
selectedFeeToken = null, isGasless = true,
)
assertThat(result.isRight()).isTrue()
result.onRight { swapFee ->
@ -337,7 +348,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
}
val extendedFee = mockk<TransactionFeeExtended>(relaxed = true)
coEvery {
cexSwapFeeCalculator.calculate(any(), any(), any(), any())
cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any())
} returns CexFeeResult(
transactionFee = TransactionFeeResult.LoadedExtended(extendedFee),
).right()
@ -348,8 +359,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
toStatus = toStatus,
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = null,
selectedFeeToken = explicitTokenStatus,
)
selectedFeeToken = explicitTokenStatus, isGasless = true,
)
assertThat(result.isRight()).isTrue()
result.onRight { swapFee ->
@ -360,8 +372,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal.ONE,
selectedFeeToken = explicitTokenStatus,
)
selectedFeeToken = explicitTokenStatus, isGasless = true,
)
}
}
@ -374,7 +387,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
}
val rawFee = TransactionFee.Single(normal = mockk<Fee.Common>(relaxed = true))
coEvery {
cexSwapFeeCalculator.calculate(any(), any(), any(), any())
cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any())
} returns CexFeeResult(
transactionFee = TransactionFeeResult.Loaded(rawFee),
).right()
@ -385,8 +398,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
toStatus = toStatus,
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = null,
selectedFeeToken = explicitNativeStatus,
)
selectedFeeToken = explicitNativeStatus, isGasless = true,
)
assertThat(result.isRight()).isTrue()
result.onRight { swapFee ->
@ -400,7 +414,7 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
val fromStatus = buildSwapCurrencyStatus(networkRawId = ethNetwork, isCoin = true)
val toStatus = buildSwapCurrencyStatus(networkRawId = ethNetwork, isCoin = true)
coEvery {
cexSwapFeeCalculator.calculate(any(), any(), any(), any())
cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any())
} returns GetFeeError.UnknownError.left()
val result = sut.loadSwapFee(
@ -409,8 +423,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
toStatus = toStatus,
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = null,
selectedFeeToken = null,
)
selectedFeeToken = null, isGasless = true,
)
assertThat(result.isLeft()).isTrue()
result.onLeft { error ->
@ -433,14 +448,15 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
toStatus = toStatus,
amount = SwapAmount(BigDecimal.ZERO, 18),
swapData = null,
selectedFeeToken = null,
)
selectedFeeToken = null, isGasless = true,
)
assertThat(result.isLeft()).isTrue()
result.onLeft { error ->
assertThat(error).isInstanceOf(GetFeeError.UnknownError::class.java)
}
coVerify(exactly = 0) { cexSwapFeeCalculator.calculate(any(), any(), any(), any()) }
coVerify(exactly = 0) { cexSwapFeeCalculator.calculate(any(), any(), any(), any(), any()) }
}
@Test
@ -459,7 +475,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
amount = SwapAmount(BigDecimal.ZERO, 18),
swapData = swapData,
selectedFeeToken = null,
)
isGasless = false,
)
assertThat(result.isLeft()).isTrue()
result.onLeft { error ->
@ -501,7 +519,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = swapData,
selectedFeeToken = explicitTokenStatus,
)
isGasless = false,
)
assertThat(result.isRight()).isTrue()
result.onRight { swapFee ->
@ -571,8 +591,9 @@ internal class SwapInteractorImplLoadSwapFeeTest : SwapInteractorImplTestBase()
toStatus = toStatus,
amount = SwapAmount(BigDecimal.ONE, 18),
swapData = swapData,
selectedFeeToken = null,
)
selectedFeeToken = null, isGasless = false,
)
// When resolveNativeFeeTokenStatus returns null → Left(UnknownError)
assertThat(result.isLeft()).isTrue()

View file

@ -69,6 +69,7 @@ internal class CexSwapFeeCalculatorTest {
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal.ZERO,
selectedFeeToken = null,
isGasless = true,
)
assertThat(result.isLeft()).isTrue()
@ -97,7 +98,7 @@ internal class CexSwapFeeCalculatorTest {
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal("1.5"),
selectedFeeToken = null,
selectedFeeToken = null, isGasless = true,
)
assertThat(result.isRight()).isTrue()
@ -130,7 +131,7 @@ internal class CexSwapFeeCalculatorTest {
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal("1.0"),
selectedFeeToken = null,
selectedFeeToken = null, isGasless = true,
)
assertThat(result.isLeft()).isTrue()
@ -160,7 +161,7 @@ internal class CexSwapFeeCalculatorTest {
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal("2.0"),
selectedFeeToken = tokenStatus,
selectedFeeToken = tokenStatus, isGasless = true,
)
assertThat(result.isRight()).isTrue()
@ -207,7 +208,7 @@ internal class CexSwapFeeCalculatorTest {
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal("3.0"),
selectedFeeToken = coinStatus,
selectedFeeToken = coinStatus, isGasless = true,
)
assertThat(result.isRight()).isTrue()
@ -251,7 +252,7 @@ internal class CexSwapFeeCalculatorTest {
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal("1.0"),
selectedFeeToken = coinStatus,
selectedFeeToken = coinStatus, isGasless = true,
)
result.onRight { cexResult ->
@ -276,7 +277,7 @@ internal class CexSwapFeeCalculatorTest {
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal("1.0"),
selectedFeeToken = coinStatus,
selectedFeeToken = coinStatus, isGasless = true,
)
assertThat(result.isLeft()).isTrue()
@ -321,7 +322,7 @@ internal class CexSwapFeeCalculatorTest {
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal("1.0"),
selectedFeeToken = coinStatus,
selectedFeeToken = coinStatus, isGasless = true,
)
result.onRight { cexResult ->
@ -355,6 +356,7 @@ internal class CexSwapFeeCalculatorTest {
fromSwapCurrencyStatus = fromStatus,
amount = BigDecimal("1.0"),
selectedFeeToken = null,
isGasless = true,
)
coVerify(exactly = 1) {

View file

@ -2176,7 +2176,8 @@ internal class SwapModel @Inject constructor(
toStatus = toSwapCurrencyStatus,
amount = swapAmount,
swapData = swapDataForCall,
selectedFeeToken = dataState.feePaidCryptoCurrency,
selectedFeeToken = null,
isGasless = false,
).map { swapFee ->
when (val res = swapFee.transactionFeeResult) {
is TransactionFeeResult.LoadedExtended -> res.fee.transactionFee
@ -2230,6 +2231,7 @@ internal class SwapModel @Inject constructor(
amount = swapAmount,
swapData = swapDataForCall,
selectedFeeToken = selectedToken,
isGasless = true,
).map { swapFee ->
// The fee selector block consumes TransactionFeeExtended; build one when
// `transactionFeeResult` is LoadedExtended, else wrap the native fee in a