From aee0f8fb54ea2b7b5d62faa33475646da5f2667c Mon Sep 17 00:00:00 2001 From: f321x Date: Wed, 17 Dec 2025 11:02:26 +0100 Subject: [PATCH] qml: OpenWalletDialog: load any wallet if password matches If the user has wallets with different passwords (non-unified pw) and enters a password on startup that fails to unlock the recently used wallet this change will automatically open any other wallet if there is another wallet that can be unlocked with this password. --- .../gui/qml/components/OpenWalletDialog.qml | 33 ++++++++++++++++++- electrum/gui/qml/components/main.qml | 8 ++++- electrum/gui/qml/qeconfig.py | 10 ++++++ electrum/gui/qml/qedaemon.py | 1 + electrum/simple_config.py | 2 ++ 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/OpenWalletDialog.qml b/electrum/gui/qml/components/OpenWalletDialog.qml index b6209dec5..51ec37e61 100644 --- a/electrum/gui/qml/components/OpenWalletDialog.qml +++ b/electrum/gui/qml/components/OpenWalletDialog.qml @@ -12,6 +12,7 @@ ElDialog { property string name property string path + property bool isStartup property bool _invalidPassword: false property bool _unlockClicked: false @@ -40,7 +41,7 @@ ElDialog { InfoTextArea { id: notice - text: Daemon.singlePasswordEnabled || !Daemon.currentWallet + text: Daemon.singlePasswordEnabled || isStartup ? qsTr('Please enter password') : qsTr('Wallet %1 requires password to unlock').arg(name) iconStyle: InfoTextArea.IconStyle.Warn @@ -94,9 +95,39 @@ ElDialog { Daemon.loadWallet(openwalletdialog.path, password.text) } + function maybeUnlockAnyOtherWallet() { + // try to open any other wallet with the password the user entered, hack to improve ux for + // users with non-unified wallet password. + // we should only fall back to opening a random wallet if: + // - the user did not select a specific wallet, otherwise this is confusing + // - there can be more than one password, otherwise this scan would be pointless + if (Daemon.availableWallets.rowCount() <= 1 || password.text === '') { + return false + } + if (Config.walletDidUseSinglePassword) { + // the last time the wallet was unlocked all wallets used the same password. + // trying to decrypt all of them now is most probably useless. + return false + } + if (!openwalletdialog.isStartup) { + return false // this dialog got opened because the user clicked on a specific wallet + } + let wallet_paths = Daemon.getWalletsUnlockableWithPassword(password.text) + if (wallet_paths && wallet_paths.length > 0) { + console.log('could not unlock recent wallet, falling back to: ' + wallet_paths[0]) + Daemon.loadWallet(wallet_paths[0], password.text) + return true + } + return false + } + Connections { target: Daemon function onWalletRequiresPassword() { + if (maybeUnlockAnyOtherWallet()) { + password.text = '' // reset pw so we cannot end up in a loop + return + } console.log('invalid password') _invalidPassword = true password.tf.forceActiveFocus() diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index bf0b0c969..c8d364525 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -634,12 +634,18 @@ ApplicationWindow } property var _opendialog: undefined + property var _opendialog_startup: true function showOpenWalletDialog(name, path) { if (_opendialog == undefined) { - _opendialog = openWalletDialog.createObject(app, { name: name, path: path }) + _opendialog = openWalletDialog.createObject(app, { + name: name, + path: path, + isStartup: _opendialog_startup, + }) _opendialog.closed.connect(function() { _opendialog = undefined + _opendialog_startup = false }) _opendialog.open() } diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 9bf2a2997..d0d4d79e1 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -340,6 +340,16 @@ class QEConfig(AuthMixin, QObject): """ return self.config.WALLET_SHOULD_USE_SINGLE_PASSWORD + walletDidUseSinglePasswordChanged = pyqtSignal() + @pyqtProperty(bool, notify=walletDidUseSinglePasswordChanged) + def walletDidUseSinglePassword(self): + """ + Allows to guess if this is a unified password instance without having + unlocked any wallet yet. Might be out of sync e.g. if wallet files get copied manually. + """ + # TODO: consider removing once encrypted wallet file headers are available + return self.config.WALLET_DID_USE_SINGLE_PASSWORD + @pyqtSlot('qint64', result=str) @pyqtSlot(QEAmount, result=str) def formatSatsForEditing(self, satoshis): diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index acbe1a780..2b1bfa35b 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -237,6 +237,7 @@ class QEDaemon(AuthMixin, QObject): self._logger.info(f'use single password: {self._use_single_password}') else: self._logger.info('use single password disabled by config') + self.daemon.config.WALLET_DID_USE_SINGLE_PASSWORD = self._use_single_password run_hook('load_wallet', wallet) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 92b11defc..a74818626 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -676,6 +676,8 @@ class SimpleConfig(Logger): WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT = ConfigVar('unconf_utxo_freeze_threshold', default=5_000, type_=int) WALLET_PAYREQ_EXPIRY_SECONDS = ConfigVar('request_expiry', default=invoices.PR_DEFAULT_EXPIRATION_WHEN_CREATING, type_=int) WALLET_SHOULD_USE_SINGLE_PASSWORD = ConfigVar('should_use_single_password', default=False, type_=bool) + # TODO: consider removing WALLET_DID_USE_SINGLE_PASSWORD once encrypted wallet file headers are available + WALLET_DID_USE_SINGLE_PASSWORD = ConfigVar('did_use_single_password', default=False, type_=bool) # note: 'use_change' and 'multiple_change' are per-wallet settings WALLET_SEND_CHANGE_TO_LIGHTNING = ConfigVar( 'send_change_to_lightning', default=False, type_=bool,