From a6f272223a8d99f8a72813d25ce713306db1e8a3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Nov 2023 16:27:08 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../api/express/TangemExpressApi.kt | 2 +- .../models/response/ExchangeProvider.kt | 8 +- .../api/express/models/response/SwapPair.kt | 2 +- .../tangem/feature/swap/SwapRepositoryImpl.kt | 17 +- .../swap/converters/SwapProviderConverter.kt | 25 +++ .../feature/swap/domain/SwapInteractor.kt | 15 +- .../feature/swap/domain/SwapInteractorImpl.kt | 154 ++++++------------ .../feature/swap/domain/SwapRepository.kt | 6 +- .../swap/domain/di/SwapDomainModule.kt | 1 - .../domain/models/domain/SwapPairLeast.kt | 12 +- .../swap/domain/models/ui/SwapState.kt | 6 +- .../feature/swap/models/SwapStateHolder.kt | 2 + .../swap/models/states/ProviderState.kt | 5 + .../tangem/feature/swap/ui/ProviderItem.kt | 15 +- .../tangem/feature/swap/ui/StateBuilder.kt | 55 ++++++- .../feature/swap/ui/SwapScreenContent.kt | 4 + .../swap/viewmodels/SwapProcessDataState.kt | 8 +- .../feature/swap/viewmodels/SwapViewModel.kt | 79 +++++---- 18 files changed, 237 insertions(+), 179 deletions(-) create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 0c1fb16cf8..2d62fb3630 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -32,7 +32,7 @@ interface TangemExpressApi { @Query("toContractAddress") toContractAddress: String, @Query("toNetwork") toNetwork: String, @Query("fromAmount") fromAmount: String, - @Query("providerId") providerId: Int, + @Query("providerId") providerId: String, @Query("rateType") rateType: String, ): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index 934d1a8497..4e2bc6d69c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -4,19 +4,19 @@ import com.squareup.moshi.Json data class ExchangeProvider( @Json(name = "id") - val id: Int, + val id: String, @Json(name = "name") val name: String, - @Json(name = "id") + @Json(name = "type") val type: ExchangeProviderType, @Json(name = "imageLarge") - val imageLargeUrl: Int, + val imageLargeUrl: String, @Json(name = "imageSmall") - val imageSmallUrl: Int, + val imageSmallUrl: String, ) enum class ExchangeProviderType { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt index 2d8b0b7cf3..f96aef7dfc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -17,7 +17,7 @@ data class SwapPair( data class SwapPairProvider( @Json(name = "providerId") - val providerId: Int, + val providerId: String, @Json(name = "rateTypes") val rateTypes: List, 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 0f8e561982..00d5f2448a 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 @@ -48,6 +48,7 @@ internal class SwapRepositoryImpl @Inject constructor( private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() private val rateTypeConverter = RateTypeConverter() + private val swapProviderConverter = SwapProviderConverter() override suspend fun getPairs( initialCurrency: LeastTokenInfo, @@ -78,6 +79,18 @@ internal class SwapRepositoryImpl @Inject constructor( } } + override suspend fun getProvidersDetails(providers: Set): List { + val providersMap = providers.associateBy { it.providerId } + return tangemExpressApi.getProviders().getOrThrow().mapNotNull { + val provider = providersMap[it.id] + if (provider != null) { + swapProviderConverter.convert(it).copy(rateTypes = provider.rateTypes) + } else { + null + } + } + } + private suspend fun getPairsInternal( from: List, to: List, @@ -133,7 +146,7 @@ internal class SwapRepositoryImpl @Inject constructor( toContractAddress: String, toNetwork: String, fromAmount: String, - providerId: Int, + providerId: String, rateType: RateType, ): AggregatedSwapDataModel { return withContext(coroutineDispatcher.io) { @@ -274,7 +287,7 @@ internal class SwapRepositoryImpl @Inject constructor( toContractAddress: String, toNetwork: String, fromAmount: String, - providerId: Int, + providerId: String, rateType: RateType, ): ExchangeQuote { val response = tangemExpressApi.getExchangeQuote( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt new file mode 100644 index 0000000000..6930923128 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt @@ -0,0 +1,25 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.express.models.response.ExchangeProvider +import com.tangem.datasource.api.express.models.response.ExchangeProviderType +import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.utils.converter.Converter +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType as ExchangeProviderTypeDomain + +class SwapProviderConverter : Converter { + override fun convert(value: ExchangeProvider): SwapProvider { + return SwapProvider( + providerId = value.id, + name = value.name, + type = convertExchangeType(value.type), + imageLarge = value.imageLargeUrl, + ) + } + + private fun convertExchangeType(type: ExchangeProviderType): ExchangeProviderTypeDomain { + return when (type) { + ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX + ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX + } + } +} \ 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 c6ab0a7308..4a196f0569 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,6 +1,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.models.SwapAmount @@ -14,16 +15,6 @@ interface SwapInteractor { fun initDerivationPathAndNetwork(derivationPath: String?, network: Network) - /** - * Init tokens to swap, load tokens list available to swap for given network - * - * @param initialCurrency currency which to swap or receive - * @return [TokensDataState] that contains info about all available to swap tokens for networkId - * and preselected tokens which initially select to swap - */ - @Deprecated("method is used in the old swap mechanism") - suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState - /** * On search token, locally search tokens in previously loaded list to swap * searching in names and symbols @@ -65,8 +56,8 @@ interface SwapInteractor { @Throws(IllegalStateException::class) suspend fun findBestQuote( networkId: String, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, providers: List, amountToSwap: String, selectedFee: FeeType = FeeType.NORMAL, 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 dad225b375..1dd4dab66c 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 @@ -24,13 +24,13 @@ import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.toFiatString +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode import javax.inject.Inject -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope @Suppress("LargeClass", "LongParameterList") internal class SwapInteractorImpl @Inject constructor( @@ -150,7 +150,16 @@ internal class SwapInteractorImpl @Inject constructor( initialCurrency: LeastTokenInfo, currenciesList: List, ): List { - return repository.getPairs(initialCurrency, currenciesList) + val pairs = repository.getPairs(initialCurrency, currenciesList) + val providers = pairs.flatMap { it.providers }.toSet() + val updatedProviders = repository.getProvidersDetails(providers).associateBy { it.providerId } + return pairs.map { pair -> + pair.copy( + providers = pair.providers.mapNotNull { currentProvider -> + updatedProviders[currentProvider.providerId] + }, + ) + } } @Deprecated("used in old swap mechanism") @@ -159,54 +168,6 @@ internal class SwapInteractorImpl @Inject constructor( this.network = network } - @Deprecated("used in old swap mechanism") - override suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState { - // TODO: refactor this function - val networkId = initialCurrency.networkId - val availableTokens = cache.getAvailableTokens(networkId) - val allLoadedTokens = availableTokens.ifEmpty { - val tokens = repository.getExchangeableTokens(networkId) - cache.cacheAvailableToSwapTokens(networkId, tokens) - tokens - }.filter { it.symbol != initialCurrency.symbol } - - // replace tokens in wallet tokens list with loaded same - val loadedOnWalletsMap = mutableSetOf() - val tokensInWallet = userWalletManager.getUserTokens( - networkId = networkId, - derivationPath = derivationPath, - isExcludeCustom = true, - ) - .map { token -> - allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let { - loadedOnWalletsMap.add(it.symbol) - it - } ?: TODO() - } - .filter { it.symbol != initialCurrency.symbol && allLoadedTokens.contains(it) } - val loadedTokens = allLoadedTokens - .filter { - !loadedOnWalletsMap.contains(it.symbol) - } - val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId, emptyList(), derivationPath) - .mapValues { SwapAmount(it.value.value, it.value.decimals) } - val appCurrency = userWalletManager.getUserAppCurrency() - val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id }) - cache.cacheBalances(networkId, derivationPath, tokensBalance) - // cache.cacheLoadedTokens(loadedTokens.map { TokenWithBalance(it) }) - // cache.cacheInWalletTokens(getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency)) - return TokensDataState( - preselectTokens = PreselectTokens( - fromToken = initialCurrency, - toToken = selectToToken(initialCurrency, tokensInWallet, loadedTokens), - ), - foundTokensState = FoundTokensState( - tokensInWallet = emptyList(), // cache.getInWalletTokens(), - loadedTokens = emptyList(), // cache.getLoadedTokens(), - ), - ) - } - @Deprecated("used in old swap mechanism") override suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress { val searchQueryLowerCase = searchQuery.lowercase() @@ -276,26 +237,26 @@ internal class SwapInteractorImpl @Inject constructor( @Deprecated("used in old swap mechanism") override suspend fun findBestQuote( networkId: String, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, providers: List, amountToSwap: String, selectedFee: FeeType, ): Map { - syncWalletBalanceForTokens(networkId, listOf(fromToken, toToken)) + syncWalletBalanceForTokens(networkId, listOf(fromToken.currency, toToken.currency)) val amountDecimal = toBigDecimalOrNull(amountToSwap) if (amountDecimal == null || amountDecimal.signum() == 0) { - return providers.associateWith { createEmptyAmountState(networkId, fromToken, toToken) } + return providers.associateWith { createEmptyAmountState(networkId, fromToken.currency, toToken.currency) } } - val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken)) - val fromTokenAddress = getTokenAddress(fromToken) - val toTokenAddress = getTokenAddress(toToken) - val isAllowedToSpend = isAllowedToSpend(networkId, fromToken, amount) + val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) + val fromTokenAddress = getTokenAddress(fromToken.currency) + val toTokenAddress = getTokenAddress(toToken.currency) + val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount) if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) transactionManager.updateWalletManager(networkId, derivationPath) } - val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken, amount, null) + val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null) return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { // TODO providers.associateWith { @@ -313,8 +274,8 @@ internal class SwapInteractorImpl @Inject constructor( loadQuoteData( networkId = networkId, amount = amount, - fromToken = fromToken, - toToken = toToken, + fromTokenStatus = fromToken, + toTokenStatus = toToken, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, providers = providers, @@ -521,12 +482,14 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun loadQuoteData( networkId: String, amount: SwapAmount, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromTokenStatus: CryptoCurrencyStatus, + toTokenStatus: CryptoCurrencyStatus, providers: List, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, ): Map { + val fromToken = fromTokenStatus.currency + val toToken = toTokenStatus.currency return coroutineScope { val quoteRequests = providers.map { provider -> async { @@ -547,11 +510,11 @@ internal class SwapInteractorImpl @Inject constructor( getState( quoteDataModel = it.second, amount = amount, - fromToken = fromToken, - toToken = toToken, + fromToken = fromTokenStatus, + toToken = toTokenStatus, networkId = networkId, isAllowedToSpend = isAllowedToSpend, - isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, ) }.associate { it.first to it.second } } @@ -560,8 +523,8 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getState( quoteDataModel: AggregatedSwapDataModel, amount: SwapAmount, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, networkId: String, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, @@ -570,15 +533,15 @@ internal class SwapInteractorImpl @Inject constructor( if (quoteModel != null) { val swapState = updateBalances( networkId = networkId, - fromToken = fromToken, - toToken = toToken, + fromTokenStatus = fromToken, + toTokenStatus = toToken, fromTokenAmount = amount, toTokenAmount = quoteModel.toTokenAmount, swapStateData = null, ) val quotesState = updatePermissionState( networkId = networkId, - fromToken = fromToken, + fromToken = fromToken.currency, swapAmount = amount, quotesLoadedState = swapState, ) @@ -612,8 +575,8 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, fromTokenAddress: String, toTokenAddress: String, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, amount: SwapAmount, selectedFee: FeeType, ): SwapState { @@ -630,7 +593,7 @@ internal class SwapInteractorImpl @Inject constructor( val feeData = transactionManager.getFee( networkId = networkId, amountToSend = amount.value, - currencyToSend = swapCurrencyConverter.convert(fromToken), + currencyToSend = swapCurrencyConverter.convert(fromToken.currency), destinationAddress = swapData.transaction.toWalletAddress, increaseBy = INCREASE_GAS_LIMIT_BY, data = swapData.transaction.data, @@ -642,17 +605,17 @@ internal class SwapInteractorImpl @Inject constructor( FeeType.PRIORITY -> txFeeState.priorityFee.feeValue } val isBalanceIncludeFeeEnough = - isBalanceEnough(networkId, fromToken, amount, feeByPriority) + isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority) val isFeeEnough = checkFeeIsEnough( fee = feeByPriority, spendAmount = amount, networkId = networkId, - fromToken = fromToken, + fromToken = fromToken.currency, ) val swapState = updateBalances( networkId = networkId, - fromToken = fromToken, - toToken = toToken, + fromTokenStatus = fromToken, + toTokenStatus = toToken, fromTokenAmount = amount, toTokenAmount = swapData.toTokenAmount, swapStateData = SwapStateData( @@ -677,42 +640,32 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("LongParameterList") private suspend fun updateBalances( networkId: String, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromTokenStatus: CryptoCurrencyStatus, + toTokenStatus: CryptoCurrencyStatus, fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, swapStateData: SwapStateData?, ): SwapState.QuotesLoadedState { + val fromToken = fromTokenStatus.currency + val toToken = toTokenStatus.currency val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) val rates = repository.getRates( appCurrency.code, listOf(fromToken.network.backendId, toToken.network.backendId, nativeToken.id), ) - val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) - val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( tokenAmount = fromTokenAmount, - coinId = fromToken.id.value, - tokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } - ?: ZERO_BALANCE, - tokenFiatBalance = fromTokenAmount.value.toFiatString( - rateValue = rates[fromToken.network.backendId]?.toBigDecimal() ?: BigDecimal.ZERO, - fiatCurrencyName = appCurrency.symbol, - formatWithSpaces = true, - ), + cryptoCurrencyStatus = fromTokenStatus, + amountFiat = rates[fromToken.network.backendId]?.toBigDecimal()?.multiply(fromTokenAmount.value) + ?: BigDecimal.ZERO, ), toTokenInfo = TokenSwapInfo( tokenAmount = toTokenAmount, - coinId = toToken.id.value, - tokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } - ?: ZERO_BALANCE, - tokenFiatBalance = toTokenAmount.value.toFiatString( - rateValue = rates[toToken.network.backendId]?.toBigDecimal() ?: BigDecimal.ZERO, - fiatCurrencyName = appCurrency.symbol, - formatWithSpaces = true, - ), + cryptoCurrencyStatus = toTokenStatus, + amountFiat = rates[toToken.network.backendId]?.toBigDecimal()?.multiply(toTokenAmount.value) + ?: BigDecimal.ZERO, ), priceImpact = calculatePriceImpact( fromTokenAmount = fromTokenAmount.value, @@ -933,7 +886,6 @@ internal class SwapInteractorImpl @Inject constructor( companion object { private const val DEFAULT_SLIPPAGE = 2 - private const val ZERO_BALANCE = "0" @Suppress("UnusedPrivateMember") private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.0 // if need to increase fee when check isEnough 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 4ba2f42f83..f8664afb69 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 @@ -10,6 +10,8 @@ interface SwapRepository { suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List): List + suspend fun getProvidersDetails(providers: Set): List + suspend fun getRates(currencyId: String, tokenIds: List): Map suspend fun getExchangeableTokens(networkId: String): List @@ -21,7 +23,7 @@ interface SwapRepository { toContractAddress: String, toNetwork: String, fromAmount: String, - providerId: Int, + providerId: String, rateType: RateType, ): AggregatedSwapDataModel @@ -73,7 +75,7 @@ interface SwapRepository { toContractAddress: String, toNetwork: String, fromAmount: String, - providerId: Int, + providerId: String, rateType: RateType, ): ExchangeQuote } \ No newline at end of file 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 56fd4124d3..8845c68949 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,7 +1,6 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCardTokensListUseCase -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index ee772becce..76c2c0795d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -27,10 +27,18 @@ data class CryptoCurrencySwapInfo( * @property rateTypes supported rate types */ data class SwapProvider( - val providerId: Int, - val rateTypes: List, + val providerId: String, + val rateTypes: List = emptyList(), + val name: String? = null, + val type: ExchangeProviderType? = null, + val imageLarge: String? = null, ) +enum class ExchangeProviderType { + DEX, + CEX, +} + /** * Rate type. * diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 28809bc066..0dc24830f3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.domain.models.ui +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState @@ -51,9 +52,8 @@ sealed class PermissionDataState { data class TokenSwapInfo( val tokenAmount: SwapAmount, - val coinId: String, - val tokenWalletBalance: String, - val tokenFiatBalance: String, + val amountFiat: BigDecimal, + val cryptoCurrencyStatus: CryptoCurrencyStatus, ) data class RequestApproveStateData( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 6427891e34..e166729104 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.feature.swap.domain.models.ui.TxFee +import com.tangem.feature.swap.models.states.ProviderState data class SwapStateHolder( val sendCardData: SwapCardState, @@ -16,6 +17,7 @@ data class SwapStateHolder( val warnings: List = emptyList(), val alert: SwapWarning.GenericWarning? = null, val updateInProgress: Boolean = false, + val providerState: ProviderState, val permissionState: SwapPermissionState = SwapPermissionState.Empty, val successState: SwapSuccessStateHolder? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index f78f6d6cb3..78feaf99a9 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -5,6 +5,11 @@ sealed class ProviderState { abstract val onProviderClick: ((String) -> Unit)? abstract val id: String + data class Empty( + override val id: String = "", + override val onProviderClick: ((String) -> Unit)? = null, + ) : ProviderState() + data class Loading( override val id: String = "", override val onProviderClick: ((String) -> Unit)? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 6602b25ca8..dfe32ead57 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -36,11 +36,13 @@ private val GrayscaleColorFilter: ColorFilter @Composable fun ProviderItemBlock(state: ProviderState) { - BaseContainer(state) { - ProviderItem( - state = state, - modifier = Modifier.align(Alignment.CenterStart), - ) + if (state !is ProviderState.Empty) { + BaseContainer(state) { + ProviderItem( + state = state, + modifier = Modifier.align(Alignment.CenterStart), + ) + } } } @@ -65,6 +67,9 @@ fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected modifier = modifier, ) } + is ProviderState.Empty -> { + // do nothing + } } } 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 2b1c60f67a..4cbd99d028 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 @@ -17,12 +17,15 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.domain.NetworkInfo +import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal /** * State builder creates a specific states for SwapScreen @@ -51,7 +54,7 @@ internal class StateBuilder( amountTextFieldValue = null, tokenIconUrl = initialCurrency.iconUrl, tokenCurrency = initialCurrency.symbol, - coinId = null, + coinId = initialCurrency.network.backendId, canSelectAnotherToken = false, isNotNativeToken = initialCurrency is CryptoCurrency.Token, balance = "", @@ -79,6 +82,7 @@ internal class StateBuilder( updateInProgress = true, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, onCancelPermissionBottomSheet = actions.hidePermissionBottomSheet, + providerState = ProviderState.Loading(), ) } @@ -146,7 +150,7 @@ internal class StateBuilder( amountEquivalent = null, tokenIconUrl = fromToken.iconUrl, tokenCurrency = fromToken.symbol, - coinId = fromToken.id.value, + coinId = fromToken.network.backendId, isNotNativeToken = fromToken is CryptoCurrency.Token, canSelectAnotherToken = canSelectSendToken, balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "", @@ -158,7 +162,7 @@ internal class StateBuilder( amountEquivalent = null, tokenIconUrl = toToken.iconUrl, tokenCurrency = toToken.symbol, - coinId = toToken.id.value, + coinId = toToken.network.backendId, isNotNativeToken = toToken is CryptoCurrency.Token, canSelectAnotherToken = canSelectReceiveToken, balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", @@ -185,6 +189,7 @@ internal class StateBuilder( uiStateHolder: SwapStateHolder, quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + swapProvider: SwapProvider, onFeeSetup: (TxFee) -> Unit, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -213,29 +218,31 @@ internal class StateBuilder( ) } val feeState = createFeeState(quoteModel, uiStateHolder, onFeeSetup) + val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus + val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, - amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance, + amountEquivalent = fromCurrencyStatus.getFormattedAmount(quoteModel.fromTokenInfo.amountFiat), tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - coinId = quoteModel.fromTokenInfo.coinId, + coinId = fromCurrencyStatus.currency.network.backendId, isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, - balance = quoteModel.fromTokenInfo.tokenWalletBalance, + balance = fromCurrencyStatus.getFormattedAmount(), isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReceiveCard(), amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()), - amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance, + amountEquivalent = toCurrencyStatus.getFormattedAmount(quoteModel.toTokenInfo.amountFiat), tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, - coinId = quoteModel.toTokenInfo.coinId, + coinId = toCurrencyStatus.currency.network.backendId, isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, - balance = quoteModel.toTokenInfo.tokenWalletBalance, + balance = toCurrencyStatus.getFormattedAmount(), isBalanceHidden = isBalanceHiddenProvider(), ), networkCurrency = quoteModel.networkCurrency, @@ -256,6 +263,10 @@ internal class StateBuilder( onClick = actions.onSwapClick, ), updateInProgress = false, + providerState = swapProvider.convertToContentClickableProviderState( + fromTokenInfo = quoteModel.fromTokenInfo, + toTokenInfo = quoteModel.toTokenInfo, + ), ) } @@ -298,6 +309,7 @@ internal class StateBuilder( onClick = { }, ), updateInProgress = false, + providerState = ProviderState.Empty(), ) } @@ -726,12 +738,37 @@ internal class StateBuilder( return "$firstAddressPart...$secondAddressPart" } + private fun SwapProvider.convertToContentClickableProviderState( + fromTokenInfo: TokenSwapInfo, + toTokenInfo: TokenSwapInfo, + ): ProviderState { + val rate = fromTokenInfo.tokenAmount.value / toTokenInfo.tokenAmount.value + val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol + val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol + val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" + return ProviderState.Content( + id = this.providerId, + name = this.name ?: "", + iconUrl = this.imageLarge ?: "", + type = this.type.toString(), + rate = rateString, + additionalBadge = ProviderState.AdditionalBadge.BestTrade, + selectionType = ProviderState.SelectionType.CLICK, + percentLowerThenBest = null, + onProviderClick = {}, + ) + } + private fun CryptoCurrencyStatus.getFormattedAmount(): String { val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) } + private fun CryptoCurrencyStatus.getFormattedAmount(amount: BigDecimal): String { + return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + } + @Suppress("UnusedPrivateMember") private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 153e3cd1b4..3e87fc6ed8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -32,6 +32,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.domain.models.ui.TxFee import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -77,6 +78,8 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick: ) { MainInfo(state) + ProviderItemBlock(state = state.providerState) + FeeItem(feeState = state.fee, currency = state.networkCurrency) if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) @@ -476,6 +479,7 @@ private val state = SwapStateHolder( onChangeCardsClicked = {}, permissionState = SwapPermissionState.InProgress, blockchainId = "POLYGON", + providerState = ProviderState.Loading(), ) @Preview diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 9c756453d6..a1da108e5f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -2,10 +2,8 @@ package com.tangem.feature.swap.viewmodels import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData -import com.tangem.feature.swap.domain.models.ui.SwapStateData -import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress -import com.tangem.feature.swap.domain.models.ui.TxFee +import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.feature.swap.domain.models.ui.* data class SwapProcessDataState( // Initial network id @@ -22,4 +20,6 @@ data class SwapProcessDataState( val swapDataModel: SwapStateData? = null, val selectedFee: TxFee? = null, val tokensDataState: TokensDataStateExpress? = null, + val selectedProvider: SwapProvider? = null, + val lastLoadedSwapStates: Map = emptyMap(), ) \ 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 03199e51f5..cef1abb918 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 @@ -18,7 +18,6 @@ import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.domain.PermissionOptions -import com.tangem.feature.swap.domain.models.domain.RateType import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* @@ -74,7 +73,7 @@ internal class SwapViewModel @Inject constructor( private val inputNumberFormatter = InputNumberFormatter(NumberFormat.getInstance(Locale.getDefault()) as DecimalFormat) private val amountDebouncer = Debouncer() - private val singleTaskScheduler = SingleTaskScheduler() + private val singleTaskScheduler = SingleTaskScheduler>() private var dataState by mutableStateOf(SwapProcessDataState(networkId = initialCryptoCurrency.network.backendId)) @@ -174,10 +173,10 @@ internal class SwapViewModel @Inject constructor( ) } else { startLoadingQuotes( - fromToken = fromCurrencyStatus.currency, - toToken = selectedCurrency.currency, + fromToken = fromCurrencyStatus, + toToken = selectedCurrency, amount = lastAmount.value, - toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency) + toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency), ) } } @@ -196,13 +195,18 @@ internal class SwapViewModel @Inject constructor( } private fun startLoadingQuotes( - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, amount: String, toProvidersList: List, ) { singleTaskScheduler.cancelTask() - uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, initialCryptoCurrency.id.value) + uiState = stateBuilder.createQuotesLoadingState( + uiState, + fromToken.currency, + toToken.currency, + initialCryptoCurrency.id.value, + ) singleTaskScheduler.scheduleTask( viewModelScope, loadQuotesTask( @@ -220,20 +224,20 @@ internal class SwapViewModel @Inject constructor( val amount = dataState.amount if (fromCurrency != null && toCurrency != null && amount != null) { startLoadingQuotes( - fromToken = fromCurrency.currency, - toToken = toCurrency.currency, + fromToken = fromCurrency, + toToken = toCurrency, amount = amount, - toProvidersList = findSwapProviders(fromCurrency, toCurrency) + toProvidersList = findSwapProviders(fromCurrency, toCurrency), ) } } private fun loadQuotesTask( - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, amount: String, toProvidersList: List, - ): PeriodicTask { + ): PeriodicTask> { return PeriodicTask( UPDATE_DELAY, task = { @@ -251,17 +255,19 @@ internal class SwapViewModel @Inject constructor( providers = toProvidersList, amountToSwap = amount, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - ).entries.first().value// TODO + ) } }, - onSuccess = { swapState -> - when (swapState) { + onSuccess = { providersState -> + val (provider, state) = updateLoadedQuotes(providersState) + when (state) { is SwapState.QuotesLoadedState -> { - fillDataState(swapState.permissionState, swapState.swapDataModel) + fillDataState(state.permissionState, state.swapDataModel) uiState = stateBuilder.createQuotesLoadedState( uiStateHolder = uiState, - quoteModel = swapState, - fromToken = fromToken, + quoteModel = state, + fromToken = fromToken.currency, + swapProvider = provider, ) { updatedFee -> dataState = dataState.copy( selectedFee = updatedFee, @@ -271,12 +277,12 @@ internal class SwapViewModel @Inject constructor( is SwapState.EmptyAmountState -> { uiState = stateBuilder.createQuotesEmptyAmountState( uiStateHolder = uiState, - emptyAmountState = swapState, + emptyAmountState = state, ) } is SwapState.SwapError -> { - Timber.e("SwapError when loading quotes ${swapState.error}") - uiState = stateBuilder.mapError(uiState, swapState.error) { startLoadingQuotesFromLastState() } + Timber.e("SwapError when loading quotes ${state.error}") + uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() } } } }, @@ -287,6 +293,15 @@ internal class SwapViewModel @Inject constructor( ) } + private fun updateLoadedQuotes(state: Map): Pair { + val selectedSwapProvider = dataState.selectedProvider ?: state.keys.first() + dataState = dataState.copy( + selectedProvider = selectedSwapProvider, + lastLoadedSwapStates = state, + ) + return state.entries.first { it.key == selectedSwapProvider }.toPair() + } + private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapStateData?) { dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) { dataState.copy( @@ -435,10 +450,10 @@ internal class SwapViewModel @Inject constructor( toCryptoCurrency = toToken, ) startLoadingQuotes( - fromToken = fromToken.currency, - toToken = toToken.currency, + fromToken = fromToken, + toToken = toToken, amount = lastAmount.value, - toProvidersList = findSwapProviders(fromToken, toToken) + toProvidersList = findSwapProviders(fromToken, toToken), ) swapRouter.openScreen(SwapNavScreen.Main) } @@ -460,10 +475,10 @@ internal class SwapViewModel @Inject constructor( inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), ) startLoadingQuotes( - fromToken = newFromToken.currency, - toToken = newToToken.currency, + fromToken = newFromToken, + toToken = newToToken, amount = lastAmount.value, - toProvidersList = findSwapProviders(newFromToken, newToToken) + toProvidersList = findSwapProviders(newFromToken, newToToken), ) } } @@ -479,10 +494,10 @@ internal class SwapViewModel @Inject constructor( stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals)) amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { startLoadingQuotes( - fromToken = fromToken.currency, - toToken = toToken.currency, + fromToken = fromToken, + toToken = toToken, amount = lastAmount.value, - toProvidersList = findSwapProviders(fromToken, toToken) + toProvidersList = findSwapProviders(fromToken, toToken), ) } }