diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 17be87d7b4..042bf0f667 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -28,7 +28,7 @@ import com.tangem.tap.store import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import com.tangem.feature.swap.domain.models.Currency as SwapCurrency +import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency class TradeCryptoMiddleware { fun handle(state: () -> AppState?, action: WalletAction.TradeCryptoAction) { 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 fc7d49d169..ba55c7532b 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -27,7 +27,6 @@ import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.store import com.tangem.tap.tangemSdk import java.math.BigDecimal -import java.math.BigInteger class TransactionManagerImpl( private val appStateHolder: AppStateHolder, @@ -38,6 +37,7 @@ class TransactionManagerImpl( amountToSend: BigDecimal, currencyToSend: Currency, feeAmount: BigDecimal, + estimatedGas: Int, destinationAddress: String, dataToSign: String, ): SendTxResult { @@ -49,7 +49,7 @@ class TransactionManagerImpl( amount = amount, fee = Amount(value = feeAmount, blockchain = blockchain), destination = destinationAddress, - ).copy(hash = dataToSign, extras = createExtras(walletManager, feeAmount, dataToSign)) + ).copy(hash = dataToSign, extras = createExtras(walletManager, estimatedGas, dataToSign)) val signer = transactionSigner(walletManager) @@ -95,10 +95,6 @@ class TransactionManagerImpl( } } - override fun getNativeAddress(networkId: String): String { - return "" //todo implement - } - private fun handleSendResult(result: SimpleResult): SendTxResult { when (result) { is SimpleResult.Success -> { @@ -155,14 +151,14 @@ class TransactionManagerImpl( private fun createExtras( walletManager: WalletManager, - feeAmount: BigDecimal, + estimatedGas: Int, transactionHash: String, ): TransactionExtras? { return when (walletManager) { is EthereumWalletManager -> { return EthereumTransactionExtras( data = transactionHash.removePrefix(HEX_PREFIX).hexToBytes(), - gasLimit = BigInteger.valueOf(DEFAULT_GAS_LIMIT), + gasLimit = estimatedGas.toBigInteger(), ) } else -> { 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 991c499fb4..95818b9980 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -27,13 +27,13 @@ class UserWalletManagerImpl( private val walletManagerFactory: WalletManagerFactory, ) : UserWalletManager { - override suspend fun getUserTokens(): List { + override suspend fun getUserTokens(networkId: String): List { val card = appStateHolder.getActualCard() val userTokensRepository = requireNotNull(appStateHolder.userTokensRepository) { "userTokensRepository is null" } - if (card != null) { + return if (card != null) { userTokensRepository.getUserTokens(card) - .filter { it.isToken() } + .filter { it.blockchain.toNetworkId() == networkId } .map { if (it is com.tangem.tap.features.wallet.models.Currency.Token) { NonNativeToken( @@ -53,8 +53,9 @@ class UserWalletManagerImpl( ) } } + } else { + emptyList() } - return emptyList() } override fun getWalletId(): String { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt index 1f25a391e0..4a72fcccc5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt @@ -7,6 +7,5 @@ data class SwapResponse( @Json(name = "toToken") val toToken: TokenOneInchDto, @Json(name = "toTokenAmount") val toTokenAmount: String, @Json(name = "fromTokenAmount") val fromTokenAmount: String, - @Json(name = "protocols") val protocols: List, - @Json(name = "transaction") val transaction: TransactionDto, + @Json(name = "tx") val transaction: TransactionDto, ) \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/converter/TwoWayConverter.kt b/core/utils/src/main/java/com/tangem/utils/converter/TwoWayConverter.kt new file mode 100644 index 0000000000..444e4048ab --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/converter/TwoWayConverter.kt @@ -0,0 +1,8 @@ +package com.tangem.utils.converter + +interface TwoWayConverter : Converter { + fun convertBack(value: O): I + fun convertListBack(input: List): List { + return input.map { convertBack(it) } + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index 87097ce482..4fbd7b0ccb 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -1,10 +1,31 @@ package com.tangem.utils.coroutines import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext suspend fun runCatching(dispatcher: CoroutineDispatcher, block: suspend () -> R): Result { return runCatching { withContext(dispatcher) { block() } } +} + +class Debouncer { + + private var debounceJob: Job? = null + + fun debounce( + waitMs: Long = 300L, + coroutineScope: CoroutineScope, + destinationFunction: () -> Unit, + ) { + debounceJob?.cancel() + debounceJob = coroutineScope.launch { + delay(waitMs) + destinationFunction.invoke() + } + } } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 10cdf4a902..cdcc527c0e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -10,10 +10,10 @@ import com.tangem.feature.swap.converters.QuotesConverter import com.tangem.feature.swap.converters.SwapConverter import com.tangem.feature.swap.converters.TokensConverter import com.tangem.feature.swap.domain.SwapRepository -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.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.ApproveModel +import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.feature.swap.domain.models.domain.QuoteModel +import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.mapErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -109,6 +109,7 @@ internal class SwapRepositoryImpl @Inject constructor( slippage = slippage, ), ) + AggregatedSwapDataModel(swapConverter.convert(swapResponse)) } catch (ex: OneIncResponseException) { AggregatedSwapDataModel(null, mapErrors(ex.data.description)) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt index 4d3574869d..758763e942 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.converters import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse -import com.tangem.feature.swap.domain.models.ApproveModel +import com.tangem.feature.swap.domain.models.domain.ApproveModel import com.tangem.utils.converter.Converter import javax.inject.Inject diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt index 57f0569e9c..b867bb47cf 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.converters import com.tangem.datasource.api.oneinch.models.QuoteResponse -import com.tangem.feature.swap.domain.models.QuoteModel +import com.tangem.feature.swap.domain.models.domain.QuoteModel import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.utils.converter.Converter import javax.inject.Inject 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 31df2484b4..b580b090af 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 @@ -2,8 +2,8 @@ package com.tangem.feature.swap.converters 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.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.TransactionModel import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.utils.converter.Converter import javax.inject.Inject diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt index 8330c0c5f8..b40a330485 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.converters import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.feature.swap.domain.models.Currency +import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.utils.converter.Converter import javax.inject.Inject 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 dbbbbe6fdb..edef9f475c 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 @@ -1,20 +1,27 @@ package com.tangem.feature.swap.domain -import com.tangem.feature.swap.domain.models.Currency import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.SwapState +import com.tangem.feature.swap.domain.models.cache.ExchangeCurrencies +import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.feature.swap.domain.models.ui.FoundTokensState +import com.tangem.feature.swap.domain.models.ui.SwapState +import com.tangem.feature.swap.domain.models.ui.TokensDataState interface SwapInteractor { - suspend fun getTokensToSwap(networkId: String): List + suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState + + suspend fun onSearchToken(searchQuery: String): FoundTokensState + + fun getExchangeCurrencies(): ExchangeCurrencies? + + fun findTokenById(id: String): Currency? /** * Give permission to swap - * - * @param tokenToApprove use token which you want to swap */ @Throws(IllegalStateException::class) - suspend fun givePermissionToSwap(tokenToApprove: Currency) + suspend fun givePermissionToSwap() @Throws(IllegalStateException::class) suspend fun findBestQuote( 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 919c71d9ab..f49cafea71 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,16 +2,23 @@ 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.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.cache.ExchangeCurrencies +import com.tangem.feature.swap.domain.models.domain.ApproveModel +import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.toStringWithRightOffset +import com.tangem.feature.swap.domain.models.ui.FoundTokensState +import com.tangem.feature.swap.domain.models.ui.PermissionDataState +import com.tangem.feature.swap.domain.models.ui.PreselectTokens +import com.tangem.feature.swap.domain.models.ui.SwapState +import com.tangem.feature.swap.domain.models.ui.TokenBalanceData +import com.tangem.feature.swap.domain.models.ui.TokenWithBalance +import com.tangem.feature.swap.domain.models.ui.TokensDataState import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager +import com.tangem.lib.crypto.models.ProxyAmount +import com.tangem.lib.crypto.models.ProxyFiatCurrency import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.toFiatString import com.tangem.utils.toFormattedString @@ -28,21 +35,91 @@ internal class SwapInteractorImpl @Inject constructor( private val cryptoCurrencyConverter = CryptoCurrencyConverter() - override suspend fun getTokensToSwap(networkId: String): List { + override suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState { + val networkId = initialCurrency.networkId + cache.cacheNetworkId(networkId) val availableTokens = cache.getAvailableTokens(networkId) - return availableTokens.ifEmpty { + val allLoadedTokens = availableTokens.ifEmpty { val tokens = repository.getExchangeableTokens(networkId) cache.cacheAvailableToSwapTokens(networkId, tokens) - cache.cacheNetworkId(networkId) tokens - } + }.filter { it.symbol != initialCurrency.symbol } + + //replace tokens in wallet tokens list with loaded same + val loadedOnWalletsMap = mutableSetOf() + val tokensInWallet = userWalletManager.getUserTokens(networkId) + .filter { it.symbol != initialCurrency.symbol } + .map { token -> + allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let { + loadedOnWalletsMap.add(it.symbol) + it + } ?: cryptoCurrencyConverter.convertBack(token) + } + val loadedTokens = allLoadedTokens + .filter { + !loadedOnWalletsMap.contains(it.symbol) + } + cache.cacheLoadedTokens(loadedTokens) + cache.cacheInWalletTokens(tokensInWallet) + val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId) + val appCurrency = userWalletManager.getUserAppCurrency() + val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id }) + return TokensDataState( + preselectTokens = PreselectTokens( + fromToken = initialCurrency, + toToken = selectToToken(initialCurrency, tokensInWallet, loadedTokens), + ), + foundTokensState = FoundTokensState( + tokensInWallet = getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency), + loadedTokens = loadedTokens.map { TokenWithBalance(it) }, + ), + ) } - override suspend fun givePermissionToSwap(tokenToApprove: Currency) { + override suspend fun onSearchToken(searchQuery: String): FoundTokensState { + val networkId = requireNotNull(cache.getNetworkId()) { "networkId is null" } + val searchQueryLowerCase = searchQuery.lowercase() + val tokensInWallet = cache.getInWalletTokens() + .filter { + it.name.lowercase().contains(searchQueryLowerCase) + || it.symbol.lowercase().contains(searchQueryLowerCase) + } + val loadedTokens = cache.getLoadedTokens() + .filter { + it.name.lowercase().contains(searchQueryLowerCase) + || it.symbol.lowercase().contains(searchQueryLowerCase) + } + val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId) + val appCurrency = userWalletManager.getUserAppCurrency() + val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id }) + return FoundTokensState( + tokensInWallet = getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency), + loadedTokens = loadedTokens.map { TokenWithBalance(it) }, + ) + } + + override fun getExchangeCurrencies(): ExchangeCurrencies? { + return cache.getExchangeCurrencies() + } + + override fun findTokenById(id: String): Currency? { + val tokensInWallet = cache.getInWalletTokens() + val loadedTokens = cache.getLoadedTokens() + return tokensInWallet.firstOrNull { it.id == id } ?: loadedTokens.firstOrNull { it.id == id } + } + + override suspend fun givePermissionToSwap() { cache.getNetworkId()?.let { networkId -> - if (tokenToApprove is Currency.NonNativeToken) { + val currencyToSend = + requireNotNull(cache.getExchangeCurrencies()?.fromCurrency) { "currency is not selected" } + if (currencyToSend is Currency.NonNativeToken) { val estimatedGas = - requireNotNull(cache.getLastQuote()?.estimatedGas) { "estimatedGas not found call findBestQuote" } + increaseByPercents( + TWENTY_FIVE_PERCENTS, + requireNotNull(cache.getLastQuote()?.estimatedGas) { + "estimatedGas not found call findBestQuote" + }, + ) val transactionData = requireNotNull(cache.getApproveTransactionData()) { "getApproveTransactionData not found, call findQuotes" } val gasPrice = transactionData.gasPrice.toBigDecimalOrNull() ?: error("cannot parse gasPrice") @@ -50,14 +127,15 @@ internal class SwapInteractorImpl @Inject constructor( val result = transactionManager.sendTransaction( networkId = networkId, amountToSend = BigDecimal.ZERO, - currencyToSend = cryptoCurrencyConverter.convert(tokenToApprove), + currencyToSend = cryptoCurrencyConverter.convert(currencyToSend), feeAmount = fee, + estimatedGas = estimatedGas, destinationAddress = transactionData.toAddress, dataToSign = transactionData.data, ) when (result) { SendTxResult.Success -> { - allowPermissionsHandler.addAddressToInProgress(tokenToApprove.contractAddress) + allowPermissionsHandler.addAddressToInProgress(currencyToSend.contractAddress) } SendTxResult.UserCancelledError -> TODO() is SendTxResult.BlockchainSdkError -> TODO() @@ -69,12 +147,14 @@ internal class SwapInteractorImpl @Inject constructor( } override suspend fun findBestQuote(fromToken: Currency, toToken: Currency, amount: SwapAmount): SwapState { - val networkId = requireNotNull(cache.getNetworkId()) { "no networkId found, please call getTokensToSwap first" } + val networkId = requireNotNull(cache.getNetworkId()) { "no networkId found, please call initTokens first" } val fromTokenAddress = getTokenAddress(fromToken) val toTokenAddress = getTokenAddress(toToken) val isAllowedToSpend = checkAllowance(networkId, fromTokenAddress) - val isBalanceZero = isNotZeroBalance(fromToken, networkId) - return if (isAllowedToSpend && isBalanceZero) { + val isNotZeroBalance = isNotZeroBalance(fromToken, networkId) + cache.cacheExchangeCurrencies(fromToken, toToken) + cache.cacheAmountToSwap(amount) + return if (isAllowedToSpend && isNotZeroBalance) { if (allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(toTokenAddress) } @@ -94,37 +174,41 @@ internal class SwapInteractorImpl @Inject constructor( amount = amount, fromToken = fromToken, toToken = toToken, + isAllowedToSpend = isAllowedToSpend, ) } } override suspend fun onSwap(): SwapState { - val quoteModel = cache.getLastQuote() - val amountToSwap = cache.getAmountToSwap() - val networkId = cache.getNetworkId() - if (quoteModel != null && amountToSwap != null && networkId != null) { - repository.prepareSwapTransaction( - networkId = networkId, - fromTokenAddress = quoteModel.fromTokenAddress, - toTokenAddress = quoteModel.toTokenAddress, - amount = amountToSwap.toStringWithRightOffset(), - slippage = DEFAULT_SLIPPAGE, - fromWalletAddress = getWalletAddress(networkId), - ).let { - val swapData = it.dataModel - if (swapData != null) { - signTransactionData(swapData.transaction) //todo implement - return SwapState.SwapSuccess( - quoteModel.fromTokenAmount, - quoteModel.toTokenAmount, - ) - } else { - return SwapState.SwapError(it.error) - } + val swapData = requireNotNull(cache.getLastSwapData()) { "swap data is not ready" } + val networkId = requireNotNull(cache.getNetworkId()) { "no networkId found, please call getTokensToSwap first" } + val currencyToSend = requireNotNull(cache.getExchangeCurrencies()?.fromCurrency) { "currency is not selected" } + val amountToSwap = requireNotNull(cache.getAmountToSwap()) { "" } + val estimatedGas = + increaseByPercents(TWENTY_FIVE_PERCENTS, swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS) + val fee = transactionManager.calculateFee( + networkId = networkId, + gasPrice = swapData.transaction.gasPrice, + estimatedGas = estimatedGas, + ) + val result = transactionManager.sendTransaction( + networkId = networkId, + amountToSend = amountToSwap.value, + currencyToSend = cryptoCurrencyConverter.convert(currencyToSend), + feeAmount = fee, + estimatedGas = estimatedGas, + destinationAddress = swapData.transaction.toWalletAddress, + dataToSign = swapData.transaction.data, + ) + when (result) { + SendTxResult.Success -> { } - } else { - throw IllegalStateException("cache is empty, call 'findBestQuote' first") + SendTxResult.UserCancelledError -> TODO() + is SendTxResult.BlockchainSdkError -> TODO() + is SendTxResult.TangemSdkError -> TODO() + is SendTxResult.UnknownError -> TODO() } + return SwapState.SwapError(DataError.UNKNOWN_ERROR) } override fun getTokenDecimals(token: Currency): Int { @@ -135,6 +219,47 @@ internal class SwapInteractorImpl @Inject constructor( } } + private fun selectToToken( + initialToken: Currency, + tokensInWallet: List, + loadedTokens: List, + ): Currency { + val toToken = if (tokensInWallet.isNotEmpty()) { + tokensInWallet.firstOrNull { it.symbol != initialToken.symbol } + ?: loadedTokens.first { it.symbol != initialToken.symbol } + } else { + val findUsdt = loadedTokens.firstOrNull { it.symbol == "USDT" && it.symbol != initialToken.symbol } + if (findUsdt == null) { + val findUsdc = loadedTokens.firstOrNull { it.symbol == "USDC" && it.symbol != initialToken.symbol } + findUsdc ?: loadedTokens.first { it.symbol != initialToken.symbol } + } else { + findUsdt + } + } + return toToken + } + + private fun getTokensWithBalance( + tokens: List, + balances: Map, + rates: Map, + appCurrency: ProxyFiatCurrency, + ): List { + return tokens.map { + val balance = balances[it.id] + TokenWithBalance( + token = it, + tokenBalanceData = TokenBalanceData( + amount = balance?.let { b -> b.value.toFormattedString(b.decimals) }, + amountEquivalent = balance?.value?.toFiatString( + rates[it.id]?.toBigDecimal() ?: BigDecimal.ZERO, + appCurrency.symbol, + ), + ), + ) + } + } + private suspend fun checkAllowance(networkId: String, fromTokenAddress: String): Boolean { val allowance = repository.checkTokensSpendAllowance( networkId = networkId, @@ -154,6 +279,7 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fromToken: Currency, toToken: Currency, + isAllowedToSpend: Boolean, ): SwapState { repository.findBestQuote( networkId = networkId, @@ -163,12 +289,7 @@ internal class SwapInteractorImpl @Inject constructor( ).let { quotes -> val quoteDataModel = quotes.dataModel if (quoteDataModel != null) { - cache.cacheSwapParams( - quoteModel = quoteDataModel, - amount = amount, - fromCurrency = fromToken, - toCurrency = toToken, - ) + cache.cacheQuoteData(quoteModel = quoteDataModel) val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken)) cache.cacheApproveTransactionData(transactionData) val swapState = updateBalances( @@ -182,7 +303,7 @@ internal class SwapInteractorImpl @Inject constructor( estimatedGas = quoteDataModel.estimatedGas, gasPrice = transactionData.gasPrice, ), - isAllowedToSpend = false, + isAllowedToSpend = isAllowedToSpend, ) return updatePermissionState( networkId = networkId, @@ -230,7 +351,7 @@ internal class SwapInteractorImpl @Inject constructor( estimatedGas = swapData.transaction.gas.toIntOrNull() ?: DEFAULT_GAS, gasPrice = swapData.transaction.gasPrice, ), - isAllowedToSpend = false, + isAllowedToSpend = true, ) return swapState.copy( permissionState = PermissionDataState.Empty, @@ -264,7 +385,7 @@ internal class SwapInteractorImpl @Inject constructor( toTokenAmount = toTokenAmount, fromTokenAddress = getTokenAddress(fromToken), toTokenAddress = getTokenAddress(toToken), - fee = fee.toPlainString(), + fee = "${fee.toPlainString()} ${userWalletManager.getCurrencyByNetworkId(networkId)}", isAllowedToSpend = isAllowedToSpend, fromTokenWalletBalance = fromTokenBalance ?: ZERO_BALANCE, fromTokenFiatBalance = fromTokenAmount.value.toFiatString( @@ -319,7 +440,7 @@ internal class SwapInteractorImpl @Inject constructor( private fun getTokenAddress(currency: Currency): String { return when (currency) { is Currency.NativeToken -> { - transactionManager.getNativeAddress(currency.networkId) + DEFAULT_BLOCKCHAIN_ADDRESS } is Currency.NonNativeToken -> { currency.contractAddress @@ -327,13 +448,15 @@ internal class SwapInteractorImpl @Inject constructor( } } - //todo implement after merge referral - private fun signTransactionData(transaction: TransactionModel) { + private fun increaseByPercents(percents: Int, value: Int): Int { + return value * (percents / 100 + 1) } companion object { private const val DEFAULT_SLIPPAGE = 2 private const val ZERO_BALANCE = "0" private const val DEFAULT_GAS = 300000 + private const val DEFAULT_BLOCKCHAIN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + private const val TWENTY_FIVE_PERCENTS = 25 } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 7082d123c4..f7a2c78934 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -1,9 +1,9 @@ package com.tangem.feature.swap.domain -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.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.ApproveModel +import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.feature.swap.domain.models.domain.QuoteModel +import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel interface SwapRepository { 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 073920c985..a415b03edc 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,24 +1,30 @@ 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.domain.ApproveModel +import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.feature.swap.domain.models.domain.QuoteModel +import com.tangem.feature.swap.domain.models.domain.SwapDataModel interface SwapDataCache { fun cacheNetworkId(networkId: String) - fun cacheSwapParams(quoteModel: QuoteModel, amount: SwapAmount, fromCurrency: Currency, toCurrency: Currency) - fun cacheApproveTransactionData(approve: ApproveModel) + fun cacheQuoteData(quoteModel: QuoteModel) fun cacheSwapData(swapDataModel: SwapDataModel) + fun cacheAmountToSwap(amount: SwapAmount) + fun cacheApproveTransactionData(approve: ApproveModel) fun cacheAvailableToSwapTokens(networkId: String, tokens: List) + fun cacheInWalletTokens(tokens: List) + fun cacheLoadedTokens(tokens: List) + fun cacheExchangeCurrencies(fromToken: Currency, toToken: Currency) fun getAvailableTokens(networkId: String): List fun getApproveTransactionData(): ApproveModel? fun getLastQuote(): QuoteModel? fun getLastSwapData(): SwapDataModel? - fun getAmountToSwap(): SwapAmount? fun getNetworkId(): String? + fun getAmountToSwap(): SwapAmount? fun getExchangeCurrencies(): ExchangeCurrencies? + fun getInWalletTokens(): List + fun getLoadedTokens(): List } \ No newline at end of file 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 b5ca9e38a1..582eb1ff73 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,35 +1,54 @@ 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 +import com.tangem.feature.swap.domain.models.domain.ApproveModel +import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.feature.swap.domain.models.domain.QuoteModel +import com.tangem.feature.swap.domain.models.domain.SwapDataModel class SwapDataCacheImpl : SwapDataCache { private var lastDataForSwap: SwapDataHolder = SwapDataHolder() private val availableTokensForNetwork: MutableMap> = mutableMapOf() + private val lastInWalletTokens = mutableListOf() + private val lastLoadedTokens = mutableListOf() - override fun cacheSwapParams( + override fun cacheQuoteData( quoteModel: QuoteModel, - amount: SwapAmount, - fromCurrency: Currency, - toCurrency: Currency, ) { + lastDataForSwap = lastDataForSwap.copy(quoteModel = quoteModel) + } + + override fun cacheExchangeCurrencies(fromToken: Currency, toToken: Currency) { lastDataForSwap = lastDataForSwap.copy( - quoteModel = quoteModel, - amountToSwap = amount, exchangeCurrencies = ExchangeCurrencies( - fromCurrency = fromCurrency, - toCurrency = toCurrency, + fromCurrency = fromToken, + toCurrency = toToken, ), ) } + override fun cacheInWalletTokens(tokens: List) { + lastInWalletTokens.clear() + lastInWalletTokens.addAll(tokens) + } + + override fun cacheLoadedTokens(tokens: List) { + lastLoadedTokens.clear() + lastLoadedTokens.addAll(tokens) + } + + override fun getInWalletTokens(): List { + return lastInWalletTokens + } + + override fun getLoadedTokens(): List { + return lastLoadedTokens + } + override fun cacheSwapData(swapDataModel: SwapDataModel) { lastDataForSwap = lastDataForSwap.copy(swapModel = swapDataModel) } @@ -54,6 +73,10 @@ class SwapDataCacheImpl : SwapDataCache { lastDataForSwap = lastDataForSwap.copy(networkId = networkId) } + override fun cacheAmountToSwap(amount: SwapAmount) { + lastDataForSwap = lastDataForSwap.copy(amountToSwap = amount) + } + override fun getNetworkId(): String? { return lastDataForSwap.networkId } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/converters/CryptoCurrencyConverter.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/converters/CryptoCurrencyConverter.kt index 68c3895f79..90b978100e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/converters/CryptoCurrencyConverter.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/converters/CryptoCurrencyConverter.kt @@ -1,14 +1,15 @@ package com.tangem.feature.swap.domain.converters -import com.tangem.feature.swap.domain.models.Currency -import com.tangem.utils.converter.Converter +import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.utils.converter.TwoWayConverter +import com.tangem.lib.crypto.models.Currency as CryptoCurrency -class CryptoCurrencyConverter : Converter { +class CryptoCurrencyConverter : TwoWayConverter { - override fun convert(value: Currency): com.tangem.lib.crypto.models.Currency { + override fun convert(value: Currency): CryptoCurrency { return when (value) { is Currency.NonNativeToken -> { - com.tangem.lib.crypto.models.Currency.NonNativeToken( + CryptoCurrency.NonNativeToken( id = value.id, name = value.name, symbol = value.symbol, @@ -18,7 +19,7 @@ class CryptoCurrencyConverter : Converter { - com.tangem.lib.crypto.models.Currency.NativeToken( + CryptoCurrency.NativeToken( id = value.id, name = value.name, symbol = value.symbol, @@ -27,4 +28,29 @@ class CryptoCurrencyConverter : Converter { + Currency.NonNativeToken( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = value.networkId, + contractAddress = value.contractAddress, + decimalCount = value.decimalCount, + logoUrl = "", + ) + } + is CryptoCurrency.NativeToken -> { + Currency.NativeToken( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = value.networkId, + logoUrl = "", + ) + } + } + } } \ 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 764ec2f37e..1271078626 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,10 +1,10 @@ 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.domain.ApproveModel +import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.feature.swap.domain.models.domain.QuoteModel import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.SwapDataModel data class SwapDataHolder( val quoteModel: QuoteModel? = null, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ApproveModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ApproveModel.kt similarity index 87% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ApproveModel.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ApproveModel.kt index 2fce5634f9..c21b52c0fa 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ApproveModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ApproveModel.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.domain.models +package com.tangem.feature.swap.domain.models.domain /** * Approve model diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/Currency.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt similarity index 93% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/Currency.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt index bcd8017e56..fec99447b7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/Currency.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.domain.models +package com.tangem.feature.swap.domain.models.domain import kotlinx.serialization.Serializable 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/domain/QuoteModel.kt similarity index 83% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/QuoteModel.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt index 14b36eed8b..871b9d77ea 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/domain/QuoteModel.kt @@ -1,4 +1,6 @@ -package com.tangem.feature.swap.domain.models +package com.tangem.feature.swap.domain.models.domain + +import com.tangem.feature.swap.domain.models.SwapAmount /** * Quote model holds data about current amounts of exchange and fees 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/domain/SwapDataModel.kt similarity index 80% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapDataModel.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt index 8af39f1219..e2b5356100 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/domain/SwapDataModel.kt @@ -1,4 +1,6 @@ -package com.tangem.feature.swap.domain.models +package com.tangem.feature.swap.domain.models.domain + +import com.tangem.feature.swap.domain.models.SwapAmount /** * Swap transaction model diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/TransactionModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt similarity index 93% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/TransactionModel.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt index eea39441ca..ac5b7b2b05 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/TransactionModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.domain.models +package com.tangem.feature.swap.domain.models.domain /** * Transaction model 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/ui/SwapState.kt similarity index 83% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapState.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 60ec7a9471..77f86cbf47 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/ui/SwapState.kt @@ -1,6 +1,7 @@ -package com.tangem.feature.swap.domain.models +package com.tangem.feature.swap.domain.models.ui -import java.math.BigDecimal +import com.tangem.feature.swap.domain.models.DataError +import com.tangem.feature.swap.domain.models.SwapAmount sealed interface SwapState { @@ -18,11 +19,6 @@ sealed interface SwapState { val permissionState: PermissionDataState = PermissionDataState.Empty, ) : SwapState - data class SwapSuccess( - val fromTokenAmount: SwapAmount, - val toTokenAmount: SwapAmount, - ) : SwapState - data class SwapError( val errorType: DataError, ) : SwapState diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt new file mode 100644 index 0000000000..2fcb993b1d --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.swap.domain.models.ui + +import com.tangem.feature.swap.domain.models.domain.Currency + +data class TokensDataState( + val preselectTokens: PreselectTokens, + val foundTokensState: FoundTokensState, +) + +data class FoundTokensState( + val tokensInWallet: List, + val loadedTokens: List, +) + +data class PreselectTokens( + val fromToken: Currency, + val toToken: Currency, +) + +data class TokenWithBalance( + val token: Currency, + val tokenBalanceData: TokenBalanceData? = null, +) + +data class TokenBalanceData( + val amount: String?, + val amountEquivalent: String?, +) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt new file mode 100644 index 0000000000..4e5deed03d --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.swap.converters + +import com.tangem.feature.swap.domain.models.ui.FoundTokensState +import com.tangem.feature.swap.domain.models.ui.TokenWithBalance +import com.tangem.feature.swap.models.SwapSelectTokenStateHolder +import com.tangem.feature.swap.models.TokenBalanceData +import com.tangem.feature.swap.models.TokenToSelect +import com.tangem.utils.converter.Converter + +class TokensDataConverter( + private val onSearchEntered: (String) -> Unit, + private val onTokenSelected: (String) -> Unit, +) : Converter { + + override fun convert(value: FoundTokensState): SwapSelectTokenStateHolder { + return SwapSelectTokenStateHolder( + tokens = (value.tokensInWallet + value.loadedTokens).map { tokenWithBalanceToTokenToSelect(it) }, + onSearchEntered = onSearchEntered, + onTokenSelected = onTokenSelected, + ) + } + + private fun tokenWithBalanceToTokenToSelect(tokenWithBalance: TokenWithBalance): TokenToSelect { + return TokenToSelect( + id = tokenWithBalance.token.id, + name = tokenWithBalance.token.name, + symbol = tokenWithBalance.token.symbol, + iconUrl = tokenWithBalance.token.logoUrl, + addedTokenBalanceData = TokenBalanceData( + amount = tokenWithBalance.tokenBalanceData?.amount, + amountEquivalent = tokenWithBalance.tokenBalanceData?.amountEquivalent, + ), + ) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt new file mode 100644 index 0000000000..60f8a74218 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.swap.models + +data class UiActions( + val onSearchEntered: (String) -> Unit, + val onTokenSelected: (String) -> Unit, + val onAmountChanged: (String) -> Unit, + val onSwapClick: () -> Unit, + val onGivePermissionClick: () -> Unit, + val onChangeCardsClicked: () -> Unit, + val onBackClicked: () -> Unit, +) \ No newline at end of file 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 5e91941a56..4d90a1c669 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,9 +1,11 @@ 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.converters.TokensDataConverter +import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.formatToUIRepresentation +import com.tangem.feature.swap.domain.models.ui.FoundTokensState +import com.tangem.feature.swap.domain.models.ui.PermissionDataState +import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.models.ApprovePermissionButton import com.tangem.feature.swap.models.CancelPermissionButton import com.tangem.feature.swap.models.FeeState @@ -13,16 +15,19 @@ import com.tangem.feature.swap.models.SwapPermissionState import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.SwapWarning import com.tangem.feature.swap.models.TransactionCardType +import com.tangem.feature.swap.models.UiActions /** * State builder creates a specific states for SwapScreen */ -class StateBuilder { +class StateBuilder(val actions: UiActions) { - fun createInitialLoadingState(networkCurrency: String, onAmountChanged: (String) -> Unit): SwapStateHolder { + private val tokensDataConverter = TokensDataConverter(actions.onSearchEntered, actions.onTokenSelected) + + fun createInitialLoadingState(networkCurrency: String): SwapStateHolder { return SwapStateHolder( sendCardData = SwapCardData( - type = TransactionCardType.SendCard(onAmountChanged), + type = TransactionCardType.SendCard(actions.onAmountChanged), amount = null, amountEquivalent = null, tokenIconUrl = "", @@ -43,8 +48,8 @@ class StateBuilder { networkCurrency = networkCurrency, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), onRefresh = {}, - onBackClicked = {}, - onChangeCardsClicked = {}, + onBackClicked = actions.onBackClicked, + onChangeCardsClicked = actions.onChangeCardsClicked, ) } @@ -66,8 +71,8 @@ class StateBuilder { ), receiveCardData = SwapCardData( type = TransactionCardType.ReceiveCard(), - amount = uiStateHolder.receiveCardData.amount, - amountEquivalent = uiStateHolder.receiveCardData.amountEquivalent, + amount = null, + amountEquivalent = null, tokenIconUrl = toToken.logoUrl, tokenCurrency = toToken.symbol, canSelectAnotherToken = mainTokenId != toToken.id, @@ -75,6 +80,7 @@ class StateBuilder { ), fee = FeeState.Loading, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), + permissionState = uiStateHolder.permissionState, ) } @@ -82,8 +88,6 @@ class StateBuilder { uiStateHolder: SwapStateHolder, quoteModel: SwapState.QuotesLoadedState, fromToken: Currency, - onSwapClick: () -> Unit, - onGivePermissionClick: () -> Unit, ): SwapStateHolder { return uiStateHolder.copy( sendCardData = SwapCardData( @@ -109,16 +113,31 @@ class StateBuilder { } else { emptyList() }, - permissionState = convertPermissionState(quoteModel.permissionState, onGivePermissionClick), + permissionState = convertPermissionState(quoteModel.permissionState, actions.onGivePermissionClick), fee = FeeState.Loaded(quoteModel.fee), swapButton = SwapButton( enabled = quoteModel.isAllowedToSpend, loading = false, - onClick = onSwapClick, + onClick = actions.onSwapClick, ), ) } + fun createSwapInProgressState(uiState: SwapStateHolder): SwapStateHolder { + return uiState.copy( + swapButton = uiState.swapButton.copy( + loading = true, + enabled = false, + ), + ) + } + + fun addTokensToState(uiState: SwapStateHolder, dataState: FoundTokensState): SwapStateHolder { + return uiState.copy( + selectTokenState = tokensDataConverter.convert(dataState), + ) + } + private fun convertPermissionState( permissionDataState: PermissionDataState, onGivePermissionClick: () -> Unit, @@ -144,4 +163,13 @@ class StateBuilder { ) } } + + fun updateSwapAmount(uiState: SwapStateHolder, amount: String, amountEquivalent: String): SwapStateHolder { + return uiState.copy( + sendCardData = uiState.sendCardData.copy( + amount = amount, + amountEquivalent = amountEquivalent, + ), + ) + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index ff81cb50e9..c5a0edeca4 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -208,7 +208,7 @@ private fun Content( } is TransactionCardType.SendCard -> { AutoSizeTextField( - amount = amount ?: "0", + amount = amount ?: "1", onAmoutChanged = { type.onAmountChanged(it) }, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/PeriodicTask.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/PeriodicTask.kt new file mode 100644 index 0000000000..138ce3c20c --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/PeriodicTask.kt @@ -0,0 +1,43 @@ +package com.tangem.feature.swap.viewmodels + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean + +class PeriodicTask( + private val delay: Long, + private val task: suspend () -> Unit, +) { + + private var isActive: AtomicBoolean = AtomicBoolean(false) + + suspend fun runTaskWithDelay() { + isActive.set(true) + while (isActive.get()) { + task.invoke() + delay(delay) + } + } + + fun cancel() { + isActive.set(false) + } +} + +class SingleTaskScheduler { + + private var lastTask: PeriodicTask? = null + + fun scheduleTask(scope: CoroutineScope, task: PeriodicTask) { + lastTask?.cancel() + lastTask = task + scope.launch { + task.runTaskWithDelay() + } + } + + fun cancelTask() { + lastTask?.cancel() + } +} \ 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 1ba7b06511..f99d5d9ac5 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 @@ -8,18 +8,21 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.feature.swap.domain.SwapInteractor -import com.tangem.feature.swap.domain.models.Currency -import com.tangem.feature.swap.domain.models.SwapState import com.tangem.feature.swap.domain.models.createFromAmountWithoutOffset +import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.feature.swap.domain.models.ui.FoundTokensState +import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.models.SwapStateHolder +import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.router.SwapScreen import com.tangem.feature.swap.ui.StateBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.Debouncer +import com.tangem.utils.coroutines.runCatching import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import javax.inject.Inject @@ -32,23 +35,31 @@ internal class SwapViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val stateBuilder = StateBuilder() + private val stateBuilder = StateBuilder( + actions = createUiActions(), + ) + + private val amountDebouncer = Debouncer() + private val singleTaskScheduler = SingleTaskScheduler() private val currency = Json.decodeFromString( savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter Currency found"), ) - var uiState: SwapStateHolder by mutableStateOf( - stateBuilder.createInitialLoadingState(currency.symbol) - { /** [REDACTED_TODO_COMMENT]*/ }, - ) + var uiState: SwapStateHolder by mutableStateOf(stateBuilder.createInitialLoadingState(currency.symbol)) private set + //shows currency order (direct - swap initial to selected, reversed = selected to initial) + private var isOrderReversed = false + private val lastAmount = mutableStateOf(INITIAL_AMOUNT) private var swapRouter: SwapRouter by Delegates.notNull() - var currentScreen = SwapScreen.Main get() = swapRouter.currentScreen + init { + initTokens(currency) + } + fun setRouter(router: SwapRouter) { swapRouter = router uiState = uiState.copy( @@ -58,21 +69,18 @@ internal class SwapViewModel @Inject constructor( ) } - init { + private fun initTokens(currency: Currency) { viewModelScope.launch(dispatchers.main) { - runCatching { - withContext(dispatchers.io) { - swapInteractor.getTokensToSwap(currency.networkId) - } + runCatching(dispatchers.io) { + swapInteractor.initTokensToSwap(currency) } - .onSuccess { tokens -> - if (tokens.size > MIN_TOKENS_IN_LIST) { - tokens.firstOrNull { token -> - token.id == currency.id - }?.let { - loadQuotes(it, tokens[1], INITIAL_AMOUNT) - } - } + .onSuccess { state -> + updateTokensState(dataState = state.foundTokensState) + startLoadingQuotes( + fromToken = state.preselectTokens.fromToken, + toToken = state.preselectTokens.toToken, + amount = lastAmount.value, + ) } .onFailure { Log.e("SwapViewModel", it.message ?: it.cause.toString()) @@ -80,20 +88,38 @@ internal class SwapViewModel @Inject constructor( } } - private fun loadQuotes(fromToken: Currency, toToken: Currency, amount: String) { + private fun updateTokensState(dataState: FoundTokensState) { + uiState = stateBuilder.addTokensToState(uiState, dataState) + } + + private fun startLoadingQuotes(fromToken: Currency, toToken: Currency, amount: String) { + singleTaskScheduler.cancelTask() uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, currency.id) + singleTaskScheduler.scheduleTask( + viewModelScope, + PeriodicTask( + delay = UPDATE_DELAY, + ) { + loadQuotesInternal( + fromToken = fromToken, + toToken = toToken, + amount = amount, + ) + }, + ) + } + + private fun loadQuotesInternal(fromToken: Currency, toToken: Currency, amount: String) { viewModelScope.launch(dispatchers.main) { - runCatching { - withContext(dispatchers.io) { - swapInteractor.findBestQuote( - fromToken = fromToken, - toToken = toToken, - amount = createFromAmountWithoutOffset( - amountWithoutOffset = amount, - decimals = swapInteractor.getTokenDecimals(fromToken), - ), - ) - } + runCatching(dispatchers.io) { + swapInteractor.findBestQuote( + fromToken = fromToken, + toToken = toToken, + amount = createFromAmountWithoutOffset( + amountWithoutOffset = amount, + decimals = swapInteractor.getTokenDecimals(fromToken), + ), + ) } .onSuccess { swapState -> when (swapState) { @@ -102,13 +128,8 @@ internal class SwapViewModel @Inject constructor( uiStateHolder = uiState, quoteModel = swapState, fromToken = fromToken, - onSwapClick = { onSwapClick(toToken, swapState) }, - onGivePermissionClick = { givePermissionsToSwap(fromToken) }, ) } - is SwapState.SwapSuccess -> { - //todo implement - } is SwapState.SwapError -> { } } @@ -117,46 +138,106 @@ internal class SwapViewModel @Inject constructor( } } - private fun onSwapClick(toToken: Currency, quoteModel: SwapState.QuotesLoadedState) { + private fun createUiActions(): UiActions { + return UiActions( + onSearchEntered = { onSearchEntered(it) }, + onTokenSelected = { onTokenSelect(it) }, + onAmountChanged = { onAmountChanged(it) }, + onSwapClick = { onSwapClick() }, + onGivePermissionClick = { givePermissionsToSwap() }, + onChangeCardsClicked = { onChangeCardsClicked() }, + onBackClicked = { onSearchEntered("") }, + ) + } + + private fun onSwapClick() { + singleTaskScheduler.cancelTask() + uiState = stateBuilder.createSwapInProgressState(uiState) viewModelScope.launch(dispatchers.main) { - runCatching { - withContext(dispatchers.io) { - if (!quoteModel.isAllowedToSpend) { - swapInteractor.givePermissionToSwap(toToken) - } else { - swapInteractor.onSwap() - } - } + runCatching(dispatchers.io) { + swapInteractor.onSwap() } .onSuccess { - if (it is SwapState) { - when (it) { - is SwapState.SwapSuccess -> { - } - is SwapState.SwapError -> { - } - else -> {} + when (it) { + is SwapState.SwapError -> { } + else -> {} } } .onFailure { } } } - private fun givePermissionsToSwap(tokenToApprove: Currency) { + private fun givePermissionsToSwap() { viewModelScope.launch(dispatchers.main) { - runCatching { - withContext(dispatchers.io) { - swapInteractor.givePermissionToSwap(tokenToApprove) - } + runCatching(dispatchers.io) { + swapInteractor.givePermissionToSwap() } .onSuccess { } .onFailure { } } } + private fun onSearchEntered(searchQuery: String) { + viewModelScope.launch(dispatchers.main) { + runCatching(dispatchers.io) { + swapInteractor.onSearchToken(searchQuery) + } + .onSuccess { + updateTokensState(it) + } + .onFailure { } + } + } + + private fun onTokenSelect(id: String) { + val foundToken = swapInteractor.findTokenById(id) + if (foundToken != null) { + val fromToken: Currency + val toToken: Currency + if (isOrderReversed) { + fromToken = foundToken + toToken = currency + } else { + fromToken = currency + toToken = foundToken + } + swapRouter.openScreen(SwapScreen.Main) + startLoadingQuotes(fromToken, toToken, lastAmount.value) + } + } + + private fun onChangeCardsClicked() { + val currencies = swapInteractor.getExchangeCurrencies() + val newFromToken = currencies?.toCurrency + val newToToken = currencies?.fromCurrency + if (newFromToken != null && newToToken != null) { + isOrderReversed = !isOrderReversed + startLoadingQuotes(newFromToken, newToToken, lastAmount.value) + } + } + + private fun onAmountChanged(value: String) { + uiState = stateBuilder.updateSwapAmount(uiState, value, value) + lastAmount.value = value + amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) { + val currencies = swapInteractor.getExchangeCurrencies() + val fromToken = currencies?.fromCurrency + val toToken = currencies?.toCurrency + if (fromToken != null && toToken != null) { + startLoadingQuotes(fromToken, toToken, lastAmount.value) + } + } + } + + override fun onCleared() { + singleTaskScheduler.cancelTask() + super.onCleared() + } + companion object { - private const val MIN_TOKENS_IN_LIST = 2 private const val INITIAL_AMOUNT = "1" + private const val UPDATE_DELAY = 10000L + private const val DEBOUNCE_AMOUNT_DELAY = 1000L } } \ No newline at end of file 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 34f2f3d06b..ca92f3faf6 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 @@ -13,6 +13,7 @@ interface TransactionManager { amountToSend: BigDecimal, currencyToSend: Currency, feeAmount: BigDecimal, + estimatedGas: Int, destinationAddress: String, dataToSign: String, ): SendTxResult @@ -25,8 +26,6 @@ interface TransactionManager { destinationAddress: String, ): ProxyAmount - fun getNativeAddress(networkId: String): String - @Throws(IllegalStateException::class) fun getNativeTokenDecimals(networkId: String): Int 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 3faa50aa4c..f4c84822ed 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 @@ -12,7 +12,7 @@ interface UserWalletManager { /** * Returns all user tokens (merged from local and backend) */ - suspend fun getUserTokens(): List + suspend fun getUserTokens(networkId: String): List /** * Returns user walletId