Updated on 2026-08-14
This commit is contained in:
parent
2887693055
commit
990b06c3ed
16 changed files with 725 additions and 198 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue