Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-12 17:43:08 +03:00
commit d54f8452b6
6 changed files with 511 additions and 5 deletions

View file

@ -1,19 +1,24 @@
// TODO: [REDACTED_JIRA]
@file:Suppress("unused", "unused_parameter")
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.TokensError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TokenStatus
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.tokens.utils.TokenListFiatBalanceOperations
import com.tangem.domain.tokens.utils.TokenListOperations
import com.tangem.domain.tokens.utils.TokensStatusesOperations
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.flatMapConcat
class GetTokenListUseCase(
private val tokensRepository: TokensRepository,
@ -23,6 +28,66 @@ class GetTokenListUseCase(
) {
operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = true): Flow<Either<TokensError, TokenList>> {
return flowOf(TokensError.EmptyTokens.left())
return channelFlow {
recover(
block = {
getTokenList(userWalletId, refresh).collect { list ->
send(list.right())
}
},
recover = { error ->
send(error.left())
},
)
}
}
private fun Raise<TokensError>.getTokenList(userWalletId: UserWalletId, refresh: Boolean): Flow<TokenList> {
return getTokensStatuses(userWalletId, refresh).flatMapConcat { tokens ->
createTokenList(userWalletId, tokens)
}
}
private fun Raise<TokensError>.getTokensStatuses(
userWalletId: UserWalletId,
refresh: Boolean,
): Flow<Set<TokenStatus>> {
val operations = TokensStatusesOperations(
tokensRepository,
quotesRepository,
networksRepository,
userWalletId,
refresh,
dispatchers,
raise = this,
)
return operations.getTokensStatusesFlow()
}
private suspend fun Raise<TokensError>.createTokenList(
userWalletId: UserWalletId,
tokens: Set<TokenStatus>,
): Flow<TokenList> {
val isAnyTokenLoading = tokens.any { it.value is TokenStatus.Loading }
val operations = TokenListOperations(
tokensRepository,
networksRepository,
userWalletId,
calculateFiatBalance(tokens, isAnyTokenLoading),
isAnyTokenLoading,
dispatchers,
raise = this,
)
return operations.getTokenListFlow(tokens)
}
private suspend fun calculateFiatBalance(
tokens: Set<TokenStatus>,
isAnyTokenLoading: Boolean,
): TokenList.FiatBalance {
val operations = TokenListFiatBalanceOperations(tokens, isAnyTokenLoading, dispatchers)
return operations.calculateFiatBalance()
}
}

View file

@ -1,6 +1,21 @@
package com.tangem.domain.tokens.error
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.Token
sealed class TokensError {
object EmptyTokens : TokensError()
object EmptyQuotes : TokensError()
object EmptyNetworks : TokensError()
object EmptyNetworkStatues : TokensError()
data class TokenAmountNotFound(val tokenId: Token.ID) : TokensError()
data class TokenFiatAmountLessThenZero(val tokenId: Token.ID) : TokensError()
data class NetworkNotFound(val networkId: Network.ID) : TokensError()
}

View file

