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

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