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

View file

@ -1,6 +1,7 @@
package com.tangem.feature.tokendetails.domain package com.tangem.feature.tokendetails.domain
import arrow.core.none import arrow.core.none
import arrow.core.some
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier 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.network.Network
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier 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.model.warnings.DynamicAddressesWarnings
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository 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.AfterEach
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.math.BigDecimal
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class GetCurrencyWarningsUseCaseTest { class GetCurrencyWarningsUseCaseTest {
@ -73,6 +76,7 @@ class GetCurrencyWarningsUseCaseTest {
coEvery { currencyChecksRepository.getExistentialDeposit(any(), any()) } returns null coEvery { currencyChecksRepository.getExistentialDeposit(any(), any()) } returns null
coEvery { currencyChecksRepository.getFeeResourceAmount(any(), any()) } returns null coEvery { currencyChecksRepository.getFeeResourceAmount(any(), any()) } returns null
coEvery { walletManagersFacade.getAssetRequirements(any(), any()) } returns null coEvery { walletManagersFacade.getAssetRequirements(any(), any()) } returns null
every { dynamicAddressesRepository.hasFundsOnAdditionalAddresses(any(), any()) } returns flowOf(false)
} }
@AfterEach @AfterEach
@ -130,12 +134,152 @@ class GetCurrencyWarningsUseCaseTest {
assertThat(result).doesNotContain(DynamicAddressesWarnings.FundsFound) 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 { private fun statusFor(currency: CryptoCurrency): CryptoCurrencyStatus {
return mockk(relaxed = true) { return mockk(relaxed = true) {
every { this@mockk.currency } returns currency 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 { private class TestDispatchers(dispatcher: CoroutineDispatcher) : CoroutineDispatcherProvider {
override val main: CoroutineDispatcher = dispatcher override val main: CoroutineDispatcher = dispatcher
override val mainImmediate: CoroutineDispatcher = dispatcher override val mainImmediate: CoroutineDispatcher = dispatcher