Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-16 15:36:03 +04:00
parent dcb26aa182
commit e0a845a474
8 changed files with 138 additions and 62 deletions

View file

@ -1,8 +1,8 @@
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
@ -21,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]
*/
@ -29,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 {
@ -61,10 +61,9 @@ 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

@ -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,
)
}
}
@ -107,21 +107,17 @@ class DefaultWalletAccountsResponseFactoryTest {
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
val defaultCoins = listOf(mockk<CryptoCurrency.Coin>())
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
@ -148,11 +144,9 @@ 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,
)
}
@ -169,17 +163,17 @@ class DefaultWalletAccountsResponseFactoryTest {
.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

View file

@ -1,12 +1,15 @@
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.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
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.currency.CryptoCurrency
@ -23,10 +26,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 +105,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,7 +140,7 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>> {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
val response = getUserTokensResponse(userWalletId = userWallet.walletId)
?: return emptyMap()
val existingNetworkWithCurrencies = responseCryptoCurrenciesFactory.createCurrencies(
@ -170,7 +158,7 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
rawIds: Set<Network.RawID>,
): Map<Network.RawID, List<CryptoCurrency>> {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
val response = getUserTokensResponse(userWalletId = userWallet.walletId)
?: return emptyMap()
val networkIds = rawIds.map { it.toBlockchain().toNetworkId() }
@ -182,6 +170,14 @@ internal class DefaultCardCryptoCurrencyFactory(
.groupBy { it.network.id.rawId }
}
private suspend fun getUserTokensResponse(userWalletId: UserWalletId): UserTokensResponse? {
return if (accountsFeatureToggles.isFeatureEnabled) {
walletAccountsFetcher.getSaved(userWalletId)?.toUserTokensResponse()
} else {
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
}
}
private fun getSingleWalletCurrencies(userWallet: UserWallet.Cold): SingleWalletCurrencies {
val resolver = userWallet.cardTypesResolver
val blockchain = resolver.getBlockchain()

View file

@ -1,8 +1,15 @@
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]
@ -43,4 +50,38 @@ class UserTokensResponseFactory @Inject constructor() {
)
}
}
fun createDefaultResponse(
userWallet: UserWallet?,
networkFactory: NetworkFactory,
accountId: AccountId? = null,
): 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,10 @@ 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.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import timber.log.Timber
import javax.inject.Inject
@ -130,7 +130,7 @@ class NetworkFactory @Inject constructor(
return true
}
private fun createDerivationPath(
fun createDerivationPath(
blockchain: Blockchain,
extraDerivationPath: String?,
cardDerivationStyleProvider: DerivationStyleProvider?,

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