From 275b53dae212262037b9b5ff0ac1e3a9a5ae015c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 21:24:16 +0400 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tap/di/domain/MarketsDomainModule.kt | 13 ++- .../tap/di/domain/TokensDomainModule.kt | 10 ++ .../data/quotes/di/QuoteFetcherModule.kt | 24 +++++ .../quotes/di/QuoteProducerFactoryModule.kt | 18 ++++ .../data/quotes/di/QuoteSupplierModule.kt | 42 ++++++++ domain/markets/build.gradle.kts | 1 + .../markets/GetCurrencyQuotesUseCase.kt | 23 ++++- domain/tokens/build.gradle.kts | 1 + .../CachedCurrenciesStatusesOperations.kt | 96 +++++++++++++++---- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 2 + features/wallet/impl/build.gradle.kts | 1 + 12 files changed, 206 insertions(+), 27 deletions(-) create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b36d773b95..c2091e3c79 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -109,6 +109,7 @@ dependencies { implementation(projects.domain.promo) implementation(projects.domain.promo.models) implementation(projects.domain.networks) + implementation(projects.domain.quotes) implementation(projects.common) implementation(projects.common.routing) @@ -152,6 +153,7 @@ dependencies { implementation(projects.data.nft) implementation(projects.data.onramp) implementation(projects.data.networks) + implementation(projects.data.quotes) /** Features */ implementation(projects.features.onboarding) diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 3a568db5e6..0e18657362 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -5,6 +5,7 @@ import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -49,8 +50,16 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase { - return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository) + fun provideGetTokenQuotesUseCase( + quotesRepository: QuotesRepository, + singleQuoteSupplier: SingleQuoteSupplier, + tokensFeatureToggles: TokensFeatureToggles, + ): GetCurrencyQuotesUseCase { + return GetCurrencyQuotesUseCase( + quotesRepository = quotesRepository, + singleQuoteSupplier = singleQuoteSupplier, + tokensFeatureToggles = tokensFeatureToggles, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 99bb5efeef..eedc9d7f2b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -7,6 +7,8 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.promo.PromoRepository +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.* import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations @@ -391,6 +393,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteFetcher: MultiQuoteFetcher, + singleQuoteSupplier: SingleQuoteSupplier, ): BaseCurrenciesStatusesOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -400,6 +404,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteFetcher = multiQuoteFetcher, + singleQuoteSupplier = singleQuoteSupplier, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -415,6 +421,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteFetcher: MultiQuoteFetcher, + singleQuoteSupplier: SingleQuoteSupplier, ): BaseCurrencyStatusOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -424,6 +432,8 @@ internal object TokensDomainModule { singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteFetcher = multiQuoteFetcher, + singleQuoteSupplier = singleQuoteSupplier, tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt new file mode 100644 index 0000000000..5a5eb2ecfd --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt @@ -0,0 +1,24 @@ +package com.tangem.data.quotes.di + +import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher +import com.tangem.data.quotes.multi.DefaultMultiQuoteUpdater +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteUpdater +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface QuoteFetcherModule { + + @Binds + @Singleton + fun bindMultiQuoteFetcher(impl: DefaultMultiQuoteFetcher): MultiQuoteFetcher + + @Binds + @Singleton + fun bindMultiQuoteUpdater(impl: DefaultMultiQuoteUpdater): MultiQuoteUpdater +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt new file mode 100644 index 0000000000..0fc3d0faed --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt @@ -0,0 +1,18 @@ +package com.tangem.data.quotes.di + +import com.tangem.data.quotes.single.DefaultSingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteProducer +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface QuoteProducerFactoryModule { + + @Binds + @Singleton + fun bindSingleQuoteProducerFactory(impl: DefaultSingleQuoteProducer.Factory): SingleQuoteProducer.Factory +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt new file mode 100644 index 0000000000..af8188e5bc --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt @@ -0,0 +1,42 @@ +package com.tangem.data.quotes.di + +import androidx.datastore.core.DataStore +import com.tangem.data.quotes.store.CurrencyIdWithQuote +import com.tangem.data.quotes.store.DefaultQuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object QuoteSupplierModule { + + @Provides + @Singleton + fun provideQuotesStoreV2( + persistenceQuotesStore: DataStore, + dispatchers: CoroutineDispatcherProvider, + ): QuotesStoreV2 { + return DefaultQuotesStoreV2( + runtimeStore = RuntimeSharedStore(), + persistenceDataStore = persistenceQuotesStore, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideSingleQuoteSupplier(factory: SingleQuoteProducer.Factory): SingleQuoteSupplier { + return object : SingleQuoteSupplier( + factory = factory, + keyCreator = { "single_quote_${it.rawCurrencyId.value}" }, + ) {} + } +} \ No newline at end of file diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 6ba3a16199..7d820cb19a 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { api(projects.domain.models) api(projects.domain.networks) api(projects.domain.staking) + api(projects.domain.quotes) api(projects.domain.wallets) api(projects.domain.wallets.models) diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt index dcd308db12..080a102047 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -3,6 +3,9 @@ package com.tangem.domain.markets import arrow.core.None import arrow.core.Option import arrow.core.toOption +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.QuotesRepository @@ -11,9 +14,10 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -@Suppress("UnusedPrivateMember") class GetCurrencyQuotesUseCase( private val quotesRepository: QuotesRepository, + private val singleQuoteSupplier: SingleQuoteSupplier, + private val tokensFeatureToggles: TokensFeatureToggles, ) { // TODO apply interval parameter [REDACTED_TASK_KEY] operator fun invoke( @@ -23,9 +27,18 @@ class GetCurrencyQuotesUseCase( ): Flow> { val rawId = currencyID.rawCurrencyId ?: return flowOf(None) - return quotesRepository.getQuotesUpdatesLegacy( - currenciesIds = setOf(rawId), - refresh = refresh, - ).map { it.filterIsInstance().firstOrNull().toOption() }.catch { emit(None) } + return if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + singleQuoteSupplier( + params = SingleQuoteProducer.Params(rawCurrencyId = rawId), + ) + .map { (it as? Quote.Value).toOption() } + } else { + quotesRepository.getQuotesUpdatesLegacy( + currenciesIds = setOf(rawId), + refresh = refresh, + ) + .map { it.filterIsInstance().firstOrNull().toOption() } + } + .catch { emit(None) } } } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 6ba910ceae..f24fb5baf6 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.domain.promo.models) implementation(projects.domain.promo) implementation(projects.domain.networks) + implementation(projects.domain.quotes) /** Project - Api */ implementation(projects.features.send.api) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 3334482cf9..d8d8c6b538 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -14,6 +14,9 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository @@ -30,7 +33,7 @@ import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, @@ -39,6 +42,8 @@ class CachedCurrenciesStatusesOperations( private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val multiQuoteFetcher: MultiQuoteFetcher, + private val singleQuoteSupplier: SingleQuoteSupplier, private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrenciesStatusesOperations, BaseCurrencyStatusOperations( @@ -176,7 +181,14 @@ class CachedCurrenciesStatusesOperations( }, async { val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } - quotesRepository.fetchQuotes(rawCurrenciesIds) + + if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + multiQuoteFetcher( + params = MultiQuoteFetcher.Params(currenciesIds = rawCurrenciesIds, appCurrencyId = null), + ) + } else { + quotesRepository.fetchQuotes(rawCurrenciesIds) + } }, async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) }, ) @@ -247,27 +259,44 @@ class CachedCurrenciesStatusesOperations( } private fun getQuotes(tokensIds: NonEmptySet): Flow>> { - return quotesRepository.getQuotesUpdates(tokensIds.mapNotNull { it.rawCurrencyId }.toSet()) - .map, Either>> { it.right() } - .retryWhen { cause, _ -> - emit(TokenListError.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_DELAY) - true - } - .distinctUntilChanged() + return if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + getQuotesUpdates( + rawCurrencyIds = tokensIds.mapNotNullTo( + destination = hashSetOf(), + transform = CryptoCurrency.ID::rawCurrencyId, + ), + ) + } else { + quotesRepository.getQuotesUpdates(tokensIds.mapNotNull { it.rawCurrencyId }.toSet()) + .map, Either>> { it.right() } + .retryWhen { cause, _ -> + emit(TokenListError.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } } override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { - return quotesRepository.getQuotesUpdates(setOf(id)) - .map, Either>> { it.right() } - .retryWhen { cause, _ -> - emit(Error.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_DELAY) - true - } - .distinctUntilChanged() + return if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { + singleQuoteSupplier( + params = SingleQuoteProducer.Params(rawCurrencyId = id), + ) + .map>> { setOf(it).right() } + .distinctUntilChanged() + } else { + quotesRepository.getQuotesUpdates(setOf(id)) + .map, Either>> { it.right() } + .retryWhen { cause, _ -> + emit(Error.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } } override fun getNetworksStatuses( @@ -361,6 +390,33 @@ class CachedCurrenciesStatusesOperations( .distinctUntilChanged() } + // temporary code because token list is built using networks list + private fun getQuotesUpdates(rawCurrencyIds: Set): EitherFlow> { + return channelFlow { + val state = MutableStateFlow(emptySet()) + + rawCurrencyIds.onEach { + launch { + singleQuoteSupplier( + params = SingleQuoteProducer.Params(rawCurrencyId = it), + ) + .onEach { quote -> + state.update { loadedStatuses -> + loadedStatuses.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId } + } + } + .launchIn(scope = this) + } + } + + state + .onEach(::send) + .launchIn(scope = this) + } + .map, Either>> { it.right() } + .distinctUntilChanged() + } + private fun isFetchingStarted(userWalletId: UserWalletId): Boolean { return fetchingState.value[userWalletId] ?: false } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index b515f71dd9..8f08bc34ea 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -186,6 +186,8 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { singleNetworkStatusSupplier = mockk(), multiNetworkStatusFetcher = mockk(), multiNetworkStatusSupplier = mockk(), + multiQuoteFetcher = mockk(), + singleQuoteSupplier = mockk(), ), dispatchers = dispatchers, ) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index da4e888dfe..b2a3af1b04 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { implementation(projects.domain.onramp.models) implementation(projects.domain.promo) implementation(projects.domain.promo.models) + implementation(projects.domain.quotes) implementation(projects.domain.settings) implementation(projects.domain.staking) implementation(projects.domain.staking.models)