Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-19 10:46:16 +04:00
parent 16fd81429c
commit 2887693055
8 changed files with 374 additions and 39 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.exchange.RampStateManager
@ -14,6 +15,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
@ -77,11 +79,11 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideIsCryptoCurrencyCoinCouldHideUseCase(
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
): IsCryptoCurrencyCoinCouldHideUseCase {
return IsCryptoCurrencyCoinCouldHideUseCase(
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
fun provideIsCryptoCurrencyCouldHideUseCase(
singleAccountListSupplier: SingleAccountListSupplier,
): IsCryptoCurrencyCouldHideUseCase {
return IsCryptoCurrencyCouldHideUseCase(
singleAccountListSupplier = singleAccountListSupplier,
)
}

View file

@ -0,0 +1,39 @@
package com.tangem.domain.account.status.usecase
import com.tangem.domain.account.status.utils.CryptoCurrencyOperations.getTokens
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
/**
* Use case to determine if a cryptocurrency can be hidden from the portfolio.
*
* Rules:
* - [CryptoCurrency.Token] can always be hidden (returns `true`)
* - [CryptoCurrency.Coin] can be hidden only if there are no tokens in the same network (returns `true` if no tokens)
*
* @property singleAccountListSupplier supplier to get the account list with all cryptocurrencies
*/
class IsCryptoCurrencyCouldHideUseCase(
private val singleAccountListSupplier: SingleAccountListSupplier,
) {
/**
* Checks if the given cryptocurrency can be hidden.
*
* @param userWalletId the user wallet identifier
* @param cryptoCurrency the cryptocurrency to check
* @return `true` if the cryptocurrency can be hidden, `false` otherwise
*/
suspend operator fun invoke(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
return when (cryptoCurrency) {
is CryptoCurrency.Token -> true
is CryptoCurrency.Coin -> {
val accountList = singleAccountListSupplier.getSyncOrNull(userWalletId = userWalletId)
?: return true
accountList.getTokens(cryptoCurrency).isEmpty()
}
}
}
}

View file

@ -69,6 +69,23 @@ object CryptoCurrencyOperations {
return getCoin(network = currency.network)
}
/**
* Retrieves all tokens that belong to the same network as the specified [coin].
*
* @receiver the [AccountList] to search within
* @param coin the coin whose network will be used to find tokens
* @return list of [CryptoCurrency.Token] in the same network, or empty list if none found
*/
fun AccountList.getTokens(coin: CryptoCurrency.Coin): List<CryptoCurrency.Token> {
return getExpectedAccounts(network = coin.network)
.flatMap { account ->
(account as? Account.CryptoPortfolio)
?.cryptoCurrencies.orEmpty()
.filterIsInstance<CryptoCurrency.Token>()
}
.filter { token -> token.network.id == coin.network.id }
}
fun AccountList.getCoin(network: Network): Option<CryptoCurrency.Coin> {
return getExpectedAccounts(network = network)
.flatMap { account ->

View file

@ -0,0 +1,184 @@
package com.tangem.domain.account.status.usecase
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.test.mock.MockAccounts
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.*
/**
* Tests for [IsCryptoCurrencyCouldHideUseCase]
*
* Test cases:
* 1. Token always returns true (can hide)
* 2. Coin without tokens in the same network returns true (can hide)
* 3. Coin with tokens in the same network returns false (cannot hide)
* 4. Coin with tokens only in other networks returns true (can hide)
* 5. Supplier returns null returns true (can hide)
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class IsCryptoCurrencyCouldHideUseCaseTest {
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val singleAccountListSupplier: SingleAccountListSupplier = mockk()
private val useCase = IsCryptoCurrencyCouldHideUseCase(
singleAccountListSupplier = singleAccountListSupplier,
)
private val userWalletId = MockAccounts.userWalletId
@AfterEach
fun tearDown() {
clearMocks(singleAccountListSupplier)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class TokenTests {
@Test
fun `Token always returns true`() = runTest {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
val token = cryptoCurrencyFactory.createToken(com.tangem.blockchain.common.Blockchain.Ethereum)
val accountList = createAccountList(listOf(coin, token))
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns accountList
// Act
val result = useCase(userWalletId = userWalletId, cryptoCurrency = token)
// Assert
Truth.assertThat(result).isTrue()
}
@Test
fun `Token returns true even when supplier returns null`() = runTest {
// Arrange
val token = cryptoCurrencyFactory.createToken(com.tangem.blockchain.common.Blockchain.Ethereum)
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns null
// Act
val result = useCase(userWalletId = userWalletId, cryptoCurrency = token)
// Assert
Truth.assertThat(result).isTrue()
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CoinTests {
@Test
fun `Coin without tokens in the same network returns true`() = runTest {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
val accountList = createAccountList(listOf(coin))
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns accountList
// Act
val result = useCase(userWalletId = userWalletId, cryptoCurrency = coin)
// Assert
Truth.assertThat(result).isTrue()
}
@Test
fun `Coin with tokens in the same network returns false`() = runTest {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
val tokenOnSameNetwork = cryptoCurrencyFactory.createToken(com.tangem.blockchain.common.Blockchain.Ethereum)
val accountList = createAccountList(listOf(coin, tokenOnSameNetwork))
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns accountList
// Act
val result = useCase(userWalletId = userWalletId, cryptoCurrency = coin)
// Assert
Truth.assertThat(result).isFalse()
}
@Test
fun `Coin with multiple tokens in the same network returns false`() = runTest {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
val token1 = cryptoCurrencyFactory.createToken(com.tangem.blockchain.common.Blockchain.Ethereum)
val token2 = cryptoCurrencyFactory.createToken(com.tangem.blockchain.common.Blockchain.Ethereum)
val accountList = createAccountList(listOf(coin, token1, token2))
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns accountList
// Act
val result = useCase(userWalletId = userWalletId, cryptoCurrency = coin)
// Assert
Truth.assertThat(result).isFalse()
}
@Test
fun `Coin with tokens only in other networks returns true`() = runTest {
// Arrange
val ethereumCoin = cryptoCurrencyFactory.ethereum
val stellarCoin = cryptoCurrencyFactory.stellar
val stellarToken = cryptoCurrencyFactory.createToken(com.tangem.blockchain.common.Blockchain.Stellar)
val accountList = createAccountList(listOf(ethereumCoin, stellarCoin, stellarToken))
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns accountList
// Act
val result = useCase(userWalletId = userWalletId, cryptoCurrency = ethereumCoin)
// Assert
Truth.assertThat(result).isTrue()
}
@Test
fun `Coin returns true when supplier returns null`() = runTest {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns null
// Act
val result = useCase(userWalletId = userWalletId, cryptoCurrency = coin)
// Assert
Truth.assertThat(result).isTrue()
}
@Test
fun `Coin returns true when account list has no currencies`() = runTest {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
val accountList = createAccountList(emptyList())
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns accountList
// Act
val result = useCase(userWalletId = userWalletId, cryptoCurrency = coin)
// Assert
Truth.assertThat(result).isTrue()
}
}
private fun createAccountList(currencies: List<CryptoCurrency>): AccountList {
return MockAccounts.createAccountList(
activeAccounts = 1,
userWalletId = userWalletId,
).let { accountList ->
val mainAccount = accountList.mainAccount.copy(cryptoCurrencies = currencies)
AccountList(
userWalletId = userWalletId,
accounts = listOf(mainAccount),
totalAccounts = 1,
totalArchivedAccounts = 0,
).getOrNull()!!
}
}
}

View file

@ -2,12 +2,15 @@ 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.blockchain.common.Blockchain
import com.tangem.domain.account.status.utils.CryptoCurrencyOperations.getCryptoCurrency
import com.tangem.domain.account.status.utils.CryptoCurrencyOperations.getTokens
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 com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@ -204,4 +207,113 @@ class CryptoCurrencyOperationsTest {
assertSome(result, targetCurrency)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetTokensFromAccountList {
@Test
fun `returns empty list when no tokens exist in the same network as the coin`() {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(coin),
)
// Act
val result = accountList.getTokens(coin)
// Assert
assertThat(result).isEmpty()
}
@Test
fun `returns empty list when only coins exist in AccountList`() {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
val otherCoin = cryptoCurrencyFactory.stellar
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(coin, otherCoin),
)
// Act
val result = accountList.getTokens(coin)
// Assert
assertThat(result).isEmpty()
}
@Test
fun `returns tokens when tokens exist in the same network`() {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
val token = cryptoCurrencyFactory.createToken(Blockchain.Ethereum)
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(coin, token),
)
// Act
val result = accountList.getTokens(coin)
// Assert
assertThat(result).containsExactly(token)
}
@Test
fun `returns multiple tokens when multiple tokens exist in the same network`() {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
val token1 = cryptoCurrencyFactory.createToken(Blockchain.Ethereum)
val token2 = cryptoCurrencyFactory.createToken(Blockchain.Ethereum)
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(coin, token1, token2),
)
// Act
val result = accountList.getTokens(coin)
// Assert
assertThat(result).containsExactly(token1, token2)
}
@Test
fun `returns empty list when tokens exist only in other networks`() {
// Arrange
val ethereumCoin = cryptoCurrencyFactory.ethereum
val stellarCoin = cryptoCurrencyFactory.stellar
val stellarToken = cryptoCurrencyFactory.createToken(Blockchain.Stellar)
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(ethereumCoin, stellarCoin, stellarToken),
)
// Act
val result = accountList.getTokens(ethereumCoin)
// Assert
assertThat(result).isEmpty()
}
@Test
fun `returns only tokens from the same network when tokens exist in multiple networks`() {
// Arrange
val ethereumCoin = cryptoCurrencyFactory.ethereum
val ethereumToken = cryptoCurrencyFactory.createToken(Blockchain.Ethereum)
val stellarCoin = cryptoCurrencyFactory.stellar
val stellarToken = cryptoCurrencyFactory.createToken(Blockchain.Stellar)
val accountList = AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(ethereumCoin, ethereumToken, stellarCoin, stellarToken),
)
// Act
val result = accountList.getTokens(ethereumCoin)
// Assert
assertThat(result).containsExactly(ethereumToken)
}
@Test
fun `returns empty list when AccountList is empty`() {
// Arrange
val coin = cryptoCurrencyFactory.ethereum
val accountList = AccountList.empty(userWalletId)
// Act
val result = accountList.getTokens(coin)
// Assert
assertThat(result).isEmpty()
}
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.domain.tokens
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
class IsCryptoCurrencyCoinCouldHideUseCase(
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
) {
suspend operator fun invoke(userWalletId: UserWalletId, cryptoCurrencyCoin: CryptoCurrency.Coin): Boolean {
return multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
)
.orEmpty()
.none { it is CryptoCurrency.Token && it.network == cryptoCurrencyCoin.network }
}
}

View file

@ -34,6 +34,7 @@ import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -114,7 +115,7 @@ internal class TokenDetailsModel @Inject constructor(
private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
private val getExploreUrlUseCase: GetExploreUrlUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val isCryptoCurrencyCoinCouldHideUseCase: IsCryptoCurrencyCoinCouldHideUseCase,
private val isCryptoCurrencyCouldHideUseCase: IsCryptoCurrencyCouldHideUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
@ -737,15 +738,10 @@ internal class TokenDetailsModel @Inject constructor(
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrency.symbol))
modelScope.launch {
val canHide = when (cryptoCurrency) {
is CryptoCurrency.Coin -> {
isCryptoCurrencyCoinCouldHideUseCase(
userWalletId = userWalletId,
cryptoCurrencyCoin = cryptoCurrency,
)
}
is CryptoCurrency.Token -> true
}
val canHide = isCryptoCurrencyCouldHideUseCase(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
)
internalUiState.value = if (canHide) {
stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency)

View file

@ -44,7 +44,7 @@ import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
import com.tangem.domain.tokens.NeedShowYieldSupplyDepositedWarningUseCase
import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.SaveViewedYieldSupplyWarningUseCase
@ -145,7 +145,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val receiveAddressesFactory: ReceiveAddressesFactory,
private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase,
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
private val isCryptoCurrencyCouldHideUseCase: IsCryptoCurrencyCouldHideUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val uiMessageSender: UiMessageSender,
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
@ -268,17 +268,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
modelScope.launch(dispatchers.main) {
val currency = cryptoCurrencyStatus.currency
val isCryptoCurrencyCoinCouldHide = currency is CryptoCurrency.Coin &&
!isCryptoCurrencyCoinCouldHide(userWalletId = accountId.userWalletId, cryptoCurrencyCoin = currency)
val canHide = isCryptoCurrencyCouldHideUseCase(
userWalletId = accountId.userWalletId,
cryptoCurrency = currency,
)
if (isCryptoCurrencyCoinCouldHide) {
uiMessageSender.send(WalletAlertUM.unableHideToken(cryptoCurrency = cryptoCurrencyStatus.currency))
} else {
if (canHide) {
uiMessageSender.send(
WalletAlertUM.hideTokenConfirm(cryptoCurrency = cryptoCurrencyStatus.currency) {
onPerformHideToken(accountId, cryptoCurrencyStatus)
},
)
} else {
uiMessageSender.send(WalletAlertUM.unableHideToken(cryptoCurrency = cryptoCurrencyStatus.currency))
}
}
}