diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/Extensions.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/Extensions.kt new file mode 100644 index 0000000000..a899479a7b --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/Extensions.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.transaction + +import arrow.core.raise.Raise +import com.tangem.domain.transaction.error.GetFeeError + +fun Raise.raiseIllegalStateError(error: String): Nothing { + raise(GetFeeError.DataError(IllegalStateException(error))) +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetAvailableFeeTokensUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetAvailableFeeTokensUseCase.kt index 019d021363..d46ccc06c3 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetAvailableFeeTokensUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetAvailableFeeTokensUseCase.kt @@ -12,6 +12,7 @@ import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.GaslessTransactionRepository import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.raiseIllegalStateError import java.math.BigDecimal class GetAvailableFeeTokensUseCase( @@ -82,8 +83,4 @@ class GetAvailableFeeTokensUseCase( } .toList() } - - private fun Raise.raiseIllegalStateError(error: String): Nothing { - raise(GetFeeError.DataError(IllegalStateException(error))) - } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForGaslessUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForGaslessUseCase.kt index d78a9f1cf1..cb48fefecd 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForGaslessUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForGaslessUseCase.kt @@ -5,14 +5,9 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager -import com.tangem.blockchain.blockchains.ethereum.tokenmethods.TransferERC20TokenCallData -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Token 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.domain.demo.DemoTransactionSender import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -23,12 +18,11 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.GaslessTransactionRepository import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.GetFeeError.GaslessError -import com.tangem.domain.transaction.error.mapToFeeError import com.tangem.domain.transaction.models.TransactionFeeExtended +import com.tangem.domain.transaction.raiseIllegalStateError import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import java.math.BigDecimal -import java.math.RoundingMode class GetFeeForGaslessUseCase( private val walletManagersFacade: WalletManagersFacade, @@ -39,6 +33,12 @@ class GetFeeForGaslessUseCase( private val getFeeUseCase: GetFeeUseCase, ) { + private val tokenFeeCalculator = TokenFeeCalculator( + walletManagersFacade = walletManagersFacade, + gaslessTransactionRepository = gaslessTransactionRepository, + demoConfig = demoConfig, + ) + suspend operator fun invoke( userWallet: UserWallet, network: Network, @@ -67,12 +67,12 @@ class GetFeeForGaslessUseCase( val walletManager = prepareWalletManager(userWallet, network) - val initialFee = calculateInitialFee( + val initialFee = tokenFeeCalculator.calculateInitialFee( userWallet = userWallet, network = network, walletManager = walletManager, transactionData = transactionData, - ) + ).bind() selectFeePaymentStrategy( userWallet = userWallet, @@ -103,27 +103,6 @@ class GetFeeForGaslessUseCase( return ethereumWalletManager } - private suspend fun Raise.calculateInitialFee( - userWallet: UserWallet, - network: Network, - walletManager: EthereumWalletManager, - transactionData: TransactionData, - ): TransactionFee { - val transactionSender = if (userWallet is UserWallet.Cold && - demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId) - ) { - demoTransactionSender(userWallet, network) - } else { - walletManager - } - - val maybeFee = when (val result = transactionSender.getFee(transactionData = transactionData)) { - is Result.Success -> result.data - is Result.Failure -> raise(result.mapToFeeError()) - } - return maybeFee - } - private suspend fun Raise.selectFeePaymentStrategy( userWallet: UserWallet, walletManager: EthereumWalletManager, @@ -183,104 +162,12 @@ class GetFeeForGaslessUseCase( val tokenForPayFeeStatus = supportedGaslessTokensStatusesSortedByBalanceDesc.firstOrNull() ?: raise(GaslessError.NoSupportedTokensFound) - return calculateTokenFee( + return tokenFeeCalculator.calculateTokenFee( walletManager = walletManager, tokenForPayFeeStatus = tokenForPayFeeStatus, nativeCurrencyStatus = nativeCurrencyStatus, initialFee = initialFee, - ) - } - - private suspend fun Raise.calculateTokenFee( - walletManager: EthereumWalletManager, - tokenForPayFeeStatus: CryptoCurrencyStatus, - nativeCurrencyStatus: CryptoCurrencyStatus, - initialFee: Fee.Ethereum, - ): TransactionFeeExtended { - val tokenForPayFee = tokenForPayFeeStatus.currency as? CryptoCurrency.Token - ?: raiseIllegalStateError("only tokens are supported") - - val transferFeeToContractAmount = createTokenAmount(tokenForPayFee, BigDecimal(FEE_TRANSFER_AMOUNT)) - val feeDestination = gaslessTransactionRepository.getTokenFeeReceiverAddress() - val feeTransferGasLimitResult = walletManager.getGasLimit( - amount = transferFeeToContractAmount, - destination = feeDestination, - callData = TransferERC20TokenCallData( - destination = feeDestination, - amount = transferFeeToContractAmount, - ), - ) - - val feeTransferGasLimit = when (feeTransferGasLimitResult) { - is Result.Failure -> raise(GetFeeError.DataError(feeTransferGasLimitResult.error)) - is Result.Success -> feeTransferGasLimitResult.data - } - - val baseGas = gaslessTransactionRepository.getBaseGasForTransaction() - - val maxTokenFeeGas = initialFee.gasLimit + feeTransferGasLimit + baseGas - - val maxFeePerGas = when (initialFee) { - is Fee.Ethereum.EIP1559 -> initialFee.maxFeePerGas - is Fee.Ethereum.Legacy -> initialFee.gasPrice - is Fee.Ethereum.TokenCurrency -> raiseIllegalStateError("initialFee could only be native") - } - - val feeInNativeCurrency = maxTokenFeeGas - .multiply(maxFeePerGas) - .multiply(GAS_PRICE_MULTIPLIER.toBigInteger()) - - val nativeFiatRate = nativeCurrencyStatus.value.fiatRate ?: raiseIllegalStateError("fiatRate is null") - val tokenFiatRate = tokenForPayFeeStatus.value.fiatRate ?: raiseIllegalStateError("fiatRate is null") - val coinPriceInToken = nativeFiatRate.divide( - tokenFiatRate, - maxOf(nativeCurrencyStatus.currency.decimals, tokenForPayFee.decimals), - RoundingMode.DOWN, - ) - - val feeInTokenCurrency = coinPriceInToken.multiply(feeInNativeCurrency.toBigDecimal()) - - val tokenBalance = tokenForPayFeeStatus.value.amount ?: BigDecimal.ZERO - if (tokenBalance < feeInTokenCurrency) { - raise(GaslessError.NotEnoughFunds) - } - - val amount = createTokenAmount(tokenForPayFee, feeInTokenCurrency) - - val fee = Fee.Ethereum.TokenCurrency( - amount = amount, - gasLimit = maxTokenFeeGas, - coinPriceInToken = coinPriceInToken, - feeTransferGasLimit = feeTransferGasLimit, - baseGas = baseGas, - ) - return TransactionFeeExtended( - transactionFee = TransactionFee.Single(normal = fee), - feeTokenId = tokenForPayFee.id, - ) - } - - private fun createTokenAmount(token: CryptoCurrency.Token, value: BigDecimal): Amount = Amount( - token = Token( - symbol = token.symbol, - contractAddress = token.contractAddress, - decimals = token.decimals, - ), - value = value, - ) - - private suspend fun Raise.demoTransactionSender( - userWallet: UserWallet, - network: Network, - ): DemoTransactionSender { - return DemoTransactionSender( - walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network) - ?: raiseIllegalStateError("WalletManager is null"), - ) - } - - private fun Raise.raiseIllegalStateError(error: String): Nothing { - raise(GetFeeError.DataError(IllegalStateException(error))) + ).bind() } private companion object { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForTokenUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForTokenUseCase.kt index f33773dca4..c1d82c2b18 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForTokenUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/GetFeeForTokenUseCase.kt @@ -1,15 +1,101 @@ package com.tangem.domain.transaction.usecase.gasless import arrow.core.Either -import arrow.core.left +import arrow.core.raise.Raise +import arrow.core.raise.either +import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.transaction.GaslessTransactionRepository import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.TransactionFeeExtended +import com.tangem.domain.transaction.raiseIllegalStateError +import com.tangem.domain.walletmanager.WalletManagersFacade -class GetFeeForTokenUseCase { +class GetFeeForTokenUseCase( + private val gaslessTransactionRepository: GaslessTransactionRepository, + private val walletManagersFacade: WalletManagersFacade, + private val demoConfig: DemoConfig, + private val currenciesRepository: CurrenciesRepository, + private val getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase, +) { - operator fun invoke(token: CryptoCurrency, transactionData: TransactionData): Either { - return GetFeeError.UnknownError.left() + private val tokenFeeCalculator = TokenFeeCalculator( + walletManagersFacade = walletManagersFacade, + gaslessTransactionRepository = gaslessTransactionRepository, + demoConfig = demoConfig, + ) + + suspend operator fun invoke( + userWallet: UserWallet, + token: CryptoCurrency, + transactionData: TransactionData, + ): Either { + return either { + if (!gaslessTransactionRepository.isNetworkSupported(token.network)) { + raise(GetFeeError.GaslessError.NetworkIsNotSupported) + } + + val walletManager = prepareWalletManager(userWallet, token.network) + + val initialTxFee = tokenFeeCalculator.calculateInitialFee( + userWallet = userWallet, + network = token.network, + walletManager = walletManager, + transactionData = transactionData, + ).bind() + + val initialFeeEth = initialTxFee.normal as? Fee.Ethereum + ?: raiseIllegalStateError( + error = "only Fee.Ethereum supported, but was different", + ) + + val nativeCurrency = currenciesRepository.getNetworkCoin( + userWalletId = userWallet.walletId, + networkId = token.network.id, + derivationPath = token.network.derivationPath, + ) + + val userCurrenciesStatusesByNetwork = getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync( + userWallet.walletId, + ).getOrNull()?.filter { + it.currency.network.id == token.network.id + } ?: raiseIllegalStateError("currencies list is null for userWalletId=${userWallet.walletId}") + + val nativeCurrencyStatus = userCurrenciesStatusesByNetwork.find { + it.currency.id == nativeCurrency.id + } ?: raiseIllegalStateError("native currency not found for network ${token.network.id}") + + val tokenCurrencyStatus = userCurrenciesStatusesByNetwork.find { + it.currency.id == token.id + } ?: raiseIllegalStateError("token currency not found for network ${token.network.id}") + + tokenFeeCalculator.calculateTokenFee( + walletManager = walletManager, + tokenForPayFeeStatus = tokenCurrencyStatus, + nativeCurrencyStatus = nativeCurrencyStatus, + initialFee = initialFeeEth, + ).bind() + } + } + + @Suppress("NullableToStringCall") + private suspend fun Raise.prepareWalletManager( + userWallet: UserWallet, + network: Network, + ): EthereumWalletManager { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + network = network, + ) + val ethereumWalletManager = walletManager as? EthereumWalletManager + ?: raiseIllegalStateError("WalletManager type ${walletManager?.javaClass?.name} not supported") + return ethereumWalletManager } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt new file mode 100644 index 0000000000..392aa39519 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/TokenFeeCalculator.kt @@ -0,0 +1,158 @@ +package com.tangem.domain.transaction.usecase.gasless + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.either +import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager +import com.tangem.blockchain.blockchains.ethereum.tokenmethods.TransferERC20TokenCallData +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Token +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.domain.demo.DemoTransactionSender +import com.tangem.domain.demo.models.DemoConfig +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.transaction.GaslessTransactionRepository +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.error.GetFeeError.GaslessError +import com.tangem.domain.transaction.error.mapToFeeError +import com.tangem.domain.transaction.models.TransactionFeeExtended +import com.tangem.domain.transaction.raiseIllegalStateError +import com.tangem.domain.walletmanager.WalletManagersFacade +import java.math.BigDecimal +import java.math.RoundingMode + +internal class TokenFeeCalculator( + private val walletManagersFacade: WalletManagersFacade, + private val gaslessTransactionRepository: GaslessTransactionRepository, + private val demoConfig: DemoConfig, +) { + + suspend fun calculateInitialFee( + userWallet: UserWallet, + network: Network, + walletManager: EthereumWalletManager, + transactionData: TransactionData, + ): Either { + return either { + val transactionSender = if (userWallet is UserWallet.Cold && + demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId) + ) { + demoTransactionSender(userWallet, network) + } else { + walletManager + } + + val maybeFee = when (val result = transactionSender.getFee(transactionData = transactionData)) { + is Result.Success -> result.data + is Result.Failure -> raise(result.mapToFeeError()) + } + maybeFee + } + } + + suspend fun calculateTokenFee( + walletManager: EthereumWalletManager, + tokenForPayFeeStatus: CryptoCurrencyStatus, + nativeCurrencyStatus: CryptoCurrencyStatus, + initialFee: Fee.Ethereum, + ): Either { + return either { + val tokenForPayFee = tokenForPayFeeStatus.currency as? CryptoCurrency.Token + ?: raiseIllegalStateError("only tokens are supported") + + val transferFeeToContractAmount = createTokenAmount( + token = tokenForPayFee, + value = BigDecimal(FEE_TRANSFER_AMOUNT), + ) + val feeDestination = gaslessTransactionRepository.getTokenFeeReceiverAddress() + val feeTransferGasLimitResult = walletManager.getGasLimit( + amount = transferFeeToContractAmount, + destination = feeDestination, + callData = TransferERC20TokenCallData( + destination = feeDestination, + amount = transferFeeToContractAmount, + ), + ) + + val feeTransferGasLimit = when (feeTransferGasLimitResult) { + is Result.Failure -> raise(GetFeeError.DataError(feeTransferGasLimitResult.error)) + is Result.Success -> feeTransferGasLimitResult.data + } + + val baseGas = gaslessTransactionRepository.getBaseGasForTransaction() + + val maxTokenFeeGas = initialFee.gasLimit + feeTransferGasLimit + baseGas + + val maxFeePerGas = when (initialFee) { + is Fee.Ethereum.EIP1559 -> initialFee.maxFeePerGas + is Fee.Ethereum.Legacy -> initialFee.gasPrice + is Fee.Ethereum.TokenCurrency -> raiseIllegalStateError("initialFee could only be native") + } + + val feeInNativeCurrency = maxTokenFeeGas + .multiply(maxFeePerGas) + .multiply(GAS_PRICE_MULTIPLIER.toBigInteger()) + + val nativeFiatRate = nativeCurrencyStatus.value.fiatRate ?: raiseIllegalStateError("fiatRate is null") + val tokenFiatRate = tokenForPayFeeStatus.value.fiatRate ?: raiseIllegalStateError("fiatRate is null") + val coinPriceInToken = nativeFiatRate.divide( + tokenFiatRate, + maxOf(nativeCurrencyStatus.currency.decimals, tokenForPayFee.decimals), + RoundingMode.DOWN, + ) + + val feeInTokenCurrency = coinPriceInToken.multiply(feeInNativeCurrency.toBigDecimal()) + + val tokenBalance = tokenForPayFeeStatus.value.amount ?: BigDecimal.ZERO + if (tokenBalance < feeInTokenCurrency) { + raise(GaslessError.NotEnoughFunds) + } + + val amount = createTokenAmount(tokenForPayFee, feeInTokenCurrency) + + val fee = Fee.Ethereum.TokenCurrency( + amount = amount, + gasLimit = maxTokenFeeGas, + coinPriceInToken = coinPriceInToken, + feeTransferGasLimit = feeTransferGasLimit, + baseGas = baseGas, + ) + TransactionFeeExtended( + transactionFee = TransactionFee.Single(normal = fee), + feeTokenId = tokenForPayFee.id, + ) + } + } + + private fun createTokenAmount(token: CryptoCurrency.Token, value: BigDecimal): Amount = Amount( + token = Token( + symbol = token.symbol, + contractAddress = token.contractAddress, + decimals = token.decimals, + ), + value = value, + ) + + private suspend fun Raise.demoTransactionSender( + userWallet: UserWallet, + network: Network, + ): DemoTransactionSender { + return DemoTransactionSender( + walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network) + ?: raiseIllegalStateError("WalletManager is null"), + ) + } + + private companion object { + /** Amount in token units for fee transfer */ + const val FEE_TRANSFER_AMOUNT = 10000 + /** Gas price safety multiplier for fee calculation */ + const val GAS_PRICE_MULTIPLIER = 2 + } +} \ No newline at end of file