Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-29 15:48:01 +03:00
commit 5f4318ca5d
39 changed files with 619 additions and 120 deletions

View file

@ -1,5 +1,8 @@
package com.tangem.tap.di.domain
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
import com.tangem.domain.managetokens.GetManagedTokensUseCase
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
@ -44,4 +47,16 @@ internal object ManageTokensDomainModule {
): CheckIsCurrencyNotAddedUseCase {
return CheckIsCurrencyNotAddedUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideSaveManagedTokensUseCase(
manageTokensRepository: ManageTokensRepository,
derivationsRepository: DerivationsRepository,
): SaveManagedTokensUseCase {
return SaveManagedTokensUseCase(
manageTokensRepository = manageTokensRepository,
derivationsRepository = derivationsRepository,
)
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
@ -30,6 +31,10 @@ internal class DefaultDerivationsRepository(
) : DerivationsRepository {
override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network))
}
override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List<Network>) {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
@ -38,7 +43,7 @@ internal class DefaultDerivationsRepository(
}
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
.find(currencies)
.findByNetworks(networks)
.ifEmpty {
Timber.d("Nothing to derive")
return

View file

@ -11,6 +11,7 @@ import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.operations.derivation.ExtendedPublicKeysMap
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
@ -26,8 +27,12 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
/** Find missed derivations for given currencies [currencies] */
fun find(currencies: List<CryptoCurrency>): Derivations {
return currencies.map { it.network }.let(::findByNetworks)
}
fun findByNetworks(networks: List<Network>): Derivations {
return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
currencies
networks
.mapToNewDerivations()
.forEach { data ->
val current = this[data.first]
@ -41,25 +46,25 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
}
}
private fun List<CryptoCurrency>.mapToNewDerivations(): List<DerivationData> {
private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
val config = CardConfig.createConfig(scanResponse.card)
return mapNotNull { currency ->
val blockchain = Blockchain.fromId(id = currency.network.id.value)
return mapNotNull { network ->
val blockchain = Blockchain.fromId(id = network.id.value)
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
findNewDerivations(curve = curve, scanResponse = scanResponse, currency = currency)
findNewDerivations(curve = curve, scanResponse = scanResponse, network = network)
}
}
private fun findNewDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: CryptoCurrency,
network: Network,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val publicKey = wallet.publicKey.toMapKey()
val derivationCandidates = currency
val derivationCandidates = network
.getDerivationCandidates(curve)
.ifEmpty { return null }
.filterAlreadyDerivedKeys(publicKey)
@ -68,13 +73,13 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
return publicKey to derivationCandidates
}
private fun CryptoCurrency.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = Blockchain.fromId(id = network.id.value)
private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = Blockchain.fromId(id = this.id.value)
return buildList {
add(blockchain.getDerivationPath(curve = curve))
add(blockchain.getCustomDerivationPath(curve = curve, currency = this@getDerivationCandidates))
add(blockchain.getCardanoDerivationPathIfNeeded(currency = this@getDerivationCandidates))
add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates))
add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates))
}
.filterNotNull()
.distinct()
@ -88,17 +93,17 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
}
}
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, currency: CryptoCurrency): DerivationPath? {
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? {
return if (getSupportedCurves().contains(curve)) {
currency.network.derivationPath.value?.let(::DerivationPath)
network.derivationPath.value?.let(::DerivationPath)
} else {
null
}
}
private fun Blockchain.getCardanoDerivationPathIfNeeded(currency: CryptoCurrency): DerivationPath? {
return if (currency is CryptoCurrency.Coin && this == Blockchain.Cardano) {
currency.network.derivationPath.value?.let {
private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? {
return if (this == Blockchain.Cardano) {
network.derivationPath.value?.let {
CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it))
}
} else {

View file

@ -30,6 +30,7 @@ class UserWalletItemUMConverter(
private val balance: TotalFiatBalance? = null,
private val isLoading: Boolean = true,
private val isBalanceHidden: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
) : Converter<UserWallet, UserWalletItemUM> {
override fun convert(value: UserWallet): UserWalletItemUM {
@ -45,6 +46,7 @@ class UserWalletItemUMConverter(
),
imageUrl = artworkUrl,
isEnabled = !isLocked,
endIcon = endIcon,
onClick = { onClick(value.walletId) },
)
}

View file

@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrency.ID
import com.tangem.domain.tokens.model.Network
import com.tangem.blockchain.common.Token as SdkToken
@ -65,10 +64,10 @@ fun getCoinIconUrl(blockchain: Blockchain): String? {
return coinId?.let(::getTokenIconUrlFromDefaultHost)
}
fun List<UserTokensResponse.Token>.hasCoinForToken(token: CryptoCurrency.Token): Boolean {
fun List<UserTokensResponse.Token>.hasCoinForToken(network: Network): Boolean {
return any {
val blockchain = getBlockchain(networkId = token.network.id)
val tokenDerivation = token.network.derivationPath.value
val blockchain = getBlockchain(networkId = network.id)
val tokenDerivation = network.derivationPath.value
it.id == blockchain.toCoinId() && it.derivationPath == tokenDerivation
}
}

View file

@ -16,10 +16,12 @@ dependencies {
implementation(projects.domain.manageTokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.legacy)
/** Project - Data */
implementation(projects.core.datasource)
implementation(projects.data.common)
implementation(projects.data.tokens)
/** Project - Utils */
implementation(projects.core.utils)

View file

@ -3,15 +3,27 @@ package com.tangem.data.managetokens
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.isSupportedInApp
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.currency.getBlockchain
import com.tangem.data.common.currency.hasCoinForToken
import com.tangem.data.common.utils.retryOnError
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory
import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility
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.request.AssetsRequestBody
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.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.util.cardTypesResolver
@ -20,7 +32,11 @@ import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow
import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext
import com.tangem.domain.managetokens.model.ManageTokensListConfig
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.walletmanager.model.TokenInfo
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.pagination.BatchFetchResult
@ -28,16 +44,27 @@ import com.tangem.pagination.BatchListSource
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
import com.tangem.pagination.toBatchFlow
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
@Suppress("LongParameterList")
internal class DefaultManageTokensRepository(
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val managedCryptoCurrencyFactory: ManagedCryptoCurrencyFactory,
private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
private val walletManagerFacade: WalletManagersFacade,
private val tangemExpressApi: TangemExpressApi,
private val expressAssetsStore: ExpressAssetsStore,
) : ManageTokensRepository {
private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory()
private val cryptoCurrencyFactory = CryptoCurrencyFactory()
private val userTokensResponseFactory = UserTokensResponseFactory()
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
// region getTokenListBatchFlow
override fun getTokenListBatchFlow(
context: ManageTokensListBatchingContext,
batchSize: Int,
@ -58,7 +85,7 @@ internal class DefaultManageTokensRepository(
prefetchDistance = batchSize,
batchSize = batchSize,
subFetcher = { request, _, isFirstBatchFetching ->
val userWallet = getUserWallet(request.params.userWalletId)
val userWallet = request.params.userWalletId?.let { getUserWallet(it) }
val supportedBlockchains = getSupportedBlockchains(userWallet)
val searchText = request.params.searchText?.takeIf { it.isNotBlank() }
@ -118,13 +145,9 @@ internal class DefaultManageTokensRepository(
}
}
private suspend fun getUserWallet(userWalletId: UserWalletId?): UserWallet? {
if (userWalletId == null) {
return null
}
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"User wallet not found"
"Unable to find a user wallet with provided ID: $userWalletId"
}
}
@ -135,4 +158,169 @@ internal class DefaultManageTokensRepository(
!it.isTestnet() && it.isSupportedInApp()
}
}
// endregion
// region addManagedCurrencies
override suspend fun saveManagedCurrencies(
userWalletId: UserWalletId,
currenciesToAdd: Map<ManagedCryptoCurrency.Token, Set<Network>>,
currenciesToRemove: Map<ManagedCryptoCurrency.Token, Set<Network>>,
) {
return withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
val filteredTokens = currenciesToAdd.mapToTokenDataList()
.filterNot { tokenData ->
val blockchain = getBlockchain(networkId = tokenData.sourceNetwork.network.id)
val networkId = blockchain.toNetworkId()
val contractAddress = (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress
savedCurrencies.tokens.firstOrNull { token ->
token.contractAddress == contractAddress &&
token.networkId == networkId &&
token.derivationPath == tokenData.sourceNetwork.network.derivationPath.value
} != null
}
val newCoins = createCoinsForNewTokens(
userWalletId = userWalletId,
newTokensNetworks = filteredTokens.mapNotNull { tokenData ->
(tokenData.sourceNetwork as? SourceNetwork.Default)?.network
},
savedCurrencies = savedCurrencies.tokens,
)
val tokensToSave = filteredTokens.mapToUserResponseToken()
val tokensToRemove = currenciesToRemove.mapToTokenDataList()
removeCurrenciesFromWalletManager(userWalletId = userWalletId, tokensDataList = tokensToRemove)
val newCurrencies = (newCoins + tokensToSave).distinct()
val updatedResponse = savedCurrencies.copy(
tokens = savedCurrencies.tokens + newCurrencies - tokensToRemove.mapToUserResponseToken().toSet(),
)
storeAndPushTokens(userWalletId = userWalletId, response = updatedResponse)
fetchExchangeableUserMarketCoinsByIds(userWalletId = userWalletId, userTokens = updatedResponse)
}
}
private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response)
appPreferencesStore.storeObject(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue),
value = compatibleUserTokensResponse,
)
pushTokens(userWalletId, response)
}
private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) {
Timber.e(it, "Unable to save user tokens for: ${userWalletId.stringValue}")
}
}
private suspend fun fetchExchangeableUserMarketCoinsByIds(
userWalletId: UserWalletId,
userTokens: UserTokensResponse,
) {
try {
val tokensList = userTokens.tokens
.map {
LeastTokenInfo(
contractAddress = it.contractAddress ?: TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE,
network = it.networkId,
)
}
if (tokensList.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
AssetsRequestBody(
tokensList = tokensList,
),
)
expressAssetsStore.store(userWalletId, response.getOrThrow())
}
} catch (e: Throwable) {
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
}
}
private suspend fun removeCurrenciesFromWalletManager(
userWalletId: UserWalletId,
tokensDataList: List<TokenWithSourceNetwork>,
) {
walletManagerFacade.removeTokensByTokenInfo(
userWalletId = userWalletId,
tokenInfos = tokensDataList.mapNotNullTo(hashSetOf()) { tokenData ->
val contractAddress =
(tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress ?: return@mapNotNullTo null
TokenInfo(
network = tokenData.sourceNetwork.network,
name = tokenData.token.name,
symbol = tokenData.token.symbol,
contractAddress = contractAddress,
decimals = tokenData.sourceNetwork.decimals,
id = tokenData.token.id.value,
)
},
)
walletManagerFacade.remove(
userWalletId = userWalletId,
networks = tokensDataList.mapNotNullTo(hashSetOf()) { tokenData ->
(tokenData.sourceNetwork as? SourceNetwork.Main)?.network
},
)
}
private suspend fun createCoinsForNewTokens(
userWalletId: UserWalletId,
newTokensNetworks: List<Network>,
savedCurrencies: List<UserTokensResponse.Token>,
): List<UserTokensResponse.Token> {
return newTokensNetworks
.filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins
.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = getBlockchain(networkId = it.id),
extraDerivationPath = it.derivationPath.value,
derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider,
)
}
.distinct()
.map(userTokensResponseFactory::createResponseToken)
}
private fun List<TokenWithSourceNetwork>.mapToUserResponseToken(): List<UserTokensResponse.Token> {
return map { tokenData ->
val blockchain = getBlockchain(tokenData.sourceNetwork.network.id)
UserTokensResponse.Token(
id = tokenData.token.id.value,
networkId = blockchain.toNetworkId(),
derivationPath = tokenData.sourceNetwork.network.derivationPath.value,
name = tokenData.token.name,
symbol = tokenData.token.symbol,
decimals = tokenData.sourceNetwork.decimals,
contractAddress = (tokenData.sourceNetwork as? SourceNetwork.Default)?.contractAddress,
)
}
}
private fun Map<ManagedCryptoCurrency.Token, Set<Network>>.mapToTokenDataList(): List<TokenWithSourceNetwork> {
return flatMap { (token, networks) ->
token.availableNetworks
.filter { sourceNetwork -> networks.contains(sourceNetwork.network) }
.map { sourceNetwork -> TokenWithSourceNetwork(token = token, sourceNetwork = sourceNetwork) }
}
}
private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(key.stringValue),
)
}
// endregion
data class TokenWithSourceNetwork(val token: ManagedCryptoCurrency.Token, val sourceNetwork: SourceNetwork)
}

View file

@ -3,12 +3,14 @@ package com.tangem.data.managetokens.di
import com.tangem.data.managetokens.DefaultCustomTokensRepository
import com.tangem.data.managetokens.DefaultManageTokensRepository
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
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.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -25,18 +27,22 @@ internal object ManageTokensDataModule {
fun provideManageTokensRepository(
tangemTechApi: TangemTechApi,
userWalletsStore: UserWalletsStore,
managedCryptoCurrencyFactory: ManagedCryptoCurrencyFactory,
manageTokensUpdateFetcher: ManageTokensUpdateFetcher,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
walletManagersFacade: WalletManagersFacade,
tangemExpressApi: TangemExpressApi,
expressAssetsStore: ExpressAssetsStore,
): ManageTokensRepository {
return DefaultManageTokensRepository(
tangemTechApi,
userWalletsStore,
managedCryptoCurrencyFactory,
manageTokensUpdateFetcher,
appPreferencesStore,
dispatchers,
walletManagersFacade,
tangemExpressApi,
expressAssetsStore,
)
}

View file

@ -31,9 +31,9 @@ internal class ManageTokensUpdateFetcher @Inject constructor() :
is ManagedCryptoCurrency.Token -> {
currency.copy(
addedIn = if (updateRequest.isSelected) {
currency.addedIn + updateRequest.networkId
currency.addedIn + updateRequest.network
} else {
currency.addedIn - updateRequest.networkId
currency.addedIn - updateRequest.network
},
)
}

View file

@ -13,11 +13,8 @@ import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.tokens.model.Network
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class ManagedCryptoCurrencyFactory @Inject constructor() {
internal class ManagedCryptoCurrencyFactory {
fun create(
coinsResponse: CoinsResponse,
@ -100,7 +97,7 @@ internal class ManagedCryptoCurrencyFactory @Inject constructor() {
availableNetworks = coinResponse.networks.mapNotNull { network ->
createSource(network, derivationStyleProvider)
},
addedIn = findAddedInNetworksIds(coinResponse.id, tokensResponse),
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, derivationStyleProvider),
)
}
@ -119,26 +116,35 @@ internal class ManagedCryptoCurrencyFactory @Inject constructor() {
return if (contractAddress.isNullOrBlank()) {
SourceNetwork.Main(
network = network,
decimals = blockchain.decimals(),
)
} else {
SourceNetwork.Default(
network = network,
decimals = requireNotNull(networkResponse.decimalCount?.toInt()),
contractAddress = contractAddress,
)
}
}
private fun findAddedInNetworksIds(currencyId: String, tokensResponse: UserTokensResponse?): Set<Network.ID> {
private fun findAddedInNetworks(
currencyId: String,
tokensResponse: UserTokensResponse?,
derivationStyleProvider: DerivationStyleProvider?,
): Set<Network> {
if (tokensResponse == null) return emptySet()
return tokensResponse.tokens
.filter { it.id == currencyId }
.map { it.networkId }
.mapNotNullTo(mutableSetOf()) { networkId ->
val blockchain = Blockchain.fromNetworkId(networkId)
.mapNotNullTo(mutableSetOf()) { token ->
val blockchain = Blockchain.fromNetworkId(token.networkId)
if (blockchain != null && blockchain.isSupportedInApp()) {
Network.ID(blockchain.id)
getNetwork(
blockchain = blockchain,
extraDerivationPath = token.derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
} else {
null
}

View file

@ -132,7 +132,7 @@ internal class DefaultCurrenciesRepository(
savedCurrencies: List<UserTokensResponse.Token>,
): List<CryptoCurrency.Coin> {
return newTokens
.filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins
.filterNot { savedCurrencies.hasCoinForToken(it.network) } // tokens without coins
.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = getBlockchain(networkId = it.network.id),

View file

@ -9,7 +9,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
* Helper to apply compatibility changes for [UserTokensResponse] to support old saved tokens
* in new application with new IDs
*/
internal class UserTokensBackwardCompatibility {
class UserTokensBackwardCompatibility {
fun applyCompatibilityAndGetUpdated(userTokensResponse: UserTokensResponse): UserTokensResponse {
return userTokensResponse.copy(

View file

@ -3,6 +3,7 @@ package com.tangem.domain.card.repository
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
@ -11,6 +12,9 @@ interface DerivationsRepository {
@Throws
suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
@Throws
suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List<Network>)
@Throws
suspend fun derivePublicKeys(
userWalletId: UserWalletId,

View file

@ -31,6 +31,7 @@ import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryState
import com.tangem.domain.walletmanager.model.SmartContractMethod
import com.tangem.domain.walletmanager.model.TokenInfo
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.walletmanager.utils.*
import com.tangem.domain.walletmanager.utils.WalletManagerFactory
@ -103,24 +104,45 @@ class DefaultWalletManagersFacade(
tokens
.groupBy(CryptoCurrency.Token::network)
.forEach { (network, networkTokens) ->
removeTokens(userWalletId, network, networkTokens)
removeTokens(
userWalletId = userWalletId,
network = network,
networkTokens = sdkTokenConverter.convertList(networkTokens),
)
}
}
private suspend fun removeTokens(
userWalletId: UserWalletId,
network: Network,
networkTokens: List<CryptoCurrency.Token>,
) {
override suspend fun removeTokensByTokenInfo(userWalletId: UserWalletId, tokenInfos: Set<TokenInfo>) {
if (tokenInfos.isEmpty()) return
tokenInfos
.groupBy { it.network }
.forEach { (network, tokenInfoList) ->
removeTokens(
userWalletId = userWalletId,
network = network,
networkTokens = tokenInfoList.map {
Token(
name = it.name,
symbol = it.symbol,
contractAddress = it.contractAddress,
decimals = it.decimals,
id = it.id,
)
},
)
}
}
private suspend fun removeTokens(userWalletId: UserWalletId, network: Network, networkTokens: List<Token>) {
withContext(dispatchers.io) {
val walletManager = walletManagersStore.getSyncOrNull(
userWalletId = userWalletId,
blockchain = Blockchain.fromId(network.id.value),
derivationPath = network.derivationPath.value,
) ?: return@withContext
val tokensToRemove = sdkTokenConverter.convertList(networkTokens)
tokensToRemove.forEach { token ->
networkTokens.forEach { token ->
walletManager.removeToken(token)
}

View file

@ -17,6 +17,7 @@ import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryState
import com.tangem.domain.walletmanager.model.TokenInfo
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -54,6 +55,8 @@ interface WalletManagersFacade {
suspend fun removeTokens(userWalletId: UserWalletId, tokens: Set<CryptoCurrency.Token>)
suspend fun removeTokensByTokenInfo(userWalletId: UserWalletId, tokenInfos: Set<TokenInfo>)
/**
* Returns [UpdateWalletManagerResult] with last pending transactions
*

View file

@ -0,0 +1,12 @@
package com.tangem.domain.walletmanager.model
import com.tangem.domain.tokens.model.Network
data class TokenInfo(
val network: Network,
val name: String,
val symbol: String,
val contractAddress: String,
val decimals: Int,
val id: String? = null,
)

View file

@ -1,8 +1,13 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.domain.managetokens"
}
dependencies {
/* Domain */
@ -10,6 +15,8 @@ dependencies {
api(projects.domain.core)
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.card)
implementation(projects.domain.legacy)
/* Core */
api(projects.core.pagination)

View file

@ -42,7 +42,7 @@ sealed class ManagedCryptoCurrency {
override val symbol: String,
override val iconUrl: String,
val availableNetworks: List<SourceNetwork>,
val addedIn: Set<Network.ID>,
val addedIn: Set<Network>,
) : ManagedCryptoCurrency() {
val isAdded: Boolean = addedIn.isNotEmpty()
@ -54,6 +54,7 @@ sealed class ManagedCryptoCurrency {
sealed class SourceNetwork {
abstract val network: Network
abstract val decimals: Int
val id: Network.ID
get() = network.id
@ -73,10 +74,12 @@ sealed class ManagedCryptoCurrency {
data class Main(
override val network: Network,
override val decimals: Int,
) : SourceNetwork()
data class Default(
override val network: Network,
override val decimals: Int,
val contractAddress: String,
) : SourceNetwork()

View file

@ -0,0 +1,33 @@
package com.tangem.domain.managetokens
import arrow.core.Either
import arrow.core.flatten
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
class SaveManagedTokensUseCase(
private val manageTokensRepository: ManageTokensRepository,
private val derivationsRepository: DerivationsRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
currenciesToAdd: Map<ManagedCryptoCurrency.Token, Set<Network>>,
currenciesToRemove: Map<ManagedCryptoCurrency.Token, Set<Network>>,
): Either<Throwable, Unit> {
return Either.catch {
derivationsRepository.derivePublicKeysByNetworks(
userWalletId = userWalletId,
networks = currenciesToAdd.values.flatten(),
)
manageTokensRepository.saveManagedCurrencies(
userWalletId = userWalletId,
currenciesToAdd = currenciesToAdd,
currenciesToRemove = currenciesToRemove,
)
}
}
}

View file

@ -6,7 +6,7 @@ sealed class ManageTokensUpdateAction {
data class AddCurrency(
val currencyId: ManagedCryptoCurrency.ID,
val networkId: Network.ID,
val network: Network,
val isSelected: Boolean,
) : ManageTokensUpdateAction()
}

View file

@ -2,8 +2,17 @@ package com.tangem.domain.managetokens.repository
import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow
import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
interface ManageTokensRepository {
fun getTokenListBatchFlow(context: ManageTokensListBatchingContext, batchSize: Int): ManageTokensListBatchFlow
suspend fun saveManagedCurrencies(
userWalletId: UserWalletId,
currenciesToAdd: Map<ManagedCryptoCurrency.Token, Set<Network>>,
currenciesToRemove: Map<ManagedCryptoCurrency.Token, Set<Network>>,
)
}

View file

@ -29,7 +29,7 @@ class GetWalletTotalBalanceUseCase(
private val stakingRepository: StakingRepository,
) {
suspend operator fun invoke(
operator fun invoke(
userTallestIds: Collection<UserWalletId>,
): LceFlow<TokenListError, Map<UserWalletId, TotalFiatBalance>> {
val flows = userTallestIds.distinct()
@ -54,7 +54,7 @@ class GetWalletTotalBalanceUseCase(
}
@OptIn(ExperimentalCoroutinesApi::class)
suspend operator fun invoke(userWalletId: UserWalletId): LceFlow<TokenListError, TotalFiatBalance> {
operator fun invoke(userWalletId: UserWalletId): LceFlow<TokenListError, TotalFiatBalance> {
val currenciesStatuses = getStatuses(userWalletId)
return currenciesStatuses.transformLatest { maybeStatuses ->

View file

@ -51,7 +51,17 @@ internal class PreviewCustomTokenSelectorComponent(
)
CurrencyNetworkUM(
networkId = n.id,
network = Network(
id = n.id,
backendId = n.id.value,
name = "",
currencySymbol = "",
derivationPath = Network.DerivationPath.Card(""),
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = false,
canHandleTokens = false,
),
name = "Network $index",
type = "N$index",
iconResId = R.drawable.ic_eth_16,

View file

@ -54,6 +54,7 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
isNextBatchLoading = true,
loadMore = { false },
saveChanges = {},
isSavingInProgress = false,
),
)
@ -134,7 +135,17 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
CurrencyNetworkUM(
networkId = Network.ID(networkIndex.toString()),
network = Network(
id = Network.ID(networkIndex.toString()),
backendId = networkIndex.toString(),
name = "",
currencySymbol = "",
derivationPath = Network.DerivationPath.Card(""),
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = false,
canHandleTokens = false,
),
name = "NETWORK$networkIndex",
type = "N$networkIndex",
iconResId = R.drawable.ic_eth_16,

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.entity.item
import com.tangem.domain.tokens.model.Network
internal data class CurrencyNetworkUM(
val networkId: Network.ID,
val network: Network,
val name: String,
val type: String,
val iconResId: Int,
@ -12,5 +12,5 @@ internal data class CurrencyNetworkUM(
override val onSelectedStateChange: (Boolean) -> Unit,
) : SelectableItemUM {
override val id: String = networkId.value
override val id: String = network.id.value
}

View file

@ -36,6 +36,7 @@ internal sealed class ManageTokensUM {
override val loadMore: () -> Boolean,
val saveChanges: () -> Unit,
val hasChanges: Boolean,
val isSavingInProgress: Boolean,
) : ManageTokensUM()
fun copySealed(
@ -44,6 +45,7 @@ internal sealed class ManageTokensUM {
hasChanges: Boolean = this is ManageContent && this.hasChanges,
isInitialBatchLoading: Boolean = this.isInitialBatchLoading,
isNextBatchLoading: Boolean = this.isNextBatchLoading,
isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress,
): ManageTokensUM {
return when (this) {
is ManageContent -> copy(
@ -52,6 +54,7 @@ internal sealed class ManageTokensUM {
hasChanges = hasChanges,
isInitialBatchLoading = isInitialBatchLoading,
isNextBatchLoading = isNextBatchLoading,
isSavingInProgress = isSavingInProgress,
)
is ReadContent -> copy(
search = search,

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
@ -21,12 +22,14 @@ import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.list.ChangedCurrencies
import com.tangem.features.managetokens.utils.list.ManageTokensListManager
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ComponentScoped
@ -35,6 +38,7 @@ internal class ManageTokensModel @Inject constructor(
private val router: Router,
private val manageTokensListManager: ManageTokensListManager,
private val messageSender: UiMessageSender,
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -76,7 +80,7 @@ internal class ManageTokensModel @Inject constructor(
popBack = router::pop,
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getInitialItems(),
items = persistentListOf(),
topBar = ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_search_tokens),
onBackButtonClick = router::pop,
@ -97,7 +101,7 @@ internal class ManageTokensModel @Inject constructor(
popBack = router::pop,
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getInitialItems(),
items = persistentListOf(),
topBar = ManageTokensTopBarUM.ManageContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = router::pop,
@ -116,6 +120,7 @@ internal class ManageTokensModel @Inject constructor(
hasChanges = false,
saveChanges = ::saveChanges,
loadMore = ::loadMoreItems,
isSavingInProgress = false,
)
}
@ -157,9 +162,16 @@ internal class ManageTokensModel @Inject constructor(
isNextBatchLoading = false,
)
}
is PaginationStatus.Paginating,
is PaginationStatus.EndOfPagination,
-> state.copySealed(
is PaginationStatus.Paginating -> {
(status.lastResult as? BatchFetchResult.Error)?.let { fetchError ->
Timber.e(fetchError.throwable)
}
state.copySealed(
isInitialBatchLoading = false,
isNextBatchLoading = false,
)
}
is PaginationStatus.EndOfPagination -> state.copySealed(
isInitialBatchLoading = false,
isNextBatchLoading = false,
)
@ -189,10 +201,6 @@ internal class ManageTokensModel @Inject constructor(
return true
}
private fun getInitialItems(): ImmutableList<CurrencyItemUM> {
return persistentListOf()
}
private fun navigateToAddCustomToken() {
params.userWalletId?.let {
bottomSheetNavigation.activate(BottomSheetConfig.AddCustomToken(it))
@ -200,7 +208,18 @@ internal class ManageTokensModel @Inject constructor(
}
private fun saveChanges() {
// TODO: [REDACTED_JIRA]
modelScope.launch {
state.update { state -> state.copySealed(isSavingInProgress = true) }
saveManagedTokensUseCase.invoke(
userWalletId = requireNotNull(params.userWalletId),
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
).fold(
ifLeft = { Timber.e(it, "Failed to save changes") },
ifRight = { router.pop() },
)
state.update { state -> state.copySealed(isSavingInProgress = false) }
}
}
private fun searchCurrencies(query: String) {

View file

@ -102,6 +102,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
isVisible = state.hasChanges,
showProgress = state.isSavingInProgress,
onClick = state.saveChanges,
)
}
@ -133,7 +134,12 @@ private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, search: SearchBarUM
}
@Composable
private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
private fun SaveChangesButton(
isVisible: Boolean,
showProgress: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
modifier = modifier,
visible = isVisible,
@ -144,6 +150,7 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier:
PrimaryButtonIconEnd(
text = stringResource(id = R.string.common_save),
iconResId = R.drawable.ic_tangem_24,
showProgress = showProgress,
onClick = onClick,
)
}

View file

@ -5,54 +5,54 @@ import com.tangem.domain.tokens.model.Network
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
internal typealias ChangedCurrencies = Map<ManagedCryptoCurrency.ID, Set<Network.ID>>
internal typealias ChangedCurrencies = Map<ManagedCryptoCurrency.Token, Set<Network>>
internal class ChangedCurrenciesManager {
val currenciesToAdd: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())
val currenciesToRemove: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())
fun addCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
updateChangedItems(currencyId, networkId, currenciesToRemove, currenciesToAdd)
fun addCurrency(currency: ManagedCryptoCurrency.Token, network: Network) {
updateChangedItems(currency, network, currenciesToRemove, currenciesToAdd)
}
fun removeCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
updateChangedItems(currencyId, networkId, currenciesToAdd, currenciesToRemove)
fun removeCurrency(currency: ManagedCryptoCurrency.Token, network: Network) {
updateChangedItems(currency, network, currenciesToAdd, currenciesToRemove)
}
fun containsCurrency(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean {
return networkId in currenciesToAdd.value[currencyId].orEmpty() ||
networkId in currenciesToRemove.value[currencyId].orEmpty()
fun containsCurrency(currency: ManagedCryptoCurrency.Token, network: Network): Boolean {
return network in currenciesToAdd.value[currency].orEmpty() ||
network in currenciesToRemove.value[currency].orEmpty()
}
private fun updateChangedItems(
currencyId: ManagedCryptoCurrency.ID,
networkId: Network.ID,
currency: ManagedCryptoCurrency.Token,
network: Network,
removeFromIfPresent: MutableStateFlow<ChangedCurrencies>,
addToIfNotPresent: MutableStateFlow<ChangedCurrencies>,
) {
val present = removeFromIfPresent.value[currencyId].orEmpty()
val present = removeFromIfPresent.value[currency].orEmpty()
if (networkId in present) {
if (network in present) {
removeFromIfPresent.update { items ->
items.toMutableMap().apply {
val ids = present - networkId
val ids = present - network
if (ids.isEmpty()) {
remove(currencyId)
remove(currency)
} else {
set(currencyId, ids)
set(currency, ids)
}
}
}
} else {
addToIfNotPresent.update { items ->
val alreadyAdded = items[currencyId] ?: emptySet()
if (networkId in alreadyAdded) {
val alreadyAdded = items[currency] ?: emptySet()
if (network in alreadyAdded) {
return@update items
}
items + (currencyId to alreadyAdded + networkId)
items + (currency to alreadyAdded + network)
}
}
}

View file

@ -59,8 +59,8 @@ internal class ManageTokensListManager @Inject constructor(
scopeProvider = Provider { scope },
)
val currenciesToAdd: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToAdd
val currenciesToRemove: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToRemove
val currenciesToAdd: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToAdd.asStateFlow()
val currenciesToRemove: StateFlow<ChangedCurrencies> = changedCurrenciesManager.currenciesToRemove.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class)
val paginationStatus: Flow<PaginationStatus<*>> = state
@ -148,32 +148,32 @@ internal class ManageTokensListManager @Inject constructor(
}
}
override fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
changedCurrenciesManager.addCurrency(currencyId, networkId)
override fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) {
changedCurrenciesManager.addCurrency(currency, network)
sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = true)
sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true)
}
override fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID) {
changedCurrenciesManager.removeCurrency(currencyId, networkId)
override fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) {
changedCurrenciesManager.removeCurrency(currency, network)
sendSelectCurrencyAction(batchKey, currencyId, networkId, isSelected = false)
sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false)
}
override fun checkNeedToShowRemoveNetworkWarning(
currencyId: ManagedCryptoCurrency.ID,
networkId: Network.ID,
): Boolean = !changedCurrenciesManager.containsCurrency(currencyId, networkId)
currency: ManagedCryptoCurrency.Token,
network: Network,
): Boolean = !changedCurrenciesManager.containsCurrency(currency, network)
private fun sendSelectCurrencyAction(
batchKey: Int,
currencyId: ManagedCryptoCurrency.ID,
networkId: Network.ID,
network: Network,
isSelected: Boolean,
) {
val request = ManageTokensUpdateAction.AddCurrency(
currencyId = currencyId,
networkId = networkId,
network = network,
isSelected = isSelected,
)
val action = BatchAction.UpdateBatches(

View file

@ -6,11 +6,11 @@ import com.tangem.domain.wallets.models.UserWalletId
internal interface ManageTokensUiActions {
fun addCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID)
fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network)
fun removeCurrency(batchKey: Int, currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID)
fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network)
fun checkNeedToShowRemoveNetworkWarning(currencyId: ManagedCryptoCurrency.ID, networkId: Network.ID): Boolean
fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean
suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean
}

View file

@ -142,19 +142,19 @@ internal class ManageTokensUiManager(
if (currency !is ManagedCryptoCurrency.Token) return@launch
if (isSelected) {
actions.addCurrency(batchKey, currency.id, source.id)
actions.addCurrency(batchKey, currency, source.network)
} else {
if (actions.checkNeedToShowRemoveNetworkWarning(currency.id, source.id)) {
if (actions.checkNeedToShowRemoveNetworkWarning(currency, source.network)) {
showRemoveNetworkWarning(
currency = currency,
network = source.network,
isCoin = source is ManagedCryptoCurrency.SourceNetwork.Main,
onConfirm = {
actions.removeCurrency(batchKey, currency.id, source.id)
actions.removeCurrency(batchKey, currency, source.network)
},
)
} else {
actions.removeCurrency(batchKey, currency.id, source.id)
actions.removeCurrency(batchKey, currency, source.network)
}
}
}

View file

@ -16,7 +16,7 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel(
NetworksUM.Expanded(
networks = availableNetworks.map {
it.toUiModel(
isSelected = it.id in addedIn,
isSelected = it.network in addedIn,
isEditable = isItemsEditable,
onSelectedStateChange = onSelectedStateChange,
)
@ -33,7 +33,7 @@ private fun SourceNetwork.toUiModel(
onSelectedStateChange: (SourceNetwork, Boolean) -> Unit,
): CurrencyNetworkUM {
return CurrencyNetworkUM(
networkId = id,
network = network,
name = network.name.uppercase(),
iconResId = id.getIconRes(isColored = isSelected || !isEditable),
isSelected = isSelected || !isEditable,

View file

@ -55,10 +55,10 @@ private fun CurrencyItemUM.Basic.updateNetworks(currency: ManagedCryptoCurrency.
is NetworksUM.Collapsed -> networks
is NetworksUM.Expanded -> networks.copy(
networks = networks.networks.map { network ->
val isSelected = network.networkId in currency.addedIn
val isSelected = network.network in currency.addedIn
network.copy(
iconResId = network.networkId.getIconRes(isSelected),
iconResId = network.network.id.getIconRes(isSelected),
isSelected = isSelected,
)
}.toImmutableList(),

View file

@ -8,7 +8,7 @@ import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
internal fun CurrencyNetworkUM.select(isSelected: Boolean): CurrencyNetworkUM {
return copy(
iconResId = networkId.getIconRes(isSelected),
iconResId = network.id.getIconRes(isSelected),
isSelected = isSelected,
)
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.markets.portfolio.impl.loader
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
/**
* Portfolio data. Combined data from all flows that required to setup portfolio
*
* @property walletsWithCurrencyStatuses wallets with crypto currency statuses
* @property appCurrency app currency
* @property isBalanceHidden flag that indicates if balance should be hidden
* @property walletsWithBalance wallets with total balance
*
[REDACTED_AUTHOR]
*/
internal data class PortfolioData(
val walletsWithCurrencyStatuses: Map<UserWallet, List<CryptoCurrencyStatus>>,
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val walletsWithBalance: Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>,
)

View file

@ -0,0 +1,105 @@
package com.tangem.features.markets.portfolio.impl.loader
import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
/**
* Loader of portfolio data
*
* @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses
* @property getSelectedAppCurrencyUseCase use case for getting selected app currency
* @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings
* @property getWalletTotalBalanceUseCase use case for getting wallet total balance
*
[REDACTED_AUTHOR]
*/
internal class PortfolioDataLoader @Inject constructor(
private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
) {
/** Load data by [currencyRawId] */
@OptIn(ExperimentalCoroutinesApi::class)
fun load(currencyRawId: String): Flow<PortfolioData> {
return combine(
flow = getAllWalletsCryptoCurrencyStatusesFlow(currencyRawId = currencyRawId),
flow2 = getSelectedAppCurrencyFlow(),
flow3 = getBalanceHidingSettingsFlow(),
) { walletsWithCurrencyStatuses, appCurrency, isBalanceHidden ->
PortfolioData(
walletsWithCurrencyStatuses = walletsWithCurrencyStatuses,
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
walletsWithBalance = emptyMap(),
)
}
// setup balances for wallets from walletsWithCurrencyStatuses
.flatMapLatest { portfolioData ->
getWalletsWithTotalBalanceFlow(
ids = portfolioData.walletsWithCurrencyStatuses.keys.map(UserWallet::walletId),
)
.map { portfolioData.copy(walletsWithBalance = it) }
}
}
private fun getAllWalletsCryptoCurrencyStatusesFlow(
currencyRawId: String,
): Flow<Map<UserWallet, List<CryptoCurrencyStatus>>> {
return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId)
.distinctUntilChanged()
.map { walletsWithMaybeStatuses ->
walletsWithMaybeStatuses.mapValues { entry ->
entry.value.mapNotNull { it.getOrNull() }
}
}
}
private fun getSelectedAppCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map {
it.getOrElse { e ->
Timber.e("Failed to load app currency: $e")
AppCurrency.Default
}
}
.distinctUntilChanged()
}
private fun getBalanceHidingSettingsFlow(): Flow<Boolean> {
return getBalanceHidingSettingsUseCase()
.map { it.isBalanceHidden }
.distinctUntilChanged()
}
private fun getWalletsWithTotalBalanceFlow(
ids: List<UserWalletId>,
): Flow<Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>> {
return combine(
flows = ids
.map { userWalletId ->
getWalletTotalBalanceUseCase(userWalletId)
.map { userWalletId to it }
.distinctUntilChanged()
},
transform = { it.toMap() },
)
.distinctUntilChanged()
.onEmpty { ids.associateWith { Lce.Loading<TotalFiatBalance>(partialContent = null) } }
}
}

View file

@ -109,7 +109,6 @@ private fun Preview() {
),
),
onBack = {},
onWalletSelected = {},
),
),
)
@ -135,7 +134,6 @@ private fun PreviewContent() {
),
),
onBack = {},
onWalletSelected = {},
),
)
}

View file

@ -6,6 +6,5 @@ import kotlinx.collections.immutable.ImmutableList
internal data class WalletSelectorBSContentUM(
val userWallets: ImmutableList<UserWalletItemUM>,
val onWalletSelected: (UserWalletItemUM) -> Unit,
val onBack: () -> Unit,
) : TangemBottomSheetConfigContent