Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-12 23:04:32 +04:00
parent e417996525
commit 4f5257e537
8 changed files with 307 additions and 27 deletions

View file

@ -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()),
),
)
}
}

View file

@ -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}"
}
}

View file

@ -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)

View file

@ -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,
}
}
}

View file

@ -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)
}

View file

@ -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))
}
}
}