Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-06 15:38:09 +04:00
parent 5463c87ff0
commit 37a31042c0
10 changed files with 1442 additions and 97 deletions

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,81 @@ 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
private fun AccountStatusList.getExpectedAccountStatuses(networks: List<Network>): List<AccountStatus> {
val possibleAccountIndexes = networks.mapNotNull { getAccountIndexOrNull(it.rawId, it.derivationPath) }
if (possibleAccountIndexes.isEmpty()) return accountStatuses
val filteredStatuses = accountStatuses.filter { accountStatus ->
val cryptoPortfolio = accountStatus.account as? Account.CryptoPortfolio ?: return@filter false
cryptoPortfolio.derivationIndex.value in possibleAccountIndexes
}
return filteredStatuses + listOf(mainAccount)
}
// endregion
// region AccountList helpers
private fun AccountList.getExpectedAccounts(network: Network?): List<Account> {
return getExpectedAccounts(rawNetworkId = network?.rawId, derivationPath = network?.derivationPath)
}
private fun AccountList.getExpectedAccounts(
rawNetworkId: String?,
derivationPath: Network.DerivationPath?,
): List<Account> {
val possibleAccountIndex = getAccountIndexOrNull(rawNetworkId, derivationPath)
return when (possibleAccountIndex) {
// currency can be in any account
null -> accountStatuses
// currency only in the main account
null -> accounts
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
val account = accounts.firstOrNull { account ->
val cryptoPortfolio = account as? Account.CryptoPortfolio ?: return@firstOrNull false
cryptoPortfolio.derivationIndex.value == possibleAccountIndex
}
listOfNotNull(accountStatus, mainAccount)
listOfNotNull(account, mainAccount)
}
}
}
private fun AccountStatusList.getExpectedAccountStatuses(networks: List<Network>): List<AccountStatus> {
val possibleAccountIndexes = networks.mapNotNull { it.getAccountIndexOrNull() }
// endregion
if (possibleAccountIndexes.isEmpty()) return this@getExpectedAccountStatuses.accountStatuses
// region Common helpers
val accountStatuses = this@getExpectedAccountStatuses.accountStatuses.filter { accountStatus ->
val cryptoPortfolio = accountStatus.account as? Account.CryptoPortfolio ?: return@filter false
private fun getAccountIndexOrNull(rawNetworkId: String?, derivationPath: Network.DerivationPath?): Int? {
if (rawNetworkId == null || derivationPath == null) return null
cryptoPortfolio.derivationIndex.value in possibleAccountIndexes
}
return accountStatuses + listOf(mainAccount)
}
private fun Network.getAccountIndexOrNull(): Int? {
return getAccountIndexOrNull(rawNetworkId = rawId, derivationPath = derivationPath)
}
private fun getAccountIndexOrNull(rawNetworkId: String, derivationPath: Network.DerivationPath): Int? {
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

@ -0,0 +1,78 @@
package com.tangem.domain.account.status.utils
import arrow.core.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.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
/**
* 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)
}
/**
* 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,89 @@
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.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)
}
// 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,321 @@
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.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.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)
}
}
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,
)
}
}