Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-18 20:43:38 +04:00
parent 42f0e3f0fe
commit 0bf2decb83
13 changed files with 244 additions and 109 deletions

View file

@ -52,6 +52,7 @@ dependencies {
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.moshi.adapters)
implementation(deps.moshi.adapters.ext)
implementation(deps.okHttp)
implementation(deps.okHttp.prettyLogging)
implementation(deps.retrofit)

View file

@ -9,6 +9,7 @@ import com.tangem.datasource.api.common.adapter.DateTimeAdapter
import com.tangem.datasource.api.common.adapter.LocalDateAdapter
import com.tangem.datasource.api.common.adapter.addStakeKitEnumFallbackAdapters
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
@ -16,6 +17,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dev.onenowy.moshipolymorphicadapter.NamePolymorphicAdapterFactory
import javax.inject.Singleton
@Module
@ -38,6 +40,11 @@ class MoshiModule {
.add(DateTimeAdapter())
.add(VisaActivationRemoteState.jsonAdapter)
.add(VisaCardActivationStatus.jsonAdapter)
.add(
NamePolymorphicAdapterFactory.of(NetworkStatusDM::class.java)
.withSubtype(NetworkStatusDM.Verified::class.java, "amounts")
.withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"),
)
.addLast(KotlinJsonAdapterFactory())
.addStakeKitEnumFallbackAdapters()
.build()

View file

@ -2,9 +2,9 @@ package com.tangem.datasource.local.network
import androidx.datastore.core.DataStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.network.entity.NetworkStatusesByWalletId
import com.tangem.datasource.local.network.utils.toDataModel
import com.tangem.datasource.local.network.utils.toDomainModel
import com.tangem.datasource.local.network.converter.NetworkStatusConverter
import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
@ -14,6 +14,8 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
private typealias NetworkStatusesByWalletId = Map<String, Set<NetworkStatusDM>>
internal class DefaultNetworksStatusesStore(
private val runtimeDataStore: RuntimeDataStore<Set<NetworkStatus>>,
private val persistenceDataStore: DataStore<NetworkStatusesByWalletId>,
@ -33,7 +35,7 @@ internal class DefaultNetworksStatusesStore(
.firstOrNull { it.id == status.networkId }
?: return@mapNotNullTo null
status.toDomainModel(network = network, isCached = true)
NetworkStatusConverter(network = network, isCached = true).convert(value = status)
}
.orEmpty()
@ -103,16 +105,14 @@ internal class DefaultNetworksStatusesStore(
}
private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, networkStatus: NetworkStatus) {
val status = networkStatus.value
val network = networkStatus.network
if (status !is NetworkStatus.Verified) return
persistenceDataStore.updateData { storedStatuses ->
val userWalletStatuses = storedStatuses[userWalletId.stringValue] ?: emptySet()
val updatedStatuses = userWalletStatuses.addOrReplace(item = status.toDataModel(network)) {
it.networkId == network.id
}
val updatedStatuses = userWalletStatuses.addOrReplace(
item = NetworkStatusDataModelConverter.convert(value = networkStatus),
predicate = { it.networkId == network.id },
)
storedStatuses.toMutableMap().apply {
this[userWalletId.stringValue] = updatedStatuses

View file

@ -0,0 +1,60 @@
package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.utils.converter.TwoWayConverter
import timber.log.Timber
/**
* Converter from [Set<NetworkStatusDM.Address>] to [NetworkAddress] and vice versa
*
[REDACTED_AUTHOR]
*/
internal class NetworkAddressConverter(
private val selectedAddress: String,
) : TwoWayConverter<Set<NetworkStatusDM.Address>, NetworkAddress> {
override fun convert(value: Set<NetworkStatusDM.Address>): NetworkAddress {
val defaultAddress = value
.firstOrNull { it.value == selectedAddress }
?.let(::toNetworkAddress)
requireNotNull(defaultAddress) { "Selected address must not be null" }
return if (value.size != 1) {
NetworkAddress.Selectable(
defaultAddress = defaultAddress,
availableAddresses = value.mapTo(destination = hashSetOf(), transform = ::toNetworkAddress),
)
} else {
NetworkAddress.Single(defaultAddress = defaultAddress)
}
}
override fun convertBack(value: NetworkAddress): Set<NetworkStatusDM.Address> {
return value.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()
}
private fun toNetworkAddress(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(value = address.value, type = type)
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.datasource.local.network.converter
import com.tangem.common.extensions.mapNotNullValues
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.utils.converter.TwoWayConverter
import java.math.BigDecimal
private typealias AmountsDataModel = Map<String, BigDecimal>
private typealias AmountsDomainModel = Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus>
/**
* Converter from [AmountsDataModel] to [AmountsDomainModel] and vice versa
*
[REDACTED_AUTHOR]
*/
internal object NetworkAmountsConverter : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
override fun convert(value: AmountsDataModel): AmountsDomainModel {
return value
.mapKeys { CryptoCurrency.ID.fromValue(value = it.key) }
.mapValues { (_, amount) -> CryptoCurrencyAmountStatus.Loaded(value = amount) }
}
override fun convertBack(value: AmountsDomainModel): AmountsDataModel {
return value
.mapKeys { (id, _) -> id.value }
.mapNotNullValues { (_, amount) ->
when (amount) {
is CryptoCurrencyAmountStatus.Loaded -> amount.value
is CryptoCurrencyAmountStatus.NotFound -> null
}
}
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.utils.converter.Converter
/**
* Converter from [NetworkStatusDM] to [NetworkStatus]
*
* @property network network
* @property isCached flag that determines whether the status is a cache
*
[REDACTED_AUTHOR]
*/
internal class NetworkStatusConverter(
private val network: Network,
private val isCached: Boolean,
) : Converter<NetworkStatusDM, NetworkStatus> {
override fun convert(value: NetworkStatusDM): NetworkStatus {
val address = NetworkAddressConverter(selectedAddress = value.selectedAddress)
.convert(value = value.availableAddresses)
val status = when (value) {
is NetworkStatusDM.Verified -> {
NetworkStatus.Verified(
address = address,
amounts = NetworkAmountsConverter.convert(value = value.amounts),
pendingTransactions = mapOf(),
isCached = isCached,
)
}
is NetworkStatusDM.NoAccount -> {
NetworkStatus.NoAccount(
address = address,
amountToCreateAccount = value.amountToCreateAccount,
errorMessage = value.errorMessage,
isCached = isCached,
)
}
}
return NetworkStatus(network = network, value = status)
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.utils.converter.Converter
/**
* Converter from [NetworkStatus] to [NetworkStatusDM]
*
[REDACTED_AUTHOR]
*/
internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, NetworkStatusDM> {
override fun convert(value: NetworkStatus): NetworkStatusDM {
return when (val status = value.value) {
is NetworkStatus.Verified -> {
NetworkStatusDM.Verified(
networkId = value.network.id,
selectedAddress = status.address.defaultAddress.value,
availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value)
.convertBack(value = status.address),
amounts = NetworkAmountsConverter.convertBack(value = status.amounts),
)
}
is NetworkStatus.NoAccount -> {
NetworkStatusDM.NoAccount(
networkId = value.network.id,
selectedAddress = status.address.defaultAddress.value,
availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value)
.convertBack(value = status.address),
amountToCreateAccount = status.amountToCreateAccount,
errorMessage = status.errorMessage,
)
}
else -> error("Unsupported network status: $value")
}
}
}

View file

@ -3,17 +3,31 @@ package com.tangem.datasource.local.network.entity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.domain.tokens.model.Network
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
import java.math.BigDecimal
internal typealias NetworkStatusesByWalletId = Map<String, Set<NetworkStatusDM>>
internal sealed interface NetworkStatusDM {
@JsonClass(generateAdapter = true)
internal data class NetworkStatusDM(
@Json(name = "network_id") val networkId: Network.ID,
@Json(name = "selected_address") val selectedAddress: String,
@Json(name = "available_addresses") val availableAddresses: Set<Address>,
@Json(name = "amounts") val amounts: Map<String, BigDecimal>,
) {
val networkId: Network.ID
val selectedAddress: String
val availableAddresses: Set<Address>
@NameLabel("amounts")
data class Verified(
@Json(name = "network_id") override val networkId: Network.ID,
@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>,
) : NetworkStatusDM
@NameLabel("amount_to_create_account")
data class NoAccount(
@Json(name = "network_id") override val networkId: Network.ID,
@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 Address(

View file

@ -1,81 +0,0 @@
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, isCached: Boolean): NetworkStatus {
return NetworkStatus(
network = network,
value = NetworkStatus.Verified(
address = mapToDomainAddress(selectedAddress, availableAddresses),
amounts = mapToDomainAmounts(amounts),
pendingTransactions = mapOf(),
),
isCached = isCached,
)
}
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

@ -26,6 +26,7 @@ internal class NetworkStatusFactory {
address = getNetworkAddress(result.selectedAddress, result.addresses),
amountToCreateAccount = result.amountToCreateAccount,
errorMessage = result.errorMessage,
isCached = false,
)
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
address = getNetworkAddress(result.selectedAddress, result.addresses),
@ -34,9 +35,9 @@ internal class NetworkStatusFactory {
transactions = result.currentTransactions,
currencies = currencies,
),
isCached = false,
)
},
isCached = false,
)
}

View file

@ -8,12 +8,10 @@ import java.math.BigDecimal
*
* @property network The network for which the status is provided.
* @property value The specific status value, represented as a sealed class to encapsulate the various possible states of the network.
* @property isCached flag that determines whether the status is a cache
*/
data class NetworkStatus(
val network: Network,
val value: Value,
val isCached: Boolean,
) {
/**
@ -21,24 +19,33 @@ data class NetworkStatus(
*
* This sealed class includes different states like unreachable, missed derivation, verified, and no account.
*/
sealed class Value
sealed class Value {
abstract val isCached: Boolean
}
/**
* Represents the state where the network is refreshing.
*/
data object Refreshing : Value()
data object Refreshing : Value() {
override val isCached: Boolean = false
}
/**
* Represents the state where the network is unreachable.
*
* @property address Network addresses.
*/
data class Unreachable(val address: NetworkAddress?) : Value()
data class Unreachable(val address: NetworkAddress?) : Value() {
override val isCached: Boolean = false
}
/**
* Represents the state where a derivation has been missed.
*/
data object MissedDerivation : Value()
data object MissedDerivation : Value() {
override val isCached: Boolean = false
}
/**
* Represents the verified state of the network, including the amounts associated with different cryptocurrencies
@ -47,12 +54,14 @@ data class NetworkStatus(
* @property address Network addresses.
* @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 isCached flag that determines whether the status is a cache
* within the network.
*/
data class Verified(
val address: NetworkAddress,
val amounts: Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus>,
val pendingTransactions: Map<CryptoCurrency.ID, Set<TxHistoryItem>>,
override val isCached: Boolean,
) : Value()
/**
@ -61,10 +70,12 @@ data class NetworkStatus(
* @property address Network addresses.
* @property amountToCreateAccount The amount required to create an account within the network.
* @property errorMessage error message
* @property isCached flag that determines whether the status is a cache
*/
data class NoAccount(
val address: NetworkAddress,
val amountToCreateAccount: BigDecimal,
val errorMessage: String,
override val isCached: Boolean,
) : Value()
}

View file

@ -61,13 +61,11 @@ internal object MockNetworks {
private val networkStatus1 = NetworkStatus(
network = network1,
value = NetworkStatus.Unreachable(address = null),
isCached = false,
)
private val networkStatus2 = NetworkStatus(
network = network2,
value = NetworkStatus.MissedDerivation,
isCached = false,
)
private val networkStatus3 = NetworkStatus(
@ -78,8 +76,8 @@ internal object MockNetworks {
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
errorMessage = "",
isCached = false,
),
isCached = false,
)
private val verifiedNetworkStatus1: NetworkStatus
@ -94,6 +92,7 @@ internal object MockNetworks {
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
isCached = false,
),
)
@ -109,6 +108,7 @@ internal object MockNetworks {
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
isCached = false,
),
)
@ -125,6 +125,7 @@ internal object MockNetworks {
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
isCached = false,
),
)
}

View file

@ -59,6 +59,7 @@ kotsonGsonExt = "2.5.0"
lottie = "3.4.0"
lottie-compose = "6.6.0"
moshi = "1.15.1"
moshiAdaptersExt = "0.1.5"
okhttp = "4.9.3"
rekotlin = "1.0.4"
retrofit = "2.11.0"
@ -224,6 +225,7 @@ material = { module = "com.google.android.material:material", version.ref = "goo
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" }
moshi-adapters-ext = { module = "dev.onenowy.moshipolymorphicadapter:moshi-polymorphic-adapter", version.ref = "moshiAdaptersExt" }
moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" }