From 6b92fc6179a73b29577b3cdb7f74e7b05457aa76 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Nov 2023 14:26:13 +0200 Subject: [PATCH 01/11] Updated on 2026-08-14 --- .../swap/converters/SwapPairInfoConverter.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 10 ++++++---- .../domain/models/domain/SwapPairLeast.kt | 20 ++++++------------- .../models/ui/TokensDataStateExpress.kt | 6 +++--- .../feature/swap/viewmodels/SwapViewModel.kt | 6 ++++-- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 627a7a8fb4..9889f7d698 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -5,7 +5,7 @@ import com.tangem.datasource.api.express.models.response.SwapPair import com.tangem.datasource.api.express.models.response.SwapPairProvider import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast as SwapPairDomain -import com.tangem.feature.swap.domain.models.domain.SwapPairProvider as SwapPairProviderDomain +import com.tangem.feature.swap.domain.models.domain.SwapProvider as SwapPairProviderDomain import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain import com.tangem.utils.converter.Converter 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 93a6fc6dff..4d0bfd3e52 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 @@ -111,15 +111,17 @@ internal class SwapInteractorImpl @Inject constructor( && tokenInfoForFilter(it).network == currency.network.backendId } - val availableCryptoCurrencies = filteredPairs.mapNotNull { - findCryptoCurrencyStatusByLeastInfo(tokenInfoForAvailable(it), cryptoCurrenciesList) + val availableCryptoCurrencies = filteredPairs.mapNotNull { pair -> + val status = findCryptoCurrencyStatusByLeastInfo(tokenInfoForAvailable(pair), cryptoCurrenciesList) + status?.let { CryptoCurrencySwapInfo(it, pair.providers) } } - val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies.toSet() + val unavailableCryptoCurrencies = + (cryptoCurrenciesList - availableCryptoCurrencies.map { it.currencyStatus }.toSet()) return CurrenciesGroup( available = availableCryptoCurrencies, - unavailable = unavailableCryptoCurrencies + unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) } ) } 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 704a27dc1b..a41f3d283d 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 @@ -1,6 +1,6 @@ package com.tangem.feature.swap.domain.models.domain -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus /** * Domain layer representation of SwapPair data network model. @@ -12,20 +12,12 @@ import com.tangem.domain.tokens.model.CryptoCurrency data class SwapPairLeast( val from: LeastTokenInfo, val to: LeastTokenInfo, - val providers: List, + val providers: List, ) -/** - * Enriched model of swap pair data. Contains full CryptoCurrency models instead of least info. - * - * @property from CryptoCurrency we want to change - * @property to CryptoCurrency we want to exchange for - * @property providers Exchange providers - */ -data class SwapPair( - val from: CryptoCurrency, - val to: CryptoCurrency, - val providers: List, +data class CryptoCurrencySwapInfo( + val currencyStatus: CryptoCurrencyStatus, + val providers: List ) /** @@ -34,7 +26,7 @@ data class SwapPair( * @property providerId provider id * @property rateTypes supported rate types */ -data class SwapPairProvider( +data class SwapProvider( val providerId: Int, val rateTypes: List, ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index d4c4f67494..8beb9ed216 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo data class TokensDataStateExpress( val initialCryptoCurrency: CryptoCurrency, @@ -10,8 +10,8 @@ data class TokensDataStateExpress( ) data class CurrenciesGroup( - val available: List, - val unavailable: List, + val available: List, + val unavailable: List, ) data class FoundTokensStateExpress( 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 119f30fea1..1513b59892 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 @@ -139,8 +139,10 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { swapInteractor.getTokensDataState(currency) }.onSuccess { state -> - // dataState = dataState.copy( - // ) + dataState = dataState.copy( + fromCryptoCurrency = state.initialCryptoCurrency, + toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency + ) cryptoCurrency = state.initialCryptoCurrency // updateTokensState(dataState = state.foundTokensState) // startLoadingQuotes( From b5961302a9202f0b5b76ec3e981b373383f5814d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Nov 2023 17:19:49 +0200 Subject: [PATCH 02/11] Updated on 2026-08-14 --- .../tangem/feature/swap/SwapRepositoryImpl.kt | 26 +++++++++++++++++++ .../swap/converters/RateTypeConverter.kt | 22 ++++++++++++++++ .../swap/converters/SwapPairInfoConverter.kt | 12 +++------ .../feature/swap/domain/SwapRepository.kt | 10 +++++++ .../domain/models/domain/ExchangeQuote.kt | 8 ++++++ .../feature/swap/viewmodels/SwapViewModel.kt | 11 ++++---- 6 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt 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 ce4a54b96e..9eb5bedf15 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 @@ -46,6 +46,7 @@ internal class SwapRepositoryImpl @Inject constructor( private val swapConverter = SwapConverter() private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() + private val rateTypeConverter = RateTypeConverter() override suspend fun getPairs( initialCurrency: LeastTokenInfo, @@ -257,6 +258,31 @@ internal class SwapRepositoryImpl @Inject constructor( return oneInchApiFactory.getApi(networkId) } + override suspend fun getExchangeQuote( + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: BigDecimal, + providerId: Int, + rateType: RateType + ): ExchangeQuote { + val response = tangemExpressApi.getExchangeQuote( + fromContractAddress, + fromNetwork, + toContractAddress, + toNetwork, + fromAmount, + providerId, + rateTypeConverter.convertBack(rateType) + ).getOrThrow() + + return ExchangeQuote( + toAmount = response.toAmount, + allowanceContract = response.allowanceContract + ) + } + companion object { // TODO("get this ids from blockchain enum later") private const val OPTIMISM_ID = "optimistic-ethereum" diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt new file mode 100644 index 0000000000..0d25c4eca2 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.express.models.response.RateType +import com.tangem.utils.converter.TwoWayConverter +import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain + +class RateTypeConverter : TwoWayConverter { + + override fun convert(value: RateType): RateTypeDomain { + return when (value) { + RateType.FIXED -> RateTypeDomain.FIXED + RateType.FLOAT -> RateTypeDomain.FLOAT + } + } + + override fun convertBack(value: RateTypeDomain): RateType { + return when (value) { + RateTypeDomain.FIXED -> RateType.FIXED + RateTypeDomain.FLOAT -> RateType.FLOAT + } + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 9889f7d698..e1e48fc630 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -1,16 +1,16 @@ package com.tangem.feature.swap.converters -import com.tangem.datasource.api.express.models.response.RateType import com.tangem.datasource.api.express.models.response.SwapPair import com.tangem.datasource.api.express.models.response.SwapPairProvider import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast as SwapPairDomain import com.tangem.feature.swap.domain.models.domain.SwapProvider as SwapPairProviderDomain -import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain import com.tangem.utils.converter.Converter class SwapPairInfoConverter : Converter { + private val rateTypeConverter = RateTypeConverter() + override fun convert(value: SwapPair): SwapPairDomain { return SwapPairDomain( from = LeastTokenInfo( @@ -30,14 +30,8 @@ class SwapPairInfoConverter : Converter { private fun convertProvider(swapPairProvider: SwapPairProvider): SwapPairProviderDomain { return SwapPairProviderDomain( providerId = swapPairProvider.providerId, - rateTypes = swapPairProvider.rateTypes.map { convertRateType(it) }, + rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) }, ) } - private fun convertRateType(rateType: RateType): RateTypeDomain { - return when (rateType) { - RateType.FIXED -> RateTypeDomain.FIXED - RateType.FLOAT -> RateTypeDomain.FLOAT - } - } } \ 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 ab0e4095c8..8942e75161 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 @@ -61,4 +61,14 @@ interface SwapRepository { currency: CryptoCurrency, amount: BigDecimal?, ): String + + suspend fun getExchangeQuote( + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: BigDecimal, + providerId: Int, + rateType: RateType, + ) : ExchangeQuote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt new file mode 100644 index 0000000000..d087b97955 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.swap.domain.models.domain + +import java.math.BigDecimal + +data class ExchangeQuote( + val toAmount: BigDecimal, + val allowanceContract: String? +) \ 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 1513b59892..7ba34b693d 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 @@ -143,13 +143,14 @@ internal class SwapViewModel @Inject constructor( fromCryptoCurrency = state.initialCryptoCurrency, toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency ) + cryptoCurrency = state.initialCryptoCurrency // updateTokensState(dataState = state.foundTokensState) - // startLoadingQuotes( - // fromToken = state.preselectTokens.fromToken, - // toToken = state.preselectTokens.toToken, - // amount = lastAmount.value, - // ) + startLoadingQuotes( + fromToken = state.initialCryptoCurrency, + toToken = state.toGroup.available.first().currencyStatus.currency, + amount = lastAmount.value, + ) }.onFailure { Timber.tag(loggingTag).e(it) } From b3df6b20b04396fea561b00ae34074dabac97769 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Nov 2023 11:17:30 +0200 Subject: [PATCH 03/11] Updated on 2026-08-14 --- .../tangem/feature/swap/domain/DefaultBlockchainInteractor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt index 0b9605485b..cb99b58076 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt @@ -27,7 +27,7 @@ internal class DefaultBlockchainInteractor @Inject constructor( return if (token is CryptoCurrency.Token) { token.decimals } else { - transactionManager.getNativeTokenDecimals(token.network.id.value) + transactionManager.getNativeTokenDecimals(token.network.backendId) } } } \ No newline at end of file From 5ad1e47e656bbded6b2bfd532672a37670666707 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Nov 2023 11:38:59 +0200 Subject: [PATCH 04/11] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 763847b303..738424d73c 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 @@ -417,7 +417,7 @@ internal class SwapInteractorImpl @Inject constructor( return if (token is CryptoCurrency.Token) { token.decimals } else { - transactionManager.getNativeTokenDecimals(token.network.id.value) + transactionManager.getNativeTokenDecimals(token.network.backendId) } } From 616c0b2c05e9b07c4442fcf1263544b97e1351f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Nov 2023 22:33:11 +0300 Subject: [PATCH 05/11] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 4 +- .../local/quote/DefaultQuotesStore.kt | 21 +++--- ...kt => GetCryptoCurrencyStatusesUseCase.kt} | 22 +----- .../CurrenciesStatusesOperations.kt | 30 ++++++++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 6 +- .../swap/converters/SwapPairInfoConverter.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 25 +++---- .../feature/swap/domain/SwapRepository.kt | 3 +- .../swap/domain/di/SwapDomainModule.kt | 8 +- .../domain/models/domain/ExchangeQuote.kt | 2 +- .../domain/models/domain/SwapPairLeast.kt | 2 +- features/swap/presentation/build.gradle.kts | 3 + .../swap/converters/TokensDataConverter.kt | 74 +++++++++++++------ .../swap/models/SwapSelectTokenStateHolder.kt | 10 +-- .../tangem/feature/swap/ui/StateBuilder.kt | 19 ++--- .../feature/swap/ui/SwapSelectTokenScreen.kt | 13 ++-- .../swap/viewmodels/SwapProcessDataState.kt | 2 + .../feature/swap/viewmodels/SwapViewModel.kt | 51 +++++++++---- 18 files changed, 173 insertions(+), 123 deletions(-) rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{GetCryptoCurrencyStatusUseCase.kt => GetCryptoCurrencyStatusesUseCase.kt} (65%) 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 66027e8a6b..80f84ebf33 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 @@ -36,8 +36,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json @Suppress("LargeClass") class TradeCryptoMiddleware { @@ -259,7 +257,7 @@ class TradeCryptoMiddleware { private fun openSwap(currency: CryptoCurrency) { val bundle = bundleOf( - SwapFragment.CURRENCY_BUNDLE_KEY to currency + SwapFragment.CURRENCY_BUNDLE_KEY to currency, ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index fc0d1e0e42..e28a90bfb2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -4,8 +4,8 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.datasource.local.quote.model.StoredQuote import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow internal class DefaultQuotesStore( private val dataStore: StringKeyDataStore, @@ -13,20 +13,17 @@ internal class DefaultQuotesStore( override fun get(currenciesIds: Set): Flow> { return channelFlow { - val flows = currenciesIds.mapNotNull { currencyId -> - currencyId.rawCurrencyId?.let(dataStore::get) - } - - if (dataStore.isEmpty() || flows.isEmpty()) { + if (dataStore.isEmpty()) { send(emptySet()) } - merge(*flows.toTypedArray()) - .scan>(emptySet()) { acc, quote -> - acc.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId } + val quotes = currenciesIds.mapNotNull { currencyId -> + currencyId.rawCurrencyId?.let { + dataStore.getSyncOrNull(it) } - .filter(Set::isNotEmpty) - .collect(::send) + } + + send(quotes.toSet()) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt similarity index 65% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt index d917a747c7..2734f1b2e3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt @@ -4,9 +4,7 @@ import arrow.core.Either import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -14,7 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* -class GetCryptoCurrencyStatusUseCase( +class GetCryptoCurrencyStatusesUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, @@ -29,25 +27,9 @@ class GetCryptoCurrencyStatusUseCase( networksRepository = networksRepository, ) - return operations.getCurrenciesStatusesFlow() + return operations.getCurrenciesStatusesMergedFlow() .map { maybeCurrenciesStatuses -> maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) } } - - @Suppress("UnusedPrivateMember") - private fun createTokenList( - userWalletId: UserWalletId, - tokens: List, - ): Flow> { - val operations = TokenListOperations( - userWalletId = userWalletId, - tokens = tokens, - currenciesRepository = currenciesRepository, - ) - - return operations.getTokenListFlow().map { maybeTokenList -> - maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) - } - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 9bce6f0d35..60e3d3b4b0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,6 +67,36 @@ internal class CurrenciesStatusesOperations( } } + fun getCurrenciesStatusesMergedFlow(): Flow>> { + return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies -> + val nonEmptyCurrencies = maybeCurrencies.fold( + ifLeft = { error -> + emit(error.left()) + return@transformLatest + }, + ifRight = List::toNonEmptyListOrNull, + ) + + if (nonEmptyCurrencies == null) { + val emptyCurrenciesStatuses = emptyList() + + emit(emptyCurrenciesStatuses.right()) + return@transformLatest + } + + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + + val currenciesFlow = combine( + getQuotes(currenciesIds), + getNetworksStatuses(networks), + ) { maybeQuotes, maybeNetworksStatuses -> + createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + } + + emitAll(currenciesFlow) + } + } + fun getCardCurrenciesStatusesFlow(): Flow>> { return flow { val nonEmptyCurrencies = recover( 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 9eb5bedf15..9aa4fc368b 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 @@ -265,7 +265,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork: String, fromAmount: BigDecimal, providerId: Int, - rateType: RateType + rateType: RateType, ): ExchangeQuote { val response = tangemExpressApi.getExchangeQuote( fromContractAddress, @@ -274,12 +274,12 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork, fromAmount, providerId, - rateTypeConverter.convertBack(rateType) + rateTypeConverter.convertBack(rateType), ).getOrThrow() return ExchangeQuote( toAmount = response.toAmount, - allowanceContract = response.allowanceContract + allowanceContract = response.allowanceContract, ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index e1e48fc630..c205bec15e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -33,5 +33,4 @@ class SwapPairInfoConverter : Converter { rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) }, ) } - } \ No newline at end of file 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 738424d73c..f9d1faa5bb 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,7 +2,7 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network @@ -39,7 +39,7 @@ internal class SwapInteractorImpl @Inject constructor( private val networksRepository: NetworksRepository, private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesUseCase, ) : SwapInteractor { // TODO: Move to DI @@ -58,7 +58,7 @@ internal class SwapInteractorImpl @Inject constructor( ifRight = { it }, ) - requireNotNull(selectedWallet) + requireNotNull(selectedWallet) { "No selected wallet" } val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) .first() @@ -112,12 +112,13 @@ internal class SwapInteractorImpl @Inject constructor( status?.let { CryptoCurrencySwapInfo(it, pair.providers) } } - val unavailableCryptoCurrencies = - (cryptoCurrenciesList - availableCryptoCurrencies.map { it.currencyStatus }.toSet()) + val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies + .map { it.currencyStatus } + .toSet() return CurrenciesGroup( available = availableCryptoCurrencies, - unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) } + unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) }, ) } @@ -138,14 +139,10 @@ internal class SwapInteractorImpl @Inject constructor( } } - private fun Currency.getContractAddress(): String { - return when (this) { - is Currency.NativeToken -> "0" - is Currency.NonNativeToken -> this.contractAddress - } - } - - suspend fun getPairs(initialCurrency: LeastTokenInfo, currenciesList: List): List { + private suspend fun getPairs( + initialCurrency: LeastTokenInfo, + currenciesList: List, + ): List { return repository.getPairs(initialCurrency, currenciesList) } 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 8942e75161..d64b0fa8f0 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 @@ -62,6 +62,7 @@ interface SwapRepository { amount: BigDecimal?, ): String + @Suppress("LongParameterList") suspend fun getExchangeQuote( fromContractAddress: String, fromNetwork: String, @@ -70,5 +71,5 @@ interface SwapRepository { fromAmount: BigDecimal, providerId: Int, rateType: RateType, - ) : ExchangeQuote + ): 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 dc882859ae..3169f16427 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 @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -35,7 +35,7 @@ class SwapDomainModule { networksRepository: NetworksRepository, walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, + @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -81,8 +81,8 @@ class SwapDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, - ): GetCryptoCurrencyStatusUseCase { - return GetCryptoCurrencyStatusUseCase( + ): GetCryptoCurrencyStatusesUseCase { + return GetCryptoCurrencyStatusesUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt index d087b97955..ee352e70c4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt @@ -4,5 +4,5 @@ import java.math.BigDecimal data class ExchangeQuote( val toAmount: BigDecimal, - val allowanceContract: String? + val allowanceContract: String?, ) \ No newline at end of file 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 a41f3d283d..ee772becce 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 @@ -17,7 +17,7 @@ data class SwapPairLeast( data class CryptoCurrencySwapInfo( val currencyStatus: CryptoCurrencyStatus, - val providers: List + val providers: List, ) /** diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 1068c49d28..8be06eaf61 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -17,6 +17,8 @@ dependencies { implementation(projects.common) /** Domain modules **/ + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) @@ -28,6 +30,7 @@ dependencies { implementation(deps.androidx.browser) /** Compose */ + implementation(deps.arrow.core) implementation(deps.compose.foundation) implementation(deps.compose.material) implementation(deps.compose.ui.tooling) 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 index bb52e2b01d..03047e24b8 100644 --- 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 @@ -2,52 +2,80 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.TokenIconState -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.feature.swap.domain.models.domain.NetworkInfo -import com.tangem.feature.swap.domain.models.ui.FoundTokensStateExpress -import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress -import com.tangem.feature.swap.models.Network +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState +import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList class TokensDataConverter( private val onSearchEntered: (String) -> Unit, private val onTokenSelected: (String) -> Unit, private val isBalanceHiddenProvider: Provider, -) { + private val appCurrencyProvider: Provider, +) : Converter { - fun convertWithNetwork(value: FoundTokensStateExpress, network: NetworkInfo): SwapSelectTokenStateHolder { + override fun convert(value: CurrenciesGroup): SwapSelectTokenStateHolder { + val availableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource + val unavailableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource return SwapSelectTokenStateHolder( - availableTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), - unavailableTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), + availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it) } + .toMutableList() + .apply { + this.add(0, availableTitle) + } + .toImmutableList(), + unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it) } + .toMutableList() + .apply { + this.add(0, unavailableTitle) + } + .toImmutableList(), onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, - network = Network(network.name, network.blockchainId), ) } - private fun tokenWithBalanceToTokenToSelect( - tokenWithBalance: TokenWithBalanceExpress, - ): TokenToSelectState.TokenToSelect { + private fun tokenWithBalanceToTokenToSelect(cryptoCurrencySwapInfo: CryptoCurrencySwapInfo): TokenToSelectState { + val cryptoCurrencyStatus = cryptoCurrencySwapInfo.currencyStatus return TokenToSelectState.TokenToSelect( - id = tokenWithBalance.token.id.value, - name = tokenWithBalance.token.name, - symbol = tokenWithBalance.token.symbol, - isNative = tokenWithBalance.token is CryptoCurrency.Coin, - // todo replace converting + id = cryptoCurrencyStatus.currency.id.value, + name = cryptoCurrencyStatus.currency.name, + symbol = cryptoCurrencyStatus.currency.symbol, tokenIcon = TokenIconState.CoinIcon( - url = "", - fallbackResId = 0, + url = cryptoCurrencyStatus.currency.iconUrl, + fallbackResId = cryptoCurrencyStatus.currency.networkIconResId, isGrayscale = false, - showCustomBadge = false, + showCustomBadge = cryptoCurrencyStatus.currency.isCustom, ), addedTokenBalanceData = TokenBalanceData( - amount = tokenWithBalance.tokenBalanceData?.amount, - amountEquivalent = tokenWithBalance.tokenBalanceData?.amountEquivalent, + amount = formatCryptoAmount(cryptoCurrencyStatus), + amountEquivalent = formatFiatAmount(cryptoCurrencyStatus, appCurrencyProvider.invoke()), isBalanceHidden = isBalanceHiddenProvider.invoke(), ), ) } + + private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String { + return BigDecimalFormatter.formatCryptoAmount( + cryptoCurrencyStatus.value.amount, + cryptoCurrencyStatus.currency.symbol, + cryptoCurrencyStatus.currency.decimals, + ) + } + + private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = cryptoCurrencyStatus.value.fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 382da8e588..ac6762d7b7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -1,30 +1,24 @@ package com.tangem.feature.swap.models import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList data class SwapSelectTokenStateHolder( val availableTokens: ImmutableList, val unavailableTokens: ImmutableList, - val network: Network, val onSearchEntered: (String) -> Unit, val onTokenSelected: (String) -> Unit, ) -data class Network( - val name: String, - val blockchainId: String, -) - sealed class TokenToSelectState { - data class Title(val title: String) : TokenToSelectState() + data class Title(val title: TextReference) : TokenToSelectState() data class TokenToSelect( val id: String, val name: String, val symbol: String, - val isNative: Boolean, val tokenIcon: TokenIconState, val available: Boolean = true, val addedTokenBalanceData: TokenBalanceData? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index bea53175af..510502e088 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 @@ -9,6 +9,7 @@ import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError @@ -24,12 +25,17 @@ import kotlinx.collections.immutable.toImmutableList * State builder creates a specific states for SwapScreen */ @Suppress("LargeClass") -internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: Provider) { +internal class StateBuilder( + private val actions: UiActions, + private val isBalanceHiddenProvider: Provider, + appCurrencyProvider: Provider, +) { private val tokensDataConverter = TokensDataConverter( onSearchEntered = actions.onSearchEntered, onTokenSelected = actions.onTokenSelected, isBalanceHiddenProvider = isBalanceHiddenProvider, + appCurrencyProvider = appCurrencyProvider, ) fun createInitialLoadingState(initialCurrency: CryptoCurrency, networkInfo: NetworkInfo): SwapStateHolder { @@ -247,15 +253,10 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: ) } - fun addTokensToState( - uiState: SwapStateHolder, - dataState: FoundTokensStateExpress, - networkInfo: NetworkInfo, - ): SwapStateHolder { + fun addTokensToState(uiState: SwapStateHolder, tokensDataState: CurrenciesGroup): SwapStateHolder { return uiState.copy( - selectTokenState = tokensDataConverter.convertWithNetwork( - value = dataState, - network = networkInfo, + selectTokenState = tokensDataConverter.convert( + value = tokensDataState, ), ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 1faa262bb3..2bbeedc808 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.Strings @@ -20,7 +19,8 @@ import com.tangem.core.ui.components.appbar.ExpandableSearchView import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.R @@ -43,8 +43,7 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) placeholderSearchText = stringResource(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered, onSearchDisplayClose = { state.onSearchEntered("") }, - subtitle = state.network.name, - icon = painterResource(id = getActiveIconRes(state.network.blockchainId)), + subtitle = "", // todo add title ) }, ) @@ -112,7 +111,7 @@ private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Mod .background(TangemTheme.colors.background.action), ) { Text( - text = item.title, + text = item.title.resolveReference(), style = TangemTheme.typography.overline, modifier = Modifier .padding( @@ -220,7 +219,6 @@ private val token = TokenToSelectState.TokenToSelect( id = "", name = "USDC", symbol = "USDC", - isNative = false, addedTokenBalanceData = TokenBalanceData( amount = "15 000 $", amountEquivalent = "15 000 " + @@ -230,7 +228,7 @@ private val token = TokenToSelectState.TokenToSelect( ) private val title = TokenToSelectState.Title( - title = "MY TOKENS", + title = stringReference("MY TOKENS"), ) @Preview @@ -243,7 +241,6 @@ private fun TokenScreenPreview() { unavailableTokens = listOf(title, token, token, token).toImmutableList(), onSearchEntered = {}, onTokenSelected = {}, - network = Network("Ethereum", "ETH"), ), onBack = {}, ) 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 65abcf814c..c9018a2345 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 @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency 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 data class SwapProcessDataState( @@ -20,4 +21,5 @@ data class SwapProcessDataState( val approveDataModel: RequestApproveStateData? = null, val swapDataModel: SwapStateData? = null, val selectedFee: TxFee? = null, + val tokensDataState: TokensDataStateExpress? = null, ) \ 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 99919c05a3..0f2d531f35 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 @@ -4,9 +4,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import arrow.core.getOrElse import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.analytics.SwapEvents @@ -27,11 +30,9 @@ 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.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json import timber.log.Timber import java.text.DecimalFormat import java.text.NumberFormat @@ -47,6 +48,7 @@ internal class SwapViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -55,9 +57,12 @@ internal class SwapViewModel @Inject constructor( private var isBalanceHidden = true + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private val stateBuilder = StateBuilder( actions = createUiActions(), isBalanceHiddenProvider = Provider { isBalanceHidden }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), ) private val inputNumberFormatter = @@ -87,7 +92,7 @@ internal class SwapViewModel @Inject constructor( derivationPath = initialCryptoCurrency.network.derivationPath.value, network = initialCryptoCurrency.network, ) - initTokens(initialCryptoCurrency) + initTokens() } override fun onCreate(owner: LifecycleOwner) { @@ -126,7 +131,7 @@ internal class SwapViewModel @Inject constructor( } @Suppress("UnusedPrivateMember") - private fun initTokens(currency: CryptoCurrency) { + private fun initTokens() { // new flow viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { @@ -134,10 +139,11 @@ internal class SwapViewModel @Inject constructor( }.onSuccess { state -> dataState = dataState.copy( fromCryptoCurrency = initialCryptoCurrency, - toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency + toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency, + tokensDataState = state, ) - // updateTokensState(dataState = state.foundTokensState) + updateTokensState(state) startLoadingQuotes( fromToken = initialCryptoCurrency, toToken = state.toGroup.available.first().currencyStatus.currency, @@ -171,11 +177,11 @@ internal class SwapViewModel @Inject constructor( // } } - private fun updateTokensState(dataState: FoundTokensStateExpress) { + private fun updateTokensState(dataState: TokensDataStateExpress) { + val tokensDataState = if (!isOrderReversed) dataState.toGroup else dataState.fromGroup uiState = stateBuilder.addTokensToState( uiState = uiState, - dataState = dataState, - networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), + tokensDataState = tokensDataState, ) } @@ -369,28 +375,31 @@ internal class SwapViewModel @Inject constructor( swapInteractor.searchTokens(dataState.networkId, searchQuery) } .onSuccess { - updateTokensState(it) + // updateTokensState(it) } .onFailure { } } } private fun onTokenSelect(id: String) { - val foundToken = swapInteractor.findTokenById(id) + val tokens = dataState.tokensDataState ?: return + val foundToken = tokens.toGroup.available.firstOrNull { + it.currencyStatus.currency.id.value == id + } analyticsEventHandler.send( - event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.symbol), + event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.currencyStatus?.currency?.symbol), ) if (foundToken != null) { val fromToken: CryptoCurrency val toToken: CryptoCurrency if (isOrderReversed) { - fromToken = foundToken + fromToken = foundToken.currencyStatus.currency toToken = initialCryptoCurrency } else { fromToken = initialCryptoCurrency - toToken = foundToken + toToken = foundToken.currencyStatus.currency } dataState = dataState.copy( fromCryptoCurrency = fromToken, @@ -532,6 +541,18 @@ internal class SwapViewModel @Inject constructor( ) } + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" From 60d8a4f2a320313fd773e906f18093c96ab9c97d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 12:03:02 +0300 Subject: [PATCH 06/11] Updated on 2026-08-14 --- .../local/quote/DefaultQuotesStore.kt | 21 +++++---- ...> GetCryptoCurrencyStatusesSyncUseCase.kt} | 11 ++--- .../CurrenciesStatusesOperations.kt | 38 ++++++--------- .../feature/swap/domain/SwapInteractorImpl.kt | 6 +-- .../swap/domain/di/SwapDomainModule.kt | 8 ++-- .../swap/converters/TokensDataConverter.kt | 46 +++++++++++++++---- .../feature/swap/ui/SwapSelectTokenScreen.kt | 21 +++++---- 7 files changed, 83 insertions(+), 68 deletions(-) rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{GetCryptoCurrencyStatusesUseCase.kt => GetCryptoCurrencyStatusesSyncUseCase.kt} (73%) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index e28a90bfb2..fc0d1e0e42 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -4,8 +4,8 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.datasource.local.quote.model.StoredQuote import com.tangem.domain.tokens.model.CryptoCurrency -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.* internal class DefaultQuotesStore( private val dataStore: StringKeyDataStore, @@ -13,17 +13,20 @@ internal class DefaultQuotesStore( override fun get(currenciesIds: Set): Flow> { return channelFlow { - if (dataStore.isEmpty()) { + val flows = currenciesIds.mapNotNull { currencyId -> + currencyId.rawCurrencyId?.let(dataStore::get) + } + + if (dataStore.isEmpty() || flows.isEmpty()) { send(emptySet()) } - val quotes = currenciesIds.mapNotNull { currencyId -> - currencyId.rawCurrencyId?.let { - dataStore.getSyncOrNull(it) + merge(*flows.toTypedArray()) + .scan>(emptySet()) { acc, quote -> + acc.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId } } - } - - send(quotes.toSet()) + .filter(Set::isNotEmpty) + .collect(::send) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt similarity index 73% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt index 2734f1b2e3..cdf58b3080 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt @@ -10,16 +10,15 @@ import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* -class GetCryptoCurrencyStatusesUseCase( +class GetCryptoCurrencyStatusesSyncUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { - operator fun invoke(userWalletId: UserWalletId): Flow>> { + suspend operator fun invoke(userWalletId: UserWalletId): Either> { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, currenciesRepository = currenciesRepository, @@ -27,9 +26,7 @@ class GetCryptoCurrencyStatusesUseCase( networksRepository = networksRepository, ) - return operations.getCurrenciesStatusesMergedFlow() - .map { maybeCurrenciesStatuses -> - maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) - } + return operations.getCurrenciesStatusesSync() + .mapLeft { error -> error.mapToTokenListError() } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 60e3d3b4b0..3ce6112296 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,33 +67,21 @@ internal class CurrenciesStatusesOperations( } } - fun getCurrenciesStatusesMergedFlow(): Flow>> { - return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies -> - val nonEmptyCurrencies = maybeCurrencies.fold( - ifLeft = { error -> - emit(error.left()) - return@transformLatest + suspend fun getCurrenciesStatusesSync(): Either> { + return either { + catch( + block = { + val nonEmptyCurrencies = + currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull() + ?: return emptyList().right() + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() + val networkStatuses = + networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() + return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses) }, - ifRight = List::toNonEmptyListOrNull, + catch = { raise(Error.DataError(it)) }, ) - - if (nonEmptyCurrencies == null) { - val emptyCurrenciesStatuses = emptyList() - - emit(emptyCurrenciesStatuses.right()) - return@transformLatest - } - - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - - val currenciesFlow = combine( - getQuotes(currenciesIds), - getNetworksStatuses(networks), - ) { maybeQuotes, maybeNetworksStatuses -> - createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) - } - - emitAll(currenciesFlow) } } 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 f9d1faa5bb..ec089f6df7 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,7 +2,7 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusesUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network @@ -22,7 +22,6 @@ 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.flow.first import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -39,7 +38,7 @@ internal class SwapInteractorImpl @Inject constructor( private val networksRepository: NetworksRepository, private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, ) : SwapInteractor { // TODO: Move to DI @@ -61,7 +60,6 @@ internal class SwapInteractorImpl @Inject constructor( requireNotNull(selectedWallet) { "No selected wallet" } val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) - .first() .getOrElse { emptyList() } val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses.filter { 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 3169f16427..7fa93f1c70 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 @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusesUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -35,7 +35,7 @@ class SwapDomainModule { networksRepository: NetworksRepository, walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesUseCase, + @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -81,8 +81,8 @@ class SwapDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, - ): GetCryptoCurrencyStatusesUseCase { - return GetCryptoCurrencyStatusesUseCase( + ): GetCryptoCurrencyStatusesSyncUseCase { + return GetCryptoCurrencyStatusesSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, 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 index 03047e24b8..e3eff35d4c 100644 --- 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 @@ -2,10 +2,13 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.getTintForTokenIcon import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup @@ -26,13 +29,13 @@ class TokensDataConverter( val availableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource val unavailableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource return SwapSelectTokenStateHolder( - availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it) } + availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() .apply { this.add(0, availableTitle) } .toImmutableList(), - unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it) } + unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } .toMutableList() .apply { this.add(0, unavailableTitle) @@ -43,18 +46,17 @@ class TokensDataConverter( ) } - private fun tokenWithBalanceToTokenToSelect(cryptoCurrencySwapInfo: CryptoCurrencySwapInfo): TokenToSelectState { + private fun tokenWithBalanceToTokenToSelect( + cryptoCurrencySwapInfo: CryptoCurrencySwapInfo, + isAvailable: Boolean, + ): TokenToSelectState { val cryptoCurrencyStatus = cryptoCurrencySwapInfo.currencyStatus return TokenToSelectState.TokenToSelect( id = cryptoCurrencyStatus.currency.id.value, name = cryptoCurrencyStatus.currency.name, symbol = cryptoCurrencyStatus.currency.symbol, - tokenIcon = TokenIconState.CoinIcon( - url = cryptoCurrencyStatus.currency.iconUrl, - fallbackResId = cryptoCurrencyStatus.currency.networkIconResId, - isGrayscale = false, - showCustomBadge = cryptoCurrencyStatus.currency.isCustom, - ), + available = isAvailable, + tokenIcon = convertIcon(cryptoCurrencyStatus.currency, isAvailable), addedTokenBalanceData = TokenBalanceData( amount = formatCryptoAmount(cryptoCurrencyStatus), amountEquivalent = formatFiatAmount(cryptoCurrencyStatus, appCurrencyProvider.invoke()), @@ -63,6 +65,32 @@ class TokensDataConverter( ) } + private fun convertIcon(currency: CryptoCurrency, isAvailable: Boolean): TokenIconState { + return when (currency) { + is CryptoCurrency.Coin -> { + TokenIconState.CoinIcon( + url = currency.iconUrl, + fallbackResId = currency.networkIconResId, + isGrayscale = !isAvailable, + showCustomBadge = currency.isCustom, + ) + } + is CryptoCurrency.Token -> { + val isGrayscale = currency.network.isTestnet + val background = currency.tryGetBackgroundForTokenIcon(isGrayscale) + val tint = getTintForTokenIcon(background) + TokenIconState.TokenIcon( + url = currency.iconUrl, + isGrayscale = !isAvailable, + showCustomBadge = currency.isCustom, + networkBadgeIconResId = currency.networkIconResId, + fallbackTint = tint, + fallbackBackground = background, + ) + } + } + } + private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String { return BigDecimalFormatter.formatCryptoAmount( cryptoCurrencyStatus.value.amount, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 2bbeedc808..6e89f78c3a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -111,7 +111,7 @@ private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Mod .background(TangemTheme.colors.background.action), ) { Text( - text = item.title.resolveReference(), + text = item.title.resolveReference().uppercase(), style = TangemTheme.typography.overline, modifier = Modifier .padding( @@ -133,7 +133,10 @@ private fun TokenItem( modifier = modifier .fillMaxWidth() .height(TangemTheme.dimens.size72) - .clickable(onClick = onTokenClick) + .clickable( + enabled = token.available, + onClick = onTokenClick, + ) .padding( vertical = TangemTheme.dimens.spacing14, horizontal = TangemTheme.dimens.spacing16, @@ -169,13 +172,7 @@ private fun TokenItem( Spacer(modifier = Modifier.weight(1f)) - if (!token.available) { - Text( - text = stringResource(id = R.string.swapping_token_not_available), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } else if (token.addedTokenBalanceData != null) { + if (token.addedTokenBalanceData != null) { Column( horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Center, @@ -190,7 +187,11 @@ private fun TokenItem( token.addedTokenBalanceData.amountEquivalent.orEmpty() }, style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, + color = if (token.available) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.tertiary + }, ) SpacerW2() Text( From fc9be53caf7ba8450639aba77c7cf7ee94791267 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 13:47:40 +0200 Subject: [PATCH 07/11] Updated on 2026-08-14 --- .../datasource/api/express/ExpressApi.kt | 53 ------------------- .../api/express/TangemExpressApi.kt | 4 +- .../models/response/ExchangeQuoteResponse.kt | 14 +++++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 41 +++++++++----- .../feature/swap/domain/SwapInteractor.kt | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 27 +++++++--- .../feature/swap/domain/SwapRepository.kt | 13 +++-- .../swap/domain/models/ui/SwapState.kt | 1 + .../feature/swap/viewmodels/SwapViewModel.kt | 16 +++++- 9 files changed, 87 insertions(+), 83 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt deleted file mode 100644 index 0ed82c76d9..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.datasource.api.express - -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.express.models.request.AssetsRequestBody -import com.tangem.datasource.api.express.models.request.PairsRequestBody -import com.tangem.datasource.api.express.models.response.* -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Query -import java.math.BigDecimal - -/** - * Interface of Tangem Express API (new swap mechanism) - */ -@Suppress("LongParameterList") -interface ExpressApi { - - @POST("assets") - suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse> - - @POST("pairs") - suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse> - - @GET("providers") - suspend fun getProviders(): ApiResponse> - - @GET("exchange-quote") - suspend fun getExchangeQuote( - @Query("fromContractAddress") fromContractAddress: String, - @Query("fromNetwork") fromNetwork: String, - @Query("toContractAddress") toContractAddress: String, - @Query("toNetwork") toNetwork: String, - @Query("fromAmount") fromAmount: BigDecimal, - @Query("providerId") providerId: Int, - @Query("rateType") rateType: RateType, - ): ApiResponse - - @GET("exchange-data") - suspend fun getExchangeData( - @Query("fromContractAddress") fromContractAddress: String, - @Query("fromNetwork") fromNetwork: String, - @Query("toContractAddress") toContractAddress: String, - @Query("toNetwork") toNetwork: String, - @Query("fromAmount") fromAmount: BigDecimal, - @Query("providerId") providerId: Int, - @Query("rateType") rateType: RateType, - @Query("toAddress") toAddress: String, - ): ApiResponse - - @GET("exchange-result") - suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse -} \ No newline at end of file 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 627e70bdd0..0c1fb16cf8 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 @@ -31,9 +31,9 @@ interface TangemExpressApi { @Query("fromNetwork") fromNetwork: String, @Query("toContractAddress") toContractAddress: String, @Query("toNetwork") toNetwork: String, - @Query("fromAmount") fromAmount: BigDecimal, + @Query("fromAmount") fromAmount: String, @Query("providerId") providerId: Int, - @Query("rateType") rateType: RateType, + @Query("rateType") rateType: String, ): ApiResponse @GET("exchange-data") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt index 214b4ac2d5..de2368e418 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt @@ -4,9 +4,23 @@ import com.squareup.moshi.Json import java.math.BigDecimal data class ExchangeQuoteResponse( + + @Json(name = "fromAmount") + val fromAmount: BigDecimal, + + @Json(name = "fromDecimals") + val fromDecimals: Int, + @Json(name = "toAmount") val toAmount: BigDecimal, + @Json(name = "toDecimals") + val toDecimals: Int, + @Json(name = "allowanceContract") val allowanceContract: String?, + + @Json(name = "minAmount") + val minAmount: BigDecimal, + ) \ 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 9eb5bedf15..ff7b2277ed 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 @@ -20,13 +20,16 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.mapErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.withContext +import retrofit2.http.Query import java.math.BigDecimal +import java.util.Locale import javax.inject.Inject import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo @@ -127,21 +130,33 @@ internal class SwapRepositoryImpl @Inject constructor( } override suspend fun findBestQuote( - networkId: String, - fromTokenAddress: String, - toTokenAddress: String, - amount: String, + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: String, + providerId: Int, + rateType: RateType, ): AggregatedSwapDataModel { return withContext(coroutineDispatcher.io) { try { - val response = oneInchErrorsHandler.handleOneInchResponse( - getOneInchApi(networkId).quote( - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - amount = amount, - ), + val response = tangemExpressApi.getExchangeQuote( + fromContractAddress = fromContractAddress, + fromNetwork = fromNetwork, + toContractAddress = toContractAddress, + toNetwork = toNetwork, + fromAmount = fromAmount, + providerId = providerId, + rateType = rateType.name.lowercase() + ).getOrThrow() + AggregatedSwapDataModel( + dataModel = QuoteModel( + SwapAmount( + value = response.toAmount, + decimals = response.toDecimals + ) + ) ) - AggregatedSwapDataModel(dataModel = quotesConverter.convert(response)) } catch (ex: OneIncResponseException) { AggregatedSwapDataModel(null, mapErrors(ex.data.description)) } @@ -263,7 +278,7 @@ internal class SwapRepositoryImpl @Inject constructor( fromNetwork: String, toContractAddress: String, toNetwork: String, - fromAmount: BigDecimal, + fromAmount: String, providerId: Int, rateType: RateType ): ExchangeQuote { @@ -274,7 +289,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork, fromAmount, providerId, - rateTypeConverter.convertBack(rateType) + rateType.name.lowercase() ).getOrThrow() return ExchangeQuote( 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 2757583249..4db2ed600b 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 @@ -66,6 +66,7 @@ interface SwapInteractor { networkId: String, fromToken: CryptoCurrency, toToken: CryptoCurrency, + providers: List, amountToSwap: String, selectedFee: FeeType = FeeType.NORMAL, ): SwapState 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 738424d73c..aaa47663ff 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 @@ -274,6 +274,7 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, fromToken: CryptoCurrency, toToken: CryptoCurrency, + providers: List, amountToSwap: String, selectedFee: FeeType, ): SwapState { @@ -311,6 +312,7 @@ internal class SwapInteractorImpl @Inject constructor( toToken = toToken, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + providers = providers ) } } @@ -518,14 +520,23 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fromToken: CryptoCurrency, toToken: CryptoCurrency, + providers: List, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, ): SwapState { repository.findBestQuote( - networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - amount = amount.toStringWithRightOffset(), + fromContractAddress = fromToken.getContractAddress(), + fromNetwork = fromToken.network.backendId, + toContractAddress = toToken.getContractAddress(), + toNetwork = toToken.network.backendId, + fromAmount = amount.toStringWithRightOffset(), + providerId = providers[0].providerId, + rateType = RateType.FLOAT + + // networkId = networkId, + // fromTokenAddress = fromTokenAddress, + // toTokenAddress = toTokenAddress, + // amount = amount.toStringWithRightOffset(), ).let { quotes -> val quoteDataModel = quotes.dataModel if (quoteDataModel != null) { @@ -668,16 +679,16 @@ internal class SwapInteractorImpl @Inject constructor( tokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: ZERO_BALANCE, tokenFiatBalance = toTokenAmount.value.toFiatString( - rateValue = rates[toToken.id.value]?.toBigDecimal() ?: BigDecimal.ZERO, + rateValue = rates[toToken.network.backendId]?.toBigDecimal() ?: BigDecimal.ZERO, fiatCurrencyName = appCurrency.symbol, formatWithSpaces = true, ), ), priceImpact = calculatePriceImpact( fromTokenAmount = fromTokenAmount.value, - fromRate = rates[fromToken.id.value] ?: 0.0, + fromRate = rates[fromToken.network.backendId] ?: 0.0, toTokenAmount = toTokenAmount.value, - toRate = rates[toToken.id.value] ?: 0.0, + toRate = rates[toToken.network.backendId] ?: 0.0, ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), swapDataModel = swapStateData, @@ -819,7 +830,7 @@ internal class SwapInteractorImpl @Inject constructor( private fun getTokenAddress(currency: CryptoCurrency): String { return when (currency) { is CryptoCurrency.Coin -> { - DEFAULT_BLOCKCHAIN_INCH_ADDRESS + "0" } is CryptoCurrency.Token -> { currency.contractAddress 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 8942e75161..2e0a8cbf0f 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 @@ -15,10 +15,13 @@ interface SwapRepository { suspend fun getExchangeableTokens(networkId: String): List suspend fun findBestQuote( - networkId: String, - fromTokenAddress: String, - toTokenAddress: String, - amount: String, + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: String, + providerId: Int, + rateType: RateType, ): AggregatedSwapDataModel /** @@ -67,7 +70,7 @@ interface SwapRepository { fromNetwork: String, toContractAddress: String, toNetwork: String, - fromAmount: BigDecimal, + fromAmount: String, providerId: Int, rateType: RateType, ) : ExchangeQuote 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..14610d708f 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 @@ -9,6 +9,7 @@ import java.math.BigDecimal sealed interface SwapState { data class QuotesLoadedState( + //add map < Provider ID, Swap state data> val fromTokenInfo: TokenSwapInfo, val toTokenInfo: TokenSwapInfo, val priceImpact: Float, 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 99919c05a3..8af439d54f 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 @@ -13,6 +13,8 @@ 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.* import com.tangem.feature.swap.models.SwapPermissionState @@ -138,10 +140,12 @@ internal class SwapViewModel @Inject constructor( ) // updateTokensState(dataState = state.foundTokensState) + val toToken = state.toGroup.available.first() startLoadingQuotes( fromToken = initialCryptoCurrency, - toToken = state.toGroup.available.first().currencyStatus.currency, + toToken = toToken.currencyStatus.currency, amount = lastAmount.value, + toProvidersList = toToken.providers ) }.onFailure { Timber.tag(loggingTag).e(it) @@ -179,7 +183,12 @@ internal class SwapViewModel @Inject constructor( ) } - private fun startLoadingQuotes(fromToken: CryptoCurrency, toToken: CryptoCurrency, amount: String) { + private fun startLoadingQuotes( + fromToken: CryptoCurrency, + toToken: CryptoCurrency, + amount: String, + toProvidersList: List = listOf(SwapProvider(1, listOf(RateType.FLOAT))), + ) { singleTaskScheduler.cancelTask() uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, initialCryptoCurrency.id.value) singleTaskScheduler.scheduleTask( @@ -188,6 +197,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromToken, toToken = toToken, amount = amount, + toProvidersList = toProvidersList ), ) } @@ -205,6 +215,7 @@ internal class SwapViewModel @Inject constructor( fromToken: CryptoCurrency, toToken: CryptoCurrency, amount: String, + toProvidersList: List, ): PeriodicTask { return PeriodicTask( UPDATE_DELAY, @@ -220,6 +231,7 @@ internal class SwapViewModel @Inject constructor( networkId = dataState.networkId, fromToken = fromToken, toToken = toToken, + providers = toProvidersList, amountToSwap = amount, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) From 0e55f23e8e380e1f717496dc566d7f21b7fd0685 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 17:44:15 +0200 Subject: [PATCH 08/11] Updated on 2026-08-14 --- .../models/response/ExchangeQuoteResponse.kt | 4 ++-- .../tangem/feature/swap/SwapRepositoryImpl.kt | 17 ++++++----------- .../swap/converters/SwapPairInfoConverter.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 18 ++++++++---------- .../feature/swap/domain/SwapRepository.kt | 3 ++- .../swap/domain/models/domain/ExchangeQuote.kt | 6 ++---- .../feature/swap/domain/models/ui/SwapState.kt | 2 +- .../feature/swap/viewmodels/SwapViewModel.kt | 4 ++-- 8 files changed, 23 insertions(+), 32 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt index de2368e418..e643811822 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt @@ -6,13 +6,13 @@ import java.math.BigDecimal data class ExchangeQuoteResponse( @Json(name = "fromAmount") - val fromAmount: BigDecimal, + val fromAmount: String, @Json(name = "fromDecimals") val fromDecimals: Int, @Json(name = "toAmount") - val toAmount: BigDecimal, + val toAmount: String, @Json(name = "toDecimals") val toDecimals: Int, 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 8d9f1918ab..0f8e561982 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 @@ -20,16 +20,14 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.SwapRepository -import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.mapErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.withContext -import retrofit2.http.Query import java.math.BigDecimal -import java.util.Locale import javax.inject.Inject import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo @@ -147,15 +145,12 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork = toNetwork, fromAmount = fromAmount, providerId = providerId, - rateType = rateType.name.lowercase() + rateType = rateType.name.lowercase(), ).getOrThrow() AggregatedSwapDataModel( dataModel = QuoteModel( - SwapAmount( - value = response.toAmount, - decimals = response.toDecimals - ) - ) + toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals), + ), ) } catch (ex: OneIncResponseException) { AggregatedSwapDataModel(null, mapErrors(ex.data.description)) @@ -280,7 +275,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork: String, fromAmount: String, providerId: Int, - rateType: RateType + rateType: RateType, ): ExchangeQuote { val response = tangemExpressApi.getExchangeQuote( fromContractAddress, @@ -289,7 +284,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork, fromAmount, providerId, - rateType.name.lowercase() + rateType.name.lowercase(), ).getOrThrow() return ExchangeQuote( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index e1e48fc630..c205bec15e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -33,5 +33,4 @@ class SwapPairInfoConverter : Converter { rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) }, ) } - } \ No newline at end of file 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 68e8846de4..88f790c9c3 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 @@ -300,14 +300,12 @@ internal class SwapInteractorImpl @Inject constructor( } else { loadQuoteData( networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, amount = amount, fromToken = fromToken, toToken = toToken, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - providers = providers + providers = providers, ) } } @@ -510,8 +508,6 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("LongParameterList") private suspend fun loadQuoteData( networkId: String, - fromTokenAddress: String, - toTokenAddress: String, amount: SwapAmount, fromToken: CryptoCurrency, toToken: CryptoCurrency, @@ -520,13 +516,13 @@ internal class SwapInteractorImpl @Inject constructor( isBalanceWithoutFeeEnough: Boolean, ): SwapState { repository.findBestQuote( - fromContractAddress = fromToken.getContractAddress(), + fromContractAddress = fromToken.getContractAddress(), fromNetwork = fromToken.network.backendId, toContractAddress = toToken.getContractAddress(), toNetwork = toToken.network.backendId, fromAmount = amount.toStringWithRightOffset(), providerId = providers[0].providerId, - rateType = RateType.FLOAT + rateType = RateType.FLOAT, // networkId = networkId, // fromTokenAddress = fromTokenAddress, @@ -653,7 +649,10 @@ internal class SwapInteractorImpl @Inject constructor( ): SwapState.QuotesLoadedState { val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) - val rates = repository.getRates(appCurrency.code, listOf(fromToken.id.value, toToken.id.value, nativeToken.id)) + 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( @@ -663,7 +662,7 @@ internal class SwapInteractorImpl @Inject constructor( tokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: ZERO_BALANCE, tokenFiatBalance = fromTokenAmount.value.toFiatString( - rateValue = rates[fromToken.id.value]?.toBigDecimal() ?: BigDecimal.ZERO, + rateValue = rates[fromToken.network.backendId]?.toBigDecimal() ?: BigDecimal.ZERO, fiatCurrencyName = appCurrency.symbol, formatWithSpaces = true, ), @@ -899,7 +898,6 @@ internal class SwapInteractorImpl @Inject constructor( companion object { private const val DEFAULT_SLIPPAGE = 2 private const val ZERO_BALANCE = "0" - private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" @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 d09b227b41..4ba2f42f83 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 @@ -14,6 +14,7 @@ interface SwapRepository { suspend fun getExchangeableTokens(networkId: String): List + @Suppress("LongParameterList") suspend fun findBestQuote( fromContractAddress: String, fromNetwork: String, @@ -74,5 +75,5 @@ interface SwapRepository { fromAmount: String, providerId: Int, rateType: RateType, - ) : ExchangeQuote + ): ExchangeQuote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt index d087b97955..41d7826e07 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt @@ -1,8 +1,6 @@ package com.tangem.feature.swap.domain.models.domain -import java.math.BigDecimal - data class ExchangeQuote( - val toAmount: BigDecimal, - val allowanceContract: String? + val toAmount: String, + val allowanceContract: String?, ) \ No newline at end of file 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 14610d708f..b1e20ec216 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 @@ -9,7 +9,7 @@ import java.math.BigDecimal sealed interface SwapState { data class QuotesLoadedState( - //add map < Provider ID, Swap state data> + // add map < Provider ID, Swap state data> val fromTokenInfo: TokenSwapInfo, val toTokenInfo: TokenSwapInfo, val priceImpact: Float, 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 245b670cbe..fe3ff3c949 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 @@ -145,7 +145,7 @@ internal class SwapViewModel @Inject constructor( tokensDataState = state, ) - //updateTokensState(state) + // updateTokensState(state) val toToken = state.toGroup.available.first() startLoadingQuotes( @@ -204,7 +204,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromToken, toToken = toToken, amount = amount, - toProvidersList = toProvidersList + toProvidersList = toProvidersList, ), ) } From bbf8147aa4605cc7bb3a038532ef5f8eb302f83e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 23:07:53 +0200 Subject: [PATCH 09/11] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 108 +++++++++++------- .../swap/domain/models/ui/SwapState.kt | 1 - 2 files changed, 68 insertions(+), 41 deletions(-) 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 88f790c9c3..27d8c090a1 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 @@ -12,6 +12,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.cache.SwapDataCache import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.toStringWithRightOffset @@ -26,6 +27,9 @@ 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( @@ -306,7 +310,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, providers = providers, - ) + ).entries.first().value // TODO } } @@ -514,46 +518,70 @@ internal class SwapInteractorImpl @Inject constructor( providers: List, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, - ): SwapState { - repository.findBestQuote( - fromContractAddress = fromToken.getContractAddress(), - fromNetwork = fromToken.network.backendId, - toContractAddress = toToken.getContractAddress(), - toNetwork = toToken.network.backendId, - fromAmount = amount.toStringWithRightOffset(), - providerId = providers[0].providerId, - rateType = RateType.FLOAT, - - // networkId = networkId, - // fromTokenAddress = fromTokenAddress, - // toTokenAddress = toTokenAddress, - // amount = amount.toStringWithRightOffset(), - ).let { quotes -> - val quoteDataModel = quotes.dataModel - if (quoteDataModel != null) { - val swapState = updateBalances( - networkId = networkId, - fromToken = fromToken, - toToken = toToken, - fromTokenAmount = amount, - toTokenAmount = quoteDataModel.toTokenAmount, - swapStateData = null, - ) - val quotesState = updatePermissionState( - networkId = networkId, - fromToken = fromToken, - swapAmount = amount, - quotesLoadedState = swapState, - ) - return quotesState.copy( - preparedSwapConfigState = quotesState.preparedSwapConfigState.copy( - isAllowedToSpend = isAllowedToSpend, - isBalanceEnough = isBalanceWithoutFeeEnough, - ), - ) - } else { - return SwapState.SwapError(quotes.error) + ): Map { + return coroutineScope { + val quoteRequests = providers.map { provider -> + async { + provider to repository.findBestQuote( + fromContractAddress = fromToken.getContractAddress(), + fromNetwork = fromToken.network.backendId, + toContractAddress = toToken.getContractAddress(), + toNetwork = toToken.network.backendId, + fromAmount = amount.toStringWithRightOffset(), + providerId = provider.providerId, + rateType = RateType.FLOAT, + ) + } } + + quoteRequests.awaitAll().map { + it.first to + getState( + quoteDataModel = it.second, + amount = amount, + fromToken = fromToken, + toToken = toToken, + networkId = networkId, + isAllowedToSpend = isAllowedToSpend, + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough + ) + }.associate { it.first to it.second } + } + } + + private suspend fun getState( + quoteDataModel: AggregatedSwapDataModel, + amount: SwapAmount, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, + networkId: String, + isAllowedToSpend: Boolean, + isBalanceWithoutFeeEnough: Boolean, + ): SwapState { + val quoteModel = quoteDataModel.dataModel + if (quoteModel != null) { + val swapState = updateBalances( + networkId = networkId, + fromToken = fromToken, + toToken = toToken, + fromTokenAmount = amount, + toTokenAmount = quoteModel.toTokenAmount, + swapStateData = null, + ) + val quotesState = updatePermissionState( + networkId = networkId, + fromToken = fromToken, + swapAmount = amount, + quotesLoadedState = swapState, + ) + return quotesState.copy( + preparedSwapConfigState = quotesState.preparedSwapConfigState.copy( + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + ), + ) + } else { + return SwapState.SwapError(quoteDataModel.error) } } 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 b1e20ec216..28809bc066 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 @@ -9,7 +9,6 @@ import java.math.BigDecimal sealed interface SwapState { data class QuotesLoadedState( - // add map < Provider ID, Swap state data> val fromTokenInfo: TokenSwapInfo, val toTokenInfo: TokenSwapInfo, val priceImpact: Float, From b1b7b1bc15e20d36e6264e476aba36a2c1c02878 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Nov 2023 01:48:04 +0200 Subject: [PATCH 10/11] Updated on 2026-08-14 --- .../feature/swap/viewmodels/SwapViewModel.kt | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) 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 9c047326e0..987762b439 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 @@ -177,6 +177,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromCurrencyStatus.currency, toToken = selectedCurrency.currency, amount = lastAmount.value, + toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency) ) } } @@ -198,7 +199,7 @@ internal class SwapViewModel @Inject constructor( fromToken: CryptoCurrency, toToken: CryptoCurrency, amount: String, - toProvidersList: List = listOf(SwapProvider(1, listOf(RateType.FLOAT))), + toProvidersList: List, ) { singleTaskScheduler.cancelTask() uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, initialCryptoCurrency.id.value) @@ -218,7 +219,12 @@ internal class SwapViewModel @Inject constructor( val toCurrency = dataState.toCryptoCurrency val amount = dataState.amount if (fromCurrency != null && toCurrency != null && amount != null) { - startLoadingQuotes(fromCurrency.currency, toCurrency.currency, amount) + startLoadingQuotes( + fromToken = fromCurrency.currency, + toToken = toCurrency.currency, + amount = amount, + toProvidersList = findSwapProviders(fromCurrency, toCurrency) + ) } } @@ -428,7 +434,12 @@ internal class SwapViewModel @Inject constructor( fromCryptoCurrency = fromToken, toCryptoCurrency = toToken, ) - startLoadingQuotes(fromToken.currency, toToken.currency, lastAmount.value) + startLoadingQuotes( + fromToken = fromToken.currency, + toToken = toToken.currency, + amount = lastAmount.value, + toProvidersList = findSwapProviders(fromToken, toToken) + ) swapRouter.openScreen(SwapNavScreen.Main) } } @@ -448,7 +459,12 @@ internal class SwapViewModel @Inject constructor( uiState, inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), ) - startLoadingQuotes(newFromToken.currency, newToToken.currency, lastAmount.value) + startLoadingQuotes( + fromToken = newFromToken.currency, + toToken = newToToken.currency, + amount = lastAmount.value, + toProvidersList = findSwapProviders(newFromToken, newToToken) + ) } } @@ -462,7 +478,12 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals)) amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { - startLoadingQuotes(fromToken.currency, toToken.currency, lastAmount.value) + startLoadingQuotes( + fromToken = fromToken.currency, + toToken = toToken.currency, + amount = lastAmount.value, + toProvidersList = findSwapProviders(fromToken, toToken) + ) } } } @@ -576,6 +597,22 @@ internal class SwapViewModel @Inject constructor( ) } + private fun findSwapProviders(fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus): List { + val groupToFind = if (isOrderReversed) { + dataState.tokensDataState?.fromGroup + } else { + dataState.tokensDataState?.toGroup + } ?: return emptyList() + + val idToFind = if (isOrderReversed) { + fromToken.currency.id.value + } else { + toToken.currency.id.value + } + + return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers ?: emptyList() + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" From 3f6a17e340efabc804d22a375e63f1ed379d12cc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Nov 2023 11:31:08 +0200 Subject: [PATCH 11/11] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractor.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 27 ++++++++++--------- .../feature/swap/viewmodels/SwapViewModel.kt | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) 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 32e9ebdf94..c6ab0a7308 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 @@ -70,7 +70,7 @@ interface SwapInteractor { providers: List, amountToSwap: String, selectedFee: FeeType = FeeType.NORMAL, - ): SwapState + ): Map /** * Starts swap transaction, perform sign transaction 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 c851cc930a..dad225b375 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 @@ -281,11 +281,11 @@ internal class SwapInteractorImpl @Inject constructor( providers: List, amountToSwap: String, selectedFee: FeeType, - ): SwapState { + ): Map { syncWalletBalanceForTokens(networkId, listOf(fromToken, toToken)) val amountDecimal = toBigDecimalOrNull(amountToSwap) if (amountDecimal == null || amountDecimal.signum() == 0) { - return createEmptyAmountState(networkId, fromToken, toToken) + return providers.associateWith { createEmptyAmountState(networkId, fromToken, toToken) } } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken)) val fromTokenAddress = getTokenAddress(fromToken) @@ -297,15 +297,18 @@ internal class SwapInteractorImpl @Inject constructor( } val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken, amount, null) return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { - loadSwapData( - networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - fromToken = fromToken, - toToken = toToken, - amount = amount, - selectedFee = selectedFee, - ) + // TODO + providers.associateWith { + loadSwapData( + networkId = networkId, + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + fromToken = fromToken, + toToken = toToken, + amount = amount, + selectedFee = selectedFee, + ) + } } else { loadQuoteData( networkId = networkId, @@ -315,7 +318,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, providers = providers, - ).entries.first().value // TODO + ) } } 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 987762b439..03199e51f5 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 @@ -251,7 +251,7 @@ internal class SwapViewModel @Inject constructor( providers = toProvidersList, amountToSwap = amount, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - ) + ).entries.first().value// TODO } }, onSuccess = { swapState ->