Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-28 18:28:34 +03:00
parent f2ab3a948d
commit 171732a41a
14 changed files with 162 additions and 45 deletions

View file

@ -26,6 +26,17 @@ internal object TokensDomainModule {
return GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideGetTokenUseCase(
tokensRepository: TokensRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetTokenUseCase {
return GetTokenUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideToggleTokenListGroupingUseCase(

View file

@ -46,14 +46,31 @@ internal class DefaultTokensRepository(
storeAndPushTokens(userWalletId, response)
}
override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>> = channelFlow {
override suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token {
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"
}
return cardTokensFactory
.createPrimaryTokenForSingleCurrencyCard(userWallet.scanResponse)
}
override fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>> {
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"
}
if (userWallet.isMultiCurrency) {
launch(dispatchers.io) {
getMultiCurrencyWalletTokens(userWallet).collectLatest(::send)
}
@ -61,8 +78,6 @@ internal class DefaultTokensRepository(
launch(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh)
}
} else {
send(getSingleCurrencyWalletTokens(userWallet))
}
}
@ -87,22 +102,6 @@ internal class DefaultTokensRepository(
}
}
private suspend fun getSingleCurrencyWalletTokens(userWallet: UserWallet): Set<Token> {
var response = userTokensStore.getSyncOrNull(userWallet.walletId)
if (response == null) {
response = userTokensResponseFactory.createUserTokensResponse(
tokens = cardTokensFactory.createTokensForSingleCurrencyCard(userWallet.scanResponse),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
userTokensStore.store(userWallet.walletId, response)
}
return responseTokensFactory.createTokens(response, userWallet.scanResponse.card)
}
private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) {
cacheRegistry.invokeOnExpire(
key = getTokensCacheKey(userWallet.walletId),

View file

@ -27,7 +27,7 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) {
return blockchains.mapNotNull { createCoin(it, card) }.toSet()
}
fun createTokensForSingleCurrencyCard(scanResponse: ScanResponse): Set<DomainToken> {
fun createPrimaryTokenForSingleCurrencyCard(scanResponse: ScanResponse): DomainToken {
val card = scanResponse.card
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
@ -39,13 +39,7 @@ internal class CardTokensFactory(private val demoConfig: DemoConfig) {
createToken(token, blockchain, card)
}
return buildSet {
add(coin)
if (primaryToken != null) {
add(primaryToken)
}
}
return primaryToken ?: coin
}
private fun createToken(sdkToken: SdkToken, blockchain: Blockchain, card: CardDTO): DomainToken? {

View file

@ -6,11 +6,4 @@ sealed class DataError : Exception() {
object NoInternetConnection : NetworkError()
}
sealed class PersistenceError : DataError() {
data class UnableToReadFile(override val cause: Throwable) : DataError()
data class UnableToWriteFile(override val cause: Throwable) : DataError()
}
}

View file

@ -18,6 +18,7 @@ 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
import kotlinx.coroutines.flow.flatMapConcat
class GetTokenListUseCase(
@ -31,7 +32,7 @@ class GetTokenListUseCase(
return channelFlow {
recover(
block = {
getTokenList(userWalletId, refresh).collect { list ->
getTokenList(userWalletId, refresh).collectLatest { list ->
send(list.right())
}
},
@ -59,7 +60,7 @@ class GetTokenListUseCase(
transformError = TokensStatusesOperations.Error::mapToTokenListError,
)
return operations.getTokensStatusesFlow()
return operations.getMultiCurrencyWalletTokensStatusesFlow()
}
private fun Raise<TokenListError>.createTokenList(

View file

@ -0,0 +1,57 @@
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.TokenError
import com.tangem.domain.tokens.error.mapper.mapToTokenError
import com.tangem.domain.tokens.model.TokenStatus
import com.tangem.domain.tokens.operations.TokensStatusesOperations
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
class GetTokenUseCase(
private val tokensRepository: TokensRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Flow<Either<TokenError, TokenStatus>> {
return channelFlow {
recover(
block = {
getToken(userWalletId, refresh).collectLatest { token ->
send(token.right())
}
},
recover = { error ->
send(error.left())
},
)
}
}
private suspend fun Raise<TokenError>.getToken(userWalletId: UserWalletId, refresh: Boolean): Flow<TokenStatus> {
val operations = TokensStatusesOperations(
tokensRepository = tokensRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
userWalletId = userWalletId,
refresh = refresh,
dispatchers = dispatchers,
raise = this,
transformError = TokensStatusesOperations.Error::mapToTokenError,
)
return operations.getSingleCurrencyWalletTokenStatusFlow()
}
}

View file

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

View file

@ -0,0 +1,14 @@
package com.tangem.domain.tokens.error.mapper
import com.tangem.domain.tokens.error.TokenError
import com.tangem.domain.tokens.operations.TokensStatusesOperations
internal fun TokensStatusesOperations.Error.mapToTokenError(): TokenError {
return when (this) {
is TokensStatusesOperations.Error.DataError -> TokenError.DataError(this.cause)
is TokensStatusesOperations.Error.EmptyNetworksStatuses,
is TokensStatusesOperations.Error.EmptyQuotes,
is TokensStatusesOperations.Error.EmptyTokens,
-> TokenError.UnableToCreateToken
}
}

View file

@ -16,6 +16,7 @@ data class TokenStatus(
sealed class Status {
open val amount: BigDecimal? = null
open val fiatAmount: BigDecimal? = null
open val fiatRate: BigDecimal? = null
open val priceChange: BigDecimal? = null
open val hasTransactionsInProgress: Boolean = false
}
@ -31,6 +32,7 @@ data class TokenStatus(
data class Loaded(
override val amount: BigDecimal,
override val fiatAmount: BigDecimal,
override val fiatRate: BigDecimal,
override val priceChange: BigDecimal,
override val hasTransactionsInProgress: Boolean,
) : Status()
@ -38,6 +40,7 @@ data class TokenStatus(
data class Custom(
override val amount: BigDecimal,
override val fiatAmount: BigDecimal?,
override val fiatRate: BigDecimal?,
override val priceChange: BigDecimal?,
override val hasTransactionsInProgress: Boolean,
) : Status()

View file

@ -48,6 +48,7 @@ internal class TokenStatusOperations(
token.isCustom -> TokenStatus.Custom(
amount = amount,
fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate),
fiatRate = quote?.fiatRate,
priceChange = quote?.priceChange,
hasTransactionsInProgress = hasTransactionsInProgress,
)
@ -55,6 +56,7 @@ internal class TokenStatusOperations(
else -> TokenStatus.Loaded(
amount = amount,
fiatAmount = calculateFiatAmount(amount, quote.fiatRate),
fiatRate = quote.fiatRate,
priceChange = quote.priceChange,
hasTransactionsInProgress = hasTransactionsInProgress,
)

View file

@ -1,8 +1,8 @@
package com.tangem.domain.tokens.operations
import arrow.core.NonEmptySet
import arrow.core.*
import arrow.core.raise.Raise
import arrow.core.toNonEmptySetOrNull
import arrow.core.raise.catch
import com.tangem.domain.core.raise.DelegatedRaise
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.model.*
@ -43,8 +43,8 @@ internal class TokensStatusesOperations<E>(
transformError = transformError,
)
fun getTokensStatusesFlow(): Flow<Set<TokenStatus>> {
return getTokens().flatMapConcat {
fun getMultiCurrencyWalletTokensStatusesFlow(): Flow<Set<TokenStatus>> {
return getMultiCurrencyWalletTokens().flatMapConcat {
val tokens = it.toNonEmptySetOrNull()
if (tokens == null) {
@ -60,6 +60,24 @@ internal class TokensStatusesOperations<E>(
}
}
suspend fun getSingleCurrencyWalletTokenStatusFlow(): Flow<TokenStatus> {
val token = getSingleCurrencyWalletToken()
val quoteFlow = getQuotes(nonEmptySetOf(token.id))
.map { quotes ->
quotes.singleOrNull { it.tokenId == token.id }
}
val statusFlow = getNetworksStatues(groupTokens(nonEmptySetOf(token)))
.map { statuses ->
statuses.singleOrNull { it.networkId == token.networkId }
}
return combine(quoteFlow, statusFlow) { quote, networkStatus ->
createStatus(token, quote, networkStatus)
}
}
private suspend fun createTokensStatuses(
tokens: Set<Token>,
quotes: Set<Quote>,
@ -84,13 +102,22 @@ internal class TokensStatusesOperations<E>(
return tokenStatusOperations.createTokenStatus()
}
private fun getTokens(): Flow<Set<Token>> {
return tokensRepository.getTokens(userWalletId, refresh)
private fun getMultiCurrencyWalletTokens(): Flow<Set<Token>> {
return tokensRepository.getMultiCurrencyWalletTokens(userWalletId, refresh)
.catch { raise(Error.DataError(it)) }
.onEmpty { raise(Error.EmptyTokens) }
.flowOn(dispatchers.io)
}
private suspend fun getSingleCurrencyWalletToken(): Token {
return withContext(dispatchers.io) {
catch(
block = { tokensRepository.getSingleCurrencyWalletToken(userWalletId) },
catch = { raise(Error.DataError(it)) },
)
}
}
private fun getQuotes(tokensIds: NonEmptySet<Token.ID>): Flow<Set<Quote>> {
return quotesRepository.getQuotes(tokensIds, refresh)
.catch { raise(Error.DataError(it)) }

View file

@ -16,7 +16,9 @@ interface TokensRepository {
isSortedByBalance: Boolean,
)
fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>>
suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token
fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>>
fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean>

View file

@ -140,6 +140,7 @@ internal object MockTokensStates {
value = TokenStatus.Loaded(
amount = amount,
fiatAmount = fiatAmount,
fiatRate = quote.fiatRate,
priceChange = quote.priceChange,
hasTransactionsInProgress = false,
),

View file

@ -3,6 +3,7 @@ package com.tangem.domain.tokens.repository
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.core.error.DataError
import com.tangem.domain.tokens.mock.MockTokens
import com.tangem.domain.tokens.model.Token
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -37,7 +38,11 @@ internal class MockTokensRepository(
isTokensSortedByBalanceAfterSortingApply = isSortedByBalance
}
override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>> {
override suspend fun getSingleCurrencyWalletToken(userWalletId: UserWalletId): Token {
return MockTokens.token1
}
override fun getMultiCurrencyWalletTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>> {
return tokens.map { it.getOrElse { e -> throw e } }
}