Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-25 13:53:04 +04:00
parent c56b64bc36
commit 4bb8d20301
7 changed files with 1021 additions and 27 deletions

View file

@ -5,23 +5,28 @@ import arrow.core.some
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.*
/**
* Produces a flow of [AccountStatusList] for multiple user wallets.
*
* @property params Parameters for the producer (currently unused).
* @property userWalletsListRepository Repository to get the list of user wallets.
* @property singleAccountStatusListSupplier Supplier to get the account status list for a single user wallet.
* @property dispatchers Coroutine dispatcher provider for managing threading.
*
[REDACTED_AUTHOR]
*/
// TODO: Finalize [REDACTED_JIRA]
internal class DefaultMultiAccountStatusListProducer @AssistedInject constructor(
@Assisted val params: Unit,
private val userWalletsListRepository: UserWalletsListRepository,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiAccountStatusListProducer {
override val fallback: Option<List<AccountStatusList>> = emptyList<AccountStatusList>().some()
@ -39,6 +44,7 @@ internal class DefaultMultiAccountStatusListProducer @AssistedInject constructor
combine(flows) { it.toList() }
}
.flowOn(dispatchers.default)
}
@AssistedFactory

View file

@ -3,45 +3,121 @@ package com.tangem.domain.account.status.producer
import arrow.core.Option
import arrow.core.none
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusesFlowFactory
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.quote.PriceChange
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.operations.PriceChangeCalculator
import com.tangem.domain.tokens.operations.TokenListFactory
import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.*
import java.math.BigDecimal
/**
* Produces a flow of [AccountStatusList] for a single user wallet.
*
* @property params Parameters containing the user wallet ID.
* @property singleAccountListSupplier Supplier to get the list of accounts for the user wallet.
* @property cryptoCurrencyStatusesFlowFactory Factory to create flows of cryptocurrency statuses.
* @property dispatchers Coroutine dispatcher provider for managing threading.
*
[REDACTED_AUTHOR]
*/
// TODO: Implement [REDACTED_JIRA]
@Suppress("UnusedPrivateProperty", "UnusedPrivateClass")
@OptIn(ExperimentalCoroutinesApi::class)
internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor(
@Assisted private val params: SingleAccountStatusListProducer.Params,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
private val stakingIdFactory: StakingIdFactory,
private val cryptoCurrencyStatusesFlowFactory: CryptoCurrencyStatusesFlowFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : SingleAccountStatusListProducer {
override val fallback: Option<AccountStatusList> = none()
override fun produce(): Flow<AccountStatusList> = emptyFlow()
override fun produce(): Flow<AccountStatusList> {
val accountListFlow = singleAccountListSupplier(
params = SingleAccountListProducer.Params(params.userWalletId),
)
private data class CryptoCurrencyStatusSources(
val networkStatus: NetworkStatus,
val yieldBalance: YieldBalance?,
val quoteStatus: QuoteStatus?,
)
return accountListFlow.flatMapLatest { accountList ->
val accountStatusFlows = accountList.accounts
.filterIsInstance<Account.CryptoPortfolio>()
.map { account ->
if (account.cryptoCurrencies.isEmpty()) {
createEmptyAccountStatusFlow(account)
} else {
getAccountStatusFlow(
userWallet = accountList.userWallet,
account = account,
groupType = accountList.groupType,
sortType = accountList.sortType,
)
}
}
combine(accountStatusFlows) { accountStatuses ->
val balances = accountStatuses.map { it.tokenList.totalFiatBalance }
AccountStatusList(
userWallet = accountList.userWallet,
accountStatuses = accountStatuses.toSet(),
totalAccounts = accountList.totalAccounts,
totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances),
)
}
}
.flowOn(dispatchers.default)
}
private fun createEmptyAccountStatusFlow(account: Account.CryptoPortfolio): Flow<AccountStatus.CryptoPortfolio> {
return flowOf(
AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Empty,
priceChangeLce = PriceChange(
value = BigDecimal.ZERO.movePointLeft(2),
source = StatusSource.ACTUAL,
).lceContent(),
),
)
}
private fun getAccountStatusFlow(
userWallet: UserWallet,
account: Account.CryptoPortfolio,
groupType: TokensGroupType,
sortType: TokensSortType,
): Flow<AccountStatus.CryptoPortfolio> {
val statusesFlows = account.cryptoCurrencies.map { currency ->
cryptoCurrencyStatusesFlowFactory.create(userWallet = userWallet, currency = currency)
}
return combine(statusesFlows) { statuses ->
val statusList = statuses.toList()
AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenListFactory.create(
statuses = statusList,
groupType = groupType,
sortType = sortType,
),
priceChangeLce = PriceChangeCalculator.calculate(statuses = statusList),
)
}
}
@AssistedFactory
interface Factory : SingleAccountStatusListProducer.Factory {

View file

@ -0,0 +1,162 @@
package com.tangem.domain.account.status.utils
import arrow.core.some
import arrow.core.toOption
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.network.getAddress
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import javax.inject.Inject
/**
* Factory that creates a flow of [CryptoCurrencyStatus] for a given [CryptoCurrency] in a [UserWallet].
*
* @property singleNetworkStatusSupplier Supplier for obtaining network status.
* @property singleQuoteStatusSupplier Supplier for obtaining quote status.
* @property singleYieldBalanceSupplier Supplier for obtaining yield balance.
* @property stakingIdFactory Factory for creating staking IDs.
*
[REDACTED_AUTHOR]
*/
internal class CryptoCurrencyStatusesFlowFactory @Inject constructor(
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
private val stakingIdFactory: StakingIdFactory,
) {
/**
* Creates a flow of [CryptoCurrencyStatus] for the specified [userWallet] and [currency].
*
* @param userWallet The user wallet containing the currency.
*/
fun create(userWallet: UserWallet, currency: CryptoCurrency): Flow<CryptoCurrencyStatus> {
return getCryptoCurrencyStatusSourcesFlow(userWallet = userWallet, currency = currency)
.map { statusSources ->
CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = statusSources.networkStatus.some(),
maybeQuoteStatus = statusSources.quoteStatus.toOption(),
maybeYieldBalance = statusSources.yieldBalance.toOption(),
)
}
.onEmpty {
emit(
CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading),
)
}
.conflate()
.distinctUntilChanged()
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun getCryptoCurrencyStatusSourcesFlow(
userWallet: UserWallet,
currency: CryptoCurrency,
): Flow<CryptoCurrencyStatusSources> {
val networkStatusFlow = getNetworkStatusFlow(userWalletId = userWallet.walletId, network = currency.network)
val yieldBalanceFlow = if (userWallet.isMultiCurrency) {
networkStatusFlow.flatMapLatest { networkStatus ->
getYieldBalanceFlow(
userWalletId = userWallet.walletId,
currencyId = currency.id,
networkStatus = networkStatus,
)
}
} else {
null
}
val quoteStatusFlow = currency.id.rawCurrencyId?.let(::getQuoteStatusFlow)
return combine(networkStatusFlow, yieldBalanceFlow, quoteStatusFlow)
}
private fun combine(
networkStatusFlow: Flow<NetworkStatus>,
yieldBalanceFlow: Flow<YieldBalance?>?,
quoteStatusFlow: Flow<QuoteStatus>?,
): Flow<CryptoCurrencyStatusSources> {
return when {
yieldBalanceFlow != null && quoteStatusFlow != null -> {
combine(
flow = networkStatusFlow,
flow2 = yieldBalanceFlow,
flow3 = quoteStatusFlow,
transform = ::CryptoCurrencyStatusSources,
)
}
yieldBalanceFlow != null -> {
combine(flow = networkStatusFlow, flow2 = yieldBalanceFlow, transform = ::CryptoCurrencyStatusSources)
}
quoteStatusFlow != null -> {
combine(flow = networkStatusFlow, flow2 = quoteStatusFlow) { networkStatus, quoteStatus ->
CryptoCurrencyStatusSources(networkStatus = networkStatus, quoteStatus = quoteStatus)
}
}
else -> networkStatusFlow.map(::CryptoCurrencyStatusSources)
}
}
private fun getNetworkStatusFlow(userWalletId: UserWalletId, network: Network): Flow<NetworkStatus> {
return singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = network),
)
.conflate()
.distinctUntilChanged()
}
private fun getQuoteStatusFlow(rawCurrencyId: CryptoCurrency.RawID): Flow<QuoteStatus> {
return singleQuoteStatusSupplier(
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
)
.conflate()
.distinctUntilChanged()
}
private fun getYieldBalanceFlow(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
networkStatus: NetworkStatus,
): Flow<YieldBalance?> {
val stakingId = stakingIdFactory.create(
currencyId = currencyId,
defaultAddress = networkStatus.getAddress(),
)
.getOrNull()
return if (stakingId != null) {
singleYieldBalanceSupplier(
params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId),
)
.conflate()
.distinctUntilChanged()
} else {
flowOf(null)
}
}
private data class CryptoCurrencyStatusSources(
val networkStatus: NetworkStatus,
val yieldBalance: YieldBalance? = null,
val quoteStatus: QuoteStatus? = null,
)
}

View file

@ -0,0 +1,198 @@
package com.tangem.domain.account.status.producer
import com.google.common.truth.Truth
import com.tangem.common.test.utils.getEmittedValues
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@Suppress("UnusedFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultMultiAccountStatusListProducerTest {
private val userWalletsListRepository: UserWalletsListRepository = mockk()
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk()
private val dispatchers = TestingCoroutineDispatcherProvider()
private val userWalletId1 = UserWalletId("001")
private val userWallet1 = mockk<UserWallet> {
every { walletId } returns userWalletId1
}
private val userWalletId2 = UserWalletId("002")
private val userWallet2 = mockk<UserWallet> {
every { walletId } returns userWalletId2
}
private val producer = DefaultMultiAccountStatusListProducer(
params = Unit,
userWalletsListRepository = userWalletsListRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
dispatchers = dispatchers,
)
@AfterEach
fun tearDown() {
clearMocks(userWalletsListRepository, singleAccountStatusListSupplier)
}
@Test
fun `produce returns status lists for all user wallets`() = runTest {
// Arrange
val wallets = listOf(userWallet1, userWallet2)
val walletsFlow = MutableStateFlow(wallets)
every { userWalletsListRepository.userWallets } returns walletsFlow
val accountStatusList1 = mockk<AccountStatusList>()
val accountStatusList2 = mockk<AccountStatusList>()
every {
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId1),
)
} returns flowOf(accountStatusList1)
every {
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId2),
)
} returns flowOf(accountStatusList2)
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
val expected = listOf(accountStatusList1, accountStatusList2)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsListRepository.userWallets
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId1),
)
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId2),
)
}
}
@Test
fun `produce returns empty flow if userWallets is empty list`() = runTest {
// Arrange
val walletsFlow = MutableStateFlow<List<UserWallet>>(emptyList())
every { userWalletsListRepository.userWallets } returns walletsFlow
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty()
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsListRepository.userWallets
}
}
@Test
fun `produce returns empty flow if userWallets is null`() = runTest {
// Arrange
val walletsFlow = MutableStateFlow<List<UserWallet>?>(null)
every { userWalletsListRepository.userWallets } returns walletsFlow
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
Truth.assertThat(actual).isEmpty()
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsListRepository.userWallets
}
}
@Test
fun `flow will updated if userWallets are updated`() = runTest {
// Arrange
val userWalletId3 = UserWalletId("003")
val userWallet3 = mockk<UserWallet> { every { walletId } returns userWalletId3 }
val walletsFlow = MutableStateFlow(listOf(userWallet1, userWallet2))
every { userWalletsListRepository.userWallets } returns walletsFlow
val accountStatusList1 = mockk<AccountStatusList>()
val accountStatusList2 = mockk<AccountStatusList>()
val accountStatusList3 = mockk<AccountStatusList>()
every {
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId1),
)
} returns flowOf(accountStatusList1)
every {
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId2),
)
} returns flowOf(accountStatusList2)
every {
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId3),
)
} returns flowOf(accountStatusList3)
// Act (first emission)
val actual1 = producer.produce().let(::getEmittedValues)
// Assert (first emission)
val expected1 = listOf(accountStatusList1, accountStatusList2)
Truth.assertThat(actual1).containsExactly(expected1)
// Act (second emission)
walletsFlow.value = listOf(userWallet1, userWallet2, userWallet3)
val actual2 = producer.produce().let(::getEmittedValues)
// Assert (second emission)
val expected2 = listOf(accountStatusList1, accountStatusList2, accountStatusList3)
Truth.assertThat(actual2).containsExactly(expected2)
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsListRepository.userWallets
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId1),
)
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId2),
)
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId1),
)
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId2),
)
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId3),
)
userWalletsListRepository.userWallets
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId1),
)
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId2),
)
singleAccountStatusListSupplier(
params = SingleAccountStatusListProducer.Params(userWalletId3),
)
}
}
}

View file

@ -0,0 +1,244 @@
package com.tangem.domain.account.status.producer
import arrow.core.nonEmptyListOf
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.utils.getEmittedValues
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusesFlowFactory
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.quote.PriceChange
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@Suppress("UnusedFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultSingleAccountStatusListProducerTest {
private val singleAccountListSupplier: SingleAccountListSupplier = mockk()
private val cryptoCurrencyStatusesFlowFactory: CryptoCurrencyStatusesFlowFactory = mockk()
private val userWalletId = UserWalletId("011")
private val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
}
private val producer = DefaultSingleAccountStatusListProducer(
params = SingleAccountStatusListProducer.Params(userWalletId),
singleAccountListSupplier = singleAccountListSupplier,
cryptoCurrencyStatusesFlowFactory = cryptoCurrencyStatusesFlowFactory,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@AfterEach
fun tearDown() {
clearMocks(singleAccountListSupplier, cryptoCurrencyStatusesFlowFactory)
}
@Test
fun `flow is mapped for user wallet id from params`() = runTest {
// Arrange
val accountList = AccountList.empty(userWallet)
every {
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
} returns flowOf(accountList)
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
val expected = AccountStatusList(
userWallet = userWallet,
accountStatuses = setOf(
AccountStatus.CryptoPortfolio(
account = accountList.mainAccount,
tokenList = TokenList.Empty,
priceChangeLce = PriceChange(value = BigDecimal("0.00"), source = StatusSource.ACTUAL).lceContent(),
),
),
totalAccounts = 1,
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
}
}
@Test
fun `flow will updated if balances are updated`() = runTest {
// Arrange
val accountList = AccountList.empty(userWallet)
val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.BALANCE)
val accountListFlow = MutableStateFlow(value = accountList)
every {
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
} returns accountListFlow
// Act (first emission)
val actual1 = producer.produce().let(::getEmittedValues)
// Assert (first emission)
val expected = AccountStatusList(
userWallet = userWallet,
accountStatuses = setOf(
AccountStatus.CryptoPortfolio(
account = accountList.mainAccount,
tokenList = TokenList.Empty,
priceChangeLce = PriceChange(value = BigDecimal("0.00"), source = StatusSource.ACTUAL).lceContent(),
),
),
totalAccounts = 1,
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
)
Truth.assertThat(actual1).containsExactly(expected)
// Act (second emission)
accountListFlow.value = updatedAccountList
val actual2 = producer.produce().let(::getEmittedValues)
// Assert (second emission)
val expected2 = AccountStatusList(
userWallet = userWallet,
accountStatuses = setOf(
AccountStatus.CryptoPortfolio(
account = updatedAccountList.mainAccount,
tokenList = TokenList.Empty,
priceChangeLce = PriceChange(value = BigDecimal("0.00"), source = StatusSource.ACTUAL).lceContent(),
),
),
totalAccounts = 1,
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
)
Truth.assertThat(actual2).containsExactly(expected2)
coVerify(ordering = Ordering.SEQUENCE) {
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
}
}
@Test
fun `flow is filtered the same balance`() = runTest {
// Arrange
val accountList = AccountList.empty(userWallet)
val accountListFlow = MutableStateFlow(value = accountList)
every {
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
} returns accountListFlow
val expected = AccountStatusList(
userWallet = userWallet,
accountStatuses = setOf(
AccountStatus.CryptoPortfolio(
account = accountList.mainAccount,
tokenList = TokenList.Empty,
priceChangeLce = PriceChange(value = BigDecimal("0.00"), source = StatusSource.ACTUAL).lceContent(),
),
),
totalAccounts = 1,
totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL),
)
// Act (first emission)
val actual1 = producer.produce().let(::getEmittedValues)
// Assert (first emission)
Truth.assertThat(actual1).containsExactly(expected)
// Act (second emission)
accountListFlow.value = accountList
val actual2 = producer.produce().let(::getEmittedValues)
// Assert (second emission)
Truth.assertThat(actual2).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
}
}
@Test
fun `flow is produced for account with non empty crypto currencies`() = runTest {
// Arrange
val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
val accountList = AccountList.empty(
userWallet = userWallet,
cryptoCurrencies = cryptoCurrencyFactory.ethereumAndStellar.toSet(),
)
every {
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
} returns flowOf(accountList)
val ethereumStatus = CryptoCurrencyStatus(
currency = cryptoCurrencyFactory.ethereum,
value = CryptoCurrencyStatus.Loading,
)
every {
cryptoCurrencyStatusesFlowFactory.create(userWallet = userWallet, currency = cryptoCurrencyFactory.ethereum)
} returns flowOf(ethereumStatus)
val stellarStatus = CryptoCurrencyStatus(
currency = cryptoCurrencyFactory.stellar,
value = CryptoCurrencyStatus.MissedDerivation(priceChange = null, fiatRate = null),
)
every {
cryptoCurrencyStatusesFlowFactory.create(userWallet = userWallet, currency = cryptoCurrencyFactory.stellar)
} returns flowOf(stellarStatus)
// Act
val actual = producer.produce().let(::getEmittedValues)
// Assert
val expected = AccountStatusList(
userWallet = userWallet,
accountStatuses = setOf(
AccountStatus.CryptoPortfolio(
account = accountList.mainAccount,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = nonEmptyListOf(ethereumStatus, stellarStatus),
),
priceChangeLce = lceLoading(),
),
),
totalAccounts = 1,
totalFiatBalance = TotalFiatBalance.Loading,
)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId))
}
}
}

View file

@ -0,0 +1,308 @@
package com.tangem.domain.account.status.utils
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.common.test.utils.getEmittedValues
import com.tangem.domain.models.StatusSource
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.network.NetworkStatus
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import io.mockk.*
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@Suppress("UnusedFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CryptoCurrencyStatusesFlowFactoryTest {
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier = mockk()
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk()
private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier = mockk()
private val stakingIdFactory: StakingIdFactory = mockk()
private val factory = CryptoCurrencyStatusesFlowFactory(
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
stakingIdFactory = stakingIdFactory,
)
private val userWalletId = UserWalletId("011")
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "0x1", type = NetworkAddress.Address.Type.Primary),
)
@AfterEach
fun tearDown() {
clearMocks(
singleNetworkStatusSupplier,
singleQuoteStatusSupplier,
singleYieldBalanceSupplier,
stakingIdFactory,
)
}
@Test
fun `if rawCurrencyId is null, there will be no subscription to the quote status`() = runTest {
// Arrange
val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
every { this@mockk.isMultiCurrency } returns true
}
val currency = cryptoCurrencyFactory.ethereum.copy(
id = cryptoCurrencyFactory.ethereum.id.copy(
suffix = CryptoCurrency.ID.Suffix.ContractAddress(contractAddress = "0x12345"),
),
)
val networkStatus = NetworkStatus(
network = currency.network,
value = NetworkStatus.Unreachable(address = networkAddress),
)
val networkStatusFlow = flowOf(networkStatus)
every {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
} returns networkStatusFlow
val stakingId = StakingID(integrationId = "id", address = networkAddress.defaultAddress.value)
coEvery {
stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value)
} returns stakingId.right()
val yieldBalance = YieldBalance.Empty(stakingId = stakingId, source = StatusSource.ACTUAL)
val yieldBalanceFlow = flowOf(yieldBalance)
every {
singleYieldBalanceSupplier(
params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId),
)
} returns yieldBalanceFlow
// Act
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
// Assert
val expected = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Unreachable(
priceChange = null,
fiatRate = null,
networkAddress = networkAddress,
),
)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value)
singleYieldBalanceSupplier(params = SingleYieldBalanceProducer.Params(userWalletId, stakingId))
}
}
@Test
fun `if userWallet is not multi-currency, there will be no subscription to the yield balance`() = runTest {
// Arrange
val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
every { this@mockk.isMultiCurrency } returns false
}
val currency = cryptoCurrencyFactory.ethereum
val networkStatus = NetworkStatus(
network = currency.network,
value = NetworkStatus.Unreachable(address = networkAddress),
)
val networkStatusFlow = flowOf(networkStatus)
every {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
} returns networkStatusFlow
val quoteStatus = QuoteStatus(
rawCurrencyId = currency.id.rawCurrencyId!!,
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ONE,
),
)
val quoteStatusFlow = flowOf(quoteStatus)
every {
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
} returns quoteStatusFlow
// Act
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
// Assert
val expected = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Unreachable(
priceChange = BigDecimal.ONE,
fiatRate = BigDecimal.ONE,
networkAddress = networkAddress,
),
)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
}
}
@Test
fun `no subscription to the quote status and yield balance`() = runTest {
// Arrange
val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
every { this@mockk.isMultiCurrency } returns false
}
val currency = cryptoCurrencyFactory.ethereum.copy(
id = cryptoCurrencyFactory.ethereum.id.copy(
suffix = CryptoCurrency.ID.Suffix.ContractAddress(contractAddress = "0x12345"),
),
)
val networkStatus = NetworkStatus(
network = currency.network,
value = NetworkStatus.Unreachable(address = networkAddress),
)
val networkStatusFlow = flowOf(networkStatus)
every {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
} returns networkStatusFlow
// Act
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
// Assert
val expected = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Unreachable(
priceChange = null,
fiatRate = null,
networkAddress = networkAddress,
),
)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
}
}
@Test
fun `if stakingId is not supported, yield balance will be null`() = runTest {
// Arrange
val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
every { this@mockk.isMultiCurrency } returns true
}
val currency = cryptoCurrencyFactory.ethereum
val networkStatus = NetworkStatus(
network = currency.network,
value = NetworkStatus.Unreachable(address = networkAddress),
)
val networkStatusFlow = flowOf(networkStatus)
every {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
} returns networkStatusFlow
val quoteStatus = QuoteStatus(
rawCurrencyId = currency.id.rawCurrencyId!!,
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ONE,
),
)
val quoteStatusFlow = flowOf(quoteStatus)
every {
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
} returns quoteStatusFlow
coEvery {
stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value)
} returns StakingIdFactory.Error.UnsupportedCurrency.left()
// Act
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
// Assert
val expected = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Unreachable(
priceChange = BigDecimal.ONE,
fiatRate = BigDecimal.ONE,
networkAddress = networkAddress,
),
)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
}
}
@Test
fun `all sources are empty`() = runTest {
// Arrange
val userWallet = mockk<UserWallet> {
every { this@mockk.walletId } returns userWalletId
every { this@mockk.isMultiCurrency } returns true
}
val currency = cryptoCurrencyFactory.ethereum
every {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
} returns emptyFlow()
every {
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
} returns emptyFlow()
// Act
val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues)
// Assert
val expected = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
Truth.assertThat(actual).containsExactly(expected)
coVerify(ordering = Ordering.SEQUENCE) {
singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network))
singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(currency.id.rawCurrencyId!!))
}
}
}

View file

@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
/**
* Represents the price change of a cryptocurrency asset over a specific time period.
*
* @param value The amount of price change.
* @param value The amount of price change (like `0.00`)
* @param source The source of the price change information.
*
[REDACTED_AUTHOR]