Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-07 13:01:47 +03:00
parent dd186d6fbc
commit 3f5f3ef68b
23 changed files with 351 additions and 128 deletions

View file

@ -1,9 +1,9 @@
package com.tangem.tap.di.domain
import com.tangem.domain.tokens.*
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.tokens.repository.TokensRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -18,23 +18,23 @@ internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideGetTokenListUseCase(
tokensRepository: TokensRepository,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetTokenListUseCase {
return GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers)
return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideGetPrimaryCurrencyUseCase(
tokensRepository: TokensRepository,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetPrimaryCurrencyUseCase {
return GetPrimaryCurrencyUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers)
): GetCurrencyUseCase {
return GetCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
}
@Provides
@ -55,9 +55,9 @@ internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideApplyTokenListSortingUseCase(
tokensRepository: TokensRepository,
currenciesRepository: CurrenciesRepository,
dispatchers: CoroutineDispatcherProvider,
): ApplyTokenListSortingUseCase {
return ApplyTokenListSortingUseCase(tokensRepository, dispatchers)
return ApplyTokenListSortingUseCase(currenciesRepository, dispatchers)
}
}

View file

@ -1,15 +1,15 @@
package com.tangem.data.tokens.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
import com.tangem.data.tokens.repository.DefaultNetworksRepository
import com.tangem.data.tokens.repository.DefaultTokensRepository
import com.tangem.data.tokens.repository.MockQuotesRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
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.tokens.repository.TokensRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -24,14 +24,14 @@ internal object TokensDataModule {
@Provides
@Singleton
fun provideTokensRepository(
fun provideCurrenciesRepository(
tangemTechApi: TangemTechApi,
userTokensStore: UserTokensStore,
userWalletsStore: UserWalletsStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
): TokensRepository {
return DefaultTokensRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers)
): CurrenciesRepository {
return DefaultCurrenciesRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers)
}
@Provides

View file

