Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-06 18:10:51 +03:00
parent 0ac390ef93
commit f0c92b05cb
11 changed files with 243 additions and 182 deletions

View file

@ -22,6 +22,7 @@ import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.ProxyFee
import com.tangem.lib.crypto.models.ProxyNetworkInfo
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -31,7 +32,11 @@ import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.tangemSdk
import java.math.BigDecimal
import java.math.BigInteger
import java.math.MathContext
import java.math.RoundingMode
@Suppress("LargeClass")
class TransactionManagerImpl(
private val appStateHolder: AppStateHolder,
private val analytics: AnalyticsHandler,
@ -40,7 +45,7 @@ class TransactionManagerImpl(
override suspend fun sendApproveTransaction(
networkId: String,
feeAmount: BigDecimal,
estimatedGas: Int,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
): SendTxResult {
@ -53,7 +58,7 @@ class TransactionManagerImpl(
amount = amount,
blockchain = blockchain,
feeAmount = feeAmount,
estimatedGas = estimatedGas,
gasLimit = gasLimit,
destinationAddress = destinationAddress,
dataToSign = dataToSign,
)
@ -63,7 +68,7 @@ class TransactionManagerImpl(
networkId: String,
amountToSend: BigDecimal,
feeAmount: BigDecimal,
estimatedGas: Int,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
isSwap: Boolean,
@ -82,7 +87,7 @@ class TransactionManagerImpl(
amount = amount,
blockchain = blockchain,
feeAmount = feeAmount,
estimatedGas = estimatedGas,
gasLimit = gasLimit,
destinationAddress = destinationAddress,
dataToSign = dataToSign,
)
@ -94,7 +99,7 @@ class TransactionManagerImpl(
amount: Amount,
blockchain: Blockchain,
feeAmount: BigDecimal,
estimatedGas: Int,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
): SendTxResult {
@ -102,12 +107,12 @@ class TransactionManagerImpl(
amount = amount,
fee = Amount(value = feeAmount, blockchain = blockchain),
destination = destinationAddress,
).copy(hash = dataToSign, extras = createExtras(walletManager, estimatedGas, dataToSign))
).copy(hash = dataToSign, extras = createExtras(walletManager, gasLimit, dataToSign))
val signer = transactionSigner(walletManager)
val sendResult = try {
(walletManager as TransactionSender).send(txData, signer)
(walletManager as? TransactionSender)?.send(txData, signer) ?: error("Cannot cast to TransactionSender")
} catch (ex: Exception) {
FirebaseCrashlytics.getInstance().recordException(ex)
return SendTxResult.UnknownError(ex)
@ -141,19 +146,54 @@ class TransactionManagerImpl(
amountToSend: BigDecimal,
currencyToSend: Currency,
destinationAddress: String,
): ProxyAmount {
data: String?,
): ProxyFee {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain)
val fee = (walletManager as TransactionSender).getFee(
amount = createAmount(amountToSend, currencyToSend, blockchain),
destination = destinationAddress,
)
when (fee) {
is Result.Success -> {
return convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found"))
if (walletManager is EthereumWalletManager) {
val gasLimit = getGasLimit(
evmWalletManager = walletManager,
blockchain = blockchain,
amount = amountToSend,
currency = currencyToSend,
destinationAddress = destinationAddress,
data = data,
)
return when (val gasPrice = walletManager.getGasPrice()) {
is Result.Success -> {
val fee = gasLimit.multiply(gasPrice.data).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
ProxyFee(
gasLimit = gasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = fee,
decimals = blockchain.decimals(),
),
)
}
is Result.Failure -> {
error(gasPrice.error.message ?: gasPrice.error.customMessage)
}
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
} else {
val fee = (walletManager as? TransactionSender)?.getFee(
amount = createAmount(amountToSend, currencyToSend, blockchain),
destination = destinationAddress,
) ?: error("Cannot cast to TransactionSender")
when (fee) {
is Result.Success -> {
// for not EVM blockchains set gasLimit ZERO for now
return ProxyFee(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found")),
)
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
}
}
}
}
@ -167,6 +207,37 @@ class TransactionManagerImpl(
)
}
@Suppress("LongParameterList")
private suspend fun getGasLimit(
evmWalletManager: EthereumWalletManager,
blockchain: Blockchain,
amount: BigDecimal,
currency: Currency,
destinationAddress: String,
data: String?,
): BigInteger {
val result = if (data.isNullOrEmpty()) {
evmWalletManager.getGasLimit(
amount = createAmount(amount, currency, blockchain),
destination = destinationAddress,
)
} else {
evmWalletManager.getGasLimit(
amount = createAmount(amount, currency, blockchain),
destination = destinationAddress,
data = data,
)
}
when (result) {
is Result.Success -> {
return result.data
}
is Result.Failure -> {
error(result.error.message ?: result.error.customMessage)
}
}
}
private fun handleSendResult(result: SimpleResult): SendTxResult {
when (result) {
is SimpleResult.Success -> {
@ -239,14 +310,14 @@ class TransactionManagerImpl(
private fun createExtras(
walletManager: WalletManager,
estimatedGas: Int,
gasLimit: Int,
transactionHash: String,
): TransactionExtras? {
return when (walletManager) {
is EthereumWalletManager -> {
return EthereumTransactionExtras(
data = transactionHash.removePrefix(HEX_PREFIX).hexToBytes(),
gasLimit = estimatedGas.toBigInteger(),
gasLimit = gasLimit.toBigInteger(),
)
}
else -> {

View file

@ -61,10 +61,10 @@ object Versions {
// endregion Other libraries
// region Tangem
const val tangemBlockchainSdk = "develop-173"
const val tangemBlockchainSdk = "develop-174"
// const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds
const val tangemCardSdk = "develop-198"
const val tangemCardSdk = "develop-199"
// const val tangemCardSdk = "0.0.1" // Keep it! - used for local builds
// endregion Tangem

View file

@ -1,11 +1,11 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ApproveModel
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TokensDataState
import com.tangem.feature.swap.domain.models.ui.TxState
@ -42,15 +42,13 @@ interface SwapInteractor {
* Gives permission to swap, this starts scan card process
*
* @param networkId network in which selected token
* @param estimatedGas estimated gas for transaction
* @param transactionData tx data to give approve, it loaded from 1inch in findBestQuote if needed
* @param approveData tx data to give approve, it loaded from 1inch in findBestQuote if needed
* @param forTokenContractAddress token contract address for which needs permission
*/
@Throws(IllegalStateException::class)
suspend fun givePermissionToSwap(
networkId: String,
estimatedGas: Int,
transactionData: ApproveModel,
approveData: RequestApproveStateData,
forTokenContractAddress: String,
): TxState
@ -76,7 +74,7 @@ interface SwapInteractor {
* Starts swap transaction, perform sign transaction
*
* @param networkId network for tokens
* @param swapData tx data to swap, contains data to sign
* @param swapStateData tx data to swap, contains data to sign
* @param currencyToSend [Currency]
* @param currencyToGet [Currency]
* @param amountToSwap amount to swap
@ -85,7 +83,7 @@ interface SwapInteractor {
@Throws(IllegalStateException::class)
suspend fun onSwap(
networkId: String,
swapData: SwapDataModel,
swapStateData: SwapStateData,
currencyToSend: Currency,
currencyToGet: Currency,
amountToSwap: String,

View file

@ -4,10 +4,8 @@ import com.tangem.feature.swap.domain.cache.SwapDataCache
import com.tangem.feature.swap.domain.converters.CryptoCurrencyConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ApproveModel
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
import com.tangem.feature.swap.domain.models.ui.AmountFormatter
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
@ -15,6 +13,7 @@ import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.PreselectTokens
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TokenBalanceData
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
@ -110,19 +109,15 @@ internal class SwapInteractorImpl @Inject constructor(
override suspend fun givePermissionToSwap(
networkId: String,
estimatedGas: Int,
transactionData: ApproveModel,
approveData: RequestApproveStateData,
forTokenContractAddress: String,
): TxState {
val increasedEstimatedGas = increaseByPercents(TWENTY_FIVE_PERCENTS, estimatedGas)
val gasPrice = transactionData.gasPrice.toBigDecimalOrNull() ?: error("cannot parse gasPrice")
val fee = transactionManager.calculateFee(networkId, gasPrice.toPlainString(), increasedEstimatedGas)
val result = transactionManager.sendApproveTransaction(
networkId = networkId,
feeAmount = fee,
estimatedGas = estimatedGas,
destinationAddress = transactionData.toAddress,
dataToSign = transactionData.data,
feeAmount = approveData.fee,
gasLimit = approveData.gasLimit,
destinationAddress = approveData.approveModel.toAddress,
dataToSign = approveData.approveModel.data,
)
return when (result) {
is SendTxResult.Success -> {
@ -156,32 +151,9 @@ internal class SwapInteractorImpl @Inject constructor(
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
transactionManager.updateWalletManager(networkId)
}
// load initial quotes data, it works despite balance
val quotesData = loadQuoteData(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
fromToken = fromToken,
toToken = toToken,
)
// get fee from loaded quotes data, if error, use blockchain fee for 0 amount tx
val fee = getInchFee(quotesData)
val isFeeEnough = checkFeeIsEnough(
fee = fee,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken,
)
val isBalanceEnough = isBalanceEnough(fromToken, amount, fee)
val preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceEnough,
isFeeEnough = isFeeEnough,
)
return if (isAllowedToSpend && isBalanceEnough && isFeeEnough) {
// if enough balance, fee and spend was allowed, request swap data
val swapData = loadSwapData(
val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null)
return if (isAllowedToSpend && isBalanceWithoutFeeEnough) {
loadSwapData(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
@ -189,42 +161,36 @@ internal class SwapInteractorImpl @Inject constructor(
toToken = toToken,
amount = amount,
)
if (swapData is SwapState.QuotesLoadedState) {
swapData.copy(preparedSwapConfigState = preparedSwapConfigState)
} else {
swapData
}
} else {
if (quotesData is SwapState.QuotesLoadedState) {
quotesData.copy(preparedSwapConfigState = preparedSwapConfigState)
} else {
quotesData
}
loadQuoteData(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
fromToken = fromToken,
toToken = toToken,
isAllowedToSpend = isAllowedToSpend,
isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough,
)
}
}
override suspend fun onSwap(
networkId: String,
swapData: SwapDataModel,
swapStateData: SwapStateData,
currencyToSend: Currency,
currencyToGet: Currency,
amountToSwap: String,
): TxState {
val amount = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format, use only digits" }
val estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS
val fee = transactionManager.calculateFee(
networkId = networkId,
gasPrice = swapData.transaction.gasPrice,
estimatedGas = estimatedGas,
)
val result = transactionManager.sendTransaction(
networkId = networkId,
amountToSend = amount,
currencyToSend = cryptoCurrencyConverter.convert(currencyToSend),
feeAmount = fee,
estimatedGas = estimatedGas,
destinationAddress = swapData.transaction.toWalletAddress,
dataToSign = swapData.transaction.data,
feeAmount = swapStateData.fee,
gasLimit = swapStateData.gasLimit,
destinationAddress = swapStateData.swapModel.transaction.toWalletAddress,
dataToSign = swapStateData.swapModel.transaction.data,
isSwap = true,
)
return when (result) {
@ -232,8 +198,14 @@ internal class SwapInteractorImpl @Inject constructor(
userWalletManager.addToken(cryptoCurrencyConverter.convert(currencyToGet))
userWalletManager.refreshWallet()
TxState.TxSent(
fromAmount = amountFormatter.formatSwapAmountToUI(swapData.fromTokenAmount, currencyToSend.symbol),
toAmount = amountFormatter.formatSwapAmountToUI(swapData.toTokenAmount, currencyToGet.symbol),
fromAmount = amountFormatter.formatSwapAmountToUI(
swapStateData.swapModel.fromTokenAmount,
currencyToSend.symbol,
),
toAmount = amountFormatter.formatSwapAmountToUI(
swapStateData.swapModel.toTokenAmount,
currencyToGet.symbol,
),
txAddress = userWalletManager.getLastTransactionHash(networkId) ?: "",
)
}
@ -265,12 +237,6 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
private fun getInchFee(quotesData: SwapState): BigDecimal? {
return if (quotesData is SwapState.QuotesLoadedState) {
quotesData.feeRaw
} else null
}
private fun selectToToken(
initialToken: Currency,
tokensInWallet: List<Currency>,
@ -353,6 +319,8 @@ internal class SwapInteractorImpl @Inject constructor(
amount: SwapAmount,
fromToken: Currency,
toToken: Currency,
isAllowedToSpend: Boolean,
isBalanceWithoutFeeEnough: Boolean,
): SwapState {
repository.findBestQuote(
networkId = networkId,
@ -362,35 +330,25 @@ internal class SwapInteractorImpl @Inject constructor(
).let { quotes ->
val quoteDataModel = quotes.dataModel
if (quoteDataModel != null) {
val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken))
val fee = transactionManager.calculateFee(
networkId = networkId,
estimatedGas = quoteDataModel.estimatedGas,
gasPrice = transactionData.gasPrice,
)
val feeFiat = getFormattedFiatFee(networkId, fromToken.id, toToken.id, fee)
val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
amount = fee,
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat
val swapState = updateBalances(
networkId = networkId,
fromToken = fromToken,
toToken = toToken,
fromTokenAmount = quoteDataModel.fromTokenAmount,
toTokenAmount = quoteDataModel.toTokenAmount,
formattedFee = formattedFee,
feeRaw = fee,
swapDataModel = null,
swapStateData = null,
formattedFee = null,
)
return updatePermissionState(
networkId = networkId,
fromToken = fromToken,
quotesLoadedState = swapState,
estimatedGas = quoteDataModel.estimatedGas,
transactionData = transactionData,
formattedFee = formattedFee,
).copy(
preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
isFeeEnough = true,
),
)
} else {
return SwapState.SwapError(quotes.error)
@ -400,13 +358,11 @@ internal class SwapInteractorImpl @Inject constructor(
private suspend fun getFormattedFiatFee(
networkId: String,
fromTokenId: String,
toTokenId: String,
fee: BigDecimal,
): String {
val appCurrency = userWalletManager.getUserAppCurrency()
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
val rates = repository.getRates(appCurrency.code, listOf(fromTokenId, toTokenId, nativeToken.id))
val rates = repository.getRates(appCurrency.code, listOf(nativeToken.id))
return rates[nativeToken.id]?.toBigDecimal()?.let { rate ->
" (${fee.toFiatString(rate, appCurrency.symbol, true)})"
}.orEmpty()
@ -434,17 +390,26 @@ internal class SwapInteractorImpl @Inject constructor(
).let {
val swapData = it.dataModel
if (swapData != null) {
val fee = transactionManager.calculateFee(
val feeData = transactionManager.getFee(
networkId = networkId,
estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS,
gasPrice = swapData.transaction.gasPrice,
amountToSend = amount.value,
currencyToSend = cryptoCurrencyConverter.convert(fromToken),
destinationAddress = swapData.transaction.toWalletAddress,
data = swapData.transaction.data,
)
val feeFiat = getFormattedFiatFee(networkId, fromToken.id, toToken.id, fee)
val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value)
val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
amount = fee,
amount = feeData.fee.value,
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat
val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeData.fee.value)
val isFeeEnough = checkFeeIsEnough(
fee = feeData.fee.value,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken,
)
val swapState = updateBalances(
networkId = networkId,
fromToken = fromToken,
@ -452,11 +417,19 @@ internal class SwapInteractorImpl @Inject constructor(
fromTokenAmount = swapData.fromTokenAmount,
toTokenAmount = swapData.toTokenAmount,
formattedFee = formattedFee,
swapDataModel = swapData,
feeRaw = fee,
swapStateData = SwapStateData(
gasLimit = feeData.gasLimit.toInt(),
fee = feeData.fee.value,
swapModel = swapData,
),
)
return swapState.copy(
permissionState = PermissionDataState.Empty,
preparedSwapConfigState = PreparedSwapConfigState(
isAllowedToSpend = true,
isBalanceEnough = isBalanceIncludeFeeEnough,
isFeeEnough = isFeeEnough,
),
)
} else {
return SwapState.SwapError(it.error)
@ -471,9 +444,8 @@ internal class SwapInteractorImpl @Inject constructor(
toToken: Currency,
fromTokenAmount: SwapAmount,
toTokenAmount: SwapAmount,
formattedFee: String,
feeRaw: BigDecimal,
swapDataModel: SwapDataModel?,
formattedFee: String?,
swapStateData: SwapStateData?,
): SwapState.QuotesLoadedState {
val appCurrency = userWalletManager.getUserAppCurrency()
val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId)
@ -511,21 +483,18 @@ internal class SwapInteractorImpl @Inject constructor(
toRate = rates[toToken.id] ?: 0.0,
),
networkCurrency = userWalletManager.getNetworkCurrency(networkId),
swapDataModel = swapDataModel,
swapDataModel = swapStateData,
tangemFee = getTangemFee(),
feeRaw = feeRaw,
)
}
@Suppress("LongParameterList")
private fun updatePermissionState(
private suspend fun updatePermissionState(
networkId: String,
fromToken: Currency,
quotesLoadedState: SwapState.QuotesLoadedState,
estimatedGas: Int,
transactionData: ApproveModel,
formattedFee: String,
): SwapState.QuotesLoadedState {
// if token balance ZERO not show permission state to avoid user to spend money for fee
val isTokenZeroBalance = getTokenBalance(fromToken).value.compareTo(BigDecimal.ZERO) == 0
if (isTokenZeroBalance) {
return quotesLoadedState.copy(
@ -537,6 +506,20 @@ internal class SwapInteractorImpl @Inject constructor(
permissionState = PermissionDataState.PermissionLoading,
)
}
val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken))
val feeData = transactionManager.getFee(
networkId = networkId,
amountToSend = BigDecimal.ZERO,
currencyToSend = userWalletManager.getNativeTokenForNetwork(networkId),
destinationAddress = transactionData.toAddress,
data = transactionData.data,
)
val feeFiat = getFormattedFiatFee(networkId, feeData.fee.value)
val formattedFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feeData.fee.value,
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat
return quotesLoadedState.copy(
permissionState = PermissionDataState.PermissionReadyForRequest(
currency = fromToken.symbol,
@ -545,7 +528,8 @@ internal class SwapInteractorImpl @Inject constructor(
spenderAddress = transactionData.toAddress,
fee = formattedFee,
requestApproveData = RequestApproveStateData(
estimatedGas = estimatedGas,
fee = feeData.fee.value,
gasLimit = feeData.gasLimit.toInt(),
approveModel = transactionData,
),
),
@ -613,11 +597,6 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
@Suppress("MagicNumber")
private fun increaseByPercents(percents: Int, value: Int): Int {
return value * (percents / 100 + 1)
}
private fun toBigDecimalOrNull(amountToSwap: String): BigDecimal? {
return amountToSwap.replace(",", ".").toBigDecimalOrNull()
}
@ -636,9 +615,7 @@ internal class SwapInteractorImpl @Inject constructor(
companion object {
private const val DEFAULT_SLIPPAGE = 2
private const val ZERO_BALANCE = "0"
private const val DEFAULT_GAS = 300000
private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
private const val TWENTY_FIVE_PERCENTS = 25
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.4
private const val USDT_SYMBOL = "USDT"
private const val USDC_SYMBOL = "USDC"

View file

@ -12,7 +12,7 @@ sealed interface SwapState {
data class QuotesLoadedState(
val fromTokenInfo: TokenSwapInfo,
val toTokenInfo: TokenSwapInfo,
val fee: String,
val fee: String?,
val priceImpact: Float,
val networkCurrency: String,
val preparedSwapConfigState: PreparedSwapConfigState = PreparedSwapConfigState(
@ -21,9 +21,8 @@ sealed interface SwapState {
isFeeEnough = false,
),
val permissionState: PermissionDataState = PermissionDataState.Empty,
val swapDataModel: SwapDataModel? = null,
val swapDataModel: SwapStateData? = null,
val tangemFee: Double,
val feeRaw: BigDecimal,
) : SwapState
data class EmptyAmountState(
@ -61,6 +60,13 @@ data class TokenSwapInfo(
)
data class RequestApproveStateData(
val estimatedGas: Int,
val fee: BigDecimal,
val gasLimit: Int,
val approveModel: ApproveModel,
)
data class SwapStateData(
val fee: BigDecimal,
val gasLimit: Int,
val swapModel: SwapDataModel,
)

View file

@ -131,9 +131,9 @@ internal class StateBuilder(val actions: UiActions) {
warnings.add(SwapWarning.HighPriceImpact((quoteModel.priceImpact * HUNDRED_PERCENTS).toInt()))
}
val feeState = if (quoteModel.preparedSwapConfigState.isFeeEnough) {
FeeState.Loaded(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee)
FeeState.Loaded(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee ?: "")
} else {
FeeState.NotEnoughFundsWarning(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee)
FeeState.NotEnoughFundsWarning(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee ?: "")
}
return uiStateHolder.copy(
sendCardData = SwapCardData(

View file

@ -255,12 +255,14 @@ private fun FeeItem(feeState: FeeState, currency: String) {
val disclaimer = stringResource(id = R.string.swapping_tangem_fee_disclaimer, "${feeState.tangemFee}%")
when (feeState) {
is FeeState.Loaded -> {
SmallInfoCardWithDisclaimer(
startText = titleString,
endText = feeState.fee,
disclaimer = disclaimer,
isLoading = false,
)
if (feeState.fee.isNotEmpty()) {
SmallInfoCardWithDisclaimer(
startText = titleString,
endText = feeState.fee,
disclaimer = disclaimer,
isLoading = false,
)
}
}
FeeState.Loading -> {
SmallInfoCardWithDisclaimer(
@ -271,16 +273,18 @@ private fun FeeItem(feeState: FeeState, currency: String) {
)
}
is FeeState.NotEnoughFundsWarning -> {
SmallInfoCardWithWarning(
startText = titleString,
endText = feeState.fee,
disclaimer = disclaimer,
warningText = stringResource(
id = R.string.swapping_not_enough_funds_for_fee,
currency,
currency,
),
)
if (feeState.fee.isNotEmpty()) {
SmallInfoCardWithWarning(
startText = titleString,
endText = feeState.fee,
disclaimer = disclaimer,
warningText = stringResource(
id = R.string.swapping_not_enough_funds_for_fee,
currency,
currency,
),
)
}
}
is FeeState.Empty -> {
SmallInfoCard(startText = titleString, endText = "")

View file

@ -1,15 +1,14 @@
package com.tangem.feature.swap.viewmodels
import com.tangem.feature.swap.domain.models.domain.ApproveModel
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapStateData
data class SwapProcessDataState(
val networkId: String,
val fromCurrency: Currency? = null,
val toCurrency: Currency? = null,
val amount: String? = null,
val estimatedGas: Int? = null,
val approveModel: ApproveModel? = null,
val swapModel: SwapDataModel? = null,
val approveDataModel: RequestApproveStateData? = null,
val swapDataModel: SwapStateData? = null,
)

View file

@ -12,11 +12,11 @@ import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TxState
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.UiActions
@ -169,9 +169,8 @@ internal class SwapViewModel @Inject constructor(
runCatching(dispatchers.io) {
dataState = dataState.copy(
amount = amount,
swapModel = null,
estimatedGas = null,
approveModel = null,
swapDataModel = null,
approveDataModel = null,
)
swapInteractor.findBestQuote(
networkId = dataState.networkId,
@ -210,15 +209,14 @@ internal class SwapViewModel @Inject constructor(
)
}
private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapDataModel?) {
private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapStateData?) {
dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) {
dataState.copy(
estimatedGas = permissionState.requestApproveData.estimatedGas,
approveModel = permissionState.requestApproveData.approveModel,
approveDataModel = permissionState.requestApproveData,
)
} else {
dataState.copy(
swapModel = swapDataModel,
swapDataModel = swapDataModel,
)
}
}
@ -230,7 +228,7 @@ internal class SwapViewModel @Inject constructor(
runCatching(dispatchers.io) {
swapInteractor.onSwap(
networkId = dataState.networkId,
swapData = requireNotNull(dataState.swapModel),
swapStateData = requireNotNull(dataState.swapDataModel),
currencyToSend = requireNotNull(dataState.fromCurrency),
currencyToGet = requireNotNull(dataState.toCurrency),
amountToSwap = requireNotNull(dataState.amount),
@ -276,8 +274,7 @@ internal class SwapViewModel @Inject constructor(
runCatching(dispatchers.io) {
swapInteractor.givePermissionToSwap(
networkId = dataState.networkId,
estimatedGas = dataState.estimatedGas!!,
transactionData = dataState.approveModel!!,
approveData = dataState.approveDataModel!!,
forTokenContractAddress = (dataState.fromCurrency as? Currency.NonNativeToken)?.contractAddress
?: "",
)

View file

@ -1,7 +1,7 @@
package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.ProxyFee
import com.tangem.lib.crypto.models.ProxyNetworkInfo
import com.tangem.lib.crypto.models.transactions.SendTxResult
import java.math.BigDecimal
@ -12,7 +12,7 @@ interface TransactionManager {
suspend fun sendApproveTransaction(
networkId: String,
feeAmount: BigDecimal,
estimatedGas: Int,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
): SendTxResult
@ -23,7 +23,7 @@ interface TransactionManager {
networkId: String,
amountToSend: BigDecimal,
feeAmount: BigDecimal,
estimatedGas: Int,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
isSwap: Boolean,
@ -36,7 +36,8 @@ interface TransactionManager {
amountToSend: BigDecimal,
currencyToSend: Currency,
destinationAddress: String,
): ProxyAmount
data: String?,
): ProxyFee
@Throws(IllegalStateException::class)
fun getNativeTokenDecimals(networkId: String): Int

View file

@ -0,0 +1,8 @@
package com.tangem.lib.crypto.models
import java.math.BigInteger
data class ProxyFee(
val gasLimit: BigInteger,
val fee: ProxyAmount,
)