Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-17 15:02:06 +04:00
parent b27e8171fb
commit 4ea8243da3
19 changed files with 375 additions and 126 deletions

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

@ -4,8 +4,8 @@ import arrow.core.Option
import arrow.core.some
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
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,10 +48,13 @@ 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()

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(
@ -89,6 +90,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 +101,7 @@ class CryptoCurrencyFactory(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
userWallet = userWallet,
accountIndex = accountIndex,
) ?: return null
return createCoin(network)

View file

@ -5,13 +5,12 @@ 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.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
@ -140,15 +139,32 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>> {
val response = getUserTokensResponse(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
@ -158,24 +174,29 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
rawIds: Set<Network.RawID>,
): Map<Network.RawID, List<CryptoCurrency>> {
val response = getUserTokensResponse(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,
)
.groupBy { it.network.id.rawId }
}
private suspend fun getUserTokensResponse(userWalletId: UserWalletId): UserTokensResponse? {
return if (accountsFeatureToggles.isFeatureEnabled) {
walletAccountsFetcher.getSaved(userWalletId)?.toUserTokensResponse()
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 {
userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
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 }
}
private fun getSingleWalletCurrencies(userWallet: UserWallet.Cold): SingleWalletCurrencies {

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
@ -24,19 +25,31 @@ class ResponseCryptoCurrenciesFactory @Inject constructor(
.first { it.id.value == currencyId }
}
fun createCurrencies(response: UserTokensResponse, userWallet: UserWallet): List<CryptoCurrency> {
return createCurrencies(tokens = response.tokens, userWallet = userWallet)
fun createCurrencies(
response: UserTokensResponse,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): List<CryptoCurrency> {
return createCurrencies(tokens = response.tokens, userWallet = userWallet, accountIndex = accountIndex)
}
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 +62,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 +84,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 +121,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

@ -12,7 +12,6 @@ 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(

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.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,
)
@ -134,10 +145,11 @@ class NetworkFactory @Inject constructor(
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 {

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

@ -17,10 +17,12 @@ 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.*
import com.tangem.domain.card.common.util.cardTypesResolver
@ -51,10 +53,15 @@ internal class DefaultManageTokensRepository(
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,
@ -126,17 +136,73 @@ internal class DefaultManageTokensRepository(
coins = coinsResponse.coins.filterNot { l2BlockchainsCoinIds.contains(it.id) },
)
val tokensResponse = request.params.userWalletId?.let { userWalletId ->
when (val params = request.params) {
is ManageTokensListConfig.Account -> {
fetchUserTokens(userWallet, userWalletId, params, loadUserTokensFromRemote)
}
is ManageTokensListConfig.Wallet -> {
fetchUserTokensLegacy(userWallet, userWalletId, loadUserTokensFromRemote)
}
val items = when (val params = request.params) {
is ManageTokensListConfig.Account -> createManagedCryptoCurrencyList(
params = params,
userWallet = userWallet,
isFirstBatchFetching = isFirstBatchFetching,
loadUserTokensFromRemote = loadUserTokensFromRemote,
query = query,
updatedCoinsResponse = updatedCoinsResponse,
)
is ManageTokensListConfig.Wallet -> createManagedCryptoCurrencyListLegacy(
params = params,
userWallet = userWallet,
isFirstBatchFetching = isFirstBatchFetching,
loadUserTokensFromRemote = loadUserTokensFromRemote,
query = query,
updatedCoinsResponse = updatedCoinsResponse,
)
}
return BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = items.size < request.limit,
)
}
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 &&
@ -147,69 +213,58 @@ internal class DefaultManageTokensRepository(
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 BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = items.size < request.limit,
)
return items
}
private suspend fun fetchUserTokens(
private suspend fun createManagedCryptoCurrencyListLegacy(
params: ManageTokensListConfig.Wallet,
userWallet: UserWallet?,
userWalletId: UserWalletId,
params: ManageTokensListConfig.Account,
isFirstBatchFetching: Boolean,
loadUserTokensFromRemote: Boolean,
): UserTokensResponse? {
val accountId = when {
params.accountId == null -> {
return null
}
loadUserTokensFromRemote -> {
AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = DerivationIndex.Main)
}
else -> requireNotNull(params.accountId)
}
val response = if (loadUserTokensFromRemote && userWallet != null) {
runCatching { walletAccountsFetcher.fetch(userWalletId = userWallet.walletId) }.getOrNull()
} else {
walletAccountsFetcher.getSaved(userWalletId)
}
val account = response?.accounts?.firstOrNull { it.id == accountId.value }
?: return null
return UserTokensResponse(
group = response.wallet.group,
sort = response.wallet.sort,
tokens = account.tokens.orEmpty(),
)
}
private suspend fun fetchUserTokensLegacy(
userWallet: UserWallet?,
userWalletId: UserWalletId,
loadUserTokensFromRemote: Boolean,
): UserTokensResponse? {
return if (loadUserTokensFromRemote && userWallet != null) {
safeApiCall(
call = { tangemTechApi.getUserTokens(userWalletId.stringValue).bind() },
onError = {
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 {
getSavedUserTokensResponseSync(userWalletId)
managedCryptoCurrencyFactory.create(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
userWallet = userWallet,
accountIndex = null,
)
}
}
@ -221,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(
@ -239,6 +344,7 @@ internal class DefaultManageTokensRepository(
},
tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId),
userWallet = userWallet,
accountIndex = null,
)
return BatchFetchResult.Success(

View file

@ -12,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
@ -39,6 +40,7 @@ internal object ManageTokensDataModule {
excludedBlockchains: ExcludedBlockchains,
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
networkFactory: NetworkFactory,
accountsFeatureToggles: AccountsFeatureToggles,
walletAccountsFetcher: WalletAccountsFetcher,
): ManageTokensRepository {
return DefaultManageTokensRepository(
@ -52,6 +54,7 @@ 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

@ -90,6 +90,14 @@ class ManageCryptoCurrenciesUseCase(
derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
parallelUpdatingScope.launch {
/*
* If only removal of currencies happened, we need to sync tokens. Otherwise, tokens will be synced
* when balances are refreshed for added currencies.
*/
if (modifiedCurrencyList.added.isEmpty() && modifiedCurrencyList.removed.isNotEmpty()) {
launch { accountsCRUDRepository.syncTokens(userWalletId) }
}
refreshBalances(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total)
clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed)
@ -119,7 +127,7 @@ class ManageCryptoCurrenciesUseCase(
val tokenToAdd = findToken(userWalletId, contractAddress, networkId)
val modifiedCurrencyList = account.cryptoCurrencies.modify(add = listOf(tokenToAdd), remove = emptyList())
val modifiedCurrencyList = account.cryptoCurrencies.modify(add = listOf(tokenToAdd))
saveAccount(account = account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()))
@ -143,7 +151,7 @@ class ManageCryptoCurrenciesUseCase(
private fun Set<CryptoCurrency>.modify(
add: List<CryptoCurrency>,
remove: List<CryptoCurrency>,
remove: List<CryptoCurrency> = emptyList(),
): ModifiedCurrencyList {
val mutableCurrencies = this.toMutableList()
val added = mutableListOf<CryptoCurrency>()
@ -176,9 +184,8 @@ class ManageCryptoCurrenciesUseCase(
remove.groupByNetwork(valuePredicate = existingCurrenciesById::containsKey)
.forEach { (network, currenciesById) ->
val coinTempId = TempID(network)
if (currenciesById.containsKey(coinTempId)) {
val isCoinBeingRemoved = currenciesById.any { it.value is CryptoCurrency.Coin }
if (isCoinBeingRemoved) {
val existingNetworkCurrenciesCount = mutableCurrencies.count { it.network == network }
if (existingNetworkCurrenciesCount != currenciesById.size) {
@ -306,7 +313,7 @@ class ManageCryptoCurrenciesUseCase(
private suspend fun clearMetadata(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
return coroutineScope {
coroutineScope {
listOf(
launch { networksCleaner(userWalletId = userWalletId, currencies = currencies) },
launch { clearStaking(userWalletId = userWalletId, currencies = currencies) },

View file

@ -1,6 +1,7 @@
package 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
@ -11,11 +12,13 @@ class GetTokenMarketCryptoCurrency(
userWalletId: UserWalletId,
tokenMarketParams: TokenMarketParams,
network: TokenMarketInfo.Network,
accountIndex: DerivationIndex? = null,
): CryptoCurrency? {
return marketsTokenRepository.createCryptoCurrency(
userWalletId = userWalletId,
token = tokenMarketParams,
network = network,
accountIndex = accountIndex,
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.markets.repositories
import com.tangem.domain.markets.*
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -45,6 +46,7 @@ interface MarketsTokenRepository {
userWalletId: UserWalletId,
token: TokenMarketParams,
network: TokenMarketInfo.Network,
accountIndex: DerivationIndex? = null,
): CryptoCurrency?
/**

View file

@ -95,6 +95,14 @@ data class Network(
data object None : DerivationPath() {
override val value: String? get() = null
}
fun copySealed(value: String): DerivationPath {
return when (this) {
is Card -> Card(value)
is Custom -> Custom(value)
None -> None
}
}
}
/**

View file

@ -7,6 +7,8 @@ import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase
import com.tangem.domain.models.account.AccountId
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.tokens.AddCryptoCurrenciesUseCase
@ -27,7 +29,13 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either<Throwable, Unit> = when (mode) {
is AddCustomTokenMode.Account -> {
manageCryptoCurrenciesUseCase(accountId = mode.accountId, add = currency)
manageCryptoCurrenciesUseCase(
accountId = AccountId.forCryptoPortfolio(
userWalletId = mode.userWalletId,
derivationIndex = DerivationIndex.Main,
),
add = currency,
)
}
is AddCustomTokenMode.Wallet -> {
addCryptoCurrenciesUseCase.invoke(

View file

@ -13,6 +13,7 @@ import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -243,6 +244,7 @@ internal class AddToPortfolioModel @Inject constructor(
cryptoCurrency = createCryptoCurrency(
userWallet = selectedPortfolio.userWallet,
network = selectedNetwork,
account = selectedPortfolio.account,
) ?: return@transform null,
selectedNetwork = selectedNetwork,
availableMoreNetwork = !selectedPortfolio.account.isSingleNetwork,
@ -254,10 +256,12 @@ internal class AddToPortfolioModel @Inject constructor(
private suspend fun createCryptoCurrency(
userWallet: UserWallet,
network: TokenMarketInfo.Network,
account: AvailableToAddAccount,
): CryptoCurrency? = getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = addToPortfolioManager.token,
network = network,
accountIndex = (account.account as? AccountStatus.CryptoPortfolio)?.account?.derivationIndex,
)
private fun routeToNetworkSelector(portfolio: SelectedPortfolio): AddToPortfolioRoutes.NetworkSelector {

View file

@ -66,12 +66,11 @@ internal class AddTokenModel @Inject constructor(
val blockchainNames = listOf(selectedNetwork.selectedNetwork)
.mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name }
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames))
val cryptoCurrency = selectedNetwork.cryptoCurrency
val accountId = selectedPortfolio.account.account.account.accountId
manageCryptoCurrenciesUseCase(
accountId = accountId,
add = cryptoCurrency,
)
manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency)
val status = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = accountId.userWalletId,
currencyId = cryptoCurrency.id,