Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-27 18:48:33 +04:00
parent 983b9b253d
commit 0ab1c7507a
18 changed files with 188 additions and 193 deletions

View file

@ -21,6 +21,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -145,4 +146,6 @@ interface ApplicationEntryPoint {
fun getColdUserWalletBuilderFactory(): ColdUserWalletBuilder.Factory
fun getApiConfigsManager(): ApiConfigsManager
fun getUserTokensResponseStore(): UserTokensResponseStore
}

View file

@ -38,6 +38,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
@ -232,6 +233,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val apiConfigsManager: ApiConfigsManager
get() = entryPoint.getApiConfigsManager()
private val userTokensResponseStore: UserTokensResponseStore
get() = entryPoint.getUserTokensResponseStore()
// endregion
private val appScope = MainScope()
@ -320,7 +324,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
}
derivationsFinder = DerivationsFinder(
appPreferencesStore = appPreferencesStore,
userTokensResponseStore = userTokensResponseStore,
dispatchers = dispatchers,
)
@ -370,6 +374,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
uiMessageSender = uiMessageSender,
onlineCardVerifier = onlineCardVerifier,
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
userTokensResponseStore = userTokensResponseStore,
),
),
)

View file

@ -18,9 +18,6 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.feedback.models.FeedbackEmailType
@ -258,10 +255,10 @@ internal class LegacyScanProcessor @Inject constructor(
onSuccess()
return
}
val appPrefStoreStore = store.inject(DaggerGraphState::appPreferencesStore)
val tokens = appPrefStoreStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
)
val userTokensResponseStore = store.inject(DaggerGraphState::userTokensResponseStore)
val tokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
if (scanResponse.card.isAccessCodeSet && tokens == null) {
store.dispatchDialogShow(
AppDialog.WalletAlreadyWasUsedDialog(

View file

@ -5,10 +5,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.models.scan.CardDTO
@ -25,7 +22,7 @@ internal data class BlockchainToDerive(
// FIXME: May be move to DI, currently unnecessary
internal class DerivationsFinder(
private val appPreferencesStore: AppPreferencesStore,
private val userTokensResponseStore: UserTokensResponseStore,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -67,10 +64,7 @@ internal class DerivationsFinder(
}
private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet<BlockchainToDerive> {
val responseTokens = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
)
?.tokens
val responseTokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)?.tokens
?: return hashSetOf()
return responseTokens.asSequence()

View file

@ -13,6 +13,7 @@ import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
@ -79,4 +80,5 @@ data class DaggerGraphState(
val onlineCardVerifier: OnlineCardVerifier? = null,
val cardArworksProvider: CardArtworksProvider? = null,
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
val userTokensResponseStore: UserTokensResponseStore? = null,
) : StateType

View file

@ -151,8 +151,6 @@ object PreferencesKeys {
fun getShouldShowInitialPermissionScreen(permission: String) =
booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission")
// endregion
fun getUserTokensKey(userWalletId: String) = stringPreferencesKey(name = "user_tokens_$userWalletId")
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -1,10 +1,13 @@
package com.tangem.datasource.local.token
import androidx.datastore.preferences.core.stringPreferencesKey
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Default implementation of [UserTokensResponseStore]
@ -17,9 +20,24 @@ internal class DefaultUserTokensResponseStore(
private val appPreferencesStore: AppPreferencesStore,
) : UserTokensResponseStore {
override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue),
override fun get(userWalletId: UserWalletId): Flow<UserTokensResponse?> {
return appPreferencesStore.getObject<UserTokensResponse>(
key = createPreferencesKey(userWalletId = userWalletId.stringValue),
)
}
override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = createPreferencesKey(userWalletId = userWalletId.stringValue),
)
}
override suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse) {
appPreferencesStore.storeObject(
key = createPreferencesKey(userWalletId = userWalletId.stringValue),
value = response,
)
}
private fun createPreferencesKey(userWalletId: String) = stringPreferencesKey(name = "user_tokens_$userWalletId")
}

View file

@ -2,6 +2,7 @@ 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
/**
* Store of [UserTokensResponse]
@ -10,6 +11,10 @@ import com.tangem.domain.wallets.models.UserWalletId
*/
interface UserTokensResponseStore {
fun get(userWalletId: UserWalletId): Flow<UserTokensResponse?>
/** Get [UserTokensResponse] synchronously by [userWalletId] or null */
suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse?
suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse)
}

