Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-10 21:24:16 +04:00
parent 47a30176a6
commit 275b53dae2
12 changed files with 206 additions and 27 deletions

View file

@ -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)

View file

@ -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

View file

@ -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,
)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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<CurrencyIdWithQuote>,
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}" },
) {}
}
}

View file

@ -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)

View file

@ -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<Option<Quote.Value>> {
val rawId = currencyID.rawCurrencyId ?: return flowOf(None)
return quotesRepository.getQuotesUpdatesLegacy(
currenciesIds = setOf(rawId),
refresh = refresh,
).map { it.filterIsInstance<Quote.Value>().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<Quote.Value>().firstOrNull().toOption() }
}
.catch { emit(None) }
}
}

View file

@ -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)

View file

@ -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<CryptoCurrency.ID>): Flow<Either<TokenListError, Set<Quote>>> {
return quotesRepository.getQuotesUpdates(tokensIds.mapNotNull { it.rawCurrencyId }.toSet())
.map<Set<Quote>, Either<TokenListError, Set<Quote>>> { 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<Set<Quote>, Either<TokenListError, Set<Quote>>> { 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<Either<Error, Set<Quote>>> {
return quotesRepository.getQuotesUpdates(setOf(id))
.map<Set<Quote>, Either<Error, Set<Quote>>> { 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<Quote, Either<Error, Set<Quote>>> { setOf(it).right() }
.distinctUntilChanged()
} else {
quotesRepository.getQuotesUpdates(setOf(id))
.map<Set<Quote>, Either<Error, Set<Quote>>> { 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<CryptoCurrency.RawID>): EitherFlow<TokenListError, Set<Quote>> {
return channelFlow {
val state = MutableStateFlow(emptySet<Quote>())
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<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() }
.distinctUntilChanged()
}
private fun isFetchingStarted(userWalletId: UserWalletId): Boolean {
return fetchingState.value[userWalletId] ?: false
}

View file

@ -186,6 +186,8 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
singleNetworkStatusSupplier = mockk(),
multiNetworkStatusFetcher = mockk(),
multiNetworkStatusSupplier = mockk(),
multiQuoteFetcher = mockk(),
singleQuoteSupplier = mockk(),
),
dispatchers = dispatchers,
)

View file

@ -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)