diff --git a/contrib/build-wine/docker/Dockerfile b/contrib/build-wine/docker/Dockerfile index 20ca548f..784b9ef3 100644 --- a/contrib/build-wine/docker/Dockerfile +++ b/contrib/build-wine/docker/Dockerfile @@ -6,8 +6,8 @@ RUN dpkg --add-architecture i386 && \ apt-get update -q && \ apt-get install -qy \ wget=1.19.4-1ubuntu2.1 \ - gnupg2=2.2.4-1ubuntu1.1 \ - dirmngr=2.2.4-1ubuntu1.1 \ + gnupg2=2.2.4-1ubuntu1.2 \ + dirmngr=2.2.4-1ubuntu1.2 \ python3-software-properties=0.96.24.32.1 \ software-properties-common=0.96.24.32.1 \ && \ diff --git a/contrib/osx/make_osx b/contrib/osx/make_osx index 76091401..b33a3462 100755 --- a/contrib/osx/make_osx +++ b/contrib/osx/make_osx @@ -75,7 +75,6 @@ tar xz --directory $BUILDDIR cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/osx echo "82c368dfd4da017ceb32b12ca885576f325503428a4966cc09302cbd62702493 contrib/osx/libusb-1.0.dylib" | \ shasum -a 256 -c || fail "libusb checksum mismatched" -DoCodeSignMaybe "libusb" "contrib/osx/libusb-1.0.dylib" "$APP_SIGN" # If APP_SIGN is empty will be a noop info "Building libsecp256k1" brew install autoconf automake libtool @@ -88,7 +87,6 @@ git clean -f -x -q make popd cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/osx -DoCodeSignMaybe "libsecp256k1" "contrib/osx/libsecp256k1.0.dylib" "$APP_SIGN" # If APP_SIGN is empty will be a noop info "Building CalinsQRReader..." d=contrib/osx/CalinsQRReader @@ -120,7 +118,7 @@ for d in ~/Library/Python/ ~/.pyenv .; do done info "Building binary" -pyinstaller --noconfirm --ascii --clean --name $VERSION contrib/osx/osx.spec || fail "Could not build binary" +APP_SIGN="$APP_SIGN" pyinstaller --noconfirm --ascii --clean --name $VERSION contrib/osx/osx.spec || fail "Could not build binary" info "Adding bitcoin URI types to Info.plist" plutil -insert 'CFBundleURLTypes' \ diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec index 7b56d6f4..887ee01d 100644 --- a/contrib/osx/osx.spec +++ b/contrib/osx/osx.spec @@ -2,13 +2,50 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs -import sys -import os +import sys, os PACKAGE='Electrum' PYPKG='electrum' MAIN_SCRIPT='run_electrum' ICONS_FILE='electrum.icns' +APP_SIGN = os.environ.get('APP_SIGN', '') + +def fail(*msg): + RED='\033[0;31m' + NC='\033[0m' # No Color + print("\r🗯 {}ERROR:{}".format(RED, NC), *msg) + sys.exit(1) + +def codesign(identity, binary): + d = os.path.dirname(binary) + saved_dir=None + if d: + # switch to directory of the binary so codesign verbose messages don't include long path + saved_dir = os.path.abspath(os.path.curdir) + os.chdir(d) + binary = os.path.basename(binary) + os.system("codesign -v -f -s '{}' '{}'".format(identity, binary))==0 or fail("Could not code sign " + binary) + if saved_dir: + os.chdir(saved_dir) + +def monkey_patch_pyinstaller_for_codesigning(identity): + # Monkey-patch PyInstaller so that we app-sign all binaries *after* they are modified by PyInstaller + # If we app-sign before that point, the signature will be invalid because PyInstaller modifies + # @loader_path in the Mach-O loader table. + try: + import PyInstaller.depend.dylib + _saved_func = PyInstaller.depend.dylib.mac_set_relative_dylib_deps + except (ImportError, NameError, AttributeError): + # Hmm. Likely wrong PyInstaller version. + fail("Could not monkey-patch PyInstaller for code signing. Please ensure that you are using PyInstaller 3.4.") + _signed = set() + def my_func(fn, distname): + _saved_func(fn, distname) + if (fn, distname) not in _signed: + codesign(identity, fn) + _signed.add((fn,distname)) # remember we signed it so we don't sign again + PyInstaller.depend.dylib.mac_set_relative_dylib_deps = my_func + for i, x in enumerate(sys.argv): if x == '--name': @@ -90,6 +127,10 @@ for x in a.binaries.copy(): a.binaries.remove(x) print('----> Removed x =', x) +# If code signing, monkey-patch in a code signing step to pyinstaller. See: https://github.com/spesmilo/electrum/issues/4994 +if APP_SIGN: + monkey_patch_pyinstaller_for_codesigning(APP_SIGN) + pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 5ba0271d..a311fb5d 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -2,7 +2,7 @@ Cython>=0.27 trezor[hidapi]>=0.11.0 safet[hidapi]>=0.1.0 keepkey -btchip-python +btchip-python>=0.1.26 ckcc-protocol>=0.7.2 websocket-client hidapi diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 5b4aecf4..97d89707 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -373,11 +373,13 @@ class AddressSynchronizer(PrintError): @profiler def load_transactions(self): # load txi, txo, tx_fees - self.txi = self.storage.get('txi', {}) + # bookkeeping data of is_mine inputs of transactions + self.txi = self.storage.get('txi', {}) # txid -> address -> (prev_outpoint, value) for txid, d in list(self.txi.items()): for addr, lst in d.items(): self.txi[txid][addr] = set([tuple(x) for x in lst]) - self.txo = self.storage.get('txo', {}) + # bookkeeping data of is_mine outputs of transactions + self.txo = self.storage.get('txo', {}) # txid -> address -> (output_index, value, is_coinbase) self.tx_fees = self.storage.get('tx_fees', {}) tx_list = self.storage.get('transactions', {}) # load transactions diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index da9cf9e3..a5050d14 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -233,16 +233,23 @@ class BaseWizard(object): title = _('Hardware Keystore') # check available plugins supported_plugins = self.plugins.get_hardware_support() - # scan devices devices = [] # type: List[Tuple[str, DeviceInfo]] devmgr = self.plugins.device_manager + debug_msg = '' + + def failed_getting_device_infos(name, e): + nonlocal debug_msg + devmgr.print_error(f'error getting device infos for {name}: {e}') + indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True)) + debug_msg += f' {name}: (error getting device infos)\n{indented_error_msg}\n' + + # scan devices try: scanned_devices = devmgr.scan_devices() except BaseException as e: - devmgr.print_error('error scanning devices: {}'.format(e)) + devmgr.print_error('error scanning devices: {}'.format(repr(e))) debug_msg = ' {}:\n {}'.format(_('Error scanning devices'), e) else: - debug_msg = '' for splugin in supported_plugins: name, plugin = splugin.name, splugin.plugin # plugin init errored? @@ -256,14 +263,17 @@ class BaseWizard(object): # see if plugin recognizes 'scanned_devices' try: # FIXME: side-effect: unpaired_device_info sets client.handler - u = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices) + device_infos = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices, + include_failing_clients=True) except BaseException as e: traceback.print_exc() - devmgr.print_error(f'error getting device infos for {name}: {e}') - indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True)) - debug_msg += f' {name}: (error getting device infos)\n{indented_error_msg}\n' + failed_getting_device_infos(name, e) continue - devices += list(map(lambda x: (name, x), u)) + device_infos_failing = list(filter(lambda di: di.exception is not None, device_infos)) + for di in device_infos_failing: + failed_getting_device_infos(name, di.exception) + device_infos_working = list(filter(lambda di: di.exception is None, device_infos)) + devices += list(map(lambda x: (name, x), device_infos_working)) if not debug_msg: debug_msg = ' {}'.format(_('No exceptions encountered.')) if not devices: diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 992dd397..a112ff48 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -476,6 +476,11 @@ class Blockchain(util.PrintError): return None return deserialize_header(h, height) + def header_at_tip(self) -> Optional[dict]: + """Return latest header.""" + height = self.height() + return self.read_header(height) + def get_hash(self, height: int) -> str: def is_height_checkpoint(): within_cp_range = height <= constants.net.max_checkpoint() diff --git a/electrum/commands.py b/electrum/commands.py index 1ac6b306..773a83ca 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -38,6 +38,7 @@ from .import util, ecc from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_encode from . import bitcoin from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS +from . import bip32 from .i18n import _ from .transaction import Transaction, multisig_script, TxOutput from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED @@ -329,7 +330,8 @@ class Commands: def broadcast(self, tx): """Broadcast a transaction to the network. """ tx = Transaction(tx) - return self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) + self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) + return tx.txid() @command('') def createmultisig(self, num, pubkeys): @@ -428,6 +430,16 @@ class Commands: """Get master private key. Return your wallet\'s master private key""" return str(self.wallet.keystore.get_master_private_key(password)) + @command('') + def convert_xkey(self, xkey, xtype): + """Convert xtype of a master key. e.g. xpub -> ypub""" + is_xprv = bip32.is_xprv(xkey) + if not bip32.is_xpub(xkey) and not is_xprv: + raise Exception('xkey should be a master public/private key') + _, depth, fingerprint, child_number, c, cK = bip32.deserialize_xkey(xkey, is_xprv) + serialize = bip32.serialize_xprv if is_xprv else bip32.serialize_xpub + return serialize(xtype, c, cK, depth, fingerprint, child_number) + @command('wp') def getseed(self, password=None): """Get seed phrase. Print the generation seed of your wallet.""" @@ -909,6 +921,7 @@ def add_network_options(parser): parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)") parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http") parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers") + parser.add_argument("--skipmerklecheck", action="store_true", dest="skipmerklecheck", default=False, help="Tolerate invalid merkle proofs from server") def add_global_options(parser): group = parser.add_argument_group('global options') diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 5abf08fd..cfe60ebd 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -16,7 +16,7 @@ from electrum.plugin import run_hook from electrum.util import format_satoshis, format_satoshis_plain from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from electrum import blockchain -from electrum.network import Network +from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed from .i18n import _ from kivy.app import App @@ -917,14 +917,16 @@ class ElectrumWindow(App): Clock.schedule_once(lambda dt: on_success(tx)) def _broadcast_thread(self, tx, on_complete): - + status = False try: self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) - except Exception as e: - ok, msg = False, repr(e) + except TxBroadcastError as e: + msg = e.get_message_for_gui() + except BestEffortRequestFailed as e: + msg = repr(e) else: - ok, msg = True, tx.txid() - Clock.schedule_once(lambda dt: on_complete(ok, msg)) + status, msg = True, tx.txid() + Clock.schedule_once(lambda dt: on_complete(status, msg)) def broadcast(self, tx, pr=None): def on_complete(ok, msg): @@ -937,11 +939,8 @@ class ElectrumWindow(App): self.wallet.invoices.save() self.update_tab('invoices') else: - display_msg = _('The server returned an error when broadcasting the transaction.') - if msg: - display_msg += '\n' + msg - display_msg = display_msg[:500] - self.show_error(display_msg) + msg = msg or '' + self.show_error(msg) if self.network and self.network.is_connected(): self.show_info(_('Sending')) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 4379c894..a5fdb78b 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -133,7 +133,7 @@ class ElectrumGui(PrintError): self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) except BaseException as e: use_dark_theme = False - self.print_error('Error setting dark theme: {}'.format(e)) + self.print_error('Error setting dark theme: {}'.format(repr(e))) # Even if we ourselves don't set the dark theme, # the OS/window manager/etc might set *a dark theme*. # Hence, try to choose colors accordingly: diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index 5c167f3e..aebfacf7 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -60,20 +60,20 @@ class ContactList(MyTreeView): def create_menu(self, position): menu = QMenu() - selected = self.selected_in_column(0) - selected_keys = [] - for idx in selected: - sel_key = self.model().itemFromIndex(idx).data(Qt.UserRole) - selected_keys.append(sel_key) idx = self.indexAt(position) + column = idx.column() or 0 + selected = self.selected_in_column(column) + selected_keys = [] + for s_idx in selected: + sel_key = self.model().itemFromIndex(s_idx).data(Qt.UserRole) + selected_keys.append(sel_key) if not selected or not idx.isValid(): menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog()) menu.addAction(_("Import file"), lambda: self.import_contacts()) menu.addAction(_("Export file"), lambda: self.export_contacts()) else: - column = idx.column() column_title = self.model().horizontalHeaderItem(column).text() - column_data = '\n'.join(self.model().itemFromIndex(idx).text() for idx in selected) + column_data = '\n'.join(self.model().itemFromIndex(s_idx).text() for s_idx in selected) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) if column in self.editable_columns: item = self.model().itemFromIndex(idx) @@ -107,4 +107,6 @@ class ContactList(MyTreeView): idx = self.model().index(row_count, 0) set_current = QPersistentModelIndex(idx) self.set_current_idx(set_current) + # FIXME refresh loses sort order; so set "default" here: + self.sortByColumn(0, Qt.AscendingOrder) run_hook('update_contacts_tab', self) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index e53a6238..4ec0e384 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -29,6 +29,7 @@ from datetime import date from typing import TYPE_CHECKING, Tuple, Dict import threading from enum import IntEnum +from decimal import Decimal from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ @@ -77,9 +78,14 @@ class HistorySortModel(QSortFilterProxyModel): item2 = self.sourceModel().data(source_right, Qt.UserRole) if item1 is None or item2 is None: raise Exception(f'UserRole not set for column {source_left.column()}') - if item1.value() is None or item2.value() is None: + v1 = item1.value() + v2 = item2.value() + if v1 is None or isinstance(v1, Decimal) and v1.is_nan(): v1 = -float("inf") + if v2 is None or isinstance(v2, Decimal) and v2.is_nan(): v2 = -float("inf") + try: + return v1 < v2 + except: return False - return item1.value() < item2.value() class HistoryModel(QAbstractItemModel, PrintError): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 5f1830d4..a0d41c9f 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -62,7 +62,7 @@ from electrum.address_synchronizer import AddTransactionException from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, sweep_preparations, InternalAddressCorruption) from electrum.version import ELECTRUM_VERSION -from electrum.network import Network +from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed from electrum.exchange_rate import FxThread from electrum.simple_config import SimpleConfig @@ -255,7 +255,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def toggle_tab(self, tab): show = not self.config.get('show_{}_tab'.format(tab.tab_name), False) self.config.set_key('show_{}_tab'.format(tab.tab_name), show) - item_text = (_("Hide") if show else _("Show")) + " " + tab.tab_description + item_text = (_("Hide {}") if show else _("Show {}")).format(tab.tab_description) tab.menu_action.setText(item_text) if show: # Find out where to place the tab @@ -1080,7 +1080,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): uri = util.create_URI(addr, amount, message) self.receive_qr.setData(uri) if self.qr_window and self.qr_window.isVisible(): - self.qr_window.set_content(addr, amount, message, uri) + self.qr_window.qrw.setData(uri) def set_feerounding_text(self, num_satoshis_added): self.feerounding_text = (_('Additional {} satoshis are going to be added.') @@ -1667,10 +1667,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if pr and pr.has_expired(): self.payment_request = None return False, _("Payment request has expired") + status = False try: self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) - except Exception as e: - status, msg = False, repr(e) + except TxBroadcastError as e: + msg = e.get_message_for_gui() + except BestEffortRequestFailed as e: + msg = repr(e) else: status, msg = True, tx.txid() if pr and status is True: @@ -1698,10 +1701,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.invoice_list.update() self.do_clear() else: - display_msg = _('The server returned an error when broadcasting the transaction.') - if msg: - display_msg += '\n' + msg - parent.show_error(display_msg) + msg = msg or '' + parent.show_error(msg) WaitingDialog(self, _('Broadcasting transaction...'), broadcast_thread, broadcast_done, self.on_error) @@ -2416,7 +2417,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): try: data = bh2u(bitcoin.base_decode(data, length=None, base=43)) except BaseException as e: - self.show_error((_('Could not decode QR code')+':\n{}').format(e)) + self.show_error((_('Could not decode QR code')+':\n{}').format(repr(e))) return tx = self.tx_from_text(data) if not tx: diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 94ae7773..3cbdab41 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -466,6 +466,9 @@ class NetworkChoiceLayout(object): self.network.run_from_another_thread(self.network.set_parameters(net_params)) def suggest_proxy(self, found_proxy): + if found_proxy is None: + self.tor_cb.hide() + return self.tor_proxy = found_proxy self.tor_cb.setText("Use Tor proxy at port " + str(found_proxy[1])) if self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5') \ @@ -505,10 +508,14 @@ class TorDetector(QThread): def run(self): # Probable ports for Tor to listen at ports = [9050, 9150] - for p in ports: - if TorDetector.is_tor_port(p): - self.found_proxy.emit(("127.0.0.1", p)) - return + while True: + for p in ports: + if TorDetector.is_tor_port(p): + self.found_proxy.emit(("127.0.0.1", p)) + break + else: + self.found_proxy.emit(None) + time.sleep(10) @staticmethod def is_tor_port(port): diff --git a/electrum/gui/qt/qrcodewidget.py b/electrum/gui/qt/qrcodewidget.py index 5482854b..5071914c 100644 --- a/electrum/gui/qt/qrcodewidget.py +++ b/electrum/gui/qt/qrcodewidget.py @@ -66,16 +66,15 @@ class QRCodeWidget(QWidget): framesize = min(r.width(), r.height()) boxsize = int( (framesize - 2*margin)/k ) size = k*boxsize - left = (r.width() - size)/2 - top = (r.height() - size)/2 - - # Make a white margin around the QR in case of dark theme use + left = (framesize - size)/2 + top = (framesize - size)/2 + # Draw white background with margin qp.setBrush(white) qp.setPen(white) - qp.drawRect(left-margin, top-margin, size+(margin*2), size+(margin*2)) + qp.drawRect(0, 0, framesize, framesize) + # Draw qr code qp.setBrush(black) qp.setPen(black) - for r in range(k): for c in range(k): if matrix[r][c]: diff --git a/electrum/gui/qt/qrwindow.py b/electrum/gui/qt/qrwindow.py index c4c3963d..3e519f3e 100644 --- a/electrum/gui/qt/qrwindow.py +++ b/electrum/gui/qt/qrwindow.py @@ -48,43 +48,9 @@ class QR_Window(QWidget): QWidget.__init__(self) self.win = win self.setWindowTitle('Electrum - '+_('Payment Request')) - self.setMinimumSize(800, 250) - self.address = '' - self.label = '' - self.amount = 0 + self.setMinimumSize(800, 800) self.setFocusPolicy(Qt.NoFocus) - main_box = QHBoxLayout() - self.qrw = QRCodeWidget() main_box.addWidget(self.qrw, 1) - - vbox = QVBoxLayout() - main_box.addLayout(vbox) - - self.address_label = QLabel("") - #self.address_label.setFont(QFont(MONOSPACE_FONT)) - vbox.addWidget(self.address_label) - - self.label_label = QLabel("") - vbox.addWidget(self.label_label) - - self.amount_label = QLabel("") - vbox.addWidget(self.amount_label) - - vbox.addStretch(1) self.setLayout(main_box) - - - def set_content(self, address, amount, message, url): - address_text = "%s" % address if address else "" - self.address_label.setText(address_text) - if amount: - amount = self.win.format_amount(amount) - amount_text = "%s %s " % (amount, self.win.base_unit()) - else: - amount_text = '' - self.amount_label.setText(amount_text) - label_text = "%s" % message if message else "" - self.label_label.setText(label_text) - self.qrw.setData(url) diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py index e3808129..6d710601 100644 --- a/electrum/gui/stdio.py +++ b/electrum/gui/stdio.py @@ -6,6 +6,7 @@ from electrum import WalletStorage, Wallet from electrum.util import format_satoshis, set_verbosity from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS from electrum.transaction import TxOutput +from electrum.network import TxBroadcastError, BestEffortRequestFailed _ = lambda x:x # i18n @@ -205,10 +206,12 @@ class ElectrumGui: print(_("Please wait...")) try: self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) - except Exception as e: - display_msg = _('The server returned an error when broadcasting the transaction.') - display_msg += '\n' + repr(e) - print(display_msg) + except TxBroadcastError as e: + msg = e.get_message_for_gui() + print(msg) + except BestEffortRequestFailed as e: + msg = repr(e) + print(msg) else: print(_('Payment sent.')) #self.do_clear() diff --git a/electrum/gui/text.py b/electrum/gui/text.py index 50d2361b..c5ddcf8e 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -12,7 +12,7 @@ from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS from electrum.transaction import TxOutput from electrum.wallet import Wallet from electrum.storage import WalletStorage -from electrum.network import NetworkParameters +from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed from electrum.interface import deserialize_server _ = lambda x:x # i18n @@ -369,10 +369,12 @@ class ElectrumGui: self.show_message(_("Please wait..."), getchar=False) try: self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) - except Exception as e: - display_msg = _('The server returned an error when broadcasting the transaction.') - display_msg += '\n' + repr(e) - self.show_message(display_msg) + except TxBroadcastError as e: + msg = e.get_message_for_gui() + self.show_message(msg) + except BestEffortRequestFailed as e: + msg = repr(e) + self.show_message(msg) else: self.show_message(_('Payment sent.')) self.do_clear() diff --git a/electrum/interface.py b/electrum/interface.py index 53ce4fde..6f38f2c2 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -207,7 +207,7 @@ class Interface(PrintError): async def _try_saving_ssl_cert_for_first_time(self, ca_ssl_context): try: ca_signed = await self.is_server_ca_signed(ca_ssl_context) - except (OSError, aiorpcx.socks.SOCKSFailure) as e: + except (OSError, aiorpcx.socks.SOCKSError) as e: raise ErrorGettingSSLCertFromServer(e) from e if ca_signed: with open(self.cert_path, 'w') as f: @@ -267,7 +267,7 @@ class Interface(PrintError): try: return await func(self, *args, **kwargs) except GracefulDisconnect as e: - self.print_error("disconnecting gracefully. {}".format(e)) + self.print_error("disconnecting gracefully. {}".format(repr(e))) finally: await self.network.connection_down(self) self.got_disconnected.set_result(1) @@ -284,7 +284,7 @@ class Interface(PrintError): return try: await self.open_session(ssl_context) - except (asyncio.CancelledError, OSError, aiorpcx.socks.SOCKSFailure) as e: + except (asyncio.CancelledError, OSError, aiorpcx.socks.SOCKSError) as e: self.print_error('disconnecting due to: {}'.format(repr(e))) return diff --git a/electrum/network.py b/electrum/network.py index 748cd46b..883a38b8 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -37,6 +37,7 @@ import traceback import dns import dns.resolver +import aiorpcx from aiorpcx import TaskGroup from aiohttp import ClientResponse @@ -53,6 +54,7 @@ from .interface import (Interface, serialize_server, deserialize_server, RequestTimedOut, NetworkTimeout) from .version import PROTOCOL_VERSION from .simple_config import SimpleConfig +from .i18n import _ NODES_RETRY_INTERVAL = 60 SERVER_RETRY_INTERVAL = 10 @@ -162,6 +164,30 @@ def deserialize_proxy(s: str) -> Optional[dict]: return proxy +class BestEffortRequestFailed(Exception): pass + + +class TxBroadcastError(Exception): + def get_message_for_gui(self): + raise NotImplementedError() + + +class TxBroadcastHashMismatch(TxBroadcastError): + def get_message_for_gui(self): + return "{}\n{}\n\n{}" \ + .format(_("The server returned an unexpected transaction ID when broadcasting the transaction."), + _("Consider trying to connect to a different server, or updating Electrum."), + str(self)) + + +class TxBroadcastServerReturnedError(TxBroadcastError): + def get_message_for_gui(self): + return "{}\n{}\n\n{}" \ + .format(_("The server returned an error when broadcasting the transaction."), + _("Consider trying to connect to a different server, or updating Electrum."), + str(self)) + + INSTANCE = None @@ -724,7 +750,7 @@ class Network(PrintError): continue # try again return success_fut.result() # otherwise; try again - raise Exception('no interface to do request on... gave up.') + raise BestEffortRequestFailed('no interface to do request on... gave up.') return make_reliable_wrapper @best_effort_reliable @@ -732,14 +758,152 @@ class Network(PrintError): return await self.interface.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height]) @best_effort_reliable - async def broadcast_transaction(self, tx, *, timeout=None): + async def broadcast_transaction(self, tx, *, timeout=None) -> None: if timeout is None: timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent) - out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout) + try: + out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout) + # note: both 'out' and exception messages are untrusted input from the server + except aiorpcx.jsonrpc.RPCError as e: + self.print_error(f"broadcast_transaction error: {repr(e)}") + raise TxBroadcastServerReturnedError(self.sanitize_tx_broadcast_response(e.message)) from e if out != tx.txid(): - # note: this is untrusted input from the server - raise Exception(out) - return out # txid + self.print_error(f"unexpected txid for broadcast_transaction: {out} != {tx.txid()}") + raise TxBroadcastHashMismatch(_("Server returned unexpected transaction ID.")) + + @staticmethod + def sanitize_tx_broadcast_response(server_msg) -> str: + # Unfortunately, bitcoind and hence the Electrum protocol doesn't return a useful error code. + # So, we use substring matching to grok the error message. + # server_msg is untrusted input so it should not be shown to the user. see #4968 + server_msg = str(server_msg) + server_msg = server_msg.replace("\n", r"\n") + # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/policy/policy.cpp + # grep "reason =" + policy_error_messages = { + r"version": _("Transaction uses non-standard version."), + r"tx-size": _("The transaction was rejected because it is too large."), + r"scriptsig-size": None, + r"scriptsig-not-pushonly": None, + r"scriptpubkey": None, + r"bare-multisig": None, + r"dust": _("Transaction could not be broadcast due to dust outputs."), + r"multi-op-return": _("The transaction was rejected because it contains multiple OP_RETURN outputs."), + } + for substring in policy_error_messages: + if substring in server_msg: + msg = policy_error_messages[substring] + return msg if msg else substring + # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/script/script_error.cpp + script_error_messages = { + r"Script evaluated without error but finished with a false/empty top stack element", + r"Script failed an OP_VERIFY operation", + r"Script failed an OP_EQUALVERIFY operation", + r"Script failed an OP_CHECKMULTISIGVERIFY operation", + r"Script failed an OP_CHECKSIGVERIFY operation", + r"Script failed an OP_NUMEQUALVERIFY operation", + r"Script is too big", + r"Push value size limit exceeded", + r"Operation limit exceeded", + r"Stack size limit exceeded", + r"Signature count negative or greater than pubkey count", + r"Pubkey count negative or limit exceeded", + r"Opcode missing or not understood", + r"Attempted to use a disabled opcode", + r"Operation not valid with the current stack size", + r"Operation not valid with the current altstack size", + r"OP_RETURN was encountered", + r"Invalid OP_IF construction", + r"Negative locktime", + r"Locktime requirement not satisfied", + r"Signature hash type missing or not understood", + r"Non-canonical DER signature", + r"Data push larger than necessary", + r"Only non-push operators allowed in signatures", + r"Non-canonical signature: S value is unnecessarily high", + r"Dummy CHECKMULTISIG argument must be zero", + r"OP_IF/NOTIF argument must be minimal", + r"Signature must be zero for failed CHECK(MULTI)SIG operation", + r"NOPx reserved for soft-fork upgrades", + r"Witness version reserved for soft-fork upgrades", + r"Public key is neither compressed or uncompressed", + r"Extra items left on stack after execution", + r"Witness program has incorrect length", + r"Witness program was passed an empty witness", + r"Witness program hash mismatch", + r"Witness requires empty scriptSig", + r"Witness requires only-redeemscript scriptSig", + r"Witness provided for non-witness script", + r"Using non-compressed keys in segwit", + r"Using OP_CODESEPARATOR in non-witness script", + r"Signature is found in scriptCode", + } + for substring in script_error_messages: + if substring in server_msg: + return substring + # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/validation.cpp + # grep "REJECT_" + # should come after script_error.cpp (due to e.g. non-mandatory-script-verify-flag) + validation_error_messages = { + r"coinbase", + r"tx-size-small", + r"non-final", + r"txn-already-in-mempool", + r"txn-mempool-conflict", + r"txn-already-known", + r"non-BIP68-final", + r"bad-txns-nonstandard-inputs", + r"bad-witness-nonstandard", + r"bad-txns-too-many-sigops", + r"mempool min fee not met", + r"min relay fee not met", + r"absurdly-high-fee", + r"too-long-mempool-chain", + r"bad-txns-spends-conflicting-tx", + r"insufficient fee", + r"too many potential replacements", + r"replacement-adds-unconfirmed", + r"mempool full", + r"non-mandatory-script-verify-flag", + r"mandatory-script-verify-flag-failed", + } + for substring in validation_error_messages: + if substring in server_msg: + return substring + # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/rpc/rawtransaction.cpp + # grep "RPC_TRANSACTION" + # grep "RPC_DESERIALIZATION_ERROR" + rawtransaction_error_messages = { + r"Missing inputs", + r"transaction already in block chain", + r"TX decode failed", + } + for substring in rawtransaction_error_messages: + if substring in server_msg: + return substring + # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/consensus/tx_verify.cpp + # grep "REJECT_" + tx_verify_error_messages = { + r"bad-txns-vin-empty", + r"bad-txns-vout-empty", + r"bad-txns-oversize", + r"bad-txns-vout-negative", + r"bad-txns-vout-toolarge", + r"bad-txns-txouttotal-toolarge", + r"bad-txns-inputs-duplicate", + r"bad-cb-length", + r"bad-txns-prevout-null", + r"bad-txns-inputs-missingorspent", + r"bad-txns-premature-spend-of-coinbase", + r"bad-txns-inputvalues-outofrange", + r"bad-txns-in-belowout", + r"bad-txns-fee-outofrange", + } + for substring in tx_verify_error_messages: + if substring in server_msg: + return substring + # otherwise: + return _("Unknown error") @best_effort_reliable async def request_chunk(self, height, tip=None, *, can_return_early=False): diff --git a/electrum/plugin.py b/electrum/plugin.py index 89b82cf5..68481f5e 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -301,8 +301,9 @@ class Device(NamedTuple): class DeviceInfo(NamedTuple): device: Device - label: str - initialized: bool + label: Optional[str] = None + initialized: Optional[bool] = None + exception: Optional[Exception] = None class HardwarePluginToScan(NamedTuple): @@ -500,7 +501,8 @@ class DeviceMgr(ThreadJob, PrintError): 'its seed (and passphrase, if any). Otherwise all FLO you ' 'receive will be unspendable.').format(plugin.device)) - def unpaired_device_infos(self, handler, plugin: 'HW_PluginBase', devices=None): + def unpaired_device_infos(self, handler, plugin: 'HW_PluginBase', devices=None, + include_failing_clients=False): '''Returns a list of DeviceInfo objects: one for each connected, unpaired device accepted by the plugin.''' if not plugin.libraries_available: @@ -515,14 +517,16 @@ class DeviceMgr(ThreadJob, PrintError): continue try: client = self.create_client(device, handler, plugin) - except UserFacingException: - raise - except BaseException as e: + except Exception as e: self.print_error(f'failed to create client for {plugin.name} at {device.path}: {repr(e)}') + if include_failing_clients: + infos.append(DeviceInfo(device=device, exception=e)) continue if not client: continue - infos.append(DeviceInfo(device, client.label(), client.is_initialized())) + infos.append(DeviceInfo(device=device, + label=client.label(), + initialized=client.is_initialized())) return infos diff --git a/electrum/plugins/email_requests/qt.py b/electrum/plugins/email_requests/qt.py index c5e2a10e..93112142 100644 --- a/electrum/plugins/email_requests/qt.py +++ b/electrum/plugins/email_requests/qt.py @@ -91,7 +91,7 @@ class Processor(threading.Thread, PrintError): self.M = imaplib.IMAP4_SSL(self.imap_server) self.M.login(self.username, self.password) except BaseException as e: - self.print_error('connecting failed: {}'.format(e)) + self.print_error('connecting failed: {}'.format(repr(e))) self.connect_wait *= 2 else: self.reset_connect_wait() @@ -100,7 +100,7 @@ class Processor(threading.Thread, PrintError): try: self.poll() except BaseException as e: - self.print_error('polling failed: {}'.format(e)) + self.print_error('polling failed: {}'.format(repr(e))) break time.sleep(self.polling_interval) time.sleep(random.randint(0, self.connect_wait)) diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py index cef051c5..47a03340 100644 --- a/electrum/plugins/keepkey/qt.py +++ b/electrum/plugins/keepkey/qt.py @@ -432,7 +432,7 @@ class SettingsDialog(WindowModalDialog): def slider_moved(): mins = timeout_slider.sliderPosition() - timeout_minutes.setText(_("%2d minutes") % mins) + timeout_minutes.setText(_("{:2d} minutes").format(mins)) def slider_released(): config.set_session_timeout(timeout_slider.sliderPosition() * 60) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 11ff152c..7d2d14f0 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -440,7 +440,7 @@ class Ledger_KeyStore(Hardware_KeyStore): self.get_client().enableAlternate2fa(False) if segwitTransaction: self.get_client().startUntrustedTransaction(True, inputIndex, - chipInputs, redeemScripts[inputIndex]) + chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) @@ -456,7 +456,7 @@ class Ledger_KeyStore(Hardware_KeyStore): while inputIndex < len(inputs): singleInput = [ chipInputs[inputIndex] ] self.get_client().startUntrustedTransaction(False, 0, - singleInput, redeemScripts[inputIndex]) + singleInput, redeemScripts[inputIndex], version=tx.version) inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ signatures.append(inputSignature) @@ -464,7 +464,7 @@ class Ledger_KeyStore(Hardware_KeyStore): else: while inputIndex < len(inputs): self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, - chipInputs, redeemScripts[inputIndex]) + chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index 1c55ae89..b11b5f5e 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -113,7 +113,7 @@ class Plugin(RevealerPlugin): self.load_noise.textChanged.connect(self.on_edit) self.load_noise.setMaximumHeight(33) self.hbox.addLayout(vbox) - vbox.addWidget(WWLabel(_("or type a existing revealer code below and click 'next':"))) + vbox.addWidget(WWLabel(_("or type an existing revealer code below and click 'next':"))) vbox.addWidget(self.load_noise) vbox.addSpacing(3) self.next_button = QPushButton(_("Next"), self.d) @@ -170,7 +170,7 @@ class Plugin(RevealerPlugin): code_id = self.versioned_seed.checksum dialog.show_message(''.join([_("{} encrypted for Revealer {}_{} saved as PNG and PDF at: ").format(self.was, version, code_id), "", self.get_path_to_revealer_file(), "", "
", - "
", "", _("Always check you backups.")]), + "
", "", _("Always check your backups.")]), rich_text=True) dialog.close() diff --git a/electrum/plugins/safe_t/qt.py b/electrum/plugins/safe_t/qt.py index 36bd4a52..f7d51f25 100644 --- a/electrum/plugins/safe_t/qt.py +++ b/electrum/plugins/safe_t/qt.py @@ -111,7 +111,7 @@ class QtPlugin(QtPluginBase): bg = QButtonGroup() for i, count in enumerate([12, 18, 24]): rb = QRadioButton(gb) - rb.setText(_("%d words") % count) + rb.setText(_("{:d} words").format(count)) bg.addButton(rb) bg.setId(rb, i) hbox1.addWidget(rb) @@ -317,7 +317,7 @@ class SettingsDialog(WindowModalDialog): def slider_moved(): mins = timeout_slider.sliderPosition() - timeout_minutes.setText(_("%2d minutes") % mins) + timeout_minutes.setText(_("{:2d} minutes").format(mins)) def slider_released(): config.set_session_timeout(timeout_slider.sliderPosition() * 60) diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index 687f4c95..ed0d7b14 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -203,6 +203,7 @@ class TrezorClientBase(PrintError): self.client, *args, input_callback=input_callback, + type=recovery_type, **kwargs) # ========= Unmodified trezorlib methods ========= diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index 5045c738..44745871 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -205,7 +205,7 @@ class QtPlugin(QtPluginBase): bg_numwords = QButtonGroup() for i, count in enumerate([12, 18, 24]): rb = QRadioButton(gb) - rb.setText(_("%d words") % count) + rb.setText(_("{:d} words").format(count)) bg_numwords.addButton(rb) bg_numwords.setId(rb, i) hbox1.addWidget(rb) @@ -407,7 +407,7 @@ class SettingsDialog(WindowModalDialog): def slider_moved(): mins = timeout_slider.sliderPosition() - timeout_minutes.setText(_("%2d minutes") % mins) + timeout_minutes.setText(_("{:2d} minutes").format(mins)) def slider_released(): config.set_session_timeout(timeout_slider.sliderPosition() * 60) diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py index a6d35c7b..ee715392 100644 --- a/electrum/tests/test_commands.py +++ b/electrum/tests/test_commands.py @@ -3,6 +3,8 @@ from decimal import Decimal from electrum.commands import Commands, eval_bool +from . import TestCaseForTestnet + class TestCommands(unittest.TestCase): @@ -39,3 +41,46 @@ class TestCommands(unittest.TestCase): self.assertTrue(eval_bool("True")) self.assertTrue(eval_bool("true")) self.assertTrue(eval_bool("1")) + + def test_convert_xkey(self): + cmds = Commands(config=None, wallet=None, network=None) + xpubs = { + ("xpub6CCWFbvCbqF92kGwm9nV7t7RvVoQUKaq5USMdyVP6jvv1NgN52KAX6NNYCeE8Ca7JQC4K5tZcnQrubQcjJ6iixfPs4pwAQJAQgTt6hBjg11", "standard"), + ("ypub6X2mZGb7kWnct3U4bWa7KyCw6TwrQwaKzaxaRNPGUkJo4UVbKgUj9A2WZQbp87E2i3Js4ZV85SmQnt2BSzWjXCLzjQXMkK7egQXXVHT4eKn", "p2wpkh-p2sh"), + ("zpub6qs2rwG2uCL6jLfBRsMjY4JSGS6JMZZpuhUoCmH9rkgg7aJpaLeHmDgeacZQ81sx7gRfp35gY77xgAdkAgvkKS2bbkDnLDw8x8bAsuKBrvP", "p2wpkh"), + } + for xkey1, xtype1 in xpubs: + for xkey2, xtype2 in xpubs: + self.assertEqual(xkey2, cmds.convert_xkey(xkey1, xtype2)) + + xprvs = { + ("xprv9yD9r6PJmTgqpGCUf8FUkkAhNTxv4rryiFWkqb5mYQPw8aMDXUzuyJ3tgv5vUqYkdK1E6Q5jKxPss4HkMBYV4q8AfG8t7rxgyS4xQX4ndAm", "standard"), + ("yprvAJ3R9m4Dv9EKfZPbVV36xqGCYS7N1UrUdN2ycyyevQmpBgASn9AUbMi2i83WUkCg2x82qsgHnckRkLuK4sxVs4omXbqJhmnBFA8bo8ssinK", "p2wpkh-p2sh"), + ("zprvAcsgTRj94pmoWraiKqpjAvMhiQFox6qyYUZCQNsYJR9hEmyg2oL3DRNAjL16UerbSbEqbMGrFH6yddWsnaNWfJVNPwXjHgbfWtCFBgDxFkX", "p2wpkh"), + } + for xkey1, xtype1 in xprvs: + for xkey2, xtype2 in xprvs: + self.assertEqual(xkey2, cmds.convert_xkey(xkey1, xtype2)) + + +class TestCommandsTestnet(TestCaseForTestnet): + + def test_convert_xkey(self): + cmds = Commands(config=None, wallet=None, network=None) + xpubs = { + ("tpubD8p5qNfjczgTGbh9qgNxsbFgyhv8GgfVkmp3L88qtRm5ibUYiDVCrn6WYfnGey5XVVw6Bc5QNQUZW5B4jFQsHjmaenvkFUgWtKtgj5AdPm9", "standard"), + ("upub59wfQ8qJTg6ZSuvwtR313Qdp8gP8TSBwTof5dPQ3QVsYp1N9t29Rr9TGF1pj8kAXUg3mKbmrTKasA2qmBJKb1bGUzB6ApDZpVC7LoHhyvBo", "p2wpkh-p2sh"), + ("vpub5UmvhoWDcMe3JD84impdFVjKJeXaQ4BSNvBJQnHvnWFRs7BP8gJzUD7QGDnK8epStKAa55NQuywR3KTKtzjbopx5rWnbQ8PJkvAzBtgaGBc", "p2wpkh"), + } + for xkey1, xtype1 in xpubs: + for xkey2, xtype2 in xpubs: + self.assertEqual(xkey2, cmds.convert_xkey(xkey1, xtype2)) + + xprvs = { + ("tprv8c83gxdVUcznP8fMx2iNUBbaQgQC7MUbBUDG3c6YU9xgt7Dn5pfcgHUeNZTAvuYmNgVHjyTzYzGWwJr7GvKCm2FkPaaJipyipbfJeB3tdPW", "standard"), + ("uprv8vxJzdJQdJYGERrUnPVzgGh5aeYe3yU66ajUpzzRrALZwD31LUqBJM8nPmQkvpCgnKc6VT4Z1ed4pbTfzcjDZFwMFvGjJjoD6Kix2pCwVe7", "p2wpkh-p2sh"), + ("vprv9FnaJHyKmz5k5j3bckHctMnakch5zbTb1hFhcPtKEAiSzJrEb8zjvQnvQyNLvircBxiuEvf7UJycht5EiK9EMVcx8Fy9techN3nbRQRFhEv", "p2wpkh"), + } + for xkey1, xtype1 in xprvs: + for xkey2, xtype2 in xprvs: + self.assertEqual(xkey2, cmds.convert_xkey(xkey1, xtype2)) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 2e5b7556..5e50d313 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -1033,7 +1033,6 @@ class TestWalletSending(TestCaseForTestnet): class NetworkMock: relay_fee = 1000 - def get_local_height(self): return 1325785 def run_from_another_thread(self, coro): loop = asyncio.get_event_loop() return loop.run_until_complete(coro) @@ -1046,7 +1045,7 @@ class TestWalletSending(TestCaseForTestnet): privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu', ] network = NetworkMock() dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2' - tx = sweep(privkeys, network, config=None, recipient=dest_addr, fee=5000) + tx = sweep(privkeys, network, config=None, recipient=dest_addr, fee=5000, locktime=1325785) tx_copy = Transaction(tx.serialize()) self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400', diff --git a/electrum/verifier.py b/electrum/verifier.py index 68071fc1..62af6a25 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -116,8 +116,11 @@ class SPV(NetworkJobOnDefaultServer): try: verify_tx_is_in_block(tx_hash, merkle_branch, pos, header, tx_height) except MerkleVerificationFailure as e: - self.print_error(str(e)) - raise GracefulDisconnect(e) + if self.network.config.get("skipmerklecheck"): + self.print_error("skipping merkle proof check %s" % tx_hash) + else: + self.print_error(str(e)) + raise GracefulDisconnect(e) # we passed all the tests self.merkle_roots[tx_hash] = header.get('merkle_root') try: self.requested_merkle.remove(tx_hash) diff --git a/electrum/wallet.py b/electrum/wallet.py index 31a6a13a..29dc3a0b 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -125,7 +125,8 @@ def sweep_preparations(privkeys, network: 'Network', imax=100): return inputs, keypairs -def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=None, imax=100): +def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=None, imax=100, + *, locktime=None): inputs, keypairs = sweep_preparations(privkeys, network, imax) total = sum(i.get('value') for i in inputs) if fee is None: @@ -138,7 +139,8 @@ def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=N raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network))) outputs = [TxOutput(TYPE_ADDRESS, recipient, total - fee)] - locktime = network.get_local_height() + if locktime is None: + locktime = get_locktime_for_new_transaction(network) tx = Transaction.from_io(inputs, outputs, locktime=locktime) tx.set_rbf(True) @@ -146,6 +148,26 @@ def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=N return tx +def get_locktime_for_new_transaction(network: 'Network') -> int: + # if no network or not up to date, just set locktime to zero + if not network: + return 0 + chain = network.blockchain() + header = chain.header_at_tip() + if not header: + return 0 + STALE_DELAY = 8 * 60 * 60 # in seconds + if header['timestamp'] + STALE_DELAY < time.time(): + return 0 + # discourage "fee sniping" + locktime = chain.height() + # sometimes pick locktime a bit further back, to help privacy + # of setups that need more time (offline/multisig/coinjoin/...) + if random.randint(0, 9) == 0: + locktime = max(0, locktime - random.randint(0, 99)) + return locktime + + class CannotBumpFee(Exception): pass @@ -694,7 +716,7 @@ class Abstract_Wallet(AddressSynchronizer): tx = Transaction.from_io(coins, outputs[:]) # Timelock tx to current height. - tx.locktime = self.get_local_height() + tx.locktime = get_locktime_for_new_transaction(self.network) # Transactions with transaction comments/floData are version 2 if flodata != "": tx.version = 2 @@ -800,7 +822,7 @@ class Abstract_Wallet(AddressSynchronizer): continue if delta > 0: raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs')) - locktime = self.get_local_height() + locktime = get_locktime_for_new_transaction(self.network) tx_new = Transaction.from_io(inputs, outputs, locktime=locktime) return tx_new @@ -820,7 +842,7 @@ class Abstract_Wallet(AddressSynchronizer): inputs = [item] out_address = self.get_unused_address() or address outputs = [TxOutput(TYPE_ADDRESS, out_address, value - fee)] - locktime = self.get_local_height() + locktime = get_locktime_for_new_transaction(self.network) return Transaction.from_io(inputs, outputs, locktime=locktime) def add_input_sig_info(self, txin, address): diff --git a/setup.py b/setup.py index 3e71caab..41c3d9f9 100755 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ class CustomInstallCommand(install): if not os.path.exists(path): subprocess.call(["pyrcc5", "icons.qrc", "-o", path]) except Exception as e: - print('Warning: building icons file failed with {}'.format(e)) + print('Warning: building icons file failed with {}'.format(repr(e))) setup(