diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a046ec5c66..6fbe346966 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -154,6 +154,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.domain.walletManager.models) implementation(projects.domain.yieldSupply) + implementation(projects.domain.blockaid) implementation(projects.common) implementation(projects.common.routing) diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index a047e3deae..0921e8b1eb 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di.domain +import com.tangem.domain.blockaid.BlockAidGasEstimate import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository @@ -42,10 +43,12 @@ internal object YieldSupplyDomainModule { fun provideYieldSupplyEstimateEnterFeeUseCase( feeRepository: FeeRepository, feeErrorResolver: FeeErrorResolver, + blockAidGasEstimate: BlockAidGasEstimate, ): YieldSupplyEstimateEnterFeeUseCase { return YieldSupplyEstimateEnterFeeUseCase( feeRepository = feeRepository, feeErrorResolver = feeErrorResolver, + blockAidGasEstimate = blockAidGasEstimate, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt index cdb6ec04e3..4444d0b604 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt @@ -1,9 +1,11 @@ package com.tangem.datasource.api.common.blockaid import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest +import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionBulkScanRequest import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse +import com.tangem.datasource.api.common.blockaid.models.response.GasEstimationResponse import com.tangem.datasource.api.common.blockaid.models.response.SolanaTransactionResponse import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse import retrofit2.http.Body @@ -19,4 +21,7 @@ interface BlockAidApi { @POST("solana/message/scan") suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): SolanaTransactionResponse + + @POST("evm/transaction-bulk/scan") + suspend fun scanEvmTransactionBulk(@Body request: EvmTransactionBulkScanRequest): List } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/BlockAidScanOptions.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/BlockAidScanOptions.kt new file mode 100644 index 0000000000..630910b832 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/BlockAidScanOptions.kt @@ -0,0 +1,7 @@ +package com.tangem.datasource.api.common.blockaid.models.request + +enum class BlockAidScanOptions(val value: String) { + Simulation("simulation"), + Validation("validation"), + GasEstimation("gas_estimation"), +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt index f0d853d793..82b699a611 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/EvmTransactionScanRequest.kt @@ -14,9 +14,25 @@ data class EvmTransactionScanRequest( @Json(name = "metadata") val metadata: TransactionMetadata, ) +@JsonClass(generateAdapter = true) +data class EvmTransactionBulkScanRequest( + @Json(name = "chain") val chain: String, + @Json(name = "options") val options: List, + @Json(name = "metadata") val metadata: TransactionMetadata, + @Json(name = "data") val data: List, + @Json(name = "aggregated") val aggregated: Boolean = false, +) + @JsonClass(generateAdapter = true) data class RpcData( @Json(name = "jsonrpc") val jsonrpc: String = "2.0", @Json(name = "method") val method: String, @Json(name = "params") val params: List>, +) + +@JsonClass(generateAdapter = true) +data class Data( + @Json(name = "from") val from: String, + @Json(name = "to") val to: String, + @Json(name = "data") val data: String, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/GasEstimationResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/GasEstimationResponse.kt new file mode 100644 index 0000000000..4f4e622045 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/GasEstimationResponse.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GasEstimationResponse( + @Json(name = "gas_estimation") val gasEstimation: GasEstimationItem, +) + +@JsonClass(generateAdapter = true) +data class GasEstimationItem( + @Json(name = "estimate") val estimate: String, +) \ No newline at end of file diff --git a/data/blockaid/build.gradle.kts b/data/blockaid/build.gradle.kts index 344e76b061..8c28cf6afc 100644 --- a/data/blockaid/build.gradle.kts +++ b/data/blockaid/build.gradle.kts @@ -12,6 +12,7 @@ android { dependencies { /* Project - Domain */ implementation(projects.data.common) + implementation(projects.domain.models) implementation(projects.domain.blockaid) implementation(projects.domain.blockaid.models) @@ -20,6 +21,7 @@ dependencies { /* Project - Core */ implementation(projects.core.utils) + implementation(projects.libs.blockchainSdk) /* DI */ implementation(deps.hilt.core) diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidEvmScanTransactionConverter.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidEvmScanTransactionConverter.kt new file mode 100644 index 0000000000..213e89f7f4 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidEvmScanTransactionConverter.kt @@ -0,0 +1,31 @@ +package com.tangem.data.blockaid + +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras +import com.tangem.blockchain.common.Blockchain +import com.tangem.datasource.api.common.blockaid.models.request.BlockAidScanOptions +import com.tangem.datasource.api.common.blockaid.models.request.Data +import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionBulkScanRequest +import com.tangem.datasource.api.common.blockaid.models.response.TransactionMetadata +import com.tangem.utils.converter.Converter +import com.tangem.blockchain.common.TransactionData as SDKTransactionData + +internal class BlockAidEvmScanTransactionConverter( + private val blockchain: Blockchain, +) : Converter, EvmTransactionBulkScanRequest> { + + override fun convert(value: List): EvmTransactionBulkScanRequest { + return EvmTransactionBulkScanRequest( + chain = blockchain.getChainId().toString(), + options = listOf(BlockAidScanOptions.GasEstimation.value), + metadata = TransactionMetadata(domain = "https://tangem.com"), + data = value.map { + Data( + from = it.sourceAddress, + to = it.destinationAddress, + data = (it.extras as? EthereumTransactionExtras)?.callData?.dataHex.orEmpty(), + ) + }, + aggregated = false, + ) + } +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt index 57e8db2559..0ce7a6aeea 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidRepository.kt @@ -3,11 +3,19 @@ package com.tangem.data.blockaid import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.dapp.DAppData import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.GasEstimationResult import com.domain.blockaid.models.transaction.TransactionData +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.blockchain.common.TransactionData as SDKTransactionData interface BlockAidRepository { suspend fun verifyDAppDomain(data: DAppData): CheckDAppResult suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult + + suspend fun getGasEstimation( + cryptoCurrency: CryptoCurrency, + transactionDataList: List, + ): GasEstimationResult } \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidGasEstimate.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidGasEstimate.kt new file mode 100644 index 0000000000..8b8e87189f --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidGasEstimate.kt @@ -0,0 +1,19 @@ +package com.tangem.data.blockaid + +import arrow.core.Either +import com.domain.blockaid.models.transaction.GasEstimationResult +import com.tangem.blockchain.common.TransactionData +import com.tangem.domain.blockaid.BlockAidGasEstimate +import com.tangem.domain.models.currency.CryptoCurrency +import javax.inject.Inject + +class DefaultBlockAidGasEstimate @Inject constructor( + private val repository: BlockAidRepository, +) : BlockAidGasEstimate { + override suspend fun getGasEstimation( + cryptoCurrency: CryptoCurrency, + transactionDataList: List, + ): Either = Either.catch { + repository.getGasEstimation(cryptoCurrency = cryptoCurrency, transactionDataList = transactionDataList) + } +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt index 985e8d7e2e..59cf796d98 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt @@ -3,12 +3,18 @@ package com.tangem.data.blockaid import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.dapp.DAppData import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.GasEstimationResult import com.domain.blockaid.models.transaction.TransactionData import com.domain.blockaid.models.transaction.TransactionParams +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.data.blockaid.converters.GasEstimationResponseConverter import com.tangem.datasource.api.common.blockaid.BlockAidApi import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import com.tangem.blockchain.common.TransactionData as SDKTransactionData internal class DefaultBlockAidRepository( private val api: BlockAidApi, @@ -30,6 +36,17 @@ internal class DefaultBlockAidRepository( } } + override suspend fun getGasEstimation( + cryptoCurrency: CryptoCurrency, + transactionDataList: List, + ): GasEstimationResult { + val blockchain = cryptoCurrency.network.toBlockchain() + return when { + blockchain.isEvm() -> scanEvmTransactionBulk(blockchain, transactionDataList) + else -> error("Gas estimation with BlockAid not supported by ${blockchain.fullName}") + } + } + private suspend fun scanEvmTransaction(data: TransactionData): CheckTransactionResult = withContext(dispatchers.io) { val response = api.scanJsonRpc(mapper.mapToEvmRequest(data)) @@ -41,4 +58,15 @@ internal class DefaultBlockAidRepository( val response = api.scanSolanaMessage(mapper.mapToSolanaRequest(data)) mapper.mapToDomain(response) } + + private suspend fun scanEvmTransactionBulk( + blockchain: Blockchain, + transactionDataList: List, + ): GasEstimationResult = withContext(dispatchers.io) { + val response = api.scanEvmTransactionBulk( + BlockAidEvmScanTransactionConverter(blockchain).convert(transactionDataList), + ) + + GasEstimationResponseConverter.convert(response) + } } \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/converters/GasEstimationResponseConverter.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/converters/GasEstimationResponseConverter.kt new file mode 100644 index 0000000000..51a9e25b75 --- /dev/null +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/converters/GasEstimationResponseConverter.kt @@ -0,0 +1,14 @@ +package com.tangem.data.blockaid.converters + +import com.domain.blockaid.models.transaction.GasEstimationResult +import com.tangem.blockchain.extensions.hexToBigInteger +import com.tangem.datasource.api.common.blockaid.models.response.GasEstimationResponse +import com.tangem.utils.converter.Converter + +internal object GasEstimationResponseConverter : Converter, GasEstimationResult> { + override fun convert(value: List): GasEstimationResult { + return GasEstimationResult( + estimatedGasList = value.map { it.gasEstimation.estimate.hexToBigInteger() }, + ) + } +} \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt index 1efc366513..c61a853d1d 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/di/BlockAidDataModule.kt @@ -1,6 +1,8 @@ package com.tangem.data.blockaid.di +import com.tangem.data.blockaid.DefaultBlockAidGasEstimate import com.tangem.data.blockaid.DefaultBlockAidVerifier +import com.tangem.domain.blockaid.BlockAidGasEstimate import com.tangem.domain.blockaid.BlockAidVerifier import dagger.Binds import dagger.Module @@ -15,4 +17,8 @@ interface BlockAidDataModule { @Binds @Singleton fun bindVerifier(verifier: DefaultBlockAidVerifier): BlockAidVerifier + + @Binds + @Singleton + fun bindBlockAidGasEstimate(gasEstimate: DefaultBlockAidGasEstimate): BlockAidGasEstimate } \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt index 49b6e3c486..07898f241d 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt @@ -1,7 +1,13 @@ package com.tangem.data.transaction +import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager +import com.tangem.blockchain.blockchains.ethereum.eip1559.isSupportEIP1559 +import com.tangem.blockchain.blockchains.ethereum.network.EthereumFeeHistory +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData +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.toBlockchain @@ -12,6 +18,8 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import java.math.BigDecimal +import java.math.BigInteger internal class DefaultFeeRepository( private val walletManagersFacade: WalletManagersFacade, @@ -22,6 +30,53 @@ internal class DefaultFeeRepository( return networkId.toBlockchain().isFeeApproximate(amountType) } + override suspend fun getEthereumFeeWithoutGas( + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + ): Fee.Ethereum { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + + val blockchain = cryptoCurrency.network.toBlockchain() + + val ethereumWalletManager = walletManager as? EthereumWalletManager + ?: error("Not supported for ${cryptoCurrency.network}") + + val fee = if (blockchain.isSupportEIP1559) { + val gasHistory = when (val gasHistory = ethereumWalletManager.getGasHistory()) { + is Result.Failure -> throw gasHistory.error + is Result.Success -> gasHistory.data + } + + val marketPriorityFee = when (gasHistory) { + is EthereumFeeHistory.Common -> gasHistory.marketPriorityFee + is EthereumFeeHistory.Fallback -> gasHistory.gasPrice.toBigDecimal() * MULTIPLIER_GAS_PRICE_NORMAL_FEE + } + + val maxFeePerGas = gasHistory.baseFee * MULTIPLIER_GAS_PRICE_NORMAL_FEE + marketPriorityFee + + getEthEip1559Fee( + maxFeePerGas = maxFeePerGas.toBigInteger(), + priorityFee = marketPriorityFee.toBigInteger(), + blockchain = blockchain, + ) + } else { + val gasPrice = when (val gasPrice = ethereumWalletManager.getGasPrice()) { + is Result.Failure -> throw gasPrice.error + is Result.Success -> gasPrice.data + } + + getEthLegacyFee( + gasPrice = gasPrice, + blockchain = blockchain, + ) + } + + return fee + } + override suspend fun calculateFee( userWallet: UserWallet, cryptoCurrency: CryptoCurrency, @@ -54,4 +109,37 @@ internal class DefaultFeeRepository( ?: error("WalletManager is null"), ) } + + private fun getEthLegacyFee(gasPrice: BigInteger, blockchain: Blockchain): Fee.Ethereum.Legacy { + val amount = Amount( + value = BigDecimal.ZERO, + blockchain = blockchain, + ) + return Fee.Ethereum.Legacy( + amount = amount, + gasLimit = BigInteger.ZERO, + gasPrice = gasPrice, + ) + } + + private fun getEthEip1559Fee( + maxFeePerGas: BigInteger, + priorityFee: BigInteger, + blockchain: Blockchain, + ): Fee.Ethereum.EIP1559 { + val amount = Amount( + value = BigDecimal.ZERO, + blockchain = blockchain, + ) + return Fee.Ethereum.EIP1559( + amount = amount, + gasLimit = BigInteger.ZERO, + maxFeePerGas = maxFeePerGas, + priorityFee = priorityFee, + ) + } + + private companion object { + val MULTIPLIER_GAS_PRICE_NORMAL_FEE = "1.2".toBigDecimal() // 120% + } } \ No newline at end of file diff --git a/domain/blockaid/build.gradle.kts b/domain/blockaid/build.gradle.kts index 9fda0da981..afae6b5750 100644 --- a/domain/blockaid/build.gradle.kts +++ b/domain/blockaid/build.gradle.kts @@ -1,13 +1,23 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.blockaid" +} + + dependencies { - /* Project - Domain */ + /** Project - Domain */ implementation(projects.domain.core) implementation(projects.domain.blockaid.models) - /* Other */ + /** Tangem SDK */ + implementation(tangemDeps.blockchain) + + + /** Other */ implementation(deps.moshi.adapters) } \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/GasEstimationResult.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/GasEstimationResult.kt new file mode 100644 index 0000000000..775eb8a970 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/GasEstimationResult.kt @@ -0,0 +1,7 @@ +package com.domain.blockaid.models.transaction + +import java.math.BigInteger + +data class GasEstimationResult( + val estimatedGasList: List, +) \ No newline at end of file diff --git a/domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidGasEstimate.kt b/domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidGasEstimate.kt new file mode 100644 index 0000000000..4cfc8eff9d --- /dev/null +++ b/domain/blockaid/src/main/kotlin/com/tangem/domain/blockaid/BlockAidGasEstimate.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.blockaid + +import arrow.core.Either +import com.domain.blockaid.models.transaction.GasEstimationResult +import com.tangem.blockchain.common.TransactionData +import com.tangem.domain.models.currency.CryptoCurrency + +interface BlockAidGasEstimate { + + suspend fun getGasEstimation( + cryptoCurrency: CryptoCurrency, + transactionDataList: List, + ): Either +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt index f2b3a91661..f0bfdcf442 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt @@ -2,6 +2,7 @@ package com.tangem.domain.transaction import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -18,4 +19,6 @@ interface FeeRepository { cryptoCurrency: CryptoCurrency, transactionData: TransactionData, ): TransactionFee + + suspend fun getEthereumFeeWithoutGas(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): Fee.Ethereum } \ No newline at end of file diff --git a/domain/yield-supply/build.gradle.kts b/domain/yield-supply/build.gradle.kts index e10c9dba42..5648c7ee9a 100644 --- a/domain/yield-supply/build.gradle.kts +++ b/domain/yield-supply/build.gradle.kts @@ -18,12 +18,15 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.transaction) implementation(projects.domain.legacy) + implementation(projects.domain.blockaid.models) + implementation(projects.domain.blockaid) /** Tandem SDK */ implementation(tangemDeps.blockchain) /** Other */ implementation(deps.arrow.core) + implementation(deps.timber) /** tests */ testImplementation(projects.common.test) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt index 23e3efbe1d..273114f51c 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt @@ -5,23 +5,85 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyEnterCallData +import com.tangem.domain.blockaid.BlockAidGasEstimate import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.transaction.error.GetFeeError import com.tangem.utils.extensions.isSingleItem +import timber.log.Timber +import java.math.BigInteger class YieldSupplyEstimateEnterFeeUseCase( private val feeRepository: FeeRepository, private val feeErrorResolver: FeeErrorResolver, + private val blockAidGasEstimate: BlockAidGasEstimate, ) { + suspend operator fun invoke( userWallet: UserWallet, cryptoCurrency: CryptoCurrency, transactionDataList: List, ): Either> = Either.catch { - val withCalculatedFee = transactionDataList.filter { + if (transactionDataList.isSingleItem()) { + transactionDataList.map { transaction -> + transaction.copy( + fee = feeRepository.calculateFee( + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + transactionData = transaction, + ).normal, + ) + } + } else { + val estimatedFees = estimateFeeWithBlockAid( + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + transactionDataList = transactionDataList, + ) + + if (estimatedFees.isNullOrEmpty()) { + // Fallback in case block aid returns error + estimateFeeWithStatic( + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + transactionDataList = transactionDataList, + ) + } else { + estimatedFees + } + } + }.mapLeft(feeErrorResolver::resolve) + + private suspend fun estimateFeeWithBlockAid( + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + transactionDataList: List, + ): List? { + val estimatedFees = blockAidGasEstimate.getGasEstimation( + cryptoCurrency = cryptoCurrency, + transactionDataList = transactionDataList, + ).onLeft(Timber::e).getOrNull() ?: return null + + if (estimatedFees.estimatedGasList.isEmpty()) return null + + val fee = feeRepository.getEthereumFeeWithoutGas( + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + ) + + return transactionDataList.zip(estimatedFees.estimatedGasList) { transaction, estimatedGas -> + transaction.copy(fee = fee.fixFee(cryptoCurrency, estimatedGas)) + } + } + + private suspend fun estimateFeeWithStatic( + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + transactionDataList: List, + ): List { + val withCalculatedFees = transactionDataList.filter { (it.extras as? EthereumTransactionExtras)?.callData !is EthereumYieldSupplyEnterCallData }.map { transaction -> transaction.copy( @@ -33,38 +95,30 @@ class YieldSupplyEstimateEnterFeeUseCase( ) } - val withEstimatedCalculatedFee = transactionDataList.filter { + val calculatedFee = withCalculatedFees.firstOrNull()?.fee ?: error("Must be any calculated fee") + + val withEstimatedFees = transactionDataList.filter { (it.extras as? EthereumTransactionExtras)?.callData is EthereumYieldSupplyEnterCallData }.map { transaction -> - if (transactionDataList.isSingleItem()) { - transaction.copy( - fee = feeRepository.calculateFee( - userWallet = userWallet, - cryptoCurrency = cryptoCurrency, - transactionData = transaction, - ).normal, - ) - } else { - transaction.copy(fee = withCalculatedFee.firstOrNull()?.fee?.fixFee(cryptoCurrency)) - } + transaction.copy(fee = calculatedFee.fixFee(cryptoCurrency, ETHEREUM_CONSTANT_GAS_LIMIT)) } - // Transactions order must be preserved - withCalculatedFee + withEstimatedCalculatedFee - }.mapLeft(feeErrorResolver::resolve) + // First return calculated fees then estimated + return withCalculatedFees + withEstimatedFees + } - private fun Fee.fixFee(cryptoCurrency: CryptoCurrency) = when (this) { + private fun Fee.fixFee(cryptoCurrency: CryptoCurrency, gasLimit: BigInteger) = when (this) { is Fee.Ethereum.Legacy -> copy( - gasLimit = ETHEREUM_CONSTANT_GAS_LIMIT, + gasLimit = gasLimit, amount = amount.copy( - value = gasPrice.multiply(ETHEREUM_CONSTANT_GAS_LIMIT) + value = gasPrice.multiply(gasLimit) .toBigDecimal().movePointLeft(cryptoCurrency.decimals), ), ) is Fee.Ethereum.EIP1559 -> copy( - gasLimit = ETHEREUM_CONSTANT_GAS_LIMIT, + gasLimit = gasLimit, amount = amount.copy( - value = maxFeePerGas.multiply(ETHEREUM_CONSTANT_GAS_LIMIT) + value = maxFeePerGas.multiply(gasLimit) .toBigDecimal().movePointLeft(cryptoCurrency.decimals), ), ) @@ -73,6 +127,6 @@ class YieldSupplyEstimateEnterFeeUseCase( private companion object { // Using constant gas limit to avoid fee calculation errors when contract address is not deployed yet - val ETHEREUM_CONSTANT_GAS_LIMIT = 350_000.toBigInteger() + val ETHEREUM_CONSTANT_GAS_LIMIT = 500_000.toBigInteger() } } \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyEstimateEnterFeeUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyEstimateEnterFeeUseCaseTest.kt index 7ba44ab1b0..8a2cc75aae 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyEstimateEnterFeeUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyEstimateEnterFeeUseCaseTest.kt @@ -1,6 +1,8 @@ package com.tangem.domain.yield.supply import arrow.core.Either +import arrow.core.right +import com.domain.blockaid.models.transaction.GasEstimationResult import com.google.common.truth.Truth import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.common.Blockchain @@ -10,6 +12,7 @@ import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderF import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.yieldsupply.YieldSupplyContractCallDataProviderFactory +import com.tangem.domain.blockaid.BlockAidGasEstimate import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -17,9 +20,7 @@ import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test @@ -30,7 +31,8 @@ import java.math.BigInteger class YieldSupplyEstimateEnterFeeUseCaseTest { private val feeRepository: FeeRepository = mockk() private val feeErrorResolver: FeeErrorResolver = mockk() - private val useCase = YieldSupplyEstimateEnterFeeUseCase(feeRepository, feeErrorResolver) + private val blockAidGasEstimate: BlockAidGasEstimate = mockk() + private val useCase = YieldSupplyEstimateEnterFeeUseCase(feeRepository, feeErrorResolver, blockAidGasEstimate) private val userWallet: UserWallet = mockk() private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) { @@ -72,45 +74,57 @@ class YieldSupplyEstimateEnterFeeUseCaseTest { @Test fun `test enter transaction uses constant gas limit Legacy`() = runTest { val fee = TransactionFee.Single(ethLegacyFee()) + val deployTx = getDeployTx() + val enterTx = getEnterTx() + coEvery { blockAidGasEstimate.getGasEstimation(any(), any()) } returns GasEstimationResult(emptyList()).right() coEvery { feeRepository.calculateFee(any(), any(), any()) } returns fee val result = useCase( userWallet = userWallet, cryptoCurrency = cryptoCurrency, - transactionDataList = listOf( - getDeployTx(), - getEnterTx(), - ), + transactionDataList = listOf(deployTx, enterTx), ) Truth.assertThat(result.isRight()).isTrue() - val txs = (result as Either.Right).value + coVerify(ordering = Ordering.ORDERED) { + blockAidGasEstimate.getGasEstimation(any(), any()) + feeRepository.calculateFee(any(), any(), deployTx) + } + coVerify(inverse = true) { + feeRepository.calculateFee(any(), any(), enterTx) + } + val txs = (result as Either.Right).value val deployFee = txs.first().fee as Fee.Ethereum.Legacy val enterFee = txs.last().fee as Fee.Ethereum.Legacy Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000)) - Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000)) + Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(500_000)) } @Test fun `test not enter transaction uses gas limit Legacy`() = runTest { val fee = TransactionFee.Single(ethLegacyFee()) + val deployTx = getDeployTx() + val approveTx = getApproveTx() + coEvery { blockAidGasEstimate.getGasEstimation(any(), any()) } returns GasEstimationResult(emptyList()).right() coEvery { feeRepository.calculateFee(any(), any(), any()) } returnsMany listOf(fee, fee) val result = useCase( userWallet = userWallet, cryptoCurrency = cryptoCurrency, - transactionDataList = listOf( - getDeployTx(), - getApproveTx(), - ), + transactionDataList = listOf(deployTx, approveTx), ) Truth.assertThat(result.isRight()).isTrue() - val txs = (result as Either.Right).value + coVerify(ordering = Ordering.ORDERED) { + blockAidGasEstimate.getGasEstimation(any(), any()) + feeRepository.calculateFee(any(), any(), deployTx) + feeRepository.calculateFee(any(), any(), approveTx) + } + val txs = (result as Either.Right).value val deployFee = txs.first().fee as Fee.Ethereum.Legacy val approveFee = txs.last().fee as Fee.Ethereum.Legacy Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000)) @@ -120,69 +134,88 @@ class YieldSupplyEstimateEnterFeeUseCaseTest { @Test fun `test enter transaction with wrong order uses gas limit Legacy`() = runTest { val fee = TransactionFee.Single(ethLegacyFee()) + val enterTx = getEnterTx() + val deployTx = getDeployTx() + coEvery { blockAidGasEstimate.getGasEstimation(any(), any()) } returns GasEstimationResult(emptyList()).right() coEvery { feeRepository.calculateFee(any(), any(), any()) } returnsMany listOf(fee, fee) val result = useCase( userWallet = userWallet, cryptoCurrency = cryptoCurrency, - transactionDataList = listOf( - getEnterTx(), - getDeployTx(), - ), + transactionDataList = listOf(enterTx, deployTx), ) Truth.assertThat(result.isRight()).isTrue() - val txs = (result as Either.Right).value + coVerify(ordering = Ordering.ORDERED) { + blockAidGasEstimate.getGasEstimation(any(), any()) + feeRepository.calculateFee(any(), any(), deployTx) + } + coVerify(inverse = true) { + feeRepository.calculateFee(any(), any(), enterTx) + } + val txs = (result as Either.Right).value val deployFee = txs.first().fee as Fee.Ethereum.Legacy val enterFee = txs.last().fee as Fee.Ethereum.Legacy Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000)) - Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000)) + Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(500_000)) } @Test fun `test enter transaction uses constant gas limit Eip1559`() = runTest { val fee = TransactionFee.Single(ethEip1559Fee()) + val deployTx = getDeployTx() + val enterTx = getEnterTx() + coEvery { blockAidGasEstimate.getGasEstimation(any(), any()) } returns GasEstimationResult(emptyList()).right() coEvery { feeRepository.calculateFee(any(), any(), any()) } returns fee val result = useCase( userWallet = userWallet, cryptoCurrency = cryptoCurrency, - transactionDataList = listOf( - getDeployTx(), - getEnterTx(), - ), + transactionDataList = listOf(deployTx, enterTx), ) Truth.assertThat(result.isRight()).isTrue() - val txs = (result as Either.Right).value + coVerify(ordering = Ordering.ORDERED) { + blockAidGasEstimate.getGasEstimation(any(), any()) + feeRepository.calculateFee(any(), any(), deployTx) + } + coVerify(inverse = true) { + feeRepository.calculateFee(any(), any(), enterTx) + } + val txs = (result as Either.Right).value val deployFee = txs.first().fee as Fee.Ethereum.EIP1559 val enterFee = txs.last().fee as Fee.Ethereum.EIP1559 Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000)) - Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000)) + Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(500_000)) } @Test fun `test not enter transaction uses gas limit Eip1559`() = runTest { val fee = TransactionFee.Single(ethEip1559Fee()) + val deployTx = getDeployTx() + val approveTx = getApproveTx() + coEvery { blockAidGasEstimate.getGasEstimation(any(), any()) } returns GasEstimationResult(emptyList()).right() coEvery { feeRepository.calculateFee(any(), any(), any()) } returnsMany listOf(fee, fee) val result = useCase( userWallet = userWallet, cryptoCurrency = cryptoCurrency, - transactionDataList = listOf( - getDeployTx(), - getApproveTx(), - ), + transactionDataList = listOf(deployTx, approveTx), ) Truth.assertThat(result.isRight()).isTrue() - val txs = (result as Either.Right).value + coVerify(ordering = Ordering.ORDERED) { + blockAidGasEstimate.getGasEstimation(any(), any()) + feeRepository.calculateFee(any(), any(), deployTx) + feeRepository.calculateFee(any(), any(), approveTx) + } + val txs = (result as Either.Right).value val deployFee = txs.first().fee as Fee.Ethereum.EIP1559 val approveFee = txs.last().fee as Fee.Ethereum.EIP1559 Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000)) @@ -192,25 +225,137 @@ class YieldSupplyEstimateEnterFeeUseCaseTest { @Test fun `test enter transaction with wrong order uses gas limit Eip1559`() = runTest { val fee = TransactionFee.Single(ethEip1559Fee()) + val enterTx = getEnterTx() + val deployTx = getDeployTx() + coEvery { blockAidGasEstimate.getGasEstimation(any(), any()) } returns GasEstimationResult(emptyList()).right() coEvery { feeRepository.calculateFee(any(), any(), any()) } returnsMany listOf(fee, fee) val result = useCase( userWallet = userWallet, cryptoCurrency = cryptoCurrency, - transactionDataList = listOf( - getEnterTx(), - getDeployTx(), - ), + transactionDataList = listOf(enterTx, deployTx), ) Truth.assertThat(result.isRight()).isTrue() - val txs = (result as Either.Right).value + coVerify(ordering = Ordering.ORDERED) { + blockAidGasEstimate.getGasEstimation(any(), any()) + feeRepository.calculateFee(any(), any(), deployTx) + } + coVerify(inverse = true) { + feeRepository.calculateFee(any(), any(), enterTx) + } + val txs = (result as Either.Right).value val deployFee = txs.first().fee as Fee.Ethereum.EIP1559 val enterFee = txs.last().fee as Fee.Ethereum.EIP1559 Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000)) - Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(350_000)) + Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(500_000)) + } + + @Test + fun `test single transaction with fee Legacy`() = runTest { + val fee = TransactionFee.Single(ethLegacyFee()) + val enterTx = getEnterTx() + + coEvery { blockAidGasEstimate.getGasEstimation(any(), any()) } returns GasEstimationResult(emptyList()).right() + coEvery { feeRepository.calculateFee(any(), any(), any()) } returns fee + + val result = useCase( + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + transactionDataList = listOf(enterTx), + ) + Truth.assertThat(result.isRight()).isTrue() + + coVerify(ordering = Ordering.ORDERED) { + feeRepository.calculateFee(any(), any(), enterTx) + } + coVerify(inverse = true) { + blockAidGasEstimate.getGasEstimation(any(), any()) + } + + val txs = (result as Either.Right).value + val enterFee = txs.first().fee as Fee.Ethereum.Legacy + Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(21_000)) + } + + @Test + fun `test enter transaction uses block aid Eip1559`() = runTest { + val fee = ethEip1559Fee() + val deployTx = getDeployTx() + val approveTx = getApproveTx() + val enterTx = getEnterTx() + + coEvery { blockAidGasEstimate.getGasEstimation(any(), any()) } returns GasEstimationResult( + estimatedGasList = listOf(1_000.toBigInteger(), 2_000.toBigInteger(), 3_000.toBigInteger()), + ).right() + coEvery { feeRepository.calculateFee(any(), any(), any()) } returns mockk() + coEvery { feeRepository.getEthereumFeeWithoutGas(any(), any()) } returns fee + + val result = useCase( + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + transactionDataList = listOf(deployTx, approveTx, enterTx), + ) + Truth.assertThat(result.isRight()).isTrue() + + coVerify(ordering = Ordering.ORDERED) { + blockAidGasEstimate.getGasEstimation(any(), any()) + feeRepository.getEthereumFeeWithoutGas(any(), any()) + } + coVerify(inverse = true) { + feeRepository.calculateFee(any(), any(), deployTx) + feeRepository.calculateFee(any(), any(), approveTx) + feeRepository.calculateFee(any(), any(), enterTx) + } + + val txs = (result as Either.Right).value + val deployFee = txs.first().fee as Fee.Ethereum.EIP1559 + val approveFee = txs[1].fee as Fee.Ethereum.EIP1559 + val enterFee = txs.last().fee as Fee.Ethereum.EIP1559 + Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(1_000)) + Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(2_000)) + Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(3_000)) + } + + @Test + fun `test enter transaction uses block aid Legacy`() = runTest { + val fee = ethLegacyFee() + val deployTx = getDeployTx() + val approveTx = getApproveTx() + val enterTx = getEnterTx() + + coEvery { blockAidGasEstimate.getGasEstimation(any(), any()) } returns GasEstimationResult( + estimatedGasList = listOf(1_000.toBigInteger(), 2_000.toBigInteger(), 3_000.toBigInteger()), + ).right() + coEvery { feeRepository.calculateFee(any(), any(), any()) } returns mockk() + coEvery { feeRepository.getEthereumFeeWithoutGas(any(), any()) } returns fee + + val result = useCase( + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + transactionDataList = listOf(deployTx, approveTx, enterTx), + ) + Truth.assertThat(result.isRight()).isTrue() + + coVerify(ordering = Ordering.ORDERED) { + blockAidGasEstimate.getGasEstimation(any(), any()) + feeRepository.getEthereumFeeWithoutGas(any(), any()) + } + coVerify(inverse = true) { + feeRepository.calculateFee(any(), any(), deployTx) + feeRepository.calculateFee(any(), any(), approveTx) + feeRepository.calculateFee(any(), any(), enterTx) + } + + val txs = (result as Either.Right).value + val deployFee = txs.first().fee as Fee.Ethereum.Legacy + val approveFee = txs[1].fee as Fee.Ethereum.Legacy + val enterFee = txs.last().fee as Fee.Ethereum.Legacy + Truth.assertThat(deployFee.gasLimit).isEqualTo(BigInteger.valueOf(1_000)) + Truth.assertThat(approveFee.gasLimit).isEqualTo(BigInteger.valueOf(2_000)) + Truth.assertThat(enterFee.gasLimit).isEqualTo(BigInteger.valueOf(3_000)) } private fun getDeployTx() = uncompiled(