Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-25 12:15:26 +03:00
parent 6016933480
commit ef19a3697d
10 changed files with 202 additions and 72 deletions

View file

@ -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<UserTokensResponse> {
return store.get(userWalletId)
override fun get(userWalletId: UserWalletId): Flow<UserTokensResponse> {
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)
}
}

View file

@ -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<UserTokensResponse>
fun get(userWalletId: UserWalletId): Flow<UserTokensResponse>
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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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),
)
}

View file

@ -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<Token>,
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<Set<Token>> = 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<Boolean> {
return userTokensStore.get(userWalletId)
.map { it.group == UserTokensResponse.GroupType.NETWORK }
.flowOn(dispatchers.io)
}
override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean> {
return userTokensStore.get(userWalletId)
.map { it.sort == UserTokensResponse.SortType.BALANCE }
.flowOn(dispatchers.io)
}
private fun getMultiCurrencyWalletTokens(userWallet: UserWallet): Flow<Set<Token>> {
return userTokensStore.get(userWallet.walletId).map { storedTokens ->
responseTokensFactory.createTokens(
response = storedTokens,
card = userWallet.scanResponse.card,
)
}
}
private suspend fun getSingleCurrencyWalletTokens(userWallet: UserWallet): Set<Token> {
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"
}
}

View file

@ -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<Token>,
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
) = Unit
override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow<Set<Token>> {
return flowOf(value = MockTokens.tokens[userWalletId]!!)
}
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
return flowOf(value = MockTokens.isGrouped[userWalletId]!!)
}
override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean> {
return flowOf(value = MockTokens.isSortedByBalance[userWalletId]!!)
}
}

View file

@ -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<DomainToken> {

View file

@ -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<Token> {

View file

@ -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(