Updated on 2026-08-14
This commit is contained in:
parent
aefaf636e8
commit
9ff657252a
8 changed files with 272 additions and 13 deletions
|
|
@ -25,12 +25,15 @@ dependencies {
|
|||
api(projects.domain.staking)
|
||||
api(projects.domain.tokens)
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.crypto)
|
||||
|
||||
implementation(deps.kotlin.datetime)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.timber)
|
||||
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.account.status.di
|
||||
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
|
|
@ -31,7 +32,9 @@ internal object AccountStatusUseCaseModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetAccountCurrencyStatusUseCase(): GetAccountCurrencyStatusUseCase {
|
||||
return GetAccountCurrencyStatusUseCase()
|
||||
fun provideGetAccountCurrencyStatusUseCase(
|
||||
singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
): GetAccountCurrencyStatusUseCase {
|
||||
return GetAccountCurrencyStatusUseCase(singleAccountStatusListSupplier = singleAccountStatusListSupplier)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,24 +2,110 @@ package com.tangem.domain.account.status.usecase
|
|||
|
||||
import arrow.core.Option
|
||||
import arrow.core.none
|
||||
import arrow.core.toOption
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
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.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
|
||||
/**
|
||||
* Use case to retrieve the status of a specific cryptocurrency associated with an account.
|
||||
*
|
||||
* @property singleAccountStatusListSupplier supplier to get the list of account statuses.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// TODO: Implement [REDACTED_JIRA]
|
||||
class GetAccountCurrencyStatusUseCase {
|
||||
class GetAccountCurrencyStatusUseCase(
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Invokes the use case to get the [AccountCryptoCurrencyStatus] for the given [currencyId].
|
||||
* Invokes the use case to get the status of a specific cryptocurrency for a given user wallet.
|
||||
*
|
||||
* @param currencyId The ID of the cryptocurrency to look up.
|
||||
*
|
||||
* @return An [Option] containing the [AccountCryptoCurrencyStatus] if found,
|
||||
* or [arrow.core.None] if not found or if any validation fails.
|
||||
* @param userWalletId the ID of the user wallet.
|
||||
* @param currency the cryptocurrency for which the status is to be retrieved.
|
||||
* @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None.
|
||||
*/
|
||||
suspend operator fun invoke(currencyId: CryptoCurrency.ID): Option<AccountCryptoCurrencyStatus> = none()
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
): Option<AccountCryptoCurrencyStatus> {
|
||||
return invoke(userWalletId = userWalletId, currencyId = currency.id, network = currency.network)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the use case to get the status of a specific cryptocurrency by its ID for a given user wallet and network.
|
||||
* If the [network] is null, it searches across all accounts for the cryptocurrency.
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet.
|
||||
* @param currencyId the ID of the cryptocurrency.
|
||||
* @param network the network associated with the cryptocurrency, can be null.
|
||||
* @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None.
|
||||
*/
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
currencyId: CryptoCurrency.ID,
|
||||
network: Network?,
|
||||
): Option<AccountCryptoCurrencyStatus> {
|
||||
val accountStatusList = singleAccountStatusListSupplier.getSyncOrNull(
|
||||
params = SingleAccountStatusListProducer.Params(userWalletId),
|
||||
) ?: return none()
|
||||
|
||||
return accountStatusList.getExpectedAccountStatuses(network)
|
||||
.asSequence()
|
||||
.filterIsInstance<AccountStatus.CryptoPortfolio>()
|
||||
.mapNotNull { accountStatus ->
|
||||
val status = accountStatus.flattenCurrencies().firstOrNull { it.currency.id == currencyId }
|
||||
?: return@mapNotNull null
|
||||
|
||||
AccountCryptoCurrencyStatus(account = accountStatus.account, status = status)
|
||||
}
|
||||
.firstOrNull()
|
||||
.toOption()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expected account statuses based on the provided [network].
|
||||
* If the [network] is null, all account statuses are returned.
|
||||
* If the network has a specific derivation index, it filters the accounts accordingly.
|
||||
*
|
||||
* @param network the network to filter accounts by, can be null.
|
||||
* @return a set of [AccountStatus] that match the expected criteria.
|
||||
*/
|
||||
private fun AccountStatusList.getExpectedAccountStatuses(network: Network?): Set<AccountStatus> {
|
||||
val possibleAccountIndex = network?.getAccountIndexOrNull()
|
||||
|
||||
return when (possibleAccountIndex) {
|
||||
// currency can be in any account
|
||||
null -> accountStatuses
|
||||
// currency only in the main account
|
||||
DerivationIndex.Main.value -> setOf(mainAccount)
|
||||
// currency only in the account with specific derivation index or in the main account
|
||||
else -> {
|
||||
val accountStatus = accountStatuses.firstOrNull {
|
||||
val cryptoPortfolio = it.account as? Account.CryptoPortfolio ?: return@firstOrNull false
|
||||
|
||||
cryptoPortfolio.derivationIndex.value == possibleAccountIndex
|
||||
}
|
||||
|
||||
setOfNotNull(accountStatus, mainAccount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Network.getAccountIndexOrNull(): Int? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = rawId) ?: return null
|
||||
val recognizer = AccountNodeRecognizer(blockchain)
|
||||
|
||||
return recognizer.recognize(derivationPath)?.toInt()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
package com.tangem.domain.account.status.usecase
|
||||
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.utils.assertNone
|
||||
import com.tangem.common.test.utils.assertSome
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
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.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.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class GetAccountCurrencyStatusUseCaseTest {
|
||||
|
||||
private val supplier = mockk<SingleAccountStatusListSupplier>()
|
||||
private val useCase = GetAccountCurrencyStatusUseCase(singleAccountStatusListSupplier = supplier)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val supplierParams = SingleAccountStatusListProducer.Params(userWalletId)
|
||||
private val currency = MockCryptoCurrencyFactory().ethereum.let {
|
||||
val derivationPath = Network.DerivationPath.Card("m/44'/60'/0'/0/1")
|
||||
|
||||
it.copy(
|
||||
network = it.network.copy(
|
||||
id = Network.ID(value = "ethereum", derivationPath = derivationPath),
|
||||
derivationPath = derivationPath,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(supplier)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns None when supplier returns null`() = runTest {
|
||||
// Arrange
|
||||
coEvery { supplier.getSyncOrNull(supplierParams) } returns null
|
||||
|
||||
// Act
|
||||
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns None when AccountList does not contain required currency id`() = runTest {
|
||||
// Arrange
|
||||
val accountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
|
||||
every { this@mockk.accountStatuses } returns setOf(accountStatus)
|
||||
}
|
||||
|
||||
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
|
||||
|
||||
// Act
|
||||
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
|
||||
|
||||
// Assert
|
||||
assertNone(actual)
|
||||
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns Some if network is not null`() = runTest {
|
||||
// Arrange
|
||||
val mainAccountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
tokenList = TokenList.Empty,
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
|
||||
every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!!
|
||||
every { this@mockk.cryptoCurrencies } returns setOf(currency)
|
||||
}
|
||||
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
|
||||
val accountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = account,
|
||||
tokenList = TokenList.Ungrouped(
|
||||
totalFiatBalance = TotalFiatBalance.Loading,
|
||||
sortedBy = TokensSortType.NONE,
|
||||
currencies = listOf(currencyStatus),
|
||||
),
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
|
||||
every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk())
|
||||
}
|
||||
|
||||
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
|
||||
|
||||
// Act
|
||||
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = currency.network)
|
||||
|
||||
// Assert
|
||||
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
|
||||
assertSome(actual, expected)
|
||||
|
||||
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns Some if network is null`() = runTest {
|
||||
// Arrange
|
||||
val account = mockk<Account.CryptoPortfolio>(relaxed = true) {
|
||||
every { this@mockk.cryptoCurrencies } returns setOf(currency)
|
||||
}
|
||||
val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)
|
||||
val accountStatus = AccountStatus.CryptoPortfolio(
|
||||
account = account,
|
||||
tokenList = TokenList.Ungrouped(
|
||||
totalFiatBalance = TotalFiatBalance.Loading,
|
||||
sortedBy = TokensSortType.NONE,
|
||||
currencies = listOf(currencyStatus),
|
||||
),
|
||||
priceChangeLce = lceLoading(),
|
||||
)
|
||||
|
||||
val accountStatusList = mockk<AccountStatusList>(relaxed = true) {
|
||||
every { this@mockk.accountStatuses } returns setOf(accountStatus)
|
||||
}
|
||||
|
||||
coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList
|
||||
|
||||
// Act
|
||||
val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null)
|
||||
|
||||
// Assert
|
||||
val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus)
|
||||
assertSome(actual, expected)
|
||||
coVerifyOrder { supplier.getSyncOrNull(supplierParams) }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.lib.crypto.derivation
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.isUTXO
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
||||
/**
|
||||
* Utility class to recognize the account node in a derivation path based on the blockchain type.
|
||||
|
|
@ -21,12 +22,21 @@ class AccountNodeRecognizer(blockchain: Blockchain) {
|
|||
NON_UTXO_BLOCKCHAIN_NODE_INDEX
|
||||
}
|
||||
|
||||
/** Recognizes the account node value from the given [derivationPath] */
|
||||
fun recognize(derivationPath: Network.DerivationPath): Long? {
|
||||
val derivationPathValue = derivationPath.value ?: return null
|
||||
|
||||
return recognize(derivationPathValue = derivationPathValue)
|
||||
}
|
||||
|
||||
/** Recognizes the account node value from the given derivation path string [derivationPathValue] */
|
||||
fun recognize(derivationPathValue: String): Long? {
|
||||
if (derivationPathValue.isBlank()) return null
|
||||
|
||||
return runCatching {
|
||||
recognize(derivationPath = DerivationPath(rawPath = derivationPathValue))
|
||||
}
|
||||
.getOrNull()
|
||||
val cardSdkDerivationPath = DerivationPath(rawPath = derivationPathValue)
|
||||
recognize(derivationPath = cardSdkDerivationPath)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
/** Recognizes the account node value from the given [derivationPath] */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue