Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-31 15:10:05 +04:00
parent c1d4bf79cb
commit 799c132a5b
13 changed files with 252 additions and 80 deletions

View file

@ -108,6 +108,7 @@ dependencies {
implementation(projects.domain.onramp)
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
implementation(projects.domain.networks)
implementation(projects.common)
implementation(projects.common.routing)
@ -150,6 +151,7 @@ dependencies {
implementation(projects.data.manageTokens)
implementation(projects.data.nft)
implementation(projects.data.onramp)
implementation(projects.data.networks)
/** Features */
implementation(projects.features.onboarding)

View file

@ -2,6 +2,8 @@ package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.*
@ -47,7 +49,12 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): FetchTokenListUseCase {
return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository)
return FetchTokenListUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
)
}
@Provides
@ -151,7 +158,12 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository)
return FetchCurrencyStatusUseCase(
currenciesRepository,
networksRepository,
quotesRepository,
stakingRepository,
)
}
@Provides
@ -349,12 +361,16 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
): BaseCurrenciesStatusesOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -367,12 +383,16 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
): BaseCurrencyStatusOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -45,7 +45,7 @@
},
{
"name": "NEW_ATTESTATION_ENABLED",
"version": "undefined"
"version": "5.23.0"
},
{
"name": "TWIN_REFACTORING_ENABLED",
@ -61,6 +61,6 @@
},
{
"name": "NETWORKS_LOADING_REFACTORING_ENABLED",
"version": "undefined"
"version": "5.23.0"
}
]

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
@ -25,22 +26,29 @@ import kotlinx.coroutines.SupervisorJob
internal object NetworksStatusesStoreModule {
@Provides
fun provideNetworksStatusesStore(
fun providePersistenceNetworksStatusesStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): DataStore<Map<String, Set<NetworkStatusDM>>> {
return DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes(valueTypes = setTypes<NetworkStatusDM>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "networks_statuses") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}
@Provides
fun provideNetworksStatusesStore(
persistenceNetworksStatusesStore: DataStore<Map<String, Set<NetworkStatusDM>>>,
): NetworksStatusesStore {
return DefaultNetworksStatusesStore(
runtimeDataStore = RuntimeDataStore(),
persistenceDataStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes(valueTypes = setTypes<NetworkStatusDM>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "networks_statuses") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),
persistenceDataStore = persistenceNetworksStatusesStore,
)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.networks.di
import com.tangem.data.networks.multi.DefaultMultiNetworkStatusFetcher
import com.tangem.data.networks.single.DefaultSingleNetworkStatusFetcher
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
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 NetworkStatusFetcherModule {
@Binds
@Singleton
fun bindSingleNetworkStatusFetcher(impl: DefaultSingleNetworkStatusFetcher): SingleNetworkStatusFetcher
@Binds
@Singleton
fun bindMultiNetworkStatusFetcher(impl: DefaultMultiNetworkStatusFetcher): MultiNetworkStatusFetcher
}

View file

@ -0,0 +1,28 @@
package com.tangem.data.networks.di
import com.tangem.data.networks.multi.DefaultMultiNetworkStatusProducer
import com.tangem.data.networks.single.DefaultSingleNetworkStatusProducer
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
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 NetworkStatusProducerFactoryModule {
@Binds
@Singleton
fun bindSingleNetworkStatusProducerFactory(
impl: DefaultSingleNetworkStatusProducer.Factory,
): SingleNetworkStatusProducer.Factory
@Binds
@Singleton
fun bindMultiNetworkStatusProducerFactory(
impl: DefaultMultiNetworkStatusProducer.Factory,
): MultiNetworkStatusProducer.Factory
}

View file

@ -0,0 +1,56 @@
package com.tangem.data.networks.di
import androidx.datastore.core.DataStore
import com.tangem.data.networks.store.DefaultNetworksStatusesStoreV2
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
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 NetworkStatusSupplierModule {
@Provides
@Singleton
fun provideNetworksStatusesStoreV2(
persistenceNetworksStatusesStore: DataStore<Map<String, Set<NetworkStatusDM>>>,
dispatchers: CoroutineDispatcherProvider,
): NetworksStatusesStoreV2 {
return DefaultNetworksStatusesStoreV2(
runtimeStore = RuntimeSharedStore(),
persistenceDataStore = persistenceNetworksStatusesStore,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideSingleNetworkStatusSupplier(factory: SingleNetworkStatusProducer.Factory): SingleNetworkStatusSupplier {
return object : SingleNetworkStatusSupplier(
factory = factory,
keyCreator = {
"single_network_status_${it.userWalletId.stringValue}_${it.network.id.value}_" +
it.network.derivationPath.value
},
) {}
}
@Provides
@Singleton
fun provideMultiNetworkStatusSupplier(factory: MultiNetworkStatusProducer.Factory): MultiNetworkStatusSupplier {
return object : MultiNetworkStatusSupplier(
factory = factory,
keyCreator = { "multi_networks_statuses_${it.userWalletId.stringValue}" },
) {}
}
}

View file

@ -26,7 +26,9 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import timber.log.Timber
@Suppress("LongParameterList")
@ -60,19 +62,6 @@ internal class DefaultNetworksRepository(
}
}
override fun getNetworkStatusesUpdatesLegacy(
userWalletId: UserWalletId,
networks: Set<Network>,
): Flow<Set<NetworkStatus>> = channelFlow {
networksStatusesStore.get(userWalletId)
.onEach(::send)
.launchIn(scope = this + dispatchers.io)
withContext(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false)
}
}
override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
val currencies = getCurrencies(userWalletId, networks)
withContext(dispatchers.io) {

View file

@ -27,6 +27,7 @@ dependencies {
implementation(projects.features.swap.domain.models)
implementation(projects.domain.promo.models)
implementation(projects.domain.promo)
implementation(projects.domain.networks)
/** Project - Api */
implementation(projects.features.send.api)
@ -50,6 +51,7 @@ dependencies {
/** Tests */
testImplementation(deps.test.junit)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.mockk)
testImplementation(tangemDeps.blockchain) {
exclude(module = "joda-time")
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens.operations
import arrow.core.*
import arrow.core.raise.either
import arrow.core.raise.recover
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
@ -9,6 +10,9 @@ import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.domain.staking.repositories.StakingRepository
@ -21,15 +25,19 @@ import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.utils.extractAddress
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@Suppress("LongParameterList")
class CachedCurrenciesStatusesOperations(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
@Suppress("UnusedPrivateMember") private val tokensFeatureToggles: TokensFeatureToggles,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) : BaseCurrenciesStatusesOperations,
BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) {
@ -145,20 +153,26 @@ class CachedCurrenciesStatusesOperations(
networks: Set<Network>,
currenciesIds: Set<CryptoCurrency.ID>,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> {
return coroutineScope {
Either.catch {
awaitAll(
async { networksRepository.fetchNetworkStatuses(userWalletId, networks) },
async {
val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId }
quotesRepository.fetchQuotes(rawCurrenciesIds)
},
async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) },
)
}
.map { }
): Either<Throwable, Unit> = either {
coroutineScope {
awaitAll(
async {
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(userWalletId, networks),
)
} else {
networksRepository.fetchNetworkStatuses(userWalletId, networks)
}
},
async {
val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId }
quotesRepository.fetchQuotes(rawCurrenciesIds)
},
async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) },
)
}
.map { }
}
private fun createCurrenciesStatuses(
@ -251,31 +265,44 @@ class CachedCurrenciesStatusesOperations(
userWalletId: UserWalletId,
network: Network,
): EitherFlow<Error, Set<NetworkStatus>> {
return networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network))
.map<Set<NetworkStatus>, Either<Error, Set<NetworkStatus>>> { it.right() }
.retryWhen { cause, _ ->
emit(Error.DataError(cause).left())
// adding delay before retry to avoid spam when flow restarted
delay(RETRY_DELAY)
true
}
.distinctUntilChanged()
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
return if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = network),
)
.map<NetworkStatus, Either<Error, Set<NetworkStatus>>> { setOf(it).right() }
.distinctUntilChanged()
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
} else {
networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network))
.map<Set<NetworkStatus>, Either<Error, Set<NetworkStatus>>> { it.right() }
.retryWhen { cause, _ ->
emit(Error.DataError(cause).left())
// adding delay before retry to avoid spam when flow restarted
delay(RETRY_DELAY)
true
}
.distinctUntilChanged()
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
}
}
private fun getNetworksStatuses(
userWalletId: UserWalletId,
networks: NonEmptySet<Network>,
): EitherFlow<TokenListError, Set<NetworkStatus>> {
return networksRepository.getNetworkStatusesUpdates(userWalletId, networks)
.map<Set<NetworkStatus>, Either<TokenListError, Set<NetworkStatus>>> { 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.isNetworksLoadingRefactoringEnabled) {
getNetworkStatusesUpdates(userWalletId, networks)
} else {
networksRepository.getNetworkStatusesUpdates(userWalletId, networks)
.map<Set<NetworkStatus>, Either<TokenListError, Set<NetworkStatus>>> { it.right() }
.retryWhen { cause, _ ->
emit(TokenListError.DataError(cause).left())
// adding delay before retry to avoid spam when flow restarted
delay(RETRY_DELAY)
true
}
.distinctUntilChanged()
}
}
private fun getYieldBalances(
@ -293,6 +320,36 @@ class CachedCurrenciesStatusesOperations(
.distinctUntilChanged()
}
// temporary code because token list is built using networks list
private fun getNetworkStatusesUpdates(
userWalletId: UserWalletId,
networks: NonEmptySet<Network>,
): EitherFlow<TokenListError, Set<NetworkStatus>> {
return channelFlow {
val state = MutableStateFlow(emptySet<NetworkStatus>())
networks.onEach {
launch {
singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = it),
)
.onEach { status ->
state.update { loadedStatuses ->
loadedStatuses.addOrReplace(status) { it.network == status.network }
}
}
.launchIn(scope = this)
}
}
state
.onEach(::send)
.launchIn(scope = this)
}
.map<Set<NetworkStatus>, Either<TokenListError, Set<NetworkStatus>>> { it.right() }
.distinctUntilChanged()
}
companion object {
internal const val RETRY_DELAY = 2000L
}

View file

@ -35,17 +35,6 @@ interface NetworksRepository {
* */
suspend fun fetchNetworkStatuses(userWalletId: UserWalletId, networks: Set<Network>, refresh: Boolean = false)
/**
* Retrieves updates of network statuses of specified blockchain networks for a specific user wallet.
*
* Loads remote network statuses if they have expired.
*
* @param userWalletId The unique identifier of the user wallet.
* @param networks A set of network which statuses are to be retrieved.
* @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks.
*/
fun getNetworkStatusesUpdatesLegacy(userWalletId: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>>
/**
* Fetches pending transactions for given network
*

View file

@ -17,6 +17,7 @@ import com.tangem.domain.tokens.repository.MockQuotesRepository
import com.tangem.domain.tokens.repository.MockStakingRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@ -176,9 +177,12 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
quotesRepository = MockQuotesRepository(quotes),
networksRepository = MockNetworksRepository(statuses),
stakingRepository = MockStakingRepository(),
tokensFeatureToggles = object : TokensFeatureToggles {
override val isNetworksLoadingRefactoringEnabled: Boolean = false
},
singleNetworkStatusSupplier = mockk(),
multiNetworkStatusFetcher = mockk(),
),
dispatchers = dispatchers,
)

View file

@ -26,13 +26,6 @@ internal class MockNetworksRepository(
/* no-op */
}
override fun getNetworkStatusesUpdatesLegacy(
userWalletId: UserWalletId,
networks: Set<Network>,
): Flow<Set<NetworkStatus>> {
return statuses.map { it.getOrElse { e -> throw e } }
}
override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
// no-op
}
@ -42,7 +35,7 @@ internal class MockNetworksRepository(
networks: Set<Network>,
refresh: Boolean,
): Set<NetworkStatus> {
return getNetworkStatusesUpdatesLegacy(userWalletId, networks).first()
return statuses.map { it.getOrElse { e -> throw e } }.first()
}
override fun isNeedToCreateAccountWithoutReserve(network: Network) = false