Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-23 18:28:45 +04:00
parent cab885e39f
commit 043602ebd6
9 changed files with 987 additions and 221 deletions

View file

@ -8,12 +8,12 @@ import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.wallets.derivations.derivationStyleProvider
/**
[REDACTED_AUTHOR]
@ -110,7 +110,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
)
}
fun createToken(blockchain: Blockchain): CryptoCurrency {
fun createToken(blockchain: Blockchain): CryptoCurrency.Token {
return factory.createToken(
sdkToken = Token(
name = "NEVER-MIND",

View file

@ -1,6 +1,8 @@
package com.tangem.data.swap
import arrow.core.right
import arrow.core.none
import arrow.core.some
import arrow.core.toOption
import com.squareup.moshi.Moshi
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.swap.converter.SwapDataConverter
@ -28,7 +30,7 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.models.*
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@ -49,7 +51,6 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
private val dataSignatureVerifier: DataSignatureVerifier,
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
private val currencyStatusProxyCreator: CurrencyStatusProxyCreator,
@NetworkMoshi moshi: Moshi,
) : SwapRepositoryV2 {
@ -391,17 +392,19 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
)
}
return currencyStatusProxyCreator.createCurrencyStatus(
val quoteStatus = quote ?: singleQuoteStatusSupplier.getSyncOrNull(
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
)
return CryptoCurrencyStatusFactory.create(
currency = cryptoCurrency,
maybeQuoteStatus = quote?.right() ?: singleQuoteStatusSupplier.getSyncOrNull(
params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId),
).right(),
maybeNetworkStatus = NetworkStatus(
network = cryptoCurrency.network,
value = NetworkStatus.MissedDerivation, // Caution!!! Do not change this status
).right(),
maybeYieldBalance = null,
).getOrNull()
).some(),
maybeQuoteStatus = quoteStatus.toOption(),
maybeYieldBalance = none(),
)
}
private fun parseTxDetails(txDetailsJson: String): TxDetails? {

View file

@ -17,7 +17,6 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.SwapTransactionRepository
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -59,7 +58,6 @@ internal object SwapDataModule {
moshi = moshi,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
currencyStatusProxyCreator = CurrencyStatusProxyCreator(),
)
}

View file

@ -54,7 +54,7 @@ abstract class BaseCurrencyStatusOperations(
private val stakingIdFactory: StakingIdFactory,
) {
protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator()
private val currencyStatusProxyCreator = CurrencyStatusProxyCreator()
abstract fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow<TokenListError, List<CryptoCurrencyStatus>>

View file

@ -171,29 +171,22 @@ class CachedCurrenciesStatusesOperations(
): Lce<TokenListError, List<CryptoCurrencyStatus>> = lce {
isLoading.set(isUpdating)
var quotesRetrievingFailed = false
val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull()
val yieldBalances = maybeYieldBalances?.bindEither()
val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) {
null
}
if (quotes == null) {
quotesRetrievingFailed = true
}
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network }
val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus)
val currencyStatus = currencyStatusProxyCreator.createCurrencyStatus(
val currencyStatus = CryptoCurrencyStatusFactory.create(
currency = currency,
quoteStatus = quote,
networkStatus = networkStatus,
yieldBalance = yieldBalance,
ignoreQuote = quotesRetrievingFailed,
maybeNetworkStatus = networkStatus.toOption(),
maybeQuoteStatus = quote.toOption(),
maybeYieldBalance = yieldBalance.toOption(),
)
currencyStatus

View file

@ -0,0 +1,266 @@
package com.tangem.domain.tokens.operations
import arrow.core.Option
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import java.math.BigDecimal
/**
* Factory to create [CryptoCurrencyStatus] from [NetworkStatus], [QuoteStatus] and [YieldBalance].
*
[REDACTED_AUTHOR]
*/
object CryptoCurrencyStatusFactory {
private val QuoteStatus?.fiatRate: BigDecimal?
get() = (this?.value as? QuoteStatus.Data)?.fiatRate
private val QuoteStatus?.priceChange: BigDecimal?
get() = (this?.value as? QuoteStatus.Data)?.priceChange
/**
* Creates [CryptoCurrencyStatus] from [NetworkStatus], [QuoteStatus] and [YieldBalance].
*
* @param maybeNetworkStatus An optional network status containing blockchain information.
* @param maybeQuoteStatus An optional quote status containing price information.
* @param maybeYieldBalance An optional yield balance containing staking information.
*/
fun create(
currency: CryptoCurrency,
maybeNetworkStatus: Option<NetworkStatus>,
maybeQuoteStatus: Option<QuoteStatus>,
maybeYieldBalance: Option<YieldBalance>,
): CryptoCurrencyStatus {
return CryptoCurrencyStatus(
currency = currency,
value = createStatus(
currency = currency,
maybeNetworkStatus = maybeNetworkStatus,
maybeYieldBalance = maybeYieldBalance,
maybeQuoteStatus = maybeQuoteStatus,
),
)
}
private fun createStatus(
currency: CryptoCurrency,
maybeNetworkStatus: Option<NetworkStatus>,
maybeQuoteStatus: Option<QuoteStatus>,
maybeYieldBalance: Option<YieldBalance>,
): CryptoCurrencyStatus.Value {
val quoteStatus = maybeQuoteStatus.getOrNull()
return when (val status = maybeNetworkStatus.getOrNull()?.value) {
is NetworkStatus.MissedDerivation -> createMissedDerivation(quoteStatus)
is NetworkStatus.Unreachable -> createUnreachable(status, quoteStatus)
is NetworkStatus.NoAccount -> createNoAccount(status, quoteStatus)
is NetworkStatus.Verified -> createStatus(currency, status, quoteStatus, maybeYieldBalance)
null -> CryptoCurrencyStatus.Loading
}
}
private fun createMissedDerivation(quoteStatus: QuoteStatus?): CryptoCurrencyStatus.MissedDerivation {
return CryptoCurrencyStatus.MissedDerivation(
priceChange = quoteStatus.priceChange,
fiatRate = quoteStatus.fiatRate,
)
}
private fun createUnreachable(
status: NetworkStatus.Unreachable,
quoteStatus: QuoteStatus?,
): CryptoCurrencyStatus.Unreachable {
return CryptoCurrencyStatus.Unreachable(
priceChange = quoteStatus.priceChange,
fiatRate = quoteStatus.fiatRate,
networkAddress = status.address,
)
}
private fun createNoAccount(
status: NetworkStatus.NoAccount,
quoteStatus: QuoteStatus?,
): CryptoCurrencyStatus.NoAccount {
return CryptoCurrencyStatus.NoAccount(
amountToCreateAccount = status.amountToCreateAccount,
fiatAmount = BigDecimal.ZERO,
priceChange = quoteStatus.priceChange,
fiatRate = quoteStatus.fiatRate,
networkAddress = status.address,
sources = CryptoCurrencyStatus.Sources(
networkSource = status.source,
quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL,
),
)
}
private fun createStatus(
currency: CryptoCurrency,
status: NetworkStatus.Verified,
quoteStatus: QuoteStatus?,
maybeYieldBalance: Option<YieldBalance>,
): CryptoCurrencyStatus.Value {
val amount = when (val amount = status.amounts[currency.id]) {
is NetworkStatus.Amount.Loaded -> amount.value
is NetworkStatus.Amount.NotFound -> {
return createNoAmount(quoteStatus = quoteStatus)
}
null -> {
return CryptoCurrencyStatus.Loading
}
}
val yieldBalance = maybeYieldBalance.getOrNull(id = currency.id, address = status.address)
if (currency is CryptoCurrency.Token && currency.isCustom) {
return createCustom(
id = currency.id,
status = status,
amount = amount,
quoteStatus = quoteStatus,
yieldBalance = yieldBalance,
)
}
// order is important for correct total balance calculation
return when (val quoteValue = quoteStatus?.value) {
is QuoteStatus.Empty -> {
createNoQuote(
id = currency.id,
status = status,
amount = amount,
quoteStatus = quoteStatus,
yieldBalance = yieldBalance,
)
}
is QuoteStatus.Data -> {
createLoaded(
id = currency.id,
status = status,
amount = amount,
quoteStatus = quoteValue,
yieldBalance = yieldBalance,
)
}
null -> CryptoCurrencyStatus.Loading
}
}
private fun createNoAmount(quoteStatus: QuoteStatus?): CryptoCurrencyStatus.NoAmount {
return CryptoCurrencyStatus.NoAmount(
priceChange = quoteStatus.priceChange,
fiatRate = quoteStatus.fiatRate,
)
}
private fun createCustom(
id: CryptoCurrency.ID,
status: NetworkStatus.Verified,
amount: BigDecimal,
quoteStatus: QuoteStatus?,
yieldBalance: YieldBalance.Data?,
): CryptoCurrencyStatus.Custom {
return CryptoCurrencyStatus.Custom(
amount = amount,
fiatAmount = quoteStatus.fiatRate?.let { calculateFiatAmount(amount, it) },
fiatRate = quoteStatus.fiatRate,
priceChange = quoteStatus.priceChange,
hasCurrentNetworkTransactions = status.hasCurrentNetworkTransactions(),
pendingTransactions = status.getCurrentTransactions(id),
networkAddress = status.address,
yieldBalance = yieldBalance,
yieldSupplyStatus = status.getYieldSupplyStatus(id),
sources = CryptoCurrencyStatus.Sources(
networkSource = status.source,
yieldBalanceSource = yieldBalance?.source ?: StatusSource.ACTUAL,
quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL,
),
)
}
private fun createNoQuote(
id: CryptoCurrency.ID,
status: NetworkStatus.Verified,
amount: BigDecimal,
quoteStatus: QuoteStatus?,
yieldBalance: YieldBalance.Data?,
): CryptoCurrencyStatus.NoQuote {
return CryptoCurrencyStatus.NoQuote(
amount = amount,
hasCurrentNetworkTransactions = status.hasCurrentNetworkTransactions(),
pendingTransactions = status.getCurrentTransactions(id),
networkAddress = status.address,
yieldBalance = yieldBalance,
yieldSupplyStatus = status.getYieldSupplyStatus(id),
sources = CryptoCurrencyStatus.Sources(
networkSource = status.source,
yieldBalanceSource = yieldBalance?.source ?: StatusSource.ACTUAL,
quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL,
),
)
}
private fun createLoaded(
id: CryptoCurrency.ID,
status: NetworkStatus.Verified,
amount: BigDecimal,
quoteStatus: QuoteStatus.Data,
yieldBalance: YieldBalance.Data?,
): CryptoCurrencyStatus.Loaded {
return CryptoCurrencyStatus.Loaded(
amount = amount,
fiatAmount = calculateFiatAmount(amount, quoteStatus.fiatRate),
fiatRate = quoteStatus.fiatRate,
priceChange = quoteStatus.priceChange,
hasCurrentNetworkTransactions = status.hasCurrentNetworkTransactions(),
pendingTransactions = status.getCurrentTransactions(id),
networkAddress = status.address,
yieldBalance = yieldBalance,
yieldSupplyStatus = status.getYieldSupplyStatus(id),
sources = CryptoCurrencyStatus.Sources(
networkSource = status.source,
yieldBalanceSource = yieldBalance?.source ?: StatusSource.ACTUAL,
quoteSource = quoteStatus.source,
),
)
}
private fun NetworkStatus.Verified.hasCurrentNetworkTransactions() = pendingTransactions.isNotEmpty()
private fun NetworkStatus.Verified.getCurrentTransactions(id: CryptoCurrency.ID): Set<TxInfo> {
return pendingTransactions.getOrElse(key = id, defaultValue = ::emptySet)
}
private fun NetworkStatus.Verified.getYieldSupplyStatus(id: CryptoCurrency.ID): YieldSupplyStatus? {
return yieldSupplyStatuses[id]
}
private fun Option<YieldBalance>.getOrNull(id: CryptoCurrency.ID, address: NetworkAddress): YieldBalance.Data? {
val yieldBalance = this.getOrNull() as? YieldBalance.Data ?: return null
val isCurrentAddressStaking = yieldBalance.stakingId.address == address.defaultAddress.value
val filteredTokenBalances = yieldBalance.balance.items.filter {
it.token.coinGeckoId == id.rawCurrencyId?.value
}
return if (isCurrentAddressStaking && filteredTokenBalances.isNotEmpty()) {
yieldBalance.copy(
balance = yieldBalance.balance.copy(items = filteredTokenBalances),
)
} else {
null
}
}
private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal {
return amount * fiatRate
}
}

View file

@ -1,161 +0,0 @@
package com.tangem.domain.tokens.operations
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.staking.YieldBalance
import java.math.BigDecimal
internal class CurrencyStatusOperations(
private val currency: CryptoCurrency,
private val quoteStatus: QuoteStatus?,
private val networkStatus: NetworkStatus?,
private val yieldBalance: YieldBalance?,
private val ignoreQuote: Boolean,
) {
private val QuoteStatus?.fiatRate: BigDecimal?
get() = (this?.value as? QuoteStatus.Data)?.fiatRate
private val QuoteStatus?.priceChange: BigDecimal?
get() = (this?.value as? QuoteStatus.Data)?.priceChange
fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus())
private fun createStatus(): CryptoCurrencyStatus.Value {
return when (val status = networkStatus?.value) {
null -> CryptoCurrencyStatus.Loading
is NetworkStatus.MissedDerivation -> createMissedDerivationStatus()
is NetworkStatus.Unreachable -> createUnreachableStatus(status)
is NetworkStatus.NoAccount -> createNoAccountStatus(status)
is NetworkStatus.Verified -> createStatus(status, yieldBalance)
}
}
private fun createMissedDerivationStatus(): CryptoCurrencyStatus.MissedDerivation =
CryptoCurrencyStatus.MissedDerivation(priceChange = quoteStatus?.priceChange, fiatRate = quoteStatus?.fiatRate)
private fun createUnreachableStatus(status: NetworkStatus.Unreachable): CryptoCurrencyStatus.Unreachable {
return CryptoCurrencyStatus.Unreachable(
priceChange = quoteStatus?.priceChange,
fiatRate = quoteStatus?.fiatRate,
networkAddress = status.address,
)
}
private fun createNoAccountStatus(status: NetworkStatus.NoAccount): CryptoCurrencyStatus.NoAccount {
return CryptoCurrencyStatus.NoAccount(
amountToCreateAccount = status.amountToCreateAccount,
fiatAmount = if (quoteStatus == null) null else BigDecimal.ZERO,
priceChange = quoteStatus?.priceChange,
fiatRate = quoteStatus?.fiatRate,
networkAddress = status.address,
sources = CryptoCurrencyStatus.Sources(
networkSource = status.source,
quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL,
),
)
}
@Suppress("CyclomaticComplexMethod", "LongMethod")
private fun createStatus(
networkStatusValue: NetworkStatus.Verified,
yieldBalance: YieldBalance?,
): CryptoCurrencyStatus.Value {
val amount = when (val amount = networkStatusValue.amounts[currency.id]) {
null -> {
return CryptoCurrencyStatus.Loading
}
is NetworkStatus.Amount.NotFound -> {
return CryptoCurrencyStatus.NoAmount(
priceChange = quoteStatus?.priceChange,
fiatRate = quoteStatus?.fiatRate,
)
}
is NetworkStatus.Amount.Loaded -> amount.value
}
val hasCurrentNetworkTransactions = networkStatusValue.pendingTransactions.isNotEmpty()
val currentTransactions = networkStatusValue.pendingTransactions.getOrElse(currency.id, ::emptySet)
val yieldBalanceData = yieldBalance as? YieldBalance.Data
val isCurrentAddressStaking =
yieldBalanceData?.stakingId?.address == networkStatusValue.address.defaultAddress.value
val filteredTokenBalances = yieldBalanceData?.balance?.items?.filter {
it.token.coinGeckoId == currency.id.rawCurrencyId?.value
}
val currentYieldBalance = if (isCurrentAddressStaking && filteredTokenBalances?.isNotEmpty() == true) {
yieldBalanceData.copy(
balance = yieldBalanceData.balance.copy(
items = filteredTokenBalances,
),
)
} else {
null
}
val yieldSupplyStatus = networkStatusValue.yieldSupplyStatuses[currency.id]
val quoteValue = quoteStatus?.value
// order is important for correct total balance calculation
return when {
currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom(
amount = amount,
fiatAmount = calculateFiatAmountOrNull(amount, quoteStatus?.fiatRate),
fiatRate = quoteStatus?.fiatRate,
priceChange = quoteStatus?.priceChange,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
networkAddress = networkStatusValue.address,
yieldBalance = currentYieldBalance,
yieldSupplyStatus = yieldSupplyStatus,
sources = CryptoCurrencyStatus.Sources(
networkSource = networkStatusValue.source,
quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL,
yieldBalanceSource = currentYieldBalance?.source ?: StatusSource.ACTUAL,
),
)
quoteValue is QuoteStatus.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote(
amount = amount,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
networkAddress = networkStatusValue.address,
yieldBalance = currentYieldBalance,
yieldSupplyStatus = yieldSupplyStatus,
sources = CryptoCurrencyStatus.Sources(
networkSource = networkStatusValue.source,
quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL,
yieldBalanceSource = currentYieldBalance?.source ?: StatusSource.ACTUAL,
),
)
quoteValue is QuoteStatus.Data -> CryptoCurrencyStatus.Loaded(
amount = amount,
fiatAmount = calculateFiatAmount(amount, quoteValue.fiatRate),
fiatRate = quoteValue.fiatRate,
priceChange = quoteValue.priceChange,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
networkAddress = networkStatusValue.address,
yieldBalance = currentYieldBalance,
yieldSupplyStatus = yieldSupplyStatus,
sources = CryptoCurrencyStatus.Sources(
networkSource = networkStatusValue.source,
quoteSource = quoteValue.source,
yieldBalanceSource = currentYieldBalance?.source ?: StatusSource.ACTUAL,
),
)
else -> CryptoCurrencyStatus.Loading
}
}
private fun calculateFiatAmountOrNull(amount: BigDecimal, fiatRate: BigDecimal?): BigDecimal? {
if (fiatRate == null) return null
return calculateFiatAmount(amount, fiatRate)
}
private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal {
return amount * fiatRate
}
}

View file

@ -3,7 +3,9 @@ package com.tangem.domain.tokens.utils
import arrow.core.Either
import arrow.core.NonEmptyList
import arrow.core.raise.either
import arrow.core.raise.recover
import arrow.core.toNonEmptySetOrNull
import arrow.core.toOption
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.NetworkStatus
@ -12,11 +14,11 @@ import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error
import com.tangem.domain.tokens.operations.CurrencyStatusOperations
/**
* Proxy creator of [CryptoCurrencyStatus]. Used [CurrencyStatusOperations] to create statuses.
* Proxy creator of [CryptoCurrencyStatus]. Used [CryptoCurrencyStatusFactory] to create statuses.
*
[REDACTED_AUTHOR]
*/
@ -28,20 +30,17 @@ class CurrencyStatusProxyCreator {
maybeNetworkStatus: Either<Error, NetworkStatus?>,
maybeYieldBalance: Either<Error, YieldBalance>?,
): Either<Error, CryptoCurrencyStatus> = either {
var quoteRetrievingFailed = false
val networkStatus = maybeNetworkStatus.bind()
val quote = arrow.core.raise.recover({ maybeQuoteStatus.bind() }) {
quoteRetrievingFailed = true
null
}
val quote = recover(
block = { maybeQuoteStatus.bind() },
recover = { null },
)
val yieldBalance = maybeYieldBalance?.getOrNull()
createCurrencyStatus(
currency = currency,
quoteStatus = quote,
networkStatus = networkStatus,
ignoreQuote = quoteRetrievingFailed,
yieldBalance = yieldBalance,
)
}
@ -52,21 +51,8 @@ class CurrencyStatusProxyCreator {
maybeNetworkStatuses: Either<Error, Set<NetworkStatus>>,
maybeYieldBalances: Either<Error, List<YieldBalance>>,
): Either<Error, List<CryptoCurrencyStatus>> = either {
var quotesRetrievingFailed = false
val networksStatuses = maybeNetworkStatuses.bind().toNonEmptySetOrNull()
val quoteStatuses: Set<QuoteStatus>? = maybeQuotes?.fold(
ifLeft = {
quotesRetrievingFailed = true
null
},
ifRight = {
it.ifEmpty {
quotesRetrievingFailed = true
null
}
},
)
val quoteStatuses: Set<QuoteStatus>? = maybeQuotes?.getOrNull()?.ifEmpty { null }
val yieldBalances = maybeYieldBalances.getOrNull()
@ -90,27 +76,22 @@ class CurrencyStatusProxyCreator {
currency = currency,
quoteStatus = quote,
networkStatus = networkStatus,
ignoreQuote = quotesRetrievingFailed,
yieldBalance = yieldBalance,
)
}
}
fun createCurrencyStatus(
private fun createCurrencyStatus(
currency: CryptoCurrency,
quoteStatus: QuoteStatus?,
networkStatus: NetworkStatus?,
ignoreQuote: Boolean,
yieldBalance: YieldBalance?,
): CryptoCurrencyStatus {
val currencyStatusOperations = CurrencyStatusOperations(
return CryptoCurrencyStatusFactory.create(
currency = currency,
quoteStatus = quoteStatus,
networkStatus = networkStatus,
ignoreQuote = ignoreQuote,
yieldBalance = yieldBalance,
maybeNetworkStatus = networkStatus.toOption(),
maybeQuoteStatus = quoteStatus.toOption(),
maybeYieldBalance = yieldBalance.toOption(),
)
return currencyStatusOperations.createTokenStatus()
}
}

View file

@ -0,0 +1,686 @@
package com.tangem.domain.tokens.operations
import arrow.core.none
import arrow.core.some
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.domain.models.network.NetworkStatus.Amount
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.YieldBalanceItem
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.domain.staking.model.StakingIntegrationID
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CryptoCurrencyStatusFactoryTest {
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
private val currency = cryptoCurrencyFactory.cardano
private val networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x123",
type = NetworkAddress.Address.Type.Primary,
),
)
private val fullQuote = QuoteStatus.Data(
fiatRate = 1800.0.toBigDecimal(),
priceChange = (-2.5).toBigDecimal(),
source = StatusSource.ACTUAL,
)
private val emptyQuoteStatus = QuoteStatus.Empty.toStatus()
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class MissedDerivation {
private val networkStatus = NetworkStatus.MissedDerivation.toStatus()
private val maybeYieldBalance = none<YieldBalance>() // not relevant for this test
@Test
fun `network is MissedDerivation and QuoteStatus is Data`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = fullQuote.toStatus().some(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.MissedDerivation(
fiatRate = fullQuote.fiatRate,
priceChange = fullQuote.priceChange,
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is MissedDerivation and QuoteStatus is Empty`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = emptyQuoteStatus.some(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.MissedDerivation(fiatRate = null, priceChange = null).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is MissedDerivation and QuoteStatus is null`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = none(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.MissedDerivation(fiatRate = null, priceChange = null).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Unreachable {
private val networkStatus = NetworkStatus.Unreachable(address = networkAddress).toStatus()
private val maybeYieldBalance = none<YieldBalance>() // not relevant for this test
@Test
fun `network is Unreachable and QuoteStatus is Data`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = fullQuote.toStatus().some(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.Unreachable(
fiatRate = fullQuote.fiatRate,
priceChange = fullQuote.priceChange,
networkAddress = networkAddress,
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is Unreachable and QuoteStatus is Empty`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = emptyQuoteStatus.some(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.Unreachable(
fiatRate = null,
priceChange = null,
networkAddress = networkAddress,
)
.toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is Unreachable and QuoteStatus is null`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = none(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.Unreachable(
fiatRate = null,
priceChange = null,
networkAddress = networkAddress,
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class NoAccount {
private val networkStatus = NetworkStatus.NoAccount(
address = networkAddress,
amountToCreateAccount = BigDecimal.ONE,
errorMessage = "error message",
source = StatusSource.ACTUAL,
).toStatus()
private val maybeYieldBalance = none<YieldBalance>() // not relevant for this test
@Test
fun `network is NoAccount and QuoteStatus is Data`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = fullQuote.toStatus().some(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.NoAccount(
amountToCreateAccount = BigDecimal.ONE,
fiatAmount = BigDecimal.ZERO,
priceChange = fullQuote.priceChange,
fiatRate = fullQuote.fiatRate,
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is NoAccount and QuoteStatus is Empty`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = emptyQuoteStatus.some(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.NoAccount(
amountToCreateAccount = BigDecimal.ONE,
fiatAmount = BigDecimal.ZERO,
priceChange = null,
fiatRate = null,
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is NoAccount and QuoteStatus is null`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = none(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.NoAccount(
amountToCreateAccount = BigDecimal.ONE,
fiatAmount = BigDecimal.ZERO,
priceChange = null,
fiatRate = null,
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class NoAmount {
private val networkStatus = createVerified(amounts = mapOf(currency.id to Amount.NotFound)).toStatus()
private val maybeYieldBalance = none<YieldBalance>() // not relevant for this test
@Test
fun `network is Verified with Amount is NotFound and QuoteStatus is Data`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = fullQuote.toStatus().some(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.NoAmount(
priceChange = fullQuote.priceChange,
fiatRate = fullQuote.fiatRate,
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is Verified with Amount is NotFound and QuoteStatus is Empty`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = emptyQuoteStatus.some(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.NoAmount(priceChange = null, fiatRate = null).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is Verified with Amount is NotFound and QuoteStatus is null`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = none(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.NoAmount(priceChange = null, fiatRate = null).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Loading {
private val maybeYieldBalance = none<YieldBalance>() // not relevant for this test
@Test
fun `network is null`() {
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = none(),
maybeQuoteStatus = none(), // not relevant for this test,
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.Loading.toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is Verified with Amount is null`() {
// Arrange
val networkStatus = createVerified().toStatus()
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = none(), // not relevant for this test,
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.Loading.toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is Verified and QuoteStatus is null`() {
// Arrange
val networkStatus = createVerified(
amounts = mapOf(currency.id to Amount.Loaded(value = BigDecimal.ZERO)),
).toStatus()
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = none(),
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.Loading.toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Custom {
private val currency = cryptoCurrencyFactory.createToken(blockchain = Blockchain.Cardano).copy(isCustom = true)
private val networkStatus = createVerified(
amounts = mapOf(currency.id to Amount.Loaded(value = BigDecimal.TEN)),
).toStatus()
@Test
fun `network is Verified, QuoteStatus is null, YieldBalance is null`() {
// Arrange
val maybeQuoteStatus = none<QuoteStatus>()
val maybeYieldBalance = none<YieldBalance>()
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = maybeQuoteStatus,
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Custom(
amount = BigDecimal.TEN,
fiatAmount = null,
fiatRate = null,
priceChange = null,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
),
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is Verified, QuoteStatus is Data, YieldBalance is Data`() {
// Arrange
val yieldBalance = YieldBalance.Data(
stakingId = StakingID(
integrationId = StakingIntegrationID.Coin.Cardano.value,
address = networkAddress.defaultAddress.value,
),
source = StatusSource.ACTUAL,
balance = YieldBalanceItem(
items = listOf(
mockk<BalanceItem> {
every { this@mockk.token.coinGeckoId } returns currency.id.rawCurrencyId?.value
},
mockk<BalanceItem> {
every { this@mockk.token.coinGeckoId } returns "unknown"
},
),
integrationId = StakingIntegrationID.Coin.Cardano.value,
),
)
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = fullQuote.toStatus().some(),
maybeYieldBalance = yieldBalance.some(),
)
// Assert
val expected = CryptoCurrencyStatus(
currency = currency,
value = CryptoCurrencyStatus.Custom(
amount = BigDecimal.TEN,
fiatAmount = BigDecimal.TEN * fullQuote.fiatRate,
fiatRate = fullQuote.fiatRate,
priceChange = fullQuote.priceChange,
yieldBalance = yieldBalance.copy(
balance = yieldBalance.balance.copy(
items = yieldBalance.balance.items.subList(0, 1),
),
),
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
),
)
Truth.assertThat(actual).isEqualTo(expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class NoQuote {
private val pendingTransactions = mapOf(currency.id to setOf(mockk<TxInfo>()))
private val yieldSupplyStatuses = mapOf(currency.id to mockk<YieldSupplyStatus>())
private val networkStatus = createVerified(
amounts = mapOf(currency.id to Amount.Loaded(value = BigDecimal.TEN)),
pendingTransactions = pendingTransactions,
yieldSupplyStatuses = yieldSupplyStatuses,
).toStatus()
private val maybeQuoteStatus = emptyQuoteStatus.some()
@Test
fun `network is Verified and YieldBalance is null`() {
// Arrange
val maybeYieldBalance = none<YieldBalance>()
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = maybeQuoteStatus,
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.NoQuote(
amount = BigDecimal.TEN,
yieldBalance = null,
yieldSupplyStatus = yieldSupplyStatuses[currency.id]!!,
hasCurrentNetworkTransactions = true,
pendingTransactions = pendingTransactions[currency.id]!!,
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is Verified and YieldBalance is Data`() {
// Arrange
val yieldBalance = YieldBalance.Data(
stakingId = StakingID(
integrationId = StakingIntegrationID.Coin.Cardano.value,
address = networkAddress.defaultAddress.value,
),
source = StatusSource.ACTUAL,
balance = YieldBalanceItem(
items = listOf(
mockk<BalanceItem> {
every { this@mockk.token.coinGeckoId } returns currency.id.rawCurrencyId?.value
},
mockk<BalanceItem> {
every { this@mockk.token.coinGeckoId } returns "unknown"
},
),
integrationId = StakingIntegrationID.Coin.Cardano.value,
),
)
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = maybeQuoteStatus,
maybeYieldBalance = yieldBalance.some(),
)
// Assert
val expected = CryptoCurrencyStatus.NoQuote(
amount = BigDecimal.TEN,
yieldBalance = yieldBalance.copy(
balance = yieldBalance.balance.copy(
items = yieldBalance.balance.items.subList(0, 1),
),
),
yieldSupplyStatus = yieldSupplyStatuses[currency.id]!!,
hasCurrentNetworkTransactions = true,
pendingTransactions = pendingTransactions[currency.id]!!,
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Loaded {
private val networkStatus = createVerified(
amounts = mapOf(currency.id to Amount.Loaded(value = BigDecimal.TEN)),
).toStatus()
private val maybeQuoteStatus = fullQuote.toStatus().some()
@Test
fun `network is Verified and YieldBalance is null`() {
// Arrange
val maybeYieldBalance = none<YieldBalance>()
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = maybeQuoteStatus,
maybeYieldBalance = maybeYieldBalance,
)
// Assert
val expected = CryptoCurrencyStatus.Loaded(
amount = BigDecimal.TEN,
fiatAmount = BigDecimal.TEN * fullQuote.fiatRate,
fiatRate = fullQuote.fiatRate,
priceChange = fullQuote.priceChange,
yieldBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `network is Verified and YieldBalance is Data`() {
// Arrange
val yieldBalance = YieldBalance.Data(
stakingId = StakingID(
integrationId = StakingIntegrationID.Coin.Cardano.value,
address = networkAddress.defaultAddress.value,
),
source = StatusSource.ACTUAL,
balance = YieldBalanceItem(
items = listOf(
mockk<BalanceItem> {
every { this@mockk.token.coinGeckoId } returns currency.id.rawCurrencyId?.value
},
mockk<BalanceItem> {
every { this@mockk.token.coinGeckoId } returns "unknown"
},
),
integrationId = StakingIntegrationID.Coin.Cardano.value,
),
)
// Act
val actual = CryptoCurrencyStatusFactory.create(
currency = currency,
maybeNetworkStatus = networkStatus.some(),
maybeQuoteStatus = fullQuote.toStatus().some(),
maybeYieldBalance = yieldBalance.some(),
)
// Assert
val expected = CryptoCurrencyStatus.Loaded(
amount = BigDecimal.TEN,
fiatAmount = BigDecimal.TEN * fullQuote.fiatRate,
fiatRate = fullQuote.fiatRate,
priceChange = fullQuote.priceChange,
yieldBalance = yieldBalance.copy(
balance = yieldBalance.balance.copy(
items = yieldBalance.balance.items.subList(0, 1),
),
),
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
).toStatus()
Truth.assertThat(actual).isEqualTo(expected)
}
}
private fun createVerified(
amounts: Map<CryptoCurrency.ID, Amount> = emptyMap(),
pendingTransactions: Map<CryptoCurrency.ID, Set<TxInfo>> = emptyMap(),
yieldSupplyStatuses: Map<CryptoCurrency.ID, YieldSupplyStatus?> = emptyMap(),
): NetworkStatus.Verified {
return NetworkStatus.Verified(
address = networkAddress,
amounts = amounts,
pendingTransactions = pendingTransactions,
yieldSupplyStatuses = yieldSupplyStatuses,
source = StatusSource.ACTUAL,
)
}
private fun NetworkStatus.Value.toStatus(): NetworkStatus {
return NetworkStatus(network = currency.network, value = this)
}
private fun CryptoCurrencyStatus.Value.toStatus(): CryptoCurrencyStatus {
return CryptoCurrencyStatus(currency = currency, value = this)
}
private fun QuoteStatus.Value.toStatus(): QuoteStatus {
return when (this) {
is QuoteStatus.Data -> QuoteStatus(rawCurrencyId = currency.id.rawCurrencyId!!, value = this)
is QuoteStatus.Empty -> QuoteStatus(rawCurrencyId = currency.id.rawCurrencyId!!, value = this)
}
}
}