From 990b06c3ed6e99a44a496ae93b6cb391e456a01b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Mar 2026 13:31:38 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../status/di/AccountStatusUseCaseModule.kt | 19 +- .../utils/CryptoCurrencyBalanceFetcher.kt | 125 +++---- .../utils/CryptoCurrencyBalanceFetcherTest.kt | 316 ++++++++++++++++++ .../tokens/BalanceFetchingOperations.kt | 134 ++++++++ .../domain/tokens/FetchErrorFormatter.kt | 49 +++ .../tokens/{wallet => }/FetchingSource.kt | 3 +- .../tokens/wallet/BaseWalletBalanceFetcher.kt | 2 +- .../tokens/wallet/WalletBalanceFetcher.kt | 139 ++++---- .../tokens/wallet/WalletFetchingSource.kt | 28 ++ .../implementor/MultiWalletBalanceFetcher.kt | 13 +- .../implementor/SingleWalletBalanceFetcher.kt | 10 +- .../SingleWalletWithTokenBalanceFetcher.kt | 10 +- .../tokens/wallet/WalletBalanceFetcherTest.kt | 46 ++- .../MultiWalletBalanceFetcherTest.kt | 11 +- .../SingleWalletBalanceFetcherTest.kt | 9 +- ...SingleWalletWithTokenBalanceFetcherTest.kt | 9 +- 16 files changed, 725 insertions(+), 198 deletions(-) create mode 100644 domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcherTest.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchErrorFormatter.kt rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{wallet => }/FetchingSource.kt (73%) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletFetchingSource.kt diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index 31d9b0d3b4..befcbcbbb0 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -17,6 +17,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.staking.utils.StakingCleaner +import com.tangem.domain.tokens.BalanceFetchingOperations import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -121,18 +122,28 @@ internal object AccountStatusUseCaseModule { @Provides @Singleton - fun provideCryptoCurrencyBalanceFetcher( + fun provideBalanceFetchingOperations( multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiStakingBalanceFetcher: MultiStakingBalanceFetcher, stakingIdFactory: StakingIdFactory, - appScope: AppCoroutineScope, - ): CryptoCurrencyBalanceFetcher { - return CryptoCurrencyBalanceFetcher( + ): BalanceFetchingOperations { + return BalanceFetchingOperations( multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, + ) + } + + @Provides + @Singleton + fun provideCryptoCurrencyBalanceFetcher( + balanceFetchingOperations: BalanceFetchingOperations, + appScope: AppCoroutineScope, + ): CryptoCurrencyBalanceFetcher { + return CryptoCurrencyBalanceFetcher( + balanceFetchingOperations = balanceFetchingOperations, parallelUpdatingScope = appScope, ) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt index 367d8411cf..bb277d0707 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt @@ -1,128 +1,101 @@ package com.tangem.domain.account.status.utils -import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher -import com.tangem.domain.tokens.wallet.FetchingSource -import kotlinx.coroutines.* +import com.tangem.domain.tokens.BalanceFetchingOperations +import com.tangem.domain.tokens.FetchErrorFormatter +import com.tangem.domain.tokens.FetchingSource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap /** * Utility class responsible for fetching and refreshing the balances of various crypto currencies * associated with a user's wallet. * - * @property multiNetworkStatusFetcher Fetcher for updating network statuses. - * @property multiQuoteStatusFetcher Fetcher for updating quote statuses. - * @property multiStakingBalanceFetcher Fetcher for updating staking balances. - * @property stakingIdFactory Factory for creating staking IDs. - * @property parallelUpdatingScope Coroutine scope for parallel balance updates. + * Uses [BalanceFetchingOperations] for the actual fetching logic. + * Uses per-wallet mutex to allow concurrent refreshes for different wallets while preventing + * concurrent refreshes for the same wallet. + * + * @property balanceFetchingOperations shared operations for fetching balance data + * @property parallelUpdatingScope coroutine scope for parallel balance updates * [REDACTED_AUTHOR] */ class CryptoCurrencyBalanceFetcher( - private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, - private val stakingIdFactory: StakingIdFactory, + private val balanceFetchingOperations: BalanceFetchingOperations, private val parallelUpdatingScope: CoroutineScope, ) { - private val mutex = Mutex() + private val mutexMap = ConcurrentHashMap() + /** + * Fire-and-forget balance refresh for a single currency. + */ operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency) { invoke(userWalletId = userWalletId, currencies = listOf(currency)) } + /** + * Fire-and-forget balance refresh for multiple currencies. + * Launches in [parallelUpdatingScope] and uses per-wallet mutex to prevent concurrent refreshes + * for the same wallet while allowing parallel refreshes for different wallets. + */ operator fun invoke(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) return parallelUpdatingScope.launch { - mutex.withLock { + getMutex(userWalletId).withLock { refreshBalances(userWalletId, currencies) } } } + /** + * Suspending balance refresh for a single currency. + * Awaits completion before returning. + */ suspend fun invokeAndAwait(userWalletId: UserWalletId, currency: CryptoCurrency) { invokeAndAwait(userWalletId = userWalletId, currencies = listOf(currency)) } + /** + * Suspending balance refresh for multiple currencies. + * Awaits completion before returning, uses per-wallet mutex to prevent concurrent refreshes + * for the same wallet while allowing parallel refreshes for different wallets. + */ suspend fun invokeAndAwait(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) return - mutex.withLock { + getMutex(userWalletId).withLock { refreshBalances(userWalletId, currencies) } } + private fun getMutex(userWalletId: UserWalletId): Mutex { + return mutexMap.computeIfAbsent(userWalletId) { Mutex() } + } + private suspend fun refreshBalances(userWalletId: UserWalletId, currencies: List) { - coroutineScope { - val results = listOf( - async { - FetchingSource.NETWORK to refreshNetworks(userWalletId = userWalletId, currencies = currencies) - }, - async { - FetchingSource.STAKING to refreshStakingBalances( - userWalletId = userWalletId, - currencies = currencies, - ) - }, - async { FetchingSource.QUOTE to refreshQuotes(currencies = currencies) }, - ) - .awaitAll() + val errors = balanceFetchingOperations.fetchAll( + userWalletId = userWalletId, + currencies = currencies, + sources = FETCHING_SOURCES, + ) - val errors = results.mapNotNull { (source, maybeResult) -> - val error = maybeResult.leftOrNull() ?: return@mapNotNull null - - source to error - } - - if (errors.isNotEmpty()) { - val message = "Failed to fetch next sources for $userWalletId:\n" + - errors.joinToString(separator = "\n") { "${it.first.name} – ${it.second}" } - - Timber.e(message) - } + if (errors.isNotEmpty()) { + Timber.e(FetchErrorFormatter.format(userWalletId, errors)) } } - private suspend fun refreshNetworks( - userWalletId: UserWalletId, - currencies: List, - ): Either { - return multiNetworkStatusFetcher( - params = MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network), - ), - ) - } - - private suspend fun refreshStakingBalances( - userWalletId: UserWalletId, - currencies: List, - ): Either { - val stakingIds = currencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() - } - - return multiStakingBalanceFetcher( - params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), - ) - } - - private suspend fun refreshQuotes(currencies: List): Either { - return multiQuoteStatusFetcher( - params = MultiQuoteStatusFetcher.Params( - currenciesIds = currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, - appCurrencyId = null, - ), + private companion object { + val FETCHING_SOURCES = setOf( + FetchingSource.NETWORK, + FetchingSource.QUOTE, + FetchingSource.STAKING, ) } } \ No newline at end of file diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcherTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcherTest.kt new file mode 100644 index 0000000000..8895897426 --- /dev/null +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcherTest.kt @@ -0,0 +1,316 @@ +package com.tangem.domain.account.status.utils + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.BalanceFetchingOperations +import com.tangem.domain.tokens.FetchingSource +import com.tangem.test.mock.MockAccounts +import io.mockk.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.* +import java.util.concurrent.atomic.AtomicInteger + +/** + * Tests for [CryptoCurrencyBalanceFetcher] + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class CryptoCurrencyBalanceFetcherTest { + + private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + private val balanceFetchingOperations: BalanceFetchingOperations = mockk() + + private val userWalletId = MockAccounts.userWalletId + private val userWalletId2 = UserWalletId("012") + + @AfterEach + fun tearDown() { + clearMocks(balanceFetchingOperations) + } + + private fun createFetcher(testScope: TestScope): CryptoCurrencyBalanceFetcher { + return CryptoCurrencyBalanceFetcher( + balanceFetchingOperations = balanceFetchingOperations, + parallelUpdatingScope = testScope, + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class InvokeWithEmptyCurrencies { + + @Test + fun `invoke with empty currencies does not fetch`() = runTest { + // Arrange + val fetcher = createFetcher(this) + val currencies = emptyList() + + // Act + fetcher(userWalletId = userWalletId, currencies = currencies) + advanceUntilIdle() + + // Assert + coVerify(exactly = 0) { balanceFetchingOperations.fetchAll(any(), any(), any()) } + } + + @Test + fun `invokeAndAwait with empty currencies does not fetch`() = runTest { + // Arrange + val fetcher = createFetcher(this) + val currencies = emptyList() + + // Act + fetcher.invokeAndAwait(userWalletId = userWalletId, currencies = currencies) + + // Assert + coVerify(exactly = 0) { balanceFetchingOperations.fetchAll(any(), any(), any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class InvokeWithSingleCurrency { + + @Test + fun `invoke with single currency fetches all sources`() = runTest { + // Arrange + val fetcher = createFetcher(this) + val currency = cryptoCurrencyFactory.ethereum + coEvery { + balanceFetchingOperations.fetchAll( + userWalletId = userWalletId, + currencies = listOf(currency), + sources = any(), + ) + } returns emptyMap() + + // Act + fetcher(userWalletId = userWalletId, currency = currency) + advanceUntilIdle() + + // Assert + coVerify(exactly = 1) { + balanceFetchingOperations.fetchAll( + userWalletId = userWalletId, + currencies = listOf(currency), + sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING), + ) + } + } + + @Test + fun `invokeAndAwait with single currency fetches all sources`() = runTest { + // Arrange + val fetcher = createFetcher(this) + val currency = cryptoCurrencyFactory.ethereum + coEvery { + balanceFetchingOperations.fetchAll( + userWalletId = userWalletId, + currencies = listOf(currency), + sources = any(), + ) + } returns emptyMap() + + // Act + fetcher.invokeAndAwait(userWalletId = userWalletId, currency = currency) + + // Assert + coVerify(exactly = 1) { + balanceFetchingOperations.fetchAll( + userWalletId = userWalletId, + currencies = listOf(currency), + sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING), + ) + } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class InvokeWithMultipleCurrencies { + + @Test + fun `invoke with multiple currencies passes correct parameters`() = runTest { + // Arrange + val fetcher = createFetcher(this) + val currencies = cryptoCurrencyFactory.ethereumAndStellar + coEvery { + balanceFetchingOperations.fetchAll( + userWalletId = userWalletId, + currencies = currencies, + sources = any(), + ) + } returns emptyMap() + + // Act + fetcher(userWalletId = userWalletId, currencies = currencies) + advanceUntilIdle() + + // Assert + coVerify(exactly = 1) { + balanceFetchingOperations.fetchAll( + userWalletId = userWalletId, + currencies = currencies, + sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING), + ) + } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ErrorHandling { + + @Test + fun `invoke logs errors when fetch fails`() = runTest { + // Arrange + val fetcher = createFetcher(this) + val currency = cryptoCurrencyFactory.ethereum + val error = RuntimeException("Network error") + val errors = mapOf(FetchingSource.NETWORK to error) + + coEvery { + balanceFetchingOperations.fetchAll(any(), any(), any()) + } returns errors + + // Act + fetcher(userWalletId = userWalletId, currency = currency) + advanceUntilIdle() + + // Assert - fetchAll was called, FetchErrorFormatter.format is used internally (object, no mock needed) + coVerify(exactly = 1) { + balanceFetchingOperations.fetchAll(any(), any(), any()) + } + } + + @Test + fun `invoke continues even when some sources fail`() = runTest { + // Arrange + val fetcher = createFetcher(this) + val currency = cryptoCurrencyFactory.ethereum + val networkError = RuntimeException("Network error") + val errors = mapOf(FetchingSource.NETWORK to networkError) + + coEvery { + balanceFetchingOperations.fetchAll(any(), any(), any()) + } returns errors + + // Act & Assert - should not throw + fetcher(userWalletId = userWalletId, currency = currency) + advanceUntilIdle() + + coVerify(exactly = 1) { balanceFetchingOperations.fetchAll(any(), any(), any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class MutexBehavior { + + @Test + fun `concurrent calls for same wallet are serialized`() = runTest { + // Arrange + val fetcher = createFetcher(this) + val currency = cryptoCurrencyFactory.ethereum + val callOrder = mutableListOf() + val callCount = AtomicInteger(0) + + coEvery { + balanceFetchingOperations.fetchAll(any(), any(), any()) + } coAnswers { + val currentCall = callCount.incrementAndGet() + callOrder.add(currentCall) + delay(100) // Simulate work + callOrder.add(-currentCall) // Mark completion + emptyMap() + } + + // Act - launch two concurrent calls for the same wallet + fetcher(userWalletId = userWalletId, currency = currency) + fetcher(userWalletId = userWalletId, currency = currency) + advanceUntilIdle() + + // Assert - calls should be serialized (1 starts, 1 finishes, 2 starts, 2 finishes) + assertThat(callOrder).containsExactly(1, -1, 2, -2).inOrder() + } + + @Test + fun `concurrent calls for different wallets run in parallel`() = runTest { + // Arrange + val fetcher = createFetcher(this) + val currency = cryptoCurrencyFactory.ethereum + val callOrder = mutableListOf() + + coEvery { + balanceFetchingOperations.fetchAll(eq(userWalletId), any(), any()) + } coAnswers { + callOrder.add("wallet1-start") + delay(100) + callOrder.add("wallet1-end") + emptyMap() + } + + coEvery { + balanceFetchingOperations.fetchAll(eq(userWalletId2), any(), any()) + } coAnswers { + callOrder.add("wallet2-start") + delay(100) + callOrder.add("wallet2-end") + emptyMap() + } + + // Act - launch two concurrent calls for different wallets + fetcher(userWalletId = userWalletId, currency = currency) + fetcher(userWalletId = userWalletId2, currency = currency) + advanceUntilIdle() + + // Assert - both should start before either finishes (parallel execution) + val wallet1StartIndex = callOrder.indexOf("wallet1-start") + val wallet2StartIndex = callOrder.indexOf("wallet2-start") + val wallet1EndIndex = callOrder.indexOf("wallet1-end") + val wallet2EndIndex = callOrder.indexOf("wallet2-end") + + // Both should start before both end + assertThat(wallet1StartIndex).isLessThan(wallet1EndIndex) + assertThat(wallet2StartIndex).isLessThan(wallet2EndIndex) + // Both starts should happen before both ends (parallel) + assertThat(maxOf(wallet1StartIndex, wallet2StartIndex)) + .isLessThan(minOf(wallet1EndIndex, wallet2EndIndex)) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class InvokeAndAwaitBehavior { + + @Test + fun `invokeAndAwait suspends until completion`() = runTest { + // Arrange + val fetcher = createFetcher(this) + val currency = cryptoCurrencyFactory.ethereum + var completed = false + + coEvery { + balanceFetchingOperations.fetchAll(any(), any(), any()) + } coAnswers { + delay(100) + completed = true + emptyMap() + } + + // Act + fetcher.invokeAndAwait(userWalletId = userWalletId, currency = currency) + + // Assert - should be completed after invokeAndAwait returns + assertThat(completed).isTrue() + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt new file mode 100644 index 0000000000..96df850a31 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt @@ -0,0 +1,134 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.right +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import timber.log.Timber + +/** + * Shared utility for fetching cryptocurrency balance data from multiple sources. + * + * This class provides reusable fetch operations for networks, quotes, and staking balances + * that can be used by different balance fetchers throughout the application. + * + * @property multiNetworkStatusFetcher Fetcher for updating network statuses. + * @property multiQuoteStatusFetcher Fetcher for updating quote statuses. + * @property multiStakingBalanceFetcher Fetcher for updating staking balances. + * @property stakingIdFactory Factory for creating staking IDs. + * +[REDACTED_AUTHOR] + */ +class BalanceFetchingOperations( + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, +) { + + /** + * Fetches balance data from specified sources in parallel. + * + * @param userWalletId the user wallet identifier + * @param currencies the list of cryptocurrencies to fetch data for + * @param sources the set of sources to fetch from + * @return map of errors (source to throwable), empty if all succeeded + */ + suspend fun fetchAll( + userWalletId: UserWalletId, + currencies: Collection, + sources: Set, + ): Map { + return coroutineScope { + sources.map { source -> + async { + val result = when (source) { + FetchingSource.NETWORK -> fetchNetworks(userWalletId, currencies) + FetchingSource.QUOTE -> fetchQuotes(currencies) + FetchingSource.STAKING -> fetchStaking(userWalletId, currencies) + } + source to result + } + } + .awaitAll() + .mapNotNull { (source, result) -> + result.leftOrNull()?.let { error -> source to error } + } + .toMap() + } + } + + /** + * Fetches network statuses for the given currencies. + * + * @param userWalletId the user wallet identifier + * @param currencies the cryptocurrencies to fetch network statuses for + * @return Either with Unit on success or Throwable on failure + */ + suspend fun fetchNetworks( + userWalletId: UserWalletId, + currencies: Collection, + ): Either { + return multiNetworkStatusFetcher( + params = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network), + ), + ) + } + + /** + * Fetches quotes for the given currencies. + * + * @param currencies the cryptocurrencies to fetch quotes for + * @return Either with Unit on success or Throwable on failure + */ + suspend fun fetchQuotes(currencies: Collection): Either { + return multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( + currenciesIds = currencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + appCurrencyId = null, + ), + ) + } + + /** + * Fetches staking balances for the given currencies. + * + * Logs errors for currencies where staking ID cannot be obtained. + * + * @param userWalletId the user wallet identifier + * @param currencies the cryptocurrencies to fetch staking balances for + * @return Either with Unit on success or Throwable on failure + */ + suspend fun fetchStaking( + userWalletId: UserWalletId, + currencies: Collection, + ): Either { + val stakingIds = currencies.mapNotNullTo(hashSetOf()) { currency -> + val stakingId = stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = currency) + + if (stakingId.isLeft { it is StakingIdFactory.Error.UnableToGetAddress }) { + Timber.e("Unable to get staking ID for user wallet $userWalletId and currency ${currency.id}") + } + + stakingId.getOrNull() + } + + if (stakingIds.isEmpty()) { + Timber.i("No staking IDs found for user wallet $userWalletId") + return Unit.right() + } + + return multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchErrorFormatter.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchErrorFormatter.kt new file mode 100644 index 0000000000..44b77a8a30 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchErrorFormatter.kt @@ -0,0 +1,49 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Formats fetch errors into a human-readable log message. + * +[REDACTED_AUTHOR] + */ +object FetchErrorFormatter { + + /** Source name constant for TangemPay errors */ + const val TANGEM_PAY_SOURCE_NAME = "TANGEM_PAY" + + /** + * Formats fetch errors into a log message. + * Used by [CryptoCurrencyBalanceFetcher] which works only with [FetchingSource]. + * + * @param userWalletId the user wallet identifier for context + * @param errors the map of source to error + * @return formatted error message + */ + fun format(userWalletId: UserWalletId, errors: Map): String { + return formatInternal( + userWalletId = userWalletId, + entries = errors.map { (source, error) -> source.name to error }, + ) + } + + /** + * Formats fetch errors into a log message. + * Used by [WalletBalanceFetcher] which works with [WalletFetchingSource] including TangemPay. + * + * @param userWalletId the user wallet identifier for context + * @param errors the map of source name to error (keys: "NETWORK", "QUOTE", "STAKING", "TANGEM_PAY") + * @return formatted error message + */ + fun formatWalletErrors(userWalletId: UserWalletId, errors: Map): String { + return formatInternal( + userWalletId = userWalletId, + entries = errors.map { (sourceName, error) -> sourceName to error }, + ) + } + + private fun formatInternal(userWalletId: UserWalletId, entries: List>): String { + return "Failed to fetch next sources for $userWalletId:\n" + + entries.joinToString(separator = "\n") { (name, error) -> "$name – $error" } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchingSource.kt similarity index 73% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchingSource.kt index 56c8bf8ef3..2689d00864 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchingSource.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.tokens.wallet +package com.tangem.domain.tokens /** * Source type that is necessary to load cryptocurrency data @@ -9,5 +9,4 @@ enum class FetchingSource { NETWORK, QUOTE, STAKING, - TANGEM_PAY, } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/BaseWalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/BaseWalletBalanceFetcher.kt index c9fd602d9b..d5efb0aa7e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/BaseWalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/BaseWalletBalanceFetcher.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.wallet.UserWallet internal interface BaseWalletBalanceFetcher { /** Fetching sources */ - val fetchingSources: Set + val fetchingSources: Set /** Get crypto currencies of [userWallet] */ suspend fun getCryptoCurrencies(userWallet: UserWallet): Set diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index e20203e84e..afb0384174 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.wallet import arrow.core.Either -import arrow.core.raise.either import arrow.core.right import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory @@ -19,6 +18,8 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.domain.tokens.BalanceFetchingOperations +import com.tangem.domain.tokens.FetchErrorFormatter import com.tangem.domain.tokens.MultiWalletAccountListFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher @@ -33,14 +34,15 @@ import timber.log.Timber /** * Fetcher of wallet balance by [UserWalletId] * + * Uses [BalanceFetchingOperations] for shared fetching logic. + * * @property userWalletsListRepository user wallets list repository * @property expressServiceFetcher express service fetcher * @property multiWalletBalanceFetcher balance fetcher of multi-currency wallet * @property singleWalletWithTokenBalanceFetcher balance fetcher of single-currency wallet with token * @property singleWalletBalanceFetcher balance fetcher of single-currency wallet - * @property multiNetworkStatusFetcher networks statuses fetcher - * @property multiQuoteStatusFetcher quotes statuses fetcher - * @property multiStakingBalanceFetcher yields balances fetcher + * @property balanceFetchingOperations shared operations for fetching balance data + * @property paymentAccountStatusFetcher payment account status fetcher * @property dispatchers dispatchers * [REDACTED_AUTHOR] @@ -52,14 +54,40 @@ class WalletBalanceFetcher internal constructor( private val multiWalletBalanceFetcher: BaseWalletBalanceFetcher, private val singleWalletWithTokenBalanceFetcher: BaseWalletBalanceFetcher, private val singleWalletBalanceFetcher: BaseWalletBalanceFetcher, - private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + private val balanceFetchingOperations: BalanceFetchingOperations, private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, - private val stakingIdFactory: StakingIdFactory, private val dispatchers: CoroutineDispatcherProvider, ) : FlowFetcher { + /** Test constructor with direct fetcher dependencies for unit testing */ + internal constructor( + userWalletsListRepository: UserWalletsListRepository, + expressServiceFetcher: ExpressServiceFetcher, + multiWalletBalanceFetcher: BaseWalletBalanceFetcher, + singleWalletWithTokenBalanceFetcher: BaseWalletBalanceFetcher, + singleWalletBalanceFetcher: BaseWalletBalanceFetcher, + multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + stakingIdFactory: StakingIdFactory, + dispatchers: CoroutineDispatcherProvider, + ) : this( + userWalletsListRepository = userWalletsListRepository, + expressServiceFetcher = expressServiceFetcher, + multiWalletBalanceFetcher = multiWalletBalanceFetcher, + singleWalletWithTokenBalanceFetcher = singleWalletWithTokenBalanceFetcher, + singleWalletBalanceFetcher = singleWalletBalanceFetcher, + balanceFetchingOperations = BalanceFetchingOperations( + multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, + stakingIdFactory = stakingIdFactory, + ), + paymentAccountStatusFetcher = paymentAccountStatusFetcher, + dispatchers = dispatchers, + ) + /** Additional constructor without internal dependencies */ constructor( userWalletsListRepository: UserWalletsListRepository, @@ -86,11 +114,13 @@ class WalletBalanceFetcher internal constructor( singleWalletBalanceFetcher = SingleWalletBalanceFetcher( cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, ), - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiStakingBalanceFetcher = multiStakingBalanceFetcher, + balanceFetchingOperations = BalanceFetchingOperations( + multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, + stakingIdFactory = stakingIdFactory, + ), paymentAccountStatusFetcher = paymentAccountStatusFetcher, - stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) @@ -130,87 +160,36 @@ class WalletBalanceFetcher internal constructor( paymentAccountRefactorEnabled: Boolean, ) { coroutineScope { - val results = fetchingSources.map { source -> + val errorDeferreds = fetchingSources.map { source -> async { - val maybeResult = when (source) { - FetchingSource.NETWORK -> fetchNetworks(userWalletId = userWalletId, currencies = currencies) - FetchingSource.QUOTE -> fetchQuotes(currencies = currencies) - FetchingSource.STAKING -> fetchStaking(userWalletId = userWalletId, currencies = currencies) - FetchingSource.TANGEM_PAY -> fetchPaymentAccount( - userWalletId = userWalletId, - paymentAccountRefactorEnabled = paymentAccountRefactorEnabled, - ) + when (source) { + is WalletFetchingSource.Balance -> { + balanceFetchingOperations.fetchAll( + userWalletId = userWalletId, + currencies = currencies, + sources = source.sources, + ).mapKeys { (fetchingSource, _) -> fetchingSource.name } + } + is WalletFetchingSource.TangemPay -> { + fetchPaymentAccount(userWalletId, paymentAccountRefactorEnabled) + .leftOrNull() + ?.let { error -> mapOf(FetchErrorFormatter.TANGEM_PAY_SOURCE_NAME to error) } + .orEmpty() + } } - - source to maybeResult } } - .awaitAll() - val errors = results.mapNotNull { (source, maybeResult) -> - val error = maybeResult.leftOrNull() ?: return@mapNotNull null - - source to error - } + val errors = errorDeferreds.awaitAll().fold(emptyMap()) { acc, map -> acc + map } check(errors.isEmpty()) { - val message = "Failed to fetch next sources for $userWalletId:\n" + - errors.joinToString(separator = "\n") { "${it.first.name} – ${it.second}" } - + val message = FetchErrorFormatter.formatWalletErrors(userWalletId, errors) Timber.e(message) - message } } } - private suspend fun fetchNetworks( - userWalletId: UserWalletId, - currencies: Set, - ): Either { - return multiNetworkStatusFetcher( - params = MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network), - ), - ) - } - - private suspend fun fetchQuotes(currencies: Set): Either { - return multiQuoteStatusFetcher( - params = MultiQuoteStatusFetcher.Params( - currenciesIds = currencies.mapNotNullTo(destination = hashSetOf(), transform = { it.id.rawCurrencyId }), - appCurrencyId = null, - ), - ) - } - - private suspend fun fetchStaking( - userWalletId: UserWalletId, - currencies: Set, - ): Either = either { - val maybeStakingIds = currencies.map { currency -> - val stakingId = stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = currency) - - if (stakingId.isLeft { it is StakingIdFactory.Error.UnableToGetAddress }) { - Timber.e("Unable to get staking ID for user wallet $userWalletId and currency ${currency.id}") - } - - stakingId - } - - val stakingIds = maybeStakingIds.mapNotNullTo(hashSetOf()) { it.getOrNull() } - - if (stakingIds.isNotEmpty()) { - multiStakingBalanceFetcher( - params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), - ) - .bind() - } else { - Timber.i("No staking IDs found for user wallet $userWalletId with currencies: $currencies") - } - } - private suspend fun fetchExpressAssets(userWallet: UserWallet, currencies: Set) { val assetIds = currencies.mapTo(hashSetOf()) { currency -> ExpressAsset.ID( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletFetchingSource.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletFetchingSource.kt new file mode 100644 index 0000000000..69a2e52b58 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletFetchingSource.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.tokens.wallet + +import com.tangem.domain.tokens.FetchingSource + +/** + * Sealed class representing different types of fetching sources for wallet balance operations. + * + * This separates TangemPay (which requires special handling) from standard balance fetching sources + * (NETWORK, QUOTE, STAKING) that are processed uniformly via [BalanceFetchingOperations]. + * +[REDACTED_AUTHOR] + */ +sealed class WalletFetchingSource { + + /** + * TangemPay account fetching source. + * Handled separately from standard balance sources via [PaymentAccountStatusFetcher]. + */ + data object TangemPay : WalletFetchingSource() + + /** + * Standard balance fetching sources (NETWORK, QUOTE, STAKING). + * Processed via [BalanceFetchingOperations.fetchAll]. + * + * @property sources set of [FetchingSource] types to fetch + */ + data class Balance(val sources: Set) : WalletFetchingSource() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt index adbd435398..e7f95702ce 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt @@ -5,8 +5,9 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.MultiWalletAccountListFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier +import com.tangem.domain.tokens.FetchingSource import com.tangem.domain.tokens.wallet.BaseWalletBalanceFetcher -import com.tangem.domain.tokens.wallet.FetchingSource +import com.tangem.domain.tokens.wallet.WalletFetchingSource import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber @@ -23,11 +24,11 @@ internal class MultiWalletBalanceFetcher( private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, ) : BaseWalletBalanceFetcher { - override val fetchingSources: Set = setOf( - FetchingSource.NETWORK, - FetchingSource.QUOTE, - FetchingSource.STAKING, - FetchingSource.TANGEM_PAY, + override val fetchingSources: Set = setOf( + WalletFetchingSource.Balance( + sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING), + ), + WalletFetchingSource.TangemPay, ) override suspend fun getCryptoCurrencies(userWallet: UserWallet): Set { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletBalanceFetcher.kt index 9e31c0b078..f78152c6de 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletBalanceFetcher.kt @@ -4,8 +4,9 @@ import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.tokens.FetchingSource import com.tangem.domain.tokens.wallet.BaseWalletBalanceFetcher -import com.tangem.domain.tokens.wallet.FetchingSource +import com.tangem.domain.tokens.wallet.WalletFetchingSource /** * Implementation of [BaseWalletBalanceFetcher] for SINGLE-CURRENCY wallet @@ -18,9 +19,10 @@ internal class SingleWalletBalanceFetcher( private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ) : BaseWalletBalanceFetcher { - override val fetchingSources: Set = setOf( - FetchingSource.NETWORK, - FetchingSource.QUOTE, + override val fetchingSources: Set = setOf( + WalletFetchingSource.Balance( + sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE), + ), ) override suspend fun getCryptoCurrencies(userWallet: UserWallet): Set { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletWithTokenBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletWithTokenBalanceFetcher.kt index 9f40bad1c4..ac016a6307 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletWithTokenBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletWithTokenBalanceFetcher.kt @@ -4,8 +4,9 @@ import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.tokens.FetchingSource import com.tangem.domain.tokens.wallet.BaseWalletBalanceFetcher -import com.tangem.domain.tokens.wallet.FetchingSource +import com.tangem.domain.tokens.wallet.WalletFetchingSource /** * Implementation of [BaseWalletBalanceFetcher] for SINGLE-CURRENCY wallet WITH TOKEN (like, NODL) @@ -18,9 +19,10 @@ internal class SingleWalletWithTokenBalanceFetcher( private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ) : BaseWalletBalanceFetcher { - override val fetchingSources: Set = setOf( - FetchingSource.NETWORK, - FetchingSource.QUOTE, + override val fetchingSources: Set = setOf( + WalletFetchingSource.Balance( + sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE), + ), ) override suspend fun getCryptoCurrencies(userWallet: UserWallet): Set { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt index eed3947fb1..c3ab25891d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -19,7 +19,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher -import com.tangem.domain.tokens.wallet.FetchingSource.* +import com.tangem.domain.tokens.FetchingSource import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher @@ -251,7 +251,9 @@ internal class WalletBalanceFetcherTest { mockColdWallet(cardTypesResolver) coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK) + every { multiWalletBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.NETWORK)), + ) coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns exception.left() // Act @@ -304,7 +306,9 @@ internal class WalletBalanceFetcherTest { mockColdWallet(cardTypesResolver) coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { multiWalletBalanceFetcher.fetchingSources } returns setOf(QUOTE) + every { multiWalletBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.QUOTE)), + ) coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns exception.left() // Act @@ -357,7 +361,9 @@ internal class WalletBalanceFetcherTest { mockColdWallet(cardTypesResolver) coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + every { multiWalletBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.STAKING)), + ) coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) } returns Either.Right(ethereumStakingId) @@ -411,7 +417,9 @@ internal class WalletBalanceFetcherTest { mockColdWallet(cardTypesResolver) coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + every { multiWalletBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.STAKING)), + ) coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = any()) } returns Either.Left(StakingIdFactory.Error.UnsupportedCurrency) @@ -462,7 +470,9 @@ internal class WalletBalanceFetcherTest { mockColdWallet(cardTypesResolver) coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + every { multiWalletBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.STAKING)), + ) coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = any()) } returns stakingId // Act @@ -512,7 +522,9 @@ internal class WalletBalanceFetcherTest { mockColdWallet(cardTypesResolver) coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + every { multiWalletBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.STAKING)), + ) coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) } returns ethereumStakingId @@ -578,7 +590,9 @@ internal class WalletBalanceFetcherTest { mockColdWallet(cardTypesResolver) coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE, STAKING) + every { multiWalletBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING)), + ) coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns exception.left() coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns exception.left() coEvery { @@ -651,7 +665,9 @@ internal class WalletBalanceFetcherTest { mockColdWallet(cardTypesResolver) coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE, STAKING) + every { multiWalletBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING)), + ) coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns Unit.right() coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() coEvery { @@ -717,7 +733,9 @@ internal class WalletBalanceFetcherTest { singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { singleWalletWithTokenBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE) + every { singleWalletWithTokenBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.NETWORK, FetchingSource.QUOTE)), + ) coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns Unit.right() coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() @@ -774,7 +792,9 @@ internal class WalletBalanceFetcherTest { mockColdWallet(cardTypesResolver) coEvery { singleWalletBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { singleWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE) + every { singleWalletBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.NETWORK, FetchingSource.QUOTE)), + ) coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns Unit.right() coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() @@ -832,7 +852,9 @@ internal class WalletBalanceFetcherTest { coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWallet = any()) } returns currencies coEvery { expressServiceFetcher.fetch(userWallet = any(), assetIds = any()) } returns mockk() - every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE, STAKING) + every { multiWalletBalanceFetcher.fetchingSources } returns setOf( + WalletFetchingSource.Balance(setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING)), + ) coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns Unit.right() coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() coEvery { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt index aa4e51b9e9..0eac725b25 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt @@ -10,7 +10,8 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.MultiWalletAccountListFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.wallet.FetchingSource +import com.tangem.domain.tokens.FetchingSource +import com.tangem.domain.tokens.wallet.WalletFetchingSource import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.every @@ -49,10 +50,10 @@ class MultiWalletBalanceFetcherTest { // Assert val expected = setOf( - FetchingSource.NETWORK, - FetchingSource.QUOTE, - FetchingSource.STAKING, - FetchingSource.TANGEM_PAY, + WalletFetchingSource.Balance( + sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING), + ), + WalletFetchingSource.TangemPay, ) Truth.assertThat(actual).isEqualTo(expected) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletBalanceFetcherTest.kt index bae0299511..252a63aa12 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletBalanceFetcherTest.kt @@ -4,7 +4,8 @@ import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.wallet.FetchingSource +import com.tangem.domain.tokens.FetchingSource +import com.tangem.domain.tokens.wallet.WalletFetchingSource import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk @@ -38,7 +39,11 @@ class SingleWalletBalanceFetcherTest { val actual = fetcher.fetchingSources // Assert - val expected = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE) + val expected = setOf( + WalletFetchingSource.Balance( + sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE), + ), + ) Truth.assertThat(actual).isEqualTo(expected) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletWithTokenBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletWithTokenBalanceFetcherTest.kt index 56d54f2d0a..b4024d77fc 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletWithTokenBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/SingleWalletWithTokenBalanceFetcherTest.kt @@ -4,7 +4,8 @@ import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.wallet.FetchingSource +import com.tangem.domain.tokens.FetchingSource +import com.tangem.domain.tokens.wallet.WalletFetchingSource import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk @@ -38,7 +39,11 @@ class SingleWalletWithTokenBalanceFetcherTest { val actual = fetcher.fetchingSources // Assert - val expected = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE) + val expected = setOf( + WalletFetchingSource.Balance( + sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE), + ), + ) Truth.assertThat(actual).isEqualTo(expected) }