From 4eb9f62f4c9539d2acc2b07d928bcf18646faa71 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 21 Feb 2025 12:25:03 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../network/DefaultNetworksStatusesStore.kt | 51 ++++++++++++------- .../NetworkDerivationPathConverter.kt | 34 +++++++++++++ .../NetworkStatusDataModelConverter.kt | 2 + .../local/network/entity/NetworkStatusDM.kt | 23 +++++++++ .../com/tangem/domain/models/StatusSource.kt | 16 ++++++ .../domain/tokens/model/CryptoCurrency.kt | 22 ++++++-- .../tokens/model/CryptoCurrencyStatus.kt | 12 +++-- .../operations/CurrencyStatusOperations.kt | 46 +++++++---------- .../TokenListFiatBalanceOperations.kt | 5 +- 9 files changed, 156 insertions(+), 55 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt index 8a2e170bb9..e5d163c5ab 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt @@ -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> @@ -21,6 +24,8 @@ internal class DefaultNetworksStatusesStore( private val persistenceDataStore: DataStore, ) : NetworksStatusesStore { + private val mutex = Mutex() + override fun get(key: UserWalletId): Flow> { return runtimeDataStore.get(provideStringKey(key)) } @@ -28,12 +33,14 @@ internal class DefaultNetworksStatusesStore( override fun get(key: UserWalletId, networks: Set): Flow> = 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) { - 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) { - 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 } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt new file mode 100644 index 0000000000..92925a866f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt @@ -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 { + + 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 + }, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt index cf42370c96..34dc695aba 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt @@ -16,6 +16,7 @@ internal object NetworkStatusDataModelConverter : Converter { 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 { 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), diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt index b07ad107c1..0ada82055f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt @@ -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
@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
, @Json(name = "amounts") val amounts: Map, @@ -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
, @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, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt index cba099bbec..c316840ea4 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt @@ -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.getResultStatusSource(): StatusSource { + return when { + any { it == StatusSource.ONLY_CACHE } -> StatusSource.ONLY_CACHE + any { it == StatusSource.CACHE } -> StatusSource.CACHE + else -> StatusSource.ACTUAL + } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt index b882eb2bf6..4575b2feae 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -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]}") } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index d2df3fd74b..4d32d97ab9 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -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, 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, 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, override val networkAddress: NetworkAddress, - ) : Value(isError = false) + ) : Value(isError = false) { + + override val source: StatusSource = StatusSource.CACHE + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index d160103afe..831989f95e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -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 { - return when { - sources.any { it == StatusSource.ONLY_CACHE } -> StatusSource.ONLY_CACHE - sources.any { it == StatusSource.CACHE } -> StatusSource.CACHE - else -> StatusSource.ACTUAL - } - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index d8cea3866c..e4d319c4ff 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -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(