From 1694ec07beac2a253d627b4886c13f8536d1d492 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Sep 2023 19:41:21 +0800 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/tap/features/home/HomeFragment.kt | 2 +- .../presentation/models/TokensListArgs.kt | 22 -- .../viewmodels/TokensListCryptoCurrencies.kt | 9 + .../viewmodels/TokensListMigration.kt | 192 +++++++++++++++++ .../viewmodels/TokensListViewModel.kt | 59 ++++-- .../tokens/legacy/redux/TokensAction.kt | 20 -- .../tokens/legacy/redux/TokensMiddleware.kt | 199 +++++++++++++++--- .../tokens/legacy/redux/TokensReducer.kt | 42 +--- .../tokens/legacy/redux/TokensState.kt | 13 +- .../wallet/ui/wallet/MultiWalletView.kt | 13 +- .../com/tangem/domain/tokens/TokensAction.kt | 36 ++++ .../wallet/viewmodels/WalletViewModel.kt | 1 + 12 files changed, 465 insertions(+), 143 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 95b558c438..6332136b24 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -17,13 +17,13 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.TokensAction import com.tangem.feature.learn2earn.presentation.Learn2earnViewModel import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.home.redux.Stories -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt deleted file mode 100644 index 207976d628..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.models - -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.store - -/** - * Required data for tokens list screen - * FIXME("Necessary to avoid using redux state") - * -[REDACTED_AUTHOR] - */ -class TokensListArgs { - /** Tokens list screen mode */ - val isManageAccess: Boolean get() = store.state.tokensState.isManageAccess - - /** Tokens list that accessible from the main screen */ - val mainScreenTokenList: List get() = store.state.tokensState.addedTokens - - /** Blockchains list that accessible from the main screen */ - val mainScreenBlockchainList: List get() = store.state.tokensState.addedBlockchains -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt new file mode 100644 index 0000000000..da83face06 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.features.tokens.impl.presentation.viewmodels + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.TokenWithBlockchain + +internal data class TokensListCryptoCurrencies( + val coins: List, + val tokens: List, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt new file mode 100644 index 0000000000..8ce3e83f02 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -0,0 +1,192 @@ +package com.tangem.tap.features.tokens.impl.presentation.viewmodels + +import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.tokens.TokensAction +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import com.tangem.tap.domain.model.WalletDataModel +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.store +import timber.log.Timber +import kotlin.properties.Delegates + +/** + * Class that divide a new and legacy logic when user uses tokens list screen + * + * @property walletFeatureToggles wallet feature toggles + * @property getSelectedWalletUseCase use case that returns selected wallet + * @property getCurrenciesUseCase use case that returns crypto currencies of a specified wallet + */ +internal class TokensListMigration( + private val walletFeatureToggles: WalletFeatureToggles, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, +) { + + private var currentNewCoins: List by Delegates.notNull() + private var currentNewTokens: List by Delegates.notNull() + private var currentUserWallet: UserWallet by Delegates.notNull() + + private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } + + suspend fun getCurrentCryptoCurrencies(): TokensListCryptoCurrencies { + return if (walletFeatureToggles.isRedesignedScreenEnabled) { + getNewCryptoCurrencies() + } else { + getLegacyCryptoCurrencies() + } + } + + private suspend fun getNewCryptoCurrencies(): TokensListCryptoCurrencies { + return when (val selectedWalletEither = getSelectedWalletUseCase()) { + is Either.Left -> { + Timber.e(selectedWalletEither.value.toString()) + TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) + } + is Either.Right -> { + currentUserWallet = selectedWalletEither.value + val derivationStyle = currentUserWallet.scanResponse.derivationStyleProvider.getDerivationStyle() + + when (val currenciesEither = getCurrenciesUseCase(userWalletId = selectedWalletEither.value.walletId)) { + is Either.Left -> { + Timber.e(currenciesEither.value.toString()) + TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) + } + is Either.Right -> { + TokensListCryptoCurrencies( + coins = currenciesEither.value + .filterIsInstance() + .filterNot { it.isCustomCurrency(derivationStyle) } + .also { currentNewCoins = it } + .map { Blockchain.fromId(it.network.id.value) }, + tokens = currenciesEither.value + .filterIsInstance() + .filterNot(CryptoCurrency.Token::isCustom) + .also { currentNewTokens = it } + .map { token -> + TokenWithBlockchain( + token = Token( + name = token.name, + symbol = token.symbol, + contractAddress = token.contractAddress, + decimals = token.decimals, + id = token.id.rawCurrencyId, + ), + blockchain = Blockchain.fromId(token.network.id.value), + ) + }, + ) + } + } + } + } + } + + private fun CryptoCurrency.Coin.isCustomCurrency(derivationStyle: DerivationStyle?): Boolean { + if (derivationPath == null || derivationStyle == null) return false + + return derivationPath != Blockchain.fromId(network.id.value).derivationPath(derivationStyle)?.rawPath + } + + private fun getLegacyCryptoCurrencies(): TokensListCryptoCurrencies { + val wallets = store.state.walletState.walletsDataFromStores + val derivationStyle = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle() + + return TokensListCryptoCurrencies( + coins = wallets.toNonCustomBlockchains(derivationStyle), + tokens = wallets.toNonCustomTokensWithBlockchains(derivationStyle), + ) + } + + private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { + return this + .mapNotNull { walletDataModel -> + if (walletDataModel.currency.isCustomCurrency(derivationStyle)) { + null + } else { + (walletDataModel.currency as? Currency.Blockchain)?.blockchain + } + } + .distinct() + } + + private fun List.toNonCustomTokensWithBlockchains( + derivationStyle: DerivationStyle?, + ): List { + return this + .mapNotNull { walletDataModel -> + if (walletDataModel.currency !is Currency.Token) return@mapNotNull null + if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null + + TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain) + } + .distinct() + } + + fun onSaveButtonClick( + currentTokensList: List, + currentBlockchainList: List, + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + if (walletFeatureToggles.isRedesignedScreenEnabled) { + saveByNewWay(changedTokensList = changedTokensList, changedBlockchainList = changedBlockchainList) + } else { + saveByOldWay(currentTokensList, currentBlockchainList, changedTokensList, changedBlockchainList) + } + } + + private fun saveByNewWay( + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + store.dispatch( + action = TokensAction.NewSaveChanges( + currentTokens = currentNewTokens, + currentCoins = currentNewCoins, + changedTokens = changedTokensList.mapNotNull { + cryptoCurrencyFactory.createToken( + sdkToken = it.token, + blockchain = it.blockchain, + derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, + ) + }, + changedCoins = changedBlockchainList.mapNotNull { + cryptoCurrencyFactory.createCoin( + blockchain = it, + derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, + ) + }, + userWallet = currentUserWallet, + ), + ) + } + + private fun saveByOldWay( + currentTokensList: List, + currentBlockchainList: List, + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + val scanResponse = store.state.globalState.scanResponse ?: return + + store.dispatch( + action = TokensAction.LegacySaveChanges( + currentTokens = currentTokensList, + currentBlockchains = currentBlockchainList, + changedTokens = changedTokensList, + changedBlockchains = changedBlockchainList, + scanResponse = scanResponse, + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 78ff6ff6ce..05237c3172 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -19,6 +19,10 @@ import com.tangem.domain.common.extensions.canHandleToken import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.extensions.fullNameWithoutTestnet import com.tangem.tap.common.extensions.getGreyedOutIconRes import com.tangem.tap.common.extensions.getNetworkName @@ -26,14 +30,11 @@ import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.tap.features.tokens.impl.domain.models.Token.Network import com.tangem.tap.features.tokens.impl.presentation.models.SupportTokensState -import com.tangem.tap.features.tokens.impl.presentation.models.TokensListArgs import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.store import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider @@ -43,9 +44,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.coroutines.plus import timber.log.Timber import javax.inject.Inject +import kotlin.properties.Delegates import com.tangem.blockchain.common.Token as BlockchainToken /** @@ -59,6 +62,7 @@ import com.tangem.blockchain.common.Token as BlockchainToken * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") @HiltViewModel internal class TokensListViewModel @Inject constructor( private val interactor: TokensListInteractor, @@ -66,9 +70,12 @@ internal class TokensListViewModel @Inject constructor( private val dispatchers: AppCoroutineDispatcherProvider, private val reduxStateHolder: AppStateHolder, analyticsEventHandler: AnalyticsEventHandler, + getCurrenciesUseCase: GetCryptoCurrenciesUseCase, + getSelectedWalletUseCase: GetSelectedWalletUseCase, + walletFeatureToggles: WalletFeatureToggles, ) : ViewModel(), DefaultLifecycleObserver { - private val args = TokensListArgs() + private val isManageAccess = store.state.tokensState.isManageAccess private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler) private val actionsHandler = ActionsHandler(router = router, debouncer = Debouncer()) @@ -76,15 +83,36 @@ internal class TokensListViewModel @Inject constructor( var uiState by mutableStateOf(value = getInitialUiState()) private set - private val changedTokensList: MutableList = args.mainScreenTokenList.toMutableList() - private val changedBlockchainList: MutableList = args.mainScreenBlockchainList.toMutableList() + private var currentTokensList: List by Delegates.notNull() + private var currentBlockchainList: List by Delegates.notNull() + + private var changedTokensList: MutableList by Delegates.notNull() + private var changedBlockchainList: MutableList by Delegates.notNull() + + private val tokensListMigration = TokensListMigration( + walletFeatureToggles = walletFeatureToggles, + getSelectedWalletUseCase = getSelectedWalletUseCase, + getCurrenciesUseCase = getCurrenciesUseCase, + ) + + init { + viewModelScope.launch(dispatchers.main) { + val (currentCoins, currentTokens) = tokensListMigration.getCurrentCryptoCurrencies() + + currentBlockchainList = currentCoins + currentTokensList = currentTokens + + changedBlockchainList = currentCoins.toMutableList() + changedTokensList = currentTokens.toMutableList() + } + } override fun onCreate(owner: LifecycleOwner) { - if (args.isManageAccess) analyticsSender.sendWhenScreenOpened() + if (isManageAccess) analyticsSender.sendWhenScreenOpened() } private fun getInitialUiState(): TokensListStateHolder { - return if (args.isManageAccess) { + return if (isManageAccess) { TokensListStateHolder.ManageContent( toolbarState = getInitialToolbarState(), isLoading = true, @@ -105,7 +133,7 @@ internal class TokensListViewModel @Inject constructor( } private fun getInitialToolbarState(): TokensListToolbarState { - return if (args.isManageAccess) { + return if (isManageAccess) { TokensListToolbarState.Title.Manage( titleResId = R.string.add_tokens_title, onBackButtonClick = actionsHandler::onBackButtonClick, @@ -130,7 +158,7 @@ internal class TokensListViewModel @Inject constructor( return interactor.getTokensList(searchText = searchText).map { it.map { token -> - if (args.isManageAccess) createManageTokenContent(token) else createReadTokenContent(token) + if (isManageAccess) createManageTokenContent(token) else createReadTokenContent(token) } } } @@ -264,7 +292,12 @@ internal class TokensListViewModel @Inject constructor( fun onSaveButtonClick() { analyticsSender.sendWhenSaveButtonClicked() - store.dispatch(TokensAction.SaveChanges(changedTokensList, changedBlockchainList)) + tokensListMigration.onSaveButtonClick( + currentTokensList = currentTokensList, + currentBlockchainList = currentBlockchainList, + changedTokensList = changedTokensList, + changedBlockchainList = changedBlockchainList, + ) } private fun onSearchValueChange(newValue: String) { @@ -291,7 +324,7 @@ internal class TokensListViewModel @Inject constructor( if (isRemoveAction) { val isTokenWithSameBlockchainFound = changedTokensList.any { it.blockchain == blockchain } - val isAddedOnMainScreen = args.mainScreenBlockchainList.contains(blockchain) + val isAddedOnMainScreen = currentBlockchainList.contains(blockchain) if (isTokenWithSameBlockchainFound) { router.openUnableHideMainTokenAlert( @@ -341,7 +374,7 @@ internal class TokensListViewModel @Inject constructor( val isRemoveAction = changedTokensList.contains(token) if (isRemoveAction) { - val isAddedOnMainScreen = args.mainScreenTokenList.contains(token) + val isAddedOnMainScreen = currentTokensList.contains(token) if (isAddedOnMainScreen) { router.openRemoveWalletAlert( diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt deleted file mode 100644 index fb79c7bbde..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.features.tokens.legacy.redux - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.tap.domain.model.WalletDataModel -import org.rekotlin.Action - -sealed interface TokensAction : Action { - - /** Single way to pass data to the screen */ - sealed interface SetArgs : TokensAction { - - data class ManageAccess(val wallets: List, val derivationStyle: DerivationStyle?) : SetArgs - - object ReadAccess : SetArgs - } - - // TODO: [REDACTED_TASK_KEY] Remove this action - data class SaveChanges(val tokens: List, val blockchains: List) : TokensAction -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt index db87942c84..012bdf1618 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt @@ -15,6 +15,10 @@ import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.supportsHdWallet import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.tokens.TokensAction +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchDebugErrorNotification @@ -23,6 +27,7 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Middleware @@ -34,28 +39,69 @@ object TokensMiddleware { { next -> { action -> when (action) { - is TokensAction.SaveChanges -> handleSaveChanges(action) + is TokensAction.LegacySaveChanges -> handleLegacySaveChanges(action) + is TokensAction.NewSaveChanges -> handleNewSaveChanges(action) } next(action) } } } - private fun handleSaveChanges(action: TokensAction.SaveChanges) { + private fun handleNewSaveChanges(action: TokensAction.NewSaveChanges) { scope.launch { - val scanResponse = store.state.globalState.scanResponse ?: return@launch + val scanResponse = action.userWallet.scanResponse - val currentTokens = store.state.tokensState.addedTokens - val currentBlockchains = store.state.tokensState.addedBlockchains + val currentTokens = action.currentTokens + val currentBlockchains = action.currentCoins - val blockchainsToAdd = action.blockchains.filterNot(currentBlockchains::contains) - val blockchainsToRemove = - store.state.tokensState.addedBlockchains.filterNot(action.blockchains::contains) + val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains) + val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains) - val tokensToAdd = action.tokens.filterNot(currentTokens::contains) - val tokensToRemove = currentTokens.filterNot { token -> action.tokens.any { it.token == token.token } } + val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains) + val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } } - removeCurrenciesIfNeeded( + removeNewCurrenciesIfNeeded( + userWalletId = action.userWallet.walletId, + currencies = blockchainsToRemove + tokensToRemove, + ) + + val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty() + val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() + if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) { + store.dispatchDebugErrorNotification(message = "Nothing to save") + store.dispatchOnMain(NavigationAction.PopBackTo()) + return@launch + } + + val currencyList = blockchainsToAdd + tokensToAdd + + if (scanResponse.supportsHdWallet()) { + deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) { + submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList) + store.dispatchOnMain(NavigationAction.PopBackTo()) + } + } else { + submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList) + store.dispatchOnMain(NavigationAction.PopBackTo()) + } + } + } + + private fun handleLegacySaveChanges(action: TokensAction.LegacySaveChanges) { + scope.launch { + val scanResponse = action.scanResponse + + val currentTokens = action.currentTokens + val currentBlockchains = action.currentBlockchains + + val blockchainsToAdd = action.changedBlockchains.filterNot(currentBlockchains::contains) + val blockchainsToRemove = currentBlockchains.filterNot(action.changedBlockchains::contains) + + val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains) + val tokensToRemove = + currentTokens.filterNot { token -> action.changedTokens.any { it.token == token.token } } + + removeLegacyCurrenciesIfNeeded( currencies = convertToCurrencies( blockchains = blockchainsToRemove, tokens = tokensToRemove, @@ -79,11 +125,11 @@ object TokensMiddleware { if (scanResponse.supportsHdWallet()) { deriveMissingBlockchains(scanResponse, currencyList) { - submitAdd(it, currencyList) + submitLegacyAdd(it, currencyList) store.dispatchOnMain(NavigationAction.PopBackTo()) } } else { - submitAdd(scanResponse, currencyList) + submitLegacyAdd(scanResponse, currencyList) store.dispatchOnMain(NavigationAction.PopBackTo()) } } @@ -94,15 +140,14 @@ object TokensMiddleware { tokens: List, derivationStyle: DerivationStyle?, ): List { - return blockchains.map { - Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) - } + tokens.map { - Currency.Token( - it.token, - it.blockchain, - it.blockchain.derivationPath(derivationStyle)?.rawPath, - ) - } + return blockchains.map { Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) } + + tokens.map { + Currency.Token( + token = it.token, + blockchain = it.blockchain, + derivationPath = it.blockchain.derivationPath(derivationStyle)?.rawPath, + ) + } } private fun deriveMissingBlockchains( @@ -113,7 +158,7 @@ object TokensMiddleware { val config = CardConfig.createConfig(scanResponse.card) val derivationDataList = currencyList.mapNotNull { val curve = config.primaryCurve(it.blockchain) - curve?.let { getDerivations(curve, scanResponse, currencyList) } + curve?.let { getLegacyDerivations(curve, scanResponse, currencyList) } } val derivations = derivationDataList.associate { it.derivations } if (derivations.isEmpty()) { @@ -153,7 +198,55 @@ object TokensMiddleware { } } - private fun getDerivations( + private fun deriveMissingCoins( + scanResponse: ScanResponse, + currencyList: List, + onSuccess: (ScanResponse) -> Unit, + ) { + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { + config.primaryCurve(blockchain = Blockchain.fromId(it.network.id.value)) + ?.let { curve -> getNewDerivations(curve, scanResponse, currencyList) } + } + val derivations = derivationDataList.associate(DerivationData::derivations) + if (derivations.isEmpty()) { + onSuccess(scanResponse) + return + } + + scope.launch { + val result = tangemSdkManager.derivePublicKeys( + cardId = null, + derivations = derivations, + ) + when (result) { + is CompletionResult.Success -> { + val newDerivedKeys = result.data.entries + val oldDerivedKeys = scanResponse.derivedKeys + + val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() + + val updatedDerivedKeys = walletKeys.associateWith { walletKey -> + val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap()) + val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) + ExtendedPublicKeysMap(oldDerivations + newDerivations) + } + val updatedScanResponse = scanResponse.copy( + derivedKeys = updatedDerivedKeys, + ) + store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) + delay(DELAY_SDK_DIALOG_CLOSE) + + onSuccess(updatedScanResponse) + } + is CompletionResult.Failure -> { + store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens")) + } + } + } + } + + private fun getLegacyDerivations( curve: EllipticCurve, scanResponse: ScanResponse, currencyList: List, @@ -190,9 +283,48 @@ object TokensMiddleware { return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) } + private fun getNewDerivations( + curve: EllipticCurve, + scanResponse: ScanResponse, + currencyList: List, + ): DerivationData? { + val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null + + val manageTokensCandidates = currencyList + .map { Blockchain.fromId(it.network.id.value) } + .distinct() + .filter { it.getSupportedCurves().contains(curve) } + .mapNotNull { it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) } + + val customTokensCandidates = currencyList + .filter { Blockchain.fromId(it.network.id.value).getSupportedCurves().contains(curve) } + .mapNotNull(CryptoCurrency::derivationPath) + .map(::DerivationPath) + + val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList() + if (bothCandidates.isEmpty()) return null + + currencyList.find { it is CryptoCurrency.Coin && Blockchain.fromId(it.network.id.value) == Blockchain.Cardano } + ?.let { currency -> + currency.derivationPath?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() + val alreadyDerivedKeys: ExtendedPublicKeysMap = + scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) + val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() + + val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } + if (toDerive.isEmpty()) return null + + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) + } + class DerivationData(val derivations: Pair>) - private fun submitAdd(scanResponse: ScanResponse, currencyList: List) { + private fun submitLegacyAdd(scanResponse: ScanResponse, currencyList: List) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to add currencies, no user wallet selected") return @@ -213,7 +345,15 @@ object TokensMiddleware { } } - private suspend fun removeCurrenciesIfNeeded(currencies: List) { + private fun submitNewAdd(userWalletId: UserWalletId, currencyList: List) { + val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) + + scope.launch { + currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList) + } + } + + private suspend fun removeLegacyCurrenciesIfNeeded(currencies: List) { if (currencies.isEmpty()) return val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to remove currencies, no user wallet selected") @@ -221,4 +361,11 @@ object TokensMiddleware { } walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies) } + + private suspend fun removeNewCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List) { + if (currencies.isEmpty()) return + val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) + + currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt index 8e33fdc8f4..389a02219a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt @@ -1,11 +1,7 @@ package com.tangem.tap.features.tokens.legacy.redux -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.model.WalletDataModel -import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.models.Currency.Token import org.rekotlin.Action object TokensReducer { @@ -16,40 +12,8 @@ private fun internalReduce(action: Action, state: AppState): TokensState { if (action !is TokensAction) return state.tokensState return when (action) { - is TokensAction.SetArgs.ManageAccess -> { - state.tokensState.copy( - isManageAccess = true, - addedWallets = action.wallets, - addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle), - addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle), - ) - } - - is TokensAction.SetArgs.ReadAccess -> { - state.tokensState.copy(isManageAccess = false) - } - + is TokensAction.SetArgs.ManageAccess -> state.tokensState.copy(isManageAccess = true) + is TokensAction.SetArgs.ReadAccess -> state.tokensState.copy(isManageAccess = false) else -> state.tokensState } -} - -private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { - return mapNotNull { walletDataModel -> - if (walletDataModel.currency.isCustomCurrency(derivationStyle)) { - null - } else { - (walletDataModel.currency as? Currency.Blockchain)?.blockchain - } - }.distinct() -} - -private fun List.toNonCustomTokensWithBlockchains( - derivationStyle: DerivationStyle?, -): List { - return mapNotNull { walletDataModel -> - if (walletDataModel.currency !is Token) return@mapNotNull null - if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null - - TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain) - }.distinct() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt index 156fd0c922..14c5d9d721 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt @@ -1,16 +1,5 @@ package com.tangem.tap.features.tokens.legacy.redux -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.tap.domain.model.WalletDataModel import org.rekotlin.StateType -data class TokensState( - val isManageAccess: Boolean = false, - val addedWallets: List = emptyList(), - val addedTokens: List = emptyList(), - val addedBlockchains: List = emptyList(), -) : StateType - -// TODO: [REDACTED_TASK_KEY] Remove this class -data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain) \ No newline at end of file +data class TokensState(val isManageAccess: Boolean = false) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 8f17e52682..290bd68881 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -6,7 +6,7 @@ import com.badoo.mvicore.modelWatcher import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Portfolio import com.tangem.tap.common.entities.FiatCurrency @@ -14,7 +14,6 @@ import com.tangem.tap.common.extensions.getQuantityString import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.domain.model.TotalFiatBalance -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState @@ -106,14 +105,8 @@ class MultiWalletView : WalletView() { binding.btnAddToken.setOnClickListener { Analytics.send(Portfolio.ButtonManageTokens()) - store.dispatch( - TokensAction.SetArgs.ManageAccess( - wallets = state.walletsDataFromStores, - derivationStyle = store.state.globalState.scanResponse - ?.derivationStyleProvider?.getDerivationStyle(), - ), - ) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) + store.dispatch(action = TokensAction.SetArgs.ManageAccess) + store.dispatch(action = NavigationAction.NavigateTo(screen = AppScreen.AddTokens)) } handleErrorStates(state = state, binding = binding, fragment = fragment) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt b/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt new file mode 100644 index 0000000000..4dcca75874 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.tokens + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import org.rekotlin.Action + +sealed interface TokensAction : Action { + + /** Single way to pass data to the screen */ + sealed interface SetArgs : TokensAction { + object ManageAccess : SetArgs + object ReadAccess : SetArgs + } + + @Deprecated("Action is used for saving data by old way. It will be removed after deleting of legacy wallet screen") + data class LegacySaveChanges( + val currentTokens: List, + val currentBlockchains: List, + val changedTokens: List, + val changedBlockchains: List, + val scanResponse: ScanResponse, + ) : TokensAction + + data class NewSaveChanges( + val currentTokens: List, + val currentCoins: List, + val changedTokens: List, + val changedCoins: List, + val userWallet: UserWallet, + ) : TokensAction +} + +data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 1f2483b904..4dc7b9293c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -332,6 +332,7 @@ internal class WalletViewModel @Inject constructor( } override fun onManageTokensClick() { + reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) router.openManageTokensScreen() }