Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-04 14:51:41 +01:00
parent 7e35a4de75
commit c0471dbf9f
20 changed files with 315 additions and 133 deletions

View file

@ -8,6 +8,7 @@ import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class ExpressDataConverter : Converter<ExchangeDataResponseWithTxDetails, SwapDataModel> {
@ -24,19 +25,30 @@ internal class ExpressDataConverter : Converter<ExchangeDataResponseWithTxDetail
dataResponse: ExchangeDataResponse,
): ExpressTransactionModel {
return if (transactionDto.txType == TxType.SWAP) {
val otherNativeFeeWei = transactionDto.otherNativeFee?.let {
if (it == "0") {
BigDecimal.ZERO
} else {
requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" }
}
}
ExpressTransactionModel.DEX(
fromAmount = createFromAmountWithOffset(dataResponse.fromAmount, dataResponse.fromDecimals),
toAmount = createFromAmountWithOffset(dataResponse.toAmount, dataResponse.toDecimals),
txValue = transactionDto.txValue,
txId = dataResponse.txId,
txTo = transactionDto.txTo,
txFrom = requireNotNull(transactionDto.txFrom),
txData = requireNotNull(transactionDto.txData),
txExtraId = transactionDto.txExtraId,
otherNativeFeeWei = otherNativeFeeWei,
gas = transactionDto.gas?.toBigIntegerOrNull() ?: error("gas is empty"),
)
} else {
ExpressTransactionModel.CEX(
fromAmount = createFromAmountWithOffset(dataResponse.fromAmount, dataResponse.fromDecimals),
toAmount = createFromAmountWithOffset(dataResponse.toAmount, dataResponse.toDecimals),
txValue = transactionDto.txValue,
txId = dataResponse.txId,
txTo = transactionDto.txTo,
externalTxId = requireNotNull(transactionDto.externalTxId),

View file

@ -1,28 +1,38 @@
package com.tangem.feature.swap.domain.models.domain
import com.tangem.feature.swap.domain.models.SwapAmount
import java.math.BigDecimal
import java.math.BigInteger
sealed class ExpressTransactionModel {
abstract val fromAmount: SwapAmount
abstract val toAmount: SwapAmount
abstract val txValue: String
abstract val txId: String
abstract val txTo: String
abstract val txExtraId: String?
/**
* @param txValue amount for tx, should use native coin decimals, this value will send as native amount in tx
*/
data class DEX(
override val fromAmount: SwapAmount,
override val toAmount: SwapAmount,
override val txValue: String,
override val txId: String,
override val txTo: String,
override val txExtraId: String?,
val txFrom: String,
val txData: String,
val otherNativeFeeWei: BigDecimal?,
val gas: BigInteger,
) : ExpressTransactionModel()
data class CEX(
override val fromAmount: SwapAmount,
override val toAmount: SwapAmount,
override val txValue: String,
override val txId: String,
override val txTo: String,
override val txExtraId: String?,

View file

@ -8,6 +8,10 @@ import java.math.BigDecimal
sealed interface SwapState {
/**
* @param txFee fee state uses for calculation and build transaction
* @param txFeeIncludeOtherNativeFee fee state uses for display and included otherNativeFee (specific for bridge)
*/
data class QuotesLoadedState(
val fromTokenInfo: TokenSwapInfo,
val toTokenInfo: TokenSwapInfo,
@ -22,7 +26,9 @@ sealed interface SwapState {
val permissionState: PermissionDataState = PermissionDataState.Empty,
val swapDataModel: SwapDataModel? = null,
val txFee: TxFeeState,
// val txFeeIncludeOtherNativeFee: TxFeeState,
val warnings: List<Warning> = emptyList(),
val swapProvider: SwapProvider,
) : SwapState
data class EmptyAmountState(val zeroAmountEquivalent: String) : SwapState
@ -102,6 +108,9 @@ data class TxFee(
val gasLimit: Int,
val feeFiatFormatted: String,
val feeCryptoFormatted: String,
val feeIncludeOtherNativeFee: BigDecimal,
val feeFiatFormattedWithNative: String,
val feeCryptoFormattedWithNative: String,
val decimals: Int,
val cryptoSymbol: String,
val feeType: FeeType,

View file

@ -4,8 +4,11 @@ import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.minimalAmount
import com.tangem.common.extensions.hexToBytes
import com.tangem.core.ui.utils.BigDecimalFormatter
@ -15,10 +18,8 @@ import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
@ -342,7 +343,7 @@ internal class SwapInteractorImpl @Inject constructor(
)
} else {
provider to getQuotesState(
exchangeProviderType = provider.type,
provider = provider,
quoteDataModel = quotes,
amount = amount,
fromToken = fromToken,
@ -366,7 +367,6 @@ internal class SwapInteractorImpl @Inject constructor(
isBalanceWithoutFeeEnough: Boolean,
): Pair<SwapProvider, SwapState> {
return provider to loadCexQuoteData(
exchangeProviderType = ExchangeProviderType.CEX,
networkId = networkId,
amount = amount,
fromTokenStatus = fromToken,
@ -595,8 +595,8 @@ internal class SwapInteractorImpl @Inject constructor(
swapData = requireNotNull(swapData),
currencyToSendStatus = currencyToSend,
currencyToGetStatus = currencyToGet,
amountToSwap = amountToSwap,
fee = fee,
amountToSwap = amountToSwap,
userWalletId = requireNotNull(getSelectedWallet()).walletId,
)
}
@ -622,8 +622,8 @@ internal class SwapInteractorImpl @Inject constructor(
)
val fee = when (val txFee = state.txFee) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeIncludeOtherNativeFee
is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee
}
val feeState = getFeeState(
fee = fee,
@ -656,8 +656,9 @@ internal class SwapInteractorImpl @Inject constructor(
val derivationPath = currencyToSendStatus.currency.network.derivationPath.value
val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX
val dataToSign = dexTransaction.txData
val amountToSend = createNativeAmountForDex(swapData.transaction.txValue, currencyToSendStatus.currency.network)
val txData = createTransactionUseCase(
amount = amount.value.convertToAmount(currencyToSendStatus.currency),
amount = amountToSend,
fee = getFeeForTransaction(
fee = fee,
blockchain = Blockchain.fromId(currencyToSendStatus.currency.network.id.value),
@ -668,7 +669,6 @@ internal class SwapInteractorImpl @Inject constructor(
network = currencyToSendStatus.currency.network,
txExtras = createDexTxExtras(fee.gasLimit, dataToSign),
hash = dataToSign,
isSwap = true,
).getOrElse {
Timber.e(it)
return SwapTransactionState.UnknownError
@ -984,7 +984,6 @@ internal class SwapInteractorImpl @Inject constructor(
*/
@Suppress("LongParameterList")
private suspend fun loadCexQuoteData(
exchangeProviderType: ExchangeProviderType,
networkId: String,
amount: SwapAmount,
fromTokenStatus: CryptoCurrencyStatus,
@ -1035,7 +1034,7 @@ internal class SwapInteractorImpl @Inject constructor(
)
getQuotesState(
exchangeProviderType = exchangeProviderType,
provider = provider,
quoteDataModel = quotes,
amount = amount,
fromToken = fromTokenStatus,
@ -1052,7 +1051,7 @@ internal class SwapInteractorImpl @Inject constructor(
@Suppress("LongMethod")
private suspend fun getQuotesState(
exchangeProviderType: ExchangeProviderType,
provider: SwapProvider,
quoteDataModel: Either<DataError, QuoteModel>,
amount: SwapAmount,
fromToken: CryptoCurrencyStatus,
@ -1074,6 +1073,7 @@ internal class SwapInteractorImpl @Inject constructor(
toTokenAmount = quoteModel.toTokenAmount,
swapData = null,
txFeeState = txFee,
provider = provider,
).copy(
warnings = manageWarnings(
fromTokenStatus = fromToken,
@ -1083,7 +1083,7 @@ internal class SwapInteractorImpl @Inject constructor(
),
)
when (exchangeProviderType) {
when (provider.type) {
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
val state = updatePermissionState(
networkId = networkId,
@ -1147,8 +1147,8 @@ internal class SwapInteractorImpl @Inject constructor(
): IncludeFeeInAmount {
val feeValue = when (txFee) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeIncludeOtherNativeFee
is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee
}
val feePaidCurrency = getFeePaidCurrency(
userWalletId = requireNotNull(getSelectedWallet()).walletId,
@ -1254,29 +1254,35 @@ internal class SwapInteractorImpl @Inject constructor(
providerId = provider.providerId,
rateType = RateType.FLOAT,
toAddress = toToken.value.networkAddress?.defaultAddress?.value.orEmpty(),
refundAddress = fromToken.value.networkAddress?.defaultAddress?.value,
).fold(
ifRight = { swapData ->
val feeData = transactionManager.getFee(
networkId = networkId,
amountToSend = amount.value,
currencyToSend = swapCurrencyConverter.convert(fromToken.currency),
destinationAddress = swapData.transaction.txTo,
increaseBy = INCREASE_GAS_LIMIT_BY,
data = (swapData.transaction as ExpressTransactionModel.DEX).txData,
derivationPath = fromToken.currency.network.derivationPath.value,
)
val txFeeState = when (feeData) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency)
val transaction = swapData.transaction as ExpressTransactionModel.DEX
val nativeCoinDecimals = Blockchain.fromNetworkId(networkId)?.decimals()
?: error("Blockchain not found")
val otherNativeFee = transaction.otherNativeFeeWei
?.movePointLeft(nativeCoinDecimals)
?: BigDecimal.ZERO
val txFeeState = when (val feeData = getFeeDataForDexSwap(networkId, transaction, fromToken.currency)) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee)
}
val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState)
val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeByPriority)
val feeToCheckFunds = feeByPriority + (otherNativeFee ?: BigDecimal.ZERO)
val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeToCheckFunds)
val feeState = getFeeState(
fee = feeByPriority,
spendAmount = amount,
networkId = networkId,
fromTokenStatus = fromToken,
)
val preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = true,
isBalanceEnough = isBalanceIncludeFeeEnough,
feeState = feeState,
hasOutgoingTransaction = hasOutgoingTransaction(fromToken),
includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex
)
val swapState = updateBalances(
networkId = networkId,
fromTokenStatus = fromToken,
@ -1285,6 +1291,7 @@ internal class SwapInteractorImpl @Inject constructor(
toTokenAmount = swapData.toTokenAmount,
swapData = swapData,
txFeeState = txFeeState,
provider = provider,
)
swapState.copy(
permissionState = PermissionDataState.Empty,
@ -1292,17 +1299,9 @@ internal class SwapInteractorImpl @Inject constructor(
fromTokenStatus = fromToken,
amount = amount,
feeState = txFeeState,
minAdaValue = (feeData as? ProxyFees.SingleFee)?.let {
(it.singleFee as? ProxyFee.CardanoToken)?.minAdaValue
},
),
preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = true,
isBalanceEnough = isBalanceIncludeFeeEnough,
feeState = feeState,
hasOutgoingTransaction = hasOutgoingTransaction(fromToken),
includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex
minAdaValue = null, // no ADA in DEX
),
preparedSwapConfigState = preparedSwapConfigState,
)
},
ifLeft = { error ->
@ -1322,8 +1321,42 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
private suspend fun getFeeDataForDexSwap(
networkId: String,
transaction: ExpressTransactionModel.DEX,
fromToken: CryptoCurrency,
): ProxyFees {
return try {
val nativeBalance = userWalletManager.getNativeTokenBalance(
networkId = networkId,
derivationPath = fromToken.network.derivationPath.value,
) ?: ProxyAmount.empty()
val amountToSend = createNativeAmountForDex(transaction.txValue, fromToken.network)
// transaction.txValue is always native coin
if (nativeBalance.value < amountToSend.value) {
error("It's impossible to calculate fee for nativeBalance.value < amountToSend.value")
}
transactionManager.getFee(
networkId = networkId,
amountToSend = amountToSend,
currencyToSend = swapCurrencyConverter.convert(fromToken),
destinationAddress = transaction.txTo,
increaseBy = INCREASE_GAS_LIMIT_BY,
data = transaction.txData,
derivationPath = fromToken.network.derivationPath.value,
)
} catch (e: IllegalStateException) {
transactionManager.getFeeForGas(
networkId = networkId,
gas = transaction.gas,
derivationPath = fromToken.network.derivationPath.value,
)
}
}
@Suppress("LongParameterList")
private suspend fun updateBalances(
provider: SwapProvider,
networkId: String,
fromTokenStatus: CryptoCurrencyStatus,
toTokenStatus: CryptoCurrencyStatus,
@ -1335,7 +1368,6 @@ internal class SwapInteractorImpl @Inject constructor(
val fromToken = fromTokenStatus.currency
val toToken = toTokenStatus.currency
val nativeToken = repository.getNativeTokenForNetwork(networkId)
val rates = getQuotes(fromToken.id, toToken.id, nativeToken.id)
return SwapState.QuotesLoadedState(
fromTokenInfo = TokenSwapInfo(
@ -1358,6 +1390,7 @@ internal class SwapInteractorImpl @Inject constructor(
),
swapDataModel = swapData,
txFee = txFeeState,
swapProvider = provider,
)
}
@ -1367,7 +1400,7 @@ internal class SwapInteractorImpl @Inject constructor(
): TxFeeState {
return txFeeResult?.fold(
ifLeft = { TxFeeState.Empty },
ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency) },
ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency, null) },
) ?: TxFeeState.Empty
}
@ -1427,7 +1460,7 @@ internal class SwapInteractorImpl @Inject constructor(
try {
transactionManager.getFee(
networkId = networkId,
amountToSend = BigDecimal.ZERO,
amountToSend = createNativeAmountForDex("0", fromToken.network),
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
destinationAddress = fromToken.getContractAddress(),
increaseBy = INCREASE_GAS_LIMIT_BY,
@ -1475,11 +1508,16 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(fromToken: CryptoCurrency): TxFeeState {
private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(
fromToken: CryptoCurrency,
otherNativeFee: BigDecimal? = null,
): TxFeeState {
val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO
val normalFeeValue = this.minFee.fee.value // in swap for normal use min fee
val normalFeeGas = this.minFee.gasLimit.toInt()
val priorityFeeValue = this.normalFee.fee.value // in swap for priority use normal fee
val priorityFeeGas = this.normalFee.gasLimit.toInt()
// region fees to use
val feesFiat = getFormattedFiatFees(fromToken, normalFeeValue, priorityFeeValue)
val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" }
val priorityFiatFee = requireNotNull(feesFiat.getOrNull(1)) { "feesFiat item 1 couldn't be null" }
@ -1491,12 +1529,35 @@ internal class SwapInteractorImpl @Inject constructor(
amount = priorityFeeValue,
decimals = normalFee.fee.decimals,
)
//
// region fees include otherNativeFee
val feesFiatWithNative = getFormattedFiatFees(
fromToken = fromToken,
normalFeeValue + otherNativeFeeValue,
priorityFeeValue + otherNativeFeeValue,
)
val normalFiatFeeWithNative =
requireNotNull(feesFiatWithNative.getOrNull(0)) { "feesFiat item 0 couldn't be null" }
val priorityFiatFeeWithNative =
requireNotNull(feesFiatWithNative.getOrNull(1)) { "feesFiat item 1 couldn't be null" }
val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI(
amount = normalFeeValue + otherNativeFeeValue,
decimals = minFee.fee.decimals,
)
val priorityCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI(
amount = priorityFeeValue + otherNativeFeeValue,
decimals = normalFee.fee.decimals,
)
//
return TxFeeState.MultipleFeeState(
normalFee = TxFee(
feeValue = normalFeeValue,
gasLimit = normalFeeGas,
feeFiatFormatted = normalFiatFee,
feeCryptoFormatted = normalCryptoFee,
feeIncludeOtherNativeFee = normalFeeValue + otherNativeFeeValue,
feeFiatFormattedWithNative = normalFiatFeeWithNative,
feeCryptoFormattedWithNative = normalCryptoFeeWithNative,
decimals = minFee.fee.decimals,
cryptoSymbol = minFee.fee.currencySymbol,
feeType = FeeType.NORMAL,
@ -1506,6 +1567,9 @@ internal class SwapInteractorImpl @Inject constructor(
gasLimit = priorityFeeGas,
feeFiatFormatted = priorityFiatFee,
feeCryptoFormatted = priorityCryptoFee,
feeIncludeOtherNativeFee = priorityFeeValue + otherNativeFeeValue,
feeFiatFormattedWithNative = priorityFiatFeeWithNative,
feeCryptoFormattedWithNative = priorityCryptoFeeWithNative,
decimals = normalFee.fee.decimals,
cryptoSymbol = normalFee.fee.currencySymbol,
feeType = FeeType.PRIORITY,
@ -1513,7 +1577,11 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState(fromToken: CryptoCurrency): TxFeeState {
private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState(
fromToken: CryptoCurrency,
otherNativeFee: BigDecimal? = null,
): TxFeeState {
val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO
val normalFeeValue = this.singleFee.fee.value
val normalFeeGas = this.singleFee.gasLimit.toInt()
val feesFiat = getFormattedFiatFees(fromToken, normalFeeValue)
@ -1522,12 +1590,28 @@ internal class SwapInteractorImpl @Inject constructor(
amount = normalFeeValue,
decimals = singleFee.fee.decimals,
)
// region fees include otherNativeFee
val feesFiatWithNative = getFormattedFiatFees(
fromToken = fromToken,
normalFeeValue + otherNativeFeeValue,
normalFeeValue + otherNativeFeeValue,
)
val normalFiatFeeWithNative =
requireNotNull(feesFiatWithNative.getOrNull(0)) { "feesFiat item 0 couldn't be null" }
val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI(
amount = normalFeeValue + otherNativeFeeValue,
decimals = singleFee.fee.decimals,
)
//
return TxFeeState.SingleFeeState(
fee = TxFee(
feeValue = normalFeeValue,
gasLimit = normalFeeGas,
feeFiatFormatted = normalFiatFee,
feeCryptoFormatted = normalCryptoFee,
feeIncludeOtherNativeFee = normalFeeValue + otherNativeFeeValue,
feeFiatFormattedWithNative = normalFiatFeeWithNative,
feeCryptoFormattedWithNative = normalCryptoFeeWithNative,
decimals = singleFee.fee.decimals,
cryptoSymbol = singleFee.fee.currencySymbol,
feeType = FeeType.NORMAL,
@ -1535,7 +1619,12 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
private suspend fun TransactionFee.toTxFeeState(fromToken: CryptoCurrency): TxFeeState {
@Suppress("LongMethod")
private suspend fun TransactionFee.toTxFeeState(
fromToken: CryptoCurrency,
otherNativeFee: BigDecimal?,
): TxFeeState {
val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO
return when (this) {
is TransactionFee.Choosable -> {
val normalFee = this.normal.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND)
@ -1553,12 +1642,31 @@ internal class SwapInteractorImpl @Inject constructor(
amount = feePriority,
decimals = priorityFee.amount.decimals,
)
// region otherNativeFee
val normalFeeWithOtherNative = feeNormal + otherNativeFeeValue
val priorityFeeWithOtherNative = feeNormal + otherNativeFeeValue
val normalFiatValueWithNative = getFormattedFiatFees(fromToken, normalFeeWithOtherNative)[0]
val priorityFiatValueWithNative = getFormattedFiatFees(fromToken, priorityFeeWithOtherNative)[0]
val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI(
amount = normalFeeWithOtherNative,
decimals = normalFee.amount.decimals,
)
val priorityCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI(
amount = priorityFeeWithOtherNative,
decimals = priorityFee.amount.decimals,
)
//
TxFeeState.MultipleFeeState(
normalFee = TxFee(
feeValue = feeNormal,
gasLimit = normalFee.getGasLimit(),
feeFiatFormatted = normalFiatValue,
feeCryptoFormatted = normalCryptoFee,
feeIncludeOtherNativeFee = normalFeeWithOtherNative,
feeFiatFormattedWithNative = normalFiatValueWithNative,
feeCryptoFormattedWithNative = normalCryptoFeeWithNative,
decimals = normalFee.amount.decimals,
cryptoSymbol = normalFee.amount.currencySymbol,
feeType = FeeType.NORMAL,
@ -1568,6 +1676,9 @@ internal class SwapInteractorImpl @Inject constructor(
gasLimit = priorityFee.getGasLimit(),
feeFiatFormatted = priorityFiatValue,
feeCryptoFormatted = priorityCryptoFee,
feeIncludeOtherNativeFee = priorityFeeWithOtherNative,
feeFiatFormattedWithNative = priorityFiatValueWithNative,
feeCryptoFormattedWithNative = priorityCryptoFeeWithNative,
decimals = priorityFee.amount.decimals,
cryptoSymbol = priorityFee.amount.currencySymbol,
feeType = FeeType.PRIORITY,
@ -1581,12 +1692,24 @@ internal class SwapInteractorImpl @Inject constructor(
amount = feeNormal,
decimals = this.normal.amount.decimals,
)
// region otherNativeFee
val normalFeeWithOtherNative = feeNormal + otherNativeFeeValue
val normalFiatValueWithNative = getFormattedFiatFees(fromToken, normalFeeWithOtherNative)[0]
val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI(
amount = normalFeeWithOtherNative,
decimals = this.normal.amount.decimals,
)
//
TxFeeState.SingleFeeState(
fee = TxFee(
feeValue = this.normal.amount.value ?: BigDecimal.ZERO,
gasLimit = this.normal.getGasLimit(),
feeFiatFormatted = normalFiatValue,
feeCryptoFormatted = normalCryptoFee,
feeIncludeOtherNativeFee = normalFeeWithOtherNative,
feeFiatFormattedWithNative = normalFiatValueWithNative,
feeCryptoFormattedWithNative = normalCryptoFeeWithNative,
decimals = normal.amount.decimals,
cryptoSymbol = normal.amount.currencySymbol,
feeType = FeeType.NORMAL,
@ -1596,6 +1719,18 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private fun createNativeAmountForDex(txValueAmount: String, network: Network): Amount {
val nativeDecimals = Blockchain.fromNetworkId(network.backendId)?.decimals()
?: error("Blockchain not found")
val decimalValue = txValueAmount.toBigDecimalOrNull()?.movePointLeft(nativeDecimals)
?: error("txValue parse error")
return Amount(
currencySymbol = network.currencySymbol,
value = decimalValue,
decimals = nativeDecimals,
)
}
/**
* Workaround to increase gas limit cause we calculate fee for random address
*/
@ -1656,7 +1791,7 @@ internal class SwapInteractorImpl @Inject constructor(
if (fromToken.currency is CryptoCurrency.Token) {
tokenBalance >= amount.value
} else {
tokenBalance > amount.value.plus(fee ?: BigDecimal.ZERO)
tokenBalance >= amount.value.plus(fee ?: BigDecimal.ZERO)
}
}
}

View file

@ -12,6 +12,7 @@ sealed class SwapPermissionState {
object Empty : SwapPermissionState()
data class ReadyForRequest(
val providerName: String,
val currency: String,
val amount: String,
val walletAddress: String,

View file

@ -1,7 +1,6 @@
package com.tangem.feature.swap.models
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
@ -74,18 +73,21 @@ data class SwapButton(
sealed interface TransactionCardType {
val headerResId: Int
val header: TextReference
val isError: Boolean
data class Inputtable(
val onAmountChanged: ((String) -> Unit),
val onFocusChanged: ((Boolean) -> Unit),
@StringRes override val headerResId: Int = R.string.swapping_from_title,
override val isError: Boolean,
override val header: TextReference = TextReference.Res(R.string.swapping_from_title),
) : TransactionCardType
data class ReadOnly(
val showWarning: Boolean = false,
val onWarningClick: (() -> Unit)? = null,
@StringRes override val headerResId: Int = R.string.swapping_to_title,
override val isError: Boolean = false,
override val header: TextReference = TextReference.Res(R.string.swapping_to_title),
) : TransactionCardType
}
@ -102,7 +104,7 @@ data class LegalState(
sealed interface SwapWarning {
data class PermissionNeeded(val notificationConfig: NotificationConfig) : SwapWarning
object InsufficientFunds : SwapWarning
data object InsufficientFunds : SwapWarning
data class NoAvailableTokensToSwap(val notificationConfig: NotificationConfig) : SwapWarning
data class GenericWarning(
val title: TextReference? = null,

View file

@ -51,7 +51,11 @@ internal class StateBuilder(
return SwapStateHolder(
blockchainId = networkInfo.blockchainId,
sendCardData = SwapCardState.SwapCardData(
type = TransactionCardType.Inputtable(actions.onAmountChanged, actions.onAmountSelected),
type = TransactionCardType.Inputtable(
onAmountChanged = actions.onAmountChanged,
onFocusChanged = actions.onAmountSelected,
isError = false,
),
amountEquivalent = null,
amountTextFieldValue = null,
token = null,
@ -152,9 +156,13 @@ internal class StateBuilder(
val canSelectReceiveToken = mainTokenId != toToken.id.value
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy(
isError = false,
header = TextReference.Res(R.string.swapping_from_title),
)
return uiStateHolder.copy(
sendCardData = SwapCardState.SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable),
type = sendInput,
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
amountEquivalent = null,
token = uiStateHolder.sendCardData.token,
@ -221,9 +229,19 @@ internal class StateBuilder(
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
val isInsufficientFunds = isInsufficientFundsCondition(quoteModel)
val insufficientFundsHeader = if (isInsufficientFunds) {
TextReference.Res(R.string.swapping_insufficient_funds)
} else {
TextReference.Res(R.string.swapping_from_title)
}
val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy(
isError = isInsufficientFunds,
header = insufficientFundsHeader,
)
return uiStateHolder.copy(
sendCardData = SwapCardState.SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable),
type = sendInput,
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
amountEquivalent = getFormattedFiatAmount(quoteModel.fromTokenInfo.amountFiat),
token = fromCurrencyStatus,
@ -257,6 +275,7 @@ internal class StateBuilder(
permissionState = convertPermissionState(
lastPermissionState = uiStateHolder.permissionState,
permissionDataState = quoteModel.permissionState,
providerName = quoteModel.swapProvider.name,
onGivePermissionClick = actions.onGivePermissionClick,
onChangeApproveType = actions.onChangeApproveType,
),
@ -320,7 +339,7 @@ internal class StateBuilder(
val warnings = mutableListOf<SwapWarning>()
maybeAddDomainWarnings(quoteModel, warnings)
maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings)
maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken)
maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken, quoteModel.swapProvider.name)
maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType)
maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings)
maybeAddInsufficientFundsWarning(quoteModel, warnings)
@ -468,6 +487,7 @@ internal class StateBuilder(
quoteModel: SwapState.QuotesLoadedState,
warnings: MutableList<SwapWarning>,
fromToken: CryptoCurrency,
providerName: String,
) {
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough &&
@ -475,7 +495,7 @@ internal class StateBuilder(
) {
warnings.add(
SwapWarning.PermissionNeeded(
createPermissionNotificationConfig(fromToken.symbol),
createPermissionNotificationConfig(fromToken.symbol, providerName),
),
)
}
@ -493,8 +513,8 @@ internal class StateBuilder(
warnings.add(
SwapWarning.GeneralWarning(
createNetworkFeeCoverageNotificationConfig(
fee.feeCryptoFormatted,
fee.feeFiatFormatted,
fee.feeCryptoFormattedWithNative,
fee.feeFiatFormattedWithNative,
),
),
)
@ -547,13 +567,16 @@ internal class StateBuilder(
warnings: MutableList<SwapWarning>,
) {
// check isBalanceEnough, but for dex includeFeeInAmount always Excluded
if (!quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
) {
if (isInsufficientFundsCondition(quoteModel)) {
warnings.add(SwapWarning.InsufficientFunds)
}
}
private fun isInsufficientFundsCondition(quoteModel: SwapState.QuotesLoadedState): Boolean {
return !quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
}
private fun getSwapButtonEnabled(quoteModel: SwapState.QuotesLoadedState): Boolean {
val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value
if (status is CryptoCurrencyStatus.NoAccount) {
@ -964,9 +987,9 @@ internal class StateBuilder(
return FeeItemState.Content(
feeType = feeType,
title = resourceReference(R.string.common_network_fee_title),
amountCrypto = fee.feeCryptoFormatted,
amountCrypto = fee.feeCryptoFormattedWithNative, // display fee with native as workaround for okx
symbolCrypto = fee.cryptoSymbol,
amountFiatFormatted = fee.feeFiatFormatted,
amountFiatFormatted = fee.feeFiatFormattedWithNative, // display fee with native as workaround for okx
isClickable = isClickable,
onClick = actions.onClickFee,
)
@ -1017,7 +1040,7 @@ internal class StateBuilder(
showStatusButton = shouldShowStatus,
providerIcon = providerState.iconUrl,
rate = providerState.subtitle,
fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"),
fee = stringReference("${fee.feeCryptoFormattedWithNative} (${fee.feeFiatFormattedWithNative})"),
fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()),
toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()),
fromTokenFiatAmount = stringReference(fromFiatAmount),
@ -1146,6 +1169,7 @@ internal class StateBuilder(
private fun convertPermissionState(
lastPermissionState: SwapPermissionState,
permissionDataState: PermissionDataState,
providerName: String,
onGivePermissionClick: () -> Unit,
onChangeApproveType: (ApproveType) -> Unit,
): SwapPermissionState {
@ -1165,6 +1189,7 @@ internal class StateBuilder(
is TxFeeState.SingleFeeState -> fee.fee
}
SwapPermissionState.ReadyForRequest(
providerName = providerName,
currency = permissionDataState.currency,
amount = permissionDataState.amount,
approveType = approveType,
@ -1358,18 +1383,18 @@ internal class StateBuilder(
FeeItemState.Content(
feeType = this.normalFee.feeType,
title = resourceReference(R.string.common_network_fee_title),
amountCrypto = this.normalFee.feeCryptoFormatted,
amountCrypto = this.normalFee.feeCryptoFormattedWithNative,
symbolCrypto = this.normalFee.cryptoSymbol,
amountFiatFormatted = this.normalFee.feeFiatFormatted,
amountFiatFormatted = this.normalFee.feeFiatFormattedWithNative,
isClickable = true,
onClick = {},
),
FeeItemState.Content(
feeType = this.priorityFee.feeType,
title = resourceReference(R.string.common_network_fee_title),
amountCrypto = this.priorityFee.feeCryptoFormatted,
amountCrypto = this.priorityFee.feeCryptoFormattedWithNative,
symbolCrypto = this.priorityFee.cryptoSymbol,
amountFiatFormatted = this.priorityFee.feeFiatFormatted,
amountFiatFormatted = this.priorityFee.feeFiatFormattedWithNative,
isClickable = true,
onClick = {},
),
@ -1402,12 +1427,12 @@ internal class StateBuilder(
}
// region warnings
private fun createPermissionNotificationConfig(fromTokenSymbol: String): NotificationConfig {
private fun createPermissionNotificationConfig(fromTokenSymbol: String, providerName: String): NotificationConfig {
return NotificationConfig(
title = resourceReference(R.string.express_provider_permission_needed),
subtitle = resourceReference(
id = R.string.swapping_permission_subheader,
formatArgs = wrappedList(fromTokenSymbol),
formatArgs = wrappedList(providerName, fromTokenSymbol),
),
iconResId = R.drawable.ic_locked_24,
)
@ -1604,7 +1629,7 @@ internal class StateBuilder(
id = this.providerId,
name = this.name,
iconUrl = this.imageLarge,
type = this.type.toString(),
type = this.type.providerName,
selectionType = selectionType,
alertText = alertText,
onProviderClick = onProviderClick,

View file

@ -68,6 +68,7 @@ private fun SwapPermissionBottomSheetContent(content: GivePermissionBottomSheetC
Text(
text = stringResource(
id = R.string.swapping_permission_subheader,
data.providerName,
data.currency,
),
color = TangemTheme.colors.text.secondary,
@ -303,6 +304,7 @@ private fun Preview_AgreementBottomSheet() {
private val previewData = GivePermissionBottomSheetConfig(
data = SwapPermissionState.ReadyForRequest(
providerName = "1icnh",
currency = "DAI",
amount = "",
walletAddress = "",

View file

@ -434,7 +434,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U
// region preview
private val sendCard = SwapCardState.SwapCardData(
type = TransactionCardType.Inputtable({}, {}),
type = TransactionCardType.Inputtable({}, {}, false),
amountTextFieldValue = TextFieldValue(),
amountEquivalent = "1 000 000",
tokenIconUrl = "",

View file

@ -37,6 +37,7 @@ import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
@ -185,10 +186,14 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
val title = type.headerResId
val titleColor = if (type.isError) {
TangemTheme.colors.text.warning
} else {
TangemTheme.colors.text.tertiary
}
Text(
text = stringResource(id = title),
color = TangemTheme.colors.text.tertiary,
text = type.header.resolveReference(),
color = titleColor,
maxLines = 1,
style = MaterialTheme.typography.subtitle2,
modifier = Modifier
@ -546,7 +551,7 @@ private fun Preview_TransactionCardWithoutPriceImpact_InDarkTheme() {
@Composable
private fun TransactionCardPreview() {
TransactionCard(
type = TransactionCardType.Inputtable({}, {}),
type = TransactionCardType.Inputtable({}, {}, false),
amountEquivalent = "1 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",