Updated on 2026-08-14
This commit is contained in:
parent
9531146e8f
commit
4eb9f62f4c
9 changed files with 156 additions and 55 deletions
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.local.network
|
|||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.network.converter.NetworkDerivationPathConverter
|
||||
import com.tangem.datasource.local.network.converter.NetworkStatusConverter
|
||||
import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
|
|
@ -13,6 +14,8 @@ import com.tangem.utils.extensions.addOrReplace
|
|||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
private typealias NetworkStatusesByWalletId = Map<String, Set<NetworkStatusDM>>
|
||||
|
||||
|
|
@ -21,6 +24,8 @@ internal class DefaultNetworksStatusesStore(
|
|||
private val persistenceDataStore: DataStore<NetworkStatusesByWalletId>,
|
||||
) : NetworksStatusesStore {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
override fun get(key: UserWalletId): Flow<Set<NetworkStatus>> {
|
||||
return runtimeDataStore.get(provideStringKey(key))
|
||||
}
|
||||
|
|
@ -28,12 +33,14 @@ internal class DefaultNetworksStatusesStore(
|
|||
override fun get(key: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>> = channelFlow {
|
||||
val cachedStatuses = persistenceDataStore.data.firstOrNull()
|
||||
?.get(key.stringValue)
|
||||
?.mapNotNullTo(mutableSetOf()) { status ->
|
||||
val network = networks
|
||||
.firstOrNull { it.id == status.networkId }
|
||||
?.mapNotNullTo(mutableSetOf()) { cached ->
|
||||
val network = networks.firstOrNull {
|
||||
it.id == cached.networkId &&
|
||||
it.derivationPath == NetworkDerivationPathConverter.convert(cached.derivationPath)
|
||||
}
|
||||
?: return@mapNotNullTo null
|
||||
|
||||
NetworkStatusConverter(network = network, isCached = true).convert(value = status)
|
||||
NetworkStatusConverter(network = network, isCached = true).convert(value = cached)
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
|
|
@ -71,25 +78,31 @@ internal class DefaultNetworksStatusesStore(
|
|||
}
|
||||
|
||||
override suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>) {
|
||||
coroutineScope {
|
||||
launch { storeInRuntimeStore(key = key, statuses = values) }
|
||||
launch { storeInPersistenceStore(userWalletId = key, statuses = values) }
|
||||
mutex.withLock {
|
||||
coroutineScope {
|
||||
launch { storeInRuntimeStore(key = key, statuses = values) }
|
||||
launch { storeInPersistenceStore(userWalletId = key, statuses = values) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun refresh(key: UserWalletId, networks: Set<Network>) {
|
||||
val currentStatuses = getSyncOrNull(key).orEmpty()
|
||||
mutex.withLock {
|
||||
val currentStatuses = getSyncOrNull(key).orEmpty()
|
||||
|
||||
storeInRuntimeStore(
|
||||
key = key,
|
||||
statuses = networks.mapNotNullTo(hashSetOf()) { network ->
|
||||
val status = currentStatuses.firstOrNull { it.network.id == network.id } ?: return@mapNotNullTo null
|
||||
storeInRuntimeStore(
|
||||
key = key,
|
||||
statuses = networks.mapNotNullTo(hashSetOf()) { network ->
|
||||
val status = currentStatuses.firstOrNull {
|
||||
it.network.id == network.id && it.network.derivationPath == network.derivationPath
|
||||
} ?: return@mapNotNullTo null
|
||||
|
||||
status.copy(
|
||||
value = status.value.copySealed(source = StatusSource.CACHE),
|
||||
)
|
||||
},
|
||||
)
|
||||
status.copy(
|
||||
value = status.value.copySealed(source = StatusSource.CACHE),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -146,7 +159,9 @@ internal class DefaultNetworksStatusesStore(
|
|||
persistenceDataStore.updateData { storedStatuses ->
|
||||
storedStatuses.toMutableMap().apply {
|
||||
val updatedValues = this[userWalletId.stringValue].orEmpty()
|
||||
.addOrReplace(newStatuses) { prev, new -> prev.networkId == new.networkId }
|
||||
.addOrReplace(newStatuses) { prev, new ->
|
||||
prev.networkId == new.networkId && prev.derivationPath == new.derivationPath
|
||||
}
|
||||
|
||||
this[userWalletId.stringValue] = updatedValues
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.datasource.local.network.converter
|
||||
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM.DerivationPath.Type
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
|
||||
/**
|
||||
* Converter from [NetworkStatusDM.DerivationPath] to [Network.DerivationPath] and vice versa
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object NetworkDerivationPathConverter :
|
||||
TwoWayConverter<NetworkStatusDM.DerivationPath, Network.DerivationPath> {
|
||||
|
||||
override fun convert(value: NetworkStatusDM.DerivationPath): Network.DerivationPath {
|
||||
return when (value.type) {
|
||||
Type.CARD -> Network.DerivationPath.Card(value.value)
|
||||
Type.CUSTOM -> Network.DerivationPath.Custom(value.value)
|
||||
Type.NONE -> Network.DerivationPath.None
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(value: Network.DerivationPath): NetworkStatusDM.DerivationPath {
|
||||
return NetworkStatusDM.DerivationPath(
|
||||
value = value.value.orEmpty(),
|
||||
type = when (value) {
|
||||
is Network.DerivationPath.Card -> Type.CARD
|
||||
is Network.DerivationPath.Custom -> Type.CUSTOM
|
||||
Network.DerivationPath.None -> Type.NONE
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, Netwo
|
|||
is NetworkStatus.Verified -> {
|
||||
NetworkStatusDM.Verified(
|
||||
networkId = value.network.id,
|
||||
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
|
||||
selectedAddress = status.address.defaultAddress.value,
|
||||
availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value)
|
||||
.convertBack(value = status.address),
|
||||
|
|
@ -25,6 +26,7 @@ internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, Netwo
|
|||
is NetworkStatus.NoAccount -> {
|
||||
NetworkStatusDM.NoAccount(
|
||||
networkId = value.network.id,
|
||||
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
|
||||
selectedAddress = status.address.defaultAddress.value,
|
||||
availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value)
|
||||
.convertBack(value = status.address),
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ import java.math.BigDecimal
|
|||
internal sealed interface NetworkStatusDM {
|
||||
|
||||
val networkId: Network.ID
|
||||
val derivationPath: DerivationPath
|
||||
val selectedAddress: String
|
||||
val availableAddresses: Set<Address>
|
||||
|
||||
@NameLabel("amounts")
|
||||
data class Verified(
|
||||
@Json(name = "network_id") override val networkId: Network.ID,
|
||||
@Json(name = "derivation_path") override val derivationPath: DerivationPath,
|
||||
@Json(name = "selected_address") override val selectedAddress: String,
|
||||
@Json(name = "available_addresses") override val availableAddresses: Set<Address>,
|
||||
@Json(name = "amounts") val amounts: Map<String, BigDecimal>,
|
||||
|
|
@ -23,12 +25,33 @@ internal sealed interface NetworkStatusDM {
|
|||
@NameLabel("amount_to_create_account")
|
||||
data class NoAccount(
|
||||
@Json(name = "network_id") override val networkId: Network.ID,
|
||||
@Json(name = "derivation_path") override val derivationPath: DerivationPath,
|
||||
@Json(name = "selected_address") override val selectedAddress: String,
|
||||
@Json(name = "available_addresses") override val availableAddresses: Set<Address>,
|
||||
@Json(name = "amount_to_create_account") val amountToCreateAccount: BigDecimal,
|
||||
@Json(name = "error_message") val errorMessage: String,
|
||||
) : NetworkStatusDM
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DerivationPath(
|
||||
@Json(name = "value") val value: String,
|
||||
@Json(name = "type") val type: Type,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Type {
|
||||
|
||||
@Json(name = "card")
|
||||
CARD,
|
||||
|
||||
@Json(name = "custom")
|
||||
CUSTOM,
|
||||
|
||||
@Json(name = "none")
|
||||
NONE,
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Address(
|
||||
@Json(name = "value") val value: String,
|
||||
|
|
|
|||
|
|
@ -21,4 +21,20 @@ enum class StatusSource {
|
|||
|
||||
/** Status is loaded from the cache and can't be updated with actual value */
|
||||
ONLY_CACHE,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get result status source for the list of [StatusSource]
|
||||
*
|
||||
* ACTUAL, ACTUAL, ACTUAL -> ACTUAL
|
||||
* ACTUAL, ACTUAL, CACHE -> CACHE
|
||||
* ACTUAL, ACTUAL, ONLY_CACHE -> ONLY_CACHE
|
||||
* ACTUAL, CACHE, ONLY_CACHE -> ONLY_CACHE
|
||||
*/
|
||||
fun List<StatusSource>.getResultStatusSource(): StatusSource {
|
||||
return when {
|
||||
any { it == StatusSource.ONLY_CACHE } -> StatusSource.ONLY_CACHE
|
||||
any { it == StatusSource.CACHE } -> StatusSource.CACHE
|
||||
else -> StatusSource.ACTUAL
|
||||
}
|
||||
}
|
||||
|
|
@ -142,14 +142,20 @@ sealed class CryptoCurrency {
|
|||
@Serializable
|
||||
data class NetworkIdWithDerivationPath(
|
||||
val rawId: String,
|
||||
val derivationPath: String,
|
||||
val derivationPathHashCode: Int,
|
||||
) : Body() {
|
||||
|
||||
override val value: String
|
||||
get() = buildString {
|
||||
append(rawId)
|
||||
append(DERIVATION_PATH_DELIMITER)
|
||||
append(derivationPath.hashCode())
|
||||
append(derivationPathHashCode)
|
||||
}
|
||||
|
||||
constructor(rawId: String, derivationPath: String) : this(
|
||||
rawId = rawId,
|
||||
derivationPathHashCode = derivationPath.hashCode(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,8 +218,13 @@ sealed class CryptoCurrency {
|
|||
|
||||
/**
|
||||
* Creates an [ID] from a string [value].
|
||||
* */
|
||||
*
|
||||
* Example:
|
||||
* 1. coin⟨BCH⟩bitcoin-cash
|
||||
* 2. coin⟨ETH→12367123⟩ethereum
|
||||
*/
|
||||
fun fromValue(value: String): ID {
|
||||
// ID(value='coin⟨BCH⟩bitcoin-cash'
|
||||
val parts = value.split(PREFIX_DELIMITER, SUFFIX_DELIMITER)
|
||||
|
||||
require(value = parts.size == ID_PARTS_COUNT) { "Invalid ID format: $value" }
|
||||
|
|
@ -224,7 +235,10 @@ sealed class CryptoCurrency {
|
|||
val bodyParts = parts[1].split(DERIVATION_PATH_DELIMITER)
|
||||
val body = when (bodyParts.size) {
|
||||
1 -> Body.NetworkId(bodyParts[0])
|
||||
2 -> Body.NetworkIdWithDerivationPath(bodyParts[0], bodyParts[1])
|
||||
2 -> Body.NetworkIdWithDerivationPath(
|
||||
rawId = bodyParts[0],
|
||||
derivationPathHashCode = bodyParts[1].toInt(),
|
||||
)
|
||||
else -> error("Invalid ID body: ${parts[1]}")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ data class CryptoCurrencyStatus(
|
|||
|
||||
/** Staking yield balance */
|
||||
open val yieldBalance: YieldBalance? = null
|
||||
|
||||
open val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/** Represents the Loading state of a cryptocurrency, typically while fetching its details. */
|
||||
|
|
@ -92,7 +94,7 @@ data class CryptoCurrencyStatus(
|
|||
override val priceChange: BigDecimal?,
|
||||
override val fiatRate: BigDecimal?,
|
||||
override val networkAddress: NetworkAddress,
|
||||
val source: StatusSource,
|
||||
override val source: StatusSource,
|
||||
) : Value(isError = false) {
|
||||
|
||||
override val amount: BigDecimal = BigDecimal.ZERO
|
||||
|
|
@ -119,7 +121,7 @@ data class CryptoCurrencyStatus(
|
|||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress,
|
||||
val source: StatusSource,
|
||||
override val source: StatusSource,
|
||||
) : Value(isError = false)
|
||||
|
||||
/**
|
||||
|
|
@ -142,6 +144,7 @@ data class CryptoCurrencyStatus(
|
|||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress,
|
||||
override val source: StatusSource,
|
||||
) : Value(isError = false)
|
||||
|
||||
/**
|
||||
|
|
@ -158,5 +161,8 @@ data class CryptoCurrencyStatus(
|
|||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress,
|
||||
) : Value(isError = false)
|
||||
) : Value(isError = false) {
|
||||
|
||||
override val source: StatusSource = StatusSource.CACHE
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.tokens.operations
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.getResultStatusSource
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.tokens.model.*
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -29,8 +30,7 @@ internal class CurrencyStatusOperations(
|
|||
|
||||
private fun createStatus(): CryptoCurrencyStatus.Value {
|
||||
return when (val status = networkStatus?.value) {
|
||||
null,
|
||||
-> CryptoCurrencyStatus.Loading
|
||||
null -> CryptoCurrencyStatus.Loading
|
||||
is NetworkStatus.MissedDerivation -> createMissedDerivationStatus()
|
||||
is NetworkStatus.Unreachable -> createUnreachableStatus(status)
|
||||
is NetworkStatus.NoAccount -> createNoAccountStatus(status)
|
||||
|
|
@ -56,15 +56,14 @@ internal class CurrencyStatusOperations(
|
|||
priceChange = quote?.priceChange,
|
||||
fiatRate = quote?.fiatRate,
|
||||
networkAddress = status.address,
|
||||
source = getResultStatusSource(
|
||||
sources = listOf(
|
||||
status.source,
|
||||
(quote as? Quote.Value)?.source ?: StatusSource.ACTUAL,
|
||||
),
|
||||
),
|
||||
source = listOf(
|
||||
status.source,
|
||||
(quote as? Quote.Value)?.source ?: StatusSource.ACTUAL,
|
||||
).getResultStatusSource(),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun createStatus(
|
||||
networkStatusValue: NetworkStatus.Verified,
|
||||
yieldBalance: YieldBalance?,
|
||||
|
|
@ -106,6 +105,11 @@ internal class CurrencyStatusOperations(
|
|||
pendingTransactions = currentTransactions,
|
||||
networkAddress = networkStatusValue.address,
|
||||
yieldBalance = currentYieldBalance,
|
||||
source = listOfNotNull(
|
||||
networkStatusValue.source,
|
||||
currentYieldBalance?.source ?: StatusSource.ACTUAL,
|
||||
(quote as? Quote.Value)?.source ?: StatusSource.ACTUAL,
|
||||
).getResultStatusSource(),
|
||||
)
|
||||
quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote(
|
||||
amount = amount,
|
||||
|
|
@ -123,13 +127,11 @@ internal class CurrencyStatusOperations(
|
|||
pendingTransactions = currentTransactions,
|
||||
networkAddress = networkStatusValue.address,
|
||||
yieldBalance = currentYieldBalance,
|
||||
source = getResultStatusSource(
|
||||
sources = listOf(
|
||||
networkStatusValue.source,
|
||||
currentYieldBalance?.source ?: StatusSource.ACTUAL,
|
||||
quote.source,
|
||||
),
|
||||
),
|
||||
source = listOf(
|
||||
networkStatusValue.source,
|
||||
currentYieldBalance?.source ?: StatusSource.ACTUAL,
|
||||
quote.source,
|
||||
).getResultStatusSource(),
|
||||
)
|
||||
else -> CryptoCurrencyStatus.Loading
|
||||
}
|
||||
|
|
@ -144,18 +146,4 @@ internal class CurrencyStatusOperations(
|
|||
private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.tokens.operations
|
|||
|
||||
import arrow.core.NonEmptyList
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.getResultStatusSource
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.TotalFiatBalance
|
||||
|
|
@ -53,7 +54,9 @@ internal class TokenListFiatBalanceOperations(
|
|||
}
|
||||
}
|
||||
|
||||
return fiatBalance
|
||||
return (fiatBalance as? TotalFiatBalance.Loaded)?.copy(
|
||||
source = currencies.map { it.value.source }.getResultStatusSource(),
|
||||
) ?: fiatBalance
|
||||
}
|
||||
|
||||
private fun recalculateNoAccountBalance(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue