Updated on 2026-08-14

This commit is contained in:
Tangem 2026-08-03 23:47:11 +04:00
parent 95ba8be221
commit 48f6177936
2 changed files with 166 additions and 10 deletions

View file

@ -45,25 +45,25 @@ internal class GetCurrencyWarningsUseCase @Inject constructor(
derivationPath: Network.DerivationPath,
): Flow<Set<CryptoCurrencyWarning>> {
val currency = currencyStatus.currency
val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, currency.network)
// don't add here notifications that require async requests
return combine(
flow = getCoinRelatedWarnings(
userWalletId = userWalletId,
currency = currency,
existentialDeposit = existentialDeposit,
),
flow2 = flowOf(currencyChecksRepository.getRentInfoWarning(userWalletId, currencyStatus)),
flow3 = flowOf(currencyChecksRepository.getExistentialDeposit(userWalletId, currency.network)),
flow4 = flowOf(currencyChecksRepository.getFeeResourceAmount(userWalletId, currency.network)),
flow5 = if (currency is CryptoCurrency.Coin) {
flow3 = flowOf(currencyChecksRepository.getFeeResourceAmount(userWalletId, currency.network)),
flow4 = if (currency is CryptoCurrency.Coin) {
dynamicAddressesRepository.hasFundsOnAdditionalAddresses(userWalletId, currency.network)
} else {
flowOf(false)
},
) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeFeeResource, hasExtraFunds ->
) { coinRelatedWarnings, maybeRentWarning, maybeFeeResource, hasExtraFunds ->
setOfNotNull(
maybeRentWarning,
maybeEdWarning?.let { getExistentialDepositWarning(currency, it) },
maybeFeeResource?.let { getFeeResourceWarning(it) },
*coinRelatedWarnings.toTypedArray(),
getNetworkUnavailableWarning(currencyStatus),
@ -80,6 +80,7 @@ internal class GetCurrencyWarningsUseCase @Inject constructor(
private suspend fun getCoinRelatedWarnings(
userWalletId: UserWalletId,
currency: CryptoCurrency,
existentialDeposit: BigDecimal?,
): Flow<List<CryptoCurrencyWarning>> {
return singleAccountStatusListSupplier(userWalletId)
.map { accountStatusList ->
@ -100,6 +101,10 @@ internal class GetCurrencyWarningsUseCase @Inject constructor(
coinStatus = coinStatus,
tokenStatus = tokenStatus,
),
getExistentialDepositWarning(
coinStatus = coinStatus,
existentialDeposit = existentialDeposit,
),
)
} else {
listOf(CryptoCurrencyWarning.SomeNetworksUnreachable)
@ -209,12 +214,19 @@ internal class GetCurrencyWarningsUseCase @Inject constructor(
}
private fun getExistentialDepositWarning(
currency: CryptoCurrency,
amount: BigDecimal,
): CryptoCurrencyWarning.ExistentialDeposit {
coinStatus: CryptoCurrencyStatus,
existentialDeposit: BigDecimal?,
): CryptoCurrencyWarning.ExistentialDeposit? {
if (existentialDeposit == null) return null
val coinAmount = coinStatus.value.amount ?: return null
if (coinAmount > existentialDeposit) return null
val coin = coinStatus.currency
return CryptoCurrencyWarning.ExistentialDeposit(
currencyName = currency.name,
edStringValueWithSymbol = "${amount.toPlainString()} ${currency.symbol}",
currencyName = coin.name,
edStringValueWithSymbol = "${existentialDeposit.toPlainString()} ${coin.symbol}",
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.tokendetails.domain
import arrow.core.none
import arrow.core.some
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
@ -11,6 +12,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.model.warnings.DynamicAddressesWarnings
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
@ -31,6 +33,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.math.BigDecimal
@OptIn(ExperimentalCoroutinesApi::class)
class GetCurrencyWarningsUseCaseTest {
@ -73,6 +76,7 @@ class GetCurrencyWarningsUseCaseTest {
coEvery { currencyChecksRepository.getExistentialDeposit(any(), any()) } returns null
coEvery { currencyChecksRepository.getFeeResourceAmount(any(), any()) } returns null
coEvery { walletManagersFacade.getAssetRequirements(any(), any()) } returns null
every { dynamicAddressesRepository.hasFundsOnAdditionalAddresses(any(), any()) } returns flowOf(false)
}
@AfterEach
@ -130,12 +134,152 @@ class GetCurrencyWarningsUseCaseTest {
assertThat(result).doesNotContain(DynamicAddressesWarnings.FundsFound)
}
@Test
fun `GIVEN coin balance above existential deposit WHEN invoke THEN ExistentialDeposit warning is absent`() =
runTest {
// GIVEN
val coin = coin()
val coinStatus = statusFor(currency = coin, amount = BigDecimal("0.98910916"))
givenCoinStatus(coinStatus)
coEvery { currencyChecksRepository.getExistentialDeposit(any(), any()) } returns BigDecimal("0.01")
// WHEN
val result = useCase.invoke(userWalletId, coinStatus, derivationPath).first()
// THEN
assertThat(result.filterIsInstance<CryptoCurrencyWarning.ExistentialDeposit>()).isEmpty()
}
@Test
fun `GIVEN coin balance equal to existential deposit WHEN invoke THEN ExistentialDeposit warning is present`() =
runTest {
// GIVEN
val coin = coin(name = "Polkadot Asset Hub", symbol = "DOT")
val coinStatus = statusFor(currency = coin, amount = BigDecimal("0.01"))
givenCoinStatus(coinStatus)
coEvery { currencyChecksRepository.getExistentialDeposit(any(), any()) } returns BigDecimal("0.01")
// WHEN
val result = useCase.invoke(userWalletId, coinStatus, derivationPath).first()
// THEN
assertThat(result).contains(
CryptoCurrencyWarning.ExistentialDeposit(
currencyName = "Polkadot Asset Hub",
edStringValueWithSymbol = "0.01 DOT",
),
)
}
@Test
fun `GIVEN coin balance below existential deposit WHEN invoke THEN ExistentialDeposit warning is present`() =
runTest {
// GIVEN
val coin = coin()
val coinStatus = statusFor(currency = coin, amount = BigDecimal("0.005"))
givenCoinStatus(coinStatus)
coEvery { currencyChecksRepository.getExistentialDeposit(any(), any()) } returns BigDecimal("0.01")
// WHEN
val result = useCase.invoke(userWalletId, coinStatus, derivationPath).first()
// THEN
assertThat(result.filterIsInstance<CryptoCurrencyWarning.ExistentialDeposit>()).hasSize(1)
}
@Test
fun `GIVEN unknown coin balance WHEN invoke THEN ExistentialDeposit warning is absent`() = runTest {
// GIVEN
val coin = coin()
val coinStatus = statusFor(currency = coin, amount = null)
givenCoinStatus(coinStatus)
coEvery { currencyChecksRepository.getExistentialDeposit(any(), any()) } returns BigDecimal("0.01")
// WHEN
val result = useCase.invoke(userWalletId, coinStatus, derivationPath).first()
// THEN
assertThat(result.filterIsInstance<CryptoCurrencyWarning.ExistentialDeposit>()).isEmpty()
}
@Test
fun `GIVEN network without existential deposit WHEN invoke THEN ExistentialDeposit warning is absent`() = runTest {
// GIVEN
val coin = coin()
val coinStatus = statusFor(currency = coin, amount = BigDecimal.ZERO)
givenCoinStatus(coinStatus)
coEvery { currencyChecksRepository.getExistentialDeposit(any(), any()) } returns null
// WHEN
val result = useCase.invoke(userWalletId, coinStatus, derivationPath).first()
// THEN
assertThat(result.filterIsInstance<CryptoCurrencyWarning.ExistentialDeposit>()).isEmpty()
}
@Test
fun `GIVEN token with balance AND coin below deposit WHEN invoke THEN warning is built from coin`() = runTest {
// GIVEN
val coin = coin(name = "Polkadot Asset Hub", symbol = "DOT")
val token: CryptoCurrency.Token = mockk(relaxed = true) {
every { this@mockk.network } returns this@GetCurrencyWarningsUseCaseTest.network
every { this@mockk.name } returns "Tether"
every { this@mockk.symbol } returns "USDT"
}
val coinStatus = statusFor(currency = coin, amount = BigDecimal("0.005"))
val tokenStatus = statusFor(currency = token, amount = BigDecimal("100"))
givenStatuses(coinStatus = coinStatus, currencyStatus = tokenStatus)
coEvery { currencyChecksRepository.getExistentialDeposit(any(), any()) } returns BigDecimal("0.01")
// WHEN
val result = useCase.invoke(userWalletId, tokenStatus, derivationPath).first()
// THEN
assertThat(result).contains(
CryptoCurrencyWarning.ExistentialDeposit(
currencyName = "Polkadot Asset Hub",
edStringValueWithSymbol = "0.01 DOT",
),
)
}
private fun coin(name: String = "Polkadot", symbol: String = "DOT"): CryptoCurrency.Coin {
return mockk(relaxed = true) {
every { this@mockk.network } returns this@GetCurrencyWarningsUseCaseTest.network
every { this@mockk.name } returns name
every { this@mockk.symbol } returns symbol
}
}
/** Makes both coin and token lookups resolve to [status], so coin-related warnings are actually built. */
private fun givenCoinStatus(status: CryptoCurrencyStatus) {
givenStatuses(coinStatus = status, currencyStatus = status)
}
private fun givenStatuses(coinStatus: CryptoCurrencyStatus, currencyStatus: CryptoCurrencyStatus) {
with(CryptoCurrencyStatusOperations) {
every { accountStatusList.getCoinStatus(any<CryptoCurrency>()) } returns coinStatus.some()
every { accountStatusList.getCryptoCurrencyStatus(any<CryptoCurrency>()) } returns currencyStatus.some()
}
}
private fun statusFor(currency: CryptoCurrency): CryptoCurrencyStatus {
return mockk(relaxed = true) {
every { this@mockk.currency } returns currency
}
}
private fun statusFor(currency: CryptoCurrency, amount: BigDecimal?): CryptoCurrencyStatus {
val statusValue: CryptoCurrencyStatus.Value = mockk(relaxed = true) {
every { this@mockk.amount } returns amount
}
return mockk(relaxed = true) {
every { this@mockk.currency } returns currency
every { this@mockk.value } returns statusValue
}
}
private class TestDispatchers(dispatcher: CoroutineDispatcher) : CoroutineDispatcherProvider {
override val main: CoroutineDispatcher = dispatcher
override val mainImmediate: CoroutineDispatcher = dispatcher