Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-19 12:03:10 +04:00
parent 0bf2decb83
commit 36e7f2d07f
23 changed files with 212 additions and 70 deletions

View file

@ -30,6 +30,7 @@ dependencies {
/** Project - Domain */ /** Project - Domain */
implementation(projects.domain.appCurrency.models) implementation(projects.domain.appCurrency.models)
implementation(projects.domain.models)
implementation(projects.domain.legacy) implementation(projects.domain.legacy)
implementation(projects.domain.staking.models) implementation(projects.domain.staking.models)
implementation(projects.domain.tokens.models) implementation(projects.domain.tokens.models)

View file

@ -5,6 +5,7 @@ import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.network.converter.NetworkStatusConverter import com.tangem.datasource.local.network.converter.NetworkStatusConverter
import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter
import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
@ -98,21 +99,42 @@ internal class DefaultNetworksStatusesStore(
cachedStatuses: Set<NetworkStatus>, cachedStatuses: Set<NetworkStatus>,
runtimeStatuses: Set<NetworkStatus>, runtimeStatuses: Set<NetworkStatus>,
): Set<NetworkStatus> { ): Set<NetworkStatus> {
val runtimeMap = runtimeStatuses.associateBy { it.network } return runtimeStatuses
val mergedCached = cachedStatuses.filter { it.network !in runtimeMap.keys } .map { runtime ->
runtime.takeIf { it.value !is NetworkStatus.Unreachable }
?: getCachedStatusIfPossible(cachedStatuses = cachedStatuses, runtime = runtime)
}
.toSet()
}
return mergedCached.toSet() + runtimeStatuses private fun getCachedStatusIfPossible(cachedStatuses: Set<NetworkStatus>, runtime: NetworkStatus): NetworkStatus {
val cached = cachedStatuses.firstOrNull { it.network == runtime.network } ?: return runtime
val updatedCachedStatus = when (val status = cached.value) {
is NetworkStatus.NoAccount -> status.copy(source = StatusSource.ONLY_CACHE)
is NetworkStatus.Verified -> status.copy(source = StatusSource.ONLY_CACHE)
is NetworkStatus.Refreshing,
is NetworkStatus.Unreachable,
is NetworkStatus.MissedDerivation,
-> null
}
return if (updatedCachedStatus != null) {
cached.copy(value = updatedCachedStatus)
} else {
runtime
}
} }
private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, networkStatus: NetworkStatus) { private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, networkStatus: NetworkStatus) {
val network = networkStatus.network val network = networkStatus.network
// Converter will return null if the network status is not supported
val newStatus = NetworkStatusDataModelConverter.convert(value = networkStatus) ?: return
persistenceDataStore.updateData { storedStatuses -> persistenceDataStore.updateData { storedStatuses ->
val userWalletStatuses = storedStatuses[userWalletId.stringValue] ?: emptySet() val userWalletStatuses = storedStatuses[userWalletId.stringValue].orEmpty()
val updatedStatuses = userWalletStatuses.addOrReplace( val updatedStatuses = userWalletStatuses.addOrReplace(item = newStatus) { it.networkId == network.id }
item = NetworkStatusDataModelConverter.convert(value = networkStatus),
predicate = { it.networkId == network.id },
)
storedStatuses.toMutableMap().apply { storedStatuses.toMutableMap().apply {
this[userWalletId.stringValue] = updatedStatuses this[userWalletId.stringValue] = updatedStatuses

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.local.network.converter package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
@ -28,7 +29,7 @@ internal class NetworkStatusConverter(
address = address, address = address,
amounts = NetworkAmountsConverter.convert(value = value.amounts), amounts = NetworkAmountsConverter.convert(value = value.amounts),
pendingTransactions = mapOf(), pendingTransactions = mapOf(),
isCached = isCached, source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
) )
} }
is NetworkStatusDM.NoAccount -> { is NetworkStatusDM.NoAccount -> {
@ -36,7 +37,7 @@ internal class NetworkStatusConverter(
address = address, address = address,
amountToCreateAccount = value.amountToCreateAccount, amountToCreateAccount = value.amountToCreateAccount,
errorMessage = value.errorMessage, errorMessage = value.errorMessage,
isCached = isCached, source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
) )
} }
} }

View file

@ -9,9 +9,9 @@ import com.tangem.utils.converter.Converter
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, NetworkStatusDM> { internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, NetworkStatusDM?> {
override fun convert(value: NetworkStatus): NetworkStatusDM { override fun convert(value: NetworkStatus): NetworkStatusDM? {
return when (val status = value.value) { return when (val status = value.value) {
is NetworkStatus.Verified -> { is NetworkStatus.Verified -> {
NetworkStatusDM.Verified( NetworkStatusDM.Verified(
@ -32,7 +32,7 @@ internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, Netwo
errorMessage = status.errorMessage, errorMessage = status.errorMessage,
) )
} }
else -> error("Unsupported network status: $value") else -> null
} }
} }
} }

View file

@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.quote.converter.QuoteConverter import com.tangem.datasource.local.quote.converter.QuoteConverter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Quote
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -32,7 +33,12 @@ internal class DefaultQuotesStore(
runtimeStore.get() runtimeStore.get()
.onEach { .onEach {
val mergedQuotes = mergeQuotes(cachedQuotes = cachedQuotes, runtimeQuotes = it) val mergedQuotes = mergeQuotes(
currenciesIds = currenciesIds,
cachedQuotes = cachedQuotes,
runtimeQuotes = it,
)
send(element = mergedQuotes) send(element = mergedQuotes)
} }
.launchIn(scope = this) .launchIn(scope = this)
@ -51,6 +57,14 @@ internal class DefaultQuotesStore(
} }
} }
override suspend fun storeEmptyQuotes(currenciesIds: Set<CryptoCurrency.RawID>) {
runtimeStore.update(default = emptySet()) { saved ->
val new = currenciesIds.map { Quote.Empty(it) }
(saved + new).distinctBy { it.rawCurrencyId }.toSet()
}
}
private suspend fun getCachedQuotes(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote.Value> { private suspend fun getCachedQuotes(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote.Value> {
val ids = currenciesIds.map(CryptoCurrency.RawID::value).toSet() val ids = currenciesIds.map(CryptoCurrency.RawID::value).toSet()
val cachedQuotes = persistenceStore.data.firstOrNull().orEmpty().filterKeys { it in ids } val cachedQuotes = persistenceStore.data.firstOrNull().orEmpty().filterKeys { it in ids }
@ -58,15 +72,27 @@ internal class DefaultQuotesStore(
return QuoteConverter(isCached = true).convertSet(input = cachedQuotes.entries) return QuoteConverter(isCached = true).convertSet(input = cachedQuotes.entries)
} }
private fun mergeQuotes(cachedQuotes: Set<Quote.Value>, runtimeQuotes: Set<Quote>): Set<Quote> { private fun mergeQuotes(
return runtimeQuotes.map { runtimeQuote -> currenciesIds: Set<CryptoCurrency.RawID>,
if (runtimeQuote is Quote.Empty) { cachedQuotes: Set<Quote.Value>,
cachedQuotes.firstOrNull { runtimeQuote.rawCurrencyId == it.rawCurrencyId } ?: runtimeQuote runtimeQuotes: Set<Quote>,
} else { ): Set<Quote> {
runtimeQuote return currenciesIds
.mapTo(hashSetOf()) { currencyId ->
val runtimeQuote = runtimeQuotes.firstOrNull { it.rawCurrencyId == currencyId }
if (runtimeQuote == null || runtimeQuote is Quote.Empty) {
getCachedQuoteIfPossible(cachedStatuses = cachedQuotes, currencyId = currencyId)
} else {
runtimeQuote
}
} }
} }
.toSet()
private fun getCachedQuoteIfPossible(cachedStatuses: Set<Quote.Value>, currencyId: CryptoCurrency.RawID): Quote {
return cachedStatuses.firstOrNull { it.rawCurrencyId == currencyId }
?.copy(source = StatusSource.ONLY_CACHE)
?: Quote.Empty(currencyId)
} }
private suspend fun storeInRuntimeStore(response: QuotesResponse) { private suspend fun storeInRuntimeStore(response: QuotesResponse) {

View file

@ -16,4 +16,7 @@ interface QuotesStore {
/** Store [response] from remote */ /** Store [response] from remote */
suspend fun store(response: QuotesResponse) suspend fun store(response: QuotesResponse)
/** Store [Quote.Empty] for [currenciesIds] */
suspend fun storeEmptyQuotes(currenciesIds: Set<CryptoCurrency.RawID>)
} }

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.local.quote.converter package com.tangem.datasource.local.quote.converter
import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Quote
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
@ -23,7 +24,7 @@ internal class QuoteConverter(private val isCached: Boolean) :
rawCurrencyId = CryptoCurrency.RawID(currencyId), rawCurrencyId = CryptoCurrency.RawID(currencyId),
fiatRate = quote.price.orZero(), fiatRate = quote.price.orZero(),
priceChange = quote.priceChange24h.orZero().movePointLeft(2), priceChange = quote.priceChange24h.orZero().movePointLeft(2),
isCached = isCached, source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
) )
} }
} }

View file

@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.token.converter.YieldBalanceConverter import com.tangem.datasource.local.token.converter.YieldBalanceConverter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace import com.tangem.utils.extensions.addOrReplace
@ -146,16 +147,25 @@ internal class DefaultStakingBalanceStore(
): Set<YieldBalance> { ): Set<YieldBalance> {
return runtimeBalances return runtimeBalances
.map { runtime -> .map { runtime ->
if (runtime is YieldBalance.Error) { runtime.takeIf { runtime !is YieldBalance.Error }
cachedBalances.getBalance(address = runtime.address, integrationId = runtime.integrationId) ?: getCachedBalanceIfPossible(cachedBalances, runtime)
?: runtime
} else {
runtime
}
} }
.toSet() .toSet()
} }
private fun getCachedBalanceIfPossible(cachedBalances: Set<YieldBalance>, runtime: YieldBalance): YieldBalance {
val cached = cachedBalances.getBalance(address = runtime.address, integrationId = runtime.integrationId)
?: return runtime
val updatedCached = when (cached) {
is YieldBalance.Data -> cached.copy(source = StatusSource.ONLY_CACHE)
is YieldBalance.Empty -> cached.copy(source = StatusSource.ONLY_CACHE)
is YieldBalance.Error -> null
}
return updatedCached ?: runtime
}
private fun Set<YieldBalance>.getBalance(address: String?, integrationId: String?): YieldBalance? { private fun Set<YieldBalance>.getBalance(address: String?, integrationId: String?): YieldBalance? {
return firstOrNull { yieldBalance -> return firstOrNull { yieldBalance ->
val data = yieldBalance as? YieldBalance.Data val data = yieldBalance as? YieldBalance.Data

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.local.token.converter package com.tangem.datasource.local.token.converter
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceItem import com.tangem.domain.staking.model.stakekit.YieldBalanceItem
@ -13,7 +14,7 @@ internal class YieldBalanceConverter(private val isCached: Boolean) : Converter<
YieldBalance.Empty( YieldBalance.Empty(
integrationId = value.integrationId, integrationId = value.integrationId,
address = value.addresses.address, address = value.addresses.address,
isCached = isCached, source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
) )
} else { } else {
YieldBalance.Data( YieldBalance.Data(
@ -39,7 +40,7 @@ internal class YieldBalanceConverter(private val isCached: Boolean) : Converter<
.sortedWith(compareBy({ it.type }, { it.amount })), .sortedWith(compareBy({ it.type }, { it.amount })),
integrationId = value.integrationId, integrationId = value.integrationId,
), ),
isCached = isCached, source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
) )
} }
} }

View file

@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetwork import com.tangem.data.common.currency.getNetwork
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.onramp.model.HotCryptoCurrency import com.tangem.domain.onramp.model.HotCryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
@ -94,7 +95,7 @@ internal class HotCryptoCurrencyConverter(
rawCurrencyId = rawCurrencyId, rawCurrencyId = rawCurrencyId,
fiatRate = fiatRate, fiatRate = fiatRate,
priceChange = priceChange.movePointLeft(2), priceChange = priceChange.movePointLeft(2),
isCached = false, // It doesn't matter source = StatusSource.ACTUAL, // It doesn't matter
) )
} else { } else {
Quote.Empty(rawCurrencyId) Quote.Empty(rawCurrencyId)

View file

@ -122,6 +122,7 @@ internal class DefaultQuotesRepository(
}, },
onError = { error -> onError = { error ->
cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) }) cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) })
quotesStore.storeEmptyQuotes(currenciesIds = rawCurrenciesIds)
throw error throw error
}, },

View file

@ -1,5 +1,6 @@
package com.tangem.data.tokens.utils package com.tangem.data.tokens.utils
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.*
import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.Address import com.tangem.domain.walletmanager.model.Address
@ -26,7 +27,7 @@ internal class NetworkStatusFactory {
address = getNetworkAddress(result.selectedAddress, result.addresses), address = getNetworkAddress(result.selectedAddress, result.addresses),
amountToCreateAccount = result.amountToCreateAccount, amountToCreateAccount = result.amountToCreateAccount,
errorMessage = result.errorMessage, errorMessage = result.errorMessage,
isCached = false, source = StatusSource.ACTUAL,
) )
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified( is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
address = getNetworkAddress(result.selectedAddress, result.addresses), address = getNetworkAddress(result.selectedAddress, result.addresses),
@ -35,7 +36,7 @@ internal class NetworkStatusFactory {
transactions = result.currentTransactions, transactions = result.currentTransactions,
currencies = currencies, currencies = currencies,
), ),
isCached = false, source = StatusSource.ACTUAL,
) )
}, },
) )

View file

@ -0,0 +1,24 @@
package com.tangem.domain.models
/**
* Source of the status of any loaded data
*
[REDACTED_AUTHOR]
*/
enum class StatusSource {
/**
* Status is loaded from the cache.
* In most cases, it's a temporary value, then the source should become either [ACTUAL] or [ONLY_CACHE]
*/
CACHE,
/**
* Status is updated by a data source that is of actual value.
* In most cases, this is a remote data source. But it can also be a value that we have updated programmatically.
*/
ACTUAL,
/** Status is loaded from the cache and can't be updated with actual value */
ONLY_CACHE,
}

View file

