Updated on 2026-08-14
This commit is contained in:
parent
e417996525
commit
4f5257e537
8 changed files with 307 additions and 27 deletions
|
|
@ -1,21 +1,39 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.network.DefaultNetworksStatusesStore
|
||||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.network.utils.NetworkStatusesSerializer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object NetworksStatusesStoreModule {
|
||||
|
||||
@Provides
|
||||
fun provideNetworksStatusesStore(): NetworksStatusesStore {
|
||||
fun provideNetworksStatusesStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): NetworksStatusesStore {
|
||||
return DefaultNetworksStatusesStore(
|
||||
dataStore = RuntimeDataStore(),
|
||||
runtimeDataStore = RuntimeDataStore(),
|
||||
persistneceDataStore = DataStoreFactory.create(
|
||||
serializer = NetworkStatusesSerializer(moshi),
|
||||
produceFile = { context.dataStoreFile(fileName = "networks_statuses") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,70 @@
|
|||
package com.tangem.datasource.local.network
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusesDM
|
||||
import com.tangem.datasource.local.network.utils.toDataModel
|
||||
import com.tangem.datasource.local.network.utils.toDomainModel
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import com.tangem.utils.extensions.replaceBy
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
internal class DefaultNetworksStatusesStore(
|
||||
dataStore: StringKeyDataStore<Set<NetworkStatus>>,
|
||||
) : NetworksStatusesStore, StringKeyDataStoreDecorator<UserWalletId, Set<NetworkStatus>>(dataStore) {
|
||||
private val runtimeDataStore: RuntimeDataStore<Set<NetworkStatus>>,
|
||||
private val persistneceDataStore: DataStore<NetworkStatusesDM>,
|
||||
) : NetworksStatusesStore {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
override fun provideStringKey(key: UserWalletId): String {
|
||||
return key.stringValue
|
||||
override fun get(key: UserWalletId): Flow<Set<NetworkStatus>> {
|
||||
return runtimeDataStore.get(provideStringKey(key))
|
||||
}
|
||||
|
||||
override fun get(key: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>> = channelFlow {
|
||||
val cachedStatuses = persistneceDataStore.data.firstOrNull()
|
||||
?.get(key.stringValue)
|
||||
?.mapNotNullTo(mutableSetOf()) { status ->
|
||||
val network = networks
|
||||
.firstOrNull { it.id == status.networkId }
|
||||
?: return@mapNotNullTo null
|
||||
|
||||
status.toDomainModel(network)
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
if (cachedStatuses.isNotEmpty()) {
|
||||
send(cachedStatuses)
|
||||
}
|
||||
|
||||
runtimeDataStore.get(provideStringKey(key))
|
||||
.onEach { runtimeStatuses ->
|
||||
if (cachedStatuses.isEmpty()) {
|
||||
send(runtimeStatuses)
|
||||
} else {
|
||||
val mergedStatuses = cachedStatuses.toMutableList()
|
||||
|
||||
runtimeStatuses.forEach { runtimeStatus ->
|
||||
val index = mergedStatuses.indexOfFirst { it.network == runtimeStatus.network }
|
||||
if (index != -1) {
|
||||
mergedStatuses[index] = runtimeStatus
|
||||
} else {
|
||||
mergedStatuses.add(runtimeStatus)
|
||||
}
|
||||
}
|
||||
|
||||
send(mergedStatuses.toSet())
|
||||
}
|
||||
}
|
||||
.launchIn(scope = this)
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>? {
|
||||
return runtimeDataStore.getSyncOrNull(provideStringKey(key))
|
||||
}
|
||||
|
||||
override suspend fun store(key: UserWalletId, value: NetworkStatus) {
|
||||
|
|
@ -25,7 +73,8 @@ internal class DefaultNetworksStatusesStore(
|
|||
?.addOrReplace(value) { it.network == value.network }
|
||||
?: setOf(value)
|
||||
|
||||
store(key, newValues)
|
||||
runtimeDataStore.store(provideStringKey(key), newValues)
|
||||
storeNetworkStatusInPersistence(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +93,29 @@ internal class DefaultNetworksStatusesStore(
|
|||
}
|
||||
}
|
||||
|
||||
store(key, updatedValues)
|
||||
runtimeDataStore.store(provideStringKey(key), updatedValues)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, networkStatus: NetworkStatus) {
|
||||
val status = networkStatus.value
|
||||
val network = networkStatus.network
|
||||
|
||||
if (status !is NetworkStatus.Verified) return
|
||||
|
||||
persistneceDataStore.updateData { storedStatuses ->
|
||||
val userWalletStatuses = storedStatuses[userWalletId.stringValue] ?: emptySet()
|
||||
val updatedStatuses = userWalletStatuses.addOrReplace(status.toDataModel(network)) {
|
||||
it.networkId == network.id
|
||||
}
|
||||
|
||||
storedStatuses.toMutableMap().apply {
|
||||
this[userWalletId.stringValue] = updatedStatuses
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideStringKey(key: UserWalletId): String {
|
||||
return "network_statuses_${key.stringValue}"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.datasource.local.network
|
||||
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -8,6 +9,8 @@ interface NetworksStatusesStore {
|
|||
|
||||
fun get(key: UserWalletId): Flow<Set<NetworkStatus>>
|
||||
|
||||
fun get(key: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>>
|
||||
|
||||
suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>?
|
||||
|
||||
suspend fun store(key: UserWalletId, value: NetworkStatus)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.datasource.local.network.entity
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal typealias NetworkStatusesDM = Map<String, Set<NetworkStatusDM>>
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class NetworkStatusDM(
|
||||
val networkId: Network.ID,
|
||||
val selectedAddress: String,
|
||||
val availableAddresses: Set<Address>,
|
||||
val amounts: Map<String, BigDecimal>,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Address(
|
||||
val value: String,
|
||||
val type: Type,
|
||||
) {
|
||||
|
||||
enum class Type {
|
||||
Primary, Secondary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.datasource.local.network.utils
|
||||
|
||||
import com.tangem.common.extensions.mapNotNullValues
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.domain.tokens.model.*
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal fun NetworkStatus.Verified.toDataModel(network: Network): NetworkStatusDM {
|
||||
return NetworkStatusDM(
|
||||
networkId = network.id,
|
||||
selectedAddress = address.defaultAddress.value,
|
||||
availableAddresses = address.availableAddresses
|
||||
.map { address ->
|
||||
NetworkStatusDM.Address(
|
||||
value = address.value,
|
||||
type = when (address.type) {
|
||||
NetworkAddress.Address.Type.Primary -> NetworkStatusDM.Address.Type.Primary
|
||||
NetworkAddress.Address.Type.Secondary -> NetworkStatusDM.Address.Type.Secondary
|
||||
},
|
||||
)
|
||||
}
|
||||
.toSet(),
|
||||
amounts = amounts
|
||||
.mapKeys { (id, _) -> id.value }
|
||||
.mapNotNullValues { (_, amount) ->
|
||||
when (amount) {
|
||||
is CryptoCurrencyAmountStatus.Loaded -> amount.value
|
||||
is CryptoCurrencyAmountStatus.NotFound -> null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun NetworkStatusDM.toDomainModel(network: Network): NetworkStatus {
|
||||
return NetworkStatus(
|
||||
network = network,
|
||||
value = NetworkStatus.Verified(
|
||||
address = mapToDomainAddress(selectedAddress, availableAddresses),
|
||||
amounts = mapToDomainAmounts(amounts),
|
||||
pendingTransactions = mapOf(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapToDomainAmounts(amounts: Map<String, BigDecimal>): Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus> {
|
||||
return amounts
|
||||
.mapKeys { CryptoCurrency.ID.fromValue(it.key) }
|
||||
.mapValues { (_, amount) -> CryptoCurrencyAmountStatus.Loaded(amount) }
|
||||
}
|
||||
|
||||
private fun mapToDomainAddress(
|
||||
selectedAddress: String,
|
||||
availableAddresses: Set<NetworkStatusDM.Address>,
|
||||
): NetworkAddress {
|
||||
val defaultAddress = availableAddresses
|
||||
.firstOrNull { it.value == selectedAddress }
|
||||
?.let(::mapToDomainAddress)
|
||||
|
||||
requireNotNull(defaultAddress) { "Selected address must not be null" }
|
||||
|
||||
return if (availableAddresses.size != 1) {
|
||||
NetworkAddress.Selectable(defaultAddress, availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress))
|
||||
} else {
|
||||
NetworkAddress.Single(defaultAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapToDomainAddress(address: NetworkStatusDM.Address): NetworkAddress.Address {
|
||||
val type = when (address.type) {
|
||||
NetworkStatusDM.Address.Type.Primary -> NetworkAddress.Address.Type.Primary
|
||||
NetworkStatusDM.Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary
|
||||
}
|
||||
|
||||
if (address.value.isBlank()) {
|
||||
Timber.w("Address value is blank")
|
||||
}
|
||||
|
||||
return NetworkAddress.Address(address.value, type)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.datasource.local.network.utils
|
||||
|
||||
import androidx.datastore.core.Serializer
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusesDM
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
internal class NetworkStatusesSerializer(moshi: Moshi) : Serializer<NetworkStatusesDM> {
|
||||
|
||||
private val adapter by lazy {
|
||||
val type = Types.newParameterizedType(
|
||||
Map::class.java,
|
||||
String::class.java,
|
||||
Types.newParameterizedType(Set::class.java, NetworkStatusDM::class.java),
|
||||
)
|
||||
|
||||
moshi.adapter<NetworkStatusesDM>(type)
|
||||
}
|
||||
|
||||
override val defaultValue: NetworkStatusesDM = emptyMap()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): NetworkStatusesDM {
|
||||
return input.bufferedReader().use { reader ->
|
||||
adapter.fromJson(reader.readText()) ?: defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: NetworkStatusesDM, output: OutputStream) {
|
||||
output.bufferedWriter().use { writer ->
|
||||
writer.write(adapter.toJson(t))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,10 +26,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
|||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -48,15 +45,18 @@ internal class DefaultNetworksRepository(
|
|||
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
|
||||
private val networkStatusFactory = NetworkStatusFactory()
|
||||
|
||||
override suspend fun fetchNetworkStatuses(userWalletId: UserWalletId, networks: Set<Network>, refresh: Boolean) {
|
||||
TODO("Will be implemented in [REDACTED_TASK_KEY]")
|
||||
}
|
||||
|
||||
override fun getNetworkStatusesUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
): Flow<Set<NetworkStatus>> {
|
||||
TODO("Will be implemented in [REDACTED_TASK_KEY]")
|
||||
return networksStatusesStore.get(userWalletId, networks)
|
||||
.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
override suspend fun fetchNetworkStatuses(userWalletId: UserWalletId, networks: Set<Network>, refresh: Boolean) {
|
||||
withContext(dispatchers.io) {
|
||||
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNetworkStatusesUpdatesLegacy(
|
||||
|
|
@ -167,12 +167,13 @@ internal class DefaultNetworksRepository(
|
|||
network: Network,
|
||||
currencies: Sequence<CryptoCurrency>,
|
||||
) {
|
||||
val networkCurrencies = currencies.filter { it.network == network }
|
||||
|
||||
val result = walletManagersFacade.update(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
extraTokens = currencies
|
||||
extraTokens = networkCurrencies
|
||||
.filterIsInstance<CryptoCurrency.Token>()
|
||||
.filter { it.network == network }
|
||||
.toSet(),
|
||||
)
|
||||
|
||||
|
|
@ -183,7 +184,7 @@ internal class DefaultNetworksRepository(
|
|||
val networkStatus = networkStatusFactory.createNetworkStatus(
|
||||
network = network,
|
||||
result = result,
|
||||
currencies = currencies.toSet(),
|
||||
currencies = networkCurrencies.toSet(),
|
||||
)
|
||||
|
||||
networksStatusesStore.store(userWalletId, networkStatus)
|
||||
|
|
@ -214,7 +215,7 @@ internal class DefaultNetworksRepository(
|
|||
|
||||
private suspend fun getCurrencies(userWalletId: UserWalletId, networks: Set<Network>): Sequence<CryptoCurrency> {
|
||||
val currencies = getCurrencies(userWalletId)
|
||||
return currencies.filter { networks.contains(it.network) }
|
||||
return currencies.filter { it.network in networks }
|
||||
}
|
||||
|
||||
private suspend fun getCurrencies(userWalletId: UserWalletId): Sequence<CryptoCurrency> {
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ sealed class CryptoCurrency {
|
|||
get() = buildString {
|
||||
append(rawId)
|
||||
if (contractAddress != null) {
|
||||
append(SUFFIX_DELIMITER)
|
||||
append(CONTRACT_ADDRESS_DELIMITER)
|
||||
append(contractAddress)
|
||||
}
|
||||
}
|
||||
|
|
@ -188,11 +188,55 @@ sealed class CryptoCurrency {
|
|||
return "ID(value='$value')"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ID
|
||||
|
||||
return value == other.value
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return value.hashCode()
|
||||
}
|
||||
|
||||
companion object {
|
||||
// should use delimiters that could be used in URL not like path or query delimiters
|
||||
private const val PREFIX_DELIMITER = '_'
|
||||
private const val SUFFIX_DELIMITER = ';'
|
||||
private const val DERIVATION_PATH_DELIMITER = 'd'
|
||||
private const val PREFIX_DELIMITER = '\u27E8' // ⟨
|
||||
private const val SUFFIX_DELIMITER = '\u27E9' // ⟩
|
||||
private const val CONTRACT_ADDRESS_DELIMITER = '\u2693' // ⚓
|
||||
private const val DERIVATION_PATH_DELIMITER = '\u2192' // →
|
||||
|
||||
private const val ID_PARTS_COUNT = 3
|
||||
|
||||
/**
|
||||
* Creates an [ID] from a string [value].
|
||||
* */
|
||||
fun fromValue(value: String): ID {
|
||||
val parts = value.split(PREFIX_DELIMITER, SUFFIX_DELIMITER)
|
||||
|
||||
require(value = parts.size == ID_PARTS_COUNT) { "Invalid ID format: $value" }
|
||||
|
||||
val prefix = Prefix.entries.firstOrNull { it.value == parts[0] }
|
||||
requireNotNull(prefix) { "Invalid ID prefix: ${parts[0]}" }
|
||||
|
||||
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])
|
||||
else -> error("Invalid ID body: ${parts[1]}")
|
||||
}
|
||||
|
||||
val suffixParts = parts[2].split(CONTRACT_ADDRESS_DELIMITER)
|
||||
val suffix = when (suffixParts.size) {
|
||||
1 -> Suffix.ContractAddress(suffixParts[0])
|
||||
2 -> Suffix.RawID(suffixParts[0], suffixParts[1])
|
||||
else -> error("Invalid ID suffix: ${parts[2]}")
|
||||
}
|
||||
|
||||
return ID(prefix, body, suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue