Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-05 10:35:43 +04:00
parent eb96c3be40
commit def92569d3
13 changed files with 196 additions and 73 deletions

View file

@ -12,6 +12,10 @@ android {
namespace = "com.tangem.data.nft"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Project - Data */
@ -53,4 +57,8 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
testImplementation(projects.test.core)
testImplementation(projects.common.test)
testRuntimeOnly(deps.test.junit5.engine)
}

View file

@ -1,5 +1,6 @@
package com.tangem.data.nft
import android.content.Context
import android.content.res.Resources
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
@ -26,15 +27,19 @@ 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.nft.repository.NFTRepository
import com.tangem.domain.nft.utils.NFTCleaner
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
@ -49,9 +54,10 @@ internal class DefaultNFTRepository @Inject constructor(
private val userWalletsStore: UserWalletsStore,
private val networkFactory: NetworkFactory,
private val excludedBlockchains: ExcludedBlockchains,
resources: Resources,
) : NFTRepository {
@ApplicationContext private val context: Context,
) : NFTRepository, NFTCleaner {
private val resources: Resources by lazy { context.resources }
private val networkJobs = ConcurrentHashMap<Network, JobHolder>()
private val collectionJobs = ConcurrentHashMap<NFTCollection.Identifier, JobHolder>()
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
@ -218,10 +224,22 @@ internal class DefaultNFTRepository @Inject constructor(
assetIdentifier = assetIdConverter.convertBack(assetIdentifier),
)
override suspend fun clearCache(userWalletId: UserWalletId, networks: List<Network>) {
networks.forEach {
getNFTPersistenceStore(userWalletId, it).clear()
getNFTRuntimeStore(userWalletId, it).clear()
// NFTCleaner implementation
override suspend fun invoke(userWalletId: UserWalletId, networks: Set<Network>) {
if (networks.isEmpty()) {
Timber.d("No networks to clear for wallet: $userWalletId")
return
}
networks.forEach { network ->
runSuspendCatching {
getNFTPersistenceStore(userWalletId = userWalletId, network = network).clear()
// FIXME: nftRuntimeStore is created with only network, so clearing it may affect other wallets
// nftRuntimeStoreFactory.provide(network = network).clear()
}
.onFailure { throwable ->
Timber.e(throwable, "Failed to clear NFT data for network $network for wallet: $userWalletId")
}
}
}

View file

@ -1,45 +1,23 @@
package com.tangem.data.nft.di
import android.content.Context
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.nft.DefaultNFTRepository
import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory
import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.domain.nft.utils.NFTCleaner
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NFTDataModule {
internal interface NFTDataModule {
@Provides
@Binds
@Singleton
fun provideNFTRepository(
@ApplicationContext context: Context,
nftPersistenceStoreFactory: NFTPersistenceStoreFactory,
nftRuntimeStoreFactory: NFTRuntimeStoreFactory,
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
userWalletsStore: UserWalletsStore,
networkFactory: NetworkFactory,
): NFTRepository = DefaultNFTRepository(
nftPersistenceStoreFactory = nftPersistenceStoreFactory,
nftRuntimeStoreFactory = nftRuntimeStoreFactory,
walletManagersFacade = walletManagersFacade,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
userWalletsStore = userWalletsStore,
networkFactory = networkFactory,
resources = context.resources,
)
fun bindNFTRepository(defaultNFTRepository: DefaultNFTRepository): NFTRepository
@Binds
@Singleton
fun bindNFTCleaner(defaultNFTRepository: DefaultNFTRepository): NFTCleaner
}

View file

@ -0,0 +1,78 @@
package com.tangem.data.nft
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
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.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class NFTCleanerTest {
private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory = mockk()
private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory = mockk()
private val nftCleaner = DefaultNFTRepository(
nftPersistenceStoreFactory = nftPersistenceStoreFactory,
nftRuntimeStoreFactory = nftRuntimeStoreFactory,
walletManagersFacade = mockk(),
dispatchers = mockk(),
userWalletsStore = mockk(),
networkFactory = mockk(),
excludedBlockchains = mockk(),
context = mockk(),
)
private val userWalletId = UserWalletId("011")
@AfterEach
fun tearDown() {
clearMocks(nftPersistenceStoreFactory, nftRuntimeStoreFactory)
}
@Test
fun `should call invoke with multiple networks`() = runTest {
// Arrange
val mockCryptoCurrencyFactory = MockCryptoCurrencyFactory()
val networks = mockCryptoCurrencyFactory.ethereumAndStellar.map(CryptoCurrency.Coin::network)
val persistenceByNetwork = networks.associateWith { mockk<NFTPersistenceStore>(relaxUnitFun = true) }
val runtimeByNetwork = networks.associateWith { mockk<NFTRuntimeStore>(relaxUnitFun = true) }
networks.forEach { network ->
every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceByNetwork[network]!!
every { nftRuntimeStoreFactory.provide(network) } returns runtimeByNetwork[network]!!
}
// Act
nftCleaner.invoke(userWalletId = userWalletId, networks = networks.toSet())
// Assert
coVerifyOrder {
networks.forEach { network ->
nftPersistenceStoreFactory.provide(userWalletId, network)
persistenceByNetwork[network]!!.clear()
// nftRuntimeStoreFactory.provide(network)
// runtimeByNetwork[network]!!.clear()
}
}
}
@Test
fun `should handle empty networks set`() = runTest {
// Act
nftCleaner.invoke(userWalletId, emptySet())
// Assert
coVerify(inverse = true) {
nftPersistenceStoreFactory.provide(userWalletId = any(), network = any())
nftRuntimeStoreFactory.provide(network = any())
}
}
}