From ef19a3697d8d70d6d1c9705a2a8150145e63846e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jul 2023 12:15:26 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../local/token/FileUserTokensStore.kt | 13 +- .../datasource/local/token/UserTokensStore.kt | 7 +- data/tokens/build.gradle.kts | 20 ++- .../tangem/data/tokens/di/TokensDataModule.kt | 17 +- .../com/tangem/data/tokens/mock/MockTokens.kt | 33 ++-- .../repository/DefaultTokensRepository.kt | 151 ++++++++++++++++++ .../tokens/repository/MockTokensRepository.kt | 30 ---- .../data/tokens/utils/CardTokensFactory.kt | 1 - .../tokens/utils/ResponseTokensFactory.kt | 1 - .../tokens/utils/UserTokensResponseFactory.kt | 1 - 10 files changed, 202 insertions(+), 72 deletions(-) create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt delete mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt index 5b12b188de..3c6e41fd4a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/FileUserTokensStore.kt @@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.files.FileReader import com.tangem.datasource.local.datastore.FileDataStore +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow internal class FileUserTokensStore(fileReader: FileReader, moshi: Moshi) : UserTokensStore { @@ -14,15 +15,15 @@ internal class FileUserTokensStore(fileReader: FileReader, moshi: Moshi) : UserT adapter = moshi.adapter(UserTokensResponse::class.java), ) - override fun get(userWalletId: String): Flow { - return store.get(userWalletId) + override fun get(userWalletId: UserWalletId): Flow { + return store.get(userWalletId.stringValue) } - override suspend fun getSyncOrNull(userWalletId: String): UserTokensResponse? { - return store.getSyncOrNull(userWalletId) + override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? { + return store.getSyncOrNull(userWalletId.stringValue) } - override suspend fun store(userWalletId: String, tokens: UserTokensResponse) { - store.store(userWalletId, tokens) + override suspend fun store(userWalletId: UserWalletId, tokens: UserTokensResponse) { + store.store(userWalletId.stringValue, tokens) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt index 43f2f1e64a..8b64c93767 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt @@ -1,13 +1,14 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow interface UserTokensStore { - fun get(userWalletId: String): Flow + fun get(userWalletId: UserWalletId): Flow - suspend fun getSyncOrNull(userWalletId: String): UserTokensResponse? + suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? - suspend fun store(userWalletId: String, tokens: UserTokensResponse) + suspend fun store(userWalletId: UserWalletId, tokens: UserTokensResponse) } \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 9fa03694fe..3a9ee305bf 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -10,25 +10,35 @@ android { } dependencies { + + /** Project - Domain */ implementation(projects.domain.core) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.demo) implementation(projects.domain.wallets.models) + /** Project - Data */ implementation(projects.core.datasource) + implementation(projects.data.common) + /** Project - Utils */ + implementation(projects.core.utils) // FIXME: For blockchain extensions, remove after refactoring implementation(projects.domain.legacy) - implementation(deps.kotlin.coroutines) - implementation(deps.arrow.core) + /** Tangem SDKs */ + implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) + /** DI */ implementation(deps.hilt.core) kapt(deps.hilt.kapt) + /** Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + implementation(deps.moshi.kotlin) + implementation(deps.jodatime) implementation(deps.timber) - - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 85b6f06d93..2b512574c4 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -1,11 +1,16 @@ package com.tangem.data.tokens.di +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.tokens.repository.DefaultTokensRepository import com.tangem.data.tokens.repository.MockNetworksRepository import com.tangem.data.tokens.repository.MockQuotesRepository -import com.tangem.data.tokens.repository.MockTokensRepository +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,7 +23,15 @@ internal object TokensDataModule { @Provides @Singleton - fun provideTokensRepository(): TokensRepository = MockTokensRepository() + fun provideTokensRepository( + tangemTechApi: TangemTechApi, + userTokensStore: UserTokensStore, + userWalletsStore: UserWalletsStore, + cacheRegistry: CacheRegistry, + dispatchers: CoroutineDispatcherProvider, + ): TokensRepository { + return DefaultTokensRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers) + } @Provides @Singleton diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt index 3cf3584e68..ac1c0809ea 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt @@ -116,27 +116,14 @@ internal object MockTokens { derivationPath = null, ) - val tokens get() = mapOf( - UserWalletId(stringValue = "123") to setOf( - token1, token2, token3, token4, token5, - token6, token7, token8, token9, token10, - ), - UserWalletId(stringValue = "321") to setOf(token1, token2, token3), - UserWalletId(stringValue = "42") to setOf(token7, token8, token9, token10), - UserWalletId(stringValue = "24") to setOf(token4, token5, token6), - ) - - val isGrouped get() = mapOf( - UserWalletId(stringValue = "123") to true, - UserWalletId(stringValue = "321") to false, - UserWalletId(stringValue = "42") to false, - UserWalletId(stringValue = "24") to true, - ) - - val isSortedByBalance get() = mapOf( - UserWalletId(stringValue = "123") to true, - UserWalletId(stringValue = "321") to false, - UserWalletId(stringValue = "42") to true, - UserWalletId(stringValue = "24") to false, - ) + val tokens + get() = mapOf( + UserWalletId(stringValue = "123") to setOf( + token1, token2, token3, token4, token5, + token6, token7, token8, token9, token10, + ), + UserWalletId(stringValue = "321") to setOf(token1, token2, token3), + UserWalletId(stringValue = "42") to setOf(token7, token8, token9, token10), + UserWalletId(stringValue = "24") to setOf(token4, token5, token6), + ) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt new file mode 100644 index 0000000000..9d4a246327 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt @@ -0,0 +1,151 @@ +package com.tangem.data.tokens.repository + +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.tokens.utils.CardTokensFactory +import com.tangem.data.tokens.utils.ResponseTokensFactory +import com.tangem.data.tokens.utils.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +internal class DefaultTokensRepository( + private val tangemTechApi: TangemTechApi, + private val userTokensStore: UserTokensStore, + private val userWalletsStore: UserWalletsStore, + private val cacheRegistry: CacheRegistry, + private val dispatchers: CoroutineDispatcherProvider, +) : TokensRepository { + + private val demoConfig = DemoConfig() + private val responseTokensFactory = ResponseTokensFactory(demoConfig) + private val cardTokensFactory = CardTokensFactory(demoConfig) + private val userTokensResponseFactory = UserTokensResponseFactory() + + override suspend fun saveTokens( + userWalletId: UserWalletId, + tokens: Set, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ) = withContext(dispatchers.io) { + val response = userTokensResponseFactory.createUserTokensResponse( + tokens = tokens, + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + + storeAndPushTokens(userWalletId, response) + } + + override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> = channelFlow { + val userWallet = withContext(dispatchers.io) { + requireNotNull(userWalletsStore.getOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + } + + if (userWallet.isMultiCurrency) { + launch(dispatchers.io) { + getMultiCurrencyWalletTokens(userWallet).collectLatest(::send) + } + + launch(dispatchers.io) { + fetchTokensIfCacheExpired(userWallet, refresh) + } + } else { + send(getSingleCurrencyWalletTokens(userWallet)) + } + } + + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { + return userTokensStore.get(userWalletId) + .map { it.group == UserTokensResponse.GroupType.NETWORK } + .flowOn(dispatchers.io) + } + + override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { + return userTokensStore.get(userWalletId) + .map { it.sort == UserTokensResponse.SortType.BALANCE } + .flowOn(dispatchers.io) + } + + private fun getMultiCurrencyWalletTokens(userWallet: UserWallet): Flow> { + return userTokensStore.get(userWallet.walletId).map { storedTokens -> + responseTokensFactory.createTokens( + response = storedTokens, + card = userWallet.scanResponse.card, + ) + } + } + + private suspend fun getSingleCurrencyWalletTokens(userWallet: UserWallet): Set { + var response = userTokensStore.getSyncOrNull(userWallet.walletId) + + if (response == null) { + response = userTokensResponseFactory.createUserTokensResponse( + tokens = cardTokensFactory.createTokensForSingleCurrencyCard(userWallet.scanResponse), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + userTokensStore.store(userWallet.walletId, response) + } + + return responseTokensFactory.createTokens(response, userWallet.scanResponse.card) + } + + private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) { + cacheRegistry.invokeOnExpire( + key = getTokensCacheKey(userWallet.walletId), + skipCache = refresh, + block = { fetchTokens(userWallet) }, + ) + } + + private suspend fun fetchTokens(userWallet: UserWallet) { + try { + val response = tangemTechApi.getUserTokens(userWallet.walletId.stringValue) + + userTokensStore.store(userWallet.walletId, response) + } catch (e: Throwable) { + handleFetchTokensErrorOrThrow(userWallet, e) + } + } + + private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + userTokensStore.store(userWalletId, response) + tangemTechApi.saveUserTokens(userWalletId.stringValue, response) + } + + private suspend fun handleFetchTokensErrorOrThrow(userWallet: UserWallet, error: Throwable) { + val errorMessage = error.message ?: throw error + + if (NOT_FOUND_HTTP_CODE in errorMessage) { + val response = userTokensStore.getSyncOrNull(userWallet.walletId) + ?: userTokensResponseFactory.createUserTokensResponse( + tokens = cardTokensFactory.createDefaultTokensForMultiCurrencyCard(userWallet.scanResponse.card), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + tangemTechApi.saveUserTokens(userWallet.walletId.stringValue, response) + } else { + throw error + } + } + + private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" + + private companion object { + const val NOT_FOUND_HTTP_CODE = "404" + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt deleted file mode 100644 index 59e58db799..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.data.tokens.repository - -import com.tangem.data.tokens.mock.MockTokens -import com.tangem.domain.tokens.model.Token -import com.tangem.domain.tokens.repository.TokensRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -internal class MockTokensRepository : TokensRepository { - - override suspend fun saveTokens( - userWalletId: UserWalletId, - tokens: Set, - isGroupedByNetwork: Boolean, - isSortedByBalance: Boolean, - ) = Unit - - override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { - return flowOf(value = MockTokens.tokens[userWalletId]!!) - } - - override fun isTokensGrouped(userWalletId: UserWalletId): Flow { - return flowOf(value = MockTokens.isGrouped[userWalletId]!!) - } - - override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { - return flowOf(value = MockTokens.isSortedByBalance[userWalletId]!!) - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt index 671810e7e7..684a668b15 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardTokensFactory.kt @@ -11,7 +11,6 @@ import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken import com.tangem.domain.tokens.model.Token as DomainToken -@Suppress("unused") // TODO: Will be used in next MR internal class CardTokensFactory(private val demoConfig: DemoConfig) { fun createDefaultTokensForMultiCurrencyCard(card: CardDTO): Set { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt index 462a115635..5ca7fea388 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseTokensFactory.kt @@ -11,7 +11,6 @@ import com.tangem.domain.tokens.model.Token import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken -@Suppress("unused") // TODO: Will be used in next MR internal class ResponseTokensFactory(private val demoConfig: DemoConfig) { fun createTokens(response: UserTokensResponse, card: CardDTO): Set { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt index 6c895bee65..721dc275f6 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -5,7 +5,6 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.Token -@Suppress("unused") // TODO: Will be used in next MR internal class UserTokensResponseFactory { fun createUserTokensResponse(