Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-10 15:53:45 +04:00
parent 926b74d491
commit a9333c10a7
2 changed files with 263 additions and 92 deletions

View file

@ -16,6 +16,8 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapNotNull
/**
* Use case to retrieve the status of a specific cryptocurrency associated with an account.
@ -35,11 +37,8 @@ class GetAccountCurrencyStatusUseCase(
* @param currency the cryptocurrency for which the status is to be retrieved.
* @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None.
*/
suspend operator fun invoke(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Option<AccountCryptoCurrencyStatus> {
return invoke(userWalletId = userWalletId, currencyId = currency.id, network = currency.network)
suspend fun invokeSync(userWalletId: UserWalletId, currency: CryptoCurrency): Option<AccountCryptoCurrencyStatus> {
return invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = currency.network)
}
/**
@ -51,7 +50,7 @@ class GetAccountCurrencyStatusUseCase(
* @param network the network associated with the cryptocurrency, can be null.
* @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None.
*/
suspend operator fun invoke(
suspend fun invokeSync(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
network: Network?,
@ -60,7 +59,48 @@ class GetAccountCurrencyStatusUseCase(
params = SingleAccountStatusListProducer.Params(userWalletId),
) ?: return none()
return accountStatusList.getExpectedAccountStatuses(network)
return accountStatusList
.toAccountCryptoCurrencyStatus(currencyId, network)
.toOption()
}
/**
* Retrieves the status of a specific cryptocurrency for a given user wallet as a [Flow].
*
* @param userWalletId The ID of the user wallet.
* @param currency The cryptocurrency for which the status is to be retrieved.
* @return A [Flow] emitting [AccountCryptoCurrencyStatus] if found.
*/
operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Flow<AccountCryptoCurrencyStatus> {
return invoke(userWalletId = userWalletId, currencyId = currency.id, network = currency.network)
}
/**
* Retrieves the status of a specific cryptocurrency by its ID for a given user wallet and network as a [Flow].
*
* @param userWalletId The ID of the user wallet.
* @param currencyId The ID of the cryptocurrency.
* @param network The network associated with the cryptocurrency, can be null.
* @return A [Flow] emitting [AccountCryptoCurrencyStatus] if found.
*/
operator fun invoke(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
network: Network?,
): Flow<AccountCryptoCurrencyStatus> {
return singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId),
)
.mapNotNull { accountStatusList ->
accountStatusList.toAccountCryptoCurrencyStatus(currencyId, network)
}
}
private fun AccountStatusList.toAccountCryptoCurrencyStatus(
currencyId: CryptoCurrency.ID,
network: Network?,
): AccountCryptoCurrencyStatus? {
return getExpectedAccountStatuses(network)
.asSequence()
.filterIsInstance<AccountStatus.CryptoPortfolio>()
.mapNotNull { accountStatus ->
@ -70,7 +110,6 @@ class GetAccountCurrencyStatusUseCase(
AccountCryptoCurrencyStatus(account = accountStatus.account, status = status)
}
.firstOrNull()
.toOption()
}
/**

View file

@ -1,8 +1,10 @@
package com.tangem.domain.account.status.usecase
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.utils.assertNone
import com.tangem.common.test.utils.assertSome
import com.tangem.common.test.utils.getEmittedValues
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
@ -18,8 +20,11 @@ import com.tangem.domain.models.network.Network
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.*
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@ -47,111 +52,238 @@ class GetAccountCurrencyStatusUseCaseTest {
clearMocks(supplier)
}
@Test
fun `invoke returns None when supplier returns null`() = runTest {
// Arrange
coEvery { supplier.getSyncOrNull(supplierParams) } returns null
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class InvokeSync {
// Act
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
@Test
fun `invokeSync returns None when supplier returns null`() = runTest {
// Arrange
coEvery { supplier.getSyncOrNull(supplierParams) } returns null
// Assert
assertNone(actual)
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
// Act
val actual = useCase.invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = null)
// Assert
assertNone(actual)
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
}
@Test
fun `invokeSync returns None when AccountList does not contain required currency id`() = runTest {
// Arrange
val accountStatus = AccountStatus.CryptoPortfolio(
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
tokenList = TokenList.Empty,
priceChangeLce = lceLoading(),
)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns setOf(accountStatus)
}
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
// Act
val actual = useCase.invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = null)
// Assert
assertNone(actual)
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
}
@Test
fun `invokeSync returns Some if network is not null`() = runTest {
// Arrange
val mainAccountStatus = AccountStatus.CryptoPortfolio(
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
tokenList = TokenList.Empty,
priceChangeLce = lceLoading(),
)
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!!
every { this@mockk.cryptoCurrencies } returns setOf(currency)
}
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = listOf(currencyStatus),
),
priceChangeLce = lceLoading(),
)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk())
}
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
// Act
val actual = useCase.invokeSync(
userWalletId = userWalletId,
currencyId = currency.id,
network = currency.network,
)
// Assert
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
assertSome(actual, expected)
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
}
@Test
fun `invokeSync returns Some if network is null`() = runTest {
// Arrange
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
every { this@mockk.cryptoCurrencies } returns setOf(currency)
}
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = listOf(currencyStatus),
),
priceChangeLce = lceLoading(),
)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns setOf(accountStatus)
}
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
// Act
val actual = useCase.invokeSync(userWalletId = userWalletId, currencyId = currency.id, network = null)
// Assert
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
assertSome(actual, expected)
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
}
}
@Test
fun `invoke returns None when AccountList does not contain required currency id`() = runTest {
// Arrange
val accountStatus = AccountStatus.CryptoPortfolio(
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
tokenList = TokenList.Empty,
priceChangeLce = lceLoading(),
)
@Suppress("UnusedFlow")
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Invoke {
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns setOf(accountStatus)
@Test
fun `invoke returns empty flow when supplier returns empty flow`() = runTest {
// Arrange
coEvery { supplier(supplierParams) } returns emptyFlow()
// Act
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
.let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty()
coVerifyOrder { supplier(supplierParams) }
}
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
@Test
fun `invoke returns empty flow when AccountList does not contain required currency id`() = runTest {
// Arrange
val accountStatus = AccountStatus.CryptoPortfolio(
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
tokenList = TokenList.Empty,
priceChangeLce = lceLoading(),
)
// Act
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns setOf(accountStatus)
}
// Assert
assertNone(actual)
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
}
coEvery { supplier(supplierParams) } returns flowOf(accountStatusList)
@Test
fun `invoke returns Some if network is not null`() = runTest {
// Arrange
val mainAccountStatus = AccountStatus.CryptoPortfolio(
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
tokenList = TokenList.Empty,
priceChangeLce = lceLoading(),
)
// Act
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
.let(::getEmittedValues)
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!!
every { this@mockk.cryptoCurrencies } returns setOf(currency)
}
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = listOf(currencyStatus),
),
priceChangeLce = lceLoading(),
)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk())
// Assert
Truth.assertThat(actual).isEmpty()
coVerifyOrder { supplier(supplierParams) }
}
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
@Test
fun `invoke returns data if network is not null`() = runTest {
// Arrange
val mainAccountStatus = AccountStatus.CryptoPortfolio(
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
tokenList = TokenList.Empty,
priceChangeLce = lceLoading(),
)
// Act
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = currency.network)
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!!
every { this@mockk.cryptoCurrencies } returns setOf(currency)
}
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = listOf(currencyStatus),
),
priceChangeLce = lceLoading(),
)
// Assert
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
assertSome(actual, expected)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk())
}
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
}
coEvery { supplier(supplierParams) } returns flowOf(accountStatusList)
@Test
fun `invoke returns Some if network is null`() = runTest {
// Arrange
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
every { this@mockk.cryptoCurrencies } returns setOf(currency)
}
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = listOf(currencyStatus),
),
priceChangeLce = lceLoading(),
)
// Act
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
.let(::getEmittedValues)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns setOf(accountStatus)
// Assert
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
Truth.assertThat(actual).containsExactly(expected)
coVerifyOrder { supplier(supplierParams) }
}
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
@Test
fun `invoke returns data if network is null`() = runTest {
// Arrange
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
every { this@mockk.cryptoCurrencies } returns setOf(currency)
}
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = listOf(currencyStatus),
),
priceChangeLce = lceLoading(),
)
// Act
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns setOf(accountStatus)
}
// Assert
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
assertSome(actual, expected)
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
coEvery { supplier(supplierParams) } returns flowOf(accountStatusList)
// Act
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
.let(::getEmittedValues)
// Assert
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
Truth.assertThat(actual).containsExactly(expected)
coVerifyOrder { supplier(supplierParams) }
}
}
}