Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-23 14:39:47 +03:00
parent 6936f04093
commit c7151d95a7
20 changed files with 335 additions and 1096 deletions

View file

@ -24,7 +24,7 @@ internal class RuntimeUserWalletsStore(
return userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == key }
}
override suspend fun getSyncStrict(key: UserWalletId): UserWallet {
override fun getSyncStrict(key: UserWalletId): UserWallet {
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
}

View file

@ -30,6 +30,12 @@ internal object TransactionDomainModule {
)
}
@Provides
@Singleton
fun provideGetEthSpecificFeeUseCase(walletManagersFacade: WalletManagersFacade): GetEthSpecificFeeUseCase {
return GetEthSpecificFeeUseCase(walletManagersFacade = walletManagersFacade)
}
@Provides
@Singleton
fun provideTransferGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetTransferFeeUseCase {

View file

@ -1,367 +0,0 @@
package com.tangem.tap.proxy
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.smartcontract.SmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.externallinkprovider.TxExploreState
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.models.*
import java.math.BigDecimal
import java.math.BigInteger
import java.math.MathContext
import java.math.RoundingMode
@Suppress("LargeClass")
class TransactionManagerImpl(
private val walletManagersFacade: WalletManagersFacade,
private val userWalletsListManager: UserWalletsListManager,
) : TransactionManager {
override fun getExplorerTransactionLink(networkId: String, txAddress: String): String {
val blockchain = Blockchain.fromNetworkId(networkId) ?: error("blockchain not found")
return when (val txUrlState = blockchain.getExploreTxUrl(txAddress)) {
TxExploreState.Unsupported -> ""
is TxExploreState.Url -> txUrlState.url
}
}
override suspend fun updateWalletManager(networkId: String, derivationPath: String?) {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
getActualWalletManager(blockchain, derivationPath).update()
}
@Throws(IllegalStateException::class)
override suspend fun getFee(
networkId: String,
amountToSend: Amount,
currencyToSend: Currency,
destinationAddress: String,
increaseBy: Int?,
callData: SmartContractCallData?,
derivationPath: String?,
): ProxyFees {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
if (walletManager is EthereumWalletManager) {
if (walletManager is EthereumOptimisticRollupWalletManager) {
return getFeeForOptimismBlockchain(
walletManager = walletManager,
amount = amountToSend,
destinationAddress = destinationAddress,
callData = callData,
)
}
return getFeeForEthereumBlockchain(
walletManager = walletManager,
blockchain = blockchain,
amountToSend = amountToSend,
destinationAddress = destinationAddress,
callData = callData,
increaseBy = increaseBy,
)
} else {
return getFeeForBlockchain(
walletManager = walletManager,
amountToSend = amountToSend,
destinationAddress = destinationAddress,
)
}
}
override fun getBlockchainInfo(networkId: String): ProxyNetworkInfo {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
return ProxyNetworkInfo(
name = blockchain.fullName,
blockchainId = blockchain.id,
)
}
private suspend fun getFeeForBlockchain(
walletManager: WalletManager,
amountToSend: Amount,
destinationAddress: String,
): ProxyFees {
val fee = (walletManager as? TransactionSender)?.getFee(
amount = amountToSend,
destination = destinationAddress,
) ?: error("Cannot cast to TransactionSender")
return when (fee) {
is Result.Success -> {
// for not EVM blockchains set gasLimit ZERO for now
when (fee.data) {
is TransactionFee.Single -> {
val singleFee = when (val normalFee = (fee.data as TransactionFee.Single).normal) {
is Fee.CardanoToken -> {
ProxyFee.CardanoToken(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = normalFee.amount),
minAdaValue = normalFee.minAdaValue,
)
}
is Fee.Filecoin -> {
ProxyFee.Filecoin(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = normalFee.amount),
gasPremium = normalFee.gasPremium,
)
}
is Fee.Sui -> {
ProxyFee.Sui(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = normalFee.amount),
gasPrice = normalFee.gasPrice,
gasBudget = normalFee.gasBudget,
)
}
else -> {
ProxyFee.Common(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = normalFee.amount),
)
}
}
ProxyFees.SingleFee(singleFee = singleFee)
}
is TransactionFee.Choosable -> {
val choosableFee = fee.data as TransactionFee.Choosable
ProxyFees.MultipleFees(
minFee = ProxyFee.Common(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = choosableFee.minimum.amount),
),
normalFee = ProxyFee.Common(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = choosableFee.normal.amount),
),
priorityFee = ProxyFee.Common(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(amount = choosableFee.priority.amount),
),
)
}
}
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
}
}
}
@Suppress("LongParameterList")
private suspend fun getFeeForEthereumBlockchain(
walletManager: EthereumWalletManager,
blockchain: Blockchain,
amountToSend: Amount,
destinationAddress: String,
callData: SmartContractCallData?,
increaseBy: Int?,
): ProxyFees {
val gasLimit = getGasLimit(
evmWalletManager = walletManager,
amount = amountToSend,
destinationAddress = destinationAddress,
callData = callData,
).increaseBigIntegerByPercents(increaseBy)
return when (val gasPrice = walletManager.getGasPrice()) {
is Result.Success -> {
createMultipleProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain)
}
is Result.Failure -> {
error(gasPrice.error.message ?: gasPrice.error.customMessage)
}
}
}
private suspend fun getFeeForOptimismBlockchain(
walletManager: EthereumOptimisticRollupWalletManager,
amount: Amount,
destinationAddress: String,
callData: SmartContractCallData?,
): ProxyFees {
val fee = if (callData == null) {
walletManager.getFee(amount, destinationAddress)
} else {
walletManager.getFee(amount, destinationAddress, callData)
}
return when (fee) {
is Result.Success -> {
val choosableFee = fee.data as? TransactionFee.Choosable ?: error("Incorrect fee type")
val minProxyFee = ProxyFee.Common(
gasLimit = (choosableFee.minimum as Fee.Ethereum).gasLimit,
fee = convertToProxyAmount(amount = choosableFee.minimum.amount),
)
val normalProxyFee = ProxyFee.Common(
gasLimit = (choosableFee.normal as Fee.Ethereum).gasLimit,
fee = convertToProxyAmount(amount = choosableFee.normal.amount),
)
val priorityProxyFee = ProxyFee.Common(
gasLimit = (choosableFee.priority as Fee.Ethereum).gasLimit,
fee = convertToProxyAmount(amount = choosableFee.priority.amount),
)
ProxyFees.MultipleFees(
minFee = minProxyFee,
normalFee = normalProxyFee,
priorityFee = priorityProxyFee,
)
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
}
}
}
private suspend fun getGasLimit(
evmWalletManager: EthereumWalletManager,
amount: Amount,
destinationAddress: String,
callData: SmartContractCallData?,
): BigInteger {
val result = if (callData == null) {
evmWalletManager.getGasLimit(
amount = amount,
destination = destinationAddress,
)
} else {
evmWalletManager.getGasLimit(
amount = amount,
destination = destinationAddress,
callData = callData,
)
}
when (result) {
is Result.Success -> {
return result.data
}
is Result.Failure -> {
error(result.error.message ?: result.error.customMessage)
}
}
}
override suspend fun getFeeForGas(networkId: String, gas: BigInteger, derivationPath: String?): ProxyFees {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
val gasPriceResult = (walletManager as? EthereumWalletManager)?.getGasPrice()
?: error("not supported for $blockchain")
val gasPrice = when (gasPriceResult) {
is Result.Failure -> error("fail to receive gasPrice")
is Result.Success -> gasPriceResult.data
}
return createMultipleProxyFees(gasPrice, gas, blockchain)
}
private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
) { "userWallet or userWalletsListManager is null" }
val walletManager = walletManagersFacade.getOrCreateWalletManager(
selectedUserWallet.walletId,
blockchain,
derivationPath,
)
return requireNotNull(walletManager) { "no wallet manager found" }
}
/**
* Create proxy fees
*
* @param gasPrice min fee gasPrice
* @param gasLimit
* @param blockchain
*/
private fun createMultipleProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees {
val patchedGasLimit = gasLimit.toBigDecimal().increaseForMantleIfNeeded(blockchain).toBigInteger()
val gasPriceNormal = gasPrice
.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE)
val gasPricePriority = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE)
val feeMin = patchedGasLimit.multiply(gasPrice).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
).increaseForMantleIfNeeded(blockchain)
val feeNormal = patchedGasLimit.multiply(gasPriceNormal).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
).increaseForMantleIfNeeded(blockchain)
val feePriority = patchedGasLimit.multiply(gasPricePriority).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
).increaseForMantleIfNeeded(blockchain)
val minFee = ProxyFee.Common(
gasLimit = patchedGasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feeMin,
decimals = blockchain.decimals(),
),
)
val normalFee = ProxyFee.Common(
gasLimit = patchedGasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feeNormal,
decimals = blockchain.decimals(),
),
)
val priorityFee = ProxyFee.Common(
gasLimit = patchedGasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = feePriority,
decimals = blockchain.decimals(),
),
)
return ProxyFees.MultipleFees(
minFee = minFee,
normalFee = normalFee,
priorityFee = priorityFee,
)
}
private fun convertToProxyAmount(amount: Amount): ProxyAmount {
return ProxyAmount(
currencySymbol = amount.currencySymbol,
value = amount.value ?: BigDecimal.ZERO,
decimals = amount.decimals,
)
}
/**
* Increase big integer by percents
*
* @param percents in format 150 -> 50%
* @return increased value
*/
private fun BigInteger.increaseBigIntegerByPercents(percents: Int?): BigInteger {
return if (percents != null && percents != 0) {
this.multiply(percents.toBigInteger()).divide(BigInteger("100"))
} else {
this
}
}
// TODO Workaround for Mantle. Remove after [REDACTED_JIRA]
private fun BigDecimal.increaseForMantleIfNeeded(blockchain: Blockchain): BigDecimal {
return if (blockchain == Blockchain.Mantle) {
this.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER)
} else {
this
}
}
companion object {
private const val MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE = 150 // 50%
private const val MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE = 200 // 50%
private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8")
}
}

View file

@ -2,10 +2,8 @@ package com.tangem.tap.proxy.di
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.TransactionManagerImpl
import com.tangem.tap.proxy.UserWalletManagerImpl
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -37,16 +35,4 @@ internal object ProxyModule {
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideTransactionManager(
walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager,
): TransactionManager {
return TransactionManagerImpl(
walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager,
)
}
}

View file

@ -13,7 +13,7 @@ interface UserWalletsStore {
fun getSyncOrNull(key: UserWalletId): UserWallet?
suspend fun getSyncStrict(key: UserWalletId): UserWallet
fun getSyncStrict(key: UserWalletId): UserWallet
suspend fun getAllSyncOrNull(): List<UserWallet>?

View file

@ -277,10 +277,7 @@ class DefaultWalletManagersFacade(
}
}
private suspend fun getUserWallet(userWalletId: UserWalletId) =
requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"
}
private fun getUserWallet(userWalletId: UserWalletId) = userWalletsStore.getSyncStrict(userWalletId)
private suspend fun getAndUpdateWalletManager(
userWallet: UserWallet,

View file

@ -1,7 +1,9 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData
import com.tangem.blockchain.common.smartcontract.SmartContractCallData
import com.tangem.common.extensions.hexToBytes
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.TransactionRepository
@ -12,14 +14,38 @@ class CreateTransactionDataExtrasUseCase(
) {
operator fun invoke(data: String, network: Network, gasLimit: BigInteger? = null, nonce: BigInteger? = null) =
Either.catch {
requireNotNull(
either {
catch(
{
transactionRepository.createTransactionDataExtras(
callData = CompiledSmartContractCallData(data.hexToBytes()),
network = network,
nonce = nonce,
gasLimit = gasLimit,
),
) { "Failed to create transaction" }
)
},
) {
raise(it)
}
}
operator fun invoke(
callData: SmartContractCallData,
network: Network,
gasLimit: BigInteger? = null,
nonce: BigInteger? = null,
) = either {
catch(
{
transactionRepository.createTransactionDataExtras(
callData = callData,
network = network,
nonce = nonce,
gasLimit = gasLimit,
)
},
) {
raise(it)
}
}
}

View file

@ -0,0 +1,128 @@
package com.tangem.domain.transaction.usecase
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import java.math.BigInteger
import java.math.MathContext
import java.math.RoundingMode
/**
* Use case to get transaction fee for ETH when we have gas from other services
*
*/
class GetEthSpecificFeeUseCase(
private val walletManagersFacade: WalletManagersFacade,
) {
suspend operator fun invoke(
userWallet: UserWallet,
cryptoCurrency: CryptoCurrency,
gasLimit: BigInteger,
gasPrice: BigInteger? = null,
) = either {
catch(
block = {
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
)
val gasPriceResult = gasPrice
?: (walletManager as? EthereumWalletManager)?.getGasPriceValue()
?: error("not supported for ${cryptoCurrency.network}")
val blockchain = Blockchain.fromNetworkId(networkId = cryptoCurrency.network.backendId)
?: error("unknown networkId ${cryptoCurrency.network.backendId}")
val minimalFee = getEthLegacyFee(
gasPrice = gasPriceResult,
gasLimit = gasLimit,
decimals = cryptoCurrency.decimals,
blockchain = blockchain,
)
val normalGasPrice = gasPriceResult.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE)
val normalFee = getEthLegacyFee(
gasPrice = normalGasPrice,
gasLimit = gasLimit,
decimals = cryptoCurrency.decimals,
blockchain = blockchain,
)
val priorityGasPrice = gasPriceResult.increaseBigIntegerByPercents(
MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE,
)
val priorityFee = getEthLegacyFee(
gasPrice = priorityGasPrice,
gasLimit = gasLimit,
decimals = cryptoCurrency.decimals,
blockchain = blockchain,
)
TransactionFee.Choosable(
minimum = minimalFee,
normal = normalFee,
priority = priorityFee,
)
},
catch = {
raise(GetFeeError.DataError(it))
},
)
}
private fun getEthLegacyFee(
gasPrice: BigInteger,
decimals: Int,
gasLimit: BigInteger,
blockchain: Blockchain,
): Fee.Ethereum.Legacy {
val amount = Amount(
value = gasLimit.multiply(gasPrice).toBigDecimal(
scale = decimals,
mathContext = MathContext(decimals, RoundingMode.HALF_EVEN),
),
blockchain = blockchain,
)
return Fee.Ethereum.Legacy(
amount = amount,
gasLimit = gasLimit,
gasPrice = gasPrice,
)
}
private suspend fun EthereumWalletManager.getGasPriceValue(): BigInteger {
return when (val gasPrice = this.getGasPrice()) {
is Result.Failure -> throw gasPrice.error
is Result.Success -> gasPrice.data
}
}
/**
* Increase big integer by percents
*
* @param percents in format 150 -> 50%
* @return increased value
*/
private fun BigInteger.increaseBigIntegerByPercents(percents: Int?): BigInteger {
return if (percents != null && percents != 0) {
this.multiply(percents.toBigInteger()).divide(BigInteger("100"))
} else {
this
}
}
private companion object {
const val MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE = 150 // 50%
const val MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE = 200 // 50%
}
}

