From 83571f3bd571f2662171e402c77e4c634fa0edb7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Jan 2023 17:01:04 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/proxy/TransactionManagerImpl.kt | 6 + .../tangem/tap/proxy/UserWalletManagerImpl.kt | 5 + .../tangem/datasource/di/OneInchApisModule.kt | 3 + .../feature/swap/converters/SwapConverter.kt | 5 +- .../swap/domain/AllowPermissionsHandler.kt | 8 + .../domain/AllowPermissionsHandlerImpl.kt | 19 ++ .../feature/swap/domain/SwapInteractor.kt | 5 + .../feature/swap/domain/SwapInteractorImpl.kt | 232 ++++++++++++++---- .../swap/domain/cache/SwapDataCache.kt | 6 + .../swap/domain/cache/SwapDataCacheImpl.kt | 18 ++ .../swap/domain/di/SwapDomainModule.kt | 2 + .../feature/swap/domain/models/QuoteModel.kt | 2 - .../swap/domain/models/SwapDataModel.kt | 6 +- .../feature/swap/domain/models/SwapState.kt | 22 +- .../domain/models/cache/SwapDataHolder.kt | 4 + .../tangem/feature/swap/ui/StateBuilder.kt | 34 ++- .../feature/swap/viewmodels/SwapViewModel.kt | 32 ++- .../tangem/lib/crypto/TransactionManager.kt | 2 + .../tangem/lib/crypto/UserWalletManager.kt | 6 + 19 files changed, 360 insertions(+), 57 deletions(-) create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/AllowPermissionsHandler.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/AllowPermissionsHandlerImpl.kt diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index bec8c6eebe..fc7d49d169 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -66,6 +66,12 @@ class TransactionManagerImpl( return Blockchain.fromNetworkId(networkId)?.decimals() ?: error("blockchain not found") } + override fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + val gasPriceValue = requireNotNull(gasPrice.toLongOrNull()) { "gasprice should be Long" } + return (gasPriceValue * estimatedGas).toBigDecimal().movePointLeft(blockchain.decimals()) + } + @Throws(IllegalStateException::class) override suspend fun getFee( networkId: String, diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 9ba2438963..991c499fb4 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -131,6 +131,11 @@ class UserWalletManagerImpl( ) } + override fun getCurrencyByNetworkId(networkId: String): String { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + return blockchain.currency + } + private fun addNativeTokenToWalletAction(token: NativeToken, card: CardDTO): Action { val blockchain = Blockchain.fromNetworkId(token.networkId) val scanResponse = appStateHolder.scanResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt index 55ee683715..7befbaaaf6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt @@ -23,6 +23,7 @@ class OneInchApisModule { val apiFactory = OneInchApiFactory() apiFactory.putApi(ETH_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_ETH_PATH, moshi)) apiFactory.putApi(BSC_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_BSC_PATH, moshi)) + apiFactory.putApi(POLYGON_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_POLYGON_PATH, moshi)) return apiFactory } @@ -45,8 +46,10 @@ class OneInchApisModule { private const val ONE_INCH_BASE_URL = "https://api.1inch.io/v5.0/" private const val ONE_INCH_ETH_PATH = "1/" private const val ONE_INCH_BSC_PATH = "56/" + private const val ONE_INCH_POLYGON_PATH = "137/" private const val ETH_NETWORK = "ethereum" private const val BSC_NETWORK = "binance-smart-chain" + private const val POLYGON_NETWORK = "polygon-pos" } } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt index ebe2d92a13..31df2484b4 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.api.oneinch.models.SwapResponse import com.tangem.datasource.api.oneinch.models.TransactionDto import com.tangem.feature.swap.domain.models.SwapDataModel import com.tangem.feature.swap.domain.models.TransactionModel +import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.utils.converter.Converter import javax.inject.Inject @@ -13,8 +14,8 @@ class SwapConverter @Inject constructor() : Converter() + + override fun addAddressToInProgress(tokenAddress: String) { + allowPermissionsInProgress.add(tokenAddress) + } + + override fun removeAddressFromProgress(tokenAddress: String) { + allowPermissionsInProgress.remove(tokenAddress) + } + + override fun isAddressAllowanceInProgress(tokenAddress: String): Boolean { + return allowPermissionsInProgress.contains(tokenAddress) + } +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 421b21d439..dbbbbe6fdb 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -8,6 +8,11 @@ interface SwapInteractor { suspend fun getTokensToSwap(networkId: String): List + /** + * Give permission to swap + * + * @param tokenToApprove use token which you want to swap + */ @Throws(IllegalStateException::class) suspend fun givePermissionToSwap(tokenToApprove: Currency) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 482f6c34a5..919c71d9ab 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2,15 +2,17 @@ package com.tangem.feature.swap.domain import com.tangem.feature.swap.domain.cache.SwapDataCache import com.tangem.feature.swap.domain.converters.CryptoCurrencyConverter +import com.tangem.feature.swap.domain.models.ApproveModel import com.tangem.feature.swap.domain.models.Currency import com.tangem.feature.swap.domain.models.DataError -import com.tangem.feature.swap.domain.models.QuoteModel +import com.tangem.feature.swap.domain.models.PermissionDataState import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.SwapState import com.tangem.feature.swap.domain.models.TransactionModel import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager +import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.toFiatString import com.tangem.utils.toFormattedString import java.math.BigDecimal @@ -21,6 +23,7 @@ internal class SwapInteractorImpl @Inject constructor( private val userWalletManager: UserWalletManager, private val repository: SwapRepository, private val cache: SwapDataCache, + private val allowPermissionsHandler: AllowPermissionsHandler, ) : SwapInteractor { private val cryptoCurrencyConverter = CryptoCurrencyConverter() @@ -40,9 +43,10 @@ internal class SwapInteractorImpl @Inject constructor( if (tokenToApprove is Currency.NonNativeToken) { val estimatedGas = requireNotNull(cache.getLastQuote()?.estimatedGas) { "estimatedGas not found call findBestQuote" } - val transactionData = repository.dataToApprove(networkId, tokenToApprove.contractAddress) + val transactionData = + requireNotNull(cache.getApproveTransactionData()) { "getApproveTransactionData not found, call findQuotes" } val gasPrice = transactionData.gasPrice.toBigDecimalOrNull() ?: error("cannot parse gasPrice") - val fee = gasPrice.multiply(estimatedGas.toBigDecimal()).movePointLeft(tokenToApprove.decimalCount) + val fee = transactionManager.calculateFee(networkId, gasPrice.toPlainString(), estimatedGas) val result = transactionManager.sendTransaction( networkId = networkId, amountToSend = BigDecimal.ZERO, @@ -51,43 +55,46 @@ internal class SwapInteractorImpl @Inject constructor( destinationAddress = transactionData.toAddress, dataToSign = transactionData.data, ) + when (result) { + SendTxResult.Success -> { + allowPermissionsHandler.addAddressToInProgress(tokenToApprove.contractAddress) + } + SendTxResult.UserCancelledError -> TODO() + is SendTxResult.BlockchainSdkError -> TODO() + is SendTxResult.TangemSdkError -> TODO() + is SendTxResult.UnknownError -> TODO() + } } } } override suspend fun findBestQuote(fromToken: Currency, toToken: Currency, amount: SwapAmount): SwapState { - val networkId = cache.getNetworkId() + val networkId = requireNotNull(cache.getNetworkId()) { "no networkId found, please call getTokensToSwap first" } val fromTokenAddress = getTokenAddress(fromToken) val toTokenAddress = getTokenAddress(toToken) - val isAllowedToSpend = if (networkId.isNullOrEmpty()) { - error("no networkId found, please call getTokensToSwap first") - } else { - val allowance = - repository.checkTokensSpendAllowance( - networkId = networkId, - tokenAddress = fromTokenAddress, - walletAddress = userWalletManager.getWalletAddress(networkId), - ) - allowance.error == DataError.NO_ERROR && allowance.dataModel != "0" - } - repository.findBestQuote( - networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - amount = amount.toStringWithRightOffset(), - ).let { quotes -> - val quoteDataModel = quotes.dataModel - if (quoteDataModel != null) { - cache.cacheSwapParams( - quoteModel = quoteDataModel.copy(isAllowedToSpend = isAllowedToSpend), - amount = amount, - fromCurrency = fromToken, - toCurrency = toToken, - ) - return updateBalances(networkId, fromToken, toToken, quoteDataModel) - } else { - return SwapState.SwapError(quotes.error) + val isAllowedToSpend = checkAllowance(networkId, fromTokenAddress) + val isBalanceZero = isNotZeroBalance(fromToken, networkId) + return if (isAllowedToSpend && isBalanceZero) { + if (allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { + allowPermissionsHandler.removeAddressFromProgress(toTokenAddress) } + loadSwapData( + networkId = networkId, + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + fromToken = fromToken, + toToken = toToken, + amount = amount, + ) + } else { + loadQuoteData( + networkId = networkId, + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + amount = amount, + fromToken = fromToken, + toToken = toToken, + ) } } @@ -128,11 +135,120 @@ internal class SwapInteractorImpl @Inject constructor( } } + private suspend fun checkAllowance(networkId: String, fromTokenAddress: String): Boolean { + val allowance = repository.checkTokensSpendAllowance( + networkId = networkId, + tokenAddress = fromTokenAddress, + walletAddress = userWalletManager.getWalletAddress(networkId), + ) + return allowance.error == DataError.NO_ERROR && allowance.dataModel != ZERO_BALANCE + } + + /** + * Load quote data calls only if spend is not allowed for token contract address + */ + private suspend fun loadQuoteData( + networkId: String, + fromTokenAddress: String, + toTokenAddress: String, + amount: SwapAmount, + fromToken: Currency, + toToken: Currency, + ): SwapState { + repository.findBestQuote( + networkId = networkId, + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + amount = amount.toStringWithRightOffset(), + ).let { quotes -> + val quoteDataModel = quotes.dataModel + if (quoteDataModel != null) { + cache.cacheSwapParams( + quoteModel = quoteDataModel, + amount = amount, + fromCurrency = fromToken, + toCurrency = toToken, + ) + val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken)) + cache.cacheApproveTransactionData(transactionData) + val swapState = updateBalances( + networkId = networkId, + fromToken = fromToken, + toToken = toToken, + fromTokenAmount = quoteDataModel.fromTokenAmount, + toTokenAmount = quoteDataModel.toTokenAmount, + fee = transactionManager.calculateFee( + networkId = networkId, + estimatedGas = quoteDataModel.estimatedGas, + gasPrice = transactionData.gasPrice, + ), + isAllowedToSpend = false, + ) + return updatePermissionState( + networkId = networkId, + fromToken = fromToken, + quotesLoadedState = swapState, + estimatedGas = quoteDataModel.estimatedGas, + transactionData = transactionData, + ) + } else { + return SwapState.SwapError(quotes.error) + } + } + } + + /** + * Load swap data calls only if spend is allowed for token contract address + */ + private suspend fun loadSwapData( + networkId: String, + fromTokenAddress: String, + toTokenAddress: String, + fromToken: Currency, + toToken: Currency, + amount: SwapAmount, + ): SwapState { + repository.prepareSwapTransaction( + networkId = networkId, + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + amount = amount.toStringWithRightOffset(), + slippage = DEFAULT_SLIPPAGE, + fromWalletAddress = getWalletAddress(networkId), + ).let { + val swapData = it.dataModel + if (swapData != null) { + cache.cacheSwapData(swapData) + val swapState = updateBalances( + networkId = networkId, + fromToken = fromToken, + toToken = toToken, + fromTokenAmount = swapData.fromTokenAmount, + toTokenAmount = swapData.toTokenAmount, + fee = transactionManager.calculateFee( + networkId = networkId, + estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS, + gasPrice = swapData.transaction.gasPrice, + ), + isAllowedToSpend = false, + ) + return swapState.copy( + permissionState = PermissionDataState.Empty, + ) + } else { + return SwapState.SwapError(it.error) + } + } + } + private suspend fun updateBalances( networkId: String, fromToken: Currency, toToken: Currency, - quoteModel: QuoteModel, + fromTokenAmount: SwapAmount, + toTokenAmount: SwapAmount, + fee: BigDecimal, + isAllowedToSpend: Boolean, ): SwapState.QuotesLoadedState { val appCurrency = userWalletManager.getUserAppCurrency() val rates = repository.getRates(appCurrency.code, listOf(fromToken.id, toToken.id)) @@ -144,25 +260,58 @@ internal class SwapInteractorImpl @Inject constructor( it.value.toFormattedString(it.decimals) } return SwapState.QuotesLoadedState( - fromTokenAmount = quoteModel.fromTokenAmount, - toTokenAmount = quoteModel.toTokenAmount, - fromTokenAddress = quoteModel.fromTokenAddress, - toTokenAddress = quoteModel.toTokenAddress, - estimatedGas = quoteModel.estimatedGas, - isAllowedToSpend = quoteModel.isAllowedToSpend, + fromTokenAmount = fromTokenAmount, + toTokenAmount = toTokenAmount, + fromTokenAddress = getTokenAddress(fromToken), + toTokenAddress = getTokenAddress(toToken), + fee = fee.toPlainString(), + isAllowedToSpend = isAllowedToSpend, fromTokenWalletBalance = fromTokenBalance ?: ZERO_BALANCE, - fromTokenFiatBalance = quoteModel.fromTokenAmount.value.toFiatString( + fromTokenFiatBalance = fromTokenAmount.value.toFiatString( rates[fromToken.id]?.toBigDecimal() ?: BigDecimal.ZERO, appCurrency.symbol, ), toTokenWalletBalance = toTokenBalance ?: ZERO_BALANCE, - toTokenFiatBalance = quoteModel.fromTokenAmount.value.toFiatString( + toTokenFiatBalance = fromTokenAmount.value.toFiatString( rates[fromToken.id]?.toBigDecimal() ?: BigDecimal.ZERO, appCurrency.symbol, ), ) } + private fun updatePermissionState( + networkId: String, + fromToken: Currency, + quotesLoadedState: SwapState.QuotesLoadedState, + estimatedGas: Int, + transactionData: ApproveModel, + ): SwapState.QuotesLoadedState { + if (allowPermissionsHandler.isAddressAllowanceInProgress(fromToken.networkId)) { + return quotesLoadedState.copy( + permissionState = PermissionDataState.PermissionLoading, + ) + } + return quotesLoadedState.copy( + permissionState = PermissionDataState.PermissionReadyForRequest( + currency = userWalletManager.getCurrencyByNetworkId(networkId), + amount = "infinite", //FIXME + walletAddress = getWalletAddress(networkId), + spenderAddress = transactionData.toAddress, + fee = transactionManager.calculateFee( + networkId = networkId, + gasPrice = transactionData.gasPrice, + estimatedGas = estimatedGas, + ).toPlainString(), + ), + ) + } + + private fun isNotZeroBalance(fromToken: Currency, networkId: String): Boolean { + /** to compare [BigDecimal] use only comparator */ + return (userWalletManager.getCurrentWalletTokensBalance(networkId)[fromToken.symbol]?.value + ?: BigDecimal.ZERO).compareTo(BigDecimal.ZERO) != 0 + } + private fun getWalletAddress(networkId: String): String { return userWalletManager.getWalletAddress(networkId) } @@ -185,5 +334,6 @@ internal class SwapInteractorImpl @Inject constructor( companion object { private const val DEFAULT_SLIPPAGE = 2 private const val ZERO_BALANCE = "0" + private const val DEFAULT_GAS = 300000 } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt index 6acefb6794..073920c985 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt @@ -1,17 +1,23 @@ package com.tangem.feature.swap.domain.cache +import com.tangem.feature.swap.domain.models.ApproveModel import com.tangem.feature.swap.domain.models.Currency import com.tangem.feature.swap.domain.models.QuoteModel import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.SwapDataModel import com.tangem.feature.swap.domain.models.cache.ExchangeCurrencies interface SwapDataCache { fun cacheNetworkId(networkId: String) fun cacheSwapParams(quoteModel: QuoteModel, amount: SwapAmount, fromCurrency: Currency, toCurrency: Currency) + fun cacheApproveTransactionData(approve: ApproveModel) + fun cacheSwapData(swapDataModel: SwapDataModel) fun cacheAvailableToSwapTokens(networkId: String, tokens: List) fun getAvailableTokens(networkId: String): List + fun getApproveTransactionData(): ApproveModel? fun getLastQuote(): QuoteModel? + fun getLastSwapData(): SwapDataModel? fun getAmountToSwap(): SwapAmount? fun getNetworkId(): String? fun getExchangeCurrencies(): ExchangeCurrencies? diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt index 0431b67ff0..b5ca9e38a1 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt @@ -1,8 +1,10 @@ package com.tangem.feature.swap.domain.cache +import com.tangem.feature.swap.domain.models.ApproveModel import com.tangem.feature.swap.domain.models.Currency import com.tangem.feature.swap.domain.models.QuoteModel import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.SwapDataModel import com.tangem.feature.swap.domain.models.cache.ExchangeCurrencies import com.tangem.feature.swap.domain.models.cache.SwapDataHolder @@ -28,6 +30,18 @@ class SwapDataCacheImpl : SwapDataCache { ) } + override fun cacheSwapData(swapDataModel: SwapDataModel) { + lastDataForSwap = lastDataForSwap.copy(swapModel = swapDataModel) + } + + override fun getLastSwapData(): SwapDataModel? { + return lastDataForSwap.swapModel + } + + override fun cacheApproveTransactionData(approve: ApproveModel) { + lastDataForSwap = lastDataForSwap.copy(approveTxModel = approve) + } + override fun getExchangeCurrencies(): ExchangeCurrencies? { return lastDataForSwap.exchangeCurrencies } @@ -48,6 +62,10 @@ class SwapDataCacheImpl : SwapDataCache { return availableTokensForNetwork.getOrElse(networkId) { emptyList() } } + override fun getApproveTransactionData(): ApproveModel? { + return lastDataForSwap.approveTxModel + } + override fun getLastQuote(): QuoteModel? { return lastDataForSwap.quoteModel } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index eb5d59171e..c5e2782f7e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.domain.di +import com.tangem.feature.swap.domain.AllowPermissionsHandlerImpl import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.SwapInteractorImpl import com.tangem.feature.swap.domain.SwapRepository @@ -28,6 +29,7 @@ class SwapDomainModule { userWalletManager = userWalletManager, repository = swapRepository, cache = SwapDataCacheImpl(), + allowPermissionsHandler = AllowPermissionsHandlerImpl(), ) } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/QuoteModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/QuoteModel.kt index 1d4aa9afdb..14b36eed8b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/QuoteModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/QuoteModel.kt @@ -8,7 +8,6 @@ package com.tangem.feature.swap.domain.models * @property fromTokenAddress address token you want to exchange * @property toTokenAddress address token you want to receive * @property estimatedGas fee - * @property isAllowedToSpend does spend allowed */ data class QuoteModel( val fromTokenAmount: SwapAmount, @@ -16,5 +15,4 @@ data class QuoteModel( val fromTokenAddress: String, val toTokenAddress: String, val estimatedGas: Int, - val isAllowedToSpend: Boolean = false, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapDataModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapDataModel.kt index f18018c63e..8af39f1219 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapDataModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapDataModel.kt @@ -11,7 +11,7 @@ package com.tangem.feature.swap.domain.models data class SwapDataModel( val fromTokenAddress: String, val toTokenAddress: String, - val toTokenAmount: String, - val fromTokenAmount: String, - val transaction: TransactionModel + val toTokenAmount: SwapAmount, + val fromTokenAmount: SwapAmount, + val transaction: TransactionModel, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapState.kt index 82bf5808f5..60ec7a9471 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapState.kt @@ -1,5 +1,7 @@ package com.tangem.feature.swap.domain.models +import java.math.BigDecimal + sealed interface SwapState { data class QuotesLoadedState( @@ -11,8 +13,9 @@ sealed interface SwapState { val fromTokenFiatBalance: String, val toTokenWalletBalance: String, val toTokenFiatBalance: String, - val estimatedGas: Int, + val fee: String, val isAllowedToSpend: Boolean = false, + val permissionState: PermissionDataState = PermissionDataState.Empty, ) : SwapState data class SwapSuccess( @@ -23,4 +26,21 @@ sealed interface SwapState { data class SwapError( val errorType: DataError, ) : SwapState +} + +sealed class PermissionDataState { + + data class PermissionReadyForRequest( + val currency: String, + val amount: String, + val walletAddress: String, + val spenderAddress: String, + val fee: String, + ) : PermissionDataState() + + object PermissionFailed : PermissionDataState() + + object PermissionLoading : PermissionDataState() + + object Empty : PermissionDataState() } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/cache/SwapDataHolder.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/cache/SwapDataHolder.kt index 41e159253f..764ec2f37e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/cache/SwapDataHolder.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/cache/SwapDataHolder.kt @@ -1,11 +1,15 @@ package com.tangem.feature.swap.domain.models.cache +import com.tangem.feature.swap.domain.models.ApproveModel import com.tangem.feature.swap.domain.models.Currency import com.tangem.feature.swap.domain.models.QuoteModel import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.SwapDataModel data class SwapDataHolder( val quoteModel: QuoteModel? = null, + val swapModel: SwapDataModel? = null, + val approveTxModel: ApproveModel? = null, val amountToSwap: SwapAmount? = null, val networkId: String? = null, val exchangeCurrencies: ExchangeCurrencies? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 067ecab8bf..5f7817f92c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1,11 +1,15 @@ package com.tangem.feature.swap.ui import com.tangem.feature.swap.domain.models.Currency +import com.tangem.feature.swap.domain.models.PermissionDataState import com.tangem.feature.swap.domain.models.SwapState import com.tangem.feature.swap.domain.models.formatToUIRepresentation +import com.tangem.feature.swap.models.ApprovePermissionButton +import com.tangem.feature.swap.models.CancelPermissionButton import com.tangem.feature.swap.models.FeeState import com.tangem.feature.swap.models.SwapButton import com.tangem.feature.swap.models.SwapCardData +import com.tangem.feature.swap.models.SwapPermissionStateHolder import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.SwapWarning import com.tangem.feature.swap.models.TransactionCardType @@ -75,6 +79,7 @@ class StateBuilder { quoteModel: SwapState.QuotesLoadedState, fromToken: Currency, onSwapClick: () -> Unit, + onGivePermissionClick: () -> Unit, ): SwapStateHolder { return uiStateHolder.copy( sendCardData = SwapCardData( @@ -102,7 +107,8 @@ class StateBuilder { } else { emptyList() }, - fee = FeeState.Loaded(quoteModel.estimatedGas.toString()), + permissionState = convertPermissionState(quoteModel.permissionState, onGivePermissionClick), + fee = FeeState.Loaded(quoteModel.fee), swapButton = SwapButton( enabled = quoteModel.isAllowedToSpend, loading = false, @@ -110,4 +116,30 @@ class StateBuilder { ), ) } + + private fun convertPermissionState( + permissionDataState: PermissionDataState, + onGivePermissionClick: () -> Unit, + ): SwapPermissionStateHolder? { + return when (permissionDataState) { + PermissionDataState.Empty -> null + PermissionDataState.PermissionFailed -> null + PermissionDataState.PermissionLoading -> null + is PermissionDataState.PermissionReadyForRequest -> SwapPermissionStateHolder( + currency = permissionDataState.currency, + amount = permissionDataState.amount, + walletAddress = permissionDataState.walletAddress, + spenderAddress = permissionDataState.spenderAddress, + fee = permissionDataState.fee, + approveButton = ApprovePermissionButton( + enabled = true, + onClick = onGivePermissionClick, + ), + cancelButton = CancelPermissionButton( + enabled = true, + onClick = {}, + ), + ) + } + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index dfcf661be7..1ba7b06511 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -98,9 +98,13 @@ internal class SwapViewModel @Inject constructor( .onSuccess { swapState -> when (swapState) { is SwapState.QuotesLoadedState -> { - uiState = stateBuilder.createQuotesLoadedState(uiState, swapState, fromToken) { - onSwapClick(toToken, swapState) - } + uiState = stateBuilder.createQuotesLoadedState( + uiStateHolder = uiState, + quoteModel = swapState, + fromToken = fromToken, + onSwapClick = { onSwapClick(toToken, swapState) }, + onGivePermissionClick = { givePermissionsToSwap(fromToken) }, + ) } is SwapState.SwapSuccess -> { //todo implement @@ -116,10 +120,12 @@ internal class SwapViewModel @Inject constructor( private fun onSwapClick(toToken: Currency, quoteModel: SwapState.QuotesLoadedState) { viewModelScope.launch(dispatchers.main) { runCatching { - if (!quoteModel.isAllowedToSpend) { - swapInteractor.givePermissionToSwap(toToken) - } else { - swapInteractor.onSwap() + withContext(dispatchers.io) { + if (!quoteModel.isAllowedToSpend) { + swapInteractor.givePermissionToSwap(toToken) + } else { + swapInteractor.onSwap() + } } } .onSuccess { @@ -137,6 +143,18 @@ internal class SwapViewModel @Inject constructor( } } + private fun givePermissionsToSwap(tokenToApprove: Currency) { + viewModelScope.launch(dispatchers.main) { + runCatching { + withContext(dispatchers.io) { + swapInteractor.givePermissionToSwap(tokenToApprove) + } + } + .onSuccess { } + .onFailure { } + } + } + companion object { private const val MIN_TOKENS_IN_LIST = 2 private const val INITIAL_AMOUNT = "1" diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 6da184fb5a..34f2f3d06b 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -29,4 +29,6 @@ interface TransactionManager { @Throws(IllegalStateException::class) fun getNativeTokenDecimals(networkId: String): Int + + fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index b29cfc2535..3faa50aa4c 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -53,4 +53,10 @@ interface UserWalletManager { * Returns selected app currency */ fun getUserAppCurrency(): ProxyFiatCurrency + + /** + * @param networkId + * @return currency name + */ + fun getCurrencyByNetworkId(networkId: String): String } \ No newline at end of file