Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-07 13:01:47 +03:00
parent dd186d6fbc
commit 3f5f3ef68b
23 changed files with 351 additions and 128 deletions

View file

@ -1,15 +1,15 @@
package com.tangem.data.tokens.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
import com.tangem.data.tokens.repository.DefaultNetworksRepository
import com.tangem.data.tokens.repository.DefaultTokensRepository
import com.tangem.data.tokens.repository.MockQuotesRepository
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.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.repository.TokensRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -24,14 +24,14 @@ internal object TokensDataModule {
@Provides
@Singleton
fun provideTokensRepository(
fun provideCurrenciesRepository(
tangemTechApi: TangemTechApi,
userTokensStore: UserTokensStore,
userWalletsStore: UserWalletsStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
): TokensRepository {
return DefaultTokensRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers)
): CurrenciesRepository {
return DefaultCurrenciesRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers)
}
@Provides

View file

@ -8,23 +8,27 @@ 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.core.error.DataError
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.TokensRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
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.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultTokensRepository(
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
private val userTokensStore: UserTokensStore,
private val userWalletsStore: UserWalletsStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
) : TokensRepository {
) : CurrenciesRepository {
private val demoConfig = DemoConfig()
private val responseCurrenciesFactory = ResponseCurrenciesFactory(demoConfig)
@ -37,6 +41,8 @@ internal class DefaultTokensRepository(
isGroupedByNetwork: Boolean,
isSortedByBalance: Boolean,
) = withContext(dispatchers.io) {
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
val response = userTokensResponseFactory.createUserTokensResponse(
currencies = currencies,
isGroupedByNetwork = isGroupedByNetwork,
@ -46,58 +52,72 @@ internal class DefaultTokensRepository(
storeAndPushTokens(userWalletId, response)
}
override suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
val userWallet = withContext(dispatchers.io) {
requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"
}
}
require(!userWallet.isMultiCurrency) {
"Single currency wallet excepted, but multi currency wallet was found: $userWalletId"
}
override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
}
}
override fun getMultiCurrencyWalletCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): Flow<Set<CryptoCurrency>> {
): Flow<Set<CryptoCurrency>> = channelFlow {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
getMultiCurrencyWalletCurrencies(userWallet).collect(::send)
}
launch(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh)
}
}
override suspend fun getMultiCurrencyWalletCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency = withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse.card)
}
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
return channelFlow {
val userWallet = withContext(dispatchers.io) {
requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"
}
}
require(userWallet.isMultiCurrency) {
"Multi currency wallet excepted, but single currency wallet was found: $userWalletId"
}
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
getMultiCurrencyWalletCurrencies(userWallet).collectLatest(::send)
}
launch(dispatchers.io) {
fetchTokensIfCacheExpired(userWallet, refresh)
userTokensStore.get(userWalletId)
.map { it.group == UserTokensResponse.GroupType.NETWORK }
.collect(::send)
}
}
}
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)
return channelFlow {
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
userTokensStore.get(userWalletId)
.map { it.sort == UserTokensResponse.SortType.BALANCE }
.collect(::send)
}
}
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<Set<CryptoCurrency>> {
return userTokensStore.get(userWallet.walletId).map { storedTokens ->
responseCurrenciesFactory.createTokens(
responseCurrenciesFactory.createCurrencies(
response = storedTokens,
card = userWallet.scanResponse.card,
)
@ -146,6 +166,39 @@ internal class DefaultTokensRepository(
}
}
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"
}
}
private suspend fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected)
}
private fun ensureIsCorrectUserWallet(userWallet: UserWallet, isMultiCurrencyWalletExpected: Boolean) {
val userWalletId = userWallet.walletId
val message = when {
!userWallet.isMultiCurrency && isMultiCurrencyWalletExpected -> {
"Multi currency wallet expected, but single currency wallet was found: $userWalletId"
}
userWallet.isMultiCurrency && !isMultiCurrencyWalletExpected -> {
"Single currency wallet expected, but multi currency wallet was found: $userWalletId"
}
else -> null
}
if (message != null) {
val error = DataError.UserWalletError.WrongUserWallet(message)
Timber.e(error)
throw error
}
}
private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}"
private companion object {

View file

@ -18,7 +18,10 @@ import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
internal class DefaultNetworksRepository(
@ -44,7 +47,9 @@ internal class DefaultNetworksRepository(
networks: Set<Network.ID>,
refresh: Boolean,
): Flow<Set<NetworkStatus>> = channelFlow {
networksStatuses.collectLatest(::send)
launch(dispatchers.io) {
networksStatuses.collect(::send)
}
launch(dispatchers.io) {
fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh)
@ -99,7 +104,7 @@ internal class DefaultNetworksRepository(
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
return responseCurrenciesFactory.createTokens(response, userWallet.scanResponse.card)
return responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse.card)
}
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId"

View file

@ -40,7 +40,7 @@ internal class NetworkStatusFactory {
is CryptoCurrency.Coin -> amounts.singleOrNull { it is CryptoCurrencyAmount.Coin }
is CryptoCurrency.Token -> amounts.singleOrNull {
it is CryptoCurrencyAmount.Token &&
it.id == getTokenIdString(currency) &&
it.id == getTokenIdString(currency.id) &&
it.tokenContractAddress == currency.contractAddress
}
}?.value

View file

@ -12,11 +12,23 @@ import com.tangem.blockchain.common.Token as SdkToken
internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
fun createTokens(response: UserTokensResponse, card: CardDTO): Set<CryptoCurrency> {
return response.tokens.mapNotNull { createToken(it, card) }.toSet()
fun createCurrency(currencyId: CryptoCurrency.ID, response: UserTokensResponse, card: CardDTO): CryptoCurrency {
val responseTokenId = getTokenIdString(currencyId)
val token = requireNotNull(response.tokens.firstOrNull { it.id == responseTokenId }) {
"Unable find a token with provided ID: $responseTokenId"
}
return requireNotNull(createCurrency(token, card)) {
"Unable to create a currency with provided ID: $currencyId"
}
}
private fun createToken(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? {
fun createCurrencies(response: UserTokensResponse, card: CardDTO): Set<CryptoCurrency> {
return response.tokens.mapNotNull { createCurrency(it, card) }.toSet()
}
private fun createCurrency(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? {
var blockchain = Blockchain.fromNetworkId(responseToken.networkId)
if (blockchain == null || blockchain == Blockchain.Unknown) {
Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}")

View file

@ -49,9 +49,14 @@ internal fun getTokenId(blockchain: Blockchain, token: SdkToken): CryptoCurrency
return getTokenOrCoinId(blockchain, token)
}
internal fun getTokenIdString(currency: CryptoCurrency): String? {
return currency.id.value.substringAfter(TOKEN_ID_DELIMITER)
.takeUnless { currency is CryptoCurrency.Token && currency.isCustom }
internal fun getTokenIdString(currencyId: CryptoCurrency.ID): String? {
val idValue = currencyId.value
return if (idValue.startsWith(CUSTOM_TOKEN_ID_PREFIX)) {
null
} else {
idValue.substringAfter(TOKEN_ID_DELIMITER)
}
}
internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {

View file

@ -30,7 +30,7 @@ internal class UserTokensResponseFactory {
val blockchain = getBlockchain(currency.networkId)
return UserTokensResponse.Token(
id = getTokenIdString(currency),
id = getTokenIdString(currency.id),
networkId = blockchain.toNetworkId(),
derivationPath = currency.derivationPath,
name = currency.name,