@ -9,6 +9,7 @@ plugins {
dependencies { dependencies {
implementation(projects.domain.core) implementation(projects.domain.core)
implementation(projects.domain.models)
implementation(deps.kotlin.serialization) implementation(deps.kotlin.serialization)
implementation(deps.jodatime) implementation(deps.jodatime)

View file

@ -1,5 +1,6 @@
package com.tangem.domain.staking.model.stakekit package com.tangem.domain.staking.model.stakekit
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import org.joda.time.DateTime import org.joda.time.DateTime
import java.math.BigDecimal import java.math.BigDecimal
@ -13,7 +14,7 @@ sealed class YieldBalance {
override val integrationId: String?, override val integrationId: String?,
override val address: String, override val address: String,
val balance: YieldBalanceItem, val balance: YieldBalanceItem,
val isCached: Boolean, val source: StatusSource,
) : YieldBalance() { ) : YieldBalance() {
fun getTotalWithRewardsStakingBalance(): BigDecimal { fun getTotalWithRewardsStakingBalance(): BigDecimal {
return balance.items.sumOf { it.amount } return balance.items.sumOf { it.amount }
@ -42,7 +43,7 @@ sealed class YieldBalance {
data class Empty( data class Empty(
override val integrationId: String?, override val integrationId: String?,
override val address: String, override val address: String,
val isCached: Boolean, val source: StatusSource,
) : YieldBalance() ) : YieldBalance()
data class Error(override val integrationId: String?, override val address: String?) : YieldBalance() data class Error(override val integrationId: String?, override val address: String?) : YieldBalance()

View file

@ -9,6 +9,7 @@ dependencies {
implementation(projects.core.analytics.models) implementation(projects.core.analytics.models)
/** Project - Domain */ /** Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.txhistory.models) implementation(projects.domain.txhistory.models)
implementation(projects.domain.staking.models) implementation(projects.domain.staking.models)

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens.model package com.tangem.domain.tokens.model
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryItem
import java.math.BigDecimal import java.math.BigDecimal
@ -83,6 +84,7 @@ data class CryptoCurrencyStatus(
* Represents a state where there is no account associated with the cryptocurrency * Represents a state where there is no account associated with the cryptocurrency
* *
* @property amountToCreateAccount base reserve amount for account creation * @property amountToCreateAccount base reserve amount for account creation
* @property source source of data
*/ */
data class NoAccount( data class NoAccount(
val amountToCreateAccount: BigDecimal, val amountToCreateAccount: BigDecimal,
@ -90,6 +92,7 @@ data class CryptoCurrencyStatus(
override val priceChange: BigDecimal?, override val priceChange: BigDecimal?,
override val fiatRate: BigDecimal?, override val fiatRate: BigDecimal?,
override val networkAddress: NetworkAddress, override val networkAddress: NetworkAddress,
val source: StatusSource,
) : Value(isError = false) { ) : Value(isError = false) {
override val amount: BigDecimal = BigDecimal.ZERO override val amount: BigDecimal = BigDecimal.ZERO
@ -105,6 +108,7 @@ data class CryptoCurrencyStatus(
* @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the
* cryptocurrency network. * cryptocurrency network.
* @property pendingTransactions The current cryptocurrency transactions. * @property pendingTransactions The current cryptocurrency transactions.
* @property source source of data
*/ */
data class Loaded( data class Loaded(
override val amount: BigDecimal, override val amount: BigDecimal,
@ -115,6 +119,7 @@ data class CryptoCurrencyStatus(
override val hasCurrentNetworkTransactions: Boolean, override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxHistoryItem>, override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress, override val networkAddress: NetworkAddress,
val source: StatusSource,
) : Value(isError = false) ) : Value(isError = false)
/** /**

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens.model package com.tangem.domain.tokens.model
import com.tangem.domain.models.StatusSource
import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryItem
import java.math.BigDecimal import java.math.BigDecimal
@ -21,14 +22,14 @@ data class NetworkStatus(
*/ */
sealed class Value { sealed class Value {
abstract val isCached: Boolean abstract val source: StatusSource
} }
/** /**
* Represents the state where the network is refreshing. * Represents the state where the network is refreshing.
*/ */
data object Refreshing : Value() { data object Refreshing : Value() {
override val isCached: Boolean = false override val source: StatusSource = StatusSource.ACTUAL
} }
/** /**
@ -37,14 +38,14 @@ data class NetworkStatus(
* @property address Network addresses. * @property address Network addresses.
*/ */
data class Unreachable(val address: NetworkAddress?) : Value() { data class Unreachable(val address: NetworkAddress?) : Value() {
override val isCached: Boolean = false override val source: StatusSource = StatusSource.ACTUAL
} }
/** /**
* Represents the state where a derivation has been missed. * Represents the state where a derivation has been missed.
*/ */
data object MissedDerivation : Value() { data object MissedDerivation : Value() {
override val isCached: Boolean = false override val source: StatusSource = StatusSource.ACTUAL
} }
/** /**
@ -54,14 +55,14 @@ data class NetworkStatus(
* @property address Network addresses. * @property address Network addresses.
* @property amounts A map containing the amounts associated with different cryptocurrencies within the network. * @property amounts A map containing the amounts associated with different cryptocurrencies within the network.
* @property pendingTransactions A map containing pending transactions associated with different cryptocurrencies * @property pendingTransactions A map containing pending transactions associated with different cryptocurrencies
* @property isCached flag that determines whether the status is a cache * @property source source of data
* within the network. * within the network.
*/ */
data class Verified( data class Verified(
val address: NetworkAddress, val address: NetworkAddress,
val amounts: Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus>, val amounts: Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus>,
val pendingTransactions: Map<CryptoCurrency.ID, Set<TxHistoryItem>>, val pendingTransactions: Map<CryptoCurrency.ID, Set<TxHistoryItem>>,
override val isCached: Boolean, override val source: StatusSource,
) : Value() ) : Value()
/** /**
@ -70,12 +71,12 @@ data class NetworkStatus(
* @property address Network addresses. * @property address Network addresses.
* @property amountToCreateAccount The amount required to create an account within the network. * @property amountToCreateAccount The amount required to create an account within the network.
* @property errorMessage error message * @property errorMessage error message
* @property isCached flag that determines whether the status is a cache * @property source source of data
*/ */
data class NoAccount( data class NoAccount(
val address: NetworkAddress, val address: NetworkAddress,
val amountToCreateAccount: BigDecimal, val amountToCreateAccount: BigDecimal,
val errorMessage: String, val errorMessage: String,
override val isCached: Boolean, override val source: StatusSource,
) : Value() ) : Value()
} }

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens.model package com.tangem.domain.tokens.model
import com.tangem.domain.models.StatusSource
import java.math.BigDecimal import java.math.BigDecimal
sealed interface Quote { sealed interface Quote {
@ -19,12 +20,12 @@ sealed interface Quote {
* @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided.
* @property fiatRate The current fiat exchange rate for the cryptocurrency. * @property fiatRate The current fiat exchange rate for the cryptocurrency.
* @property priceChange The price change for the cryptocurrency. * @property priceChange The price change for the cryptocurrency.
* @property isCached flag that determines whether the quote is a cache * @property source source of data
*/ */
data class Value( data class Value(
override val rawCurrencyId: CryptoCurrency.RawID, override val rawCurrencyId: CryptoCurrency.RawID,
val fiatRate: BigDecimal, val fiatRate: BigDecimal,
val priceChange: BigDecimal, val priceChange: BigDecimal,
val isCached: Boolean, val source: StatusSource,
) : Quote ) : Quote
} }

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens.operations package com.tangem.domain.tokens.operations
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.*
import java.math.BigDecimal import java.math.BigDecimal
@ -49,17 +50,27 @@ internal class CurrencyStatusOperations(
) )
} }
private fun createNoAccountStatus(status: NetworkStatus.NoAccount): CryptoCurrencyStatus.NoAccount = private fun createNoAccountStatus(status: NetworkStatus.NoAccount): CryptoCurrencyStatus.NoAccount {
CryptoCurrencyStatus.NoAccount( return CryptoCurrencyStatus.NoAccount(
amountToCreateAccount = status.amountToCreateAccount, amountToCreateAccount = status.amountToCreateAccount,
fiatAmount = if (quote == null) null else BigDecimal.ZERO, fiatAmount = if (quote == null) null else BigDecimal.ZERO,
priceChange = quote?.priceChange, priceChange = quote?.priceChange,
fiatRate = quote?.fiatRate, fiatRate = quote?.fiatRate,
networkAddress = status.address, networkAddress = status.address,
source = getResultStatusSource(
sources = listOf(
status.source,
(quote as? Quote.Value)?.source ?: StatusSource.ACTUAL,
),
),
) )
}
private fun createStatus(status: NetworkStatus.Verified, yieldBalance: YieldBalance?): CryptoCurrencyStatus.Value { private fun createStatus(
val amount = when (val amount = status.amounts[currency.id]) { networkStatusValue: NetworkStatus.Verified,
yieldBalance: YieldBalance?,
): CryptoCurrencyStatus.Value {
val amount = when (val amount = networkStatusValue.amounts[currency.id]) {
null -> { null -> {
return CryptoCurrencyStatus.Loading return CryptoCurrencyStatus.Loading
} }
@ -69,10 +80,10 @@ internal class CurrencyStatusOperations(
is CryptoCurrencyAmountStatus.Loaded -> amount.value is CryptoCurrencyAmountStatus.Loaded -> amount.value
} }
val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty() val hasCurrentNetworkTransactions = networkStatusValue.pendingTransactions.isNotEmpty()
val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet) val currentTransactions = networkStatusValue.pendingTransactions.getOrElse(currency.id, ::emptySet)
val yieldBalanceData = yieldBalance as? YieldBalance.Data val yieldBalanceData = yieldBalance as? YieldBalance.Data
val isCurrentAddressStaking = yieldBalanceData?.address == status.address.defaultAddress.value val isCurrentAddressStaking = yieldBalanceData?.address == networkStatusValue.address.defaultAddress.value
val filteredTokenBalances = yieldBalanceData?.balance?.items?.filter { val filteredTokenBalances = yieldBalanceData?.balance?.items?.filter {
it.token.coinGeckoId == currency.id.rawCurrencyId?.value it.token.coinGeckoId == currency.id.rawCurrencyId?.value
} }
@ -94,14 +105,14 @@ internal class CurrencyStatusOperations(
priceChange = quote?.priceChange, priceChange = quote?.priceChange,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions, pendingTransactions = currentTransactions,
networkAddress = status.address, networkAddress = networkStatusValue.address,
yieldBalance = currentYieldBalance, yieldBalance = currentYieldBalance,
) )
quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote( quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote(
amount = amount, amount = amount,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions, pendingTransactions = currentTransactions,
networkAddress = status.address, networkAddress = networkStatusValue.address,
yieldBalance = currentYieldBalance, yieldBalance = currentYieldBalance,
) )
quote is Quote.Value -> CryptoCurrencyStatus.Loaded( quote is Quote.Value -> CryptoCurrencyStatus.Loaded(
@ -111,8 +122,15 @@ internal class CurrencyStatusOperations(
priceChange = quote.priceChange, priceChange = quote.priceChange,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions, pendingTransactions = currentTransactions,
networkAddress = status.address, networkAddress = networkStatusValue.address,
yieldBalance = currentYieldBalance, yieldBalance = currentYieldBalance,
source = getResultStatusSource(
sources = listOf(
networkStatusValue.source,
currentYieldBalance?.source ?: StatusSource.ACTUAL,
quote.source,
),
),
) )
else -> CryptoCurrencyStatus.Loading else -> CryptoCurrencyStatus.Loading
} }
@ -127,4 +145,18 @@ internal class CurrencyStatusOperations(
private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal { private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal {
return amount * fiatRate return amount * fiatRate
} }
/*
* ACTUAL, ACTUAL, ACTUAL -> ACTUAL
* ACTUAL, ACTUAL, CACHE -> CACHE
* ACTUAL, ACTUAL, ONLY_CACHE -> ONLY_CACHE
* ACTUAL, CACHE, ONLY_CACHE -> ONLY_CACHE
*/
private fun getResultStatusSource(sources: List<StatusSource>): StatusSource {
return when {
sources.any { it == StatusSource.ONLY_CACHE } -> StatusSource.ONLY_CACHE
sources.any { it == StatusSource.CACHE } -> StatusSource.CACHE
else -> StatusSource.ACTUAL
}
}
} }

View file

@ -2,6 +2,7 @@ package com.tangem.domain.tokens.mock
import arrow.core.NonEmptySet import arrow.core.NonEmptySet
import arrow.core.nonEmptySetOf import arrow.core.nonEmptySetOf
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkAddress
@ -76,7 +77,7 @@ internal object MockNetworks {
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
), ),
errorMessage = "", errorMessage = "",
isCached = false, source = StatusSource.ACTUAL,
), ),
) )
@ -92,7 +93,7 @@ internal object MockNetworks {
address = NetworkAddress.Single( address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
), ),
isCached = false, source = StatusSource.ACTUAL,
), ),
) )
@ -108,7 +109,7 @@ internal object MockNetworks {
address = NetworkAddress.Single( address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
), ),
isCached = false, source = StatusSource.ACTUAL,
), ),
) )
@ -125,7 +126,7 @@ internal object MockNetworks {
address = NetworkAddress.Single( address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
), ),
isCached = false, source = StatusSource.ACTUAL,
), ),
) )
} }

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens.mock package com.tangem.domain.tokens.mock
import arrow.core.nonEmptySetOf import arrow.core.nonEmptySetOf
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.Quote
import java.math.BigDecimal import java.math.BigDecimal
@ -12,70 +13,70 @@ internal object MockQuotes {
rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!,
fiatRate = BigDecimal("1.23"), fiatRate = BigDecimal("1.23"),
priceChange = BigDecimal("0.01"), priceChange = BigDecimal("0.01"),
isCached = false, source = StatusSource.ACTUAL,
) )
val quote2 = Quote.Value( val quote2 = Quote.Value(
rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!,
fiatRate = BigDecimal("2.34"), fiatRate = BigDecimal("2.34"),
priceChange = BigDecimal("-0.02"), priceChange = BigDecimal("-0.02"),
isCached = false, source = StatusSource.ACTUAL,
) )
val quote3 = Quote.Value( val quote3 = Quote.Value(
rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!,
fiatRate = BigDecimal("3.45"), fiatRate = BigDecimal("3.45"),
priceChange = BigDecimal("0.03"), priceChange = BigDecimal("0.03"),
isCached = false, source = StatusSource.ACTUAL,
) )
val quote4 = Quote.Value( val quote4 = Quote.Value(
rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!,
fiatRate = BigDecimal("4.56"), fiatRate = BigDecimal("4.56"),
priceChange = BigDecimal("-0.04"), priceChange = BigDecimal("-0.04"),
isCached = false, source = StatusSource.ACTUAL,
) )
val quote5 = Quote.Value( val quote5 = Quote.Value(
rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!,
fiatRate = BigDecimal("5.67"), fiatRate = BigDecimal("5.67"),
priceChange = BigDecimal("0.05"), priceChange = BigDecimal("0.05"),
isCached = false, source = StatusSource.ACTUAL,
) )
val quote6 = Quote.Value( val quote6 = Quote.Value(
rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!,
fiatRate = BigDecimal("6.78"), fiatRate = BigDecimal("6.78"),
priceChange = BigDecimal("-0.06"), priceChange = BigDecimal("-0.06"),
isCached = false, source = StatusSource.ACTUAL,
) )
val quote7 = Quote.Value( val quote7 = Quote.Value(
rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!,
fiatRate = BigDecimal("7.89"), fiatRate = BigDecimal("7.89"),
priceChange = BigDecimal("0.07"), priceChange = BigDecimal("0.07"),
isCached = false, source = StatusSource.ACTUAL,
) )
val quote8 = Quote.Value( val quote8 = Quote.Value(
rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!,
fiatRate = BigDecimal("8.90"), fiatRate = BigDecimal("8.90"),
priceChange = BigDecimal("-0.08"), priceChange = BigDecimal("-0.08"),
isCached = false, source = StatusSource.ACTUAL,
) )
val quote9 = Quote.Value( val quote9 = Quote.Value(
rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!,
fiatRate = BigDecimal("9.01"), fiatRate = BigDecimal("9.01"),
priceChange = BigDecimal("0.09"), priceChange = BigDecimal("0.09"),
isCached = false, source = StatusSource.ACTUAL,
) )
val quote10 = Quote.Value( val quote10 = Quote.Value(
rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!, rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!,
fiatRate = BigDecimal("10.12"), fiatRate = BigDecimal("10.12"),
priceChange = BigDecimal("-0.10"), priceChange = BigDecimal("-0.10"),
isCached = false, source = StatusSource.ACTUAL,
) )
val quote11 = Quote.Empty(CryptoCurrency.RawID("null")) val quote11 = Quote.Empty(CryptoCurrency.RawID("null"))

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens.mock package com.tangem.domain.tokens.mock
import arrow.core.nonEmptyListOf import arrow.core.nonEmptyListOf
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.*
import java.math.BigDecimal import java.math.BigDecimal
@ -74,6 +75,7 @@ internal object MockTokensStates {
networkAddress = NetworkAddress.Single( networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
), ),
source = StatusSource.ACTUAL,
), ),
) )
@ -87,6 +89,7 @@ internal object MockTokensStates {
networkAddress = NetworkAddress.Single( networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
), ),
source = StatusSource.ACTUAL,
), ),
) )
@ -100,6 +103,7 @@ internal object MockTokensStates {
networkAddress = NetworkAddress.Single( networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
), ),
source = StatusSource.ACTUAL,
), ),
) )
@ -113,6 +117,7 @@ internal object MockTokensStates {
networkAddress = NetworkAddress.Single( networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
), ),
source = StatusSource.ACTUAL,
), ),
) )
@ -157,6 +162,7 @@ internal object MockTokensStates {
hasCurrentNetworkTransactions = false, hasCurrentNetworkTransactions = false,
networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address,
yieldBalance = null, yieldBalance = null,
source = StatusSource.ACTUAL,
) )
} }
status.copy(value = value) status.copy(value = value)