Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-11 11:07:03 +03:00
commit f0d03e3648
286 changed files with 6353 additions and 5898 deletions

View file

@ -12,13 +12,11 @@ android {
dependencies {
/** Project - Domain */
implementation(projects.domain.core)
implementation(projects.domain.appTheme)
implementation(projects.domain.appTheme.models)
/** Project - Data */
implementation(projects.core.datasource)
implementation(projects.data.common)
/** Project - Utils */
implementation(projects.core.utils)
@ -29,6 +27,4 @@ dependencies {
/** Other */
implementation(deps.kotlin.coroutines)
implementation(deps.timber)
implementation(deps.jodatime)
}

View file

@ -0,0 +1,35 @@
package com.tangem.data.apptheme
import com.tangem.datasource.local.apptheme.AppThemeModeStore
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
internal class DefaultAppThemeModeRepository(
private val appThemeModeStore: AppThemeModeStore,
private val dispatchers: CoroutineDispatcherProvider,
) : AppThemeModeRepository {
override fun getAppThemeMode(): Flow<AppThemeMode> {
return channelFlow {
launch(dispatchers.io) {
if (appThemeModeStore.isEmpty()) {
appThemeModeStore.store(AppThemeMode.DEFAULT)
}
}
launch(dispatchers.io) {
appThemeModeStore.get().collect(::send)
}
}
}
override suspend fun changeAppThemeMode(mode: AppThemeMode) {
withContext(dispatchers.io) {
appThemeModeStore.store(mode)
}
}
}

View file

@ -1,19 +0,0 @@
package com.tangem.data.apptheme
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
internal class MockAppThemeModeRepository : AppThemeModeRepository {
private val appThemeModeFlow = MutableStateFlow(AppThemeMode.DEFAULT)
override fun getAppThemeMode(): Flow<AppThemeMode> {
return appThemeModeFlow
}
override suspend fun changeAppThemeMode(mode: AppThemeMode) {
appThemeModeFlow.value = mode
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.data.apptheme.di
import com.tangem.data.apptheme.MockAppThemeModeRepository
import com.tangem.data.apptheme.DefaultAppThemeModeRepository
import com.tangem.datasource.local.apptheme.AppThemeModeStore
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -14,7 +16,10 @@ internal object AppThemeModeDataModule {
@Provides
@Singleton
fun provideAppThemeModeRepository(): AppThemeModeRepository {
return MockAppThemeModeRepository()
fun provideAppThemeModeRepository(
appThemeModeStore: AppThemeModeStore,
dispatchers: CoroutineDispatcherProvider,
): AppThemeModeRepository {
return DefaultAppThemeModeRepository(appThemeModeStore, dispatchers)
}
}

View file

@ -17,6 +17,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets.models)
/** Project - Data */

View file

@ -2,14 +2,17 @@ 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.DefaultMarketCryptoCurrencyRepository
import com.tangem.data.tokens.repository.DefaultNetworksRepository
import com.tangem.data.tokens.repository.DefaultQuotesRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.token.UserMarketCoinsStore
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.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -30,10 +33,18 @@ internal object TokensDataModule {
tangemTechApi: TangemTechApi,
userTokensStore: UserTokensStore,
userWalletsStore: UserWalletsStore,
userMarketCoinsStore: UserMarketCoinsStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
): CurrenciesRepository {
return DefaultCurrenciesRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers)
return DefaultCurrenciesRepository(
tangemTechApi = tangemTechApi,
userTokensStore = userTokensStore,
userWalletsStore = userWalletsStore,
userMarketCoinsStore = userMarketCoinsStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
)
}
@Provides
@ -46,11 +57,11 @@ internal object TokensDataModule {
dispatchers: CoroutineDispatcherProvider,
): QuotesRepository {
return DefaultQuotesRepository(
tangemTechApi,
quotesStore,
selectedAppCurrencyStore,
cacheRegistry,
dispatchers,
tangemTechApi = tangemTechApi,
quotesStore = quotesStore,
selectedAppCurrencyStore = selectedAppCurrencyStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
)
}
@ -64,11 +75,19 @@ internal object TokensDataModule {
dispatchers: CoroutineDispatcherProvider,
): NetworksRepository {
return DefaultNetworksRepository(
walletManagersFacade,
userWalletsStore,
userTokensStore,
cacheRegistry,
dispatchers,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
userTokensStore = userTokensStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideDefaultMarketCoinsRepository(
userMarketCoinsStore: UserMarketCoinsStore,
): MarketCryptoCurrencyRepository {
return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore)
}
}

View file

@ -1,17 +1,19 @@
package com.tangem.data.tokens.repository
import com.tangem.blockchain.common.Blockchain
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.utils.CardCurrenciesFactory
import com.tangem.data.tokens.utils.ResponseCurrenciesFactory
import com.tangem.data.tokens.utils.UserTokensResponseFactory
import com.tangem.data.tokens.utils.*
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserMarketCoinsStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -27,6 +29,7 @@ internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
private val userTokensStore: UserTokensStore,
private val userWalletsStore: UserWalletsStore,
private val userMarketCoinsStore: UserMarketCoinsStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
) : CurrenciesRepository {
@ -53,6 +56,53 @@ internal class DefaultCurrenciesRepository(
storeAndPushTokens(userWalletId, response)
}
override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
return withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = userTokensStore.getSyncOrNull(userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
val newCoins = createCoinsForNewTokens(
userWalletId = userWalletId,
newTokens = currencies.filterIsInstance<CryptoCurrency.Token>(),
savedCurrencies = savedCurrencies.tokens,
)
val newCurrencies = newCoins + currencies
storeAndPushTokens(
userWalletId = userWalletId,
response = savedCurrencies.copy(
tokens = savedCurrencies.tokens + newCurrencies.map(userTokensResponseFactory::createResponseToken),
),
)
}
}
private suspend fun createCoinsForNewTokens(
userWalletId: UserWalletId,
newTokens: List<CryptoCurrency.Token>,
savedCurrencies: List<UserTokensResponse.Token>,
): List<CryptoCurrency.Coin> {
return newTokens
.filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins
.mapNotNull {
CryptoCurrencyFactory().createCoin(
blockchain = getBlockchain(networkId = it.network.id),
derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider,
)
}
}
private fun List<UserTokensResponse.Token>.hasCoinForToken(token: CryptoCurrency.Token): Boolean {
return any {
val blockchain = getBlockchain(networkId = token.network.id)
it.id == getCoinId(blockchain).rawCurrencyId
}
}
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) =
withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
@ -69,6 +119,23 @@ internal class DefaultCurrenciesRepository(
)
}
override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
return withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = userTokensStore.getSyncOrNull(userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" },
)
val tokens = currencies.map(userTokensResponseFactory::createResponseToken)
storeAndPushTokens(
userWalletId = userWalletId,
response = savedCurrencies.copy(
tokens = savedCurrencies.tokens.filterNot(tokens::contains),
),
)
}
}
override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
@ -126,6 +193,27 @@ internal class DefaultCurrenciesRepository(
responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse.card)
}
override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val storedCoin = storedTokens.tokens.find { it.networkId == Blockchain.fromId(networkId.value).toNetworkId() }
?: error("Coin in this network $networkId not found")
val coin = responseCurrenciesFactory.createCurrency(
responseToken = storedCoin,
card = userWallet.scanResponse.card,
)
return coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
}
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
return channelFlow {
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
@ -172,6 +260,7 @@ internal class DefaultCurrenciesRepository(
val response = tangemTechApi.getUserTokens(userWallet.walletId.stringValue)
userTokensStore.store(userWallet.walletId, response)
fetchUserMarketCoinsByIds(userWallet.walletId, response)
} catch (e: Throwable) {
handleFetchTokensErrorOrThrow(userWallet, e)
}
@ -182,6 +271,17 @@ internal class DefaultCurrenciesRepository(
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
}
private suspend fun fetchUserMarketCoinsByIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
try {
val response = tangemTechApi.getCoins(
networkIds = userTokens.tokens.joinToString(separator = ",") { it.networkId },
)
userMarketCoinsStore.store(userWalletId, response)
} catch (e: Throwable) {
Timber.e("Unable to fetch user market coins for: ${userWalletId.stringValue} ${e.message}")
}
}
private suspend fun handleFetchTokensErrorOrThrow(userWallet: UserWallet, error: Throwable) {
val errorMessage = error.message ?: throw error

View file

@ -0,0 +1,18 @@
package com.tangem.data.tokens.repository
import com.tangem.datasource.local.token.UserMarketCoinsStore
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.wallets.models.UserWalletId
class DefaultMarketCryptoCurrencyRepository(
private val userMarketCoinsStore: UserMarketCoinsStore,
) : MarketCryptoCurrencyRepository {
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean {
return userMarketCoinsStore.getSyncOrNull(userWalletId)?.coins
?.firstOrNull { it.id == cryptoCurrencyId.rawCurrencyId }
?.networks
?.firstOrNull { it.networkId == cryptoCurrencyId.rawNetworkId }?.exchangeable ?: false
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
@ -68,25 +69,29 @@ internal class DefaultNetworksRepository(
networks: Set<Network.ID>,
refresh: Boolean,
) {
cacheRegistry.invokeOnExpire(
key = getNetworksStatusesCacheKey(userWalletId),
skipCache = refresh,
block = { fetchNetworksStatuses(userWalletId, networks) },
)
}
private suspend fun fetchNetworksStatuses(userWalletId: UserWalletId, networks: Set<Network.ID>) {
coroutineScope {
networks
.map { networkId ->
async {
fetchNetworkStatus(userWalletId, networkId)
fetchNetworkStatusIfCacheExpired(userWalletId, networkId, refresh)
}
}
.awaitAll()
}
}
private suspend fun fetchNetworkStatusIfCacheExpired(
userWalletId: UserWalletId,
networkId: Network.ID,
refresh: Boolean,
) {
cacheRegistry.invokeOnExpire(
key = getNetworksStatusesCacheKey(userWalletId, networkId),
skipCache = refresh,
block = { fetchNetworkStatus(userWalletId, networkId) },
)
}
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) {
val currencies = getCurrencies(userWalletId)
.asSequence()
@ -97,6 +102,17 @@ internal class DefaultNetworksRepository(
networkId = networkId,
extraTokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
// Invalidate cache key if wallet manager update failed
when (result) {
is UpdateWalletManagerResult.Verified,
is UpdateWalletManagerResult.NoAccount,
-> Unit
is UpdateWalletManagerResult.Unreachable,
is UpdateWalletManagerResult.MissedDerivation,
-> cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, networkId))
}
val networkStatus = networkStatusFactory.createNetworkStatus(
networkId = networkId,
result = result,
@ -126,5 +142,7 @@ internal class DefaultNetworksRepository(
}
}
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId"
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, nerworkId: Network.ID): String {
return "network_status_${userWalletId}_${nerworkId.value}"
}
}

View file

@ -2,9 +2,9 @@ package com.tangem.data.tokens.utils
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.PendingTransaction
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
@ -31,7 +31,6 @@ internal class NetworkStatusFactory {
address = getNetworkAddress(result.defaultAddress, result.addresses),
amounts = formatAmounts(result.currenciesAmounts, currencies),
pendingTransactions = formatTransactions(
networksAddresses = result.addresses,
transactions = result.currentTransactions,
currencies = currencies,
),
@ -67,10 +66,9 @@ internal class NetworkStatusFactory {
}
private fun formatTransactions(
networksAddresses: Set<String>,
transactions: Set<CryptoCurrencyTransaction>,
currencies: Set<CryptoCurrency>,
): Map<CryptoCurrency.ID, Set<PendingTransaction>> {
): Map<CryptoCurrency.ID, Set<TxHistoryItem>> {
if (transactions.isEmpty()) return emptyMap()
return currencies
@ -87,48 +85,13 @@ internal class NetworkStatusFactory {
}
}
currency.id to createCurrentTransactions(networksAddresses, currencyTransactions)
currency.id to createCurrentTransactions(currencyTransactions)
}
.toMap()
}
private fun createCurrentTransactions(
networksAddresses: Set<String>,
transactions: Set<CryptoCurrencyTransaction>,
): Set<PendingTransaction> {
return transactions.mapNotNullTo(hashSetOf()) { createCurrentTransaction(networksAddresses, it) }
}
private fun createCurrentTransaction(
networksAddresses: Set<String>,
transaction: CryptoCurrencyTransaction,
): PendingTransaction? {
val direction = when {
transaction.toAddress in networksAddresses -> PendingTransaction.Direction.Incoming(
fromAddress = transaction.fromAddress,
)
transaction.fromAddress in networksAddresses -> PendingTransaction.Direction.Outgoing(
toAddress = transaction.toAddress,
)
else -> {
Timber.e(
"""
Unable to find transaction direction
|- To address: ${transaction.toAddress}
|- From address: ${transaction.fromAddress}
|- Network addresses: $networksAddresses
""".trimIndent(),
)
return null
}
}
return PendingTransaction(
amount = transaction.amount,
direction = direction,
sentAt = transaction.sentAt,
)
private fun createCurrentTransactions(transactions: Set<CryptoCurrencyTransaction>): Set<TxHistoryItem> {
return transactions.mapTo(hashSetOf()) { it.txHistoryItem }
}
private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set<String>): NetworkAddress {

View file

@ -28,7 +28,7 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
return response.tokens.mapNotNull { createCurrency(it, card) }
}
private fun createCurrency(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? {
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}")