Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-31 10:46:03 +03:00
parent 8d3d9fc22e
commit d677abc44d
30 changed files with 423 additions and 164 deletions

View file

@ -15,6 +15,16 @@ import dagger.hilt.android.scopes.ViewModelScoped
@InstallIn(ViewModelComponent::class)
internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideFetchTokenListUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
): FetchTokenListUseCase {
return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository)
}
@Provides
@ViewModelScoped
fun provideGetTokenListUseCase(
@ -42,8 +52,8 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyUseCase {
return GetCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
): GetCurrencyStatusUpdatesUseCase {
return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
}
@Provides
@ -53,8 +63,23 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetPrimaryCurrencyUseCase {
return GetPrimaryCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
): GetPrimaryCurrencyStatusUpdatesUseCase {
return GetPrimaryCurrencyStatusUpdatesUseCase(
currenciesRepository,
quotesRepository,
networksRepository,
dispatchers,
)
}
@Provides
@ViewModelScoped
fun provideFetchCurrencyStatusUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository)
}
@Provides

View file

@ -7,6 +7,8 @@ interface SelectedAppCurrencyStore {
fun get(): Flow<CurrenciesResponse.Currency>
suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
suspend fun store(item: CurrenciesResponse.Currency)
suspend fun isEmpty(): Boolean

View file

@ -8,7 +8,6 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore
internal class DefaultSelectedAppCurrencyStore(
dataStore: StringKeyDataStore<CurrenciesResponse.Currency>,
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore) {
override suspend fun isEmpty(): Boolean {
return getSyncOrNull() == null
}

View file

@ -78,19 +78,18 @@ internal class DefaultCurrenciesRepository(
}
}
override fun getMultiCurrencyWalletCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): Flow<List<CryptoCurrency>> = channelFlow {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return channelFlow {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
getMultiCurrencyWalletCurrencies(userWallet).collect(::send)
}
launch(dispatchers.io) {
getMultiCurrencyWalletCurrencies(userWallet).collect(::send)
}
launch(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh)
launch(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh = false)
}
}
}
@ -102,7 +101,11 @@ internal class DefaultCurrenciesRepository(
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
fetchTokensIfCacheExpired(userWallet, refresh)
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId))
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
return responseCurrenciesFactory.createCurrencies(
response = storedTokens,
card = userWallet.scanResponse.card,

View file

@ -16,14 +16,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
internal class DefaultNetworksRepository(
private val walletManagersFacade: WalletManagersFacade,
@ -45,10 +39,9 @@ internal class DefaultNetworksRepository(
return networkConverter.convertSet(networksIds)
}
override fun getNetworkStatuses(
override fun getNetworkStatusesUpdates(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
refresh: Boolean,
): Flow<Set<NetworkStatus>> = channelFlow {
launch(dispatchers.io) {
networksStatuses.collect {
@ -57,10 +50,19 @@ internal class DefaultNetworksRepository(
}
launch(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false)
}
}
override suspend fun getNetworkStatusesSync(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
refresh: Boolean,
): Set<NetworkStatus> = withContext(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
networksStatuses.first().toSet()
}
private suspend fun fetchNetworksStatusesIfCacheExpired(
userWalletId: UserWalletId,
networks: Set<Network.ID>,

View file

@ -9,11 +9,9 @@ import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultQuotesRepository(
@ -28,7 +26,7 @@ internal class DefaultQuotesRepository(
private var quotesFetchedForAppCurrency: String? = null
override fun getQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>> {
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
return channelFlow {
launch(dispatchers.io) {
quotesStore.get(currenciesIds)
@ -38,12 +36,26 @@ internal class DefaultQuotesRepository(
launch(dispatchers.io) {
selectedAppCurrencyStore.get().collectLatest { appCurrency ->
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh)
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
}
}
}
}
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
return withContext(dispatchers.io) {
val selectedAppCurrency = requireNotNull(selectedAppCurrencyStore.getSyncOrNull()) {
"Unable to get selected application currency to update quotes"
}
fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, refresh)
val quotes = quotesStore.get(currenciesIds).first()
quotesConverter.convertSet(quotes)
}
}
private suspend fun fetchExpiredQuotes(
currenciesIds: Set<CryptoCurrency.ID>,
appCurrencyId: String,

View file

@ -68,7 +68,7 @@ class ApplyTokenListSortingUseCase(
private suspend fun Raise<TokenListSortingError>.getCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
val tokens = catch(
block = {
currenciesRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh = false).firstOrNull()
currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWalletId).firstOrNull()
},
catch = { raise(TokenListSortingError.DataError(it)) },
)

View file

@ -0,0 +1,121 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
/**
* Use case responsible for fetching currency status information, including network status
* and quotes for a given cryptocurrency. It provides methods to fetch currency status either
* by providing a specific currency ID or fetching the status of the primary currency.
*
* @param currenciesRepository The repository for retrieving currency-related data.
* @param networksRepository The repository for retrieving network-related data.
* @param quotesRepository The repository for retrieving cryptocurrency quotes.
*/
// TODO: Add tests
class FetchCurrencyStatusUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val quotesRepository: QuotesRepository,
) {
/**
* Fetches the status of a specific cryptocurrency for a given user wallet.
*
* @param userWalletId The ID of the user's wallet.
* @param id The ID of the cryptocurrency.
* @param refresh Indicates whether to force a refresh of the status data.
* @return An [Either] representing success (Right) or an error (Left) in fetching the status.
*/
suspend operator fun invoke(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
refresh: Boolean = false,
): Either<CurrencyStatusError, Unit> {
return either {
val currency = getCurrency(userWalletId, id)
fetchCurrencyStatus(userWalletId, currency, refresh)
}
}
/**
* Fetches the status of the primary cryptocurrency for a given user wallet.
*
* @param userWalletId The ID of the user's wallet.
* @param refresh Indicates whether to force a refresh of the status data.
* @return An [Either] representing success (Right) or an error (Left) in fetching the status.
*/
suspend operator fun invoke(
userWalletId: UserWalletId,
refresh: Boolean = false,
): Either<CurrencyStatusError, Unit> {
return either {
val currency = getPrimaryCurrency(userWalletId)
fetchCurrencyStatus(userWalletId, currency, refresh)
}
}
private suspend fun Raise<CurrencyStatusError>.fetchCurrencyStatus(
userWalletId: UserWalletId,
currency: CryptoCurrency,
refresh: Boolean,
) = coroutineScope {
val fetchStatus = async {
fetchNetworkStatus(userWalletId, currency.network.id, refresh)
}
val fetchQuote = async {
fetchQuote(currency.id, refresh)
}
awaitAll(fetchStatus, fetchQuote)
}
private suspend fun Raise<CurrencyStatusError>.getCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency {
return catch({ currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) }) {
raise(CurrencyStatusError.DataError(it))
}
}
private suspend fun Raise<CurrencyStatusError>.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
return catch({ currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }) {
raise(CurrencyStatusError.DataError(it))
}
}
private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(
userWalletId: UserWalletId,
networkId: Network.ID,
refresh: Boolean,
) {
catch(
block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(networkId), refresh) },
) {
raise(CurrencyStatusError.DataError(it))
}
}
private suspend fun Raise<CurrencyStatusError>.fetchQuote(currencyId: CryptoCurrency.ID, refresh: Boolean) {
catch(
block = { quotesRepository.getQuotesSync(setOf(currencyId), refresh) },
) {
raise(CurrencyStatusError.DataError(it))
}
}
}

View file

@ -0,0 +1,101 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptyListOrNull
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
/**
* Use case responsible for fetching token list information, including currency data,
* network statuses, and quotes for tokens associated with a user's wallet.
*
* @param currenciesRepository The repository for retrieving currency-related data.
* @param networksRepository The repository for retrieving network-related data.
* @param quotesRepository The repository for retrieving cryptocurrency quotes.
*/
// TODO: Add tests
class FetchTokenListUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val quotesRepository: QuotesRepository,
) {
/**
* Fetches the token list information for a user's wallet, including currency data,
* network statuses, and quotes for associated tokens.
*
* @param userWalletId The ID of the user's wallet.
* @param refresh Indicates whether to force a refresh of the token list data.
* @return An [Either] representing success (Right) or an error (Left) in fetching the token list.
*/
suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either<TokenListError, Unit> {
return either {
val currencies = fetchCurrencies(userWalletId, refresh)
coroutineScope {
val fetchStatuses = async {
fetchNetworksStatuses(
userWalletId,
currencies.mapTo(hashSetOf()) { it.network.id },
refresh,
)
}
val fetchQuotes = async {
fetchQuotes(
currencies.mapTo(hashSetOf()) { it.id },
refresh,
)
}
awaitAll(fetchStatuses, fetchQuotes)
}
}
}
private suspend fun Raise<TokenListError>.fetchCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): List<CryptoCurrency> {
val currencies = catch(
block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, refresh) },
) {
raise(TokenListError.DataError(it))
}
return ensureNotNull(currencies.toNonEmptyListOrNull()) {
TokenListError.EmptyTokens
}
}
private suspend fun Raise<TokenListError>.fetchNetworksStatuses(
userWalletId: UserWalletId,
networksIds: Set<Network.ID>,
refresh: Boolean,
) {
catch(
block = { networksRepository.getNetworkStatusesSync(userWalletId, networksIds, refresh) },
) {
raise(TokenListError.DataError(it))
}
}
private suspend fun Raise<TokenListError>.fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean) {
catch(
block = { quotesRepository.getQuotesSync(currenciesIds, refresh) },
) {
raise(TokenListError.DataError(it))
}
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.models.CryptoCurrency
@ -14,14 +14,13 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
/**
* Use case for fetching the status of a specific cryptocurrency associated with a user wallet.
* Use case for fetching the status of a cryptocurrency associated with a user wallet.
*
* @property currenciesRepository Repository for managing and fetching cryptocurrencies.
* @property quotesRepository Repository for managing and fetching cryptocurrency quotes.
* @property networksRepository Repository for managing and fetching information related to blockchain networks.
* @property dispatchers Provides coroutine dispatchers.
*/
class GetCurrencyUseCase(
class GetCurrencyStatusUpdatesUseCase(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
@ -33,30 +32,26 @@ class GetCurrencyUseCase(
*
* @param userWalletId The unique identifier of the user's wallet.
* @param currencyId The unique identifier of the cryptocurrency.
* @param refresh A boolean flag indicating whether the data should be refreshed.
* @return A [Flow] emitting either a [CurrencyError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
*/
operator fun invoke(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
refresh: Boolean = false,
): Flow<Either<CurrencyError, CryptoCurrencyStatus>> {
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return flow {
emitAll(getCurrency(userWalletId, currencyId, refresh))
emitAll(getCurrency(userWalletId, currencyId))
}.flowOn(dispatchers.io)
}
private suspend fun getCurrency(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
refresh: Boolean,
): Flow<Either<CurrencyError, CryptoCurrencyStatus>> {
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
userWalletId = userWalletId,
refresh = refresh,
)
return operations.getCurrencyStatusFlow(currencyId).map { maybeCurrency ->

View file

@ -1,7 +1,7 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.*
* @property networksRepository Repository for managing and fetching information related to blockchain networks.
* @property dispatchers Provides coroutine dispatchers.
*/
class GetPrimaryCurrencyUseCase(
class GetPrimaryCurrencyStatusUpdatesUseCase(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
@ -32,27 +32,22 @@ class GetPrimaryCurrencyUseCase(
*
* @param userWalletId The unique identifier of the user's wallet.
* @param refresh A boolean flag indicating whether the data should be refreshed.
* @return A [Flow] emitting either a [CurrencyError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
*/
operator fun invoke(
userWalletId: UserWalletId,
refresh: Boolean = false,
): Flow<Either<CurrencyError, CryptoCurrencyStatus>> {
operator fun invoke(userWalletId: UserWalletId): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return flow {
emitAll(getPrimaryCurrency(userWalletId, refresh))
emitAll(getPrimaryCurrency(userWalletId))
}.flowOn(dispatchers.io)
}
private suspend fun getPrimaryCurrency(
userWalletId: UserWalletId,
refresh: Boolean,
): Flow<Either<CurrencyError, CryptoCurrencyStatus>> {
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
userWalletId = userWalletId,
refresh = refresh,
)
return operations.getPrimaryCurrencyStatusFlow().map { maybeCurrency ->

View file

@ -14,7 +14,10 @@ import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
class GetTokenListUseCase(
internal val currenciesRepository: CurrenciesRepository,
@ -24,8 +27,8 @@ class GetTokenListUseCase(
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Flow<Either<TokenListError, TokenList>> {
return getTokensStatuses(userWalletId, refresh).flatMapMerge { maybeTokens ->
operator fun invoke(userWalletId: UserWalletId): Flow<Either<TokenListError, TokenList>> {
return getTokensStatuses(userWalletId).flatMapMerge { maybeTokens ->
maybeTokens.fold(
ifLeft = { error ->
flowOf(error.left())
@ -39,11 +42,9 @@ class GetTokenListUseCase(
private fun getTokensStatuses(
userWalletId: UserWalletId,
refresh: Boolean,
): Flow<Either<TokenListError, List<CryptoCurrencyStatus>>> {
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
refresh = refresh,
useCase = this@GetTokenListUseCase,
)

View file

@ -1,8 +0,0 @@
package com.tangem.domain.tokens.error
sealed class CurrencyError {
object UnableToCreateCurrency : CurrencyError()
data class DataError(val cause: Throwable) : CurrencyError()
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.tokens.error
sealed class CurrencyStatusError {
object UnableToCreateCurrency : CurrencyStatusError()
data class DataError(val cause: Throwable) : CurrencyStatusError()
}

View file

@ -1,15 +1,15 @@
package com.tangem.domain.tokens.error.mapper
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencyError {
internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencyStatusError {
return when (this) {
is CurrenciesStatusesOperations.Error.DataError -> CurrencyError.DataError(this.cause)
is CurrenciesStatusesOperations.Error.DataError -> CurrencyStatusError.DataError(this.cause)
is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses,
is CurrenciesStatusesOperations.Error.EmptyQuotes,
is CurrenciesStatusesOperations.Error.EmptyCurrencies,
is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus,
-> CurrencyError.UnableToCreateCurrency
-> CurrencyStatusError.UnableToCreateCurrency
}
}

View file

@ -20,19 +20,16 @@ internal class CurrenciesStatusesOperations(
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val userWalletId: UserWalletId,
private val refresh: Boolean,
) {
constructor(
userWalletId: UserWalletId,
refresh: Boolean,
useCase: GetTokenListUseCase,
) : this(
currenciesRepository = useCase.currenciesRepository,
quotesRepository = useCase.quotesRepository,
networksRepository = useCase.networksRepository,
userWalletId = userWalletId,
refresh = refresh,
)
@OptIn(ExperimentalCoroutinesApi::class)
@ -51,16 +48,16 @@ internal class CurrenciesStatusesOperations(
emit(emptyCurrenciesStatuses.right())
return@transformLatest
} else if (!refresh) {
val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
)
emit(maybeLoadingCurrenciesStatuses)
}
val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
)
emit(maybeLoadingCurrenciesStatuses)
val (networksIds, currenciesIds) = getIds(nonEmptyCurrencies)
val currenciesFlow = combine(
@ -168,7 +165,7 @@ internal class CurrenciesStatusesOperations(
}
private fun getMultiCurrencyWalletCurrencies(): Flow<Either<Error, List<CryptoCurrency>>> {
return currenciesRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh)
return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWalletId)
.map<List<CryptoCurrency>, Either<Error, List<CryptoCurrency>>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyCurrencies.left()) }
@ -188,14 +185,14 @@ internal class CurrenciesStatusesOperations(
}
private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<Error, Set<Quote>>> {
return quotesRepository.getQuotes(tokensIds, refresh)
return quotesRepository.getQuotesUpdates(tokensIds)
.map<Set<Quote>, Either<Error, Set<Quote>>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyQuotes.left()) }
}
private fun getNetworksStatuses(networks: NonEmptySet<Network.ID>): Flow<Either<Error, Set<NetworkStatus>>> {
return networksRepository.getNetworkStatuses(userWalletId, networks, refresh)
return networksRepository.getNetworkStatusesUpdates(userWalletId, networks)
.map<Set<NetworkStatus>, Either<Error, Set<NetworkStatus>>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }

View file

@ -48,19 +48,22 @@ interface CurrenciesRepository {
suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency
/**
* Retrieves the list of cryptocurrencies within a multi-currency wallet.
* Retrieves updates of the list of cryptocurrencies within a multi-currency wallet.
*
* Loads remote cryptocurrencies if they have expired.
*
* @param userWalletId The unique identifier of the user wallet.
* @param refresh A boolean flag indicating whether the data should be refreshed.
* @return A [Flow] emitting the set of cryptocurrencies associated with the user wallet.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
fun getMultiCurrencyWalletCurrencies(userWalletId: UserWalletId, refresh: Boolean): Flow<List<CryptoCurrency>>
fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>>
/**
* Retrieves the list of cryptocurrencies within a multi-currency wallet.
*
* Loads cryptocurrencies if they have expired or if [refresh] is `true`.
*
* @param userWalletId The unique identifier of the user wallet.
* @param refresh A boolean flag indicating whether the data should be refreshed.
* @return A list of [CryptoCurrency].

View file

@ -19,16 +19,29 @@ interface NetworksRepository {
fun getNetworks(networksIds: Set<Network.ID>): Set<Network>
/**
* Retrieves the statuses of specified blockchain networks for a specific user wallet.
* Retrieves updates of network statuses of specified blockchain networks for a specific user wallet.
*
* Loads remote network statuses if they have expired.
*
* @param userWalletId The unique identifier of the user wallet.
* @param networks A set of network IDs which statuses are to be retrieved.
* @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks.
*/
fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set<Network.ID>): Flow<Set<NetworkStatus>>
/**
* Retrieves network statuses of specified blockchain networks for a specific user wallet.
*
* Loads remote network statuses if they have expired or if [refresh] is `true`.
*
* @param userWalletId The unique identifier of the user wallet.
* @param networks A set of network IDs which statuses are to be retrieved.
* @param refresh A boolean flag indicating whether the data should be refreshed.
* @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks.
*/
fun getNetworkStatuses(
suspend fun getNetworkStatusesSync(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
refresh: Boolean,
): Flow<Set<NetworkStatus>>
): Set<NetworkStatus>
}

View file

@ -10,11 +10,23 @@ import kotlinx.coroutines.flow.Flow
interface QuotesRepository {
/**
* Retrieves the quotes for a set of specified cryptocurrencies, identified by their unique IDs.
* Retrieves updates of quotes for a set of specified cryptocurrencies, identified by their unique IDs.
*
* Loads remote quotes if they have expired.
*
* @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved.
* @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies.
*/
fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>>
/**
* Retrieves quotes for a set of specified cryptocurrencies, identified by their unique IDs.
*
* Loads remote quotes if they have expired or if [refresh] is `true`.
*
* @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved.
* @param refresh A boolean flag indicating whether the data should be refreshed.
* @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies.
*/
fun getQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>>
suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote>
}

View file

@ -4,7 +4,7 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.error.DataError
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.mock.MockNetworks
import com.tangem.domain.tokens.mock.MockQuotes
import com.tangem.domain.tokens.mock.MockTokens
@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
internal class GetPrimaryCurrencyUseCaseTest {
internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
private val dispatchers = TestingCoroutineDispatcherProvider()
private val userWalletId = UserWalletId(value = null)
@ -47,7 +47,7 @@ internal class GetPrimaryCurrencyUseCaseTest {
@Test
fun `when token getting failed then error should be received`() = runTest {
// Given
val expectedResult = CurrencyError.DataError(DataError.NetworkError.NoInternetConnection).left()
val expectedResult = CurrencyStatusError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(token = DataError.NetworkError.NoInternetConnection.left())
@ -75,7 +75,7 @@ internal class GetPrimaryCurrencyUseCaseTest {
@Test
fun `when networks statuses getting failed then error should be received`() = runTest {
// Given
val expectedResult = CurrencyError.DataError(DataError.NetworkError.NoInternetConnection).left()
val expectedResult = CurrencyStatusError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left()))
@ -88,7 +88,7 @@ internal class GetPrimaryCurrencyUseCaseTest {
@Test
fun `when networks statuses flow is empty then error should be received`() = runTest {
val expectedResult = CurrencyError.UnableToCreateCurrency.left()
val expectedResult = CurrencyStatusError.UnableToCreateCurrency.left()
val useCase = getUseCase(statuses = flowOf())
@ -153,7 +153,7 @@ internal class GetPrimaryCurrencyUseCaseTest {
removeCurrencyResult: Either<DataError, Unit> = Unit.right(),
quotes: Flow<Either<DataError, Set<Quote>>> = flowOf(MockQuotes.quotes.right()),
statuses: Flow<Either<DataError, Set<NetworkStatus>>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()),
) = GetPrimaryCurrencyUseCase(
) = GetPrimaryCurrencyStatusUpdatesUseCase(
dispatchers = dispatchers,
currenciesRepository = MockCurrenciesRepository(
sortTokensResult = Unit.right(),

View file

@ -51,27 +51,6 @@ internal class GetTokenListUseCaseTest {
assertEquals(expectedResult, result)
}
@Test
fun `when list refreshed then correct token list should be returned`() = runTest {
// Given
val expectedResult = listOf(
MockTokenLists.failedUngroupedTokenList.right(),
)
val useCase = getUseCase(
isGrouped = flowOf(false.right()),
isSortedByBalance = flowOf(false.right()),
)
// When
val result = useCase(userWalletId, refresh = true)
.take(count = 1)
.toList()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when tokens getting failed then error should be received`() = runTest {
// Given
@ -108,20 +87,6 @@ internal class GetTokenListUseCaseTest {
assertEquals(expectedResult, result)
}
@Test
fun `when networks statuses getting failed then error should be received`() = runTest {
// Given
val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase(userWalletId, refresh = true).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when grouping type getting failed then error should be received`() = runTest {
// Given

View file

@ -55,10 +55,7 @@ internal class MockCurrenciesRepository(
return token.getOrElse { e -> throw e }
}
override fun getMultiCurrencyWalletCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): Flow<List<CryptoCurrency>> {
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return tokens.map { it.getOrElse { e -> throw e } }
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
internal class MockNetworksRepository(
@ -18,11 +19,18 @@ internal class MockNetworksRepository(
return networks.getOrElse { throw it }
}
override fun getNetworkStatuses(
override fun getNetworkStatusesUpdates(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
refresh: Boolean,
): Flow<Set<NetworkStatus>> {
return statuses.map { it.getOrElse { e -> throw e } }
}
override suspend fun getNetworkStatusesSync(
userWalletId: UserWalletId,
networks: Set<Network.ID>,
refresh: Boolean,
): Set<NetworkStatus> {
return getNetworkStatusesUpdates(userWalletId, networks).first()
}
}

View file

@ -6,13 +6,18 @@ import com.tangem.domain.core.error.DataError
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Quote
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
internal class MockQuotesRepository(
private val quotes: Flow<Either<DataError, Set<Quote>>>,
) : QuotesRepository {
override fun getQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>> {
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
return quotes.map { it.getOrElse { e -> throw e } }
}
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
return getQuotesUpdates(currenciesIds).first()
}
}

View file

@ -6,7 +6,7 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
@ -16,9 +16,9 @@ import java.math.BigDecimal
internal class TokenDetailsLoadedBalanceConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<Either<CurrencyError, CryptoCurrencyStatus>, TokenDetailsState> {
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, TokenDetailsState> {
override fun convert(value: Either<CurrencyError, CryptoCurrencyStatus>): TokenDetailsState {
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): TokenDetailsState {
return value.fold(ifLeft = { convertError() }, ifRight = ::convert)
}

View file

@ -4,7 +4,7 @@ import androidx.paging.PagingData
import arrow.core.Either
import com.tangem.common.Provider
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.models.CryptoCurrency
@ -63,7 +63,7 @@ internal class TokenDetailsStateFactory(
}
fun getCurrencyLoadedBalanceState(
cryptoCurrencyEither: Either<CurrencyError, CryptoCurrencyStatus>,
cryptoCurrencyEither: Either<CurrencyStatusError, CryptoCurrencyStatus>,
): TokenDetailsState {
return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither)
}

View file

@ -11,7 +11,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetCurrencyUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.models.CryptoCurrency
@ -39,7 +39,7 @@ import kotlin.properties.Delegates
internal class TokenDetailsViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val getCurrencyUseCase: GetCurrencyUseCase,
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
@ -71,7 +71,7 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onCreate(owner: LifecycleOwner) {
getWallet()
updateContent(selectedWallet = wallet, refresh = false)
updateContent(selectedWallet = wallet)
}
private fun getWallet() {
@ -82,8 +82,8 @@ internal class TokenDetailsViewModel @Inject constructor(
)
}
private fun updateContent(selectedWallet: UserWallet, refresh: Boolean) {
updateMarketPrice(selectedWallet = selectedWallet, refresh = refresh)
private fun updateContent(selectedWallet: UserWallet) {
updateMarketPrice(selectedWallet = selectedWallet)
updateButtons(userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id.value)
updateTxHistory()
}
@ -96,8 +96,11 @@ internal class TokenDetailsViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private fun updateMarketPrice(selectedWallet: UserWallet, refresh: Boolean) {
getCurrencyUseCase(userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id, refresh = refresh)
private fun updateMarketPrice(selectedWallet: UserWallet) {
getCurrencyStatusUpdatesUseCase(
userWalletId = selectedWallet.walletId,
currencyId = cryptoCurrency.id,
)
.distinctUntilChanged()
.onEach { either ->
uiState = stateFactory.getCurrencyLoadedBalanceState(either)

View file

@ -7,7 +7,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeConfig
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
@ -170,7 +170,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
}
data class SingleCurrencyLoadedBalanceModel(
val cryptoCurrencyEither: Either<CurrencyError, CryptoCurrencyStatus>,
val cryptoCurrencyEither: Either<CurrencyStatusError, CryptoCurrencyStatus>,
val isRefreshing: Boolean,
)
}

View file

@ -5,7 +5,7 @@ import arrow.core.Either
import com.tangem.common.Provider
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenActionsState
@ -199,7 +199,7 @@ internal class WalletStateFactory(
fun getLockedState(): WalletState = lockedConverter.convert(Unit)
fun getSingleCurrencyLoadedBalanceState(
cryptoCurrencyEither: Either<CurrencyError, CryptoCurrencyStatus>,
cryptoCurrencyEither: Either<CurrencyStatusError, CryptoCurrencyStatus>,
isRefreshing: Boolean,
): WalletState {
return singleCurrencyLoadedBalanceConverter.convert(

View file

@ -22,7 +22,7 @@ import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase
import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -73,7 +73,7 @@ internal class WalletViewModel @Inject constructor(
private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase,
private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase,
private val getTokenListUseCase: GetTokenListUseCase,
private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyUseCase,
private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getCardWasScannedUseCase: GetCardWasScannedUseCase,
private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
@ -180,7 +180,7 @@ internal class WalletViewModel @Inject constructor(
"Impossible to update tokens list if state isn't WalletMultiCurrencyState"
}
getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[index].id, refresh = isRefreshing)
getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[index].id)
.distinctUntilChanged()
.onEach { tokenListEither ->
uiState = stateFactory.getStateByTokensList(