Updated on 2026-08-14
This commit is contained in:
parent
396760e61f
commit
40cbbfaa41
11 changed files with 243 additions and 182 deletions
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = "")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
?: "",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.lib.crypto.models
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
data class ProxyFee(
|
||||
val gasLimit: BigInteger,
|
||||
val fee: ProxyAmount,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue