Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-18 18:18:29 +04:00
parent 097e9c1bce
commit 2437fa39d8
9 changed files with 120 additions and 22 deletions

View file

@ -11,9 +11,11 @@ import com.tangem.domain.common.wallets.getSyncStrict
import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -21,6 +23,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
/**
* Default implementation of [SingleAccountListProducer].
@ -43,23 +46,45 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
override val fallback: Option<AccountList> = none()
private val logger = TangemLogger.withTag(TAG)
@OptIn(ExperimentalCoroutinesApi::class)
override fun produce(): Flow<AccountList> {
val walletId = params.userWalletId
logger.i("produce() called for $walletId")
val accountListFlow: Flow<AccountList> = if (tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled) {
combineWithPaymentAccount()
combineWithPaymentAccount(walletId)
} else {
walletAccountListFlowFactory.create(userWalletId = params.userWalletId)
walletAccountListFlowFactory.create(userWalletId = walletId)
}
return accountListFlow.flowOn(dispatchers.default)
return accountListFlow
.onEach { accountList ->
logger.i(
"produce()[$walletId] emit: accounts=${accountList.accounts.size}, " +
"currencies=${accountList.flattenMapCurrencies().size}",
)
}
.flowOn(dispatchers.default)
}
private fun combineWithPaymentAccount(): Flow<AccountList> {
return walletAccountListFlowFactory.create(params.userWalletId)
private fun combineWithPaymentAccount(walletId: UserWalletId): Flow<AccountList> {
return walletAccountListFlowFactory.create(walletId)
.onEach { accountList ->
logger.i(
"produce()[$walletId]: accountList received accounts=${accountList.accounts.size}, " +
"currencies=${accountList.flattenMapCurrencies().size}",
)
}
.map { accountList ->
val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId)
if (userWallet.isPaymentAccountSupported()) {
accountList.plus(Account.Payment(params.userWalletId)).getOrElse { throwable ->
val userWallet = userWalletsListRepository.getSyncStrict(id = walletId)
val isPaymentSupported = userWallet.isPaymentAccountSupported()
logger.i(
"produce()[$walletId]: userWallet resolved (type=${userWallet::class.simpleName}), " +
"isPaymentAccountSupported=$isPaymentSupported",
)
if (isPaymentSupported) {
accountList.plus(Account.Payment(walletId)).getOrElse { throwable ->
error("Can not combine account list and payment account status: $throwable")
}
} else {
@ -77,4 +102,8 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
interface Factory : SingleAccountListProducer.Factory {
override fun create(params: SingleAccountListProducer.Params): DefaultSingleAccountListProducer
}
private companion object {
const val TAG = "SingleAccountListProducer"
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ -47,7 +48,15 @@ internal class WalletAccountListFlowFactory @Inject constructor(
return accountsResponseStoreFactory.create(userWallet.walletId).data
.filterNotNull()
.filter { it.accounts.isNotEmpty() }
.filter { response ->
val hasAccounts = response.accounts.isNotEmpty()
if (!hasAccounts) {
TangemLogger.e("Account list is empty for ${userWallet.walletId}")
}
hasAccounts
}
.distinctUntilChanged()
.map { converter.convert(it) }
}

View file

@ -135,10 +135,9 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>> {
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
?: return emptyMap()
val response = walletAccountsFetcher.getSaved(userWallet.walletId) ?: return emptyMap()
val existingNetworkWithCurrencies = response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
val currenciesByNetworkId = response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
?: return@flatMapTo emptySet()
@ -152,9 +151,11 @@ internal class DefaultCardCryptoCurrencyFactory(
accountIndex = accountIndex,
)
}
.groupBy(CryptoCurrency::network)
.groupBy { it.network.id }
return networks.associateWith { emptyList<CryptoCurrency>() } + existingNetworkWithCurrencies
return networks.associateWith { network ->
currenciesByNetworkId[network.id].orEmpty()
}
}
private suspend fun getMultiWalletCurrenciesByRawId(

View file

@ -1,6 +1,7 @@
package com.tangem.data.networks.fetcher
import arrow.core.Either
import arrow.core.right
import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.data.networks.store.setSourceAsOnlyCache
import com.tangem.data.networks.store.storeStatus
@ -44,6 +45,13 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
network: Network,
networkCurrencies: Set<CryptoCurrency>,
): Either<Throwable, Unit> {
// Guard: empty networkCurrencies would result in NetworkStatus.Verified(amounts=emptyMap()),
// which overwrites any valid cached status and leaves all currencies in this network as Loading.
if (networkCurrencies.isEmpty()) {
TangemLogger.w("Skipping fetch for $userWalletId [${network.rawId}]: networkCurrencies is empty")
return Unit.right()
}
return Either.catchOn(dispatchers.default) {
val result = withContext(dispatchers.io) {
walletManagersFacade.update(

View file

@ -73,6 +73,32 @@ internal class CommonNetworkStatusFetcherTest {
}
}
@Test
fun `fetch skips wallet manager update when networkCurrencies is empty`() = runTest {
// Arrange
val userWalletId = UserWalletId("011")
val network = cryptoCurrencyFactory.ethereum.network
// Act
val actual = fetcher.fetch(
userWalletId = userWalletId,
network = network,
networkCurrencies = emptySet(),
)
// Assert: returns success without touching the wallet manager facade — guard prevents storing
// a NetworkStatus.Verified with empty `amounts` over an existing valid cached status.
Truth.assertThat(actual).isEqualTo(Either.Right(Unit))
coVerify(inverse = true) {
walletManagersFacade.update(
userWalletId = any(),
network = any(),
extraTokens = any(),
)
}
}
@ParameterizedTest
@ProvideTestModels
fun `fetch successfully for any result of walletManagersFacade`(model: SuccessTestModel) = runTest {