Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-12 13:20:39 +05:00
parent 89b3e67f64
commit 296df28f0a
31 changed files with 958 additions and 0 deletions

1
data/nft/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

51
data/nft/build.gradle.kts Normal file
View file

@ -0,0 +1,51 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.ksp)
id("configuration")
}
android {
namespace = "com.tangem.data.nft"
}
dependencies {
/** Project - Data */
implementation(projects.core.datasource)
implementation(projects.data.common)
/** Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.nft)
implementation(projects.domain.nft.models)
/** Project - Utils */
implementation(projects.core.utils)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
/** Libs - Other */
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
implementation(deps.arrow.fx)
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.androidx.paging.runtime)
implementation(deps.moshi.kotlin)
ksp(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
/** Libs - Tangem */
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
/** DI */
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,227 @@
package com.tangem.data.nft
import arrow.core.Either
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.local.nft.NFTPersistenceStore
import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory
import com.tangem.datasource.local.nft.NFTRuntimeStore
import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter
import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter
import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter
import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection
internal class DefaultNFTRepository @Inject constructor(
private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory,
private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory,
private val walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
) : NFTRepository {
private val scope = CoroutineScope(dispatchers.io + SupervisorJob())
private val jobs = mutableMapOf<Network, JobHolder>()
private val nftRuntimeStores = mutableMapOf<Network, NFTRuntimeStore>()
private val nftPersistenceStores = mutableMapOf<Network, NFTPersistenceStore>()
private val nftSdkAssetIdentifierConverter = NFTSdkAssetIdentifierConverter()
private val nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter()
private val nftSdkAssetConverter = NFTSdkAssetConverter(
nftSdkAssetIdentifierConverter = nftSdkAssetIdentifierConverter,
nftSdkCollectionIdentifierConverter = nftSdkCollectionIdentifierConverter,
)
private val collectionConverter = NFTSdkCollectionConverter(
nftSdkCollectionIdentifierConverter = nftSdkCollectionIdentifierConverter,
nftSdkAssetConverter = nftSdkAssetConverter,
)
override fun observeCollections(userWalletId: UserWalletId, networks: List<Network>): Flow<List<NFTCollections>> =
flow {
emitAll(
combine(
networks.map { getNFTRuntimeStore(it).getCollections() },
) { it.asList() },
)
}.onStart {
refreshCollections(userWalletId, networks)
}
override suspend fun refreshCollections(userWalletId: UserWalletId, networks: List<Network>) {
networks.map { network ->
scope.launch {
expireCollections(network)
Either.catch {
walletManagersFacade.getNFTCollections(userWalletId, network)
}.onLeft {
saveFailedStateInRuntime(
network = network,
error = it,
)
}.onRight {
val mergedCollections = it.mergeWithStoredAssets(network)
saveCollectionsInRuntime(
network = network,
collections = mergedCollections,
)
saveCollectionsInPersistence(
network = network,
collections = mergedCollections,
)
}
}.saveIn(getJobHolder(network))
}.joinAll()
}
private suspend fun expireCollections(network: Network) {
val runtimeStore = getNFTRuntimeStore(network)
val expiredCollections = runtimeStore
.getCollectionsSync()
.let { collections ->
collections.copy(
content = when (val content = collections.content) {
is NFTCollections.Content.Collections -> content.copy(
source = StatusSource.CACHE,
)
is NFTCollections.Content.Error -> content
},
)
}
runtimeStore.saveCollections(expiredCollections)
}
private suspend fun saveCollectionsInRuntime(network: Network, collections: List<SdkNFTCollection>) {
getNFTRuntimeStore(network).saveCollections(
NFTCollections(
network = network,
content = NFTCollections.Content.Collections(
collections = collections.map { collection ->
collectionConverter.convert(network to collection)
},
source = StatusSource.ACTUAL,
),
),
)
}
private suspend fun saveFailedStateInRuntime(network: Network, error: Throwable) {
getNFTRuntimeStore(network).saveCollections(
NFTCollections(
network = network,
content = NFTCollections.Content.Error(
error = error,
),
),
)
}
private suspend fun saveCollectionsInPersistence(network: Network, collections: List<SdkNFTCollection>) {
getNFTPersistenceStore(network).saveCollections(collections)
}
private fun getJobHolder(network: Network): JobHolder = jobs[network] ?: run {
JobHolder().also {
jobs[network] = it
}
}
private fun getNFTPersistenceStore(network: Network): NFTPersistenceStore = nftPersistenceStores[network] ?: run {
nftPersistenceStoreFactory.provide(network).also {
nftPersistenceStores[network] = it
}
}
private suspend fun getNFTRuntimeStore(network: Network): NFTRuntimeStore = nftRuntimeStores[network] ?: run {
nftRuntimeStoreFactory.provide(network).also {
nftRuntimeStores[network] = it
val storedCollections = getStoredCollections(network)
val storedPrices = getStoredPrices(network)
it.initialize(
collections = storedCollections,
prices = storedPrices,
)
}
}
private suspend fun getStoredCollections(network: Network) = getNFTPersistenceStore(network)
.getCollectionsSync()
.let {
NFTCollections(
network = network,
content = NFTCollections.Content.Collections(
collections = it?.map { collection ->
collectionConverter.convert(network to collection)
},
source = StatusSource.CACHE,
),
)
}
private suspend fun getStoredPrices(network: Network) = getNFTPersistenceStore(network)
.getSalePricesSync()
.orEmpty()
.let { prices ->
prices
.mapKeys {
val (assetId, _) = it
nftSdkAssetIdentifierConverter.convert(assetId)
}
.mapValues {
val (assetId, price) = it
NFTSalePrice.Value(
assetId = assetId,
value = price.value,
symbol = price.symbol,
source = StatusSource.CACHE,
)
}
}
private suspend fun List<SdkNFTCollection>.mergeWithStoredAssets(network: Network): List<SdkNFTCollection> {
val storedCollections =
getNFTPersistenceStore(network)
.getCollectionsSync()
.orEmpty()
.associateBy(NFTCollection::identifier)
return this.map { updatedCollection ->
if (!storedCollections.containsKey(updatedCollection.identifier)) {
// if there is no collection in cache, then write a new one
updatedCollection
} else if (updatedCollection.assets.isNotEmpty()) {
// if there are assets in a new collection, then write this
updatedCollection
} else {
// otherwise, just update a stored collection fields
storedCollections[updatedCollection.identifier]
?.copy(
name = updatedCollection.name,
count = updatedCollection.count,
description = updatedCollection.description,
logoUrl = updatedCollection.logoUrl,
)
?: updatedCollection
}
}
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.data.nft.di
import com.tangem.data.nft.DefaultNFTRepository
import com.tangem.domain.nft.repository.NFTRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface NFTDataModule {
@Binds
@Singleton
fun bindNFTRepository(repository: DefaultNFTRepository): NFTRepository
}