Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-18 14:12:15 +03:00
commit 20279dcc7a
602 changed files with 18887 additions and 6569 deletions

View file

@ -21,4 +21,10 @@ abstract class SingleAccountListSupplier(
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
)
}
suspend fun getSyncOrNull(userWalletId: UserWalletId): AccountList? {
return getSyncOrNull(
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
)
}
}

View file

@ -27,6 +27,7 @@ dependencies {
api(projects.domain.referral)
api(projects.domain.staking)
api(projects.domain.tokens)
api(projects.domain.tokens.models)
api(projects.domain.walletManager)
api(projects.domain.wallets)
@ -36,6 +37,7 @@ dependencies {
implementation(deps.kotlin.datetime)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
implementation(deps.kermit)
implementation(tangemDeps.blockchain)

View file

@ -1,7 +1,6 @@
package com.tangem.domain.account.status.di
import com.tangem.domain.account.status.producer.*
import com.tangem.domain.core.flow.FlowProducerScope
import com.tangem.domain.core.flow.FlowProducerTools
import dagger.Binds
import dagger.Module
@ -28,8 +27,4 @@ internal interface AccountStatusListProducerFactoryModule {
@Binds
@Singleton
fun bindDefaultFlowProducerTools(impl: DefaultFlowProducerTools): FlowProducerTools
@Binds
@Singleton
fun bindFlowProducerScope(impl: DefaultFlowProducerAppScope): FlowProducerScope
}

View file

@ -21,13 +21,12 @@ import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -76,10 +75,10 @@ internal object AccountStatusUseCaseModule {
@Provides
@Singleton
fun provideGetWalletTotalBalanceUseCaseV2(
fun provideGetWalletTotalBalanceUseCase(
multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
): GetWalletTotalBalanceUseCaseV2 {
return GetWalletTotalBalanceUseCaseV2(
): GetWalletTotalBalanceUseCase {
return GetWalletTotalBalanceUseCase(
multiAccountStatusListSupplier = multiAccountStatusListSupplier,
)
}
@ -104,6 +103,7 @@ internal object AccountStatusUseCaseModule {
cryptoCurrencyMetadataCleaner: CryptoCurrencyMetadataCleaner,
expressServiceFetcher: ExpressServiceFetcher,
dispatchers: CoroutineDispatcherProvider,
appScope: AppCoroutineScope,
): ManageCryptoCurrenciesUseCase {
return ManageCryptoCurrenciesUseCase(
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
@ -114,7 +114,7 @@ internal object AccountStatusUseCaseModule {
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
cryptoCurrencyMetadataCleaner = cryptoCurrencyMetadataCleaner,
expressServiceFetcher = expressServiceFetcher,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
parallelUpdatingScope = appScope,
dispatchers = dispatchers,
)
}
@ -126,14 +126,14 @@ internal object AccountStatusUseCaseModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
appScope: AppCoroutineScope,
): CryptoCurrencyBalanceFetcher {
return CryptoCurrencyBalanceFetcher(
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
parallelUpdatingScope = appScope,
)
}
@ -157,6 +157,18 @@ internal object AccountStatusUseCaseModule {
)
}
@Provides
@Singleton
fun provideGetFeePaidCryptoCurrencyStatusSyncUseCase(
currenciesRepository: CurrenciesRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
): GetFeePaidCryptoCurrencyStatusSyncUseCase {
return GetFeePaidCryptoCurrencyStatusSyncUseCase(
currenciesRepository = currenciesRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
)
}
@Provides
@Singleton
fun provideCryptoCurrencyMetadataCleaner(

View file

@ -1,54 +1,19 @@
package com.tangem.domain.account.status.producer
import co.touchlab.kermit.Logger
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.core.flow.FlowProducerScope
import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
class DefaultFlowProducerAppScope @Inject constructor(
dispatchers: CoroutineDispatcherProvider,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : FlowProducerScope {
private val tag = "FlowProducerScope"
override val coroutineContext: CoroutineContext = SupervisorJob() +
dispatchers.default +
CoroutineName(tag) +
CoroutineExceptionHandler { context, throwable ->
@Suppress("NullableToStringCall")
val coroutineName = context[CoroutineName]?.name.toString()
logError(throwable, coroutineName)
}
private fun logError(throwable: Throwable, coroutineName: String) {
Timber.tag("FlowProducerExceptionHandler").e(
throwable,
"CoroutineName $coroutineName",
)
val event = ExceptionAnalyticsEvent(
exception = throwable,
params = mapOf(
"source" to tag,
"coroutineName" to coroutineName,
),
)
analyticsExceptionHandler.sendException(event)
}
}
class DefaultFlowProducerTools @Inject constructor(
private val scope: FlowProducerScope,
private val scope: AppCoroutineScope,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : FlowProducerTools {
@ -72,6 +37,7 @@ class DefaultFlowProducerTools @Inject constructor(
}
return upstream
.flowOn(dispatchers.default)
.distinctUntilChanged()
.shareIn(
scope = scope,
@ -89,8 +55,8 @@ class DefaultFlowProducerTools @Inject constructor(
private fun logError(cause: Throwable, flowProducerName: String, attempt: Long) {
val tag = "FlowProducerRetryWhen"
Timber.tag(tag)
.e(cause, "flowProducerName $flowProducerName attempt $attempt")
Logger.withTag(tag)
.e("flowProducerName $flowProducerName attempt $attempt", cause)
val event = ExceptionAnalyticsEvent(
exception = cause,

View file

@ -20,4 +20,9 @@ abstract class SingleAccountStatusListSupplier(
val params = SingleAccountStatusListProducer.Params(userWalletId)
return this.invoke(params)
}
suspend fun getSyncOrNull(userWalletId: UserWalletId): AccountStatusList? {
val params = SingleAccountStatusListProducer.Params(userWalletId)
return this.getSyncOrNull(params)
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.domain.account.status.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCoinStatus
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
class GetFeePaidCryptoCurrencyStatusSyncUseCase(
private val currenciesRepository: CurrenciesRepository,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Either<TokenListError, CryptoCurrencyStatus?> {
val cryptoCurrency = cryptoCurrencyStatus.currency
val network = cryptoCurrency.network
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, network)
return either {
when (feePaidCurrency) {
is FeePaidCurrency.Coin -> {
val accountStatusList = singleAccountStatusListSupplier.getSyncOrNull(userWalletId)
accountStatusList?.getCoinStatus(network)?.getOrNull()
}
is FeePaidCurrency.Token -> {
val accountStatusList = singleAccountStatusListSupplier.getSyncOrNull(userWalletId)
accountStatusList.getCryptoCurrencyStatus(
currencyId = feePaidCurrency.tokenId,
network = network,
).getOrNull()
}
is FeePaidCurrency.SameCurrency,
is FeePaidCurrency.FeeResource,
-> cryptoCurrencyStatus
}
}
}
}

View file

@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.map
*
* @param multiAccountStatusListSupplier Supplier that provides the status list for multiple accounts.
*/
class GetWalletTotalBalanceUseCaseV2(
class GetWalletTotalBalanceUseCase(
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
) {

View file

@ -8,6 +8,7 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.core.utils.eitherOn
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
@ -51,7 +52,7 @@ class ManageCryptoCurrenciesUseCase(
private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
private val cryptoCurrencyMetadataCleaner: CryptoCurrencyMetadataCleaner,
private val expressServiceFetcher: ExpressServiceFetcher,
private val parallelUpdatingScope: CoroutineScope,
private val parallelUpdatingScope: AppCoroutineScope,
private val dispatchers: CoroutineDispatcherProvider,
) {

View file

@ -0,0 +1,70 @@
package com.tangem.domain.account.status.utils
import arrow.core.Option
import arrow.core.raise.option
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.status.model.AccountCryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
/**
* Extension functions for retrieving [AccountCryptoCurrency] from an [AccountList].
*
* This object provides convenient extension functions to search and retrieve [AccountCryptoCurrency] instances
* from an [AccountList] using various parameters such as cryptocurrency ID and network.
*
* Usage example:
* ```
* val accountList: AccountList? = ...
* val result: Option<AccountCryptoCurrency> = accountList.getAccountCryptoCurrency(currency)
* result.fold(
* ifEmpty = { /* handle not found */ },
* ifSome = { accountCryptoCurrency -> /* use the found result */ }
* )
* ```
*
* @see AccountCryptoCurrency
* @see AccountList
[REDACTED_AUTHOR]
*/
object AccountCryptoCurrencyOperations {
// region AccountList
/**
* Retrieves the [AccountCryptoCurrency] for the specified [currency] from this [AccountList].
*
* @receiver the [AccountList] to search within, can be null
* @param currency the cryptocurrency whose account association is to be retrieved
* @return [Option] containing the [AccountCryptoCurrency] if found, or [Option.None] otherwise
*/
fun AccountList?.getAccountCryptoCurrency(currency: CryptoCurrency): Option<AccountCryptoCurrency> {
return getAccountCryptoCurrency(currencyId = currency.id, network = currency.network)
}
/**
* Retrieves the [AccountCryptoCurrency] for the specified [currencyId] and [network] from this [AccountList].
*
* @receiver the [AccountList] to search within, can be null
* @param currencyId the ID of the cryptocurrency whose account association is to be retrieved
* @param network the network associated with the cryptocurrency, can be null
* @return [Option] containing the [AccountCryptoCurrency] if found, or [Option.None] otherwise
*/
fun AccountList?.getAccountCryptoCurrency(
currencyId: CryptoCurrency.ID,
network: Network?,
): Option<AccountCryptoCurrency> = option {
val accountList = this@getAccountCryptoCurrency
ensureNotNull(accountList)
val accountCryptoCurrency = AccountCryptoCurrencyStatusFinder(
accountList = accountList,
currencyId = currencyId,
network = network,
)
ensureNotNull(accountCryptoCurrency)
}
// endregion
}

View file

@ -1,7 +1,9 @@
package com.tangem.domain.account.status.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrency
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatuses
import com.tangem.domain.models.account.Account
@ -9,16 +11,23 @@ import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
/**
* Finds the status of a specific cryptocurrency associated with an account from the provided account status list.
* Finds a specific cryptocurrency or its status associated with an account from the provided account list.
*
* The base implementation uses [AccountList] for searching. For [AccountStatusList], it first converts
* to [AccountList], finds the [AccountCryptoCurrency], then retrieves the status using accountId and currencyId.
*
[REDACTED_AUTHOR]
*/
@Suppress("MethodOverloading")
internal object AccountCryptoCurrencyStatusFinder {
// region AccountCryptoCurrencyStatus methods
/**
* Retrieves the [AccountCryptoCurrencyStatus] for the specified [currency] from the given [accountStatusList].
*
@ -48,25 +57,19 @@ internal object AccountCryptoCurrencyStatusFinder {
currencyId: CryptoCurrency.ID,
network: Network?,
): AccountCryptoCurrencyStatus? {
return accountStatusList.getExpectedAccountStatuses(network)
.asSequence()
.filterCryptoPortfolio()
.mapNotNull { accountStatus ->
val status = accountStatus.flattenCurrencies().firstOrNull { it.currency.id == currencyId }
?: return@mapNotNull null
val accountList = accountStatusList.toAccountList().getOrNull() ?: return null
val accountCurrency = invoke(accountList, currencyId, network) ?: return null
AccountCryptoCurrencyStatus(account = accountStatus.account, status = status)
}
.firstOrNull()
return accountStatusList.findStatus(accountCurrency)
}
/**
* Retrieves a map of accounts to their corresponding list of [AccountCryptoCurrencyStatus] for the specified
* Retrieves a map of accounts to their corresponding list of [CryptoCurrencyStatus] for the specified
* list of [currencies] from the given [accountStatusList].
*
* @param accountStatusList the list of account statuses to search within.
* @param currencies the list of cryptocurrencies whose statuses are to be retrieved.
* @return a map where the key is the account and the value is a list of [AccountCryptoCurrencyStatus].
* @return a map where the key is the account and the value is a list of [CryptoCurrencyStatus].
*/
operator fun invoke(
accountStatusList: AccountStatusList,
@ -104,15 +107,86 @@ internal object AccountCryptoCurrencyStatusFinder {
derivationPath: Network.DerivationPath,
contractAddress: String?,
): AccountCryptoCurrencyStatus? {
return accountStatusList.getExpectedAccountStatuses(
val accountList = accountStatusList.toAccountList().getOrNull() ?: return null
val accountCurrency = invoke(
accountList = accountList,
networkId = networkId,
derivationPath = derivationPath,
contractAddress = contractAddress,
) ?: return null
return accountStatusList.findStatus(accountCurrency)
}
// endregion
// region AccountCryptoCurrency methods
/**
* Retrieves the [AccountCryptoCurrency] for the specified [currency] from the given [accountList].
*
* @param accountList the list of accounts to search within.
* @param currency the cryptocurrency to be retrieved.
* @return the [AccountCryptoCurrency] if found, otherwise null.
*/
operator fun invoke(accountList: AccountList, currency: CryptoCurrency): AccountCryptoCurrency? {
return invoke(
accountList = accountList,
currencyId = currency.id,
network = currency.network,
)
}
/**
* Retrieves the [AccountCryptoCurrency] for the specified [currencyId] and [network]
* from the given [accountList].
*
* @param accountList the list of accounts to search within.
* @param currencyId the ID of the cryptocurrency to be retrieved.
* @param network the network associated with the cryptocurrency.
* @return the [AccountCryptoCurrency] if found, otherwise null.
*/
operator fun invoke(
accountList: AccountList,
currencyId: CryptoCurrency.ID,
network: Network?,
): AccountCryptoCurrency? {
return accountList.getExpectedAccounts(network)
.asSequence()
.filterIsInstance<Account.CryptoPortfolio>()
.mapNotNull { account ->
val currency = account.cryptoCurrencies.firstOrNull { it.id == currencyId }
?: return@mapNotNull null
AccountCryptoCurrency(account = account, cryptoCurrency = currency)
}
.firstOrNull()
}
/**
* Retrieves the [AccountCryptoCurrency] for the specified [networkId], [derivationPath],
* and optional [contractAddress] from the given [accountList].
*
* @param accountList the list of accounts to search within.
* @param networkId the ID of the network associated with the cryptocurrency.
* @param derivationPath the derivation path of the account.
* @param contractAddress the optional contract address of the token (if applicable).
* @return the [AccountCryptoCurrency] if found, otherwise null.
*/
operator fun invoke(
accountList: AccountList,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
contractAddress: String?,
): AccountCryptoCurrency? {
return accountList.getExpectedAccounts(
rawNetworkId = networkId.rawId.value,
derivationPath = derivationPath,
)
.asSequence()
.filterCryptoPortfolio()
.mapNotNull { accountStatus ->
val status = accountStatus.flattenCurrencies().firstOrNull {
val currency = it.currency
.filterIsInstance<Account.CryptoPortfolio>()
.mapNotNull { account ->
val currency = account.cryptoCurrencies.firstOrNull { currency ->
val isContractAddressMatch = contractAddress == null ||
currency.id.contractAddress.equals(contractAddress, ignoreCase = true)
@ -122,74 +196,101 @@ internal object AccountCryptoCurrencyStatusFinder {
}
?: return@mapNotNull null
AccountCryptoCurrencyStatus(account = accountStatus.account, status = status)
AccountCryptoCurrency(account = account, cryptoCurrency = currency)
}
.firstOrNull()
}
private fun AccountStatusList.getExpectedAccountStatuses(network: Network?): List<AccountStatus> {
return getExpectedAccountStatuses(rawNetworkId = network?.rawId, derivationPath = network?.derivationPath)
// endregion
// region AccountStatusList helpers
private fun AccountStatusList.findStatus(accountCurrency: AccountCryptoCurrency): AccountCryptoCurrencyStatus? {
val accountStatus = accountStatuses
.filterCryptoPortfolio()
.firstOrNull { it.account.accountId == accountCurrency.account.accountId }
?: return null
val currencyStatus = accountStatus.flattenCurrencies()
.firstOrNull { it.currency.id == accountCurrency.cryptoCurrency.id }
?: return null
return AccountCryptoCurrencyStatus(account = accountCurrency.account, status = currencyStatus)
}
/**
* Retrieves the expected account statuses based on the provided [rawNetworkId] and [derivationPath].
* If either parameter is null, all account statuses are returned.
* If both parameters are provided, it filters the accounts based on the derivation index.
*
* @param rawNetworkId the raw ID of the network to filter accounts by, can be null.
* @param derivationPath the derivation path of the network to filter accounts by, can be null.
* @return a list of [AccountStatus] that match the expected criteria.
*/
private fun AccountStatusList.getExpectedAccountStatuses(
rawNetworkId: String?,
derivationPath: Network.DerivationPath?,
): List<AccountStatus> {
val possibleAccountIndex = if (rawNetworkId != null && derivationPath != null) {
getAccountIndexOrNull(rawNetworkId, derivationPath)
} else {
null
}
internal fun AccountStatusList.getExpectedAccountStatuses(networkId: Network.ID): List<AccountStatus> {
val possibleAccountIndex = getAccountIndexOrNull(
rawNetworkId = networkId.rawId.value,
derivationPath = networkId.derivationPath,
)
return when (possibleAccountIndex) {
// currency can be in any account
null -> accountStatuses
// currency only in the main account
DerivationIndex.Main.value -> listOf(mainAccount)
// currency only in the account with specific derivation index or in the main account
else -> {
val accountStatus = accountStatuses.firstOrNull { accountStatus ->
val cryptoPortfolio = accountStatus.account as? Account.CryptoPortfolio ?: return@firstOrNull false
cryptoPortfolio.derivationIndex.value == possibleAccountIndex
val account = accountStatuses.firstOrNull { account ->
val cryptoPortfolio = account as? AccountStatus.CryptoPortfolio ?: return@firstOrNull false
cryptoPortfolio.account.derivationIndex.value == possibleAccountIndex
}
listOfNotNull(accountStatus, mainAccount)
listOfNotNull(account, mainAccount)
}
}
}
private fun AccountStatusList.getExpectedAccountStatuses(networks: List<Network>): List<AccountStatus> {
val possibleAccountIndexes = networks.mapNotNull { it.getAccountIndexOrNull() }
internal fun AccountStatusList.getExpectedAccountStatuses(networks: List<Network>): List<AccountStatus> {
val possibleAccountIndexes = networks.mapNotNull { getAccountIndexOrNull(it.rawId, it.derivationPath) }
if (possibleAccountIndexes.isEmpty()) return this@getExpectedAccountStatuses.accountStatuses
if (possibleAccountIndexes.isEmpty()) return accountStatuses
val accountStatuses = this@getExpectedAccountStatuses.accountStatuses.filter { accountStatus ->
val filteredStatuses = accountStatuses.filter { accountStatus ->
val cryptoPortfolio = accountStatus.account as? Account.CryptoPortfolio ?: return@filter false
cryptoPortfolio.derivationIndex.value in possibleAccountIndexes
}
return accountStatuses + listOf(mainAccount)
return filteredStatuses + listOf(mainAccount)
}
private fun Network.getAccountIndexOrNull(): Int? {
return getAccountIndexOrNull(rawNetworkId = rawId, derivationPath = derivationPath)
// endregion
// region AccountList helpers
internal fun AccountList.getExpectedAccounts(network: Network?): List<Account> {
return getExpectedAccounts(rawNetworkId = network?.rawId, derivationPath = network?.derivationPath)
}
private fun getAccountIndexOrNull(rawNetworkId: String, derivationPath: Network.DerivationPath): Int? {
private fun AccountList.getExpectedAccounts(
rawNetworkId: String?,
derivationPath: Network.DerivationPath?,
): List<Account> {
val possibleAccountIndex = getAccountIndexOrNull(rawNetworkId, derivationPath)
return when (possibleAccountIndex) {
null -> accounts
DerivationIndex.Main.value -> listOf(mainAccount)
// currency only in the account with specific derivation index or in the main account
else -> {
val account = accounts.firstOrNull { account ->
val cryptoPortfolio = account as? Account.CryptoPortfolio ?: return@firstOrNull false
cryptoPortfolio.derivationIndex.value == possibleAccountIndex
}
listOfNotNull(account, mainAccount)
}
}
}
// endregion
// region Common helpers
private fun getAccountIndexOrNull(rawNetworkId: String?, derivationPath: Network.DerivationPath?): Int? {
if (rawNetworkId == null || derivationPath == null) return null
val blockchain = Blockchain.fromId(id = rawNetworkId)
val recognizer = AccountNodeRecognizer(blockchain)
return recognizer.recognize(derivationPath)?.toInt()
}
// endregion
}

View file

@ -0,0 +1,77 @@
package com.tangem.domain.account.status.utils
import arrow.core.Option
import arrow.core.raise.option
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
/**
* Extension functions for retrieving [AccountCryptoCurrencyStatus] from an [AccountStatusList].
*
* This object provides convenient extension functions to search and retrieve [AccountCryptoCurrencyStatus]
* instances from an [AccountStatusList] using various parameters such as cryptocurrency ID and network.
*
* The primary difference from [AccountCryptoCurrencyOperations] is that this object works with
* [AccountStatusList] and returns status information along with the account-currency association.
*
* Usage example:
* ```
* val accountStatusList: AccountStatusList? = ...
* val result: Option<AccountCryptoCurrencyStatus> = accountStatusList.getAccountCryptoCurrencyStatus(currency)
* result.fold(
* ifEmpty = { /* handle not found */ },
* ifSome = { accountCurrencyStatus -> /* use the found result */ }
* )
* ```
*
* @see AccountCryptoCurrencyStatus
* @see AccountStatusList
* @see AccountCryptoCurrencyOperations
[REDACTED_AUTHOR]
*/
object AccountCryptoCurrencyStatusOperations {
// region AccountStatusList
/**
* Retrieves the [AccountCryptoCurrencyStatus] for the specified [currency] from this [AccountStatusList].
*
* @receiver the [AccountStatusList] to search within, can be null
* @param currency the cryptocurrency whose account status association is to be retrieved
* @return [Option] containing the [AccountCryptoCurrencyStatus] if found, or [Option.None] otherwise
*/
fun AccountStatusList?.getAccountCryptoCurrencyStatus(
currency: CryptoCurrency,
): Option<AccountCryptoCurrencyStatus> {
return getAccountCryptoCurrencyStatus(currencyId = currency.id, network = currency.network)
}
/**
* Retrieves the [AccountCryptoCurrencyStatus] for the specified [currencyId] and [network]
* from this [AccountStatusList].
*
* @receiver the [AccountStatusList] to search within, can be null
* @param currencyId the ID of the cryptocurrency whose account status association is to be retrieved
* @param network the network associated with the cryptocurrency, can be null
* @return [Option] containing the [AccountCryptoCurrencyStatus] if found, or [Option.None] otherwise
*/
fun AccountStatusList?.getAccountCryptoCurrencyStatus(
currencyId: CryptoCurrency.ID,
network: Network?,
): Option<AccountCryptoCurrencyStatus> = option {
val accountStatusList = this@getAccountCryptoCurrencyStatus
ensureNotNull(accountStatusList)
val accountCryptoCurrencyStatus = AccountCryptoCurrencyStatusFinder(
accountStatusList = accountStatusList,
currencyId = currencyId,
network = network,
)
ensureNotNull(accountCryptoCurrencyStatus)
}
// endregion
}

View file

@ -35,6 +35,10 @@ class CryptoCurrencyBalanceFetcher(
private val mutex = Mutex()
operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency) {
invoke(userWalletId = userWalletId, currencies = listOf(currency))
}
operator fun invoke(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
@ -45,6 +49,18 @@ class CryptoCurrencyBalanceFetcher(
}
}
suspend fun invokeAndAwait(userWalletId: UserWalletId, currency: CryptoCurrency) {
invokeAndAwait(userWalletId = userWalletId, currencies = listOf(currency))
}
suspend fun invokeAndAwait(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
mutex.withLock {
refreshBalances(userWalletId, currencies)
}
}
private suspend fun refreshBalances(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
coroutineScope {
val results = listOf(

View file

@ -0,0 +1,110 @@
package com.tangem.domain.account.status.utils
import arrow.core.None
import arrow.core.Option
import arrow.core.raise.catch
import arrow.core.raise.option
import arrow.core.toOption
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.status.utils.AccountCryptoCurrencyOperations.getAccountCryptoCurrency
import com.tangem.domain.account.status.utils.AccountCryptoCurrencyStatusFinder.getExpectedAccounts
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import timber.log.Timber
/**
* Extension functions for retrieving [CryptoCurrency] from an [AccountList] or [Account.CryptoPortfolio].
*
* This object provides convenient extension functions to search and retrieve [CryptoCurrency] instances
* from account-related data structures using various parameters such as cryptocurrency ID and network.
*
* Unlike [AccountCryptoCurrencyOperations], this object returns only the [CryptoCurrency] without
* the associated account information.
*
* Usage example:
* ```
* val accountList: AccountList? = ...
* val result: Option<CryptoCurrency> = accountList.getCryptoCurrency(currencyId, network)
* result.fold(
* ifEmpty = { /* handle not found */ },
* ifSome = { currency -> /* use the found currency */ }
* )
* ```
*
* @see CryptoCurrency
* @see AccountList
* @see Account.CryptoPortfolio
* @see AccountCryptoCurrencyOperations
[REDACTED_AUTHOR]
*/
object CryptoCurrencyOperations {
// region AccountList
/**
* Retrieves the [CryptoCurrency] matching the specified [cryptoCurrency] from this [AccountList].
*
* @receiver the [AccountList] to search within, can be null
* @param cryptoCurrency the cryptocurrency to match
* @return [Option] containing the [CryptoCurrency] if found, or [Option.None] otherwise
*/
fun AccountList?.getCryptoCurrency(cryptoCurrency: CryptoCurrency): Option<CryptoCurrency> {
return getCryptoCurrency(currencyId = cryptoCurrency.id, network = cryptoCurrency.network)
}
fun AccountList?.getCryptoCurrency(currencyIdValue: String): Option<CryptoCurrency> = option {
val currencyId = catch(
block = { CryptoCurrency.ID.fromValue(currencyIdValue) },
catch = { throwable ->
Timber.e("Error on converting currencyId: $throwable")
raise(None)
},
)
return getCryptoCurrency(currencyId = currencyId, network = null)
}
fun AccountList.getCoin(currency: CryptoCurrency): Option<CryptoCurrency.Coin> {
return getCoin(network = currency.network)
}
fun AccountList.getCoin(network: Network): Option<CryptoCurrency.Coin> {
return getExpectedAccounts(network = network)
.flatMap { account ->
(account as? Account.CryptoPortfolio)
?.cryptoCurrencies.orEmpty()
.filterIsInstance<CryptoCurrency.Coin>()
}
.firstOrNull { currency -> currency.network.id == network.id }
.toOption()
}
/**
* Retrieves the [CryptoCurrency] for the specified [currencyId] and [network] from this [AccountList].
*
* @receiver the [AccountList] to search within, can be null
* @param currencyId the ID of the cryptocurrency to be retrieved
* @param network the network associated with the cryptocurrency, can be null
* @return [Option] containing the [CryptoCurrency] if found, or [Option.None] otherwise
*/
fun AccountList?.getCryptoCurrency(currencyId: CryptoCurrency.ID, network: Network?): Option<CryptoCurrency> {
return getAccountCryptoCurrency(currencyId, network)
.map { it.cryptoCurrency }
}
// endregion
// region Account.CryptoPortfolio
/**
* Retrieves the [CryptoCurrency] matching the specified [currencyId] from this [Account.CryptoPortfolio].
*
* @receiver the [Account.CryptoPortfolio] to search within
* @param currencyId the ID of the cryptocurrency to be retrieved
* @return [Option] containing the [CryptoCurrency] if found, or [Option.None] otherwise
*/
fun Account.CryptoPortfolio.getCryptoCurrency(currencyId: CryptoCurrency.ID): Option<CryptoCurrency> {
return cryptoCurrencies.firstOrNull { it.id == currencyId }.toOption()
}
// endregion
}

View file

@ -0,0 +1,109 @@
package com.tangem.domain.account.status.utils
import arrow.core.Option
import arrow.core.toOption
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.account.status.utils.AccountCryptoCurrencyStatusFinder.getExpectedAccountStatuses
import com.tangem.domain.account.status.utils.AccountCryptoCurrencyStatusOperations.getAccountCryptoCurrencyStatus
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
/**
* Extension functions for retrieving [CryptoCurrencyStatus] from an [AccountStatusList] or
* [AccountStatus.CryptoPortfolio].
*
* This object provides convenient extension functions to search and retrieve [CryptoCurrencyStatus]
* instances from account status-related data structures using various parameters such as cryptocurrency ID
* and network.
*
* Unlike [AccountCryptoCurrencyStatusOperations], this object returns only the [CryptoCurrencyStatus] without
* the associated account information.
*
* Usage example:
* ```
* val accountStatusList: AccountStatusList? = ...
* val result: Option<CryptoCurrencyStatus> = accountStatusList.getCryptoCurrencyStatus(currency)
* result.fold(
* ifEmpty = { /* handle not found */ },
* ifSome = { status -> /* use the found status */ }
* )
* ```
*
* @see CryptoCurrencyStatus
* @see AccountStatusList
* @see AccountStatus.CryptoPortfolio
* @see AccountCryptoCurrencyStatusOperations
[REDACTED_AUTHOR]
*/
object CryptoCurrencyStatusOperations {
// region AccountStatusList
/**
* Retrieves the [CryptoCurrencyStatus] for the specified [currency] from this [AccountStatusList].
*
* @receiver the [AccountStatusList] to search within, can be null
* @param currency the cryptocurrency whose status is to be retrieved
* @return [Option] containing the [CryptoCurrencyStatus] if found, or [Option.None] otherwise
*/
fun AccountStatusList?.getCryptoCurrencyStatus(currency: CryptoCurrency): Option<CryptoCurrencyStatus> {
return getCryptoCurrencyStatus(currencyId = currency.id, network = currency.network)
}
/**
* Retrieves the [CryptoCurrencyStatus] for the specified [currencyId] and [network]
* from this [AccountStatusList].
*
* @receiver the [AccountStatusList] to search within, can be null
* @param currencyId the ID of the cryptocurrency whose status is to be retrieved
* @param network the network associated with the cryptocurrency, can be null
* @return [Option] containing the [CryptoCurrencyStatus] if found, or [Option.None] otherwise
*/
fun AccountStatusList?.getCryptoCurrencyStatus(
currencyId: CryptoCurrency.ID,
network: Network?,
): Option<CryptoCurrencyStatus> {
return getAccountCryptoCurrencyStatus(currencyId = currencyId, network = network)
.map(AccountCryptoCurrencyStatus::status)
}
fun AccountStatusList.getCoinStatus(currency: CryptoCurrency): Option<CryptoCurrencyStatus> {
return getCoinStatus(network = currency.network)
}
fun AccountStatusList.getCoinStatus(network: Network): Option<CryptoCurrencyStatus> {
return getCoinStatus(networkId = network.id)
}
fun AccountStatusList.getCoinStatus(networkId: Network.ID): Option<CryptoCurrencyStatus> {
return getExpectedAccountStatuses(networkId)
.flatMap { accountStatus ->
(accountStatus as? AccountStatus.CryptoPortfolio)
?.flattenCurrencies().orEmpty()
.filter { it.currency is CryptoCurrency.Coin }
}
.firstOrNull { status -> status.currency.network.id == networkId }
.toOption()
}
// endregion
// region AccountStatus.CryptoPortfolio
/**
* Retrieves the [CryptoCurrencyStatus] for the specified [currencyId]
* from this [AccountStatus.CryptoPortfolio].
*
* @receiver the [AccountStatus.CryptoPortfolio] to search within
* @param currencyId the ID of the cryptocurrency whose status is to be retrieved
* @return [Option] containing the [CryptoCurrencyStatus] if found, or [Option.None] otherwise
*/
fun AccountStatus.CryptoPortfolio.getCryptoCurrencyStatus(
currencyId: CryptoCurrency.ID,
): Option<CryptoCurrencyStatus> {
return flattenCurrencies().firstOrNull { it.currency.id == currencyId }.toOption()
}
// endregion
}

View file

@ -7,11 +7,10 @@ import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.core.utils.lceLoading
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.account.DerivationIndex
import com.tangem.domain.models.account.*
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.tokenlist.TokenList
@ -19,7 +18,10 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.test.core.assertNone
import com.tangem.test.core.assertSome
import com.tangem.test.core.getEmittedValues
import io.mockk.*
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@ -78,9 +80,15 @@ class GetAccountCurrencyStatusUseCaseTest {
priceChangeLce = lceLoading(),
)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns listOf(accountStatus)
}
val accountStatusList = AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(accountStatus),
totalAccounts = 1,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
@ -101,10 +109,14 @@ class GetAccountCurrencyStatusUseCaseTest {
priceChangeLce = lceLoading(),
)
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!!
every { this@mockk.cryptoCurrencies } returns listOf(currency)
}
val derivationIndex = DerivationIndex(1).getOrNull()!!
val account = Account.CryptoPortfolio(
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex),
accountName = AccountName("Test Account").getOrNull()!!,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = derivationIndex,
cryptoCurrencies = listOf(currency),
)
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
@ -116,9 +128,15 @@ class GetAccountCurrencyStatusUseCaseTest {
priceChangeLce = lceLoading(),
)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns listOf(mainAccountStatus, accountStatus, mockk())
}
val accountStatusList = AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(mainAccountStatus, accountStatus),
totalAccounts = 2,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
@ -139,9 +157,10 @@ class GetAccountCurrencyStatusUseCaseTest {
@Test
fun `invokeSync returns Some if network is null`() = runTest {
// Arrange
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
every { this@mockk.cryptoCurrencies } returns listOf(currency)
}
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
@ -153,9 +172,15 @@ class GetAccountCurrencyStatusUseCaseTest {
priceChangeLce = lceLoading(),
)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns listOf(accountStatus)
}
val accountStatusList = AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(accountStatus),
totalAccounts = 1,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
@ -197,9 +222,15 @@ class GetAccountCurrencyStatusUseCaseTest {
priceChangeLce = lceLoading(),
)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns listOf(accountStatus)
}
val accountStatusList = AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(accountStatus),
totalAccounts = 1,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
coEvery { supplier(supplierParams) } returns flowOf(accountStatusList)
@ -221,10 +252,14 @@ class GetAccountCurrencyStatusUseCaseTest {
priceChangeLce = lceLoading(),
)
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!!
every { this@mockk.cryptoCurrencies } returns listOf(currency)
}
val derivationIndex = DerivationIndex(1).getOrNull()!!
val account = Account.CryptoPortfolio(
accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex),
accountName = AccountName("Test Account").getOrNull()!!,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
derivationIndex = derivationIndex,
cryptoCurrencies = listOf(currency),
)
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
@ -236,9 +271,15 @@ class GetAccountCurrencyStatusUseCaseTest {
priceChangeLce = lceLoading(),
)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns listOf(mainAccountStatus, accountStatus, mockk())
}
val accountStatusList = AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(mainAccountStatus, accountStatus),
totalAccounts = 2,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
coEvery { supplier(supplierParams) } returns flowOf(accountStatusList)
@ -256,9 +297,10 @@ class GetAccountCurrencyStatusUseCaseTest {
@Test
fun `invoke returns data if network is null`() = runTest {
// Arrange
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
every { this@mockk.cryptoCurrencies } returns listOf(currency)
}
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
@ -270,9 +312,15 @@ class GetAccountCurrencyStatusUseCaseTest {
priceChangeLce = lceLoading(),
)
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
every { this@mockk.accountStatuses } returns listOf(accountStatus)
}
val accountStatusList = AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(accountStatus),
totalAccounts = 1,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
coEvery { supplier(supplierParams) } returns flowOf(accountStatusList)

View file

@ -0,0 +1,169 @@
package com.tangem.domain.account.status.utils
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.status.model.AccountCryptoCurrency
import com.tangem.domain.account.status.utils.AccountCryptoCurrencyOperations.getAccountCryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.test.core.assertNone
import com.tangem.test.core.assertSome
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
* Tests for [AccountCryptoCurrencyOperations].
*
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AccountCryptoCurrencyOperationsTest {
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val userWalletId = UserWalletId("011")
private val currency = cryptoCurrencyFactory.ethereum
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetAccountCryptoCurrencyByCurrency {
@Test
fun `returns None when AccountList is null`() {
// Arrange
val accountList: AccountList? = null
// Act
val result = accountList.getAccountCryptoCurrency(currency)
// Assert
assertNone(result)
}
@Test
fun `returns None when currency is not found in AccountList`() {
// Arrange
val accountList = AccountList.empty(userWalletId)
// Act
val result = accountList.getAccountCryptoCurrency(currency)
// Assert
assertNone(result)
}
@Test
fun `returns Some when currency is found in AccountList`() {
// Arrange
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
val expected = AccountCryptoCurrency(
account = accountList.mainAccount,
cryptoCurrency = currency,
)
// Act
val result = accountList.getAccountCryptoCurrency(currency)
// Assert
assertSome(result, expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetAccountCryptoCurrencyByCurrencyIdAndNetwork {
@Test
fun `returns None when AccountList is null`() {
// Arrange
val accountList: AccountList? = null
// Act
val result = accountList.getAccountCryptoCurrency(
currencyId = currency.id,
network = currency.network,
)
// Assert
assertNone(result)
}
@Test
fun `returns None when currency id is not found`() {
// Arrange
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
val otherCurrencyId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId("bitcoin"),
suffix = CryptoCurrency.ID.Suffix.RawID("bitcoin"),
)
// Act
val result = accountList.getAccountCryptoCurrency(
currencyId = otherCurrencyId,
network = currency.network,
)
// Assert
assertNone(result)
}
@Test
fun `returns Some when currency id is found with null network`() {
// Arrange
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
val expected = AccountCryptoCurrency(
account = accountList.mainAccount,
cryptoCurrency = currency,
)
// Act
val result = accountList.getAccountCryptoCurrency(
currencyId = currency.id,
network = null,
)
// Assert
assertSome(result, expected)
}
@Test
fun `returns Some when currency id and network match`() {
// Arrange
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
val expected = AccountCryptoCurrency(
account = accountList.mainAccount,
cryptoCurrency = currency,
)
// Act
val result = accountList.getAccountCryptoCurrency(
currencyId = currency.id,
network = currency.network,
)
// Assert
assertSome(result, expected)
}
@Test
fun `returns Some with first matching currency when multiple currencies exist`() {
// Arrange
val currencies = cryptoCurrencyFactory.ethereumAndStellar
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = currencies,
)
val targetCurrency = currencies.first()
val expected = AccountCryptoCurrency(
account = accountList.mainAccount,
cryptoCurrency = targetCurrency,
)
// Act
val result = accountList.getAccountCryptoCurrency(
currencyId = targetCurrency.id,
network = targetCurrency.network,
)
// Assert
assertSome(result, expected)
}
}
}

View file

@ -0,0 +1,205 @@
package com.tangem.domain.account.status.utils
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.account.status.utils.AccountCryptoCurrencyStatusOperations.getAccountCryptoCurrencyStatus
import com.tangem.domain.core.utils.lceLoading
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.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.test.core.assertNone
import com.tangem.test.core.assertSome
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
* Tests for [AccountCryptoCurrencyStatusOperations].
*
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AccountCryptoCurrencyStatusOperationsTest {
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val userWalletId = UserWalletId("011")
private val currency = cryptoCurrencyFactory.ethereum
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetAccountCryptoCurrencyStatusByCurrency {
@Test
fun `returns None when AccountStatusList is null`() {
val accountStatusList: AccountStatusList? = null
val result = accountStatusList.getAccountCryptoCurrencyStatus(currency)
assertNone(result)
}
@Test
fun `returns None when currency is not found in AccountStatusList`() {
val accountStatusList = createAccountStatusList(currencies = emptyList())
val result = accountStatusList.getAccountCryptoCurrencyStatus(currency)
assertNone(result)
}
@Test
fun `returns Some when currency is found in AccountStatusList`() {
val currencyStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(currencyStatus),
)
val expected = AccountCryptoCurrencyStatus(
account = accountStatusList.mainAccount.account,
status = currencyStatus,
)
val result = accountStatusList.getAccountCryptoCurrencyStatus(currency)
assertSome(result, expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetAccountCryptoCurrencyStatusByCurrencyIdAndNetwork {
@Test
fun `returns None when AccountStatusList is null`() {
val accountStatusList: AccountStatusList? = null
val result = accountStatusList.getAccountCryptoCurrencyStatus(
currencyId = currency.id,
network = currency.network,
)
assertNone(result)
}
@Test
fun `returns None when currency id is not found`() {
val currencyStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(currencyStatus),
)
val otherCurrencyId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId("bitcoin"),
suffix = CryptoCurrency.ID.Suffix.RawID("bitcoin"),
)
val result = accountStatusList.getAccountCryptoCurrencyStatus(
currencyId = otherCurrencyId,
network = currency.network,
)
assertNone(result)
}
@Test
fun `returns Some when currency id is found with null network`() {
val currencyStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(currencyStatus),
)
val expected = AccountCryptoCurrencyStatus(
account = accountStatusList.mainAccount.account,
status = currencyStatus,
)
val result = accountStatusList.getAccountCryptoCurrencyStatus(
currencyId = currency.id,
network = null,
)
assertSome(result, expected)
}
@Test
fun `returns Some when currency id and network match`() {
val currencyStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(currencyStatus),
)
val expected = AccountCryptoCurrencyStatus(
account = accountStatusList.mainAccount.account,
status = currencyStatus,
)
val result = accountStatusList.getAccountCryptoCurrencyStatus(
currencyId = currency.id,
network = currency.network,
)
assertSome(result, expected)
}
@Test
fun `returns Some with first matching currency when multiple currencies exist`() {
val currencies = cryptoCurrencyFactory.ethereumAndStellar
val currencyStatuses = currencies.map {
CryptoCurrencyStatus(currency = it, value = CryptoCurrencyStatus.Loading)
}
val accountStatusList = createAccountStatusList(
currencies = currencies,
currencyStatuses = currencyStatuses,
)
val targetCurrency = currencies.first()
val expected = AccountCryptoCurrencyStatus(
account = accountStatusList.mainAccount.account,
status = currencyStatuses.first(),
)
val result = accountStatusList.getAccountCryptoCurrencyStatus(
currencyId = targetCurrency.id,
network = targetCurrency.network,
)
assertSome(result, expected)
}
}
private fun createAccountStatusList(
currencies: List<CryptoCurrency>,
currencyStatuses: List<CryptoCurrencyStatus> = emptyList(),
): AccountStatusList {
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = currencies,
)
val tokenList = if (currencyStatuses.isEmpty()) {
TokenList.Empty
} else {
TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = currencyStatuses,
)
}
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = tokenList,
priceChangeLce = lceLoading(),
)
return AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(accountStatus),
totalAccounts = 1,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
}
}

View file

@ -0,0 +1,207 @@
package com.tangem.domain.account.status.utils
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.status.utils.CryptoCurrencyOperations.getCryptoCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.test.core.assertNone
import com.tangem.test.core.assertSome
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
* Tests for [CryptoCurrencyOperations].
*
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CryptoCurrencyOperationsTest {
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val userWalletId = UserWalletId("011")
private val currency = cryptoCurrencyFactory.ethereum
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetCryptoCurrencyFromAccountListByCurrency {
@Test
fun `returns None when currency is not found in AccountList`() {
// Arrange
val accountList = AccountList.empty(userWalletId)
// Act
val result = accountList.getCryptoCurrency(currency)
// Assert
assertNone(result)
}
@Test
fun `returns Some when currency is found in AccountList`() {
// Arrange
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
// Act
val result = accountList.getCryptoCurrency(currency)
// Assert
assertSome(result, currency)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetCryptoCurrencyFromAccountListByCurrencyIdAndNetwork {
@Test
fun `returns None when AccountList is null`() {
// Arrange
val accountList: AccountList? = null
// Act
val result = accountList.getCryptoCurrency(
currencyId = currency.id,
network = currency.network,
)
// Assert
assertNone(result)
}
@Test
fun `returns None when currency id is not found`() {
// Arrange
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
val otherCurrencyId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId("bitcoin"),
suffix = CryptoCurrency.ID.Suffix.RawID("bitcoin"),
)
// Act
val result = accountList.getCryptoCurrency(
currencyId = otherCurrencyId,
network = currency.network,
)
// Assert
assertNone(result)
}
@Test
fun `returns Some when currency id is found with null network`() {
// Arrange
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
// Act
val result = accountList.getCryptoCurrency(
currencyId = currency.id,
network = null,
)
// Assert
assertSome(result, currency)
}
@Test
fun `returns Some when currency id and network match`() {
// Arrange
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
// Act
val result = accountList.getCryptoCurrency(
currencyId = currency.id,
network = currency.network,
)
// Assert
assertSome(result, currency)
}
@Test
fun `returns Some with first matching currency when multiple currencies exist`() {
// Arrange
val currencies = cryptoCurrencyFactory.ethereumAndStellar
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = currencies,
)
val targetCurrency = currencies.first()
// Act
val result = accountList.getCryptoCurrency(
currencyId = targetCurrency.id,
network = targetCurrency.network,
)
// Assert
assertSome(result, targetCurrency)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetCryptoCurrencyFromCryptoPortfolio {
@Test
fun `returns None when currency id is not found in CryptoPortfolio`() {
// Arrange
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
val otherCurrencyId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId("bitcoin"),
suffix = CryptoCurrency.ID.Suffix.RawID("bitcoin"),
)
// Act
val result = account.getCryptoCurrency(otherCurrencyId)
// Assert
assertNone(result)
}
@Test
fun `returns Some when currency id is found in CryptoPortfolio`() {
// Arrange
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
// Act
val result = account.getCryptoCurrency(currency.id)
// Assert
assertSome(result, currency)
}
@Test
fun `returns None when CryptoPortfolio has no currencies`() {
// Arrange
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = emptyList(),
)
// Act
val result = account.getCryptoCurrency(currency.id)
// Assert
assertNone(result)
}
@Test
fun `returns Some with matching currency when multiple currencies exist`() {
// Arrange
val currencies = cryptoCurrencyFactory.ethereumAndStellar
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = currencies,
)
val targetCurrency = currencies.last()
// Act
val result = account.getCryptoCurrency(targetCurrency.id)
// Assert
assertSome(result, targetCurrency)
}
}
}

View file

@ -0,0 +1,545 @@
package com.tangem.domain.account.status.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCoinStatus
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus
import com.tangem.domain.core.utils.lceLoading
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.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.test.core.assertNone
import com.tangem.test.core.assertSome
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
* Tests for [CryptoCurrencyStatusOperations].
*
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CryptoCurrencyStatusOperationsTest {
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val userWalletId = UserWalletId("011")
private val currency = cryptoCurrencyFactory.ethereum
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetCryptoCurrencyStatusFromAccountStatusListByCurrency {
@Test
fun `returns None when AccountStatusList is null`() {
// Arrange
val accountStatusList: AccountStatusList? = null
// Act
val result = accountStatusList.getCryptoCurrencyStatus(currency)
// Assert
assertNone(result)
}
@Test
fun `returns None when currency is not found in AccountStatusList`() {
// Arrange
val accountStatusList = createAccountStatusList(currencies = emptyList())
// Act
val result = accountStatusList.getCryptoCurrencyStatus(currency)
// Assert
assertNone(result)
}
@Test
fun `returns Some when currency is found in AccountStatusList`() {
// Arrange
val currencyStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(currencyStatus),
)
// Act
val result = accountStatusList.getCryptoCurrencyStatus(currency)
// Assert
assertSome(result, currencyStatus)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetCryptoCurrencyStatusFromAccountStatusListByCurrencyIdAndNetwork {
@Test
fun `returns None when AccountStatusList is null`() {
// Arrange
val accountStatusList: AccountStatusList? = null
// Act
val result = accountStatusList.getCryptoCurrencyStatus(
currencyId = currency.id,
network = currency.network,
)
// Assert
assertNone(result)
}
@Test
fun `returns None when currency id is not found`() {
// Arrange
val currencyStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(currencyStatus),
)
val otherCurrencyId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId("bitcoin"),
suffix = CryptoCurrency.ID.Suffix.RawID("bitcoin"),
)
// Act
val result = accountStatusList.getCryptoCurrencyStatus(
currencyId = otherCurrencyId,
network = currency.network,
)
// Assert
assertNone(result)
}
@Test
fun `returns Some when currency id is found with null network`() {
// Arrange
val currencyStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(currencyStatus),
)
// Act
val result = accountStatusList.getCryptoCurrencyStatus(
currencyId = currency.id,
network = null,
)
// Assert
assertSome(result, currencyStatus)
}
@Test
fun `returns Some when currency id and network match`() {
// Arrange
val currencyStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(currencyStatus),
)
// Act
val result = accountStatusList.getCryptoCurrencyStatus(
currencyId = currency.id,
network = currency.network,
)
// Assert
assertSome(result, currencyStatus)
}
@Test
fun `returns Some with first matching currency status when multiple currencies exist`() {
// Arrange
val currencies = cryptoCurrencyFactory.ethereumAndStellar
val currencyStatuses = currencies.map {
CryptoCurrencyStatus(currency = it, value = CryptoCurrencyStatus.Loading)
}
val accountStatusList = createAccountStatusList(
currencies = currencies,
currencyStatuses = currencyStatuses,
)
val targetCurrency = currencies.first()
val expectedStatus = currencyStatuses.first()
// Act
val result = accountStatusList.getCryptoCurrencyStatus(
currencyId = targetCurrency.id,
network = targetCurrency.network,
)
// Assert
assertSome(result, expectedStatus)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetCryptoCurrencyStatusFromCryptoPortfolio {
@Test
fun `returns None when currency id is not found in CryptoPortfolio`() {
// Arrange
val currencyStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = listOf(currencyStatus),
),
priceChangeLce = lceLoading(),
)
val otherCurrencyId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId("bitcoin"),
suffix = CryptoCurrency.ID.Suffix.RawID("bitcoin"),
)
// Act
val result = accountStatus.getCryptoCurrencyStatus(otherCurrencyId)
// Assert
assertNone(result)
}
@Test
fun `returns Some when currency id is found in CryptoPortfolio`() {
// Arrange
val currencyStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = listOf(currency),
)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = listOf(currencyStatus),
),
priceChangeLce = lceLoading(),
)
// Act
val result = accountStatus.getCryptoCurrencyStatus(currency.id)
// Assert
assertSome(result, currencyStatus)
}
@Test
fun `returns None when CryptoPortfolio has empty token list`() {
// Arrange
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = emptyList(),
)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Empty,
priceChangeLce = lceLoading(),
)
// Act
val result = accountStatus.getCryptoCurrencyStatus(currency.id)
// Assert
assertNone(result)
}
@Test
fun `returns Some with matching currency status when multiple currencies exist`() {
// Arrange
val currencies = cryptoCurrencyFactory.ethereumAndStellar
val currencyStatuses = currencies.map {
CryptoCurrencyStatus(currency = it, value = CryptoCurrencyStatus.Loading)
}
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = currencies,
)
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = currencyStatuses,
),
priceChangeLce = lceLoading(),
)
val targetCurrency = currencies.last()
val expectedStatus = currencyStatuses.last()
// Act
val result = accountStatus.getCryptoCurrencyStatus(targetCurrency.id)
// Assert
assertSome(result, expectedStatus)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetCoinStatusByCurrency {
@Test
fun `returns None when no coin exists for the currency network`() {
// Arrange
val token = cryptoCurrencyFactory.createToken(Blockchain.Ethereum)
val tokenStatus = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(token),
currencyStatuses = listOf(tokenStatus),
)
// Act
val result = accountStatusList.getCoinStatus(token)
// Assert
assertNone(result)
}
@Test
fun `returns Some when coin exists for the currency network`() {
// Arrange
val coinStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(coinStatus),
)
// Act
val result = accountStatusList.getCoinStatus(currency)
// Assert
assertSome(result, coinStatus)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetCoinStatusByNetwork {
@Test
fun `returns None when AccountStatusList has empty token list`() {
// Arrange
val accountStatusList = createAccountStatusList(currencies = emptyList())
// Act
val result = accountStatusList.getCoinStatus(currency.network)
// Assert
assertNone(result)
}
@Test
fun `returns None when only tokens exist for network`() {
// Arrange
val token = cryptoCurrencyFactory.createToken(Blockchain.Ethereum)
val tokenStatus = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(token),
currencyStatuses = listOf(tokenStatus),
)
// Act
val result = accountStatusList.getCoinStatus(currency.network)
// Assert
assertNone(result)
}
@Test
fun `returns Some when coin exists for network`() {
// Arrange
val coinStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(coinStatus),
)
// Act
val result = accountStatusList.getCoinStatus(currency.network)
// Assert
assertSome(result, coinStatus)
}
@Test
fun `returns coin status when both coin and token exist for same network`() {
// Arrange
val token = cryptoCurrencyFactory.createToken(Blockchain.Ethereum)
val coinStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val tokenStatus = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency, token),
currencyStatuses = listOf(coinStatus, tokenStatus),
)
// Act
val result = accountStatusList.getCoinStatus(currency.network)
// Assert
assertSome(result, coinStatus)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetCoinStatusByNetworkId {
@Test
fun `returns None when AccountStatusList has empty token list`() {
// Arrange
val accountStatusList = createAccountStatusList(currencies = emptyList())
// Act
val result = accountStatusList.getCoinStatus(currency.network.id)
// Assert
assertNone(result)
}
@Test
fun `returns None when network id does not match any currency`() {
// Arrange
val coinStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(coinStatus),
)
val otherNetworkId = Network.ID(value = "bitcoin", derivationPath = Network.DerivationPath.None)
// Act
val result = accountStatusList.getCoinStatus(otherNetworkId)
// Assert
assertNone(result)
}
@Test
fun `returns None when only tokens exist for network id`() {
// Arrange
val token = cryptoCurrencyFactory.createToken(Blockchain.Ethereum)
val tokenStatus = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(token),
currencyStatuses = listOf(tokenStatus),
)
// Act
val result = accountStatusList.getCoinStatus(token.network.id)
// Assert
assertNone(result)
}
@Test
fun `returns Some when coin exists for network id`() {
// Arrange
val coinStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency),
currencyStatuses = listOf(coinStatus),
)
// Act
val result = accountStatusList.getCoinStatus(currency.network.id)
// Assert
assertSome(result, coinStatus)
}
@Test
fun `returns coin status when both coin and token exist for same network id`() {
// Arrange
val token = cryptoCurrencyFactory.createToken(Blockchain.Ethereum)
val coinStatus = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Loading,
)
val tokenStatus = CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Loading,
)
val accountStatusList = createAccountStatusList(
currencies = listOf(currency, token),
currencyStatuses = listOf(coinStatus, tokenStatus),
)
// Act
val result = accountStatusList.getCoinStatus(currency.network.id)
// Assert
assertSome(result, coinStatus)
}
@Test
fun `returns correct coin when multiple networks exist`() {
// Arrange
val currencies = cryptoCurrencyFactory.ethereumAndStellar
val currencyStatuses = currencies.map {
CryptoCurrencyStatus(currency = it, value = CryptoCurrencyStatus.Loading)
}
val accountStatusList = createAccountStatusList(
currencies = currencies,
currencyStatuses = currencyStatuses,
)
val targetCurrency = currencies.last()
val expectedStatus = currencyStatuses.last()
// Act
val result = accountStatusList.getCoinStatus(targetCurrency.network.id)
// Assert
assertSome(result, expectedStatus)
}
}
private fun createAccountStatusList(
currencies: List<CryptoCurrency>,
currencyStatuses: List<CryptoCurrencyStatus> = emptyList(),
): AccountStatusList {
val account = Account.CryptoPortfolio.createMainAccount(
userWalletId = userWalletId,
cryptoCurrencies = currencies,
)
val tokenList = if (currencyStatuses.isEmpty()) {
TokenList.Empty
} else {
TokenList.Ungrouped(
totalFiatBalance = TotalFiatBalance.Loading,
sortedBy = TokensSortType.NONE,
currencies = currencyStatuses,
)
}
val accountStatus = AccountStatus.CryptoPortfolio(
account = account,
tokenList = tokenList,
priceChangeLce = lceLoading(),
)
return AccountStatusList(
userWalletId = userWalletId,
accountStatuses = listOf(accountStatus),
totalAccounts = 1,
totalArchivedAccounts = 0,
totalFiatBalance = TotalFiatBalance.Loading,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
}
}