@ -8,23 +8,27 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.TokensRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultTokensRepository(
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
private val userTokensStore: UserTokensStore,
private val userWalletsStore: UserWalletsStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
) : TokensRepository {
) : CurrenciesRepository {
private val demoConfig = DemoConfig()
private val responseCurrenciesFactory = ResponseCurrenciesFactory(demoConfig)
@ -37,6 +41,8 @@ internal class DefaultTokensRepository(
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
) = withContext(dispatchers.io) {
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
val response = userTokensResponseFactory.createUserTokensResponse(
currencies = currencies,
isGroupedByNetwork = isGroupedByNetwork,
@ -46,58 +52,72 @@ internal class DefaultTokensRepository(
storeAndPushTokens(userWalletId, response)
}
override suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
val userWallet = withContext(dispatchers.io) {
requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"
}
}
require(!userWallet.isMultiCurrency) {
"Single currency wallet excepted, but multi currency wallet was found: $userWalletId"
}
override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
}
}
override fun getMultiCurrencyWalletCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): Flow<Set<CryptoCurrency>> {
): Flow<Set<CryptoCurrency>> = channelFlow {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
getMultiCurrencyWalletCurrencies(userWallet).collect(::send)
}
launch(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh)
}
}
override suspend fun getMultiCurrencyWalletCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency = withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse.card)
}
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
return channelFlow {
val userWallet = withContext(dispatchers.io) {
requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"
}
}
require(userWallet.isMultiCurrency) {
"Multi currency wallet excepted, but single currency wallet was found: $userWalletId"
}
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
getMultiCurrencyWalletCurrencies(userWallet).collectLatest(::send)
}
launch(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh)
userTokensStore.get(userWalletId)
.map { it.group == UserTokensResponse.GroupType.NETWORK }
.collect(::send)
}
}
}
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
return userTokensStore.get(userWalletId)
.map { it.group == UserTokensResponse.GroupType.NETWORK }
.flowOn(dispatchers.io)
}
override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean> {
return userTokensStore.get(userWalletId)
.map { it.sort == UserTokensResponse.SortType.BALANCE }
.flowOn(dispatchers.io)
return channelFlow {
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
userTokensStore.get(userWalletId)
.map { it.sort == UserTokensResponse.SortType.BALANCE }
.collect(::send)
}
}
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<Set<CryptoCurrency>> {
return userTokensStore.get(userWallet.walletId).map { storedTokens ->
responseCurrenciesFactory.createTokens(
responseCurrenciesFactory.createCurrencies(
response = storedTokens,
card = userWallet.scanResponse.card,
)
@ -146,6 +166,39 @@ internal class DefaultTokensRepository(
}
}
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"
}
}
private suspend fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected)
}
private fun ensureIsCorrectUserWallet(userWallet: UserWallet, isMultiCurrencyWalletExpected: Boolean) {
val userWalletId = userWallet.walletId
val message = when {
!userWallet.isMultiCurrency && isMultiCurrencyWalletExpected -> {
"Multi currency wallet expected, but single currency wallet was found: $userWalletId"
}
userWallet.isMultiCurrency && !isMultiCurrencyWalletExpected -> {
"Single currency wallet expected, but multi currency wallet was found: $userWalletId"
}
else -> null
}
if (message != null) {
val error = DataError.UserWalletError.WrongUserWallet(message)
Timber.e(error)
throw error
}
}
private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}"
private companion object {

View file

@ -18,7 +18,10 @@ import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
internal class DefaultNetworksRepository(
@ -44,7 +47,9 @@ internal class DefaultNetworksRepository(
networks: Set<Network.ID>,
refresh: Boolean,
): Flow<Set<NetworkStatus>> = channelFlow {
networksStatuses.collectLatest(::send)
launch(dispatchers.io) {
networksStatuses.collect(::send)
}
launch(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
@ -99,7 +104,7 @@ internal class DefaultNetworksRepository(
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
return responseCurrenciesFactory.createTokens(response, userWallet.scanResponse.card)
return responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse.card)
}
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId"

View file

@ -40,7 +40,7 @@ internal class NetworkStatusFactory {
is CryptoCurrency.Coin -> amounts.singleOrNull { it is CryptoCurrencyAmount.Coin }
is CryptoCurrency.Token -> amounts.singleOrNull {
it is CryptoCurrencyAmount.Token &&
it.id == getTokenIdString(currency) &&
it.id == getTokenIdString(currency.id) &&
it.tokenContractAddress == currency.contractAddress
}
}?.value

View file

@ -12,11 +12,23 @@ import com.tangem.blockchain.common.Token as SdkToken
internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
fun createTokens(response: UserTokensResponse, card: CardDTO): Set<CryptoCurrency> {
return response.tokens.mapNotNull { createToken(it, card) }.toSet()
fun createCurrency(currencyId: CryptoCurrency.ID, response: UserTokensResponse, card: CardDTO): CryptoCurrency {
val responseTokenId = getTokenIdString(currencyId)
val token = requireNotNull(response.tokens.firstOrNull { it.id == responseTokenId }) {
"Unable find a token with provided ID: $responseTokenId"
}
return requireNotNull(createCurrency(token, card)) {
"Unable to create a currency with provided ID: $currencyId"
}
}
private fun createToken(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? {
fun createCurrencies(response: UserTokensResponse, card: CardDTO): Set<CryptoCurrency> {
return response.tokens.mapNotNull { createCurrency(it, card) }.toSet()
}
private fun createCurrency(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? {
var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
if (blockchain == null || blockchain == Blockchain.Unknown) {
Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")

View file

@ -49,9 +49,14 @@ internal fun getTokenId(blockchain: Blockchain, token: SdkToken): CryptoCurrency
return getTokenOrCoinId(blockchain, token)
}
internal fun getTokenIdString(currency: CryptoCurrency): String? {
return currency.id.value.substringAfter(TOKEN_ID_DELIMITER)
.takeUnless { currency is CryptoCurrency.Token && currency.isCustom }
internal fun getTokenIdString(currencyId: CryptoCurrency.ID): String? {
val idValue = currencyId.value
return if (idValue.startsWith(CUSTOM_TOKEN_ID_PREFIX)) {
null
} else {
idValue.substringAfter(TOKEN_ID_DELIMITER)
}
}
internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {

View file

@ -30,7 +30,7 @@ internal class UserTokensResponseFactory {
val blockchain = getBlockchain(currency.networkId)
return UserTokensResponse.Token(
id = getTokenIdString(currency),
id = getTokenIdString(currency.id),
networkId = blockchain.toNetworkId(),
derivationPath = currency.derivationPath,
name = currency.name,

View file

@ -6,4 +6,9 @@ sealed class DataError : Exception() {
object NoInternetConnection : NetworkError()
}
sealed class UserWalletError : DataError() {
data class WrongUserWallet(override val message: String) : UserWalletError()
}
}

View file

@ -9,14 +9,14 @@ import arrow.core.toNonEmptySetOrNull
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.TokensRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
class ApplyTokenListSortingUseCase(
private val tokensRepository: TokensRepository,
private val currenciesRepository: CurrenciesRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -67,7 +67,9 @@ class ApplyTokenListSortingUseCase(
private suspend fun Raise<TokenListSortingError>.getCurrencies(userWalletId: UserWalletId): Set<CryptoCurrency> {
val tokens = catch(
block = { tokensRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh = false).firstOrNull() },
block = {
currenciesRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh = false).firstOrNull()
},
catch = { raise(TokenListSortingError.DataError(it)) },
)
@ -83,7 +85,7 @@ class ApplyTokenListSortingUseCase(
isSortedByBalance: Boolean,
) = withContext(dispatchers.io) {
catch(
block = { tokensRepository.saveTokens(userWalletId, tokens, isGrouped, isSortedByBalance) },
block = { currenciesRepository.saveTokens(userWalletId, tokens, isGrouped, isSortedByBalance) },
catch = { raise(TokenListSortingError.DataError(it)) },
)
}

View file

@ -0,0 +1,82 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.Raise
import arrow.core.raise.recover
import arrow.core.right
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.error.mapper.mapToTokenError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
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 com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
/**
* Use case for fetching the status of a specific 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(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
/**
* Invokes the use case.
*
* @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.
*/
operator fun invoke(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
refresh: Boolean = false,
): Flow<Either<CurrencyError, CryptoCurrencyStatus>> {
return channelFlow {
recover(
block = {
getCurrency(userWalletId, currencyId, refresh).collectLatest { currencyStatus ->
send(currencyStatus.right())
}
},
recover = { error ->
send(error.left())
},
)
}
}
private suspend fun Raise<CurrencyError>.getCurrency(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
refresh: Boolean,
): Flow<CryptoCurrencyStatus> {
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
userWalletId = userWalletId,
refresh = refresh,
dispatchers = dispatchers,
raise = this,
transformError = CurrenciesStatusesOperations.Error::mapToTokenError,
)
return operations.getCurrencyStatusFlow(currencyId)
}
}

View file

@ -5,35 +5,50 @@ import arrow.core.left
import arrow.core.raise.Raise
import arrow.core.raise.recover
import arrow.core.right
import com.tangem.domain.tokens.error.TokenError
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.error.mapper.mapToTokenError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
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.tokens.repository.TokensRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
/**
* Use case for fetching the status of the primary 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 GetPrimaryCurrencyUseCase(
private val tokensRepository: TokensRepository,
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
/**
* Invokes the use case.
*
* @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.
*/
operator fun invoke(
userWalletId: UserWalletId,
refresh: Boolean = false,
): Flow<Either<TokenError, CryptoCurrencyStatus>> {
): Flow<Either<CurrencyError, CryptoCurrencyStatus>> {
return channelFlow {
recover(
block = {
getToken(userWalletId, refresh).collectLatest { token ->
send(token.right())
getCurrency(userWalletId, refresh).collectLatest { currencyStatus ->
send(currencyStatus.right())
}
},
recover = { error ->
@ -43,12 +58,12 @@ class GetPrimaryCurrencyUseCase(
}
}
private suspend fun Raise<TokenError>.getToken(
private suspend fun Raise<CurrencyError>.getCurrency(
userWalletId: UserWalletId,
refresh: Boolean,
): Flow<CryptoCurrencyStatus> {
val operations = CurrenciesStatusesOperations(
tokensRepository = tokensRepository,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
userWalletId = userWalletId,

View file

@ -11,9 +11,9 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.TokenListOperations
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.tokens.repository.TokensRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flatMapConcat
class GetTokenListUseCase(
internal val tokensRepository: TokensRepository,
internal val currenciesRepository: CurrenciesRepository,
internal val quotesRepository: QuotesRepository,
internal val networksRepository: NetworksRepository,
internal val dispatchers: CoroutineDispatcherProvider,
@ -60,7 +60,7 @@ class GetTokenListUseCase(
transformError = CurrenciesStatusesOperations.Error::mapToTokenListError,
)
return operations.getMultiCurrencyWalletStatusesFlow()
return operations.getCurrenciesStatusesFlow()
}
private fun Raise<TokenListError>.createTokenList(

View file

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

View file

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

View file

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

View file

@ -7,9 +7,9 @@ import com.tangem.domain.core.raise.DelegatedRaise
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.model.*
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.tokens.repository.TokensRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
@ -17,7 +17,7 @@ import kotlinx.coroutines.withContext
@Suppress("LongParameterList")
internal class CurrenciesStatusesOperations<E>(
private val tokensRepository: TokensRepository,
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val userWalletId: UserWalletId,
@ -34,7 +34,7 @@ internal class CurrenciesStatusesOperations<E>(
raise: Raise<E>,
transformError: (Error) -> E,
) : this(
tokensRepository = useCase.tokensRepository,
currenciesRepository = useCase.currenciesRepository,
quotesRepository = useCase.quotesRepository,
networksRepository = useCase.networksRepository,
userWalletId = userWalletId,
@ -44,7 +44,7 @@ internal class CurrenciesStatusesOperations<E>(
transformError = transformError,
)
fun getMultiCurrencyWalletStatusesFlow(): Flow<Set<CryptoCurrencyStatus>> {
fun getCurrenciesStatusesFlow(): Flow<Set<CryptoCurrencyStatus>> {
return getMultiCurrencyWalletCurrencies().flatMapConcat {
val currencies = it.toNonEmptySetOrNull()
@ -68,9 +68,19 @@ internal class CurrenciesStatusesOperations<E>(
}
}
suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow<CryptoCurrencyStatus> {
val currency = getMultiCurrencyWalletCurrency(currencyId)
return getCurrencyStatusFlow(currency)
}
suspend fun getPrimaryCurrencyStatusFlow(): Flow<CryptoCurrencyStatus> {
val currency = getPrimaryCurrency()
return getCurrencyStatusFlow(currency)
}
private fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow<CryptoCurrencyStatus> {
val quoteFlow = getQuotes(nonEmptySetOf(currency.id))
.map { quotes ->
quotes.singleOrNull { it.currencyId == currency.id }
@ -116,34 +126,36 @@ internal class CurrenciesStatusesOperations<E>(
return currencyStatusOperations.createTokenStatus()
}
private suspend fun getMultiCurrencyWalletCurrency(currencyId: CryptoCurrency.ID): CryptoCurrency {
return catch(
block = { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, currencyId) },
catch = { raise(Error.DataError(it)) },
)
}
private fun getMultiCurrencyWalletCurrencies(): Flow<Set<CryptoCurrency>> {
return tokensRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh)
return currenciesRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh)
.catch { raise(Error.DataError(it)) }
.onEmpty { raise(Error.EmptyCurrencies) }
.flowOn(dispatchers.io)
}
private suspend fun getPrimaryCurrency(): CryptoCurrency {
return withContext(dispatchers.io) {
catch(
block = { tokensRepository.getPrimaryCurrency(userWalletId) },
catch = { raise(Error.DataError(it)) },
)
}
return catch(
block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) },
catch = { raise(Error.DataError(it)) },
)
}
private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Set<Quote>> {
return quotesRepository.getQuotes(tokensIds, refresh)
.catch { raise(Error.DataError(it)) }
.onEmpty { raise(Error.EmptyQuotes) }
.flowOn(dispatchers.io)
}
private fun getNetworksStatues(networks: NonEmptySet<Network.ID>): Flow<Set<NetworkStatus>> {
return networksRepository.getNetworkStatuses(userWalletId, networks, refresh)
.catch { raise(Error.DataError(it)) }
.onEmpty { raise(Error.EmptyNetworksStatuses) }
.flowOn(dispatchers.io)
}
sealed class Error {

View file

@ -10,8 +10,8 @@ import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
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.TokensRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
@ -19,7 +19,7 @@ import kotlinx.coroutines.withContext
@Suppress("LongParameterList")
internal class TokenListOperations<E>(
private val tokensRepository: TokensRepository,
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val userWalletId: UserWalletId,
private val tokens: Set<CryptoCurrencyStatus>,
@ -35,7 +35,7 @@ internal class TokenListOperations<E>(
raise: Raise<E>,
transform: (Error) -> E,
) : this(
tokensRepository = useCase.tokensRepository,
currenciesRepository = useCase.currenciesRepository,
networksRepository = useCase.networksRepository,
userWalletId = userWalletId,
tokens = tokens,
@ -149,14 +149,14 @@ internal class TokenListOperations<E>(
}
private fun getIsGrouped(): Flow<Boolean> {
return tokensRepository.isTokensGrouped(userWalletId)
return currenciesRepository.isTokensGrouped(userWalletId)
.catch { raise(Error.DataError(it)) }
.onEmpty { emit(value = false) }
.flowOn(dispatchers.io)
}
private fun getIsSortedByBalance(): Flow<Boolean> {
return tokensRepository.isTokensSortedByBalance(userWalletId)
return currenciesRepository.isTokensSortedByBalance(userWalletId)
.catch { raise(Error.DataError(it)) }
.onEmpty { emit(value = false) }
.flowOn(dispatchers.io)

View file

@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.Flow
/**
* Repository for everything related to the tokens of user wallet
* */
interface TokensRepository {
interface CurrenciesRepository {
/**
* Saves the given set of cryptocurrencies, along with the preferences for grouping and sorting, for a specific
@ -17,6 +17,8 @@ interface TokensRepository {
* @param currencies The set of cryptocurrencies to be saved.
* @param isGroupedByNetwork A boolean flag indicating whether the tokens should be grouped by network.
* @param isSortedByBalance A boolean flag indicating whether the tokens should be sorted by balance.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun saveTokens(
userWalletId: UserWalletId,
@ -30,8 +32,10 @@ interface TokensRepository {
*
* @param userWalletId The unique identifier of the user wallet.
* @return The primary cryptocurrency associated with the user wallet.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* ID provided.
*/
suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency
suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency
/**
* Retrieves the set of cryptocurrencies within a multi-currency wallet.
@ -39,14 +43,29 @@ interface TokensRepository {
* @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<Set<CryptoCurrency>>
/**
* Retrieves the cryptocurrency for a specific multi-currency user wallet.
*
* @param userWalletId The unique identifier of the user wallet.
* @param id The unique identifier of the cryptocurrency to be retrieved.
* @return The cryptocurrency associated with the user wallet and ID.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency
/**
* Determines whether the tokens within a specific multi-currency user wallet are grouped.
*
* @param userWalletId The unique identifier of the user wallet.
* @return A [Flow] emitting a boolean value indicating whether the tokens are grouped.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean>
@ -55,6 +74,8 @@ interface TokensRepository {
*
* @param userWalletId The unique identifier of the user wallet.
* @return A [Flow] emitting a boolean value indicating whether the tokens are sorted by balance.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean>
}

View file

@ -7,7 +7,7 @@ import com.tangem.domain.core.error.DataError
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.mock.MockTokens
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.MockTokensRepository
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import junit.framework.TestCase.assertEquals
@ -183,16 +183,16 @@ internal class ApplyTokenListSortingUseCaseTest {
.sortedBy { Random.nextInt(0, MockTokens.tokens.size) }
.toSet()
private fun getUseCase(tokensRepository: MockTokensRepository = getTokensRepository()) =
private fun getUseCase(tokensRepository: MockCurrenciesRepository = getTokensRepository()) =
ApplyTokenListSortingUseCase(
tokensRepository = tokensRepository,
currenciesRepository = tokensRepository,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private fun getTokensRepository(
sortTokensResult: Either<DataError, Unit> = Unit.right(),
tokens: Flow<Either<DataError, Set<CryptoCurrency>>> = flowOf(MockTokens.tokens.right()),
): MockTokensRepository {
return MockTokensRepository(sortTokensResult, MockTokens.token1.right(), tokens, emptyFlow(), emptyFlow())
): MockCurrenciesRepository {
return MockCurrenciesRepository(sortTokensResult, MockTokens.token1.right(), tokens, emptyFlow(), emptyFlow())
}
}

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.TokenError
import com.tangem.domain.tokens.error.CurrencyError
import com.tangem.domain.tokens.mock.MockNetworks
import com.tangem.domain.tokens.mock.MockQuotes
import com.tangem.domain.tokens.mock.MockTokens
@ -13,9 +13,9 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.tokens.repository.MockNetworksRepository
import com.tangem.domain.tokens.repository.MockQuotesRepository
import com.tangem.domain.tokens.repository.MockTokensRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import junit.framework.TestCase.assertEquals
@ -47,7 +47,7 @@ internal class GetPrimaryCurrencyUseCaseTest {
@Test
fun `when token getting failed then error should be received`() = runTest {
// Given
val expectedResult = TokenError.DataError(DataError.NetworkError.NoInternetConnection).left()
val expectedResult = CurrencyError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(token = DataError.NetworkError.NoInternetConnection.left())
@ -61,7 +61,7 @@ internal class GetPrimaryCurrencyUseCaseTest {
@Test
fun `when quotes getting failed then error should be received`() = runTest {
// Given
val expectedResult = TokenError.DataError(DataError.NetworkError.NoInternetConnection).left()
val expectedResult = CurrencyError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(quotes = flowOf(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 = TokenError.DataError(DataError.NetworkError.NoInternetConnection).left()
val expectedResult = CurrencyError.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 = TokenError.UnableToCreateToken.left()
val expectedResult = CurrencyError.UnableToCreateCurrency.left()
val useCase = getUseCase(statuses = flowOf())
@ -101,7 +101,7 @@ internal class GetPrimaryCurrencyUseCaseTest {
@Test
fun `when quotes flow is empty then error should be received`() = runTest {
val expectedResult = TokenError.UnableToCreateToken.left()
val expectedResult = CurrencyError.UnableToCreateCurrency.left()
val useCase = getUseCase(quotes = flowOf())
@ -154,7 +154,7 @@ internal class GetPrimaryCurrencyUseCaseTest {
statuses: Flow<Either<DataError, Set<NetworkStatus>>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()),
) = GetPrimaryCurrencyUseCase(
dispatchers = dispatchers,
tokensRepository = MockTokensRepository(
currenciesRepository = MockCurrenciesRepository(
sortTokensResult = Unit.right(),
token = token,
tokens = flowOf(),

View file

@ -13,9 +13,9 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.tokens.repository.MockNetworksRepository
import com.tangem.domain.tokens.repository.MockQuotesRepository
import com.tangem.domain.tokens.repository.MockTokensRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import junit.framework.TestCase.assertEquals
@ -326,7 +326,7 @@ internal class GetTokenListUseCaseTest {
isSortedByBalance: Flow<Either<DataError, Boolean>> = flowOf(MockTokenLists.isSortedByBalance.right()),
) = GetTokenListUseCase(
dispatchers = dispatchers,
tokensRepository = MockTokensRepository(
currenciesRepository = MockCurrenciesRepository(
sortTokensResult = Unit.right(),
token = MockTokens.token1.right(),
tokens = tokens,

View file

@ -8,13 +8,13 @@ import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
internal class MockTokensRepository(
internal class MockCurrenciesRepository(
private val sortTokensResult: Either<DataError, Unit>,
private val token: Either<DataError, CryptoCurrency>,
private val tokens: Flow<Either<DataError, Set<CryptoCurrency>>>,
private val isGrouped: Flow<Either<DataError, Boolean>>,
private val isSortedByBalance: Flow<Either<DataError, Boolean>>,
) : TokensRepository {
) : CurrenciesRepository {
var tokensIdsAfterSortingApply: Set<CryptoCurrency>? = null
private set
@ -38,7 +38,7 @@ internal class MockTokensRepository(
isTokensSortedByBalanceAfterSortingApply = isSortedByBalance
}
override suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
return token.getOrElse { e -> throw e }
}
@ -49,6 +49,17 @@ internal class MockTokensRepository(
return tokens.map { it.getOrElse { e -> throw e } }
}
override suspend fun getMultiCurrencyWalletCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency {
val token = token.getOrElse { e -> throw e }
require(token.id == id)
return token
}
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
return isGrouped.map { it.getOrElse { e -> throw e } }
}