View file

@ -19,6 +19,7 @@ dependencies {
implementation(projects.core.datasource)
/** Other Libraries **/
implementation(tangemDeps.blockchain)
implementation(deps.kotlin.serialization)
implementation(deps.arrow.core)
implementation(deps.moshi.kotlin)

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.domain.models.ui
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.feature.swap.domain.models.ExpressDataError
@ -86,11 +87,6 @@ data class RequestApproveStateData(
val spenderAddress: String,
)
// data class SwapStateData(
// val fee: TxFeeState,
// val swapModel: SwapDataModel,
// )
sealed class TxFeeState {
data class MultipleFeeState(
val normalFee: TxFee,
@ -114,34 +110,15 @@ sealed class TxFeeState {
data class TxFee(
val feeValue: BigDecimal,
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,
val params: Params?,
) {
sealed class Params {
data class Filecoin(
val gasPremium: Long,
) : Params()
data class Sui(
val gasBudget: Long,
val gasPrice: Long,
) : Params()
data class Hedera(
val additionalHBARFee: BigDecimal,
) : Params()
}
}
val fee: Fee,
)
enum class FeeType {
NORMAL, PRIORITY

View file

@ -1,15 +0,0 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
interface BlockchainInteractor {
/**
* In app blockchain id, actual in blockchain sdk, not the same as networkId
*
* workaround till not use backend only and not integrated server vs sdk
*/
fun getBlockchainInfo(networkId: String): NetworkInfo
fun getExplorerTransactionLink(networkId: String, txHash: String): String
}

View file

@ -1,23 +0,0 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
import com.tangem.lib.crypto.TransactionManager
import javax.inject.Inject
internal class DefaultBlockchainInteractor @Inject constructor(
private val transactionManager: TransactionManager,
) : BlockchainInteractor {
override fun getBlockchainInfo(networkId: String): NetworkInfo {
return transactionManager.getBlockchainInfo(networkId).let {
NetworkInfo(
name = it.name,
blockchainId = it.blockchainId,
)
}
}
override fun getExplorerTransactionLink(networkId: String, txHash: String): String {
return transactionManager.getExplorerTransactionLink(networkId, txHash)
}
}

View file

@ -3,16 +3,13 @@ package com.tangem.feature.swap.domain
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Blockchain.*
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionExtras
import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData
import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.extensions.hexToBytes
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -20,6 +17,7 @@ import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.quotes.QuotesRepositoryV2
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.GetCurrencyCheckUseCase
import com.tangem.domain.tokens.TokensFeatureToggles
@ -35,17 +33,13 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.ProxyFee
import com.tangem.lib.crypto.models.ProxyFees
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -57,11 +51,11 @@ import java.math.RoundingMode
@Suppress("LargeClass", "LongParameterList")
internal class SwapInteractorImpl @AssistedInject constructor(
private val transactionManager: TransactionManager,
private val userWalletManager: UserWalletManager,
private val repository: SwapRepository,
private val allowPermissionsHandler: AllowPermissionsHandler,
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val createTransactionUseCase: CreateTransactionUseCase,
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
@ -78,6 +72,8 @@ internal class SwapInteractorImpl @AssistedInject constructor(
private val initialToCurrencyResolver: InitialToCurrencyResolver,
private val validateTransactionUseCase: ValidateTransactionUseCase,
private val estimateFeeUseCase: EstimateFeeUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val getEthSpecificFeeUseCase: GetEthSpecificFeeUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
private val amountFormatter: AmountFormatter,
@ -88,7 +84,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
GetSelectedAppCurrencyUseCase(appCurrencyRepository)
}
private val swapCurrencyConverter = SwapCurrencyConverter()
private val hundredPercent = BigInteger("100")
private val userWallet
@ -218,10 +213,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
val approveTransaction = createApprovalTransactionUseCase(
fee = getFeeForTransaction(
fee = permissionOptions.txFee,
blockchain = Blockchain.fromId(permissionOptions.fromToken.network.id.value),
),
fee = permissionOptions.txFee.fee,
userWalletId = userWalletId,
cryptoCurrency = permissionOptions.fromToken as CryptoCurrency.Token,
amount = amount?.value,
@ -338,10 +330,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
transactionManager.updateWalletManager(
networkId,
fromToken.currency.network.derivationPath.value,
)
fetchCurrencyStatusUseCase(userWalletId, fromToken.currency.id, true)
}
return if (isAllowedToSpend && isBalanceWithoutFeeEnough) {
provider to loadDexSwapData(
@ -538,7 +527,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
swapData = requireNotNull(swapData),
currencyToSendStatus = currencyToSend,
currencyToGetStatus = currencyToGet,
fee = fee,
txFee = fee,
amountToSwap = amountToSwap,
)
}
@ -593,7 +582,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
currencyToSendStatus: CryptoCurrencyStatus,
currencyToGetStatus: CryptoCurrencyStatus,
amountToSwap: String,
fee: TxFee,
txFee: TxFee,
): SwapTransactionState {
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals)
@ -603,15 +592,12 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val amountToSend = createNativeAmountForDex(swapData.transaction.txValue, currencyToSendStatus.currency.network)
val txData = createTransactionUseCase(
amount = amountToSend,
fee = getFeeForTransaction(
fee = fee,
blockchain = Blockchain.fromId(currencyToSendStatus.currency.network.id.value),
),
fee = txFee.fee,
memo = null,
destination = swapData.transaction.txTo,
userWalletId = userWalletId,
network = currencyToSendStatus.currency.network,
txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, fee.gasLimit),
txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, txFee.fee.getGasLimit()),
).getOrElse {
Timber.e(it, "Failed to create swap dex tx data")
return SwapTransactionState.Error.UnknownError
@ -706,10 +692,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val txData = createTransferTransactionUseCase(
amount = amount.value.convertToSdkAmount(currencyToSend.currency),
fee = getFeeForTransaction(
fee = txFee,
blockchain = Blockchain.fromId(currencyToSend.currency.network.id.value),
),
fee = txFee.fee,
memo = exchangeDataCex.txExtraId,
destination = exchangeDataCex.txTo,
userWalletId = userWalletId,
@ -777,235 +760,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
)
}
@Suppress("LongMethod")
private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee {
val feeAmountValue = fee.feeValue
val feeAmount = Amount(
value = fee.feeValue,
currencySymbol = fee.cryptoSymbol,
decimals = fee.decimals,
type = AmountType.Coin,
)
return if (blockchain.isEvm()) {
val feeAmountWithDecimals = feeAmountValue.movePointRight(fee.decimals)
Fee.Ethereum.Legacy(
amount = feeAmount,
gasLimit = fee.gasLimit.toBigInteger(),
gasPrice = (feeAmountWithDecimals / fee.gasLimit.toBigDecimal()).toBigInteger(),
)
} else {
when (blockchain) {
// region Blockchains with their own fees
VeChain,
VeChainTestnet,
-> {
Fee.VeChain(
amount = feeAmount,
gasPriceCoef = Fee.VeChain.getGasPriceCoef(fee.gasLimit.toLong(), fee.feeValue),
gasLimit = fee.gasLimit.toLong(),
)
}
Aptos,
AptosTestnet,
-> {
val gasUnitPrice = fee.feeValue.divide(
fee.gasLimit.toBigDecimal(),
Aptos.decimals(),
RoundingMode.HALF_UP,
)
Fee.Aptos(
amount = feeAmount,
gasUnitPrice = gasUnitPrice
.movePointRight(Aptos.decimals())
.toLong(),
gasLimit = fee.gasLimit.toLong(),
)
}
Filecoin,
-> {
val gasUnitPrice = fee.feeValue.divide(
BigDecimal(fee.gasLimit),
Filecoin.decimals(),
RoundingMode.HALF_UP,
)
val feeParams = requireNotNull(fee.params as? TxFee.Params.Filecoin)
Fee.Filecoin(
amount = feeAmount,
gasUnitPrice = gasUnitPrice
.movePointRight(Filecoin.decimals())
.toLong(),
gasLimit = fee.gasLimit.toLong(),
gasPremium = feeParams.gasPremium,
)
}
Sui,
SuiTestnet,
-> {
val feeParams = requireNotNull(fee.params as? TxFee.Params.Sui)
Fee.Sui(
amount = feeAmount,
gasPrice = feeParams.gasPrice,
gasBudget = feeParams.gasBudget,
)
}
Hedera,
HederaTestnet,
-> {
val feeParams = requireNotNull(fee.params as? TxFee.Params.Hedera)
Fee.Hedera(amount = feeAmount, additionalHBARFee = feeParams.additionalHBARFee)
}
// endregion
// region Blockchains with common fees or EVM-like fees
Unknown,
Arbitrum,
ArbitrumTestnet,
Avalanche,
AvalancheTestnet,
Binance,
BinanceTestnet,
BSC,
BSCTestnet,
Bitcoin,
BitcoinTestnet,
BitcoinCash,
BitcoinCashTestnet,
Cardano,
Cosmos,
CosmosTestnet,
Dogecoin,
Ducatus,
Ethereum,
EthereumTestnet,
EthereumClassic,
EthereumClassicTestnet,
Fantom,
FantomTestnet,
Litecoin,
Near,
NearTestnet,
Polkadot,
PolkadotTestnet,
Kava,
KavaTestnet,
Kusama,
Polygon,
PolygonTestnet,
RSK,
Sei,
SeiTestnet,
Stellar,
StellarTestnet,
Solana,
SolanaTestnet,
Tezos,
Tron,
TronTestnet,
XRP,
Gnosis,
Dash,
Optimism,
OptimismTestnet,
Dischain,
EthereumPow,
EthereumPowTestnet,
Kaspa,
KaspaTestnet,
Telos,
TelosTestnet,
TON,
TONTestnet,
Ravencoin,
RavencoinTestnet,
TerraV1,
TerraV2,
Cronos,
AlephZero,
AlephZeroTestnet,
OctaSpace,
OctaSpaceTestnet,
Chia,
ChiaTestnet,
Decimal,
DecimalTestnet,
XDC,
XDCTestnet,
Playa3ull,
Shibarium,
ShibariumTestnet,
Algorand,
AlgorandTestnet,
Aurora,
AuroraTestnet,
Areon,
AreonTestnet,
PulseChain,
PulseChainTestnet,
ZkSyncEra,
ZkSyncEraTestnet,
Nexa,
NexaTestnet,
Moonbeam,
MoonbeamTestnet,
Manta,
MantaTestnet,
PolygonZkEVM,
PolygonZkEVMTestnet,
Radiant,
Fact0rn,
Base,
BaseTestnet,
Moonriver,
MoonriverTestnet,
Mantle,
MantleTestnet,
Flare,
FlareTestnet,
Taraxa,
TaraxaTestnet,
Koinos,
KoinosTestnet,
Joystream,
Bittensor,
Blast,
BlastTestnet,
Cyber,
CyberTestnet,
InternetComputer,
EnergyWebChain,
EnergyWebChainTestnet,
EnergyWebX,
EnergyWebXTestnet,
Casper,
CasperTestnet,
Core,
CoreTestnet,
Xodex,
Canxium,
Chiliz,
ChilizTestnet,
Alephium,
AlephiumTestnet,
Clore,
VanarChain,
VanarChainTestnet,
OdysseyChain, OdysseyChainTestnet,
Bitrock, BitrockTestnet,
Sonic, SonicTestnet,
ApeChain, ApeChainTestnet,
Scroll, ScrollTestnet,
ZkLinkNova, ZkLinkNovaTestnet,
Pepecoin, PepecoinTestnet,
-> Fee.Common(feeAmount)
// endregion
}
}
}
private suspend fun storeSwapTransaction(
currencyToSend: CryptoCurrencyStatus,
currencyToGet: CryptoCurrencyStatus,
@ -1418,17 +1172,14 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val otherNativeFee = transaction.otherNativeFeeWei
?.movePointLeft(nativeCoinDecimals)
?: BigDecimal.ZERO
val txFeeState = when (
val feeData = getFeeDataForDexSwap(
networkId = networkId,
val txFeeState = getFeeDataForDexSwap(
network = fromToken.currency.network,
transaction = transaction,
fromToken = fromToken.currency,
cardId = userWallet.scanResponse.card.cardId,
)
) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee)
}
.patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX)
.toTxFeeState(fromToken.currency, otherNativeFee)
val includeFeeInAmount = IncludeFeeInAmount.Excluded // exclude for dex
val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState)
val feeToCheckFunds = feeByPriority + (otherNativeFee ?: BigDecimal.ZERO)
@ -1492,17 +1243,13 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
private suspend fun getFeeDataForDexSwap(
networkId: String,
network: Network,
transaction: ExpressTransactionModel.DEX,
fromToken: CryptoCurrency,
cardId: String?,
): ProxyFees {
if (cardId != null && isDemoCardUseCase(cardId)) {
return getDemoFees(fromToken)
}
): TransactionFee {
return try {
val nativeBalance = userWalletManager.getNativeTokenBalance(
networkId = networkId,
networkId = network.backendId,
derivationPath = fromToken.network.derivationPath.value,
) ?: ProxyAmount.empty()
val amountToSend = createNativeAmountForDex(transaction.txValue, fromToken.network)
@ -1510,21 +1257,30 @@ internal class SwapInteractorImpl @AssistedInject constructor(
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),
val extras = createTransactionExtrasUseCase(
data = transaction.txData,
network = network,
).getOrNull() ?: error("unable to create extras")
val transactionData = TransactionData.Uncompiled(
amount = amountToSend,
destinationAddress = transaction.txTo,
increaseBy = INCREASE_GAS_LIMIT_BY,
callData = CompiledSmartContractCallData(transaction.txData.hexToBytes()),
derivationPath = fromToken.network.derivationPath.value,
fee = null,
sourceAddress = transaction.txFrom,
extras = extras,
)
getFeeUseCase(
transactionData = transactionData,
network = network,
userWallet = userWallet,
).getOrNull() ?: error("unable to calculate fee")
} catch (e: IllegalStateException) {
transactionManager.getFeeForGas(
networkId = networkId,
gas = transaction.gas.multiply(INCREASE_GAS_LIMIT_BY.toBigInteger()).divide(100.toBigInteger()),
derivationPath = fromToken.network.derivationPath.value,
)
getEthSpecificFeeUseCase(
userWallet = userWallet,
cryptoCurrency = fromToken,
gasLimit = transaction.gas,
).getOrNull() ?: error("can't get fee for getEthSpecificFeeUseCase")
}
}
@ -1575,7 +1331,11 @@ internal class SwapInteractorImpl @AssistedInject constructor(
): TxFeeState {
return txFeeResult?.fold(
ifLeft = { TxFeeState.Empty },
ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency, null) },
ifRight = { txFee ->
txFee
.patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_SEND)
.toTxFeeState(fromToken.currency, null)
},
) ?: TxFeeState.Empty
}
@ -1625,20 +1385,27 @@ internal class SwapInteractorImpl @AssistedInject constructor(
amount = swapAmount.value.convertToSdkAmount(fromToken),
blockchain = Blockchain.fromId(fromToken.network.id.value),
)
val cardId = userWallet.scanResponse.card.cardId
val feeData = if (isDemoCardUseCase(cardId)) {
getDemoFees(fromTokenStatus.currency)
} else {
try {
transactionManager.getFee(
networkId = networkId,
amountToSend = createNativeAmountForDex("0", fromToken.network),
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
destinationAddress = fromToken.getContractAddress(),
increaseBy = INCREASE_GAS_LIMIT_BY,
val feeData = try {
val extras = createTransactionExtrasUseCase(
callData = callData,
derivationPath = derivationPath,
network = fromToken.network,
).getOrNull() ?: error("unable to create extras")
val fromAddress = requireNotNull(
fromTokenStatus.value.networkAddress?.defaultAddress?.value,
) { "networkAddress cant be null" }
val transactionData = TransactionData.Uncompiled(
amount = createNativeAmountForDex("0", fromToken.network),
destinationAddress = fromToken.getContractAddress(),
fee = null,
sourceAddress = fromAddress,
extras = extras,
)
getFeeUseCase(
transactionData = transactionData,
network = fromToken.network,
userWallet = userWallet,
).getOrNull() ?: error("unable to calculate fee")
} catch (e: Exception) {
Timber.e(e, "Failed to get fee")
// it's impossible next steps without fee
@ -1649,11 +1416,11 @@ internal class SwapInteractorImpl @AssistedInject constructor(
expressDataError = ExpressDataError.UnknownError,
)
}
}
val feeState = when (feeData) {
is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken)
is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken)
}
val feeState = feeData
.patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX)
.toTxFeeState(fromToken, null)
val fee = when (feeState) {
TxFeeState.Empty -> BigDecimal.ZERO
is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue
@ -1683,120 +1450,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
)
}
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" }
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = normalFeeValue,
decimals = minFee.fee.decimals,
)
val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = priorityFeeValue,
decimals = normalFee.fee.decimals,
)
// endregion
// 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,
)
// endregion
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,
params = getSwapFeeParams(minFee),
),
priorityFee = TxFee(
feeValue = priorityFeeValue,
gasLimit = priorityFeeGas,
feeFiatFormatted = priorityFiatFee,
feeCryptoFormatted = priorityCryptoFee,
feeIncludeOtherNativeFee = priorityFeeValue + otherNativeFeeValue,
feeFiatFormattedWithNative = priorityFiatFeeWithNative,
feeCryptoFormattedWithNative = priorityCryptoFeeWithNative,
decimals = normalFee.fee.decimals,
cryptoSymbol = normalFee.fee.currencySymbol,
feeType = FeeType.PRIORITY,
params = getSwapFeeParams(normalFee),
),
)
}
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)
val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" }
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
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,
)
// endregion
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,
params = getSwapFeeParams(singleFee),
),
)
}
@Suppress("LongMethod")
private suspend fun TransactionFee.toTxFeeState(
fromToken: CryptoCurrency,
@ -1805,20 +1458,18 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO
return when (this) {
is TransactionFee.Choosable -> {
val normalFee = this.normal.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND)
val priorityFee = this.priority.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND)
val feeNormal = normalFee.amount.value ?: BigDecimal.ZERO
val feePriority = priorityFee.amount.value ?: BigDecimal.ZERO
val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO
val feePriority = this.priority.amount.value ?: BigDecimal.ZERO
val normalFiatValue = getFormattedFiatFees(fromToken, feeNormal)[0]
val priorityFiatValue = getFormattedFiatFees(fromToken, feePriority)[0]
val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feeNormal,
decimals = normalFee.amount.decimals,
decimals = this.normal.amount.decimals,
)
val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI(
amount = feePriority,
decimals = priorityFee.amount.decimals,
decimals = this.priority.amount.decimals,
)
// region otherNativeFee
@ -1829,39 +1480,35 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI(
amount = normalFeeWithOtherNative,
decimals = normalFee.amount.decimals,
decimals = this.normal.amount.decimals,
)
val priorityCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI(
amount = priorityFeeWithOtherNative,
decimals = priorityFee.amount.decimals,
decimals = this.priority.amount.decimals,
)
// endregion
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,
cryptoSymbol = this.normal.amount.currencySymbol,
feeType = FeeType.NORMAL,
params = getSwapFeeParams(normalFee),
fee = this.normal,
),
priorityFee = TxFee(
feeValue = feePriority,
gasLimit = priorityFee.getGasLimit(),
feeFiatFormatted = priorityFiatValue,
feeCryptoFormatted = priorityCryptoFee,
feeIncludeOtherNativeFee = priorityFeeWithOtherNative,
feeFiatFormattedWithNative = priorityFiatValueWithNative,
feeCryptoFormattedWithNative = priorityCryptoFeeWithNative,
decimals = priorityFee.amount.decimals,
cryptoSymbol = priorityFee.amount.currencySymbol,
cryptoSymbol = this.priority.amount.currencySymbol,
feeType = FeeType.PRIORITY,
params = getSwapFeeParams(priorityFee),
fee = this.priority,
),
)
}
@ -1884,59 +1531,20 @@ internal class SwapInteractorImpl @AssistedInject constructor(
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,
params = getSwapFeeParams(normal),
fee = this.normal,
),
)
}
}
}
private fun getSwapFeeParams(fee: Fee): TxFee.Params? = when (fee) {
is Fee.Filecoin -> TxFee.Params.Filecoin(
gasPremium = fee.gasPremium,
)
is Fee.Sui -> TxFee.Params.Sui(
gasPrice = fee.gasPrice,
gasBudget = fee.gasBudget,
)
is Fee.Hedera -> TxFee.Params.Hedera(
additionalHBARFee = fee.additionalHBARFee,
)
is Fee.Aptos,
is Fee.Bitcoin,
is Fee.CardanoToken,
is Fee.Common,
is Fee.Ethereum.EIP1559,
is Fee.Ethereum.Legacy,
is Fee.Kaspa,
is Fee.Tron,
is Fee.VeChain,
is Fee.Alephium,
-> null
}
private fun getSwapFeeParams(proxyFee: ProxyFee): TxFee.Params? = when (proxyFee) {
is ProxyFee.Filecoin -> TxFee.Params.Filecoin(
gasPremium = proxyFee.gasPremium,
)
is ProxyFee.Sui -> TxFee.Params.Sui(
gasPrice = proxyFee.gasPrice,
gasBudget = proxyFee.gasBudget,
)
is ProxyFee.CardanoToken,
is ProxyFee.Common,
-> null
}
private fun createNativeAmountForDex(txValueAmount: String, network: Network): Amount {
val nativeDecimals = Blockchain.fromNetworkId(network.backendId)?.decimals()
?: error("Blockchain not found")
@ -1950,7 +1558,50 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
/**
* Workaround to increase gas limit cause we calculate fee for random address
* We need to increase gasLimit for Ethereum fees for 2 cases
*
* DEX: for dex calculated gasLimit for given data might be changed when transaction processing
* for that case dex providers recommend to increase gasLimit for few percents to ensure transaction completes
*
* CEX: for that case we calculate fee for random generated address and gasLimit might be different for it
* and result address to send. That's why we should increase gasLimit a little
*
*/
private fun TransactionFee.patchTransactionFeeForSwap(increaseBy: Int): TransactionFee {
return when (this) {
is TransactionFee.Choosable -> {
this.copy(
minimum = this.minimum.increaseEthGasLimitInNeeded(increaseBy),
normal = this.normal.increaseEthGasLimitInNeeded(increaseBy),
priority = this.priority.increaseEthGasLimitInNeeded(increaseBy),
)
}
is TransactionFee.Single -> this.copy(normal = this.normal.increaseEthGasLimitInNeeded(increaseBy))
}
}
private fun Fee.increaseEthGasLimitInNeeded(increaseBy: Int): Fee {
return when (this) {
is Fee.Ethereum.EIP1559,
is Fee.Ethereum.Legacy,
-> this.increaseGasLimitBy(increaseBy)
is Fee.Alephium,
is Fee.Aptos,
is Fee.Bitcoin,
is Fee.CardanoToken,
is Fee.Common,
is Fee.Filecoin,
is Fee.Hedera,
is Fee.Kaspa,
is Fee.Sui,
is Fee.Tron,
is Fee.VeChain,
-> this
}
}
/**
* Increase gasLimit for Fee.Ethereum
*/
private fun Fee.increaseGasLimitBy(percentage: Int): Fee {
if (this !is Fee.Ethereum) return this
@ -1973,13 +1624,13 @@ internal class SwapInteractorImpl @AssistedInject constructor(
return cryptoCurrencyStatuses.value.pendingTransactions.any { it.isOutgoing }
}
private fun Fee.getGasLimit(): Int {
private fun Fee.getGasLimit(): Int? {
return when (this) {
is Fee.Ethereum -> gasLimit.toInt()
is Fee.VeChain -> gasLimit.toInt()
is Fee.Aptos -> gasLimit.toInt()
is Fee.Filecoin -> gasLimit.toInt()
else -> 0
else -> null
}
}
@ -2163,36 +1814,10 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
}
private fun getDemoFees(cryptoCurrency: CryptoCurrency): ProxyFees.MultipleFees {
val demoFee = ProxyAmount(
currencySymbol = cryptoCurrency.symbol,
value = minDemoFee,
decimals = cryptoCurrency.decimals,
)
return ProxyFees.MultipleFees(
minFee = ProxyFee.Common(
gasLimit = 1.toBigInteger(),
fee = demoFee,
),
normalFee = ProxyFee.Common(
gasLimit = 1.toBigInteger(),
fee = demoFee.copy(value = normalDemoFee),
),
priorityFee = ProxyFee.Common(
gasLimit = 1.toBigInteger(),
fee = demoFee.copy(value = priorityDemoFee),
),
)
}
companion object {
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
private const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12%
private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
private const val INFINITY_SYMBOL = ""
private val minDemoFee = "0.0001".toBigDecimal()
private val normalDemoFee = "0.0002".toBigDecimal()
private val priorityDemoFee = "0.0003".toBigDecimal()
}
@AssistedFactory

View file

@ -3,7 +3,6 @@ package com.tangem.feature.swap.domain.di
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.feature.swap.domain.*
import com.tangem.lib.crypto.TransactionManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -25,14 +24,6 @@ internal class SwapDomainModule {
return factory
}
@Provides
@Singleton
fun provideBlockchainInteractor(transactionManager: TransactionManager): BlockchainInteractor {
return DefaultBlockchainInteractor(
transactionManager = transactionManager,
)
}
@Provides
@Singleton
fun providesGetCryptoCurrencyStatusUseCase(

View file

@ -40,6 +40,8 @@ dependencies {
implementation(projects.domain.feedback)
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
implementation(projects.domain.txhistory)
implementation(projects.domain.txhistory.models)
/** Feature modules */
implementation(projects.features.swap.domain)

View file

@ -35,12 +35,12 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.swap.analytics.StoriesEvents
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.ExpressDataError
import com.tangem.feature.swap.domain.models.ExpressException
@ -80,7 +80,6 @@ typealias SuccessLoadedSwapData = Map<SwapProvider, SwapState.QuotesLoadedState>
internal class SwapModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val blockchainInteractor: BlockchainInteractor,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsErrorEventHandler: AnalyticsErrorHandler,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
@ -93,6 +92,7 @@ internal class SwapModel @Inject constructor(
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
@ -135,7 +135,7 @@ internal class SwapModel @Inject constructor(
stateBuilder.createInitialLoadingState(
initialCurrencyFrom = initialCurrencyFrom,
initialCurrencyTo = initialCurrencyTo,
fromNetworkInfo = blockchainInteractor.getBlockchainInfo(initialCurrencyFrom.network.backendId),
fromNetworkInfo = initialCurrencyFrom.getNetworkInfo(),
),
)
private set
@ -261,9 +261,7 @@ internal class SwapModel @Inject constructor(
uiState = stateBuilder.createInitialLoadingState(
initialCurrencyFrom = initialCurrencyFrom,
initialCurrencyTo = initialCurrencyTo,
fromNetworkInfo = blockchainInteractor.getBlockchainInfo(
initialCurrencyFrom.network.backendId,
),
fromNetworkInfo = initialCurrencyFrom.getNetworkInfo(),
)
initTokens(isReverseFromTo)
}
@ -603,10 +601,14 @@ internal class SwapModel @Inject constructor(
when (it) {
is SwapTransactionState.TxSent -> {
sendSuccessSwapEvent(fromCurrency.currency, fee.feeType)
val url = blockchainInteractor.getExplorerTransactionLink(
networkId = fromCurrency.currency.network.backendId,
val url = getExplorerTransactionUrlUseCase(
txHash = it.txHash,
)
networkId = fromCurrency.currency.network.id,
).getOrElse {
Timber.i("tx hash explore not supported")
""
}
updateWalletBalance()
uiState = stateBuilder.createSuccessState(
uiState = uiState,
@ -1356,6 +1358,13 @@ internal class SwapModel @Inject constructor(
}
}
private fun CryptoCurrency.getNetworkInfo(): NetworkInfo {
return NetworkInfo(
name = this.network.name,
blockchainId = this.network.id.value,
)
}
private companion object {
const val INITIAL_AMOUNT = ""
const val UPDATE_DELAY = 10000L

View file

@ -1,50 +0,0 @@
package com.tangem.lib.crypto
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.smartcontract.SmartContractCallData
import com.tangem.lib.crypto.models.*
import java.math.BigInteger
interface TransactionManager {
/**
* Get fee
*
* @param networkId network id of blockchain
* @param amountToSend amount
* @param currencyToSend currency to send in tx
* @param destinationAddress address to send tx
* @param increaseBy percents in format 125 = 25%
* @param callData tx smart contract call data
* @param derivationPath derivation path
* @return
*/
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun getFee(
networkId: String,
amountToSend: Amount,
currencyToSend: Currency,
destinationAddress: String,
increaseBy: Int?,
callData: SmartContractCallData?,
derivationPath: String?,
): ProxyFees
@Throws(IllegalStateException::class)
suspend fun getFeeForGas(networkId: String, gas: BigInteger, derivationPath: String?): ProxyFees
@Throws(IllegalStateException::class)
suspend fun updateWalletManager(networkId: String, derivationPath: String?)
/**
* In app blockchain id, actual in blockchain sdk, not the same as networkId
*
* workaround till not use backend only and not integrated server vs sdk
*/
@Throws(IllegalStateException::class)
fun getBlockchainInfo(networkId: String): ProxyNetworkInfo
@Throws(IllegalStateException::class)
fun getExplorerTransactionLink(networkId: String, txAddress: String): String
}

View file

@ -1,34 +0,0 @@
package com.tangem.lib.crypto.models
import java.math.BigDecimal
import java.math.BigInteger
sealed interface ProxyFee {
val gasLimit: BigInteger
val fee: ProxyAmount
data class Common(
override val gasLimit: BigInteger,
override val fee: ProxyAmount,
) : ProxyFee
data class CardanoToken(
override val gasLimit: BigInteger,
override val fee: ProxyAmount,
val minAdaValue: BigDecimal,
) : ProxyFee
data class Filecoin(
override val gasLimit: BigInteger,
override val fee: ProxyAmount,
val gasPremium: Long,
) : ProxyFee
data class Sui(
override val gasLimit: BigInteger,
override val fee: ProxyAmount,
val gasPrice: Long,
val gasBudget: Long,
) : ProxyFee
}

View file

@ -1,14 +0,0 @@
package com.tangem.lib.crypto.models
sealed class ProxyFees {
data class MultipleFees(
val minFee: ProxyFee,
val normalFee: ProxyFee,
val priorityFee: ProxyFee,
) : ProxyFees()
data class SingleFee(
val singleFee: ProxyFee,
) : ProxyFees()
}

View file

@ -1,6 +0,0 @@
package com.tangem.lib.crypto.models
data class ProxyNetworkInfo(
val name: String,
val blockchainId: String,
)