@ -0,0 +1,88 @@
package com.tangem.domain.tokens.utils
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TokenStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.math.BigDecimal
internal class TokenListFiatBalanceOperations(
private val tokens: Set<TokenStatus>,
private val isAnyTokenLoading: Boolean,
private val dispatcher: CoroutineDispatcherProvider,
) {
suspend fun calculateFiatBalance(): TokenList.FiatBalance {
return withContext(dispatcher.single) {
var fiatBalance: TokenList.FiatBalance = TokenList.FiatBalance.Loading
if (tokens.isEmpty() || isAnyTokenLoading) return@withContext fiatBalance
for (token in tokens) {
when (val status = token.value) {
is TokenStatus.Loading -> {
fiatBalance = TokenList.FiatBalance.Loading
break
}
is TokenStatus.MissedDerivation,
is TokenStatus.Unreachable,
-> {
fiatBalance = TokenList.FiatBalance.Failed
break
}
is TokenStatus.NoAccount -> {
fiatBalance = recalculateBalanceForNoAccountStatus(fiatBalance)
}
is TokenStatus.Loaded -> {
fiatBalance = recalculateBalance(status, fiatBalance)
}
is TokenStatus.Custom -> {
fiatBalance = recalculateBalance(status, fiatBalance)
}
}
}
fiatBalance
}
}
private fun recalculateBalanceForNoAccountStatus(currentBalance: TokenList.FiatBalance): TokenList.FiatBalance {
return with(currentBalance) {
(this as? TokenList.FiatBalance.Loaded)?.copy(
isAllAmountsSummarized = false,
) ?: TokenList.FiatBalance.Loaded(
amount = BigDecimal.ZERO,
isAllAmountsSummarized = false,
)
}
}
private fun recalculateBalance(
status: TokenStatus.Loaded,
currentBalance: TokenList.FiatBalance,
): TokenList.FiatBalance {
return with(currentBalance) {
(this as? TokenList.FiatBalance.Loaded)?.copy(
amount = this.amount + status.fiatAmount,
) ?: TokenList.FiatBalance.Loaded(
amount = status.fiatAmount,
isAllAmountsSummarized = true,
)
}
}
private fun recalculateBalance(
status: TokenStatus.Custom,
currentBalance: TokenList.FiatBalance,
): TokenList.FiatBalance {
return with(currentBalance) {
val isTokenAmountCanBeSummarized = status.fiatAmount != null
(this as? TokenList.FiatBalance.Loaded)?.copy(
amount = this.amount + (status.fiatAmount ?: BigDecimal.ZERO),
isAllAmountsSummarized = isTokenAmountCanBeSummarized,
) ?: TokenList.FiatBalance.Loaded(
amount = status.fiatAmount ?: BigDecimal.ZERO,
isAllAmountsSummarized = isTokenAmountCanBeSummarized,
)
}
}
}

View file

@ -0,0 +1,161 @@
package com.tangem.domain.tokens.utils
import arrow.core.NonEmptySet
import arrow.core.raise.Raise
import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptySetOrNull
import com.tangem.domain.tokens.error.TokensError
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TokenStatus
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.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@Suppress("LongParameterList")
internal class TokenListOperations(
private val tokensRepository: TokensRepository,
private val networksRepository: NetworksRepository,
private val userWalletId: UserWalletId,
private val totalFiatBalance: TokenList.FiatBalance,
private val isAnyTokenLoading: Boolean,
private val dispatchers: CoroutineDispatcherProvider,
raise: Raise<TokensError>,
) : Raise<TokensError> by raise {
fun getTokenListFlow(tokens: Set<TokenStatus>): Flow<TokenList> {
return combine(getIsGrouped(), getIsSortedByBalance()) { isGrouped, isSortedByBalance ->
createTokenList(tokens, isGrouped, isSortedByBalance)
}
}
private suspend fun createTokenList(
tokens: Set<TokenStatus>,
isGrouped: Boolean,
isSortedByBalance: Boolean,
): TokenList = withContext(dispatchers.single) {
val tokensNes = tokens.toNonEmptySetOrNull()
when {
tokensNes == null -> TokenList.NotInitialized
isGrouped -> {
val networks = getNetworks(tokensNes)
createGroupedTokenList(tokensNes, networks, isSortedByBalance)
}
else -> createUngroupedTokenList(tokensNes, isSortedByBalance)
}
}
private fun createUngroupedTokenList(
tokens: NonEmptySet<TokenStatus>,
isSortedByBalance: Boolean,
): TokenList.Ungrouped {
return TokenList.Ungrouped(
sortedBy = getSortType(isSortedByBalance),
totalFiatBalance = totalFiatBalance,
tokens = if (isSortedByBalance) sortTokensByBalance(tokens) else tokens,
)
}
private fun createGroupedTokenList(
tokens: NonEmptySet<TokenStatus>,
networks: NonEmptySet<Network>,
isSortedByBalance: Boolean,
): TokenList.GroupedByNetwork {
return TokenList.GroupedByNetwork(
sortedBy = getSortType(isSortedByBalance),
totalFiatBalance = totalFiatBalance,
groups = if (isSortedByBalance) groupAndSortTokens(tokens, networks) else groupTokens(tokens, networks),
)
}
private fun groupAndSortTokens(
tokens: NonEmptySet<TokenStatus>,
networks: NonEmptySet<Network>,
): NonEmptySet<NetworkGroup> {
val groupsWithSortedTokens = groupTokens(tokens, networks)
.map { group ->
group.copy(tokens = sortTokensByBalance(group.tokens))
}
val sortedGroups = if (isAnyTokenLoading) {
groupsWithSortedTokens
} else {
sortGroupsByBalance(groupsWithSortedTokens)
}
return sortedGroups
}
private fun sortTokensByBalance(tokens: NonEmptySet<TokenStatus>): NonEmptySet<TokenStatus> {
val sortedTokens = if (isAnyTokenLoading) {
tokens
} else {
tokens
.sortedByDescending { it.value.fiatAmount ?: BigDecimal.ZERO }
.toNonEmptySetOrNull()
}
return sortedTokens!!
}
private fun groupTokens(
tokens: NonEmptySet<TokenStatus>,
networks: NonEmptySet<Network>,
): NonEmptySet<NetworkGroup> {
val groups = tokens
.groupBy { it.networkId }
.map { (networkId, tokens) ->
val network = ensureNotNull(networks.firstOrNull { it.id == networkId }) {
TokensError.NetworkNotFound(networkId)
}
NetworkGroup(
networkId = network.id,
name = network.name,
tokens = tokens.toNonEmptySetOrNull()!!,
)
}
.toNonEmptySetOrNull()
return groups!!
}
private suspend fun getNetworks(tokens: NonEmptySet<TokenStatus>): NonEmptySet<Network> {
return withContext(dispatchers.io) {
val networks = networksRepository.getNetworks(tokens.map { it.networkId }).bind()
ensureNotNull(networks.toNonEmptySetOrNull()) { TokensError.EmptyNetworks }
}
}
private fun getIsGrouped(): Flow<Boolean> {
return tokensRepository.isTokensGrouped(userWalletId)
.map { it.bind() }
.flowOn(dispatchers.io)
}
private fun getIsSortedByBalance(): Flow<Boolean> {
return tokensRepository.isTokensSortedByBalance(userWalletId)
.map { it.bind() }
.flowOn(dispatchers.io)
}
private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptySet<NetworkGroup>): NonEmptySet<NetworkGroup> {
return groupsWithSortedTokens
.sortedByDescending { group ->
group.tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }
}
.toNonEmptySetOrNull()!!
}
private fun getSortType(isSortedByBalance: Boolean) =
if (isSortedByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE
}

View file

@ -0,0 +1,84 @@
package com.tangem.domain.tokens.utils
import arrow.core.raise.Raise
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
import com.tangem.domain.tokens.error.TokensError
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.Token
import com.tangem.domain.tokens.model.TokenStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.math.BigDecimal
internal class TokenStatusOperations(
private val token: Token,
private val quote: Quote?,
private val networkStatus: NetworkStatus?,
private val dispatchers: CoroutineDispatcherProvider,
raise: Raise<TokensError>,
) : Raise<TokensError> by raise {
suspend fun createTokenStatus(): TokenStatus = withContext(dispatchers.single) {
TokenStatus(
id = token.id,
networkId = token.networkId,
name = token.name,
symbol = token.symbol,
isCoin = token.isCoin,
value = createStatus(),
)
}
private fun createStatus(): TokenStatus.Status {
return when (val status = networkStatus?.value) {
null -> TokenStatus.Loading
is NetworkStatus.MissedDerivation -> TokenStatus.MissedDerivation
is NetworkStatus.Unreachable -> TokenStatus.Unreachable
is NetworkStatus.NoAccount -> TokenStatus.NoAccount
is NetworkStatus.TransactionInProgress,
is NetworkStatus.Verified,
-> createStatus(
amount = getTokenAmount(),
hasTransactionsInProgress = status is NetworkStatus.TransactionInProgress,
)
}
}
private fun createStatus(amount: BigDecimal, hasTransactionsInProgress: Boolean): TokenStatus.Status {
return when {
token.isCustom -> TokenStatus.Custom(
amount = amount,
fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate),
priceChange = quote?.priceChange,
hasTransactionsInProgress = hasTransactionsInProgress,
)
quote == null -> TokenStatus.Loading
else -> TokenStatus.Loaded(
amount = amount,
fiatAmount = calculateFiatAmount(amount, quote.fiatRate),
priceChange = quote.priceChange,
hasTransactionsInProgress = hasTransactionsInProgress,
)
}
}
private fun getTokenAmount(): BigDecimal {
val amount = networkStatus?.value?.amounts?.get(token.id)
return ensureNotNull(amount) { TokensError.TokenAmountNotFound(token.id) }
}
private fun calculateFiatAmountOrNull(amount: BigDecimal, fiatRate: BigDecimal?): BigDecimal? {
if (fiatRate == null) return null
return calculateFiatAmount(amount, fiatRate)
}
private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal {
val fiatAmount = amount * fiatRate
ensure(condition = fiatAmount >= BigDecimal.ZERO) { TokensError.TokenFiatAmountLessThenZero(token.id) }
return fiatAmount
}
}

View file

@ -0,0 +1,93 @@
package com.tangem.domain.tokens.utils
import arrow.core.NonEmptySet
import arrow.core.raise.Raise
import arrow.core.toNonEmptySetOrNull
import com.tangem.domain.tokens.error.TokensError
import com.tangem.domain.tokens.model.*
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.*
import kotlinx.coroutines.withContext
@Suppress("LongParameterList")
internal class TokensStatusesOperations(
private val tokensRepository: TokensRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val userWalletId: UserWalletId,
private val refresh: Boolean,
private val dispatchers: CoroutineDispatcherProvider,
raise: Raise<TokensError>,
) : Raise<TokensError> by raise {
fun getTokensStatusesFlow(): Flow<Set<TokenStatus>> {
return getTokens().flatMapConcat {
val tokens = it.toNonEmptySetOrNull()
if (tokens == null) {
flowOf(emptySet())
} else {
val tokensIds = tokens.map { token -> token.id }
val groupedTokens = groupTokens(tokens)
combine(getQuotes(tokensIds), getNetworksStatues(groupedTokens)) { quotes, networksStatuses ->
createTokensStatuses(tokens, quotes, networksStatuses)
}
}
}
}
private suspend fun createTokensStatuses(
tokens: Set<Token>,
quotes: Set<Quote>,
networkStatuses: Set<NetworkStatus>,
): Set<TokenStatus> = withContext(dispatchers.single) {
tokens.map { token ->
val quote = quotes.firstOrNull { it.tokenId == token.id }
val networkStatus = networkStatuses.firstOrNull { it.networkId == token.networkId }
createStatus(token, quote, networkStatus)
}.toSet()
}
private suspend fun createStatus(token: Token, quote: Quote?, networkStatus: NetworkStatus?): TokenStatus {
val operations = TokenStatusOperations(token, quote, networkStatus, dispatchers, raise = this)
return operations.createTokenStatus()
}
private fun getTokens(): Flow<Set<Token>> {
return tokensRepository.getTokens(userWalletId, refresh)
.onEmpty { raise(TokensError.EmptyTokens) }
.map { it.bind() }
.flowOn(dispatchers.io)
}
private fun getQuotes(tokensIds: NonEmptySet<Token.ID>): Flow<Set<Quote>> {
return quotesRepository.getQuotes(tokensIds, refresh)
.onEmpty { raise(TokensError.EmptyQuotes) }
.map { it.bind() }
.flowOn(dispatchers.io)
}
private fun getNetworksStatues(groupedTokens: Map<Network.ID, NonEmptySet<Token.ID>>): Flow<Set<NetworkStatus>> {
return networksRepository.getNetworkStatuses(userWalletId, groupedTokens, refresh)
.onEmpty { raise(TokensError.EmptyNetworkStatues) }
.map { it.bind() }
.flowOn(dispatchers.io)
}
private suspend fun groupTokens(tokens: NonEmptySet<Token>): Map<Network.ID, NonEmptySet<Token.ID>> =
withContext(dispatchers.single) {
tokens
.groupBy { it.networkId }
.mapValues { (_, tokens) ->
tokens.toNonEmptySetOrNull()
?.map { it.id }
?: raise(TokensError.EmptyTokens)
}
}
}