Updated on 2026-08-14
This commit is contained in:
parent
c65a934d02
commit
8aa7bf9b75
9 changed files with 412 additions and 378 deletions
|
|
@ -4,12 +4,10 @@ import arrow.core.Either
|
|||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import arrow.core.raise.withError
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.tokens.error.TokenListSortingError
|
||||
import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError
|
||||
import com.tangem.domain.tokens.operations.TokenListSortingOperations
|
||||
import com.tangem.domain.tokens.operations.TokenListFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
|
@ -30,36 +28,24 @@ class ToggleTokenListGroupingUseCase(
|
|||
}
|
||||
|
||||
private fun Raise<TokenListSortingError>.groupTokens(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork {
|
||||
ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) {
|
||||
TokenListSortingError.TokenListIsLoading
|
||||
}
|
||||
validate(tokenList)
|
||||
|
||||
val sortingOperations = TokenListSortingOperations(tokenList)
|
||||
|
||||
return TokenList.GroupedByNetwork(
|
||||
groups = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) {
|
||||
sortingOperations.getGroupedTokens().bind()
|
||||
},
|
||||
totalFiatBalance = tokenList.totalFiatBalance,
|
||||
sortedBy = sortingOperations.getSortType(),
|
||||
)
|
||||
return TokenListFactory.createGroupedByNetwork(tokenList)
|
||||
}
|
||||
|
||||
private fun Raise<TokenListSortingError>.ungroupTokens(
|
||||
tokenList: TokenList.GroupedByNetwork,
|
||||
): TokenList.Ungrouped {
|
||||
private fun Raise<TokenListSortingError>.ungroupTokens(tokenList: TokenList.GroupedByNetwork): TokenList.Ungrouped {
|
||||
validate(tokenList)
|
||||
|
||||
return TokenListFactory.createUngrouped(tokenList)
|
||||
}
|
||||
|
||||
private fun Raise<TokenListSortingError>.validate(tokenList: TokenList) {
|
||||
ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) {
|
||||
TokenListSortingError.TokenListIsLoading
|
||||
}
|
||||
|
||||
val sortingOperations = TokenListSortingOperations(tokenList)
|
||||
|
||||
return TokenList.Ungrouped(
|
||||
currencies = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) {
|
||||
sortingOperations.getTokens().bind()
|
||||
},
|
||||
totalFiatBalance = tokenList.totalFiatBalance,
|
||||
sortedBy = sortingOperations.getSortType(),
|
||||
)
|
||||
ensure(tokenList.flattenCurrencies().isNotEmpty()) {
|
||||
TokenListSortingError.TokenListIsEmpty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import arrow.core.raise.withError
|
||||
import com.tangem.domain.models.TokensGroupType
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.tokens.error.TokenListSortingError
|
||||
import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError
|
||||
import com.tangem.domain.tokens.operations.TokenListSortingOperations
|
||||
import com.tangem.domain.tokens.operations.TokenListFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
|
@ -21,53 +19,20 @@ class ToggleTokenListSortingUseCase(
|
|||
suspend operator fun invoke(tokenList: TokenList): Either<TokenListSortingError, TokenList> {
|
||||
return withContext(dispatchers.default) {
|
||||
either {
|
||||
when (tokenList) {
|
||||
is TokenList.GroupedByNetwork -> sortGroupedTokenList(tokenList)
|
||||
is TokenList.Ungrouped -> sortUngroupedTokenList(tokenList)
|
||||
is TokenList.Empty -> raise(TokenListSortingError.TokenListIsEmpty)
|
||||
ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) {
|
||||
TokenListSortingError.TokenListIsLoading
|
||||
}
|
||||
|
||||
TokenListFactory.create(
|
||||
statuses = tokenList.flattenCurrencies(),
|
||||
groupType = when (tokenList) {
|
||||
is TokenList.GroupedByNetwork -> TokensGroupType.NETWORK
|
||||
is TokenList.Ungrouped -> TokensGroupType.NONE
|
||||
is TokenList.Empty -> raise(TokenListSortingError.TokenListIsEmpty)
|
||||
},
|
||||
sortType = TokensSortType.BALANCE,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Raise<TokenListSortingError>.sortGroupedTokenList(
|
||||
tokenList: TokenList.GroupedByNetwork,
|
||||
): TokenList.GroupedByNetwork {
|
||||
ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) {
|
||||
TokenListSortingError.TokenListIsLoading
|
||||
}
|
||||
|
||||
val operations = getSortingOperations(tokenList)
|
||||
|
||||
return tokenList.copy(
|
||||
groups = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) {
|
||||
operations.getGroupedTokens().bind()
|
||||
},
|
||||
sortedBy = operations.getSortType(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Raise<TokenListSortingError>.sortUngroupedTokenList(
|
||||
tokenList: TokenList.Ungrouped,
|
||||
): TokenList.Ungrouped {
|
||||
ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) {
|
||||
TokenListSortingError.TokenListIsLoading
|
||||
}
|
||||
|
||||
val operations = getSortingOperations(tokenList)
|
||||
|
||||
return tokenList.copy(
|
||||
currencies = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) {
|
||||
operations.getTokens().bind()
|
||||
},
|
||||
sortedBy = operations.getSortType(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSortingOperations(tokenList: TokenList): TokenListSortingOperations {
|
||||
return TokenListSortingOperations(
|
||||
tokenList = tokenList,
|
||||
sortByBalance = tokenList.sortedBy != TokensSortType.BALANCE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.domain.tokens.error.mapper
|
||||
|
||||
import com.tangem.domain.tokens.error.TokenListSortingError
|
||||
import com.tangem.domain.tokens.operations.TokenListSortingOperations
|
||||
|
||||
internal fun TokenListSortingOperations.Error.mapToTokenListSortingError(): TokenListSortingError {
|
||||
return when (this) {
|
||||
is TokenListSortingOperations.Error.EmptyTokens -> TokenListSortingError.TokenListIsEmpty
|
||||
is TokenListSortingOperations.Error.NetworkNotFound,
|
||||
-> TokenListSortingError.UnableToSortTokenList
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package com.tangem.domain.tokens.operations
|
||||
|
||||
import arrow.core.NonEmptyList
|
||||
import arrow.core.toNonEmptyListOrNull
|
||||
import com.tangem.domain.models.TokensGroupType
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup
|
||||
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* This factory creates a [TokenList] based on the provided list of [CryptoCurrencyStatus], [TokensGroupType],
|
||||
* and [TokensSortType].
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object TokenListFactory {
|
||||
|
||||
/**
|
||||
* Creates a [TokenList] based on the provided parameters.
|
||||
*
|
||||
* @param statuses A list of [CryptoCurrencyStatus] representing the cryptocurrencies to be included in the token list.
|
||||
* @param groupType The type of grouping to be applied to the token list, either by network or none.
|
||||
* @param sortType The type of sorting to be applied to the token list, either by balance or none.
|
||||
*/
|
||||
fun create(statuses: List<CryptoCurrencyStatus>, groupType: TokensGroupType, sortType: TokensSortType): TokenList {
|
||||
val nonEmptyStatuses = statuses.toNonEmptyListOrNull() ?: return TokenList.Empty
|
||||
val totalFiatBalance = TotalFiatBalanceCalculator.calculate(statuses = nonEmptyStatuses)
|
||||
|
||||
return when (groupType) {
|
||||
TokensGroupType.NONE -> createUngrouped(totalFiatBalance, nonEmptyStatuses, sortType)
|
||||
TokensGroupType.NETWORK -> createGroupedByNetwork(totalFiatBalance, nonEmptyStatuses, sortType)
|
||||
}
|
||||
}
|
||||
|
||||
fun createUngrouped(tokenList: TokenList.GroupedByNetwork): TokenList.Ungrouped {
|
||||
return createUngrouped(
|
||||
totalFiatBalance = tokenList.totalFiatBalance,
|
||||
nonEmptyStatuses = tokenList.flattenCurrencies().toNonEmptyListOrNull()
|
||||
?: error("GroupedByNetwork.flattenCurrencies should be non empty list"),
|
||||
sortType = tokenList.sortedBy,
|
||||
)
|
||||
}
|
||||
|
||||
fun createGroupedByNetwork(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork {
|
||||
return createGroupedByNetwork(
|
||||
totalFiatBalance = tokenList.totalFiatBalance,
|
||||
nonEmptyStatuses = tokenList.flattenCurrencies().toNonEmptyListOrNull()
|
||||
?: error("Ungrouped.flattenCurrencies should be non empty list"),
|
||||
sortType = tokenList.sortedBy,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUngrouped(
|
||||
totalFiatBalance: TotalFiatBalance,
|
||||
nonEmptyStatuses: NonEmptyList<CryptoCurrencyStatus>,
|
||||
sortType: TokensSortType,
|
||||
): TokenList.Ungrouped {
|
||||
return TokenList.Ungrouped(
|
||||
totalFiatBalance = totalFiatBalance,
|
||||
sortedBy = sortType,
|
||||
currencies = nonEmptyStatuses.sortStatuses(sortType),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createGroupedByNetwork(
|
||||
totalFiatBalance: TotalFiatBalance,
|
||||
nonEmptyStatuses: NonEmptyList<CryptoCurrencyStatus>,
|
||||
sortType: TokensSortType,
|
||||
): TokenList.GroupedByNetwork {
|
||||
return TokenList.GroupedByNetwork(
|
||||
totalFiatBalance = totalFiatBalance,
|
||||
sortedBy = sortType,
|
||||
groups = nonEmptyStatuses.groupByNetworkAndSort(sortType),
|
||||
)
|
||||
}
|
||||
|
||||
private fun NonEmptyList<CryptoCurrencyStatus>.groupByNetworkAndSort(sortType: TokensSortType): List<NetworkGroup> {
|
||||
return this
|
||||
.groupBy { it.currency.network }
|
||||
.map { (network, currencies) ->
|
||||
NetworkGroup(
|
||||
network = network,
|
||||
currencies = currencies.sortStatuses(sortType),
|
||||
)
|
||||
}
|
||||
.sortGroups(sortType)
|
||||
}
|
||||
|
||||
private fun List<NetworkGroup>.sortGroups(type: TokensSortType): List<NetworkGroup> {
|
||||
return when (type) {
|
||||
TokensSortType.NONE -> this
|
||||
TokensSortType.BALANCE -> {
|
||||
val hasLoading = this.asSequence()
|
||||
.flatMap(NetworkGroup::currencies)
|
||||
.any { it.value is CryptoCurrencyStatus.Loading }
|
||||
|
||||
if (hasLoading) return this
|
||||
|
||||
sortedByDescending { group ->
|
||||
group.currencies.sumOf { it.calculateBalance() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<CryptoCurrencyStatus>.sortStatuses(type: TokensSortType): List<CryptoCurrencyStatus> {
|
||||
return when (type) {
|
||||
TokensSortType.NONE -> this
|
||||
TokensSortType.BALANCE -> {
|
||||
val hasLoading = any { it.value is CryptoCurrencyStatus.Loading }
|
||||
|
||||
if (hasLoading) return this
|
||||
|
||||
sortedByDescending { it.calculateBalance() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.calculateBalance(): BigDecimal {
|
||||
val yieldBalance = value.yieldBalance as? YieldBalance.Data
|
||||
val totalYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.network.rawId).orZero()
|
||||
val totalFiatYieldBalance = totalYieldBalance.multiply(value.fiatRate.orZero())
|
||||
|
||||
return value.fiatAmount?.plus(totalFiatYieldBalance).orZero()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
package com.tangem.domain.tokens.operations
|
||||
|
||||
import arrow.core.*
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.withError
|
||||
import arrow.core.right
|
||||
import arrow.core.toNonEmptyListOrNull
|
||||
import com.tangem.domain.models.TokensGroupType
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -30,80 +31,13 @@ internal class TokenListOperations(
|
|||
}
|
||||
}
|
||||
|
||||
private fun Raise<Error>.createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList {
|
||||
private fun createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList {
|
||||
val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() ?: return TokenList.Empty
|
||||
|
||||
return createTokenList(
|
||||
currencies = nonEmptyCurrencies,
|
||||
fiatBalance = TotalFiatBalanceCalculator.calculate(statuses = nonEmptyCurrencies),
|
||||
isGrouped = isGrouped,
|
||||
isSortedByBalance = isSortedByBalance,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Raise<Error>.createTokenList(
|
||||
currencies: NonEmptyList<CryptoCurrencyStatus>,
|
||||
fiatBalance: TotalFiatBalance,
|
||||
isGrouped: Boolean,
|
||||
isSortedByBalance: Boolean,
|
||||
): TokenList {
|
||||
val sortingOperations = TokenListSortingOperations(
|
||||
currencies = currencies,
|
||||
isAnyTokenLoading = currencies.any { it.value is CryptoCurrencyStatus.Loading },
|
||||
sortByBalance = isSortedByBalance,
|
||||
)
|
||||
|
||||
return createTokenList(sortingOperations, fiatBalance, isGrouped)
|
||||
}
|
||||
|
||||
private fun Raise<Error>.createTokenList(
|
||||
sortingOperations: TokenListSortingOperations,
|
||||
fiatBalance: TotalFiatBalance,
|
||||
isGrouped: Boolean,
|
||||
): TokenList {
|
||||
return if (isGrouped) {
|
||||
createGroupedTokenList(sortingOperations, fiatBalance)
|
||||
} else {
|
||||
createUngroupedTokenList(sortingOperations, fiatBalance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Raise<Error>.createUngroupedTokenList(
|
||||
sortingOperations: TokenListSortingOperations,
|
||||
fiatBalance: TotalFiatBalance,
|
||||
): TokenList.Ungrouped = TokenList.Ungrouped(
|
||||
sortedBy = sortingOperations.getSortType(),
|
||||
totalFiatBalance = fiatBalance,
|
||||
currencies = withError(
|
||||
transform = { e ->
|
||||
Error.fromTokenListOperations(e) { createUnsortedUngroupedTokenList(tokens, fiatBalance) }
|
||||
},
|
||||
block = { sortingOperations.getTokens().bind() },
|
||||
),
|
||||
)
|
||||
|
||||
private fun Raise<Error>.createGroupedTokenList(
|
||||
sortingOperations: TokenListSortingOperations,
|
||||
fiatBalance: TotalFiatBalance,
|
||||
): TokenList.GroupedByNetwork = TokenList.GroupedByNetwork(
|
||||
sortedBy = sortingOperations.getSortType(),
|
||||
totalFiatBalance = fiatBalance,
|
||||
groups = withError(
|
||||
transform = { e ->
|
||||
Error.fromTokenListOperations(e) { createUnsortedUngroupedTokenList(tokens, fiatBalance) }
|
||||
},
|
||||
block = { sortingOperations.getGroupedTokens().bind() },
|
||||
),
|
||||
)
|
||||
|
||||
private fun createUnsortedUngroupedTokenList(
|
||||
tokens: List<CryptoCurrencyStatus>,
|
||||
fiatBalance: TotalFiatBalance,
|
||||
): TokenList.Ungrouped {
|
||||
return TokenList.Ungrouped(
|
||||
sortedBy = TokensSortType.NONE,
|
||||
totalFiatBalance = fiatBalance,
|
||||
currencies = tokens,
|
||||
return TokenListFactory.create(
|
||||
statuses = nonEmptyCurrencies,
|
||||
groupType = if (isGrouped) TokensGroupType.NETWORK else TokensGroupType.NONE,
|
||||
sortType = if (isSortedByBalance) TokensSortType.BALANCE else TokensSortType.NONE,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -128,19 +62,5 @@ internal class TokenListOperations(
|
|||
data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : Error()
|
||||
|
||||
data class DataError(val cause: Throwable) : Error()
|
||||
|
||||
internal companion object {
|
||||
|
||||
fun fromTokenListOperations(
|
||||
e: TokenListSortingOperations.Error,
|
||||
createUnsortedUngroupedTokenList: () -> TokenList.Ungrouped,
|
||||
): Error = when (e) {
|
||||
is TokenListSortingOperations.Error.EmptyTokens,
|
||||
is TokenListSortingOperations.Error.NetworkNotFound,
|
||||
-> UnableToSortTokenList(
|
||||
unsortedTokenList = createUnsortedUngroupedTokenList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
package com.tangem.domain.tokens.operations
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.NonEmptyList
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import arrow.core.raise.ensureNotNull
|
||||
import arrow.core.toNonEmptyListOrNull
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup
|
||||
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class TokenListSortingOperations(
|
||||
private val currencies: List<CryptoCurrencyStatus>,
|
||||
private val isAnyTokenLoading: Boolean,
|
||||
private val sortByBalance: Boolean,
|
||||
) {
|
||||
|
||||
constructor(
|
||||
tokenList: TokenList,
|
||||
sortByBalance: Boolean = tokenList.sortedBy == TokensSortType.BALANCE,
|
||||
isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TotalFiatBalance.Loading,
|
||||
) : this(
|
||||
currencies = tokenList.flattenCurrencies(),
|
||||
isAnyTokenLoading = isAnyTokenLoading,
|
||||
sortByBalance = sortByBalance,
|
||||
)
|
||||
|
||||
fun getGroupedTokens(): Either<Error, NonEmptyList<NetworkGroup>> = either {
|
||||
ensure(currencies.isNotEmpty()) { Error.EmptyTokens }
|
||||
|
||||
if (sortByBalance) {
|
||||
groupAndSortTokensByBalance()
|
||||
} else {
|
||||
groupTokens()
|
||||
}
|
||||
}
|
||||
|
||||
fun getTokens(): Either<Error, NonEmptyList<CryptoCurrencyStatus>> = either {
|
||||
val nonEmptyCurrencies = ensureNotNull(currencies.toNonEmptyListOrNull()) {
|
||||
Error.EmptyTokens
|
||||
}
|
||||
|
||||
if (sortByBalance) sortTokensByBalance(nonEmptyCurrencies) else nonEmptyCurrencies
|
||||
}
|
||||
|
||||
fun getSortType(): TokensSortType = if (sortByBalance) TokensSortType.BALANCE else TokensSortType.NONE
|
||||
|
||||
private fun Raise<Error>.groupTokens(): NonEmptyList<NetworkGroup> {
|
||||
val groupedTokens = currencies
|
||||
.groupBy { it.currency.network }
|
||||
.map { (network, tokens) ->
|
||||
NetworkGroup(
|
||||
network = network,
|
||||
currencies = ensureNotNull(tokens.toNonEmptyListOrNull()) { Error.EmptyTokens },
|
||||
)
|
||||
}
|
||||
.toNonEmptyListOrNull()
|
||||
|
||||
return ensureNotNull(groupedTokens) { Error.EmptyTokens }
|
||||
}
|
||||
|
||||
private fun Raise<Error>.groupAndSortTokensByBalance(): NonEmptyList<NetworkGroup> {
|
||||
val groupsWithSortedTokens = groupTokens()
|
||||
.map { group ->
|
||||
val tokens = group.currencies as? NonEmptyList<CryptoCurrencyStatus>
|
||||
?: error("Tokens can not be empty here")
|
||||
group.copy(currencies = sortTokensByBalance(tokens))
|
||||
}
|
||||
|
||||
return if (isAnyTokenLoading) {
|
||||
groupsWithSortedTokens
|
||||
} else {
|
||||
sortGroupsByBalance(groupsWithSortedTokens)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortTokensByBalance(tokens: NonEmptyList<CryptoCurrencyStatus>): NonEmptyList<CryptoCurrencyStatus> {
|
||||
return if (isAnyTokenLoading) {
|
||||
tokens
|
||||
} else {
|
||||
tokens.sortedByDescending { it.getTotalBalance() }
|
||||
.toNonEmptyListOrNull()
|
||||
?: error("Tokens can not be empty here")
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptyList<NetworkGroup>): NonEmptyList<NetworkGroup> {
|
||||
return groupsWithSortedTokens
|
||||
.sortedByDescending { group ->
|
||||
group.currencies.sumOf { it.getTotalBalance() }
|
||||
}
|
||||
.toNonEmptyListOrNull()
|
||||
?: error("Tokens can not be empty here")
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.getTotalBalance(): BigDecimal {
|
||||
val yieldBalance = value.yieldBalance as? YieldBalance.Data
|
||||
val totalYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.network.rawId).orZero()
|
||||
val totalFiatYieldBalance = totalYieldBalance.multiply(value.fiatRate.orZero())
|
||||
return value.fiatAmount?.plus(totalFiatYieldBalance).orZero()
|
||||
}
|
||||
|
||||
sealed class Error {
|
||||
|
||||
object EmptyTokens : Error()
|
||||
|
||||
data class NetworkNotFound(val networkId: Network.ID) : Error()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,114 +2,102 @@ package com.tangem.domain.tokens
|
|||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.tokens.error.TokenListSortingError
|
||||
import com.tangem.domain.tokens.mock.MockTokenLists
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ToggleTokenListGroupingTest {
|
||||
|
||||
private val useCase = ToggleTokenListGroupingUseCase(
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `when list is empty then error should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = TokenListSortingError.TokenListIsEmpty.left()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = TokenListSortingError.TokenListIsEmpty.left()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.emptyUngroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.emptyUngroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is grouped and loading then error should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = TokenListSortingError.TokenListIsLoading.left()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = TokenListSortingError.TokenListIsLoading.left()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.loadingGroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.loadingGroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is ungrouped and loading then error should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = TokenListSortingError.TokenListIsLoading.left()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = TokenListSortingError.TokenListIsLoading.left()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.loadingUngroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.loadingUngroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is ungrouped and sorted then sorted grouped list should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = MockTokenLists.sortedGroupedTokenList.right()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = MockTokenLists.sortedGroupedTokenList.right()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.sortedUngroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.sortedUngroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is ungrouped and unsorted then unsorted grouped list should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = MockTokenLists.unsortedGroupedTokenList.right()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = MockTokenLists.unsortedGroupedTokenList.right()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.unsortedUngroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.unsortedUngroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is grouped and sorted then sorted ungrouped list should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = MockTokenLists.sortedUngroupedTokenList.right()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = MockTokenLists.sortedUngroupedTokenList.right()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.sortedGroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.sortedGroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is grouped and unsorted then unsorted ungrouped list should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = MockTokenLists.unsortedUngroupedTokenList.right()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = MockTokenLists.unsortedUngroupedTokenList.right()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.unsortedGroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.unsortedGroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun getUseCase() = ToggleTokenListGroupingUseCase(
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
}
|
||||
|
|
@ -2,114 +2,102 @@ package com.tangem.domain.tokens
|
|||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.tokens.error.TokenListSortingError
|
||||
import com.tangem.domain.tokens.mock.MockTokenLists
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ToggleTokenListSortingUseCaseTest {
|
||||
|
||||
private val useCase = ToggleTokenListSortingUseCase(
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `when list is empty then error should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = TokenListSortingError.TokenListIsEmpty.left()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = TokenListSortingError.TokenListIsEmpty.left()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.emptyTokenList)
|
||||
val actual = useCase(MockTokenLists.emptyTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is grouped and loading then error should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = TokenListSortingError.TokenListIsLoading.left()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = TokenListSortingError.TokenListIsLoading.left()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.loadingGroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.loadingGroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is ungrouped and loading then error should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = TokenListSortingError.TokenListIsLoading.left()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = TokenListSortingError.TokenListIsLoading.left()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.loadingUngroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.loadingUngroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is grouped and unsorted then grouped and sorted list should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = MockTokenLists.sortedGroupedTokenList.right()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = MockTokenLists.sortedGroupedTokenList.right()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.unsortedGroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.unsortedGroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is ungrouped and unsorted then ungrouped and sorted list should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = MockTokenLists.sortedUngroupedTokenList.right()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = MockTokenLists.sortedUngroupedTokenList.right()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.unsortedUngroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.unsortedUngroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is grouped and sorted then grouped and unsorted list should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = MockTokenLists.sortedGroupedTokenList.right()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = MockTokenLists.sortedGroupedTokenList.right()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.unsortedGroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.unsortedGroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when list is ungrouped and sorted then ungrouped and unsorted list should be received`() = runTest {
|
||||
// Given
|
||||
val expectedResult = MockTokenLists.sortedUngroupedTokenList.right()
|
||||
|
||||
val useCase = getUseCase()
|
||||
val expected = MockTokenLists.sortedUngroupedTokenList.right()
|
||||
|
||||
// When
|
||||
val result = useCase(MockTokenLists.unsortedUngroupedTokenList)
|
||||
val actual = useCase(MockTokenLists.unsortedUngroupedTokenList)
|
||||
|
||||
// Then
|
||||
assertEquals(expectedResult, result)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun getUseCase() = ToggleTokenListSortingUseCase(
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
package com.tangem.domain.tokens.operations
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TokensGroupType
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.tokens.mock.MockTokens
|
||||
import com.tangem.domain.tokens.mock.MockTokensStates
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class TokenListFactoryTest {
|
||||
|
||||
@Test
|
||||
fun `statuses is empty, return TokenList Empty`() {
|
||||
// Act
|
||||
val actual = TokenListFactory.create(
|
||||
statuses = emptyList(),
|
||||
groupType = TokensGroupType.NONE, // doesn't matter
|
||||
sortType = TokensSortType.BALANCE, // doesn't matter
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = TokenList.Empty
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create UNGROUPED token list WITHOUT sorting`() {
|
||||
// Act
|
||||
val actual = TokenListFactory.create(
|
||||
statuses = MockTokensStates.loadedTokensStates,
|
||||
groupType = TokensGroupType.NONE,
|
||||
sortType = TokensSortType.NONE,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = TokenList.Ungrouped(
|
||||
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal("599.50"), source = StatusSource.ACTUAL),
|
||||
sortedBy = TokensSortType.NONE,
|
||||
currencies = MockTokensStates.loadedTokensStates,
|
||||
)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create UNGROUPED token list WITH SORTING BY BALANCE`() {
|
||||
// Act
|
||||
val actual = TokenListFactory.create(
|
||||
statuses = MockTokensStates.loadedTokensStates,
|
||||
groupType = TokensGroupType.NONE,
|
||||
sortType = TokensSortType.BALANCE,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = TokenList.Ungrouped(
|
||||
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal("599.50"), source = StatusSource.ACTUAL),
|
||||
sortedBy = TokensSortType.BALANCE,
|
||||
currencies = MockTokensStates.loadedTokensStates.reversed(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create GROUPED token list WITHOUT sorting`() {
|
||||
// Act
|
||||
val actual = TokenListFactory.create(
|
||||
statuses = MockTokensStates.loadedTokensStates,
|
||||
groupType = TokensGroupType.NETWORK,
|
||||
sortType = TokensSortType.NONE,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = TokenList.GroupedByNetwork(
|
||||
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal("599.50"), source = StatusSource.ACTUAL),
|
||||
sortedBy = TokensSortType.NONE,
|
||||
groups = listOf(
|
||||
TokenList.GroupedByNetwork.NetworkGroup(
|
||||
network = MockTokens.token3.network,
|
||||
currencies = listOf(
|
||||
MockTokensStates.loadedTokensStates[0],
|
||||
MockTokensStates.loadedTokensStates[1],
|
||||
MockTokensStates.loadedTokensStates[2],
|
||||
),
|
||||
),
|
||||
TokenList.GroupedByNetwork.NetworkGroup(
|
||||
network = MockTokens.token6.network,
|
||||
currencies = listOf(
|
||||
MockTokensStates.loadedTokensStates[3],
|
||||
MockTokensStates.loadedTokensStates[4],
|
||||
MockTokensStates.loadedTokensStates[5],
|
||||
),
|
||||
),
|
||||
TokenList.GroupedByNetwork.NetworkGroup(
|
||||
network = MockTokens.token10.network,
|
||||
currencies = listOf(
|
||||
MockTokensStates.loadedTokensStates[6],
|
||||
MockTokensStates.loadedTokensStates[7],
|
||||
MockTokensStates.loadedTokensStates[8],
|
||||
MockTokensStates.loadedTokensStates[9],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create GROUPED token list WITH SORTING BY BALANCE`() {
|
||||
// Act
|
||||
val actual = TokenListFactory.create(
|
||||
statuses = MockTokensStates.loadedTokensStates,
|
||||
groupType = TokensGroupType.NETWORK,
|
||||
sortType = TokensSortType.BALANCE,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = TokenList.GroupedByNetwork(
|
||||
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal("599.50"), source = StatusSource.ACTUAL),
|
||||
sortedBy = TokensSortType.BALANCE,
|
||||
groups = listOf(
|
||||
TokenList.GroupedByNetwork.NetworkGroup(
|
||||
network = MockTokens.token10.network,
|
||||
currencies = listOf(
|
||||
MockTokensStates.loadedTokensStates[9],
|
||||
MockTokensStates.loadedTokensStates[8],
|
||||
MockTokensStates.loadedTokensStates[7],
|
||||
MockTokensStates.loadedTokensStates[6],
|
||||
),
|
||||
),
|
||||
TokenList.GroupedByNetwork.NetworkGroup(
|
||||
network = MockTokens.token6.network,
|
||||
currencies = listOf(
|
||||
MockTokensStates.loadedTokensStates[5],
|
||||
MockTokensStates.loadedTokensStates[4],
|
||||
MockTokensStates.loadedTokensStates[3],
|
||||
),
|
||||
),
|
||||
TokenList.GroupedByNetwork.NetworkGroup(
|
||||
network = MockTokens.token3.network,
|
||||
currencies = listOf(
|
||||
MockTokensStates.loadedTokensStates[2],
|
||||
MockTokensStates.loadedTokensStates[1],
|
||||
MockTokensStates.loadedTokensStates[0],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sorting will not be applied if any status is Loading`() {
|
||||
// Arrange
|
||||
val statuses = listOf(
|
||||
CryptoCurrencyStatus(currency = MockTokens.token1, value = CryptoCurrencyStatus.Loading),
|
||||
MockTokensStates.tokenState10,
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = TokenListFactory.create(
|
||||
statuses = statuses,
|
||||
groupType = TokensGroupType.NONE,
|
||||
sortType = TokensSortType.BALANCE,
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = TokenList.Ungrouped(
|
||||
totalFiatBalance = TotalFiatBalance.Loading,
|
||||
sortedBy = TokensSortType.BALANCE,
|
||||
currencies = statuses,
|
||||
)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue