Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-19 13:31:38 +04:00
parent 2887693055
commit 990b06c3ed
16 changed files with 725 additions and 198 deletions

View file

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

View file

@ -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<UserWalletId, Mutex>()
/**
* 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<CryptoCurrency>) {
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<CryptoCurrency>) {
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<CryptoCurrency>) {
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<CryptoCurrency>,
): Either<Throwable, Unit> {
return multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network),
),
)
}
private suspend fun refreshStakingBalances(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> {
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<CryptoCurrency>): Either<Throwable, Unit> {
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,
)
}
}

View file

@ -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<CryptoCurrency>()
// 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<CryptoCurrency>()
// 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<Int>()
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<String>()
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()
}
}
}

View file

@ -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<CryptoCurrency>,
sources: Set<FetchingSource>,
): Map<FetchingSource, Throwable> {
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<CryptoCurrency>,
): Either<Throwable, Unit> {
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<CryptoCurrency>): Either<Throwable, Unit> {
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<CryptoCurrency>,
): Either<Throwable, Unit> {
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),
)
}
}

View file

@ -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<FetchingSource, Throwable>): 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, Throwable>): String {
return formatInternal(
userWalletId = userWalletId,
entries = errors.map { (sourceName, error) -> sourceName to error },
)
}
private fun formatInternal(userWalletId: UserWalletId, entries: List<Pair<String, Throwable>>): String {
return "Failed to fetch next sources for $userWalletId:\n" +
entries.joinToString(separator = "\n") { (name, error) -> "$name $error" }
}
}

View file

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

View file

@ -11,7 +11,7 @@ import com.tangem.domain.models.wallet.UserWallet
internal interface BaseWalletBalanceFetcher {
/** Fetching sources */
val fetchingSources: Set<FetchingSource>
val fetchingSources: Set<WalletFetchingSource>
/** Get crypto currencies of [userWallet] */
suspend fun getCryptoCurrencies(userWallet: UserWallet): Set<CryptoCurrency>

View file

@ -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<WalletBalanceFetcher.Params> {
/** 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<String, Throwable>()) { 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<CryptoCurrency>,
): Either<Throwable, Unit> {
return multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network),
),
)
}
private suspend fun fetchQuotes(currencies: Set<CryptoCurrency>): Either<Throwable, Unit> {
return multiQuoteStatusFetcher(
params = MultiQuoteStatusFetcher.Params(
currenciesIds = currencies.mapNotNullTo(destination = hashSetOf(), transform = { it.id.rawCurrencyId }),
appCurrencyId = null,
),
)
}
private suspend fun fetchStaking(
userWalletId: UserWalletId,
currencies: Set<CryptoCurrency>,
): Either<Throwable, Unit> = 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<CryptoCurrency>) {
val assetIds = currencies.mapTo(hashSetOf()) { currency ->
ExpressAsset.ID(

View file

@ -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<FetchingSource>) : WalletFetchingSource()
}

View file

@ -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<FetchingSource> = setOf(
FetchingSource.NETWORK,
FetchingSource.QUOTE,
FetchingSource.STAKING,
FetchingSource.TANGEM_PAY,
override val fetchingSources: Set<WalletFetchingSource> = setOf(
WalletFetchingSource.Balance(
sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING),
),
WalletFetchingSource.TangemPay,
)
override suspend fun getCryptoCurrencies(userWallet: UserWallet): Set<CryptoCurrency> {

View file

@ -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<FetchingSource> = setOf(
FetchingSource.NETWORK,
FetchingSource.QUOTE,
override val fetchingSources: Set<WalletFetchingSource> = setOf(
WalletFetchingSource.Balance(
sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE),
),
)
override suspend fun getCryptoCurrencies(userWallet: UserWallet): Set<CryptoCurrency> {

View file

@ -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<FetchingSource> = setOf(
FetchingSource.NETWORK,
FetchingSource.QUOTE,
override val fetchingSources: Set<WalletFetchingSource> = setOf(
WalletFetchingSource.Balance(
sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE),
),
)
override suspend fun getCryptoCurrencies(userWallet: UserWallet): Set<CryptoCurrency> {

View file

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

View file

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

View file

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

View file

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