Updated on 2026-08-14
This commit is contained in:
commit
ca49abf2b1
892 changed files with 17979 additions and 6555 deletions
|
|
@ -11,4 +11,9 @@ interface AuthProvider {
|
|||
fun getCardPublicKey(): String
|
||||
|
||||
fun getCardId(): String
|
||||
|
||||
/**
|
||||
* Returns map where keys(cardId) associated with cardPublicKey
|
||||
*/
|
||||
fun getCardsPublicKeys(): Map<String, String>
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config.managers
|
|||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Api configs manager
|
||||
|
|
@ -10,8 +11,11 @@ import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
|||
*/
|
||||
interface ApiConfigsManager {
|
||||
|
||||
/** Flag that determines whether the manager is initialized */
|
||||
val isInitialized: StateFlow<Boolean>
|
||||
|
||||
/** Initialize resources */
|
||||
fun initialize() {}
|
||||
fun initialize()
|
||||
|
||||
/** Get environment config by [id] */
|
||||
fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig
|
||||
|
|
|
|||
|
|
@ -29,7 +29,13 @@ internal class DevApiConfigsManager(
|
|||
|
||||
private val _apiConfigs = MutableStateFlow(value = apiConfigs.associateWith { it.defaultEnvironment })
|
||||
|
||||
override val isInitialized: StateFlow<Boolean> get() = _isInitialized.asStateFlow()
|
||||
|
||||
private val _isInitialized = MutableStateFlow(value = false)
|
||||
|
||||
override fun initialize() {
|
||||
_isInitialized.value = false
|
||||
|
||||
// We can't use appPreferencesStore.getObjectMap as base flow,
|
||||
// because we should keep possibility to work with configs synchronous.
|
||||
// See [getBaseUrl]
|
||||
|
|
@ -42,8 +48,12 @@ internal class DevApiConfigsManager(
|
|||
savedEnvironments[config.id.name] ?: currentEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isInitialized.value) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
}
|
||||
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.main))
|
||||
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.default))
|
||||
}
|
||||
|
||||
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.datasource.api.common.config.managers
|
|||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiConfigs
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Implementation of [ApiConfigsManager] in PROD environment
|
||||
|
|
@ -13,6 +15,10 @@ internal class ProdApiConfigsManager(
|
|||
private val apiConfigs: ApiConfigs,
|
||||
) : ApiConfigsManager {
|
||||
|
||||
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)
|
||||
|
||||
override fun initialize() = Unit
|
||||
|
||||
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
|
||||
val config = apiConfigs.firstOrNull { it.id == id }
|
||||
?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ interface TangemTechApi {
|
|||
suspend fun updatePushTokenForApplicationId(
|
||||
@Path("application_id") applicationId: String,
|
||||
@Body body: NotificationApplicationCreateBody,
|
||||
): ApiResponse<String>
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@PATCH("user-wallets/wallets/{wallet_id}/notify")
|
||||
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
|
||||
|
|
@ -172,6 +172,9 @@ interface TangemTechApi {
|
|||
|
||||
@GET("user-wallets/wallets/{wallet_id}")
|
||||
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
|
||||
|
||||
@GET("user-wallets/wallets/by-app/{app_id}")
|
||||
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CardInfoBody(
|
||||
@Json(name = "card_id") val cardId: String,
|
||||
@Json(name = "card_public_key") val cardPublicKey: String,
|
||||
)
|
||||
|
|
@ -9,6 +9,7 @@ data class UserTokensResponse(
|
|||
@Json(name = "version") val version: Int = 0,
|
||||
@Json(name = "group") val group: GroupType,
|
||||
@Json(name = "sort") val sort: SortType,
|
||||
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
|
||||
@Json(name = "tokens") val tokens: List<Token> = emptyList(),
|
||||
) {
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ data class UserTokensResponse(
|
|||
@Json(name = "symbol") val symbol: String,
|
||||
@Json(name = "decimals") val decimals: Int,
|
||||
@Json(name = "contractAddress") val contractAddress: String?,
|
||||
@Json(name = "addresses") val addresses: List<String>? = null,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
val otherToken = other as? Token ?: return false
|
||||
|
|
|
|||
|
|
@ -6,4 +6,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class WalletIdBody(
|
||||
@Json(name = "id") val walletId: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "cards") val cards: List<CardInfoBody>,
|
||||
)
|
||||
|
|
@ -25,16 +25,18 @@ interface TangemVisaApi {
|
|||
// region: auth
|
||||
|
||||
@POST("v1/auth/challenge")
|
||||
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse
|
||||
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse<GenerateNonceResponse>
|
||||
|
||||
@POST("v1/auth/challenge")
|
||||
suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse
|
||||
suspend fun generateNonceByCardWallet(
|
||||
@Body request: GenerateNoneByCardWalletRequest,
|
||||
): ApiResponse<GenerateNonceResponse>
|
||||
|
||||
@POST("v1/auth/token")
|
||||
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse
|
||||
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
|
||||
|
||||
@POST("v1/auth/token")
|
||||
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse
|
||||
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>
|
||||
|
||||
@POST("v1/auth/token/refresh")
|
||||
suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse<JWTResponse>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.api.visa.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VisaErrorResponse(
|
||||
@Json(name = "error") val error: Error,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Error(
|
||||
@Json(name = "code") val code: Int,
|
||||
)
|
||||
}
|
||||
|
|
@ -43,6 +43,10 @@ internal object NetworkModule {
|
|||
private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L
|
||||
private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L
|
||||
|
||||
private val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
|
||||
// ApiConfig.ID.StakeKit,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApiConfigManager(
|
||||
|
|
@ -317,7 +321,7 @@ internal object NetworkModule {
|
|||
}
|
||||
b
|
||||
}
|
||||
.addLoggers(context)
|
||||
.addLoggers(context = context, id = id)
|
||||
.clientBuilder()
|
||||
.build(),
|
||||
)
|
||||
|
|
@ -325,6 +329,12 @@ internal object NetworkModule {
|
|||
.create(T::class.java)
|
||||
}
|
||||
|
||||
private fun OkHttpClient.Builder.addLoggers(context: Context, id: ApiConfig.ID): OkHttpClient.Builder {
|
||||
if (id in excludedApiForLogging) return this
|
||||
|
||||
return addLoggers(context)
|
||||
}
|
||||
|
||||
private data class Timeouts(
|
||||
val callTimeoutSeconds: Long? = null,
|
||||
val connectTimeoutSeconds: Long? = null,
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
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.entity.NetworkStatusDM
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
import com.tangem.datasource.utils.setTypes
|
||||
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
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object NetworksStatusesStoreModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providePersistenceNetworksStatusesStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): DataStore<Map<String, Set<NetworkStatusDM>>> {
|
||||
return DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithStringKeyTypes(valueTypes = setTypes<NetworkStatusDM>()),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "networks_statuses") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideNetworksStatusesStore(
|
||||
persistenceNetworksStatusesStore: DataStore<Map<String, Set<NetworkStatusDM>>>,
|
||||
): NetworksStatusesStore {
|
||||
return DefaultNetworksStatusesStore(
|
||||
runtimeDataStore = RuntimeDataStore(),
|
||||
persistenceDataStore = persistenceNetworksStatusesStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.di.local
|
||||
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.DefaultUserTokensResponseStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object LocalTokenModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserTokensResponseStore(appPreferencesStore: AppPreferencesStore): UserTokensResponseStore {
|
||||
return DefaultUserTokensResponseStore(appPreferencesStore = appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
|
|||
bittensorOnfinalityApiKey = value.bittensorOnfinalityKey,
|
||||
koinosProApiKey = value.koinosProApiKey,
|
||||
alephiumApiKey = value.alephiumTangemApiKey,
|
||||
moralisApiKey = value.moralisApiKey,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,201 +0,0 @@
|
|||
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
|
||||
import com.tangem.domain.models.StatusSource
|
||||
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 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>>
|
||||
|
||||
internal class DefaultNetworksStatusesStore(
|
||||
private val runtimeDataStore: RuntimeDataStore<Set<NetworkStatus>>,
|
||||
private val persistenceDataStore: DataStore<NetworkStatusesByWalletId>,
|
||||
) : NetworksStatusesStore {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
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 = persistenceDataStore.data.firstOrNull()
|
||||
?.get(key.stringValue)
|
||||
?.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 = cached)
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
if (cachedStatuses.isNotEmpty()) {
|
||||
send(cachedStatuses)
|
||||
}
|
||||
|
||||
runtimeDataStore.get(provideStringKey(key))
|
||||
.onEach { runtimeStatuses ->
|
||||
val mergedStatuses = mergeStatuses(
|
||||
networks = networks,
|
||||
cachedStatuses = cachedStatuses,
|
||||
runtimeStatuses = runtimeStatuses,
|
||||
)
|
||||
|
||||
send(mergedStatuses)
|
||||
}
|
||||
.launchIn(scope = this)
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>? {
|
||||
val runtimeStatuses = runtimeDataStore.getSyncOrNull(key = provideStringKey(key)) ?: return null
|
||||
|
||||
val networks = runtimeStatuses.map(NetworkStatus::network).toSet()
|
||||
|
||||
val cachedStatuses = persistenceDataStore.data.firstOrNull()
|
||||
?.get(key.stringValue)
|
||||
?.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 = cached)
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
return mergeStatuses(
|
||||
networks = networks,
|
||||
cachedStatuses = cachedStatuses,
|
||||
runtimeStatuses = runtimeStatuses,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun store(key: UserWalletId, value: NetworkStatus) {
|
||||
storeAll(key = key, values = setOf(value))
|
||||
}
|
||||
|
||||
override suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>) {
|
||||
mutex.withLock {
|
||||
coroutineScope {
|
||||
launch { storeInRuntimeStore(key = key, statuses = values) }
|
||||
launch { storeInPersistenceStore(userWalletId = key, statuses = values) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun refresh(key: UserWalletId, networks: Set<Network>) {
|
||||
mutex.withLock {
|
||||
val currentStatuses = getSyncOrNull(key).orEmpty()
|
||||
|
||||
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),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge [cachedStatuses] with [runtimeStatuses]
|
||||
* The resulting set contains statuses from both sets.
|
||||
* If a status with the same network is in both sets, the status from [runtimeStatuses] is used.
|
||||
*/
|
||||
private fun mergeStatuses(
|
||||
networks: Set<Network>,
|
||||
cachedStatuses: Set<NetworkStatus>,
|
||||
runtimeStatuses: Set<NetworkStatus>,
|
||||
): Set<NetworkStatus> {
|
||||
return networks.mapNotNullTo(hashSetOf()) { network ->
|
||||
val runtimeStatus = runtimeStatuses.firstOrNull { it.network == network }
|
||||
|
||||
if (runtimeStatus == null) {
|
||||
getCachedStatusIfPossible(
|
||||
cachedStatuses = cachedStatuses,
|
||||
network = network,
|
||||
source = StatusSource.CACHE,
|
||||
)
|
||||
} else if (runtimeStatus.value is NetworkStatus.Unreachable) {
|
||||
getCachedStatusIfPossible(
|
||||
cachedStatuses = cachedStatuses,
|
||||
network = network,
|
||||
source = StatusSource.ONLY_CACHE,
|
||||
)
|
||||
?: runtimeStatus
|
||||
} else {
|
||||
runtimeStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCachedStatusIfPossible(
|
||||
cachedStatuses: Set<NetworkStatus>,
|
||||
network: Network,
|
||||
source: StatusSource,
|
||||
): NetworkStatus? {
|
||||
val cached = cachedStatuses.firstOrNull { it.network == network } ?: return null
|
||||
|
||||
val updatedCachedStatus = when (val status = cached.value) {
|
||||
is NetworkStatus.NoAccount -> status.copy(source = source)
|
||||
is NetworkStatus.Verified -> status.copy(source = source)
|
||||
is NetworkStatus.Unreachable,
|
||||
is NetworkStatus.MissedDerivation,
|
||||
-> null
|
||||
}
|
||||
|
||||
return if (updatedCachedStatus != null) {
|
||||
cached.copy(value = updatedCachedStatus)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeInRuntimeStore(key: UserWalletId, statuses: Set<NetworkStatus>) {
|
||||
val updatedValues = getSyncOrNull(key).orEmpty()
|
||||
.addOrReplace(items = statuses) { prev, new -> prev.network == new.network }
|
||||
|
||||
runtimeDataStore.store(key = provideStringKey(key), value = updatedValues)
|
||||
}
|
||||
|
||||
private suspend fun storeInPersistenceStore(userWalletId: UserWalletId, statuses: Set<NetworkStatus>) {
|
||||
// Converter will return null if the network status is not supported
|
||||
val newStatuses = NetworkStatusDataModelConverter.convertSet(input = statuses).filterNotNull().toSet()
|
||||
|
||||
persistenceDataStore.updateData { storedStatuses ->
|
||||
storedStatuses.toMutableMap().apply {
|
||||
val updatedValues = this[userWalletId.stringValue].orEmpty()
|
||||
.addOrReplace(newStatuses) { prev, new ->
|
||||
prev.networkId == new.networkId && prev.derivationPath == new.derivationPath
|
||||
}
|
||||
|
||||
this[userWalletId.stringValue] = updatedValues
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideStringKey(key: UserWalletId): String {
|
||||
return "network_statuses_${key.stringValue}"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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
|
||||
|
||||
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)
|
||||
|
||||
suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>)
|
||||
|
||||
suspend fun refresh(key: UserWalletId, networks: Set<Network>)
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
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]
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
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]
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
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]
|
||||
*/
|
||||
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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
package com.tangem.datasource.local.network.converter
|
||||
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.domain.models.StatusSource
|
||||
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(),
|
||||
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
|
||||
)
|
||||
}
|
||||
is NetworkStatusDM.NoAccount -> {
|
||||
NetworkStatus.NoAccount(
|
||||
address = address,
|
||||
amountToCreateAccount = value.amountToCreateAccount,
|
||||
errorMessage = value.errorMessage,
|
||||
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return NetworkStatus(network = network, value = status)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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]
|
||||
*/
|
||||
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,
|
||||
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
|
||||
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,
|
||||
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
|
||||
selectedAddress = status.address.defaultAddress.value,
|
||||
availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value)
|
||||
.convertBack(value = status.address),
|
||||
amountToCreateAccount = status.amountToCreateAccount,
|
||||
errorMessage = status.errorMessage,
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,29 +2,63 @@ 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 com.tangem.datasource.local.network.entity.NetworkStatusDM.NoAccount
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM.Verified
|
||||
import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType
|
||||
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Network status for storage in the local cache. Supports two types - the [Verified] and [NoAccount].
|
||||
*
|
||||
* @see [com.tangem.domain.tokens.model.NetworkStatus]
|
||||
*/
|
||||
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
|
||||
sealed interface NetworkStatusDM {
|
||||
|
||||
val networkId: Network.ID
|
||||
/** Network id */
|
||||
val networkId: ID
|
||||
|
||||
/** Derivation path */
|
||||
val derivationPath: DerivationPath
|
||||
|
||||
/** Selected address */
|
||||
val selectedAddress: String
|
||||
|
||||
/** Available address */
|
||||
val availableAddresses: Set<Address>
|
||||
|
||||
/**
|
||||
* Verified
|
||||
*
|
||||
* @property networkId network id
|
||||
* @property derivationPath derivation path
|
||||
* @property selectedAddress selected address
|
||||
* @property availableAddresses available addresses
|
||||
* @property amounts amounts
|
||||
*/
|
||||
@NameLabel("amounts")
|
||||
data class Verified(
|
||||
@Json(name = "network_id") override val networkId: Network.ID,
|
||||
@Json(name = "network_id") override val networkId: 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>,
|
||||
) : NetworkStatusDM
|
||||
|
||||
/**
|
||||
* No account
|
||||
*
|
||||
* @property networkId network id
|
||||
* @property derivationPath derivation path
|
||||
* @property selectedAddress selected address
|
||||
* @property availableAddresses available addresses
|
||||
* @property amountToCreateAccount amount to create account
|
||||
* @property errorMessage error message
|
||||
*/
|
||||
@NameLabel("amount_to_create_account")
|
||||
data class NoAccount(
|
||||
@Json(name = "network_id") override val networkId: Network.ID,
|
||||
@Json(name = "network_id") override val networkId: 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>,
|
||||
|
|
@ -32,6 +66,11 @@ sealed interface NetworkStatusDM {
|
|||
@Json(name = "error_message") val errorMessage: String,
|
||||
) : NetworkStatusDM
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ID(
|
||||
@Json(name = "value") val value: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DerivationPath(
|
||||
@Json(name = "value") val value: String,
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ package com.tangem.datasource.local.nft
|
|||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
import com.tangem.blockchain.nft.models.NFTCollection
|
||||
import com.tangem.datasource.local.nft.custom.NFTPriceId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class DefaultNFTPersistenceStore(
|
||||
private val collectionsPersistenceStore: DataStore<List<NFTCollection>>,
|
||||
private val pricesPersistenceStore: DataStore<Map<NFTAsset.Identifier, NFTAsset.SalePrice>>,
|
||||
private val pricesPersistenceStore: DataStore<List<NFTPriceId>>,
|
||||
) : NFTPersistenceStore {
|
||||
|
||||
override fun getCollections(): Flow<List<NFTCollection>?> = collectionsPersistenceStore.data
|
||||
|
|
@ -27,11 +28,12 @@ internal class DefaultNFTPersistenceStore(
|
|||
}
|
||||
|
||||
override fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTAsset.SalePrice?> = pricesPersistenceStore.data
|
||||
.map { it[assetId] }
|
||||
.map { data -> data.associate { it.assetId to it.price }[assetId] }
|
||||
|
||||
override suspend fun getSalePricesSync(): Map<NFTAsset.Identifier, NFTAsset.SalePrice>? = pricesPersistenceStore
|
||||
.data
|
||||
.firstOrNull()
|
||||
?.associate { it.assetId to it.price }
|
||||
|
||||
override suspend fun saveCollections(collections: List<NFTCollection>) {
|
||||
collectionsPersistenceStore.updateData {
|
||||
|
|
@ -41,9 +43,14 @@ internal class DefaultNFTPersistenceStore(
|
|||
|
||||
override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) {
|
||||
pricesPersistenceStore.updateData {
|
||||
it.toMutableMap().apply { this[assetId] = salePrice }
|
||||
it.toMutableList() + NFTPriceId(assetId = assetId, price = salePrice)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
collectionsPersistenceStore.updateData { emptyList() }
|
||||
pricesPersistenceStore.updateData { emptyList() }
|
||||
}
|
||||
|
||||
private fun NFTCollection.getAsset(assetId: NFTAsset.Identifier) = assets.firstOrNull { it.identifier == assetId }
|
||||
}
|
||||
|
|
@ -2,11 +2,11 @@ package com.tangem.datasource.local.nft
|
|||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.nft.models.NFTCollections
|
||||
import com.tangem.domain.nft.models.NFTSalePrice
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
@ -71,6 +71,16 @@ internal class DefaultNFTRuntimeStore(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
collectionsRuntimeStore.store(
|
||||
NFTCollections(
|
||||
network = network,
|
||||
content = NFTCollections.Content.Collections(null, StatusSource.ONLY_CACHE),
|
||||
),
|
||||
)
|
||||
pricesRuntimeStore.store(emptyMap())
|
||||
}
|
||||
|
||||
private fun NFTCollections.getCollection(collectionId: NFTCollection.Identifier): NFTCollection? =
|
||||
(content as? NFTCollections.Content.Collections)
|
||||
?.collections
|
||||
|
|
@ -105,7 +115,6 @@ internal class DefaultNFTRuntimeStore(
|
|||
-> assets
|
||||
is NFTCollection.Assets.Value -> assets.copy(
|
||||
items = assets.items
|
||||
.filter { !it.name.isNullOrEmpty() }
|
||||
.sortedBy { it.name }
|
||||
.map { asset ->
|
||||
asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id))
|
||||
|
|
@ -122,7 +131,7 @@ internal class DefaultNFTRuntimeStore(
|
|||
)
|
||||
}
|
||||
?.filter { it.count > 0 }
|
||||
?.sortedBy { it.name },
|
||||
?.sortedBy { it.name?.lowercase() },
|
||||
source = this.source,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,4 +18,6 @@ interface NFTPersistenceStore {
|
|||
suspend fun saveCollections(collections: List<NFTCollection>)
|
||||
|
||||
suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice)
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
@ -5,13 +5,12 @@ import androidx.datastore.core.DataStore
|
|||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
import com.tangem.blockchain.nft.models.NFTCollection
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.nft.custom.NFTPriceId
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.listTypes
|
||||
import com.tangem.datasource.utils.mapWithCustomKeyTypes
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
|
@ -48,8 +47,8 @@ class NFTPersistenceStoreFactory @Inject constructor(
|
|||
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices
|
||||
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices
|
||||
fileName = "nft_${userWalletStringId}_${networkStringId}_prices",
|
||||
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(),
|
||||
defaultValue = emptyMap(),
|
||||
types = listTypes<NFTPriceId>(),
|
||||
defaultValue = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -65,7 +64,7 @@ class NFTPersistenceStoreFactory @Inject constructor(
|
|||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
|
||||
private fun Network.ID.formatted(): String = value
|
||||
private fun Network.ID.formatted(): String = rawId.value
|
||||
.filter(Char::isLetterOrDigit)
|
||||
.lowercase()
|
||||
|
||||
|
|
|
|||
|
|
@ -23,4 +23,6 @@ interface NFTRuntimeStore {
|
|||
suspend fun saveCollections(collections: NFTCollections)
|
||||
|
||||
suspend fun saveSalePrice(salePrice: NFTSalePrice)
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.datasource.local.nft
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.models.network.Network
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.datasource.local.nft.converter
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTSalePrice
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ object NFTSdkAssetConverter : TwoWayConverter<Pair<Network, SdkNFTAsset>, NFTAss
|
|||
return value.network to SdkNFTAsset(
|
||||
identifier = assetId,
|
||||
collectionIdentifier = collectionId,
|
||||
blockchainId = value.network.id.value,
|
||||
blockchainId = value.network.rawId,
|
||||
contractType = value.contractType,
|
||||
owner = value.owner,
|
||||
name = value.name,
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@ import com.tangem.domain.nft.models.NFTSalePrice
|
|||
import com.tangem.utils.converter.TwoWayConverter
|
||||
import com.tangem.blockchain.nft.models.NFTAsset.SalePrice as SDKSalePrice
|
||||
|
||||
internal class NFTSdkAssetSalePriceConverter(
|
||||
class NFTSdkAssetSalePriceConverter(
|
||||
private val assetId: NFTAsset.Identifier,
|
||||
) : TwoWayConverter<SDKSalePrice, NFTSalePrice.Value> {
|
||||
override fun convert(value: SDKSalePrice): NFTSalePrice.Value {
|
||||
return NFTSalePrice.Value(
|
||||
assetId = assetId,
|
||||
fiatValue = null,
|
||||
value = value.value,
|
||||
symbol = value.symbol,
|
||||
symbol = value.symbol.orEmpty(),
|
||||
decimals = value.decimals ?: 0,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -20,6 +22,7 @@ internal class NFTSdkAssetSalePriceConverter(
|
|||
return SDKSalePrice(
|
||||
symbol = value.symbol,
|
||||
value = value.value,
|
||||
decimals = value.decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,26 @@
|
|||
package com.tangem.datasource.local.nft.converter
|
||||
|
||||
import android.content.res.Resources
|
||||
import com.tangem.datasource.R
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection
|
||||
|
||||
object NFTSdkCollectionConverter : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
|
||||
class NFTSdkCollectionConverter(
|
||||
private val resources: Resources,
|
||||
) : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
|
||||
override fun convert(value: Pair<Network, SdkNFTCollection>): NFTCollection {
|
||||
val (network, collection) = value
|
||||
val collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier)
|
||||
return NFTCollection(
|
||||
id = collectionId,
|
||||
network = network,
|
||||
name = collection.name,
|
||||
// We use localised strings from resources here to proceed sorting and searching correctly
|
||||
// Sorting is invoked on data layer, searching is simplified to filtering for now and invoked in Model
|
||||
name = collection.toName(collectionId),
|
||||
description = collection.description,
|
||||
logoUrl = collection.logoUrl,
|
||||
count = collection.count,
|
||||
|
|
@ -37,4 +43,26 @@ object NFTSdkCollectionConverter : Converter<Pair<Network, SdkNFTCollection>, NF
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun SdkNFTCollection.toName(collectionId: NFTCollection.Identifier) = when (collectionId) {
|
||||
is NFTCollection.Identifier.EVM ->
|
||||
name.toCollectionName()
|
||||
is NFTCollection.Identifier.TON -> if (collectionId.contractAddress == null) {
|
||||
resources.getString(R.string.nft_no_collection)
|
||||
} else {
|
||||
name.toCollectionName()
|
||||
}
|
||||
is NFTCollection.Identifier.Solana -> if (collectionId.collectionAddress == null) {
|
||||
resources.getString(R.string.nft_no_collection)
|
||||
} else {
|
||||
name.toCollectionName()
|
||||
}
|
||||
NFTCollection.Identifier.Unknown -> null
|
||||
}
|
||||
|
||||
private fun String?.toCollectionName() = if (this.isNullOrEmpty()) {
|
||||
resources.getString(R.string.nft_untitled_collection)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter<SdkNFTCollection.Id
|
|||
contractAddress = value.contractAddress,
|
||||
)
|
||||
is SdkNFTCollection.Identifier.Solana -> NFTCollection.Identifier.Solana(
|
||||
collection = value.collection,
|
||||
collectionAddress = value.collectionAddress,
|
||||
)
|
||||
is SdkNFTCollection.Identifier.Unknown -> NFTCollection.Identifier.Unknown
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter<SdkNFTCollection.Id
|
|||
contractAddress = value.contractAddress,
|
||||
)
|
||||
is NFTCollection.Identifier.Solana -> SdkNFTCollection.Identifier.Solana(
|
||||
collection = value.collection,
|
||||
collectionAddress = value.collectionAddress,
|
||||
)
|
||||
is NFTCollection.Identifier.Unknown -> SdkNFTCollection.Identifier.Unknown
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.datasource.local.nft.custom
|
||||
|
||||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
|
||||
data class NFTPriceId(
|
||||
val assetId: NFTAsset.Identifier,
|
||||
val price: NFTAsset.SalePrice,
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.datasource.local.nft.custom
|
||||
|
||||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
|
||||
data class NFTPriceKeyValue(
|
||||
val key: NFTAsset.Identifier,
|
||||
val value: NFTAsset.SalePrice,
|
||||
)
|
||||
|
|
@ -5,7 +5,7 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
|||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.quote.converter.QuoteConverter
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Quote
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.datasource.local.quote
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Quote
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.datasource.local.quote.converter
|
|||
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Quote
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
/**
|
||||
* Default implementation of [UserTokensResponseStore]
|
||||
*
|
||||
* @property appPreferencesStore app preferences store
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultUserTokensResponseStore(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : UserTokensResponseStore {
|
||||
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? {
|
||||
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
|
||||
key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
/**
|
||||
* Store of [UserTokensResponse]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface UserTokensResponseStore {
|
||||
|
||||
/** Get [UserTokensResponse] synchronously by [userWalletId] or null */
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse?
|
||||
}
|
||||
|
|
@ -2,25 +2,25 @@ package com.tangem.datasource.local.txhistory
|
|||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.txhistory.models.Page
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
|
||||
internal class DefaultTxHistoryItemsStore(
|
||||
dataStore: StringKeyDataStore<Set<PaginationWrapper<TxHistoryItem>>>,
|
||||
dataStore: StringKeyDataStore<Set<PaginationWrapper<TxInfo>>>,
|
||||
) : TxHistoryItemsStore,
|
||||
StringKeyDataStoreDecorator<TxHistoryItemsStore.Key, Set<PaginationWrapper<TxHistoryItem>>>(dataStore) {
|
||||
StringKeyDataStoreDecorator<TxHistoryItemsStore.Key, Set<PaginationWrapper<TxInfo>>>(dataStore) {
|
||||
|
||||
override fun provideStringKey(key: TxHistoryItemsStore.Key): String = key.toString()
|
||||
|
||||
override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Page): PaginationWrapper<TxHistoryItem>? {
|
||||
override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Page): PaginationWrapper<TxInfo>? {
|
||||
val storedValue = getSyncOrNull(key)
|
||||
|
||||
return storedValue?.firstOrNull { it.currentPage == page }
|
||||
}
|
||||
|
||||
override suspend fun store(key: TxHistoryItemsStore.Key, value: PaginationWrapper<TxHistoryItem>) {
|
||||
override suspend fun store(key: TxHistoryItemsStore.Key, value: PaginationWrapper<TxInfo>) {
|
||||
val oldValue = getSyncOrNull(key).orEmpty()
|
||||
val newValue = oldValue.addOrReplace(value) {
|
||||
it.currentPage == value.currentPage
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
package com.tangem.datasource.local.txhistory
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.txhistory.models.Page
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface TxHistoryItemsStore {
|
||||
|
||||
suspend fun getSyncOrNull(key: Key, page: Page): PaginationWrapper<TxHistoryItem>?
|
||||
suspend fun getSyncOrNull(key: Key, page: Page): PaginationWrapper<TxInfo>?
|
||||
|
||||
suspend fun remove(key: Key)
|
||||
|
||||
suspend fun store(key: Key, value: PaginationWrapper<TxHistoryItem>)
|
||||
suspend fun store(key: Key, value: PaginationWrapper<TxInfo>)
|
||||
|
||||
data class Key(
|
||||
val userWalletId: UserWalletId,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
package com.tangem.datasource.local.network.entity
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.datasource.api.common.adapter.BigDecimalAdapter
|
||||
import dev.onenowy.moshipolymorphicadapter.NamePolymorphicAdapterFactory
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class NetworkStatusDMSerializationTest {
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(
|
||||
NamePolymorphicAdapterFactory.of(NetworkStatusDM::class.java)
|
||||
.withSubtype(NetworkStatusDM.Verified::class.java, "amounts")
|
||||
.withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"),
|
||||
)
|
||||
.add(BigDecimalAdapter())
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private val adapter = moshi.adapter<NetworkStatusDM>()
|
||||
|
||||
@Test
|
||||
fun `deserialize JSON to Verified`() {
|
||||
// Arrange
|
||||
val json = """
|
||||
{
|
||||
"network_id": { "value": "ETH" },
|
||||
"derivation_path": {
|
||||
"value": "m/44'/60'/0'/0/0",
|
||||
"type": "card"
|
||||
},
|
||||
"selected_address": "0x123456",
|
||||
"available_addresses": [
|
||||
{ "value": "0x123456", "type": "primary" },
|
||||
{ "value": "0xabcdef", "type": "secondary" }
|
||||
],
|
||||
"amounts": { "ETH": "1.2345" }
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// Act
|
||||
val result = adapter.fromJson(json)
|
||||
|
||||
// Assert
|
||||
val expected = NetworkStatusDM.Verified(
|
||||
networkId = NetworkStatusDM.ID("ETH"),
|
||||
derivationPath = NetworkStatusDM.DerivationPath(
|
||||
value = "m/44'/60'/0'/0/0",
|
||||
type = NetworkStatusDM.DerivationPath.Type.CARD,
|
||||
),
|
||||
selectedAddress = "0x123456",
|
||||
availableAddresses = setOf(
|
||||
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
|
||||
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
|
||||
),
|
||||
amounts = mapOf("ETH" to BigDecimal("1.2345")),
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize Verified to JSON`() {
|
||||
// Arrange
|
||||
val model = NetworkStatusDM.Verified(
|
||||
networkId = NetworkStatusDM.ID("ETH"),
|
||||
derivationPath = NetworkStatusDM.DerivationPath(
|
||||
value = "m/44'/60'/0'/0/0",
|
||||
type = NetworkStatusDM.DerivationPath.Type.CARD,
|
||||
),
|
||||
selectedAddress = "0x123456",
|
||||
availableAddresses = setOf(
|
||||
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
|
||||
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
|
||||
),
|
||||
amounts = mapOf("ETH" to BigDecimal("1.2345")),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = adapter.toJson(model)
|
||||
|
||||
// Assert
|
||||
val expected = """
|
||||
{
|
||||
"network_id": { "value": "ETH" },
|
||||
"derivation_path": {
|
||||
"value": "m/44'/60'/0'/0/0",
|
||||
"type": "card"
|
||||
},
|
||||
"selected_address": "0x123456",
|
||||
"available_addresses": [
|
||||
{ "value": "0x123456", "type": "primary" },
|
||||
{ "value": "0xabcdef", "type": "secondary" }
|
||||
],
|
||||
"amounts": { "ETH": "1.2345" }
|
||||
}
|
||||
""".stripJsonWhitespace()
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize JSON to NoAccount`() {
|
||||
// Arrange
|
||||
val json = """
|
||||
{
|
||||
"network_id": { "value": "ETH" },
|
||||
"derivation_path": {
|
||||
"value": "m/44'/60'/0'/0/0",
|
||||
"type": "card"
|
||||
},
|
||||
"selected_address": "0x123456",
|
||||
"available_addresses": [
|
||||
{ "value": "0x123456", "type": "primary" },
|
||||
{ "value": "0xabcdef", "type": "secondary" }
|
||||
],
|
||||
"amount_to_create_account": "0.05",
|
||||
"error_message": "Account not found"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// Act
|
||||
val result = adapter.fromJson(json)
|
||||
|
||||
// Assert
|
||||
val expected = NetworkStatusDM.NoAccount(
|
||||
networkId = NetworkStatusDM.ID("ETH"),
|
||||
derivationPath = NetworkStatusDM.DerivationPath(
|
||||
value = "m/44'/60'/0'/0/0",
|
||||
type = NetworkStatusDM.DerivationPath.Type.CARD,
|
||||
),
|
||||
selectedAddress = "0x123456",
|
||||
availableAddresses = setOf(
|
||||
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
|
||||
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
|
||||
),
|
||||
amountToCreateAccount = BigDecimal("0.05"),
|
||||
errorMessage = "Account not found",
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize NoAccount to JSON`() {
|
||||
// Arrange
|
||||
val model = NetworkStatusDM.NoAccount(
|
||||
networkId = NetworkStatusDM.ID("ETH"),
|
||||
derivationPath = NetworkStatusDM.DerivationPath(
|
||||
value = "m/44'/60'/0'/0/0",
|
||||
type = NetworkStatusDM.DerivationPath.Type.CARD,
|
||||
),
|
||||
selectedAddress = "0x123456",
|
||||
availableAddresses = setOf(
|
||||
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
|
||||
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
|
||||
),
|
||||
amountToCreateAccount = BigDecimal("0.05"),
|
||||
errorMessage = "error",
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = adapter.toJson(model)
|
||||
|
||||
// Assert
|
||||
val expected = """
|
||||
{
|
||||
"network_id": { "value": "ETH" },
|
||||
"derivation_path": {
|
||||
"value": "m/44'/60'/0'/0/0",
|
||||
"type": "card"
|
||||
},
|
||||
"selected_address": "0x123456",
|
||||
"available_addresses": [
|
||||
{ "value": "0x123456", "type": "primary" },
|
||||
{ "value": "0xabcdef", "type": "secondary" }
|
||||
],
|
||||
"amount_to_create_account": "0.05",
|
||||
"error_message": "error"
|
||||
}
|
||||
""".stripJsonWhitespace()
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun String.stripJsonWhitespace(): String = replace(regex = "\\s".toRegex(), replacement = "")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue