Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-22 15:31:48 +01:00
parent 68a1fede86
commit 0bf534776f
5 changed files with 2 additions and 720 deletions

View file

@ -600,288 +600,6 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
@Suppress("NullableToStringCall")
override suspend fun onSwapWithUnifiedFee(
fromSwapCurrencyStatus: SwapCurrencyStatus,
toSwapCurrencyStatus: SwapCurrencyStatus,
swapProvider: SwapProvider,
swapData: SwapDataModel?,
amountToSwap: String,
includeFeeInAmount: IncludeFeeInAmount,
fee: SwapFee?,
expressOperationType: ExpressOperationType,
isTangemPayWithdrawal: Boolean,
): SwapTransactionState {
TangemLogger.i(
"""
Swap (unified fee)
|- swapProvider: $swapProvider
|- swapData: $swapData
|- fromSwapCurrencyStatus:
|---- walletId: ${fromSwapCurrencyStatus.userWalletId}
|---- accountId: ${fromSwapCurrencyStatus.account.accountId}
|---- currencyId: ${fromSwapCurrencyStatus.currency.id}
|- toSwapCurrencyStatus: $toSwapCurrencyStatus
|---- walletId: ${toSwapCurrencyStatus.userWalletId}
|---- accountId: ${toSwapCurrencyStatus.account.accountId}
|---- currencyId: ${toSwapCurrencyStatus.currency.id}
|- amountToSwap: $amountToSwap
|- includeFeeInAmount: $includeFeeInAmount
|- fee: $fee
""".trimIndent(),
shouldSanitize = false,
)
val userWallet = fromSwapCurrencyStatus.userWallet
if (userWallet is UserWallet.Cold && isDemoCardUseCase(userWallet.scanResponse.card.cardId)) {
return SwapTransactionState.DemoMode
}
return when (swapProvider.type) {
ExchangeProviderType.CEX -> {
val amountDecimal = toBigDecimalOrNull(amountToSwap)
val amount = SwapAmount(requireNotNull(amountDecimal), fromSwapCurrencyStatus.currency.decimals)
val amountToSwapWithFee = if (includeFeeInAmount is IncludeFeeInAmount.Included) {
includeFeeInAmount.amountSubtractFee
} else {
amount
}
onSwapCexUnified(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
amount = amountToSwapWithFee,
swapFee = fee,
swapProvider = swapProvider,
expressOperationType = expressOperationType,
isTangemPayWithdrawal = isTangemPayWithdrawal,
)
}
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
val networkId = fromSwapCurrencyStatus.currency.network.rawId
if (isSolana(networkId)) {
onSwapSolanaDex(
provider = swapProvider,
swapData = requireNotNull(swapData),
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
amountToSwap = amountToSwap,
)
} else {
if (fee == null) return SwapTransactionState.Error.UnknownError
onSwapDex(
provider = swapProvider,
swapData = requireNotNull(swapData),
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
swapFee = fee,
amountToSwap = amountToSwap,
)
}
}
}
}
private suspend fun onSwapDex(
fromSwapCurrencyStatus: SwapCurrencyStatus,
toSwapCurrencyStatus: SwapCurrencyStatus,
provider: SwapProvider,
swapData: SwapDataModel,
amountToSwap: String,
swapFee: SwapFee,
): SwapTransactionState {
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
val txValue = requireNotNull(swapData.transaction.txValue) { "txValue is null" }
val amount = SwapAmount(amountDecimal, fromSwapCurrencyStatus.currency.decimals)
val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX
val dataToSign = dexTransaction.txData
val amountToSend = createNativeAmountForDex(txValue, fromSwapCurrencyStatus.currency.network)
val txData = createTransactionUseCase(
amount = amountToSend,
fee = swapFee.fee,
memo = null,
destination = swapData.transaction.txTo,
userWalletId = fromSwapCurrencyStatus.userWalletId,
network = toSwapCurrencyStatus.currency.network,
txExtras = createDexTxExtras(
dataToSign,
fromSwapCurrencyStatus.currency.network,
swapFee.fee.getGasLimit(),
),
).getOrElse { error ->
TangemLogger.e("Failed to create swap dex tx data", error)
return SwapTransactionState.Error.UnknownError
}
return handleSwapResult(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
provider = provider,
swapData = swapData,
amount = amount,
txData = txData,
payInAddress = getPayoutAddress(txData),
)
}
/**
* Branch selection:
* - Gasless token path: `swapFee.transactionFeeResult is LoadedExtended && selectedFeeToken.currency is Token`
* `createAndSendGaslessTransactionUseCase`.
* - Otherwise `sendTransactionUseCase` with `swapFee.fee`.
*/
@Suppress("LongMethod", "CanBeNonNullable")
private suspend fun onSwapCex(
fromSwapCurrencyStatus: SwapCurrencyStatus,
toSwapCurrencyStatus: SwapCurrencyStatus,
amount: SwapAmount,
swapFee: SwapFee?,
swapProvider: SwapProvider,
expressOperationType: ExpressOperationType,
isTangemPayWithdrawal: Boolean,
): SwapTransactionState {
val fromNetworkAddress = fromSwapCurrencyStatus.status.value.networkAddress
val fromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty()
val toNetworkAddress = toSwapCurrencyStatus.status.value.networkAddress
val toAddress = toNetworkAddress?.defaultAddress?.value.orEmpty()
val exchangeData = repository.getExchangeData(
userWallet = fromSwapCurrencyStatus.userWallet,
fromContractAddress = fromSwapCurrencyStatus.currency.getContractAddress(),
fromNetwork = fromSwapCurrencyStatus.currency.network.rawId,
toContractAddress = toSwapCurrencyStatus.currency.getContractAddress(),
fromAddress = fromAddress,
toNetwork = toSwapCurrencyStatus.currency.network.rawId,
fromAmount = amount.toStringWithRightOffset(),
fromDecimals = amount.decimals,
toDecimals = toSwapCurrencyStatus.currency.decimals,
providerId = swapProvider.providerId,
rateType = RateType.FLOAT,
expressOperationType = expressOperationType,
toAddress = toAddress,
refundAddress = fromNetworkAddress?.defaultAddress?.value,
refundExtraId = null, // currently always null,
).getOrElse { error -> return SwapTransactionState.Error.ExpressError(error) }
val exchangeDataCex =
exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError
if (isTangemPayWithdrawal) {
return SwapTransactionState.TangemPayWithdrawalData(
cryptoAmount = amount.value,
cryptoCurrencyId = requireNotNull(fromSwapCurrencyStatus.currency.id.rawCurrencyId),
cexAddress = exchangeDataCex.txTo,
fromAmount = amountFormatter.formatSwapAmountToUI(
amount,
fromSwapCurrencyStatus.currency.symbol,
),
fromAmountValue = amount.value,
toAmount = amountFormatter.formatSwapAmountToUI(
exchangeData.toTokenAmount,
toSwapCurrencyStatus.currency.symbol,
),
toAmountValue = exchangeData.toTokenAmount.value,
storeData = SwapTransactionState.TangemPayWithdrawalData.StoreTransactionData(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
amount = amount,
swapProvider = swapProvider,
swapDataModel = exchangeData,
txExternalUrl = exchangeDataCex.externalTxUrl,
txExternalId = exchangeDataCex.externalTxId,
averageDuration = null,
),
exchangeData = TangemPayWithdrawExchangeState(
txId = exchangeDataCex.txId,
fromNetwork = fromSwapCurrencyStatus.currency.network.rawId,
fromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty(),
payInAddress = exchangeData.transaction.txTo,
payInExtraId = exchangeDataCex.txExtraId,
),
)
}
val userWallet = fromSwapCurrencyStatus.userWallet
if (userWallet is UserWallet.Cold && isDemoCardUseCase(userWallet.scanResponse.card.cardId)) {
return SwapTransactionState.Error.UnknownError
}
val fee = requireNotNull(swapFee)
val txData = createTransferTransactionUseCase(
amount = amount.value.convertToSdkAmount(fromSwapCurrencyStatus.status),
fee = fee.fee,
memo = exchangeDataCex.txExtraId,
destination = exchangeDataCex.txTo,
userWalletId = fromSwapCurrencyStatus.userWalletId,
network = fromSwapCurrencyStatus.currency.network,
).getOrElse { error ->
TangemLogger.e("Failed to create swap CEX tx data", error)
return SwapTransactionState.Error.UnknownError
}
if (txData.extras == null && exchangeDataCex.txExtraId != null) {
return SwapTransactionState.Error.UnknownError
}
val isGaslessToken = fee.selectedFeeToken.currency is CryptoCurrency.Token &&
fee.transactionFeeResult is TransactionFeeResult.LoadedExtended
val result = if (isGaslessToken) {
createAndSendGaslessTransactionUseCase.invoke(
transactionData = txData,
userWallet = userWallet,
fee = fee.transactionFeeResult.fee,
)
} else {
sendTransactionUseCase(
txData = txData,
userWallet = userWallet,
network = fromSwapCurrencyStatus.currency.network,
)
}
val cexNetworkAddress = fromSwapCurrencyStatus.status.value.networkAddress
val cexFromAddress = cexNetworkAddress?.defaultAddress?.value.orEmpty()
return result.fold(
ifLeft = { error -> SwapTransactionState.Error.TransactionError(error) },
ifRight = { txHash ->
repository.exchangeSent(
userWallet = userWallet,
txId = exchangeDataCex.txId,
fromNetwork = fromSwapCurrencyStatus.currency.network.rawId,
fromAddress = cexFromAddress,
payInAddress = getPayoutAddress(txData),
txHash = txHash,
payInExtraId = exchangeDataCex.txExtraId,
)
val timestamp = System.currentTimeMillis()
val txExternalUrl = exchangeDataCex.externalTxUrl
storeSwapTransaction(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
amount = amount,
swapProvider = swapProvider,
swapDataModel = exchangeData,
timestamp = timestamp,
txExternalUrl = txExternalUrl,
txExternalId = exchangeDataCex.externalTxId,
)
storeLastCryptoCurrencyId(toSwapCurrencyStatus)
SwapTransactionState.TxSent(
fromAmount = amountFormatter.formatSwapAmountToUI(
amount,
fromSwapCurrencyStatus.currency.symbol,
),
fromAmountValue = amount.value,
toAmount = amountFormatter.formatSwapAmountToUI(
exchangeData.toTokenAmount,
toSwapCurrencyStatus.currency.symbol,
),
toAmountValue = exchangeData.toTokenAmount.value,
txHash = txHash,
txExternalUrl = txExternalUrl,
timestamp = timestamp,
)
},
)
}
private suspend fun onSwapDex(
fromSwapCurrencyStatus: SwapCurrencyStatus,
toSwapCurrencyStatus: SwapCurrencyStatus,
@ -1501,301 +1219,6 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
/**
* [REDACTED_TASK_KEY] Phase 3 unified fee API. Delegates to [DexSwapFeeCalculator] /
* [CexSwapFeeCalculator] and wraps the result in a [SwapFee].
*
* Behavior parity with the legacy `loadFeeForSwapTransaction` overloads is intentional
* the legacy methods stay in place through Phase 4. See `SwapInteractor.loadSwapFee` for
* the full contract.
*/
@Suppress("LongParameterList", "ReturnCount")
override suspend fun loadSwapFee(
provider: SwapProvider,
fromStatus: SwapCurrencyStatus,
toStatus: SwapCurrencyStatus,
amount: SwapAmount,
swapData: SwapDataModel?,
selectedFeeToken: CryptoCurrencyStatus?,
): Either<GetFeeError, SwapFee> = either {
if (amount.value.signum() == 0) {
raise(GetFeeError.UnknownError)
}
return when (provider.type) {
ExchangeProviderType.DEX,
ExchangeProviderType.DEX_BRIDGE,
-> loadDexSwapFee(
fromStatus = fromStatus,
swapData = swapData,
selectedFeeToken = selectedFeeToken,
)
ExchangeProviderType.CEX -> loadCexSwapFee(
fromStatus = fromStatus,
amount = amount,
selectedFeeToken = selectedFeeToken,
)
}
}
/**
* [REDACTED_TASK_KEY] DEX branch of [loadSwapFee]. Pulls the cached `ExpressTransactionModel.DEX`
* out of [swapData] and hands it to [DexSwapFeeCalculator]. Maps [ExpressDataError]
* `Left(GetFeeError.UnknownError)` to keep the unified surface a single error type, matching
* what the legacy `loadFeeForSwapTransaction` overload 2 does for DEX failures (line 1027 of
* the original code).
*/
private suspend fun loadDexSwapFee(
fromStatus: SwapCurrencyStatus,
swapData: SwapDataModel?,
selectedFeeToken: CryptoCurrencyStatus?,
): Either<GetFeeError, SwapFee> {
val transaction = swapData?.transaction as? ExpressTransactionModel.DEX
?: return GetFeeError.UnknownError.left()
return dexSwapFeeCalculator.calculate(
fromSwapCurrencyStatus = fromStatus,
transaction = transaction,
selectedToken = selectedFeeToken,
).fold(
ifLeft = { error -> GetFeeError.DataError(error).left() },
ifRight = { dexFeeResult ->
val feeToken = selectedFeeToken
?: resolveNativeFeeTokenStatus(fromStatus)
?: return@fold GetFeeError.UnknownError.left()
SwapFeeFactory.from(
transactionFeeResult = dexFeeResult.transactionFee,
selectedFeeToken = feeToken,
otherNativeFee = dexFeeResult.otherNativeFee,
feeBucket = FeeBucket.MARKET,
).right()
},
)
}
/**
* [REDACTED_TASK_KEY] CEX branch of [loadSwapFee]. Native-fallback behavior is preserved: when
* [selectedFeeToken] is null the gasless use case (invoked inside [CexSwapFeeCalculator])
* decides native vs token. The resulting `SwapFee.selectedFeeToken` is the explicit choice
* if provided, otherwise the native coin status of the from-token's network.
*/
private suspend fun loadCexSwapFee(
fromStatus: SwapCurrencyStatus,
amount: SwapAmount,
selectedFeeToken: CryptoCurrencyStatus?,
): Either<GetFeeError, SwapFee> {
return cexSwapFeeCalculator.calculate(
userWallet = fromStatus.userWallet,
fromSwapCurrencyStatus = fromStatus,
amount = amount.value,
selectedFeeToken = selectedFeeToken,
).fold(
ifLeft = { it.left() },
ifRight = { cexFeeResult ->
val feeToken = selectedFeeToken
?: resolveNativeFeeTokenStatus(fromStatus)
?: return@fold GetFeeError.UnknownError.left()
SwapFeeFactory.from(
transactionFeeResult = cexFeeResult.transactionFee,
selectedFeeToken = feeToken,
otherNativeFee = BigDecimal.ZERO,
feeBucket = FeeBucket.MARKET,
).right()
},
)
}
/**
* [REDACTED_TASK_KEY] resolves the native-coin [CryptoCurrencyStatus] for the from-token's network.
* Used as the default `selectedFeeToken` of [SwapFee] when the caller did not provide an
* explicit choice. Mirrors how `SwapModel.updateFeePaidCryptoCurrencyFor` populates
* `dataState.feePaidCryptoCurrency`.
*/
private suspend fun resolveNativeFeeTokenStatus(fromStatus: SwapCurrencyStatus): CryptoCurrencyStatus? {
return getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = fromStatus.userWalletId,
cryptoCurrencyStatus = fromStatus.status,
).getOrNull() ?: run {
val feeNetwork = fromStatus.currency.network
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(
fromStatus.userWalletId,
feeNetwork,
)
val (feeCurrency, balance) = when (feePaidCurrency) {
FeePaidCurrency.Coin -> currenciesRepository.createCoinCurrency(feeNetwork) to
walletManagersFacade.getNativeTokenBalance(
userWalletId = fromStatus.userWalletId,
networkId = feeNetwork.rawId,
derivationPath = feeNetwork.derivationPath.value,
)
is FeePaidCurrency.Token -> currenciesRepository.createTokenCurrency(
userWalletId = fromStatus.userWalletId,
contractAddress = feePaidCurrency.contractAddress,
networkId = feeNetwork.rawId,
) to feePaidCurrency.balance
is FeePaidCurrency.FeeResource,
FeePaidCurrency.SameCurrency,
-> fromStatus.currency to fromStatus.status.value.amount
}
val feeCurrencyRawID = feeCurrency.id.rawCurrencyId ?: return@run null
val quote = quotesRepository.getMultiQuoteSyncOrNull(setOf(feeCurrencyRawID))
?.firstOrNull()?.value as? QuoteStatus.Data
CryptoCurrencyStatus(
currency = feeCurrency,
value = if (quote == null) {
CryptoCurrencyStatus.NoQuote(
amount = balance.orZero(),
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = fromStatus.status.value.networkAddress ?: return@run null,
sources = CryptoCurrencyStatus.Sources(),
)
} else {
CryptoCurrencyStatus.Loaded(
amount = balance.orZero(),
fiatAmount = quote.fiatRate.multiply(balance),
fiatRate = quote.fiatRate,
priceChange = quote.priceChange,
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = fromStatus.status.value.networkAddress ?: return@run null,
sources = CryptoCurrencyStatus.Sources(),
)
},
)
}
}
/**
* Patches an existing [SwapState.QuotesLoadedState] with a freshly resolved [SwapFee].
* See [SwapInteractor.applySwapFee] for the full contract.
*
* Numeric fee used for downstream computation:
* - If `fee.selectedFeeToken.currency` is a token `0` for the balance / include-fee math
* when the fee currency differs from the from-token (matches legacy `manageWarnings`
* semantics at line 422 of the pre-Phase-4 code).
* - Otherwise `fee.fee.amount.value + fee.otherNativeFee` (the bridge-aware native fee).
*
* The fee is folded into a single [SwapBalanceStatus] by [computeBalanceStatus], which is
* then assigned to `preparedSwapConfigState.balanceStatus`.
*/
override suspend fun applySwapFee(
state: SwapState.QuotesLoadedState,
fee: SwapFee,
lastReducedBalanceBy: BigDecimal,
): SwapState.QuotesLoadedState {
val fromSwapCurrencyStatus = state.fromTokenInfo.swapCurrencyStatus
val amount = state.fromTokenInfo.tokenAmount
val isFeeInToken = fee.selectedFeeToken.currency is CryptoCurrency.Token
val nativeFee = (fee.fee.amount.value ?: BigDecimal.ZERO) + fee.otherNativeFee
// Mirrors legacy manageWarnings: token-fee paths skip the native deduction.
val warningsFee = if (isFeeInToken && fromSwapCurrencyStatus.currency.id != fee.selectedFeeToken.currency.id) {
BigDecimal.ZERO
} else {
nativeFee
}
val balanceStatus = computeBalanceStatus(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
amount = amount,
reduceBalanceBy = lastReducedBalanceBy,
feeValue = nativeFee,
selectedFeeToken = fee.selectedFeeToken,
provider = state.swapProvider,
)
val currencyCheck = manageWarnings(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
amount = amount,
fee = warningsFee,
balanceStatus = balanceStatus,
)
val validationResult = manageTransactionValidationWarnings(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
amount = amount,
feeValue = nativeFee,
)
val minAdaValue = (fee.fee as? Fee.CardanoToken)?.minAdaValue
return state.copy(
preparedSwapConfigState = state.preparedSwapConfigState.copy(
balanceStatus = balanceStatus,
),
currencyCheck = currencyCheck,
validationResult = validationResult,
minAdaValue = minAdaValue,
)
}
/**
* Decision tree (matches the user-approved derivation table plus the implicit Token-fee sub-case):
* 1. `Included` from `getIncludeFeeInAmountInternal` [SwapBalanceStatus.FeeAdjustedAmount].
* 2. `!isBalanceEnough` (from-token balance can't cover the amount itself)
* [SwapBalanceStatus.InsufficientAmount].
* 3. `feeBalanceState is NotEnough` [SwapBalanceStatus.InsufficientFee]. This catches:
* - From-token is a Token, native balance can't cover the fee
* (legacy `includeFeeInAmount=BalanceNotEnough` for the Token branch).
* - From-token is a Coin and `balance - amount < fee`
* (legacy `feeState=NotEnough && includeFeeInAmount=Excluded`).
* 4. Otherwise [SwapBalanceStatus.Sufficient].
*
* The legacy ambiguity where `BalanceNotEnough` meant "amount > balance" for Coin
* from-currencies but "fee > native balance" for Token from-currencies is resolved here
* by consulting `isBalanceEnough` (amount-alone check) directly.
*/
private suspend fun computeBalanceStatus(
fromSwapCurrencyStatus: SwapCurrencyStatus,
amount: SwapAmount,
reduceBalanceBy: BigDecimal,
feeValue: BigDecimal,
selectedFeeToken: CryptoCurrencyStatus?,
provider: SwapProvider,
): SwapBalanceStatus {
when (provider.type) {
ExchangeProviderType.CEX -> {
val includeStatus = getIncludeFeeInAmountInternal(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
amount = amount,
reduceBalanceBy = reduceBalanceBy,
feeValue = feeValue,
selectedFeeToken = selectedFeeToken,
)
if (includeStatus is IncludeFeeInAmountInternal.Included) {
return SwapBalanceStatus.FeeAdjustedAmount(adjustedAmount = includeStatus.amountSubtractFee)
}
}
ExchangeProviderType.DEX,
ExchangeProviderType.DEX_BRIDGE,
-> Unit
}
val isAmountAlone = isBalanceEnough(fromSwapCurrencyStatus, amount, fee = feeValue)
if (!isAmountAlone) {
return SwapBalanceStatus.InsufficientAmount
}
val feeBalanceState = getFeeBalanceState(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
fee = feeValue,
spendAmount = amount,
selectedFeeToken = selectedFeeToken,
)
return when (feeBalanceState) {
is FeeBalanceState.Enough -> SwapBalanceStatus.Sufficient
is FeeBalanceState.NotEnough -> SwapBalanceStatus.InsufficientFee(
feeCurrencyName = feeBalanceState.currencyName,
feeCurrencySymbol = feeBalanceState.currencySymbol,
)
}
}
private suspend fun storeLastCryptoCurrencyId(swapCurrencyStatus: SwapCurrencyStatus) {
swapTransactionRepository.storeLastSwappedCryptoCurrencyId(
userWalletId = swapCurrencyStatus.userWalletId,

View file

@ -1,18 +1,10 @@
package com.tangem.feature.swap.domain.di
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.domain.transaction.usecase.CreateTransactionDataExtrasUseCase
import com.tangem.domain.transaction.usecase.EstimateFeeUseCase
import com.tangem.domain.transaction.usecase.GetEthSpecificFeeUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.feature.swap.domain.AllowPermissionsHandler
import com.tangem.feature.swap.domain.AllowPermissionsHandlerImpl
import com.tangem.feature.swap.domain.GetSwapUiModeUseCase
import com.tangem.feature.swap.domain.SetSwapUiModeUseCase
import com.tangem.feature.swap.domain.SwapFeedbackUseCase
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.SwapInteractorImpl
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.transaction.usecase.gasless.EstimateFeeForGaslessTxUseCase
import com.tangem.domain.transaction.usecase.gasless.EstimateFeeForTokenUseCase
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
@ -23,9 +15,9 @@ import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.fee.CexSwapFeeCalculator
import com.tangem.feature.swap.domain.fee.DexSwapFeeCalculator
import com.tangem.feature.swap.domain.fee.PatchEthGasLimitForSwap
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor
import com.tangem.feature.swap.domain.transfer.SwapTransferInteractorImpl
import com.tangem.features.swap.SwapFeatureToggles
import dagger.Binds
import dagger.Module
import dagger.Provides

View file

@ -238,7 +238,6 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase(
providers = listOf(dexProvider),
amountToSwap = "1.0",
reduceBalanceBy = BigDecimal.ZERO,
)
// Then — has a result entry for the DEX provider; type of state is decided by internal logic

View file

@ -1326,55 +1326,6 @@ internal class SwapModel @Inject constructor(
}
}
private fun onTransferClick() {
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus
val toSwapCurrencyStatus = dataState.toSwapCurrencyStatus
val fee = (feeSelectorRepository.state.value as? FeeSelectorUM.Content)?.selectedFeeItem?.fee
if (fromSwapCurrencyStatus == null || toSwapCurrencyStatus == null || fee == null) {
TangemLogger.e("onTransferClick: missing currency status or fee, aborting")
showAlert()
return
}
uiState = swapTransferStateBuilder.createTransferInProgressState(uiState)
modelScope.launch(dispatchers.main) {
swapTransferInteractor.sendTransfer(
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
toSwapCurrencyStatus = toSwapCurrencyStatus,
fromTokenAmount = lastAmount.value,
fee = fee,
transactionFeeResult = requireNotNull(getSelectedSwapFee()?.transactionFeeResult) {
"It should be not null at this stage"
},
).fold(
ifLeft = { error ->
TangemLogger.e("onTransferClick: transfer failed: ${error.getAnalyticsDescription()}")
refreshTransferUIStateAfterFeeUpdate()
showAlert()
},
ifRight = { txHash ->
val txUrl = getExplorerTransactionUrlUseCase(
txHash = txHash,
currency = fromSwapCurrencyStatus.currency,
).getOrElse {
TangemLogger.i("onTransferClick: tx hash explore not supported")
""
}
updateWalletBalance()
uiState = swapTransferStateBuilder.createSuccessState(
uiState = uiState,
dataState = dataState,
appCurrency = selectedAppCurrencyFlow.value,
isAccountsMode = isAccountsMode,
txUrl = txUrl,
timestamp = System.currentTimeMillis(),
fee = null,
)
router.replaceAll(SwapRoute.Success)
},
)
}
}
private suspend fun processTangemPayWithdrawal(
fromSwapCurrencyStatus: SwapCurrencyStatus,
swapTransactionState: SwapTransactionState.TangemPayWithdrawalData,
@ -1754,15 +1705,6 @@ internal class SwapModel @Inject constructor(
appRouter.push(route)
},
openTokenDetailsScreen = { cryptoCurrency ->
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus ?: return@UiActions
val route = AppRoute.CurrencyDetails(
userWalletId = fromSwapCurrencyStatus.userWalletId,
currency = cryptoCurrency,
)
appRouter.push(route)
},
onRetryClick = {
startLoadingQuotesFromLastState()
},
@ -2119,34 +2061,6 @@ internal class SwapModel @Inject constructor(
is FeeItem.Loading -> FeeBucket.MARKET
}
/**
* [REDACTED_TASK_KEY] Phase 4. Extracts the bridge protocol fee from the cached
* [SwapDataModel.transaction] payload (DEX bridge providers carry `otherNativeFeeWei`).
*
* The UI's `FeeSelectorUM` doesn't carry this value, so we read it from the most-recent
* swap data. Returns [BigDecimal.ZERO] when no swap data is cached, the transaction is not
* a DEX payload, or `otherNativeFeeWei` is null (non-bridge providers).
*/
private fun resolveOtherNativeFee(): BigDecimal {
val transaction =
dataState.getCurrentLoadedSwapState()?.swapDataModel?.transaction as? ExpressTransactionModel.DEX
?: return BigDecimal.ZERO
val otherNativeFeeWei = transaction.otherNativeFeeWei ?: return BigDecimal.ZERO
val nativeDecimals = dataState.fromSwapCurrencyStatus?.currency?.network?.let { network ->
Blockchain.fromNetworkId(network.rawId)?.decimals()
} ?: return BigDecimal.ZERO
return otherNativeFeeWei.movePointLeft(nativeDecimals)
}
private fun FeeItem.toFeeBucket(): FeeBucket = when (this) {
is FeeItem.Slow -> FeeBucket.SLOW
is FeeItem.Market -> FeeBucket.MARKET
is FeeItem.Fast -> FeeBucket.FAST
is FeeItem.Suggested -> FeeBucket.SUGGESTED
is FeeItem.Custom -> FeeBucket.CUSTOM
is FeeItem.Loading -> FeeBucket.MARKET
}
inner class FeeSelectorRepository : SwapFeeSelectorBlockComponent.ModelRepositoryExtended {
override val state = MutableStateFlow<FeeSelectorUM>(

View file

@ -379,52 +379,6 @@ internal class SwapNotificationsFactory(
}
}
private fun MutableList<NotificationUM>.maybeAddFeeErrorNotification(
feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
quoteModel: SwapState.QuotesLoadedState,
feeError: GetFeeError?,
) {
if (
feeError == null || feeCryptoCurrencyStatus == null ||
quoteModel.permissionState !is PermissionDataState.Empty
) {
return
}
when (feeError) {
is GetFeeError.DataError -> {
val error = feeError.cause
if (error is ExpressDataError) {
addAll(
getQuotesErrorStateNotifications(
expressDataError = error,
fromToken = quoteModel.fromTokenInfo.swapCurrencyStatus.currency,
balanceStatus = quoteModel.preparedSwapConfigState.balanceStatus,
swapFee = null,
),
)
} else {
addFeeUnreachableNotification(
tokenStatus = quoteModel.fromTokenInfo.swapCurrencyStatus.status,
coinStatus = feeCryptoCurrencyStatus,
feeError = feeError,
dustValue = quoteModel.currencyCheck?.dustValue,
onReload = actions.onRetryClick,
onClick = actions.openTokenDetailsScreen,
)
}
}
else -> addFeeUnreachableNotification(
tokenStatus = quoteModel.fromTokenInfo.swapCurrencyStatus.status,
coinStatus = feeCryptoCurrencyStatus,
feeError = feeError,
dustValue = quoteModel.currencyCheck?.dustValue,
onReload = actions.onRetryClick,
onClick = actions.openTokenDetailsScreen,
)
}
}
private fun MutableList<NotificationUM>.addReduceAmountNotification(
cryptoCurrencyStatus: CryptoCurrencyStatus,
fromAmount: SwapAmount,