Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-05 14:04:36 +04:00
parent 41171e9e0c
commit 97363be1e1
7 changed files with 560 additions and 2 deletions

View file

@ -19,7 +19,7 @@ internal class MultiWalletCryptoCurrenciesSupplierModule {
): MultiWalletCryptoCurrenciesSupplier {
return object : MultiWalletCryptoCurrenciesSupplier(
factory = factory,
keyCreator = { "multi_crypto_currency_${it.userWalletId}" },
keyCreator = { "multi_crypto_currency_${it.userWalletId.stringValue}" },
) {}
}
}

View file

@ -21,7 +21,7 @@ internal object AccountStatusListSupplierModule {
): SingleAccountStatusListSupplier {
return object : SingleAccountStatusListSupplier(
factory = factory,
keyCreator = { "account_status_list_${it.userWalletId}" },
keyCreator = { "account_status_list_${it.userWalletId.stringValue}" },
) {}
}

View file

@ -0,0 +1,61 @@
package com.tangem.domain.account.status.usecase
import arrow.core.Either
import arrow.core.raise.ensure
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.core.utils.eitherOn
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.operations.TokenListFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* Use case to toggle the grouping of token lists in account statuses.
*
* If the token list is currently ungrouped, it will be grouped by network,
* and vice versa. The use case ensures that the token list is not empty
* and not in a loading state before performing the toggle operation.
*
* @property dispatchers Provides coroutine dispatchers for executing tasks.
*/
class ToggleTokenListGroupingUseCaseV2(
private val dispatchers: CoroutineDispatcherProvider,
) {
/**
* Toggles the grouping of token lists in the provided [accountStatusList].
*
* @param accountStatusList The list of account statuses containing token lists to be toggled.
* @return Either a [TokenListSortingError] if an error occurs, or the updated [AccountStatusList]
* with toggled token list grouping.
*/
suspend operator fun invoke(
accountStatusList: AccountStatusList,
): Either<TokenListSortingError, AccountStatusList> = eitherOn(dispatchers.default) {
ensure(accountStatusList.flattenCurrencies().isNotEmpty()) {
raise(TokenListSortingError.TokenListIsEmpty)
}
ensure(accountStatusList.totalFiatBalance !is TotalFiatBalance.Loading) {
TokenListSortingError.TokenListIsLoading
}
accountStatusList.copy(
accountStatuses = accountStatusList.accountStatuses.map { account ->
if (account !is AccountStatus.CryptoPortfolio) return@map account
account.copy(tokenList = account.tokenList.reverseGroupType())
},
)
}
private fun TokenList.reverseGroupType(): TokenList {
return when (this) {
is TokenList.Ungrouped -> TokenListFactory.createGroupedByNetwork(this)
is TokenList.GroupedByNetwork -> TokenListFactory.createUngrouped(this)
is TokenList.Empty -> this
}
}
}

View file

@ -0,0 +1,71 @@
package com.tangem.domain.account.status.usecase
import arrow.core.Either
import arrow.core.raise.ensure
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.core.utils.eitherOn
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.operations.TokenListFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* Use case to toggle the sorting of token lists in account statuses to be sorted by balance.
*
* The use case ensures that the token list is not empty
* and not in a loading state before performing the sorting operation.
*
* @property dispatchers Provides coroutine dispatchers for executing tasks.
*/
class ToggleTokenListSortingUseCaseV2(
private val dispatchers: CoroutineDispatcherProvider,
) {
/**
* Toggles the sorting of token lists in the provided [accountStatusList] to be sorted by balance.
*
* @param accountStatusList The list of account statuses containing token lists to be sorted.
* @return Either a [TokenListSortingError] if an error occurs, or the updated [AccountStatusList]
* with token lists sorted by balance.
*/
suspend operator fun invoke(
accountStatusList: AccountStatusList,
): Either<TokenListSortingError, AccountStatusList> = eitherOn(dispatchers.default) {
ensure(accountStatusList.flattenCurrencies().isNotEmpty()) {
raise(TokenListSortingError.TokenListIsEmpty)
}
ensure(accountStatusList.totalFiatBalance !is TotalFiatBalance.Loading) {
TokenListSortingError.TokenListIsLoading
}
accountStatusList.copy(
accountStatuses = accountStatusList.accountStatuses.map { account ->
if (account !is AccountStatus.CryptoPortfolio) return@map account
account.copy(tokenList = account.tokenList.sortByBalance())
},
sortType = TokensSortType.BALANCE,
)
}
private fun TokenList.sortByBalance(): TokenList {
if (this is TokenList.Empty) return this
return TokenListFactory.create(
statuses = flattenCurrencies(),
groupType = when (this) {
is TokenList.GroupedByNetwork -> TokensGroupType.NETWORK
is TokenList.Ungrouped -> TokensGroupType.NONE
is TokenList.Empty -> {
return this
}
},
sortType = TokensSortType.BALANCE,
)
}
}

View file

@ -0,0 +1,213 @@
package com.tangem.domain.account.status.usecase
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.utils.createGroupedByNetwork
import com.tangem.domain.account.status.utils.createStatus
import com.tangem.domain.account.status.utils.createUngrouped
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ToggleTokenListGroupingUseCaseV2Test {
private val useCase = ToggleTokenListGroupingUseCaseV2(dispatchers = TestingCoroutineDispatcherProvider())
private val userWalletId = UserWalletId("011")
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
@Test
fun `when list is empty then error should be received`() = runTest {
// Arrange
val accountStatusList = AccountStatusList(
userWalletId = userWalletId,
accountStatuses = emptyList(),
totalAccounts = 0,
totalFiatBalance = TotalFiatBalance.Failed,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
// Act
val actual = useCase(accountStatusList)
// Assert
val expected = TokenListSortingError.TokenListIsEmpty.left()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when totalFiatBalance is loading then error should be received`() = runTest {
// Arrange
val tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = listOf(mockk(relaxed = true)),
)
val accountStatusList = createAccountStatusList(tokenList)
// Act
val actual = useCase(accountStatusList)
// Assert
val expected = TokenListSortingError.TokenListIsLoading.left()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is ungrouped and sorted then sorted grouped list should be received`() = runTest {
// Arrange
val tokenList = createUngrouped(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
),
sortedBy = TokensSortType.BALANCE,
)
val accountStatusList = createAccountStatusList(tokenList)
val updatedTokenList = createGroupedByNetwork(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
),
sortedBy = TokensSortType.BALANCE,
)
// Act
val actual = useCase(accountStatusList)
// Assert
val expected = createAccountStatusList(updatedTokenList).right()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is ungrouped and unsorted then unsorted grouped list should be received`() = runTest {
// Arrange
val tokenList = createUngrouped(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
),
)
val accountStatusList = createAccountStatusList(tokenList)
val updatedTokenList = createGroupedByNetwork(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
),
)
// Act
val actual = useCase(accountStatusList)
// Assert
val expected = createAccountStatusList(updatedTokenList).right()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is grouped and sorted then sorted ungrouped list should be received`() = runTest {
// Arrange
val tokenList = createGroupedByNetwork(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
),
sortedBy = TokensSortType.BALANCE,
)
val accountStatusList = createAccountStatusList(tokenList)
val updatedTokenList = createUngrouped(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
),
sortedBy = TokensSortType.BALANCE,
)
// Act
val actual = useCase(accountStatusList)
// Assert
val expected = createAccountStatusList(updatedTokenList).right()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is grouped and unsorted then unsorted ungrouped list should be received`() = runTest {
// Arrange
val tokenList = createGroupedByNetwork(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
),
)
val accountStatusList = createAccountStatusList(tokenList)
val updatedTokenList = createUngrouped(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
),
)
// Act
val actual = useCase(accountStatusList)
// Assert
val expected = createAccountStatusList(updatedTokenList).right()
Truth.assertThat(actual).isEqualTo(expected)
}
private fun createAccountStatusList(tokenList: TokenList): AccountStatusList {
val accountStatus = AccountStatus.CryptoPortfolio(
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
tokenList = tokenList,
priceChangeLce = Unit.lceError(),
)
return AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(accountStatus),
totalAccounts = 1,
totalFiatBalance = tokenList.totalFiatBalance,
sortType = tokenList.sortedBy,
groupType = TokensGroupType.NONE,
)
}
}

View file

@ -0,0 +1,153 @@
package com.tangem.domain.account.status.usecase
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.utils.createGroupedByNetwork
import com.tangem.domain.account.status.utils.createStatus
import com.tangem.domain.account.status.utils.createUngrouped
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ToggleTokenListSortingUseCaseV2Test {
private val useCase = ToggleTokenListSortingUseCaseV2(dispatchers = TestingCoroutineDispatcherProvider())
private val userWalletId = UserWalletId("011")
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
@Test
fun `when list is empty then error should be received`() = runTest {
// Arrange
val accountStatusList = AccountStatusList(
userWalletId = userWalletId,
accountStatuses = emptyList(),
totalAccounts = 0,
totalFiatBalance = TotalFiatBalance.Failed,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
// Act
val actual = useCase(accountStatusList)
// Assert
val expected = TokenListSortingError.TokenListIsEmpty.left()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when totalFiatBalance is loading then error should be received`() = runTest {
// Arrange
val tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = listOf(mockk(relaxed = true)),
)
val accountStatusList = createAccountStatusList(tokenList)
// Act
val actual = useCase(accountStatusList)
// Assert
val expected = TokenListSortingError.TokenListIsLoading.left()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is grouped and unsorted then grouped and sorted list should be received`() = runTest {
// Arrange
val tokenList = createGroupedByNetwork(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
),
)
val accountStatusList = createAccountStatusList(tokenList)
val updatedTokenList = createGroupedByNetwork(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
),
sortedBy = TokensSortType.BALANCE,
)
// Act
val actual = useCase(accountStatusList)
// Assert
val expected = createAccountStatusList(updatedTokenList).right()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `when list is ungrouped and unsorted then ungrouped and sorted list should be received`() = runTest {
// Arrange
val tokenList = createUngrouped(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
),
)
val accountStatusList = createAccountStatusList(tokenList)
val updatedTokenList = createUngrouped(
statuses = listOf(
createStatus(currency = cryptoCurrencyFactory.chia, fiatAmount = BigDecimal.TEN),
createStatus(currency = cryptoCurrencyFactory.ethereum, fiatAmount = BigDecimal.ONE),
createStatus(currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO),
),
sortedBy = TokensSortType.BALANCE,
)
// Act
val actual = useCase(accountStatusList)
// Assert
val expected = createAccountStatusList(updatedTokenList).right()
Truth.assertThat(actual).isEqualTo(expected)
}
private fun createAccountStatusList(tokenList: TokenList): AccountStatusList {
val accountStatus = AccountStatus.CryptoPortfolio(
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
tokenList = tokenList,
priceChangeLce = Unit.lceError(),
)
return AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(accountStatus),
totalAccounts = 1,
totalFiatBalance = tokenList.totalFiatBalance,
sortType = tokenList.sortedBy,
groupType = TokensGroupType.NONE,
)
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.domain.account.status.utils
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.tokenlist.TokenList
import java.math.BigDecimal
internal fun createGroupedByNetwork(
statuses: List<CryptoCurrencyStatus>,
sortedBy: TokensSortType = TokensSortType.NONE,
): TokenList {
return TokenList.GroupedByNetwork(
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal("11"), source = StatusSource.ACTUAL),
sortedBy = sortedBy,
groups = statuses.map {
TokenList.GroupedByNetwork.NetworkGroup(
network = it.currency.network,
currencies = listOf(it),
)
},
)
}
internal fun createUngrouped(
statuses: List<CryptoCurrencyStatus>,
sortedBy: TokensSortType = TokensSortType.NONE,
): TokenList {
return TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal("11"), source = StatusSource.ACTUAL),
sortedBy = sortedBy,
currencies = statuses,
)
}
internal fun createStatus(currency: CryptoCurrency, fiatAmount: BigDecimal): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loaded(
amount = fiatAmount,
fiatRate = BigDecimal.ONE,
fiatAmount = fiatAmount,
priceChange = BigDecimal.ZERO,
yieldBalance = null,
hasCurrentNetworkTransactions = false,
yieldSupplyStatus = null,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "addr1q9",
type = NetworkAddress.Address.Type.Primary,
),
),
sources = CryptoCurrencyStatus.Sources(),
),
)
}