View file

@ -4,17 +4,15 @@ import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.tokens.UserTokensBackwardCompatibility
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
class UserTokensSaver constructor(
class UserTokensSaver(
private val tangemTechApi: TangemTechApi,
private val appPreferencesStore: AppPreferencesStore,
private val userTokensResponseStore: UserTokensResponseStore,
private val dispatchers: CoroutineDispatcherProvider,
private val userTokensResponseAddressesEnricher: UserTokensResponseAddressesEnricher,
) {
@ -31,10 +29,8 @@ class UserTokensSaver constructor(
} else {
compatibleUserTokensResponse
}
appPreferencesStore.storeObject(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue),
value = enrichedUserTokensResponse,
)
userTokensResponseStore.store(userWalletId = userWalletId, response = enrichedUserTokensResponse)
}
suspend fun storeAndPush(userWalletId: UserWalletId, response: UserTokensResponse) {

View file

@ -8,7 +8,6 @@ import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.common.quote.DefaultQuotesFetcher
import com.tangem.data.common.quote.QuotesFetcher
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.demo.DemoConfig
@ -61,13 +60,13 @@ internal object DataCommonModule {
@Singleton
fun provideUserTokensSaver(
tangemTechApi: TangemTechApi,
appPreferencesStore: AppPreferencesStore,
userTokensResponseStore: UserTokensResponseStore,
dispatchers: CoroutineDispatcherProvider,
enricher: UserTokensResponseAddressesEnricher,
): UserTokensSaver {
return UserTokensSaver(
tangemTechApi = tangemTechApi,
appPreferencesStore = appPreferencesStore,
userTokensResponseStore = userTokensResponseStore,
dispatchers = dispatchers,
userTokensResponseAddressesEnricher = enricher,
)

View file

@ -1,51 +1,35 @@
package com.tangem.data.common.currency
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserTokensSaverTest {
private val tangemTechApi: TangemTechApi = mockk()
private val dataStore: DataStore<Preferences> = mockk(relaxed = true)
private val appPreferenceStore = AppPreferencesStore(
moshi = Moshi.Builder().build(),
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxed = true)
private val enricher: UserTokensResponseAddressesEnricher = mockk()
private val userTokensSaver: UserTokensSaver = UserTokensSaver(
tangemTechApi = tangemTechApi,
userTokensResponseStore = userTokensResponseStore,
userTokensResponseAddressesEnricher = enricher,
dispatchers = TestingCoroutineDispatcherProvider(),
preferencesDataStore = dataStore,
)
private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider()
private val userTokensResponseAddressesEnricher: UserTokensResponseAddressesEnricher = mockk()
private lateinit var userTokensSaver: UserTokensSaver
@Before
fun setup() {
coEvery { dataStore.data } returns flowOf(mockk(relaxed = true))
userTokensSaver = UserTokensSaver(
tangemTechApi = tangemTechApi,
appPreferencesStore = appPreferenceStore,
dispatchers = dispatchers,
userTokensResponseAddressesEnricher = userTokensResponseAddressesEnricher,
)
}
@After
fun tearDown() {
clearAllMocks()
@BeforeEach
fun resetMocks() {
clearMocks(tangemTechApi, userTokensResponseStore, enricher)
}
@Test
@ -58,57 +42,30 @@ class UserTokensSaverTest {
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
val enrichedResponse = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
sort = UserTokensResponse.SortType.MANUAL,
tokens = emptyList(),
)
coEvery { userTokensResponseAddressesEnricher(userWalletId, response) } returns enrichedResponse
coEvery { dataStore.updateData(any()) } returns mockk(relaxed = true)
coEvery { enricher(userWalletId, response) } returns enrichedResponse
// WHEN
userTokensSaver.store(userWalletId, response)
// THEN
coVerify {
dataStore.updateData(any())
coVerifyOrder {
enricher(userWalletId, response)
userTokensResponseStore.store(userWalletId, enrichedResponse)
}
coVerify(exactly = 0) {
coVerify(inverse = true) {
tangemTechApi.saveUserTokens(any(), any())
}
}
@Test
fun `GIVEN user wallet id and response WHEN storeAndPush THEN should store and push enriched response`() = runTest {
// GIVEN
val userWalletId = UserWalletId("1234567890abcdef")
val response = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
val enrichedResponse = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
coEvery { userTokensResponseAddressesEnricher(userWalletId, response) } returns enrichedResponse
coEvery { dataStore.updateData(any()) } returns mockk(relaxed = true)
coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns ApiResponse.Success(Unit)
// WHEN
userTokensSaver.storeAndPush(userWalletId, response)
// THEN
coVerify {
dataStore.updateData(any())
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
}
}
@Test
fun `GIVEN user wallet id and response WHEN push AND api call fails THEN should log error and call onFailSend`() =
runTest {
@ -123,14 +80,13 @@ class UserTokensSaverTest {
val enrichedResponse = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
sort = UserTokensResponse.SortType.MANUAL,
tokens = emptyList(),
)
val error = ApiResponseError.UnknownException(Exception("API Error"))
var onFailSendCalled = false
coEvery { userTokensResponseAddressesEnricher(userWalletId, response) } returns enrichedResponse
coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns
ApiResponse.Error(error) as ApiResponse<Unit>
coEvery { enricher(userWalletId, response) } returns enrichedResponse
coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse<Unit>
// WHEN
userTokensSaver.push(
@ -140,12 +96,42 @@ class UserTokensSaverTest {
)
// THEN
coVerify {
coVerifyOrder {
enricher(userWalletId, response)
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
}
coVerify(exactly = 0) {
dataStore.updateData(any())
}
assert(onFailSendCalled) { "onFailSend callback should be called when API call fails" }
}
@Test
fun `GIVEN user wallet id and response WHEN storeAndPush THEN should store and push enriched response`() = runTest {
// GIVEN
val userWalletId = UserWalletId("1234567890abcdef")
val response = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
val enrichedResponse = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
coEvery { enricher(userWalletId, response) } returns enrichedResponse
coEvery {
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
} returns ApiResponse.Success(Unit)
// WHEN
userTokensSaver.storeAndPush(userWalletId, response)
// THEN
coVerifyOrder {
enricher(userWalletId, response)
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
}
}
}

View file

@ -12,10 +12,7 @@ import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.managetokens.utils.TokenAddressesConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.extensions.canHandleBlockchain
import com.tangem.domain.common.extensions.supportedBlockchains
@ -35,7 +32,7 @@ import kotlinx.coroutines.withContext
internal class DefaultCustomTokensRepository(
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val appPreferencesStore: AppPreferencesStore,
private val userTokensResponseStore: UserTokensResponseStore,
private val walletManagersFacade: WalletManagersFacade,
private val excludedBlockchains: ExcludedBlockchains,
private val dispatchers: CoroutineDispatcherProvider,
@ -71,9 +68,8 @@ internal class DefaultCustomTokensRepository(
contractAddress: String?,
): Boolean {
return withContext(dispatchers.io) {
val storedCurrencies = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
)
val storedCurrencies = userTokensResponseStore.getSyncOrNull(userWalletId)
requireNotNull(storedCurrencies) {
"User tokens not found for user wallet [$userWalletId] while checking if currency is not added"
}
@ -230,9 +226,8 @@ internal class DefaultCustomTokensRepository(
contractAddress = currency.contractAddress,
)
}
val storedCurrencies = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
)
val storedCurrencies = userTokensResponseStore.getSyncOrNull(userWalletId)
requireNotNull(storedCurrencies) {
"User tokens not found for user wallet [$userWalletId] while removing currency"
}

View file

@ -17,9 +17,7 @@ import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.canHandleBlockchain
@ -46,7 +44,7 @@ internal class DefaultManageTokensRepository(
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
private val appPreferencesStore: AppPreferencesStore,
private val userTokensResponseStore: UserTokensResponseStore,
private val testnetTokensStorage: TestnetTokensStorage,
private val excludedBlockchains: ExcludedBlockchains,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
@ -251,9 +249,7 @@ internal class DefaultManageTokensRepository(
}
private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(key.stringValue),
)
return userTokensResponseStore.getSyncOrNull(userWalletId = key)
}
override suspend fun checkCurrencyUnsupportedState(

View file

@ -9,7 +9,7 @@ import com.tangem.data.managetokens.DefaultManageTokensRepository
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
@ -31,7 +31,7 @@ internal object ManageTokensDataModule {
tangemTechApi: TangemTechApi,
userWalletsStore: UserWalletsStore,
manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
appPreferencesStore: AppPreferencesStore,
userTokensResponseStore: UserTokensResponseStore,
testnetTokensStorage: TestnetTokensStorage,
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
@ -42,7 +42,7 @@ internal object ManageTokensDataModule {
tangemTechApi = tangemTechApi,
userWalletsStore = userWalletsStore,
manageTokensUpdateFetcher = manageTokensUpdateFetcher,
appPreferencesStore = appPreferencesStore,
userTokensResponseStore = userTokensResponseStore,
testnetTokensStorage = testnetTokensStorage,
excludedBlockchains = excludedBlockchains,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
@ -56,7 +56,7 @@ internal object ManageTokensDataModule {
fun provideCustomTokensRepository(
tangemTechApi: TangemTechApi,
userWalletsStore: UserWalletsStore,
appPreferencesStore: AppPreferencesStore,
userTokensResponseStore: UserTokensResponseStore,
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
@ -66,7 +66,7 @@ internal object ManageTokensDataModule {
return DefaultCustomTokensRepository(
tangemTechApi = tangemTechApi,
userWalletsStore = userWalletsStore,
appPreferencesStore = appPreferencesStore,
userTokensResponseStore = userTokensResponseStore,
walletManagersFacade = walletManagersFacade,
excludedBlockchains = excludedBlockchains,
dispatchers = dispatchers,

View file

@ -18,6 +18,7 @@ import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.extensions.canHandleBlockchain
import com.tangem.domain.common.extensions.canHandleToken
@ -58,6 +59,7 @@ internal class DefaultHotCryptoRepository(
private val userWalletsStore: UserWalletsStore,
private val tangemTechApi: TangemTechApi,
private val appPreferencesStore: AppPreferencesStore,
private val userTokensResponseStore: UserTokensResponseStore,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : HotCryptoRepository {
@ -103,9 +105,7 @@ internal class DefaultHotCryptoRepository(
private fun getWalletsWithTokensFlow(): Flow<Map<UserWallet, List<UserTokensResponse.Token>>> {
return userWalletsStore.userWallets.flatMapLatest { userWallets ->
val flows = userWallets.map { userWallet ->
appPreferencesStore.getObject<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue),
)
userTokensResponseStore.get(userWalletId = userWallet.walletId)
.map { userWallet to it?.tokens.orEmpty() }
}

View file

@ -8,7 +8,6 @@ import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.domain.onramp.repositories.LegacyTopUpRepository
import com.tangem.data.onramp.legacy.MercuryoTopUpRepository
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
@ -24,11 +23,9 @@ import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.onramp.repositories.HotCryptoRepository
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.onramp.repositories.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -104,6 +101,7 @@ internal object OnrampDataModule {
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
userTokensResponseStore: UserTokensResponseStore,
): HotCryptoRepository {
return DefaultHotCryptoRepository(
excludedBlockchains = excludedBlockchains,
@ -113,6 +111,7 @@ internal object OnrampDataModule {
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
analyticsEventHandler = analyticsEventHandler,
userTokensResponseStore = userTokensResponseStore,
)
}

View file

@ -10,6 +10,7 @@ import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckReposi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
@ -30,7 +31,7 @@ internal object TokensDataModule {
@Singleton
fun provideCurrenciesRepository(
tangemTechApi: TangemTechApi,
appPreferencesStore: AppPreferencesStore,
userTokensResponseStore: UserTokensResponseStore,
userWalletsStore: UserWalletsStore,
walletManagersFacade: WalletManagersFacade,
cacheRegistry: CacheRegistry,
@ -45,7 +46,7 @@ internal object TokensDataModule {
userWalletsStore = userWalletsStore,
walletManagersFacade = walletManagersFacade,
cacheRegistry = cacheRegistry,
appPreferencesStore = appPreferencesStore,
userTokensResponseStore = userTokensResponseStore,
expressServiceLoader = expressServiceLoader,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,

View file

@ -14,10 +14,7 @@ import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.error.DataError
@ -28,11 +25,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isLocked
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.models.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@ -45,11 +38,11 @@ internal class DefaultCurrenciesRepository(
private val userWalletsStore: UserWalletsStore,
private val walletManagersFacade: WalletManagersFacade,
private val cacheRegistry: CacheRegistry,
private val appPreferencesStore: AppPreferencesStore,
private val expressServiceLoader: ExpressServiceLoader,
private val dispatchers: CoroutineDispatcherProvider,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val userTokensSaver: UserTokensSaver,
private val userTokensResponseStore: UserTokensResponseStore,
excludedBlockchains: ExcludedBlockchains,
) : CurrenciesRepository {
@ -96,7 +89,10 @@ internal class DefaultCurrenciesRepository(
response = updatedResponse,
)
fetchExpressAssetsByNetworkIds(userWalletId, updatedResponse)
fetchExpressAssetsByNetworkIds(
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
userTokens = updatedResponse,
)
}
}
@ -119,7 +115,11 @@ internal class DefaultCurrenciesRepository(
userWalletId = userWalletId,
response = updatedResponse,
)
fetchExpressAssetsByNetworkIds(userWalletId, updatedResponse)
fetchExpressAssetsByNetworkIds(
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
userTokens = updatedResponse,
)
}
}
@ -205,7 +205,7 @@ internal class DefaultCurrenciesRepository(
override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return channelFlow {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
if (userWallet.isMultiCurrency) {
getMultiCurrencyWalletCurrenciesUpdates(userWalletId).collect(::send)
@ -222,13 +222,19 @@ internal class DefaultCurrenciesRepository(
refresh: Boolean,
): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(
userWallet.requireColdWallet().scanResponse,
)
fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency), refresh)
fetchExpressAssetsByNetworkIds(
userWallet = userWallet,
cryptoCurrencies = listOf(currency),
refresh = refresh,
)
currency
}
}
@ -238,7 +244,8 @@ internal class DefaultCurrenciesRepository(
refresh: Boolean,
): List<CryptoCurrency> {
return withContext(dispatchers.io) {
val scanResponse = getUserWallet(userWalletId).requireColdWallet().scanResponse
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
val scanResponse = userWallet.requireColdWallet().scanResponse
val currencies = if (scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = scanResponse)
@ -248,7 +255,12 @@ internal class DefaultCurrenciesRepository(
}
}
fetchExpressAssetsByNetworkIds(userWalletId, currencies, refresh)
fetchExpressAssetsByNetworkIds(
userWallet = userWallet,
cryptoCurrencies = currencies,
refresh = refresh,
)
currencies
}
}
@ -258,7 +270,7 @@ internal class DefaultCurrenciesRepository(
id: CryptoCurrency.ID,
): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(
@ -266,14 +278,14 @@ internal class DefaultCurrenciesRepository(
)
.find { it.id == id }
requireNotNull(currency) { "Unable to find currency with provided ID: $id" }
fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency))
fetchExpressAssetsByNetworkIds(userWallet, listOf(currency))
currency
}
}
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return channelFlow {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
getMultiCurrencyWalletCurrencies(userWallet)
@ -290,7 +302,7 @@ internal class DefaultCurrenciesRepository(
userWalletId: UserWalletId,
refresh: Boolean,
): List<CryptoCurrency> = withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
fetchTokensIfCacheExpired(userWallet, refresh)
@ -307,7 +319,7 @@ internal class DefaultCurrenciesRepository(
override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId) =
withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val storedTokens = requireNotNull(
@ -329,7 +341,7 @@ internal class DefaultCurrenciesRepository(
override suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency =
withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val response = requireNotNull(
@ -352,7 +364,7 @@ internal class DefaultCurrenciesRepository(
derivationPath: Network.DerivationPath,
): CryptoCurrency.Coin {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
@ -382,7 +394,7 @@ internal class DefaultCurrenciesRepository(
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
return channelFlow {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
if (userWallet.isMultiCurrency) {
getSavedUserTokensResponse(userWalletId)
@ -398,7 +410,7 @@ internal class DefaultCurrenciesRepository(
override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean> {
return channelFlow {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
if (userWallet.isMultiCurrency) {
getSavedUserTokensResponse(userWalletId)
@ -470,7 +482,7 @@ internal class DefaultCurrenciesRepository(
contractAddress: String,
networkId: String,
): CryptoCurrency.Token {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
val token = withContext(dispatchers.io) {
val foundToken = tangemTechApi.getCoins(
contractAddress = contractAddress,
@ -599,7 +611,7 @@ internal class DefaultCurrenciesRepository(
userTokensSaver.store(userWalletId, compatibleUserTokensResponse)
fetchExpressAssetsByNetworkIds(userWalletId, compatibleUserTokensResponse)
fetchExpressAssetsByNetworkIds(userWallet, compatibleUserTokensResponse)
}
private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet.Cold): Boolean {
@ -608,7 +620,7 @@ internal class DefaultCurrenciesRepository(
return demoConfig.isDemoCardId(userWallet.cardId) && response == null
}
private suspend fun fetchExpressAssetsByNetworkIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) {
val tokens = userTokens.tokens.map { token ->
LeastTokenInfo(
contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
@ -617,12 +629,12 @@ internal class DefaultCurrenciesRepository(
}
coroutineScope {
launch { expressServiceLoader.update(getUserWallet(userWalletId), tokens) }
launch { expressServiceLoader.update(userWallet, tokens) }
}
}
private suspend fun fetchExpressAssetsByNetworkIds(
userWalletId: UserWalletId,
userWallet: UserWallet,
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean = false,
) {
@ -634,11 +646,11 @@ internal class DefaultCurrenciesRepository(
)
}
cacheRegistry.invokeOnExpire(
key = getAssetsCacheKey(userWalletId),
key = getAssetsCacheKey(userWallet.walletId),
skipCache = refresh,
block = {
coroutineScope {
launch { expressServiceLoader.update(getUserWallet(userWalletId), tokens) }
launch { expressServiceLoader.update(userWallet, tokens) }
}
},
)
@ -648,9 +660,8 @@ internal class DefaultCurrenciesRepository(
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {
val userWalletId = userWallet.walletId
val response = appPreferencesStore.getObjectSyncOrNull(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
) ?: createDefaultUserTokensResponse(userWallet)
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
?: createDefaultUserTokensResponse(userWallet = userWallet)
if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId")
@ -666,20 +677,14 @@ internal class DefaultCurrenciesRepository(
private fun createDefaultUserTokensResponse(userWallet: UserWallet) =
userTokensResponseFactory.createUserTokensResponse(
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(
userWallet.requireColdWallet().scanResponse,
userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
private fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"
}
}
private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) {
val userWallet = getUserWallet(userWalletId)
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected)
}
@ -708,14 +713,10 @@ internal class DefaultCurrenciesRepository(
private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}"
private fun getSavedUserTokensResponse(key: UserWalletId): Flow<UserTokensResponse> {
return appPreferencesStore
.getObject<UserTokensResponse>(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue))
.filterNotNull()
return userTokensResponseStore.get(userWalletId = key).filterNotNull()
}
private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(key.stringValue),
)
return userTokensResponseStore.getSyncOrNull(userWalletId = key)
}
}