Updated on 2026-08-14
This commit is contained in:
parent
4f3d1f10c7
commit
9b23c9a7a2
33 changed files with 484 additions and 115 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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>>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue