Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-13 12:58:22 +04:00
parent 9fb5124c38
commit 3b8da73b9c
18 changed files with 373 additions and 433 deletions

View file

@ -1,35 +1,30 @@
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.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncStrict
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.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params
import com.tangem.domain.tokens.MultiWalletAccountListFetcher
import com.tangem.domain.tokens.MultiWalletAccountListFetcher.Params
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* Implementation of [MultiWalletCryptoCurrenciesFetcher] that fetches crypto currencies of all accounts
* Implementation of [MultiWalletAccountListFetcher] that fetches the account list for a multi-currency wallet
* by delegating to [WalletAccountsFetcher].
*
* @property userWalletsListRepository repository to get user wallets
* @property walletAccountsFetcher instance of [WalletAccountsFetcher] to fetch accounts for a multi wallet
* @property expressServiceFetcher fetcher of express service
* @property dispatchers dispatchers
* @property dispatchers provider for coroutine dispatchers used to run fetch operations
*
[REDACTED_AUTHOR]
*/
internal class AccountListCryptoCurrenciesFetcher(
private val userWalletsListRepository: UserWalletsListRepository,
private val walletAccountsFetcher: WalletAccountsFetcher,
private val expressServiceFetcher: ExpressServiceFetcher,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiWalletCryptoCurrenciesFetcher {
) : MultiWalletAccountListFetcher {
override suspend fun invoke(params: Params): Either<Throwable, Unit> {
return Either.catchOn(dispatchers.default) {
@ -37,16 +32,7 @@ internal class AccountListCryptoCurrenciesFetcher(
if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet")
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()
walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
}
}
}

View file

@ -3,8 +3,7 @@ package com.tangem.data.tokens.di
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.tokens.AccountListCryptoCurrenciesFetcher
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
import com.tangem.domain.tokens.MultiWalletAccountListFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -14,20 +13,18 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal class MultiWalletCryptoCurrenciesFetcherModule {
internal class MultiWalletAccountListFetcherModule {
@Singleton
@Provides
fun provideMultiWalletCryptoCurrenciesFetcher(
fun provideMultiWalletAccountListFetcher(
userWalletsListRepository: UserWalletsListRepository,
walletAccountsFetcher: WalletAccountsFetcher,
expressServiceFetcher: ExpressServiceFetcher,
dispatchers: CoroutineDispatcherProvider,
): MultiWalletCryptoCurrenciesFetcher {
): MultiWalletAccountListFetcher {
return AccountListCryptoCurrenciesFetcher(
userWalletsListRepository = userWalletsListRepository,
walletAccountsFetcher = walletAccountsFetcher,
expressServiceFetcher = expressServiceFetcher,
dispatchers = dispatchers,
)
}

View file

@ -1,8 +1,6 @@
package com.tangem.data.tokens.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository
import com.tangem.data.tokens.repository.DefaultTokenReceiveWarningsViewedRepository
@ -11,7 +9,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.TokenReceiveWarningActionStore
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
@ -34,21 +31,15 @@ internal object TokensDataModule {
tangemTechApi: TangemTechApi,
userWalletsListRepository: UserWalletsListRepository,
walletManagersFacade: WalletManagersFacade,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
expressServiceFetcher: ExpressServiceFetcher,
excludedBlockchains: ExcludedBlockchains,
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
tangemTechApi = tangemTechApi,
userWalletsListRepository = userWalletsListRepository,
walletManagersFacade = walletManagersFacade,
cacheRegistry = cacheRegistry,
expressServiceFetcher = expressServiceFetcher,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
)
}

View file

@ -5,119 +5,33 @@ import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getTokenId
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncStrict
import com.tangem.domain.core.error.DataError
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
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
private val userWalletsListRepository: UserWalletsListRepository,
private val walletManagersFacade: WalletManagersFacade,
private val cacheRegistry: CacheRegistry,
private val expressServiceFetcher: ExpressServiceFetcher,
private val dispatchers: CoroutineDispatcherProvider,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
excludedBlockchains: ExcludedBlockchains,
) : CurrenciesRepository {
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
override suspend fun getSingleCurrencyWalletPrimaryCurrency(
userWalletId: UserWalletId,
refresh: Boolean,
): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
userWallet.requireColdWallet()
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(
userWallet = userWallet,
)
fetchExpressAssetsByNetworkIds(
userWallet = userWallet,
cryptoCurrencies = listOf(currency),
refresh = refresh,
)
currency
}
}
override suspend fun getSingleCurrencyWalletWithCardCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): List<CryptoCurrency> {
return withContext(dispatchers.io) {
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
val scanResponse = userWallet.requireColdWallet().scanResponse
val currencies = if (scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = userWallet)
} else {
cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet)
.run {
listOf(this)
}
}
fetchExpressAssetsByNetworkIds(
userWallet = userWallet,
cryptoCurrencies = currencies,
refresh = refresh,
)
currencies
}
}
override suspend fun getSingleCurrencyWalletWithCardCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
userWallet.requireColdWallet()
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(
userWallet = userWallet,
)
.find { it.id == id }
requireNotNull(currency) { "Unable to find currency with provided ID: $id" }
fetchExpressAssetsByNetworkIds(userWallet, listOf(currency))
currency
}
}
override suspend fun isSendBlockedByPendingTransactions(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
@ -223,56 +137,4 @@ internal class DefaultCurrenciesRepository(
val blockchain = Blockchain.fromNetworkId(network.backendId)
return blockchain?.isNetworkFeeZero() == true
}
override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? {
return (userWalletsListRepository.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver
}
private suspend fun fetchExpressAssetsByNetworkIds(
userWallet: UserWallet,
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean = false,
) {
val tokens = cryptoCurrencies.mapTo(hashSetOf()) { currency ->
val tokenCurrency = currency as? CryptoCurrency.Token
ExpressAsset.ID(
networkId = currency.network.backendId,
contractAddress = tokenCurrency?.contractAddress,
)
}
cacheRegistry.invokeOnExpire(
key = getAssetsCacheKey(userWallet.walletId),
skipCache = refresh,
block = {
coroutineScope {
launch { expressServiceFetcher.fetch(userWallet, tokens) }
}
},
)
}
private fun getAssetsCacheKey(userWalletId: UserWalletId): String = "assets_cache_key_${userWalletId.stringValue}"
private fun ensureIsCorrectUserWallet(userWallet: UserWallet, isMultiCurrencyWalletExpected: Boolean) {
val userWalletId = userWallet.walletId
val message = when {
!userWallet.isMultiCurrency && isMultiCurrencyWalletExpected -> {
"Multi currency wallet expected, but single currency wallet was found: $userWalletId"
}
userWallet.isMultiCurrency && !isMultiCurrencyWalletExpected -> {
"Single currency wallet expected, but multi currency wallet was found: $userWalletId"
}
else -> null
}
if (message != null) {
val error = DataError.UserWalletError.WrongUserWallet(message)
Timber.e(error)
throw error
}
}
}

View file

@ -1,15 +1,13 @@
package com.tangem.data.tokens
import arrow.core.left
import arrow.core.right
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.domain.common.wallets.UserWalletsListRepository
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
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher
import com.tangem.domain.tokens.MultiWalletAccountListFetcher
import com.tangem.test.core.assertEither
import com.tangem.test.core.assertEitherRight
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
@ -25,13 +23,11 @@ internal class AccountListCryptoCurrenciesFetcherTest {
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val walletAccountsFetcher: WalletAccountsFetcher = mockk(relaxUnitFun = true)
private val expressServiceFetcher: ExpressServiceFetcher = mockk()
private val dispatchers = TestingCoroutineDispatcherProvider()
private val fetcher = AccountListCryptoCurrenciesFetcher(
userWalletsListRepository = userWalletsListRepository,
walletAccountsFetcher = walletAccountsFetcher,
expressServiceFetcher = expressServiceFetcher,
dispatchers = dispatchers,
)
@ -43,7 +39,7 @@ internal class AccountListCryptoCurrenciesFetcherTest {
@Test
fun `returns failure if wallet is not multi-currency`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val params = MultiWalletAccountListFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns false
@ -68,7 +64,7 @@ internal class AccountListCryptoCurrenciesFetcherTest {
@Test
fun `returns accounts if wallet is multi-currency`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val params = MultiWalletAccountListFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns true
@ -79,7 +75,6 @@ internal class AccountListCryptoCurrenciesFetcherTest {
every { userWalletsListRepository.userWallets } returns userWalletsFlow
coEvery { walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } returns response
coEvery { expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet()) } returns Unit.right()
// Act
val actual = fetcher(params)
@ -90,14 +85,13 @@ internal class AccountListCryptoCurrenciesFetcherTest {
coVerify(ordering = Ordering.SEQUENCE) {
userWalletsListRepository.userWallets
walletAccountsFetcher.fetch(userWalletId = params.userWalletId)
expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet())
}
}
@Test
fun `returns error if walletAccountsFetcher returns error`() = runTest {
// Arrange
val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId)
val params = MultiWalletAccountListFetcher.Params(userWalletId = userWalletId)
val mockUserWallet = mockk<UserWallet> {
every { walletId } returns userWalletId
every { isMultiCurrency } returns true