From 5077e013f3f902f2f836470e3afab88360385c56 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 4 Jan 2024 12:25:40 +0100 Subject: [PATCH 01/13] qml: don't show fiat amount when timestamp more than a day old and historic rates are disabled --- .../qml/components/controls/FormattedAmount.qml | 5 +++-- .../components/controls/HistoryItemDelegate.qml | 6 +++++- electrum/gui/qml/qefx.py | 14 +++++++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/components/controls/FormattedAmount.qml b/electrum/gui/qml/components/controls/FormattedAmount.qml index b33880ad3..80d6d778a 100644 --- a/electrum/gui/qml/components/controls/FormattedAmount.qml +++ b/electrum/gui/qml/components/controls/FormattedAmount.qml @@ -47,8 +47,9 @@ GridLayout { if (historic && timestamp) fiatLabel.text = '(' + Daemon.fx.fiatValueHistoric(amount, timestamp) + ' ' + Daemon.fx.fiatCurrency + ')' else - fiatLabel.text = '(' + Daemon.fx.fiatValue(amount) + ' ' + Daemon.fx.fiatCurrency + ')' - + fiatLabel.text = Daemon.fx.isRecent(timestamp) + ? '(' + Daemon.fx.fiatValue(amount) + ' ' + Daemon.fx.fiatCurrency + ')' + : '' } onAmountChanged: setFiatValue() diff --git a/electrum/gui/qml/components/controls/HistoryItemDelegate.qml b/electrum/gui/qml/components/controls/HistoryItemDelegate.qml index 79e40b4fd..0c7940bb4 100644 --- a/electrum/gui/qml/components/controls/HistoryItemDelegate.qml +++ b/electrum/gui/qml/components/controls/HistoryItemDelegate.qml @@ -108,7 +108,11 @@ Item { } else if (Daemon.fx.historicRates) { text = Daemon.fx.fiatValueHistoric(model.value, model.timestamp) + ' ' + Daemon.fx.fiatCurrency } else { - text = Daemon.fx.fiatValue(model.value, false) + ' ' + Daemon.fx.fiatCurrency + if (Daemon.fx.isRecent(model.timestamp)) { + text = Daemon.fx.fiatValue(model.value, false) + ' ' + Daemon.fx.fiatCurrency + } else { + text = '' + } } } Component.onCompleted: updateText() diff --git a/electrum/gui/qml/qefx.py b/electrum/gui/qml/qefx.py index fcb7316da..5546aca63 100644 --- a/electrum/gui/qml/qefx.py +++ b/electrum/gui/qml/qefx.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from decimal import Decimal from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRegularExpression @@ -164,3 +164,15 @@ def satoshiValue(self, fiat, plain=True): return str(v.to_integral_value()) else: return self.config.format_amount(v) + + @pyqtSlot(str, result=bool) + def isRecent(self, timestamp): + # return True if unknown, e.g. timestamp not known yet, tx in mempool + try: + td = Decimal(timestamp) + if td == 0: + return True + except Exception: + return True + dt = datetime.fromtimestamp(int(td)) + return dt + timedelta(days=1) > datetime.today() From 0b7fa9cd99dc18cf5e6c6cfca3fafb19532d124b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 4 Jan 2024 12:34:17 +0100 Subject: [PATCH 02/13] bip21: fail bip21 uri if unsupported req-* parameter is present. fixes #8781 --- electrum/bip21.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/bip21.py b/electrum/bip21.py index bcf6bd361..6b9cdf2fc 100644 --- a/electrum/bip21.py +++ b/electrum/bip21.py @@ -43,6 +43,9 @@ def parse_bip21_URI(uri: str) -> dict: for k, v in pq.items(): if len(v) != 1: raise InvalidBitcoinURI(f'Duplicate Key: {repr(k)}') + if k.startswith('req-'): + # we have no support for any req-* query parameters + raise InvalidBitcoinURI(f'Unsupported Key: {repr(k)}') out = {k: v[0] for k, v in pq.items()} if address: From 313b79cfaf40c5a08a221ec75000aa12c0e1ea25 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 4 Jan 2024 13:08:17 +0100 Subject: [PATCH 03/13] qml: add txid not empty assert to removeLocalTx. ref #8775 --- electrum/gui/qml/qetxdetails.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 12ac1c9d3..f67fe9599 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -441,9 +441,10 @@ def onBroadcastFailed(self, txid, code, reason): @pyqtSlot() @pyqtSlot(bool) - def removeLocalTx(self, confirm = False): - assert self._can_remove + def removeLocalTx(self, confirm=False): + assert self._can_remove, 'cannot remove' txid = self._txid + assert txid, 'txid unset' if not confirm: num_child_txs = len(self._wallet.wallet.adb.get_depending_transactions(txid)) From bd88b6ba298e4a09849efa48c66576424d60bdcd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jan 2024 16:00:24 +0000 Subject: [PATCH 04/13] tests: add unit test for prev --- electrum/tests/test_util.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/electrum/tests/test_util.py b/electrum/tests/test_util.py index 1767bc8bf..f64af19d3 100644 --- a/electrum/tests/test_util.py +++ b/electrum/tests/test_util.py @@ -149,6 +149,13 @@ def test_parse_URI_invalid(self): def test_parse_URI_parameter_pollution(self): self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003&label=test&amount=30.0') + @as_testnet + def test_parse_URI_unsupported_req_key(self): + self._do_test_parse_URI('bitcoin:TB1QXJ6KVTE6URY2MX695METFTFT7LR5HYK4M3VT5F?amount=0.00100000&label=test&somethingyoudontunderstand=50', + {'address': 'TB1QXJ6KVTE6URY2MX695METFTFT7LR5HYK4M3VT5F', 'amount': 100000, 'label': 'test', 'somethingyoudontunderstand': '50'}) + # now test same URI but with "req-test=1" added + self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:TB1QXJ6KVTE6URY2MX695METFTFT7LR5HYK4M3VT5F?amount=0.00100000&label=test&req-test=1&somethingyoudontunderstand=50') + @as_testnet def test_parse_URI_lightning_consistency(self): # bip21 uri that *only* includes a "lightning" key. LN part does not have fallback address From a51b3bdbfbbc075d2f785012307ffbb68382e2a5 Mon Sep 17 00:00:00 2001 From: accumulator Date: Thu, 4 Jan 2024 17:31:00 +0100 Subject: [PATCH 05/13] qml: show fiat price when historic rates are enabled and no timestamp available Co-authored-by: ghost43 --- electrum/gui/qml/components/controls/HistoryItemDelegate.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/HistoryItemDelegate.qml b/electrum/gui/qml/components/controls/HistoryItemDelegate.qml index 0c7940bb4..3491a2a9a 100644 --- a/electrum/gui/qml/components/controls/HistoryItemDelegate.qml +++ b/electrum/gui/qml/components/controls/HistoryItemDelegate.qml @@ -105,7 +105,7 @@ Item { function updateText() { if (!Daemon.fx.enabled) { text = '' - } else if (Daemon.fx.historicRates) { + } else if (Daemon.fx.historicRates && model.timestamp) { text = Daemon.fx.fiatValueHistoric(model.value, model.timestamp) + ' ' + Daemon.fx.fiatCurrency } else { if (Daemon.fx.isRecent(model.timestamp)) { From 3b630c7a5be6c306659dc9edff5fae3a847e2283 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jan 2024 12:14:33 +0000 Subject: [PATCH 06/13] tests: add "short seed cheat sheet" hopefully I can remember what to ctrl+f to find this --- electrum/tests/test_mnemonic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/tests/test_mnemonic.py b/electrum/tests/test_mnemonic.py index 6bf17e757..4674d24c8 100644 --- a/electrum/tests/test_mnemonic.py +++ b/electrum/tests/test_mnemonic.py @@ -179,13 +179,16 @@ class Test_seeds(ElectrumTestCase): ('ostrich security deer aunt climb inner alpha arm mutual marble solid task', 'standard'), ('OSTRICH SECURITY DEER AUNT CLIMB INNER ALPHA ARM MUTUAL MARBLE SOLID TASK', 'standard'), (' oStRiCh sEcUrItY DeEr aUnT ClImB InNeR AlPhA ArM MuTuAl mArBlE SoLiD TaSk ', 'standard'), - ('x8', 'standard'), ('science dawn member doll dutch real can brick knife deny drive list', '2fa'), ('science dawn member doll dutch real ca brick knife deny drive list', ''), (' sCience dawn member doll Dutch rEAl can brick knife deny drive lisT', '2fa'), ('frost pig brisk excite novel report camera enlist axis nation novel desert', 'segwit'), (' fRoSt pig brisk excIte novel rePort CamEra enlist axis nation nOVeL dEsert ', 'segwit'), + # short seed cheat sheet: + ('x8', 'standard'), ('9dk', 'segwit'), + ('abandon bike', 'segwit'), # <- has valid English words + ('6vs', '2fa_segwit'), } def test_new_seed(self): From 201c0ab71b162f3e5f30a5da840a39bda75a022c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jan 2024 12:51:54 +0000 Subject: [PATCH 07/13] qt wizard: fix restoring from 2fa seed follow-up 7df057aaf9589b128069575656791677bb341d71 --- electrum/gui/qt/wizard/wallet.py | 5 +++-- electrum/tests/test_mnemonic.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index 1d58bb715..0c6a5ad01 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -617,12 +617,13 @@ def on_ready(self): self.layout().addStretch(1) def is_seed(self, x): + t = mnemonic.seed_type(x) if self.wizard_data['wallet_type'] == 'standard': return mnemonic.is_seed(x) elif self.wizard_data['wallet_type'] == '2fa': - return mnemonic.is_any_2fa_seed_type(x) + return mnemonic.is_any_2fa_seed_type(t) else: - return mnemonic.seed_type(x) in ['standard', 'segwit'] + return t in ['standard', 'segwit'] def validate(self): # precond: only call when SeedLayout deems seed a valid seed diff --git a/electrum/tests/test_mnemonic.py b/electrum/tests/test_mnemonic.py index 4674d24c8..8b98b8a7b 100644 --- a/electrum/tests/test_mnemonic.py +++ b/electrum/tests/test_mnemonic.py @@ -189,6 +189,7 @@ class Test_seeds(ElectrumTestCase): ('9dk', 'segwit'), ('abandon bike', 'segwit'), # <- has valid English words ('6vs', '2fa_segwit'), + ('agree install', '2fa_segwit'), # <- has valid English words } def test_new_seed(self): From 37173845c20219fce25d275a5f490ed305e5c40c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jan 2024 13:26:33 +0000 Subject: [PATCH 08/13] qt wizard: WizardComponent: (fix) also inherit ABC for `@abstractmethod` decorator to work (except, turns out, it's not so simple because of pyqt's own magic for QWidget) --- electrum/gui/qt/util.py | 7 +++++++ electrum/gui/qt/wizard/wizard.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index eba59166f..f24fa57a9 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -1,3 +1,4 @@ +from abc import ABC, ABCMeta import os.path import time import sys @@ -1430,6 +1431,12 @@ def decorator(self, *args): return decorator +class _ABCQObjectMeta(type(QObject), ABCMeta): pass +class _ABCQWidgetMeta(type(QWidget), ABCMeta): pass +class AbstractQObject(QObject, ABC, metaclass=_ABCQObjectMeta): pass +class AbstractQWidget(QWidget, ABC, metaclass=_ABCQWidgetMeta): pass + + if __name__ == "__main__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done")) diff --git a/electrum/gui/qt/wizard/wizard.py b/electrum/gui/qt/wizard/wizard.py index 791bceadf..424b5c7ff 100644 --- a/electrum/gui/qt/wizard/wizard.py +++ b/electrum/gui/qt/wizard/wizard.py @@ -10,7 +10,7 @@ from electrum.i18n import _ from electrum.logging import get_logger -from electrum.gui.qt.util import Buttons, icon_path, MessageBoxMixin, WWLabel, ResizableStackedWidget +from electrum.gui.qt.util import Buttons, icon_path, MessageBoxMixin, WWLabel, ResizableStackedWidget, AbstractQWidget if TYPE_CHECKING: from electrum.simple_config import SimpleConfig @@ -233,7 +233,7 @@ def is_finalized(self, wizard_data: dict) -> bool: return True -class WizardComponent(QWidget): +class WizardComponent(AbstractQWidget): updated = pyqtSignal(object) def __init__(self, parent: QWidget, wizard: QEAbstractWizard, *, title: str = None, layout: QLayout = None): From dab768b9320343343d53f1a978b5d31101910274 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jan 2024 13:34:57 +0000 Subject: [PATCH 09/13] qt/wizard/wallet: improve typing: introduce WalletWizardComponent cls --- electrum/gui/qt/wizard/wallet.py | 89 ++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index 0c6a5ad01..3789ed7b8 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -1,8 +1,9 @@ +from abc import ABC import os import sys import threading -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from PyQt5.QtCore import Qt, QTimer, QRect, pyqtSignal from PyQt5.QtGui import QPen, QPainter, QPalette @@ -252,9 +253,17 @@ def query_choice(self, msg, choices, title=None, default_choice=None): return clayout.selected_index() -class WCWalletName(WizardComponent, Logger): +class WalletWizardComponent(WizardComponent, ABC): + # ^ this class only exists to help with typing + wizard: QENewWalletWizard + + def __init__(self, parent: QWidget, wizard: QENewWalletWizard, **kwargs): + WizardComponent.__init__(self, parent, wizard, **kwargs) + + +class WCWalletName(WalletWizardComponent, Logger): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Electrum wallet')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Electrum wallet')) Logger.__init__(self) path = wizard._path @@ -396,9 +405,9 @@ def apply(self): self.wizard_data['wallet_needs_hw_unlock'] = self.wallet_needs_hw_unlock -class WCWalletType(WizardComponent): +class WCWalletType(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Create new wallet')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Create new wallet')) message = _('What kind of wallet do you want to create?') wallet_kinds = [ ('standard', _('Standard wallet')), @@ -417,9 +426,9 @@ def apply(self): self.wizard_data['wallet_type'] = self.choice_w.selected_item[0] -class WCKeystoreType(WizardComponent): +class WCKeystoreType(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Keystore')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Keystore')) message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') choices = [ ('createseed', _('Create a new seed')), @@ -437,9 +446,9 @@ def apply(self): self.wizard_data['keystore_type'] = self.choice_w.selected_item[0] -class WCCreateSeed(WizardComponent): +class WCCreateSeed(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Wallet Seed')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Wallet Seed')) self._busy = True self.seed_type = 'standard' if self.wizard.config.WIZARD_DONT_CREATE_SEGWIT else 'segwit' self.slayout = None @@ -476,9 +485,9 @@ def create_seed(self): self.valid = True -class WCConfirmSeed(WizardComponent): +class WCConfirmSeed(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Confirm Seed')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Confirm Seed')) message = ' '.join([ _('Your seed is important!'), _('If you lose your seed, your money will be permanently lost.'), @@ -507,9 +516,9 @@ def apply(self): pass -class WCEnterExt(WizardComponent, Logger): +class WCEnterExt(WalletWizardComponent, Logger): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Seed Extension')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Seed Extension')) Logger.__init__(self) message = '\n'.join([ @@ -564,9 +573,9 @@ def apply(self): cosigner_data['seed_extra_words'] = self.ext_edit.text() -class WCConfirmExt(WizardComponent): +class WCConfirmExt(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Confirm Seed Extension')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Confirm Seed Extension')) message = '\n'.join([ _('Your seed extension must be saved together with your seed.'), _('Please type it here.'), @@ -583,9 +592,9 @@ def apply(self): pass -class WCHaveSeed(WizardComponent, Logger): +class WCHaveSeed(WalletWizardComponent, Logger): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Enter Seed')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Enter Seed')) Logger.__init__(self) self.slayout = None @@ -630,7 +639,7 @@ def validate(self): seed = self.slayout.get_seed() seed_variant = self.slayout.seed_type wallet_type = self.wizard_data['wallet_type'] - seed_valid, seed_type, validation_message = self.wizard.validate_seed(seed, seed_variant, wallet_type) + seed_valid, seed_type, validation_message = self.wizard.validate_seed(seed, seed_variant, wallet_type) # is_cosigner = self.wizard_data['wallet_type'] == 'multisig' and 'multisig_current_cosigner' in self.wizard_data @@ -658,9 +667,9 @@ def apply(self): cosigner_data['seed_extra_words'] = '' # empty default -class WCScriptAndDerivation(WizardComponent, Logger): +class WCScriptAndDerivation(WalletWizardComponent, Logger): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Script type and Derivation path')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Script type and Derivation path')) Logger.__init__(self) self.choice_w = None @@ -761,9 +770,9 @@ def apply(self): cosigner_data['derivation_path'] = str(self.derivation_path_edit.text()) -class WCCosignerKeystore(WizardComponent): +class WCCosignerKeystore(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard) + WalletWizardComponent.__init__(self, parent, wizard) message = _('Add a cosigner to your multi-sig wallet') choices = [ @@ -809,9 +818,9 @@ def apply(self): } -class WCHaveMasterKey(WizardComponent): +class WCHaveMasterKey(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Create keystore from a master key')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Create keystore from a master key')) self.slayout = None @@ -872,9 +881,9 @@ def apply(self): cosigner_data['master_key'] = text -class WCMultisig(WizardComponent): +class WCMultisig(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Multi-Signature Wallet')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Multi-Signature Wallet')) def on_m(m): m_label.setText(_('Require {0} signatures').format(m)) @@ -933,9 +942,9 @@ def apply(self): self.wizard_data['multisig_cosigner_data'] = {} -class WCImport(WizardComponent): +class WCImport(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses or Private Keys')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses or Private Keys')) message = _( 'Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.') header_layout = QHBoxLayout() @@ -969,9 +978,9 @@ def apply(self): self.wizard_data['private_key_list'] = text -class WCWalletPassword(WizardComponent): +class WCWalletPassword(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Wallet Password')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Wallet Password')) # TODO: PasswordLayout assumes a button, refactor PasswordLayout # for now, fake next_button.setEnabled @@ -1063,12 +1072,12 @@ def paintEvent(self, event): qp.end() -class WCChooseHWDevice(WizardComponent, Logger): +class WCChooseHWDevice(WalletWizardComponent, Logger): scanFailed = pyqtSignal([str, str], arguments=['code', 'message']) scanComplete = pyqtSignal() def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Choose Hardware Device')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Choose Hardware Device')) Logger.__init__(self) self.scanFailed.connect(self.on_scan_failed) self.scanComplete.connect(self.on_scan_complete) @@ -1224,9 +1233,9 @@ def apply(self): cosigner_data['hardware_device'] = self.choice_w.selected_item[0] -class WCWalletPasswordHardware(WizardComponent): +class WCWalletPasswordHardware(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Encrypt using hardware')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Encrypt using hardware')) self.plugins = wizard.plugins self.playout = PasswordLayoutForHW(MSG_HW_STORAGE_ENCRYPTION) @@ -1246,9 +1255,9 @@ def apply(self): self.wizard_data['password'] = client.get_password_for_storage_encryption() -class WCHWUnlock(WizardComponent, Logger): +class WCHWUnlock(WalletWizardComponent, Logger): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Unlocking hardware')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Unlocking hardware')) Logger.__init__(self) self.plugins = wizard.plugins self.plugin = None @@ -1310,9 +1319,9 @@ def apply(self): self.wizard_data['password'] = self.password -class WCHWXPub(WizardComponent, Logger): +class WCHWXPub(WalletWizardComponent, Logger): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Retrieving extended public key from hardware')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Retrieving extended public key from hardware')) Logger.__init__(self) self.plugins = wizard.plugins self.plugin = None @@ -1393,9 +1402,9 @@ def apply(self): cosigner_data['soft_device_id'] = self.soft_device_id -class WCHWUninitialized(WizardComponent): +class WCHWUninitialized(WalletWizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title=_('Hardware not initialized')) + WalletWizardComponent.__init__(self, parent, wizard, title=_('Hardware not initialized')) def on_ready(self): cosigner_data = self.wizard.current_cosigner(self.wizard_data) From 77c55d78b79ad83b825fc98a891ffabf0e8c4066 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jan 2024 15:00:45 +0000 Subject: [PATCH 10/13] qt wizard: show warning when trying to restore 2fa seed as std wallet With wallet_type=="standard", if the user enters a 2fa electrum seed, the "next" btn is disabled. This is a regression in the new wizard, the old one used to "redirect" seamlessly. This commit does not fix this, but at least shows a user-friendly warning message. Note: would be nice if the wizard redirected automatically, in both directions (2fa->std, std->2fa). The old wizard implemented std->2fa (probably the more common case hit by users), and had this warning message shown for the 2fa->std case. Now I am repurposing the warning also for std->2fa. --- electrum/gui/qt/seed_dialog.py | 8 ++++++-- electrum/gui/qt/wizard/wallet.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/seed_dialog.py b/electrum/gui/qt/seed_dialog.py index cd4af5e0e..6971e649f 100644 --- a/electrum/gui/qt/seed_dialog.py +++ b/electrum/gui/qt/seed_dialog.py @@ -32,7 +32,7 @@ QScrollArea, QWidget, QPushButton) from electrum.i18n import _ -from electrum.mnemonic import Mnemonic, seed_type +from electrum.mnemonic import Mnemonic, seed_type, is_any_2fa_seed_type from electrum import old_mnemonic from electrum import slip39 @@ -295,10 +295,14 @@ def on_edit(self): t = seed_type(s) label = _('Seed Type') + ': ' + t if t else '' if t and not b: # electrum seed, but does not conform to dialog rules + # FIXME we should just accept any electrum seed and "redirect" the wizard automatically. + # i.e. if user selected wallet_type=="standard" but entered a 2fa seed, accept and redirect + # if user selected wallet_type=="2fa" but entered a std electrum seed, accept and redirect + wiztype_fullname = _('Wallet with two-factor authentication') if is_any_2fa_seed_type(t) else _("Standard wallet") msg = ' '.join([ '' + _('Warning') + ': ', _("Looks like you have entered a valid seed of type '{}' but this dialog does not support such seeds.").format(t), - _("If unsure, try restoring as '{}'.").format(_("Standard wallet")), + _("If unsure, try restoring as '{}'.").format(wiztype_fullname), ]) self.seed_warning.setText(msg) else: diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index 3789ed7b8..bf6ebbd74 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -628,10 +628,11 @@ def on_ready(self): def is_seed(self, x): t = mnemonic.seed_type(x) if self.wizard_data['wallet_type'] == 'standard': - return mnemonic.is_seed(x) + return mnemonic.is_seed(x) and not mnemonic.is_any_2fa_seed_type(t) elif self.wizard_data['wallet_type'] == '2fa': return mnemonic.is_any_2fa_seed_type(t) else: + # multisig? by default, only accept modern non-2fa electrum seeds return t in ['standard', 'segwit'] def validate(self): From 683c6083c9a8fe1ea3d00973400cafc5f5159588 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jan 2024 15:14:33 +0000 Subject: [PATCH 11/13] wizard: do not log sensitive data (add more keys) --- electrum/wizard.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index 7aa4fa417..227cef317 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -163,7 +163,11 @@ def log_stack(self): self._logger.debug(logstr) def sanitize_stack_item(self, _stack_item) -> dict: - sensitive_keys = ['seed', 'seed_extra_words', 'master_key', 'private_key_list', 'password'] + sensitive_keys = [ + 'seed', 'seed_extra_words', 'master_key', 'private_key_list', 'password', + # trustedcoin: + 'xprv1', 'xpub1', 'xpub2', + ] def sanitize(_dict): result = {} From 66b8ec1833d2eb15ec708397438de6910257c77f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jan 2024 15:29:21 +0000 Subject: [PATCH 12/13] trustedcoin: rm some dead code used by old qt wizard --- electrum/plugins/trustedcoin/qt.py | 90 ------------------------------ 1 file changed, 90 deletions(-) diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py index 1bc97f838..64e4ca8b8 100644 --- a/electrum/plugins/trustedcoin/qt.py +++ b/electrum/plugins/trustedcoin/qt.py @@ -223,96 +223,6 @@ def on_click(b, k): vbox.addLayout(Buttons(CloseButton(d))) d.exec_() - def accept_terms_of_use(self, window): - vbox = QVBoxLayout() - vbox.addWidget(QLabel(_("Terms of Service"))) - - tos_e = TOS() - tos_e.setReadOnly(True) - vbox.addWidget(tos_e) - tos_received = False - - vbox.addWidget(QLabel(_("Please enter your e-mail address"))) - email_e = QLineEdit() - vbox.addWidget(email_e) - - next_button = window.next_button - prior_button_text = next_button.text() - next_button.setText(_('Accept')) - - def request_TOS(): - try: - tos = server.get_terms_of_service() - except Exception as e: - self.logger.exception('Could not retrieve Terms of Service') - tos_e.error_signal.emit(_('Could not retrieve Terms of Service:') - + '\n' + repr(e)) - return - self.TOS = tos - tos_e.tos_signal.emit() - - def on_result(): - tos_e.setText(self.TOS) - nonlocal tos_received - tos_received = True - set_enabled() - - def on_error(msg): - window.show_error(str(msg)) - window.terminate() - - def set_enabled(): - next_button.setEnabled(tos_received and is_valid_email(email_e.text())) - - tos_e.tos_signal.connect(on_result) - tos_e.error_signal.connect(on_error) - t = threading.Thread(target=request_TOS) - t.daemon = True - t.start() - email_e.textChanged.connect(set_enabled) - email_e.setFocus(True) - window.exec_layout(vbox, next_enabled=False) - next_button.setText(prior_button_text) - email = str(email_e.text()) - self.create_remote_key(email, window) - - def request_otp_dialog(self, window, short_id, otp_secret, xpub3): - vbox = QVBoxLayout() - if otp_secret is not None: - uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret) - l = QLabel("Please scan the following QR code in Google Authenticator. You may as well use the following key: %s"%otp_secret) - l.setWordWrap(True) - vbox.addWidget(l) - qrw = QRCodeWidget(uri) - vbox.addWidget(qrw, 1) - msg = _('Then, enter your Google Authenticator code:') - else: - label = QLabel( - "This wallet is already registered with TrustedCoin. " - "To finalize wallet creation, please enter your Google Authenticator Code. " - ) - label.setWordWrap(1) - vbox.addWidget(label) - msg = _('Google Authenticator code:') - hbox = QHBoxLayout() - hbox.addWidget(WWLabel(msg)) - pw = AmountEdit(None, is_int = True) - pw.setFocus(True) - pw.setMaximumWidth(50) - hbox.addWidget(pw) - vbox.addLayout(hbox) - cb_lost = QCheckBox(_("I have lost my Google Authenticator account")) - cb_lost.setToolTip(_("Check this box to request a new secret. You will need to retype your seed.")) - vbox.addWidget(cb_lost) - cb_lost.setVisible(otp_secret is None) - def set_enabled(): - b = True if cb_lost.isChecked() else len(pw.text()) == 6 - window.next_button.setEnabled(b) - pw.textChanged.connect(set_enabled) - cb_lost.toggled.connect(set_enabled) - window.exec_layout(vbox, next_enabled=False, raise_on_cancel=False) - self.check_otp(window, short_id, otp_secret, xpub3, pw.get_amount(), cb_lost.isChecked()) - @hook def init_wallet_wizard(self, wizard: 'QENewWalletWizard'): wizard.trustedcoin_qhelper = TrustedcoinPluginQObject(self, wizard, None) From f7ea2e0d3c6619e68641e471ccc6614509f56bf0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jan 2024 15:34:20 +0000 Subject: [PATCH 13/13] trustedcoin: fix qt wizard two-part-wallet-creation, online phase ``` 25.30 | E | gui.qt.exception_window.Exception_Hook | exception caught by crash reporter Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/gui/qt/__init__.py", line 439, in _start_wizard_to_select_or_create_wallet wallet = self.daemon.load_wallet(wallet_file, d['password'], upgrade=True) File "/home/user/wspace/electrum/electrum/daemon.py", line 481, in func_wrapper return func(self, *args, **kwargs) File "/home/user/wspace/electrum/electrum/daemon.py", line 491, in load_wallet wallet = self._load_wallet(path, password, upgrade=upgrade, config=self.config) File "/home/user/wspace/electrum/electrum/util.py", line 481, in do_profile o = func(*args, **kw_args) File "/home/user/wspace/electrum/electrum/daemon.py", line 516, in _load_wallet raise WalletUnfinished(db) electrum.wallet_db.WalletUnfinished: During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/gui/qt/wizard/wizard.py", line 203, in on_next_button_clicked if self.is_finalized(wd): File "/home/user/wspace/electrum/electrum/gui/qt/wizard/wallet.py", line 178, in is_finalized if not wizard_data['wallet_exists'] or wizard_data['wallet_is_open']: KeyError: 'wallet_exists' ``` --- electrum/gui/qt/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index bbcbeddcd..58c6bee08 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -451,12 +451,13 @@ def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wa xprv = k1.get_master_private_key(d['password']) else: xprv = db.get('x1')['xprv'] - data = { + _wiz_data_updates = { 'wallet_name': os.path.basename(wallet_file), 'xprv1': xprv, 'xpub1': db.get('x1')['xpub'], 'xpub2': db.get('x2')['xpub'], } + data = {**d, **_wiz_data_updates} wizard = QENewWalletWizard(self.config, self.app, self.plugins, self.daemon, path, start_viewstate=WizardViewState('trustedcoin_tos', data, {})) result = wizard.exec()