diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index a7f064da55..1cee11bdbc 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -5,6 +5,8 @@ import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -48,4 +50,10 @@ internal object CardDomainModule { @Provides @ViewModelScoped fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig()) + + @Provides + @ViewModelScoped + fun provideDerivePublicKeysUseCase(tangemSdkManager: TangemSdkManager): DerivePublicKeysUseCase { + return DefaultDerivePublicKeysUseCase(tangemSdkManager = tangemSdkManager) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivePublicKeysUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivePublicKeysUseCase.kt new file mode 100644 index 0000000000..d402c97a7c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivePublicKeysUseCase.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.domain.card + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.tap.domain.TangemSdkManager + +// TODO: [REDACTED_JIRA] +internal class DefaultDerivePublicKeysUseCase( + private val tangemSdkManager: TangemSdkManager, +) : DerivePublicKeysUseCase { + + override suspend fun invoke( + cardId: String?, + derivations: Map>, + ): Either { + tangemSdkManager.derivePublicKeys(cardId = cardId, derivations = derivations) + .doOnSuccess { return it.right() } + .doOnFailure { return Unit.left() } + + return Unit.left() + } +} \ 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 79abcbe9f6..bbea323a16 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 @@ -77,11 +77,15 @@ object TokensMiddleware { if (scanResponse.supportsHdWallet()) { deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) { - submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList) + submitNewAdd( + userWalletId = action.userWallet.walletId, + updatedScanResponse = it, + currencyList = currencyList, + ) store.dispatchOnMain(NavigationAction.PopBackTo()) } } else { - submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList) + submitNewAdd(userWalletId = action.userWallet.walletId, scanResponse, currencyList = currencyList) store.dispatchOnMain(NavigationAction.PopBackTo()) } } @@ -242,9 +246,8 @@ object TokensMiddleware { val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) ExtendedPublicKeysMap(oldDerivations + newDerivations) } - val updatedScanResponse = scanResponse.copy( - derivedKeys = updatedDerivedKeys, - ) + val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys) + store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) delay(DELAY_SDK_DIALOG_CLOSE) @@ -351,10 +354,19 @@ object TokensMiddleware { } } - private fun submitNewAdd(userWalletId: UserWalletId, currencyList: List) { + private fun submitNewAdd( + userWalletId: UserWalletId, + updatedScanResponse: ScanResponse, + currencyList: List, + ) { val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) scope.launch { + userWalletsListManager.update( + userWalletId = userWalletId, + update = { it.copy(scanResponse = updatedScanResponse) }, + ) + currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList) } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/DerivePublicKeysUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/DerivePublicKeysUseCase.kt new file mode 100644 index 0000000000..8a82c6b79d --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/DerivePublicKeysUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.card + +import arrow.core.Either +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.operations.derivation.DerivationTaskResponse + +interface DerivePublicKeysUseCase { + + suspend operator fun invoke( + cardId: String? = null, + derivations: Map>, + ): Either +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt index 1990c26dda..4fbd06b10b 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt @@ -22,14 +22,14 @@ class UpdateWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { suspend operator fun invoke( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, - ): Either { + ): Either { val userWalletsListManager = walletsStateHolder.userWalletsListManager ?: return UpdateWalletError.DataError.left() userWalletsListManager.update(userWalletId, update) - .doOnSuccess { return Unit.right() } + .doOnSuccess { return it.right() } .doOnFailure { return UpdateWalletError.DataError.left() } - return Unit.right() + return UpdateWalletError.DataError.left() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index e0da1fe275..4e7d1b3d34 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -9,7 +9,7 @@ internal interface WalletClickIntents { fun onBackClick() - fun onGenerateMissedAddressesClick() + fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) fun onScanToUnlockWalletClick() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index e6a3d1a22c..183f993df2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -5,6 +5,7 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -30,7 +31,7 @@ internal class WalletNotificationsListFactory( fun create( cardTypesResolver: CardTypesResolver, - cryptoCurrencyList: List, + cryptoCurrencyList: List, ): Flow> { return flow { emit( @@ -78,21 +79,25 @@ internal class WalletNotificationsListFactory( } private fun MutableList.addMissingAddressesNotification( - cryptoCurrencyList: List, + cryptoCurrencyList: List, ) { - val missedAddressesCount = cryptoCurrencyList.getMissedAddressesCount() + val missedAddressCurrencies = cryptoCurrencyList.getMissedAddressCurrencies() addIf( element = WalletNotification.MissingAddresses( - missingAddressesCount = missedAddressesCount, - onGenerateClick = clickIntents::onGenerateMissedAddressesClick, + missingAddressesCount = missedAddressCurrencies.count(), + onGenerateClick = { + clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = missedAddressCurrencies) + }, ), - condition = missedAddressesCount > 0, + condition = missedAddressCurrencies.isNotEmpty(), ) } - private fun List.getMissedAddressesCount(): Int { - return filterIsInstance().count() + private fun List.getMissedAddressCurrencies(): List { + return this + .filter { it.value is CryptoCurrencyStatus.MissedDerivation } + .map(CryptoCurrencyStatus::currency) } // TODO: [REDACTED_JIRA] @@ -109,7 +114,7 @@ internal class WalletNotificationsListFactory( private suspend fun MutableList.addWarningNotifications( cardTypesResolver: CardTypesResolver, - cryptoCurrencyList: List, + cryptoCurrencyList: List, ) { addIf( element = WalletNotification.Warning.MissingBackup( @@ -163,12 +168,12 @@ internal class WalletNotificationsListFactory( if (condition) add(element = element) } - private fun List.hasUnreachableNetworks(): Boolean { - return any { it is CryptoCurrencyStatus.Unreachable } + private fun List.hasUnreachableNetworks(): Boolean { + return any { it.value is CryptoCurrencyStatus.Unreachable } } - private fun List.hasNoAccountStatus(): Boolean { - return any { it is CryptoCurrencyStatus.NoAccount } + private fun List.hasNoAccountStatus(): Boolean { + return any { it.value is CryptoCurrencyStatus.NoAccount } } private suspend fun checkSignedHashes(cardTypesResolver: CardTypesResolver, isDemo: Boolean): Boolean { 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 6e932219ae..0a92d35a37 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 @@ -3,24 +3,35 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels import androidx.lifecycle.* import androidx.paging.cachedIn import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.cardano.CardanoUtils +import com.tangem.blockchain.common.Blockchain import com.tangem.common.Provider +import com.tangem.common.card.EllipticCurve import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.toMapKey import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.navigation.AppScreen import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.card.* import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.settings.* +import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase +import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -43,6 +54,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory +import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -79,6 +91,7 @@ internal class WalletViewModel @Inject constructor( private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val scanCardProcessor: ScanCardProcessor, + private val derivePublicKeysUseCase: DerivePublicKeysUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, @@ -252,17 +265,107 @@ internal class WalletViewModel @Inject constructor( } } - override fun onGenerateMissedAddressesClick() { + override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { + val state = uiState as? WalletState.ContentState ?: return + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeScanYourCardTapped) - scanToUpdateSelectedWallet( - onSuccessSave = { - // Reload currencies with missed derivation - fetchTokenListUseCase(userWalletId = it.walletId) - }, - ) + viewModelScope.launch(dispatchers.io) { + val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) + + deriveMissingCurrencies( + scanResponse = userWallet.scanResponse, + currencyList = missedAddressCurrencies, + ) { scannedCardResponse -> + updateWalletUseCase( + userWalletId = userWallet.walletId, + update = { it.copy(scanResponse = scannedCardResponse) }, + ) + .onRight { + fetchTokenListUseCase(userWalletId = it.walletId, refresh = true) + } + } + } } + // TODO: [REDACTED_JIRA] + private fun deriveMissingCurrencies( + scanResponse: ScanResponse, + currencyList: List, + onSuccess: suspend (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) + .ifEmpty { return } + + viewModelScope.launch(dispatchers.io) { + derivePublicKeysUseCase(cardId = null, derivations = derivations) + .onRight { + val newDerivedKeys = it.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) + + onSuccess(updatedScanResponse) + } + } + } + + 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 { it.network.derivationPath.value } + .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.network.derivationPath.value?.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>) + override fun onScanToUnlockWalletClick() { scanToUpdateSelectedWallet() } @@ -708,13 +811,12 @@ internal class WalletViewModel @Inject constructor( is TokenList.GroupedByNetwork -> { tokenList.groups .flatMap(NetworkGroup::currencies) - .map(CryptoCurrencyStatus::value) } - is TokenList.Ungrouped -> tokenList.currencies.map(CryptoCurrencyStatus::value) + is TokenList.Ungrouped -> tokenList.currencies is TokenList.NotInitialized -> emptyList() } } else { - listOfNotNull(singleWalletCryptoCurrencyStatus?.value) + listOfNotNull(singleWalletCryptoCurrencyStatus) }, ) .distinctUntilChanged()