Updated on 2026-08-14
This commit is contained in:
parent
68a1fede86
commit
0bf534776f
5 changed files with 2 additions and 720 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue