From 192ec8596d085f9d3dfb9b6a9231ada68ef0fbb8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 4 Jan 2019 10:15:26 +0100 Subject: [PATCH 01/23] trezor: fix matrix recovery closes #4983 --- electrum/plugins/trezor/clientbase.py | 1 + 1 file changed, 1 insertion(+) 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 ========= From fd5ad9ac70a7d081a7ad63956703c93b9dcde1cb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 4 Jan 2019 10:58:59 +0100 Subject: [PATCH 02/23] qt network dialog: detect Tor proxy dynamically keep thread running to detect changes --- electrum/gui/qt/network_dialog.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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): From 4d0030363beb44980e4bc75497c222ca76f9ff6e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 4 Jan 2019 11:00:48 +0100 Subject: [PATCH 03/23] interface: catch more SOCKS exceptions --- electrum/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index 53ce4fde..92ac5911 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: @@ -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 From 8fd84f77c7cfc69fa88ffb97fe42379bd22b8ead Mon Sep 17 00:00:00 2001 From: Tom Kneiphof Date: Mon, 7 Jan 2019 10:49:11 +0100 Subject: [PATCH 04/23] Fix ledger transaction version (#4991) --- electrum/plugins/ledger/ledger.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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)) From 2f789468ea6d04e5024d6f1c41c88ab0ba38d06e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 7 Jan 2019 11:00:50 +0100 Subject: [PATCH 05/23] requirements: lower bound for btchip-python --- contrib/requirements/requirements-hw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 424430723b6f7d8f0bf691eb634bbe5c3dff1235 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 8 Jan 2019 17:22:53 +0100 Subject: [PATCH 06/23] [Revealer] Fix spelling mistake --- electrum/plugins/revealer/qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index 1c55ae89..126fc641 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -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() From c60583293a632b782e3840cd5af0a0083b64bfbd Mon Sep 17 00:00:00 2001 From: Romano Date: Fri, 11 Jan 2019 21:00:26 +0100 Subject: [PATCH 07/23] [Docker] fix gnupg2 and dirmngr --- contrib/build-wine/docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 \ && \ From 019566b383cf3852897010ddce3e110f4f45e571 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Sat, 12 Jan 2019 21:58:21 +0100 Subject: [PATCH 08/23] Change string formatting to improve translation For example, "Hide Console" would be "Konsole anzeigen" in German. Currently, translators can only show "Anzeigen Konsole" which doesn't make much sense. --- electrum/gui/qt/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 7a64e383..a37c2be8 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -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 From 0caf8e30cd70848baf9c83bfc52fb3063df91386 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Sat, 12 Jan 2019 22:06:47 +0100 Subject: [PATCH 09/23] Revealer: Fix typo --- electrum/plugins/revealer/qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index 126fc641..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) From 5ec330680ee1e2f722adefc4c7caf512da5085b6 Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Wed, 16 Jan 2019 10:58:44 +0200 Subject: [PATCH 10/23] [MacOS] Fixed code signing on macos to codesign all embdedded binaries This was pulled from Electron Cash #1110 --- contrib/osx/make_osx | 4 +--- contrib/osx/osx.spec | 45 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) 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, From d5c8a0e0d0597bc510b79b76c73c26689f3d692d Mon Sep 17 00:00:00 2001 From: chris-belcher Date: Wed, 16 Jan 2019 17:48:10 +0000 Subject: [PATCH 11/23] Add flag --skipmerklecheck (#4957) The --skipmerklecheck optional flag makes Electrum tolerate invalid merkle proofs from the server. This is useful for building Electrum servers that need a minimum amount of storage, though of course users should only enable it if they completely trust the connected server. --- electrum/commands.py | 1 + electrum/verifier.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 1ac6b306..a5bae9cf 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -909,6 +909,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/verifier.py b/electrum/verifier.py index 247d124d..a4c220cc 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) From dc19cf1fa1083b4236755621a36c3642bf7a3bfc Mon Sep 17 00:00:00 2001 From: ghost43 Date: Wed, 16 Jan 2019 18:51:59 +0100 Subject: [PATCH 12/23] wallet: randomise locktime of transactions a bit. also check if stale. (#4967) --- electrum/blockchain.py | 5 ++++ electrum/tests/test_wallet_vertical.py | 3 +-- electrum/wallet.py | 32 ++++++++++++++++++++++---- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 92a58723..aec290ab 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -426,6 +426,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/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/wallet.py b/electrum/wallet.py index 6ac10929..260be490 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 @@ -692,7 +714,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) run_hook('make_unsigned_transaction', self, tx) return tx @@ -794,7 +816,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 @@ -814,7 +836,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): From c7f3adb67ea6afd50fec60221ba711ddea110281 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 16 Jan 2019 19:11:04 +0100 Subject: [PATCH 13/23] trezor: fix minor string formatting re translations closes #4996 --- electrum/plugins/keepkey/qt.py | 2 +- electrum/plugins/safe_t/qt.py | 4 ++-- electrum/plugins/trezor/qt.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) 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/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/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) From 44a2ceab3c23ec6ba1fa4dfd182086cefd1e5cf6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 17 Jan 2019 17:09:22 +0100 Subject: [PATCH 14/23] qt history list: fix minor sorting issue closes #4989 --- electrum/gui/qt/history_list.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index e785ddd0..acf610e7 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): From d77e4d8f5d5afebe303f412cd3b28829f4156fb7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 17 Jan 2019 17:16:19 +0100 Subject: [PATCH 15/23] exception formatting: use repr(e) instead of str(e) in messages repr(e) is more useful --- electrum/base_wizard.py | 2 +- electrum/gui/qt/__init__.py | 2 +- electrum/gui/qt/main_window.py | 2 +- electrum/interface.py | 2 +- electrum/plugins/email_requests/qt.py | 4 ++-- setup.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index da9cf9e3..52b5abc8 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -239,7 +239,7 @@ class BaseWizard(object): 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 = '' 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/main_window.py b/electrum/gui/qt/main_window.py index a37c2be8..7ee309c5 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2409,7 +2409,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/interface.py b/electrum/interface.py index 92ac5911..6f38f2c2 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -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) 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/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( From 7ffd928e8052a59c33081cd463d0325304bd6bec Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 17 Jan 2019 17:19:08 +0100 Subject: [PATCH 16/23] wallet: add comment --- electrum/address_synchronizer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index ab1f64d1..846eb41f 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 From 5403ae7687e8940027ee2823530ec6d356afd471 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 18 Jan 2019 19:59:12 +0100 Subject: [PATCH 17/23] network: sanitize tx broadcast response --- electrum/commands.py | 3 +- electrum/gui/kivy/main_window.py | 21 ++-- electrum/gui/qt/main_window.py | 15 +-- electrum/gui/stdio.py | 11 +- electrum/gui/text.py | 12 ++- electrum/network.py | 176 +++++++++++++++++++++++++++++-- 6 files changed, 204 insertions(+), 34 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index a5bae9cf..1f98d78c 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -329,7 +329,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): diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index c865635f..33e2a392 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/main_window.py b/electrum/gui/qt/main_window.py index 7ee309c5..0282c221 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 @@ -1660,10 +1660,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: @@ -1691,10 +1694,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) 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 d263efbe..bc4af2d8 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/network.py b/electrum/network.py index 748cd46b..8a8dd410 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": None, + 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 more than 1 OP_RETURN input."), + } + 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): From e39e2ed8f13b543c1c56a90b9b8465bc6d9571a3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 19 Jan 2019 17:50:22 +0100 Subject: [PATCH 18/23] fix typo follow-up #5011 closes #5014 --- electrum/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index 8a8dd410..883a38b8 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -781,14 +781,14 @@ class Network(PrintError): # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/policy/policy.cpp # grep "reason =" policy_error_messages = { - r"version": None, + 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 more than 1 OP_RETURN input."), + 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: From 9cff42328d5b6c92c87a2b41bb1f4e463c39aab7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 19 Jan 2019 23:11:21 +0100 Subject: [PATCH 19/23] qt contact list: fix copying address, sort order closes #5015 --- electrum/gui/qt/contact_list.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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) From 5fc715cdee8c54ecb260ac256a5baccecc358e4b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 20 Jan 2019 15:49:42 +0100 Subject: [PATCH 20/23] commands: add convert_xkey for converting between {x,y,z}|{pub,prv} --- electrum/commands.py | 11 ++++++++ electrum/tests/test_commands.py | 45 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/electrum/commands.py b/electrum/commands.py index 1f98d78c..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 @@ -429,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.""" 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)) From f2ad116b0b03eb57e4c898dda2994a310b6768fd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 21 Jan 2019 18:44:36 +0100 Subject: [PATCH 21/23] wizard: better hww debug messages when unpaired_device_infos fails [DeviceMgr] scanning devices... [DeviceMgr] failed to create client for ledger at b'0002:0007:00': OSError('open failed',) [DeviceMgr] error getting device infos for ledger: open failed ^ GUI did not contain any info about failure --- electrum/base_wizard.py | 24 +++++++++++++++++------- electrum/plugin.py | 18 +++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 52b5abc8..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(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/plugin.py b/electrum/plugin.py index f9072601..a7077e9b 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 bitcoins 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 From f994cd4a5d8e30da8f7261fab628eb0f4cd01988 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 16 Jul 2018 13:05:59 +0200 Subject: [PATCH 22/23] draw qrcode with fixed framesize --- electrum/gui/qt/qrcodewidget.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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]: From 920d4c2b27190f5ad8f808cbe288855dbd171fa5 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 23 Jan 2019 17:17:13 +0100 Subject: [PATCH 23/23] simplify qr window --- electrum/gui/qt/main_window.py | 2 +- electrum/gui/qt/qrwindow.py | 36 +--------------------------------- 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 0282c221..1a6d94d2 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -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.') 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)