Updated on 2026-08-14
This commit is contained in:
parent
89b3e67f64
commit
296df28f0a
31 changed files with 958 additions and 0 deletions
1
data/nft/.gitignore
vendored
Normal file
1
data/nft/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
51
data/nft/build.gradle.kts
Normal file
51
data/nft/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue