Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-24 00:07:05 +03:00
commit edec3fcdff
391 changed files with 10988 additions and 3221 deletions

View file

@ -27,6 +27,7 @@ dependencies {
api(projects.domain.common)
api(projects.domain.models)
api(projects.domain.tokens)
api(projects.domain.wallets)
// endregion
// region Project - Data

View file

@ -36,6 +36,7 @@ internal class CryptoPortfolioConverter @AssistedInject constructor(
responseCryptoCurrenciesFactory.createCurrencies(
tokens = tokens,
userWallet = userWallet,
accountIndex = value.derivationIndex.toDerivationIndex(),
).toSet()
} else {
emptySet()

View file

@ -10,9 +10,9 @@ import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.repository.AccountsCRUDRepository
@ -42,7 +42,6 @@ internal object AccountDataModule {
accountsResponseStoreFactory: AccountsResponseStoreFactory,
userWalletsStore: UserWalletsStore,
userTokensSaver: UserTokensSaver,
eTagsStore: ETagsStore,
accountConverterFactoryContainer: AccountConverterFactoryContainer,
dispatchers: CoroutineDispatcherProvider,
): AccountsCRUDRepository {
@ -53,7 +52,7 @@ internal object AccountDataModule {
archivedAccountsStoreFactory = ArchivedAccountsStoreFactory,
userWalletsStore = userWalletsStore,
userTokensSaver = userTokensSaver,
eTagsStore = eTagsStore,
archivedAccountsETagStore = RuntimeStateStore(emptyMap()),
convertersContainer = accountConverterFactoryContainer,
dispatchers = dispatchers,
)

View file

@ -0,0 +1,18 @@
package com.tangem.data.account.di
import com.tangem.data.account.producer.DefaultSingleAccountProducer
import com.tangem.domain.account.producer.SingleAccountProducer
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface SingleAccountProducerFactoryModule {
@Binds
@Singleton
fun bindSingleAccountProducerFactory(impl: DefaultSingleAccountProducer.Factory): SingleAccountProducer.Factory
}

View file

@ -0,0 +1,23 @@
package com.tangem.data.account.di
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.supplier.SingleAccountSupplier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object SingleAccountSupplierModule {
@Provides
@Singleton
fun provideSingleAccountSupplier(factory: SingleAccountProducer.Factory): SingleAccountSupplier {
return object : SingleAccountSupplier(
factory = factory,
keyCreator = { "single_account_${it.accountId.value}" },
) {}
}
}

View file

@ -4,7 +4,6 @@ import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.account.utils.assignTokens
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.api.safeApiCall
@ -19,6 +18,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.utils.getSyncOrNull
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -51,18 +51,25 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
) : WalletAccountsFetcher, WalletAccountsSaver {
override suspend fun fetch(userWalletId: UserWalletId) {
override suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse {
val savedAccountsResponse = getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
val accountsResponse = fetchWalletAccounts(userWalletId, savedAccountsResponse)
?: return
if (accountsResponse.accounts.isEmpty()) {
initializeAccounts(userWalletId, accountsResponse)
} else if (accountsResponse.unassignedTokens.isNotEmpty()) {
assignTokens(userWalletId, accountsResponse)
return when {
accountsResponse.accounts.isEmpty() -> {
initializeAccounts(userWalletId, accountsResponse)
}
accountsResponse.unassignedTokens.isNotEmpty() -> {
assignTokens(userWalletId, accountsResponse)
}
else -> accountsResponse
}
}
override suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse? {
return getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
}
override suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
val store = getAccountsResponseStore(userWalletId = userWalletId)
@ -115,7 +122,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
private suspend fun fetchWalletAccounts(
userWalletId: UserWalletId,
savedAccountsResponse: GetWalletAccountsResponse?,
): GetWalletAccountsResponse? {
): GetWalletAccountsResponse {
return safeApiCall(
call = {
val apiResponse = withContext(dispatchers.io) {
@ -145,7 +152,10 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
)
}
private suspend fun initializeAccounts(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) {
private suspend fun initializeAccounts(
userWalletId: UserWalletId,
accountsResponse: GetWalletAccountsResponse,
): GetWalletAccountsResponse {
val response = defaultWalletAccountsResponseFactory.create(
userWalletId = userWalletId,
userTokensResponse = UserTokensResponse(
@ -156,14 +166,17 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
)
userTokensSaver.push(userWalletId = userWalletId, response = response.toUserTokensResponse())
val syncedResponse = push(userWalletId = userWalletId, accounts = response.accounts)
val syncedResponse = push(userWalletId = userWalletId, accounts = response.accounts) ?: response
if (syncedResponse != null) {
store(userWalletId = userWalletId, response = syncedResponse)
}
store(userWalletId = userWalletId, response = syncedResponse)
return syncedResponse
}
private suspend fun assignTokens(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) {
private suspend fun assignTokens(
userWalletId: UserWalletId,
accountsResponse: GetWalletAccountsResponse,
): GetWalletAccountsResponse {
val accountsResponseWithTokens = accountsResponse.assignTokens(userWalletId)
store(userWalletId = userWalletId, response = accountsResponseWithTokens)
@ -172,6 +185,8 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
userWalletId = userWalletId,
response = accountsResponseWithTokens.toUserTokensResponse(),
)
return accountsResponseWithTokens
}
private suspend fun getETag(userWalletId: UserWalletId): String? {

View file

@ -1,7 +1,6 @@
package com.tangem.data.account.fetcher
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponseError
@ -10,6 +9,7 @@ import com.tangem.datasource.api.common.response.isNetworkError
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.models.wallet.UserWalletId
import timber.log.Timber
@ -49,11 +49,13 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
savedAccountsResponse: GetWalletAccountsResponse?,
pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse?,
storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit,
): GetWalletAccountsResponse? {
): GetWalletAccountsResponse {
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
if (isResponseUpToDate) {
Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId")
return savedAccountsResponse
return requireNotNull(savedAccountsResponse) {
"Saved accounts response is null for wallet: $userWalletId"
}
}
var response = savedAccountsResponse ?: createDefaultResponse(userWalletId)

View file

@ -3,9 +3,9 @@ package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.some
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
@ -48,12 +48,16 @@ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor(
.map { response ->
if (response == null) return@map emptySet()
responseCryptoCurrenciesFactory.createCurrencies(
response = response.toUserTokensResponse(),
userWallet = userWallet,
).toSet()
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
responseCryptoCurrenciesFactory.createCurrencies(
tokens = accountDTO.tokens.orEmpty(),
userWallet = userWallet,
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
)
}
}
.onEmpty { emit(emptySet()) }
.distinctUntilChanged()
.flowOn(dispatchers.default)
}

View file

@ -0,0 +1,55 @@
package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.none
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.account.Account
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapNotNull
/**
* Default implementation of [SingleAccountProducer] that produces a flow of [Account.CryptoPortfolio]
* for a single account identified by [SingleAccountProducer.Params.accountId].
*
* It uses [SingleAccountListSupplier] to get the list of accounts and filters it to find the
* specific account.
*
* @property params Parameters containing the account ID for which the portfolio is produced.
* @property singleAccountListSupplier Supplier to get the list of accounts.
* @property dispatchers Coroutine dispatcher provider for managing threading.
*/
internal class DefaultSingleAccountProducer @AssistedInject constructor(
@Assisted val params: SingleAccountProducer.Params,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val dispatchers: CoroutineDispatcherProvider,
) : SingleAccountProducer {
override val fallback: Option<Account.CryptoPortfolio>
get() = none()
override fun produce(): Flow<Account.CryptoPortfolio> {
return singleAccountListSupplier(
params = SingleAccountListProducer.Params(userWalletId = params.accountId.userWalletId),
)
.mapNotNull { accountList ->
accountList.accounts.firstOrNull {
it is Account.CryptoPortfolio && params.accountId == it.accountId
} as? Account.CryptoPortfolio
}
.distinctUntilChanged()
.flowOn(dispatchers.default)
}
@AssistedFactory
interface Factory : SingleAccountProducer.Factory {
override fun create(params: SingleAccountProducer.Params): DefaultSingleAccountProducer
}
}

View file

@ -9,13 +9,16 @@ import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.store.ArchivedAccountsStore
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
import com.tangem.datasource.api.common.response.ETAG_HEADER
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.utils.getSyncOrNull
import com.tangem.domain.account.models.AccountList
@ -43,7 +46,7 @@ internal class DefaultAccountsCRUDRepository(
private val archivedAccountsStoreFactory: ArchivedAccountsStoreFactory,
private val userWalletsStore: UserWalletsStore,
private val userTokensSaver: UserTokensSaver,
private val eTagsStore: ETagsStore,
private val archivedAccountsETagStore: RuntimeStateStore<Map<String, String?>>,
private val convertersContainer: AccountConverterFactoryContainer,
private val dispatchers: CoroutineDispatcherProvider,
) : AccountsCRUDRepository {
@ -90,19 +93,38 @@ internal class DefaultAccountsCRUDRepository(
}
override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) {
val response = withContext(dispatchers.io) {
tangemTechApi.getWalletArchivedAccounts(
walletId = userWalletId.stringValue,
eTag = getETag(userWalletId),
).getOrThrow()
}
val eTag = archivedAccountsETagStore.getSyncOrNull()?.get(key = userWalletId.stringValue)
val store = getArchivedAccountsStore(userWalletId = userWalletId)
val converter = ArchivedAccountConverter(userWalletId = userWalletId)
val archivedAccounts = converter.convertList(input = response.accounts)
val response = safeApiCall(
call = {
val apiResponse = withContext(dispatchers.io) {
tangemTechApi.getWalletArchivedAccounts(
walletId = userWalletId.stringValue,
eTag = eTag,
)
}
store.store(value = archivedAccounts)
saveETag(userWalletId, apiResponse)
apiResponse.bind()
},
onError = {
if (it is HttpException && it.code == HttpException.Code.NOT_MODIFIED) {
null
} else {
throw it
}
},
)
if (response != null) {
val converter = ArchivedAccountConverter(userWalletId = userWalletId)
val archivedAccounts = converter.convertList(input = response.accounts)
store.store(value = archivedAccounts)
}
}
override suspend fun saveAccounts(accountList: AccountList) {
@ -154,9 +176,17 @@ internal class DefaultAccountsCRUDRepository(
return accountListResponse.wallet.totalAccounts.toOption()
}
override fun getTotalAccountsCount(userWalletId: UserWalletId): Flow<Option<Int>> {
override suspend fun getTotalActiveAccountsCountSync(userWalletId: UserWalletId): Option<Int> = option {
val accountListResponse = getAccountsResponseSync(userWalletId = userWalletId)
ensureNotNull(accountListResponse)
return accountListResponse.accounts.size.toOption()
}
override fun getTotalActiveAccountsCount(userWalletId: UserWalletId): Flow<Option<Int>> {
return getAccountsResponseStore(userWalletId = userWalletId).data
.map { it?.wallet?.totalAccounts.toOption() }
.map { it?.accounts?.size.toOption() }
}
override fun getUserWallet(userWalletId: UserWalletId): UserWallet {
@ -167,8 +197,12 @@ internal class DefaultAccountsCRUDRepository(
override fun getUserWalletsSync(): List<UserWallet> = userWalletsStore.userWalletsSync
private suspend fun getETag(userWalletId: UserWalletId): String? {
return eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
private suspend fun saveETag(userWalletId: UserWalletId, apiResponse: ApiResponse<*>) {
val eTag = apiResponse.headers[ETAG_HEADER]?.firstOrNull()
archivedAccountsETagStore.update {
it + (userWalletId.stringValue to eTag)
}
}
private suspend fun getAccountsResponseSync(userWalletId: UserWalletId): GetWalletAccountsResponse? {

View file

@ -9,11 +9,11 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.utils.assignTokens
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.utils.getSyncOrNull
import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.models.account.DerivationIndex
@ -78,6 +78,8 @@ internal class DefaultMainAccountTokensMigration(
},
)
store.updateData { updatedResponse }
userTokensSaver.push(
userWalletId = userWalletId,
response = updatedResponse.toUserTokensResponse(),

View file

@ -1,14 +1,16 @@
package com.tangem.data.account.utils
import com.tangem.data.account.converter.CryptoPortfolioConverter
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import javax.inject.Inject
@ -19,7 +21,7 @@ import javax.inject.Inject
* @property userWalletsListRepository repository to get user wallet information
* @property cryptoPortfolioCF converter factory to convert crypto portfolio accounts
* @property userTokensResponseFactory factory to create [UserTokensResponse]
* @property cardCryptoCurrencyFactory factory to get default coins for multi-currency wallet
* @property networkFactory factory to create network derivation path
*
[REDACTED_AUTHOR]
*/
@ -27,7 +29,7 @@ internal class DefaultWalletAccountsResponseFactory @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory,
private val userTokensResponseFactory: UserTokensResponseFactory,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val networkFactory: NetworkFactory,
) {
suspend fun create(userWalletId: UserWalletId, userTokensResponse: UserTokensResponse?): GetWalletAccountsResponse {
@ -59,10 +61,12 @@ internal class DefaultWalletAccountsResponseFactory @Inject constructor(
private fun UserTokensResponse?.orDefault(userWallet: UserWallet?): UserTokensResponse {
if (this != null) return this
return userTokensResponseFactory.createUserTokensResponse(
currencies = userWallet?.let(cardCryptoCurrencyFactory::createDefaultCoinsForMultiCurrencyWallet).orEmpty(),
isGroupedByNetwork = false,
isSortedByBalance = false,
return userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = userWallet?.let {
AccountId.forCryptoPortfolio(userWalletId = it.walletId, derivationIndex = DerivationIndex.Main)
},
)
}
}

View file

@ -6,20 +6,6 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.models.wallet.UserWalletId
/** Flattens the tokens from all wallet accounts into a single list */
internal fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
return accounts.flatMap { it.tokens.orEmpty() }
}
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
internal fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
return UserTokensResponse(
group = wallet.group,
sort = wallet.sort,
tokens = flattenTokens(),
)
}
/**
* Assigns tokens from a [UserTokensResponse] to the wallet accounts in the [GetWalletAccountsResponse]
*

View file

@ -29,6 +29,8 @@ class DefaultSingleAccountListFetcherTest {
// Arrange
val params = SingleAccountListFetcher.Params(userWalletId = userWalletId)
coEvery { walletAccountsFetcher.fetch(userWalletId) } returns mockk()
// Act
val actual = fetcher.invoke(params)

View file

@ -199,7 +199,7 @@ class DefaultWalletAccountsFetcherTest {
@Test
fun `fetch should call error handler when getWalletAccounts returns error`() = runTest {
// Arrange
val savedAccountsResponse = null
val savedAccountsResponse = createGetWalletAccountsResponse(userWalletId)
val apiError = ApiResponse.Error(ApiResponseError.NetworkException())
accountsResponseStoreFlow.value = savedAccountsResponse
@ -212,7 +212,7 @@ class DefaultWalletAccountsFetcherTest {
fetchWalletAccountsErrorHandler.handle(
error = apiError.cause,
userWalletId = userWalletId,
savedAccountsResponse = null,
savedAccountsResponse = savedAccountsResponse,
pushWalletAccounts = any(),
storeWalletAccounts = any(),
)
@ -231,7 +231,7 @@ class DefaultWalletAccountsFetcherTest {
fetchWalletAccountsErrorHandler.handle(
error = apiError.cause,
userWalletId = userWalletId,
savedAccountsResponse = null,
savedAccountsResponse = savedAccountsResponse,
pushWalletAccounts = any(),
storeWalletAccounts = any(),
)

View file

@ -3,13 +3,13 @@ package com.tangem.data.account.fetcher
import com.tangem.data.account.converter.createGetWalletAccountsResponse
import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.clearMocks
@ -53,6 +53,8 @@ class FetchWalletAccountsErrorHandlerTest {
@Test
fun `does not update accounts when response is up to date`() = runTest {
// Arrange
val response = createGetWalletAccountsResponse(userWalletId)
val error = ApiResponseError.HttpException(
code = Code.NOT_MODIFIED,
message = "Not Modified",
@ -63,7 +65,7 @@ class FetchWalletAccountsErrorHandlerTest {
handler.handle(
error = error,
userWalletId = userWalletId,
savedAccountsResponse = null,
savedAccountsResponse = response,
pushWalletAccounts = pushWalletAccounts,
storeWalletAccounts = storeWalletAccounts,
)

View file

@ -10,7 +10,6 @@ import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.store.ArchivedAccountsStore
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -54,7 +53,7 @@ class DefaultAccountsCRUDRepositoryTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val userTokensSaver: UserTokensSaver = mockk()
private val eTagsStore: ETagsStore = mockk()
private val archivedAccountsETagStore: RuntimeStateStore<Map<String, String?>> = mockk(relaxUnitFun = true)
private val convertersContainer: AccountConverterFactoryContainer = mockk()
private val accountListConverter: AccountListConverter = mockk()
@ -67,7 +66,7 @@ class DefaultAccountsCRUDRepositoryTest {
archivedAccountsStoreFactory = archivedAccountsStoreFactory,
userWalletsStore = userWalletsStore,
userTokensSaver = userTokensSaver,
eTagsStore = eTagsStore,
archivedAccountsETagStore = archivedAccountsETagStore,
convertersContainer = convertersContainer,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@ -532,7 +531,7 @@ class DefaultAccountsCRUDRepositoryTest {
val archivedAccount = ArchivedAccountConverter(userWalletId).convert(accountDTO)
coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag
coEvery { archivedAccountsETagStore.getSyncOrNull() } returns mapOf(userWalletId.stringValue to eTag)
coEvery {
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
@ -546,9 +545,10 @@ class DefaultAccountsCRUDRepositoryTest {
Truth.assertThat(actual).containsExactly(archivedAccount)
coVerifyOrder {
eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts)
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
archivedAccountsETagStore.getSyncOrNull()
archivedAccountsStoreFactory.create(userWalletId)
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
archivedAccountsETagStore.update(any())
}
}
@ -558,7 +558,7 @@ class DefaultAccountsCRUDRepositoryTest {
val eTag = "etag123"
val exception = Exception("API error")
coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag
coEvery { archivedAccountsETagStore.getSyncOrNull() } returns mapOf(userWalletId.stringValue to eTag)
coEvery { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag) } throws exception
// Act
@ -570,7 +570,7 @@ class DefaultAccountsCRUDRepositoryTest {
Truth.assertThat(archivedAccountsStore.getSyncOrNull()).isNull()
coVerifyOrder {
eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts)
archivedAccountsETagStore.getSyncOrNull()
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
}
}
@ -644,15 +644,15 @@ class DefaultAccountsCRUDRepositoryTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetTotalAccountsCountSync {
inner class GetTotalActiveAccountsCountSync {
@Test
fun `getTotalAccountsCountSync returns None if account list response is null`() = runTest {
fun `getTotalActiveAccountsCountSync returns None if account list response is null`() = runTest {
// Arrange
accountsResponseStoreFlow.value = null
// Act
val actual = repository.getTotalAccountsCountSync(userWalletId)
val actual = repository.getTotalActiveAccountsCountSync(userWalletId)
// Assert
Truth.assertThat(actual).isEqualTo(None)
@ -664,20 +664,22 @@ class DefaultAccountsCRUDRepositoryTest {
}
@Test
fun `getTotalAccountsCountSync returns Some with totalAccounts when response is valid`() = runTest {
fun `getTotalActiveAccountsCountSync returns Some with totalAccounts when response is valid`() = runTest {
// Arrange
val totalAccounts = 5
val response = mockk<GetWalletAccountsResponse> {
every { this@mockk.wallet.totalAccounts } returns totalAccounts
val response = createGetWalletAccountsResponse(userWalletId).let {
it.copy(
wallet = it.wallet.copy(totalAccounts = totalAccounts),
)
}
accountsResponseStoreFlow.value = response
// Act
val actual = repository.getTotalAccountsCountSync(userWalletId)
val actual = repository.getTotalActiveAccountsCountSync(userWalletId)
// Assert
val expected = totalAccounts.toOption()
val expected = 1.toOption()
Truth.assertThat(actual).isEqualTo(expected)
verifyOrder {
@ -689,15 +691,15 @@ class DefaultAccountsCRUDRepositoryTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetTotalAccountsCount {
inner class GetTotalActiveAccountsCount {
@Test
fun `getTotalAccountsCount emits 0 when account list response is null`() = runTest {
fun `getTotalActiveAccountsCount emits 0 when account list response is null`() = runTest {
// Arrange
accountsResponseStoreFlow.value = null
// Act
val flow = repository.getTotalAccountsCount(userWalletId)
val flow = repository.getTotalActiveAccountsCount(userWalletId)
val actual = getEmittedValues(flow)
// Assert
@ -710,21 +712,23 @@ class DefaultAccountsCRUDRepositoryTest {
}
@Test
fun `getTotalAccountsCount emits correct value when response is valid`() = runTest {
fun `getTotalActiveAccountsCount emits correct value when response is valid`() = runTest {
// Arrange
val totalAccounts = 7
val response = mockk<GetWalletAccountsResponse> {
every { this@mockk.wallet.totalAccounts } returns totalAccounts
val response = createGetWalletAccountsResponse(userWalletId).let {
it.copy(
wallet = it.wallet.copy(totalAccounts = totalAccounts),
)
}
accountsResponseStoreFlow.value = response
// Act
val flow = repository.getTotalAccountsCount(userWalletId)
val flow = repository.getTotalActiveAccountsCount(userWalletId)
val actual = getEmittedValues(flow)
// Assert
Truth.assertThat(actual).containsExactly(totalAccounts.toOption())
Truth.assertThat(actual).containsExactly(1.toOption())
verifyOrder {
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.data

View file

@ -7,10 +7,10 @@ import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId
@ -183,6 +183,8 @@ class DefaultMainAccountTokensMigrationTest {
accountsResponseStoreFlow.value = response
coEvery { accountsResponseStore.updateData(any()) } returns mockk()
// Act
val actual = migration.migrate(userWalletId, derivationIndex)
@ -199,6 +201,7 @@ class DefaultMainAccountTokensMigrationTest {
coVerifySequence {
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.data
accountsResponseStore.updateData(any())
userTokensSaver.push(
userWalletId = userWalletId,
response = migratedResponse.toUserTokensResponse(),

View file

@ -3,8 +3,8 @@ package com.tangem.data.account.utils
import com.google.common.truth.Truth
import com.tangem.data.account.converter.CryptoPortfolioConverter
import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.domain.account.models.AccountList
@ -27,13 +27,13 @@ class DefaultWalletAccountsResponseFactoryTest {
private val cryptoPortfolioCF = mockk<CryptoPortfolioConverter.Factory>()
private val cryptoPortfolioConverter = mockk<CryptoPortfolioConverter>()
private val userTokensResponseFactory = mockk<UserTokensResponseFactory>()
private val cardCryptoCurrencyFactory = mockk<CardCryptoCurrencyFactory>()
private val networkFactory = mockk<NetworkFactory>()
private val factory = DefaultWalletAccountsResponseFactory(
userWalletsListRepository = userWalletsListRepository,
cryptoPortfolioCF = cryptoPortfolioCF,
userTokensResponseFactory = userTokensResponseFactory,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
networkFactory = networkFactory,
)
private val userWalletId = UserWalletId("011")
@ -50,7 +50,7 @@ class DefaultWalletAccountsResponseFactoryTest {
cryptoPortfolioCF,
cryptoPortfolioConverter,
userTokensResponseFactory,
cardCryptoCurrencyFactory,
networkFactory,
)
}
@ -65,10 +65,10 @@ class DefaultWalletAccountsResponseFactoryTest {
coEvery { userWalletsListRepository.userWalletsSync() } returns emptyList()
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = null,
networkFactory = networkFactory,
accountId = null,
)
} returns userTokensResponse
@ -89,10 +89,10 @@ class DefaultWalletAccountsResponseFactoryTest {
coVerifyOrder {
userWalletsListRepository.userWalletsSync()
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = null,
networkFactory = networkFactory,
accountId = null,
)
}
}
@ -104,27 +104,24 @@ class DefaultWalletAccountsResponseFactoryTest {
every { walletId } returns userWalletId
}
val defaultCoins = listOf(mockk<CryptoCurrency.Coin>())
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns defaultCoins
val defaultResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = listOf(mockk(relaxed = true)),
)
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = defaultCoins,
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = accounts.first().accountId,
)
} returns defaultResponse
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
val accountsDTO = createWalletAccountDTO(userWalletId)
every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO)
@ -147,11 +144,10 @@ class DefaultWalletAccountsResponseFactoryTest {
coVerifyOrder {
userWalletsListRepository.userWalletsSync()
cryptoPortfolioConverter.convertListBack(accounts)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet)
userTokensResponseFactory.createUserTokensResponse(
currencies = defaultCoins,
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = accounts.first().accountId,
)
}
}
@ -162,22 +158,26 @@ class DefaultWalletAccountsResponseFactoryTest {
val userWallet = mockk<UserWallet>(relaxed = true) {
every { walletId } returns userWalletId
}
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList()
val defaultResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = accounts.first().accountId,
)
} returns defaultResponse
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
every { cryptoPortfolioConverter.convertListBack(accounts) } returns emptyList()
// Act

View file

@ -4,6 +4,8 @@ import com.google.common.truth.Truth
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.flattenTokens
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId

View file

@ -1,5 +1,6 @@
package com.tangem.data.common.account
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.domain.models.wallet.UserWalletId
/**
@ -11,5 +12,8 @@ interface WalletAccountsFetcher {
/** Fetch wallet accounts by [userWalletId] */
@Throws
suspend fun fetch(userWalletId: UserWalletId)
suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse
/** Get saved wallet accounts by [userWalletId] */
suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse?
}

View file

@ -7,6 +7,7 @@ import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.data.common.network.NetworkFactory
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
@ -18,7 +19,7 @@ class CryptoCurrencyFactory(
private val excludedBlockchains: ExcludedBlockchains,
) {
private val networkFactory by lazy(LazyThreadSafetyMode.NONE) { NetworkFactory(excludedBlockchains) }
val networkFactory by lazy(LazyThreadSafetyMode.NONE) { NetworkFactory(excludedBlockchains) }
@Suppress("LongParameterList") // Yep, it's long
fun createToken(
@ -48,6 +49,7 @@ class CryptoCurrencyFactory(
blockchain: Blockchain,
extraDerivationPath: String?,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): CryptoCurrency.Token? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
@ -58,6 +60,7 @@ class CryptoCurrencyFactory(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
userWallet = userWallet,
accountIndex = accountIndex,
) ?: return null
val id = getTokenId(network, sdkToken)
@ -74,11 +77,16 @@ class CryptoCurrencyFactory(
)
}
fun createCoin(chainId: Int, extraDerivationPath: String?, userWallet: UserWallet): CryptoCurrency.Coin? {
fun createCoin(
chainId: Int,
extraDerivationPath: String?,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): CryptoCurrency.Coin? {
val blockchain: Blockchain? = Chain.entries.find { it.id == chainId }?.blockchain
return if (blockchain != null) {
createCoin(blockchain, extraDerivationPath, userWallet)
createCoin(blockchain, extraDerivationPath, userWallet, accountIndex)
} else {
Timber.e("Unable to get blockchain from chainId == $chainId")
null
@ -89,6 +97,7 @@ class CryptoCurrencyFactory(
blockchain: Blockchain,
extraDerivationPath: String?,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): CryptoCurrency.Coin? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
@ -99,6 +108,7 @@ class CryptoCurrencyFactory(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
userWallet = userWallet,
accountIndex = accountIndex,
) ?: return null
return createCoin(network)

View file

@ -1,14 +1,16 @@
package com.tangem.data.common.currency
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.tokens.getDefaultWalletBlockchains
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
@ -23,10 +25,13 @@ import com.tangem.domain.models.wallet.isMultiCurrency
* @property userWalletsStore user wallets store
* @property userTokensResponseStore user tokens response store
*/
@Suppress("LongParameterList")
internal class DefaultCardCryptoCurrencyFactory(
private val demoConfig: DemoConfig,
private val excludedBlockchains: ExcludedBlockchains,
private val userWalletsStore: UserWalletsStore,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val walletAccountsFetcher: WalletAccountsFetcher,
private val userTokensResponseStore: UserTokensResponseStore,
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
) : CardCryptoCurrencyFactory {
@ -99,25 +104,7 @@ internal class DefaultCardCryptoCurrencyFactory(
override fun createDefaultCoinsForMultiCurrencyWallet(userWallet: UserWallet): List<CryptoCurrency.Coin> {
require(userWallet.isMultiCurrency) { "It isn't multi-currency wallet" }
val blockchains = when (userWallet) {
is UserWallet.Cold -> {
val card = userWallet.scanResponse.card
var blockchainsInternal = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
if (card.isTestCard) {
blockchainsInternal = blockchainsInternal.mapNotNull { it.getTestnetVersion() }
}
blockchainsInternal
}
is UserWallet.Hot -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
val blockchains = getDefaultWalletBlockchains(userWallet, demoConfig)
return blockchains.mapNotNull {
cryptoCurrencyFactory.createCoin(
@ -152,15 +139,32 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>> {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
val existingNetworkWithCurrencies = if (accountsFeatureToggles.isFeatureEnabled) {
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
?: return emptyMap()
val existingNetworkWithCurrencies = responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token ->
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
},
userWallet = userWallet,
)
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
responseCryptoCurrenciesFactory.createCurrencies(
tokens = accountDTO.tokens.orEmpty().filter { token ->
networks.any {
it.backendId == token.networkId && it.derivationPath.value == token.derivationPath
}
},
userWallet = userWallet,
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
)
}
} else {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token ->
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
},
userWallet = userWallet,
)
}
.groupBy(CryptoCurrency::network)
return networks.associateWith { emptyList<CryptoCurrency>() } + existingNetworkWithCurrencies
@ -170,15 +174,28 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
rawIds: Set<Network.RawID>,
): Map<Network.RawID, List<CryptoCurrency>> {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
val networkIds = rawIds.map { it.toBlockchain().toNetworkId() }
return responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token -> token.networkId in networkIds },
userWallet = userWallet,
)
return if (accountsFeatureToggles.isFeatureEnabled) {
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
?: return emptyMap()
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
responseCryptoCurrenciesFactory.createCurrencies(
tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds },
userWallet = userWallet,
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
)
}
} else {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token -> token.networkId in networkIds },
userWallet = userWallet,
)
}
.groupBy { it.network.id.rawId }
}

View file

@ -7,6 +7,7 @@ import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import timber.log.Timber
@ -17,26 +18,31 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
private val networkFactory: NetworkFactory,
) {
fun createCurrency(currencyId: String, response: UserTokensResponse, userWallet: UserWallet): CryptoCurrency {
return response.tokens
.asSequence()
.mapNotNull { createCurrency(it, userWallet) }
.first { it.id.value == currencyId }
fun createCurrencies(
response: UserTokensResponse,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): List<CryptoCurrency> {
return createCurrencies(tokens = response.tokens, userWallet = userWallet, accountIndex = accountIndex)
}
fun createCurrencies(response: UserTokensResponse, userWallet: UserWallet): List<CryptoCurrency> {
return createCurrencies(tokens = response.tokens, userWallet = userWallet)
}
fun createCurrencies(tokens: List<UserTokensResponse.Token>, userWallet: UserWallet): List<CryptoCurrency> {
fun createCurrencies(
tokens: List<UserTokensResponse.Token>,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): List<CryptoCurrency> {
return tokens
.asSequence()
.mapNotNull { createCurrency(it, userWallet) }
.mapNotNull { createCurrency(it, userWallet, accountIndex) }
.distinctBy(CryptoCurrency::id)
.toList()
}
fun createCurrency(responseToken: UserTokensResponse.Token, userWallet: UserWallet): CryptoCurrency? {
fun createCurrency(
responseToken: UserTokensResponse.Token,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): 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}")
@ -49,9 +55,9 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
val sdkToken = createSdkToken(responseToken)
return if (sdkToken == null) {
createCoin(blockchain, responseToken, userWallet)
createCoin(blockchain, responseToken, userWallet, accountIndex)
} else {
createToken(blockchain, sdkToken, responseToken.derivationPath, userWallet)
createToken(blockchain, sdkToken, responseToken.derivationPath, userWallet, accountIndex)
}
}
@ -71,11 +77,13 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
blockchain: Blockchain,
responseToken: UserTokensResponse.Token,
userWallet: UserWallet,
accountIndex: DerivationIndex?,
): CryptoCurrency.Coin? {
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = responseToken.derivationPath,
userWallet = userWallet,
accountIndex = accountIndex,
) ?: return null
return CryptoCurrency.Coin(
@ -106,11 +114,13 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
sdkToken: Token,
responseDerivationPath: String?,
userWallet: UserWallet,
accountIndex: DerivationIndex?,
): CryptoCurrency.Token? {
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = responseDerivationPath,
userWallet = userWallet,
accountIndex = accountIndex,
) ?: return null
val id = getTokenId(network, sdkToken)

View file

@ -1,11 +1,17 @@
package com.tangem.data.common.currency
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.common.tokens.getDefaultWalletBlockchains
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import javax.inject.Inject
// TODO: [REDACTED_JIRA]
class UserTokensResponseFactory @Inject constructor() {
fun createUserTokensResponse(
@ -43,4 +49,38 @@ class UserTokensResponseFactory @Inject constructor() {
)
}
}
fun createDefaultResponse(
userWallet: UserWallet?,
networkFactory: NetworkFactory,
accountId: AccountId?,
): UserTokensResponse {
val tokens = userWallet?.let {
getDefaultWalletBlockchains(userWallet = it, demoConfig = DemoConfig())
.map { blockchain ->
val derivationPath = networkFactory.createDerivationPath(
blockchain = blockchain,
extraDerivationPath = null,
cardDerivationStyleProvider = userWallet.derivationStyleProvider,
).value
UserTokensResponse.Token(
id = blockchain.toCoinId(),
accountId = accountId?.value,
networkId = blockchain.toNetworkId(),
derivationPath = derivationPath,
name = blockchain.getCoinName(),
symbol = blockchain.currency,
decimals = blockchain.decimals(),
contractAddress = null,
)
}
}
return UserTokensResponse(
group = UserTokensResponse.GroupType.NONE,
sort = UserTokensResponse.SortType.MANUAL,
tokens = tokens.orEmpty(),
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.data.common.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.cache.etag.DefaultETagsStore
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.*
@ -30,6 +31,8 @@ internal object DataCommonModule {
fun provideCardCryptoCurrencyFactory(
excludedBlockchains: ExcludedBlockchains,
userWalletsStore: UserWalletsStore,
accountsFeatureToggles: AccountsFeatureToggles,
walletAccountsFetcher: WalletAccountsFetcher,
userTokensResponseStore: UserTokensResponseStore,
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
): CardCryptoCurrencyFactory {
@ -37,6 +40,8 @@ internal object DataCommonModule {
demoConfig = DemoConfig(),
excludedBlockchains = excludedBlockchains,
userWalletsStore = userWalletsStore,
accountsFeatureToggles = accountsFeatureToggles,
walletAccountsFetcher = walletAccountsFetcher,
userTokensResponseStore = userTokensResponseStore,
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
)

View file

@ -7,10 +7,12 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.card.common.extensions.canHandleToken
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.lib.crypto.derivation.toMutable
import timber.log.Timber
import javax.inject.Inject
@ -21,6 +23,7 @@ import javax.inject.Inject
*
[REDACTED_AUTHOR]
*/
@Suppress("LargeClass")
class NetworkFactory @Inject constructor(
private val excludedBlockchains: ExcludedBlockchains,
) {
@ -32,13 +35,19 @@ class NetworkFactory @Inject constructor(
* @param extraDerivationPath extra derivation path
* @param userWallet user wallet
*/
fun create(blockchain: Blockchain, extraDerivationPath: String?, userWallet: UserWallet): Network? {
fun create(
blockchain: Blockchain,
extraDerivationPath: String?,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): Network? {
return create(
blockchain = blockchain,
derivationPath = createDerivationPath(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
cardDerivationStyleProvider = userWallet.derivationStyleProvider,
accountIndex = accountIndex,
),
canHandleTokens = userWallet.canHandleToken(
blockchain = blockchain,
@ -80,6 +89,7 @@ class NetworkFactory @Inject constructor(
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider?,
canHandleTokens: Boolean,
accountIndex: DerivationIndex? = null,
): Network? {
return create(
blockchain = blockchain,
@ -87,6 +97,7 @@ class NetworkFactory @Inject constructor(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
cardDerivationStyleProvider = derivationStyleProvider,
accountIndex = accountIndex,
),
canHandleTokens = canHandleTokens,
)
@ -130,14 +141,15 @@ class NetworkFactory @Inject constructor(
return true
}
private fun createDerivationPath(
fun createDerivationPath(
blockchain: Blockchain,
extraDerivationPath: String?,
cardDerivationStyleProvider: DerivationStyleProvider?,
accountIndex: DerivationIndex? = null,
): Network.DerivationPath {
if (cardDerivationStyleProvider == null) return Network.DerivationPath.None
val defaultDerivationPath = getDefaultDerivationPath(blockchain, cardDerivationStyleProvider)
val defaultDerivationPath = getDefaultDerivationPath(blockchain, cardDerivationStyleProvider, accountIndex)
return if (extraDerivationPath.isNullOrBlank()) {
if (defaultDerivationPath.isNullOrBlank()) {
@ -146,10 +158,11 @@ class NetworkFactory @Inject constructor(
Network.DerivationPath.Card(defaultDerivationPath)
}
} else {
if (extraDerivationPath == defaultDerivationPath) {
Network.DerivationPath.Card(defaultDerivationPath)
} else {
val isMainIndexOrNull = accountIndex == null || accountIndex == DerivationIndex.Main
if (extraDerivationPath != defaultDerivationPath && isMainIndexOrNull) {
Network.DerivationPath.Custom(extraDerivationPath)
} else {
Network.DerivationPath.Card(extraDerivationPath)
}
}
}
@ -157,8 +170,19 @@ class NetworkFactory @Inject constructor(
private fun getDefaultDerivationPath(
blockchain: Blockchain,
derivationStyleProvider: DerivationStyleProvider,
accountIndex: DerivationIndex?,
): String? {
return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath
val default = blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())
?: return null
return if (accountIndex == null || accountIndex == DerivationIndex.Main) {
default
} else {
default.toMutable()
.replaceAccountNode(value = accountIndex.value.toLong(), blockchain = blockchain)
.apply()
}
.rawPath
}
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
@ -326,8 +350,8 @@ class NetworkFactory @Inject constructor(
Blockchain.Pepecoin, Blockchain.PepecoinTestnet,
Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet,
Blockchain.Quai, Blockchain.QuaiTestnet,
// Blockchain.Linea, Blockchain.LineaTestnet,
// Blockchain.ArbitrumNova,
Blockchain.Linea, Blockchain.LineaTestnet,
Blockchain.ArbitrumNova,
-> Network.TransactionExtrasType.NONE
// endregion
}

View file

@ -0,0 +1,33 @@
package com.tangem.data.common.tokens
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.models.wallet.UserWallet
/**
* Returns the default blockchains for the multi-currency wallet.
*
* @param userWallet The user's wallet, which can be either a cold or hot wallet.
* @param demoConfig Configuration for demo cards, which may specify different default blockchains.
*/
fun getDefaultWalletBlockchains(userWallet: UserWallet, demoConfig: DemoConfig): Collection<Blockchain> {
return when (userWallet) {
is UserWallet.Cold -> {
val card = userWallet.scanResponse.card
var blockchainsInternal = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
if (card.isTestCard) {
blockchainsInternal = blockchainsInternal.mapNotNull { it.getTestnetVersion() }
}
blockchainsInternal
}
is UserWallet.Hot -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
}

View file

@ -9,10 +9,12 @@ import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.demo.models.DemoConfig
@ -37,6 +39,8 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val userTokensResponseStore: UserTokensResponseStore = mockk()
private val excludedBlockchains = ExcludedBlockchains()
private val accountsFeatureToggles = mockk<AccountsFeatureToggles>()
private val walletAccountsFetcher = mockk<WalletAccountsFetcher>()
private val factory = DefaultCardCryptoCurrencyFactory(
demoConfig = DemoConfig(),
@ -46,6 +50,8 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory(
networkFactory = NetworkFactory(excludedBlockchains = excludedBlockchains),
),
accountsFeatureToggles = accountsFeatureToggles,
walletAccountsFetcher = walletAccountsFetcher,
)
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
@ -57,7 +63,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
@BeforeEach
fun init() {
clearMocks(userWalletsStore, userTokensResponseStore, iconUri)
clearMocks(userWalletsStore, userTokensResponseStore, accountsFeatureToggles, walletAccountsFetcher, iconUri)
mockkStatic(Uri::class)
every { Uri.parse(any()) } returns iconUri
@ -75,6 +81,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
val userTokensResponse = model.userTokensResponse
val network = ethereum.network
every { accountsFeatureToggles.isFeatureEnabled } returns false
coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet
coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse
@ -236,6 +243,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest {
val networks = setOf(ethereum.network, bitcoin.network)
val userTokensResponse = model.userTokensResponse
every { accountsFeatureToggles.isFeatureEnabled } returns false
coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse
// Act

View file

@ -0,0 +1,113 @@
package com.tangem.data.express
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.data.express.converter.ExpressAssetConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.exchangeservice.swap.ExpressUtils.getRefCode
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import timber.log.Timber
import javax.inject.Inject
typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<ExpressAsset>>>
/**
* Default implementation of [ExpressServiceFetcher]
*
* @property tangemExpressApi express api
* @property expressAssetsStore local storage
*
[REDACTED_AUTHOR]
*/
internal class DefaultExpressServiceFetcher @Inject constructor(
private val tangemExpressApi: TangemExpressApi,
private val expressAssetsStore: ExpressAssetsStore,
private val appPreferencesStore: AppPreferencesStore,
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : ExpressServiceFetcher {
private val initializationStatuses =
MutableStateFlow<Map<UserWalletId, InitializationStatusFlow>>(value = emptyMap())
override suspend fun fetch(userWalletId: UserWalletId, assetIds: Set<ExpressAsset.ID>): Either<Throwable, Unit> =
either {
val userWallet = arrow.core.raise.catch(
block = { userWalletsStore.getSyncStrict(userWalletId) },
catch = ::raise,
)
fetch(userWallet = userWallet, assetIds = assetIds).bind()
}
override suspend fun fetch(userWallet: UserWallet, assetIds: Set<ExpressAsset.ID>): Either<Throwable, Unit> {
return Either.catchOn(dispatchers.io) {
val initializationStatus = getInitializationStatusInternal(userWallet.walletId)
try {
if (assetIds.isNotEmpty()) {
val tokenList = assetIds.map {
LeastTokenInfo(contractAddress = it.contractAddress, network = it.networkId)
}
val response = tangemExpressApi.getAssets(
userWalletId = userWallet.walletId.stringValue,
refCode = getRefCode(userWallet, appPreferencesStore),
body = AssetsRequestBody(tokensList = tokenList),
).getOrThrow()
expressAssetsStore.store(userWallet.walletId, response)
val expressAssets = ExpressAssetConverter.convertList(response)
initializationStatus.update { expressAssets.lceContent() }
}
} catch (e: Throwable) {
if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) {
initializationStatus.update { e.lceError() }
}
Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}")
throw e
}
}
}
override fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<ExpressAsset>>> {
return flow { getInitializationStatusInternal(userWalletId).collect { emit(it) } }
}
@Suppress("SuspendFunWithFlowReturnType")
private suspend fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
val initializationStatus = initializationStatuses.value[userWalletId]
if (initializationStatus != null) return initializationStatus
val cached = expressAssetsStore.getSyncOrNull(userWalletId)?.let(ExpressAssetConverter::convertList)
val default: InitializationStatusFlow = MutableStateFlow(value = cached?.lceContent() ?: lceLoading())
initializationStatuses.update { statuses ->
statuses.toMutableMap().apply {
put(key = userWalletId, value = default)
}
}
return default
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.express.converter
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.utils.converter.Converter
/**
* Converts an [Asset] from the data layer to an [ExpressAsset] in the domain layer.
*
[REDACTED_AUTHOR]
*/
internal object ExpressAssetConverter : Converter<Asset, ExpressAsset> {
override fun convert(value: Asset): ExpressAsset {
return ExpressAsset(
id = ExpressAsset.ID(
networkId = value.network,
contractAddress = value.contractAddress,
),
isExchangeAvailable = value.exchangeAvailable,
isOnrampAvailable = value.onrampAvailable,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.express.di
import com.squareup.moshi.Moshi
import com.tangem.data.express.DefaultExpressRepository
import com.tangem.data.express.DefaultExpressServiceFetcher
import com.tangem.data.express.converter.ExpressErrorConverter
import com.tangem.data.express.error.DefaultExpressErrorResolver
import com.tangem.datasource.api.express.TangemExpressApi
@ -10,6 +11,7 @@ import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.express.ExpressErrorResolver
import com.tangem.domain.express.ExpressRepository
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -43,4 +45,10 @@ internal object ExpressDataModule {
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideExpressServiceFetcher(impl: DefaultExpressServiceFetcher): ExpressServiceFetcher {
return impl
}
}

View file

@ -15,6 +15,7 @@ android {
dependencies {
/** Project - Domain */
implementation(projects.domain.account)
implementation(projects.domain.demo)
implementation(projects.domain.models)
implementation(projects.domain.manageTokens)

View file

@ -238,7 +238,7 @@ internal class DefaultCustomTokensRepository(
"User tokens not found for user wallet [$userWalletId] while removing currency"
}
val token = userTokensResponseFactory.createResponseToken(cryptoCurrency)
val token = userTokensResponseFactory.createResponseToken(currency = cryptoCurrency, accountId = null)
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = storedCurrencies.copy(tokens = storedCurrencies.tokens.filterNot { it == token }),
@ -249,6 +249,27 @@ internal class DefaultCustomTokensRepository(
}
}
override suspend fun convertToCryptoCurrency(
userWalletId: UserWalletId,
currency: ManagedCryptoCurrency.Custom,
): CryptoCurrency {
return when (currency) {
is ManagedCryptoCurrency.Custom.Coin -> createCoin(
userWalletId = userWalletId,
networkId = currency.network.id,
derivationPath = currency.network.derivationPath,
)
is ManagedCryptoCurrency.Custom.Token -> cryptoCurrencyFactory.createToken(
network = currency.network,
rawId = currency.currencyId.rawCurrencyId,
name = currency.name,
symbol = currency.symbol,
decimals = currency.decimals,
contractAddress = currency.contractAddress,
)
}
}
override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> = withContext(dispatchers.io) {
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"User wallet [$userWalletId] not found while getting supported networks"

View file

@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
@ -16,20 +17,20 @@ import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
import com.tangem.domain.card.common.extensions.canHandleBlockchain
import com.tangem.domain.card.common.extensions.canHandleToken
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
import com.tangem.domain.card.common.extensions.supportedBlockchains
import com.tangem.domain.card.common.extensions.supportedTokens
import com.tangem.domain.card.common.extensions.*
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.managetokens.model.*
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -40,7 +41,7 @@ import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher.Request
import com.tangem.pagination.toBatchFlow
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class DefaultManageTokensRepository(
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
@ -51,10 +52,16 @@ internal class DefaultManageTokensRepository(
private val excludedBlockchains: ExcludedBlockchains,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val dispatchers: CoroutineDispatcherProvider,
private val walletAccountsFetcher: WalletAccountsFetcher,
private val accountsFeatureToggles: AccountsFeatureToggles,
networkFactory: NetworkFactory,
) : ManageTokensRepository {
private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory(networkFactory, excludedBlockchains)
private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory(
networkFactory = networkFactory,
excludedBlockchains = excludedBlockchains,
accountsFeatureToggles = accountsFeatureToggles,
)
private val userTokensResponseFactory = UserTokensResponseFactory()
// region getTokenListBatchFlow
@ -82,7 +89,10 @@ internal class DefaultManageTokensRepository(
val userWallet = request.params.userWalletId?.let(userWalletsStore::getSyncStrict)
if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.isTestCard) {
fetchTestnetCurrencies(userWallet, request)
when (val params = request.params) {
is ManageTokensListConfig.Account -> fetchTestnetCurrencies(userWallet, params)
is ManageTokensListConfig.Wallet -> fetchTestnetCurrenciesLegacy(userWallet, params)
}
} else {
fetchCurrencies(
userWallet = userWallet,
@ -94,7 +104,6 @@ internal class DefaultManageTokensRepository(
},
)
@Suppress("ComplexCondition")
private suspend fun fetchCurrencies(
userWallet: UserWallet?,
request: Request<ManageTokensListConfig>,
@ -127,31 +136,22 @@ internal class DefaultManageTokensRepository(
coins = coinsResponse.coins.filterNot { l2BlockchainsCoinIds.contains(it.id) },
)
val tokensResponse = request.params.userWalletId?.let { userWalletId ->
if (loadUserTokensFromRemote && userWallet != null) {
safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) {
// save tokens response only if loadUserTokensFromRemote is true and it means onboarding call
createAndSaveDefaultUserTokensResponse(userWallet = userWallet)
}
} else {
getSavedUserTokensResponseSync(userWalletId)
}
}
val items = if (isFirstBatchFetching &&
tokensResponse != null &&
userWallet != null &&
query == null
) {
managedCryptoCurrencyFactory.createWithCustomTokens(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
val items = when (val params = request.params) {
is ManageTokensListConfig.Account -> createManagedCryptoCurrencyList(
params = params,
userWallet = userWallet,
isFirstBatchFetching = isFirstBatchFetching,
loadUserTokensFromRemote = loadUserTokensFromRemote,
query = query,
updatedCoinsResponse = updatedCoinsResponse,
)
} else {
managedCryptoCurrencyFactory.create(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
is ManageTokensListConfig.Wallet -> createManagedCryptoCurrencyListLegacy(
params = params,
userWallet = userWallet,
isFirstBatchFetching = isFirstBatchFetching,
loadUserTokensFromRemote = loadUserTokensFromRemote,
query = query,
updatedCoinsResponse = updatedCoinsResponse,
)
}
@ -162,6 +162,112 @@ internal class DefaultManageTokensRepository(
)
}
private suspend fun createManagedCryptoCurrencyList(
params: ManageTokensListConfig.Account,
userWallet: UserWallet?,
isFirstBatchFetching: Boolean,
loadUserTokensFromRemote: Boolean,
query: String?,
updatedCoinsResponse: CoinsResponse,
): List<ManagedCryptoCurrency> {
val response = params.userWalletId?.let { userWalletId ->
if (loadUserTokensFromRemote && userWallet != null) {
runCatching { walletAccountsFetcher.fetch(userWalletId = userWallet.walletId) }.getOrNull()
} else {
walletAccountsFetcher.getSaved(userWalletId)
}
}
val accountId = when {
params.accountId == null -> null
loadUserTokensFromRemote -> {
AccountId.forCryptoPortfolio(
userWalletId = requireNotNull(params.accountId).userWalletId,
derivationIndex = DerivationIndex.Main,
)
}
else -> requireNotNull(params.accountId)
}
val accountDTO = if (response != null && accountId != null) {
response.accounts.firstOrNull { it.id == accountId.value }
} else {
null
}
val tokensResponse = response?.let {
UserTokensResponse(
group = response.wallet.group,
sort = response.wallet.sort,
tokens = accountDTO?.tokens.orEmpty(),
)
}
val isCreateWithCustom = isFirstBatchFetching &&
tokensResponse != null &&
userWallet != null &&
query == null
val items = if (isCreateWithCustom) {
managedCryptoCurrencyFactory.createWithCustomTokens(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
userWallet = userWallet,
accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull(),
)
} else {
managedCryptoCurrencyFactory.create(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
userWallet = userWallet,
accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull(),
)
}
return items
}
private suspend fun createManagedCryptoCurrencyListLegacy(
params: ManageTokensListConfig.Wallet,
userWallet: UserWallet?,
isFirstBatchFetching: Boolean,
loadUserTokensFromRemote: Boolean,
query: String?,
updatedCoinsResponse: CoinsResponse,
): List<ManagedCryptoCurrency> {
val tokensResponse = params.userWalletId?.let { userWalletId ->
if (loadUserTokensFromRemote && userWallet != null) {
safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) {
// save tokens response only if loadUserTokensFromRemote is true and it means onboarding call
createAndSaveDefaultUserTokensResponse(userWallet = userWallet)
}
} else {
getSavedUserTokensResponseSync(userWalletId)
}
}
val isCreateWithCustom = isFirstBatchFetching &&
tokensResponse != null &&
userWallet != null &&
query == null
return if (isCreateWithCustom) {
managedCryptoCurrencyFactory.createWithCustomTokens(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
userWallet = userWallet,
accountIndex = null,
)
} else {
managedCryptoCurrencyFactory.create(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
userWallet = userWallet,
accountIndex = null,
)
}
}
private suspend fun createAndSaveDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse {
val userTokensResponse = createDefaultUserTokensResponse(userWallet)
userTokenSaver.store(userWallet.walletId, userTokensResponse, useEnricher = false)
@ -170,9 +276,59 @@ internal class DefaultManageTokensRepository(
private suspend fun fetchTestnetCurrencies(
userWallet: UserWallet,
request: Request<ManageTokensListConfig>,
params: ManageTokensListConfig.Account,
): BatchFetchResult.Success<List<ManagedCryptoCurrency>> {
val searchText = request.params.searchText
val searchText = params.searchText
val testnetTokensConfig = testnetTokensStorage.getConfig()
val response = params.userWalletId?.let { userWalletId ->
walletAccountsFetcher.getSaved(userWalletId)
}
val accountId = params.accountId
val accountDTO = if (response != null && accountId != null) {
response.accounts.firstOrNull { it.id == accountId.value }
} else {
null
}
val tokensResponse = response?.let {
UserTokensResponse(
group = response.wallet.group,
sort = response.wallet.sort,
tokens = accountDTO?.tokens.orEmpty(),
)
}
val items = managedCryptoCurrencyFactory.createTestnetWithCustomTokens(
testnetTokensConfig = if (!searchText.isNullOrBlank()) {
testnetTokensConfig.copy(
tokens = testnetTokensConfig.tokens.filter { token ->
token.symbol.contains(other = searchText, ignoreCase = true) ||
token.name.contains(other = searchText, ignoreCase = true)
},
)
} else {
testnetTokensConfig
},
tokensResponse = tokensResponse,
userWallet = userWallet,
accountIndex = accountDTO?.derivationIndex?.let(DerivationIndex::invoke)?.getOrNull(),
)
return BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = true,
)
}
private suspend fun fetchTestnetCurrenciesLegacy(
userWallet: UserWallet,
params: ManageTokensListConfig.Wallet,
): BatchFetchResult.Success<List<ManagedCryptoCurrency>> {
val searchText = params.searchText
val testnetTokensConfig = testnetTokensStorage.getConfig()
val items = managedCryptoCurrencyFactory.createTestnetWithCustomTokens(
@ -188,6 +344,7 @@ internal class DefaultManageTokensRepository(
},
tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId),
userWallet = userWallet,
accountIndex = null,
)
return BatchFetchResult.Success(

View file

@ -1,6 +1,7 @@
package com.tangem.data.managetokens.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.common.network.NetworkFactory
@ -11,6 +12,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -38,6 +40,8 @@ internal object ManageTokensDataModule {
excludedBlockchains: ExcludedBlockchains,
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
networkFactory: NetworkFactory,
accountsFeatureToggles: AccountsFeatureToggles,
walletAccountsFetcher: WalletAccountsFetcher,
): ManageTokensRepository {
return DefaultManageTokensRepository(
tangemTechApi = tangemTechApi,
@ -50,6 +54,8 @@ internal object ManageTokensDataModule {
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
networkFactory = networkFactory,
dispatchers = dispatchers,
accountsFeatureToggles = accountsFeatureToggles,
walletAccountsFetcher = walletAccountsFetcher,
)
}

View file

@ -13,9 +13,11 @@ import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.extensions.canHandleToken
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
@ -26,15 +28,17 @@ import timber.log.Timber
internal class ManagedCryptoCurrencyFactory(
private val networkFactory: NetworkFactory,
private val excludedBlockchains: ExcludedBlockchains,
private val accountsFeatureToggles: AccountsFeatureToggles,
) {
fun create(
coinsResponse: CoinsResponse,
tokensResponse: UserTokensResponse?,
userWallet: UserWallet?,
accountIndex: DerivationIndex?,
): List<ManagedCryptoCurrency> {
return coinsResponse.coins.mapNotNull { coin ->
createToken(coin, tokensResponse, coinsResponse.imageHost, userWallet)
createToken(coin, tokensResponse, coinsResponse.imageHost, userWallet, accountIndex)
}
}
@ -42,9 +46,10 @@ internal class ManagedCryptoCurrencyFactory(
coinsResponse: CoinsResponse,
tokensResponse: UserTokensResponse,
userWallet: UserWallet,
accountIndex: DerivationIndex?,
): List<ManagedCryptoCurrency> {
val customTokens = createCustomTokens(tokensResponse, userWallet)
val tokens = create(coinsResponse, tokensResponse, userWallet)
val customTokens = createCustomTokens(tokensResponse, userWallet, accountIndex)
val tokens = create(coinsResponse, tokensResponse, userWallet, accountIndex)
return customTokens + tokens
}
@ -53,9 +58,10 @@ internal class ManagedCryptoCurrencyFactory(
testnetTokensConfig: TestnetTokensConfig,
tokensResponse: UserTokensResponse?,
userWallet: UserWallet,
accountIndex: DerivationIndex?,
): List<ManagedCryptoCurrency> {
val customTokens = tokensResponse
?.let { createCustomTokens(it, userWallet) }
?.let { createCustomTokens(it, userWallet, accountIndex) }
?: emptyList()
val testnetTokens = testnetTokensConfig.tokens.map { testnetToken ->
ManagedCryptoCurrency.Token(
@ -69,9 +75,10 @@ internal class ManagedCryptoCurrencyFactory(
contractAddress = network.address,
decimals = network.decimalCount,
userWallet = userWallet,
accountIndex = accountIndex,
)
} ?: emptyList(),
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, userWallet),
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, userWallet, accountIndex),
)
}
@ -81,28 +88,47 @@ internal class ManagedCryptoCurrencyFactory(
private fun createCustomTokens(
tokensResponse: UserTokensResponse,
userWallet: UserWallet,
accountIndex: DerivationIndex?,
): List<ManagedCryptoCurrency> = tokensResponse.tokens
.mapNotNull { token ->
maybeCreateCustomToken(token, userWallet)
maybeCreateCustomToken(token, userWallet, accountIndex)
}
private fun maybeCreateCustomToken(
token: UserTokensResponse.Token,
userWallet: UserWallet,
accountIndex: DerivationIndex?,
): ManagedCryptoCurrency? {
val blockchain = Blockchain.fromNetworkId(token.networkId)
?.takeUnless { it in excludedBlockchains }
?: return null
if (!checkIsCustomToken(token, blockchain, userWallet.derivationStyleProvider)) {
return null
val network = if (accountsFeatureToggles.isFeatureEnabled) {
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = token.derivationPath,
userWallet = userWallet,
accountIndex = accountIndex,
) ?: return null
if (!checkIsCustomToken(token, network.derivationPath)) {
return null
}
network
} else {
if (!checkIsCustomToken(token, blockchain, userWallet.derivationStyleProvider)) {
return null
}
networkFactory.create(
blockchain = blockchain,
extraDerivationPath = token.derivationPath,
userWallet = userWallet,
accountIndex = accountIndex,
) ?: return null
}
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = token.derivationPath,
userWallet = userWallet,
) ?: return null
val contractAddress = token.contractAddress
return if (contractAddress.isNullOrBlank()) {
@ -135,6 +161,7 @@ internal class ManagedCryptoCurrencyFactory(
tokensResponse: UserTokensResponse?,
imageHost: String?,
userWallet: UserWallet?,
accountIndex: DerivationIndex?,
): ManagedCryptoCurrency? {
if (coinResponse.networks.isEmpty() || !coinResponse.active) return null
@ -146,6 +173,7 @@ internal class ManagedCryptoCurrencyFactory(
contractAddress = network.contractAddress,
decimals = network.decimalCount?.toInt(),
userWallet = userWallet,
accountIndex = accountIndex,
)
}
.ifEmpty { return null }
@ -156,7 +184,7 @@ internal class ManagedCryptoCurrencyFactory(
symbol = coinResponse.symbol,
iconUrl = getIconUrl(coinResponse.id, imageHost),
availableNetworks = availableNetworks,
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, userWallet),
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, userWallet, accountIndex),
)
}
@ -166,6 +194,7 @@ internal class ManagedCryptoCurrencyFactory(
decimals: Int?,
userWallet: UserWallet?,
extraDerivationPath: String? = null,
accountIndex: DerivationIndex?,
): SourceNetwork? {
val blockchain = Blockchain.fromNetworkId(networkId)
?.takeUnless { it in excludedBlockchains }
@ -176,6 +205,7 @@ internal class ManagedCryptoCurrencyFactory(
extraDerivationPath = extraDerivationPath,
derivationStyleProvider = userWallet?.derivationStyleProvider,
canHandleTokens = userWallet?.canHandleToken(blockchain, excludedBlockchains) == true,
accountIndex = accountIndex,
) ?: return null
return when {
@ -205,6 +235,7 @@ internal class ManagedCryptoCurrencyFactory(
currencyId: String,
tokensResponse: UserTokensResponse?,
userWallet: UserWallet?,
accountIndex: DerivationIndex?,
): Set<Network> {
if (tokensResponse == null) return emptySet()
@ -219,6 +250,7 @@ internal class ManagedCryptoCurrencyFactory(
extraDerivationPath = token.derivationPath,
derivationStyleProvider = userWallet?.derivationStyleProvider,
canHandleTokens = userWallet?.canHandleToken(blockchain, excludedBlockchains) != false,
accountIndex = accountIndex,
)
} else {
null
@ -237,6 +269,10 @@ internal class ManagedCryptoCurrencyFactory(
): Boolean = token.id.isNullOrBlank() ||
checkIsCustomDerivationPath(token.derivationPath, blockchain, derivationStyleProvider)
private fun checkIsCustomToken(token: UserTokensResponse.Token, derivationPath: Network.DerivationPath): Boolean {
return token.id.isNullOrBlank() || derivationPath is Network.DerivationPath.Custom
}
private fun checkIsCustomDerivationPath(
derivationPath: String?,
blockchain: Blockchain,

View file

@ -22,6 +22,7 @@ import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.pagination.*
@ -250,6 +251,7 @@ internal class DefaultMarketsTokenRepository(
userWalletId: UserWalletId,
token: TokenMarketParams,
network: TokenMarketInfo.Network,
accountIndex: DerivationIndex?,
): CryptoCurrency? {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("UserWalletId [$userWalletId] not found")
val blockchain = Blockchain.fromNetworkId(network.networkId) ?: error("Unknown network [${network.networkId}]")
@ -259,12 +261,14 @@ internal class DefaultMarketsTokenRepository(
blockchain = blockchain,
extraDerivationPath = null,
userWallet = userWallet,
accountIndex = accountIndex,
)
} else {
val currencyNetwork = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
userWallet = userWallet,
accountIndex = accountIndex,
) ?: return null
cryptoCurrencyFactory.createToken(

View file

@ -29,6 +29,7 @@ dependencies {
implementation(projects.domain.walletManager)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.models)
implementation(projects.domain.express.models)
// region DI

View file

@ -12,7 +12,6 @@ import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.TangemExpressValues
import com.tangem.datasource.api.express.models.response.ExchangeProvider
import com.tangem.datasource.api.express.models.response.ExchangeProviderType
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
@ -38,6 +37,7 @@ 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.express.models.ExpressAsset
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -560,7 +560,7 @@ internal class DefaultOnrampRepository(
}
private fun CryptoCurrency.getContractAddress(): String = when (this) {
is CryptoCurrency.Coin -> TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
is CryptoCurrency.Coin -> ExpressAsset.EMPTY_CONTRACT_ADDRESS_VALUE
is CryptoCurrency.Token -> this.contractAddress
}

View file

@ -75,6 +75,7 @@ internal class HotCryptoCurrencyConverter(
}
}
// TODO account
private fun createNetwork(networkId: String): Network? {
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null

View file

@ -160,7 +160,7 @@ public val Blockchain.mercuryoNetwork: String?
Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> null
Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> null
Blockchain.Quai, Blockchain.QuaiTestnet -> null
// Blockchain.Linea, Blockchain.LineaTestnet -> null
// Blockchain.ArbitrumNova -> null
Blockchain.Linea, Blockchain.LineaTestnet -> null
Blockchain.ArbitrumNova -> null
}
}

View file

@ -28,6 +28,7 @@ dependencies {
implementation(projects.domain.card)
implementation(projects.domain.core)
implementation(projects.domain.demo)
implementation(projects.domain.express)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.staking)

View file

@ -1,9 +1,13 @@
package com.tangem.data.tokens
import arrow.core.Either
import arrow.core.right
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.datasource.api.tangemTech.models.account.flattenTokens
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
@ -15,6 +19,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
*
* @property userWalletsStore [UserWallet]'s store
* @property walletAccountsFetcher instance of [WalletAccountsFetcher] to fetch accounts for a multi wallet
* @property expressServiceFetcher fetcher of express service
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
@ -22,14 +27,26 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
internal class AccountListCryptoCurrenciesFetcher(
private val userWalletsStore: UserWalletsStore,
private val walletAccountsFetcher: WalletAccountsFetcher,
private val expressServiceFetcher: ExpressServiceFetcher,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiWalletCryptoCurrenciesFetcher {
override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) {
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
override suspend fun invoke(params: Params): Either<Throwable, Unit> {
return Either.catchOn(dispatchers.default) {
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet")
if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet")
walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
val response = walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
expressServiceFetcher.fetch(
userWallet = userWallet,
assetIds = response.flattenTokens().mapTo(hashSetOf()) {
ExpressAsset.ID(networkId = it.networkId, contractAddress = it.contractAddress)
},
)
Unit.right()
}
}
}

View file

@ -7,15 +7,14 @@ import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
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.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
@ -32,7 +31,7 @@ import timber.log.Timber
* @property userTokensResponseStore store of [UserTokensResponse]
* @property userTokensSaver user tokens saver
* @property cardCryptoCurrencyFactory factory for creating crypto currencies for specified card
* @property expressServiceLoader express service loader
* @property expressServiceFetcher express service loader
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
@ -46,7 +45,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher(
private val userTokensResponseStore: UserTokensResponseStore,
private val userTokensSaver: UserTokensSaver,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val expressServiceLoader: ExpressServiceLoader,
private val expressServiceFetcher: ExpressServiceFetcher,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiWalletCryptoCurrenciesFetcher {
@ -109,14 +108,14 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher(
}
private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) {
val tokens = userTokens.tokens.map { token ->
LeastTokenInfo(
contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = token.networkId,
val tokens = userTokens.tokens.mapTo(hashSetOf()) { token ->
ExpressAsset.ID(
networkId = token.networkId,
contractAddress = token.contractAddress,
)
}
expressServiceLoader.update(userWallet = userWallet, userTokens = tokens)
expressServiceFetcher.fetch(userWallet = userWallet, assetIds = tokens)
}
private fun createDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse {
@ -124,6 +123,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher(
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet),
isGroupedByNetwork = false,
isSortedByBalance = false,
accountId = null,
)
}
}

View file

@ -7,11 +7,11 @@ import com.tangem.data.tokens.AccountListCryptoCurrenciesFetcher
import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesFetcher
import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -33,7 +33,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule {
userTokensResponseStore: UserTokensResponseStore,
userTokensSaver: UserTokensSaver,
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
expressServiceLoader: ExpressServiceLoader,
expressServiceFetcher: ExpressServiceFetcher,
walletAccountsFetcher: WalletAccountsFetcher,
dispatchers: CoroutineDispatcherProvider,
): MultiWalletCryptoCurrenciesFetcher {
@ -41,6 +41,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule {
AccountListCryptoCurrenciesFetcher(
userWalletsStore = userWalletsStore,
walletAccountsFetcher = walletAccountsFetcher,
expressServiceFetcher = expressServiceFetcher,
dispatchers = dispatchers,
)
} else {
@ -56,7 +57,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule {
userTokensResponseStore = userTokensResponseStore,
userTokensSaver = userTokensSaver,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
expressServiceLoader = expressServiceLoader,
expressServiceFetcher = expressServiceFetcher,
dispatchers = dispatchers,
)
}

View file

@ -5,22 +5,14 @@ import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository
import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckRepository
import com.tangem.data.tokens.repository.DefaultTokenReceiveWarningsViewedRepository
import com.tangem.data.tokens.repository.DefaultYieldSupplyWarningsViewedRepository
import com.tangem.data.tokens.repository.*
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.TokenReceiveWarningActionStore
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
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -42,7 +34,7 @@ internal object TokensDataModule {
walletManagersFacade: WalletManagersFacade,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
expressServiceLoader: ExpressServiceLoader,
expressServiceFetcher: ExpressServiceFetcher,
excludedBlockchains: ExcludedBlockchains,
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
tokensSaver: UserTokensSaver,
@ -54,7 +46,7 @@ internal object TokensDataModule {
walletManagersFacade = walletManagersFacade,
cacheRegistry = cacheRegistry,
userTokensResponseStore = userTokensResponseStore,
expressServiceLoader = expressServiceLoader,
expressServiceFetcher = expressServiceFetcher,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,

View file

@ -10,17 +10,16 @@ import com.tangem.data.common.currency.*
import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
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.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
@ -40,7 +39,7 @@ internal class DefaultCurrenciesRepository(
private val userWalletsStore: UserWalletsStore,
private val walletManagersFacade: WalletManagersFacade,
private val cacheRegistry: CacheRegistry,
private val expressServiceLoader: ExpressServiceLoader,
private val expressServiceFetcher: ExpressServiceFetcher,
private val dispatchers: CoroutineDispatcherProvider,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val userTokensSaver: UserTokensSaver,
@ -74,26 +73,6 @@ internal class DefaultCurrenciesRepository(
userTokensSaver.storeAndPush(userWalletId, response)
}
override suspend fun saveCurrenciesLocal(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
withContext(dispatchers.io) {
val savedResponse = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action." },
)
val updatedResponse = savedResponse.copy(
tokens = currencies.map(userTokensResponseFactory::createResponseToken),
)
userTokensSaver.store(userWalletId = userWalletId, response = updatedResponse)
fetchExpressAssetsByNetworkIds(
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
userTokens = updatedResponse,
)
}
}
override suspend fun addCurrenciesCache(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
@ -322,50 +301,6 @@ internal class DefaultCurrenciesRepository(
)
}
override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId) =
withContext(dispatchers.io) {
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val storedTokens = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWallet.walletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCryptoCurrenciesFactory.createCurrencies(
storedTokens,
userWallet = userWallet,
)
}
override suspend fun getMultiCurrencyWalletCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency = withContext(dispatchers.io) {
getMultiCurrencyWalletCurrency(userWalletId, id.value)
}
override suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency =
withContext(dispatchers.io) {
val userWallet = userWalletsStore.getSyncStrict(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val response = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCryptoCurrenciesFactory.createCurrency(
currencyId = id,
response = response,
userWallet = userWallet,
)
}
override suspend fun getNetworkCoin(
userWalletId: UserWalletId,
networkId: Network.ID,
@ -667,16 +602,17 @@ internal class DefaultCurrenciesRepository(
return demoConfig.isDemoCardId(userWallet.cardId) && response == null
}
// TODO [REDACTED_JIRA]
private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) {
val tokens = userTokens.tokens.map { token ->
LeastTokenInfo(
contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = token.networkId,
val tokens = userTokens.tokens.mapTo(hashSetOf()) { token ->
ExpressAsset.ID(
networkId = token.networkId,
contractAddress = token.contractAddress,
)
}
coroutineScope {
launch { expressServiceLoader.update(userWallet, tokens) }
launch { expressServiceFetcher.fetch(userWallet, tokens) }
}
}
@ -685,11 +621,11 @@ internal class DefaultCurrenciesRepository(
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean = false,
) {
val tokens = cryptoCurrencies.map { currency ->
val tokens = cryptoCurrencies.mapTo(hashSetOf()) { currency ->
val tokenCurrency = currency as? CryptoCurrency.Token
LeastTokenInfo(
contractAddress = tokenCurrency?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = currency.network.backendId,
ExpressAsset.ID(
networkId = currency.network.backendId,
contractAddress = tokenCurrency?.contractAddress,
)
}
cacheRegistry.invokeOnExpire(
@ -697,7 +633,7 @@ internal class DefaultCurrenciesRepository(
skipCache = refresh,
block = {
coroutineScope {
launch { expressServiceLoader.update(userWallet, tokens) }
launch { expressServiceFetcher.fetch(userWallet, tokens) }
}
},
)
@ -731,6 +667,7 @@ internal class DefaultCurrenciesRepository(
),
isGroupedByNetwork = false,
isSortedByBalance = false,
accountId = null,
)
private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) {

View file

@ -1,10 +1,13 @@
package com.tangem.data.tokens
import arrow.core.left
import arrow.core.right
import com.tangem.common.test.utils.assertEither
import com.tangem.common.test.utils.assertEitherRight
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
@ -21,11 +24,13 @@ internal class AccountListCryptoCurrenciesFetcherTest {
private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true)
private val walletAccountsFetcher: WalletAccountsFetcher = mockk(relaxUnitFun = true)
private val expressServiceFetcher: ExpressServiceFetcher = mockk()
private val dispatchers = TestingCoroutineDispatcherProvider()
private val fetcher = AccountListCryptoCurrenciesFetcher(
userWalletsStore = userWalletsStore,
walletAccountsFetcher = walletAccountsFetcher,
expressServiceFetcher = expressServiceFetcher,
dispatchers = dispatchers,
)
@ -59,8 +64,11 @@ internal class AccountListCryptoCurrenciesFetcherTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet> { every { isMultiCurrency } returns true }
val response = mockk<GetWalletAccountsResponse>(relaxed = true)
every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet
coEvery { walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } returns response
coEvery { expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet()) } returns Unit.right()
// Act
val actual = fetcher(params)
@ -71,6 +79,7 @@ internal class AccountListCryptoCurrenciesFetcherTest {
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsStore.getSyncStrict(key = params.userWalletId)
walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet())
}
}

View file

@ -11,14 +11,13 @@ import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
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.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
@ -45,7 +44,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true)
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
private val expressServiceLoader: ExpressServiceLoader = mockk(relaxUnitFun = true)
private val expressServiceFetcher: ExpressServiceFetcher = mockk(relaxUnitFun = true)
private val fetcher = DefaultMultiWalletCryptoCurrenciesFetcher(
demoConfig = DemoConfig(),
@ -55,7 +54,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
userTokensResponseStore = userTokensResponseStore,
userTokensSaver = userTokensSaver,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
expressServiceLoader = expressServiceLoader,
expressServiceFetcher = expressServiceFetcher,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@ -67,7 +66,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
userTokensResponseStore,
userTokensSaver,
cardCryptoCurrencyFactory,
expressServiceLoader,
expressServiceFetcher,
)
}
@ -131,6 +130,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
} returns userTokensResponse
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = userTokensResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
@ -144,7 +150,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens())
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId())
}
}
@ -170,6 +176,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
} returns apiResponse.data
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = defaultResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
@ -183,7 +196,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data)
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens())
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
}
coVerify(inverse = true) {
@ -212,6 +225,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
} returns apiResponse.data
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = apiResponse.data.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
@ -224,7 +244,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data)
userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data)
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens())
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
}
coVerify(inverse = true) {
@ -273,6 +293,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
} returns userTokensResponse
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = userTokensResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
@ -286,7 +313,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens())
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId())
}
coVerify(inverse = true) {
@ -317,6 +344,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
} returns defaultResponse
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = defaultResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
@ -330,7 +364,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse)
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens())
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
}
coVerify(inverse = true) {
@ -383,6 +417,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
} returns userTokensResponse
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = userTokensResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
@ -398,7 +439,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
userTokensSaver.push(userWalletId = params.userWalletId, response = userTokensResponse)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse)
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens())
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId())
}
}
@ -429,6 +470,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
} returns defaultResponse
coEvery {
expressServiceFetcher.fetch(
userWallet = mockUserWallet,
assetIds = defaultResponse.toAssetId(),
)
} returns Unit.right()
// Act
val actual = fetcher(params)
@ -443,7 +491,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
userTokensSaver.push(userWalletId = params.userWalletId, response = defaultResponse)
customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse)
userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse)
expressServiceLoader.update(userWallet = mockUserWallet, userTokens = defaultResponse.toLeastTokens())
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId())
}
coVerify(inverse = true) {
@ -471,11 +519,11 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest {
),
)
fun UserTokensResponse.toLeastTokens(): List<LeastTokenInfo> {
return tokens.map { token ->
LeastTokenInfo(
contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE,
network = token.networkId,
fun UserTokensResponse.toAssetId(): Set<ExpressAsset.ID> {
return tokens.mapTo(hashSetOf()) { token ->
ExpressAsset.ID(
networkId = token.networkId,
contractAddress = token.contractAddress,
)
}
}

View file

@ -1,8 +1,10 @@
package com.tangem.data.pay.repository
import com.squareup.moshi.Moshi
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.visa.utils.TangemPayTxHistoryItemConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
@ -26,8 +28,11 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
private val cacheRegistry: CacheRegistry,
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
private val dispatchers: CoroutineDispatcherProvider,
@NetworkMoshi private val moshi: Moshi,
) : TangemPayTxHistoryRepository {
private val txHistoryItemConverter by lazy { TangemPayTxHistoryItemConverter(moshi) }
override fun getTxHistoryBatchFlow(
batchSize: Int,
context: TangemPayTxHistoryListBatchingContext,
@ -84,7 +89,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
val result = requestPerformer.request { authHeader ->
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
}.result
val items = TangemPayTxHistoryItemConverter.convertList(result.transactions).filterNotNull()
val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull()
txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items)
}.onLeft { error(it.toString()) }
}

View file

@ -1,14 +1,19 @@
package com.tangem.data.visa.utils
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.pay.models.response.TangemPayTxHistoryResponse
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.utils.converter.Converter
import timber.log.Timber
import java.util.Currency
internal object TangemPayTxHistoryItemConverter :
internal class TangemPayTxHistoryItemConverter(moshi: Moshi) :
Converter<TangemPayTxHistoryResponse.Transaction, TangemPayTxHistoryItem?> {
private val spendAdapter = moshi.adapter(TangemPayTxHistoryResponse.Spend::class.java)
private val paymentAdapter = moshi.adapter(TangemPayTxHistoryResponse.Payment::class.java)
private val feeAdapter = moshi.adapter(TangemPayTxHistoryResponse.Fee::class.java)
override fun convert(value: TangemPayTxHistoryResponse.Transaction): TangemPayTxHistoryItem? {
return value.spend?.let { convertSpend(id = value.id, spend = it) }
?: value.payment?.let { convertPayment(id = value.id, payment = it) }
@ -22,6 +27,7 @@ internal object TangemPayTxHistoryItemConverter :
private fun convertSpend(id: String, spend: TangemPayTxHistoryResponse.Spend): TangemPayTxHistoryItem.Spend {
return TangemPayTxHistoryItem.Spend(
id = id,
jsonRepresentation = spendAdapter.toJson(spend),
// If postedAt is null, it means transaction wasn't posted and was likely declined. Use authorizedAt
date = spend.postedAt ?: spend.authorizedAt,
amount = spend.amount,
@ -41,15 +47,18 @@ internal object TangemPayTxHistoryItemConverter :
): TangemPayTxHistoryItem.Payment {
return TangemPayTxHistoryItem.Payment(
id = id,
jsonRepresentation = paymentAdapter.toJson(payment),
date = payment.postedAt,
currency = Currency.getInstance(payment.currency),
amount = payment.amount,
transactionHash = payment.transactionHash,
)
}
private fun convertFee(id: String, fee: TangemPayTxHistoryResponse.Fee): TangemPayTxHistoryItem.Fee {
return TangemPayTxHistoryItem.Fee(
id = id,
jsonRepresentation = feeAdapter.toJson(fee),
date = fee.postedAt,
currency = Currency.getInstance(fee.currency),
amount = fee.amount,