Updated on 2026-08-14
This commit is contained in:
parent
6016933480
commit
ef19a3697d
10 changed files with 202 additions and 72 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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]!!)
|
||||
}
|
||||
}
|
||||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue