diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index 0f52a195f9..024c43ef42 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -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> { - 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.getTokenList(userWalletId: UserWalletId, refresh: Boolean): Flow { + return getTokensStatuses(userWalletId, refresh).flatMapConcat { tokens -> + createTokenList(userWalletId, tokens) + } + } + + private fun Raise.getTokensStatuses( + userWalletId: UserWalletId, + refresh: Boolean, + ): Flow> { + val operations = TokensStatusesOperations( + tokensRepository, + quotesRepository, + networksRepository, + userWalletId, + refresh, + dispatchers, + raise = this, + ) + + return operations.getTokensStatusesFlow() + } + + private suspend fun Raise.createTokenList( + userWalletId: UserWalletId, + tokens: Set, + ): Flow { + 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, + isAnyTokenLoading: Boolean, + ): TokenList.FiatBalance { + val operations = TokenListFiatBalanceOperations(tokens, isAnyTokenLoading, dispatchers) + + return operations.calculateFiatBalance() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt index 4647779219..6571bc1d78 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokensError.kt @@ -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() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListFiatBalanceOperations.kt new file mode 100644 index 0000000000..6d3360512e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListFiatBalanceOperations.kt @@ -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, + 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, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt new file mode 100644 index 0000000000..eeec365e6b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenListOperations.kt @@ -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, +) : Raise by raise { + + fun getTokenListFlow(tokens: Set): Flow { + return combine(getIsGrouped(), getIsSortedByBalance()) { isGrouped, isSortedByBalance -> + createTokenList(tokens, isGrouped, isSortedByBalance) + } + } + + private suspend fun createTokenList( + tokens: Set, + 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, + isSortedByBalance: Boolean, + ): TokenList.Ungrouped { + return TokenList.Ungrouped( + sortedBy = getSortType(isSortedByBalance), + totalFiatBalance = totalFiatBalance, + tokens = if (isSortedByBalance) sortTokensByBalance(tokens) else tokens, + ) + } + + private fun createGroupedTokenList( + tokens: NonEmptySet, + networks: NonEmptySet, + 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, + networks: NonEmptySet, + ): NonEmptySet { + 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): NonEmptySet { + val sortedTokens = if (isAnyTokenLoading) { + tokens + } else { + tokens + .sortedByDescending { it.value.fiatAmount ?: BigDecimal.ZERO } + .toNonEmptySetOrNull() + } + + return sortedTokens!! + } + + private fun groupTokens( + tokens: NonEmptySet, + networks: NonEmptySet, + ): NonEmptySet { + 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): NonEmptySet { + return withContext(dispatchers.io) { + val networks = networksRepository.getNetworks(tokens.map { it.networkId }).bind() + ensureNotNull(networks.toNonEmptySetOrNull()) { TokensError.EmptyNetworks } + } + } + + private fun getIsGrouped(): Flow { + return tokensRepository.isTokensGrouped(userWalletId) + .map { it.bind() } + .flowOn(dispatchers.io) + } + + private fun getIsSortedByBalance(): Flow { + return tokensRepository.isTokensSortedByBalance(userWalletId) + .map { it.bind() } + .flowOn(dispatchers.io) + } + + private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptySet): NonEmptySet { + 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 +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt new file mode 100644 index 0000000000..09231e0526 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokenStatusOperations.kt @@ -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, +) : Raise 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 + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt new file mode 100644 index 0000000000..e301066bc6 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/TokensStatusesOperations.kt @@ -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, +) : Raise by raise { + + fun getTokensStatusesFlow(): Flow> { + 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, + quotes: Set, + networkStatuses: Set, + ): Set = 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> { + return tokensRepository.getTokens(userWalletId, refresh) + .onEmpty { raise(TokensError.EmptyTokens) } + .map { it.bind() } + .flowOn(dispatchers.io) + } + + private fun getQuotes(tokensIds: NonEmptySet): Flow> { + return quotesRepository.getQuotes(tokensIds, refresh) + .onEmpty { raise(TokensError.EmptyQuotes) } + .map { it.bind() } + .flowOn(dispatchers.io) + } + + private fun getNetworksStatues(groupedTokens: Map>): Flow> { + return networksRepository.getNetworkStatuses(userWalletId, groupedTokens, refresh) + .onEmpty { raise(TokensError.EmptyNetworkStatues) } + .map { it.bind() } + .flowOn(dispatchers.io) + } + + private suspend fun groupTokens(tokens: NonEmptySet): Map> = + withContext(dispatchers.single) { + tokens + .groupBy { it.networkId } + .mapValues { (_, tokens) -> + tokens.toNonEmptySetOrNull() + ?.map { it.id } + ?: raise(TokensError.EmptyTokens) + } + } +} \ No newline at end of file