From d8408048186c025520cd63d6ffe705f8d0316f44 Mon Sep 17 00:00:00 2001 From: Ilya Shalyapin Date: Wed, 19 Sep 2018 13:07:19 +0500 Subject: [PATCH 001/301] use system language by default --- electrum/gui/qt/__init__.py | 4 ++-- electrum/i18n.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 80adeaa9..4fdb1b52 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -38,7 +38,7 @@ from PyQt5.QtWidgets import * from PyQt5.QtCore import * import PyQt5.QtCore as QtCore -from electrum.i18n import _, set_language +from electrum.i18n import _, set_language, get_default_language from electrum.plugin import run_hook from electrum.storage import WalletStorage from electrum.base_wizard import GoBack @@ -89,7 +89,7 @@ class QNetworkUpdatedSignalObject(QObject): class ElectrumGui(PrintError): def __init__(self, config, daemon, plugins): - set_language(config.get('language')) + set_language(config.get('language', get_default_language())) # Uncomment this call to verify objects are being properly # GC-ed when windows are closed #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer, diff --git a/electrum/i18n.py b/electrum/i18n.py index 9c6fad99..bb4a2da5 100644 --- a/electrum/i18n.py +++ b/electrum/i18n.py @@ -26,6 +26,8 @@ import os import gettext +from PyQt5.QtCore import QLocale + LOCALE_DIR = os.path.join(os.path.dirname(__file__), 'locale') language = gettext.translation('electrum', LOCALE_DIR, fallback=True) @@ -41,6 +43,11 @@ def set_language(x): language = gettext.translation('electrum', LOCALE_DIR, fallback=True, languages=[x]) +def get_default_language(): + system_locale = QLocale.system().name() + return languages.get(system_locale, 'en_UK') + + languages = { '': _('Default'), 'ar_SA': _('Arabic'), From 9586157479c2f9b6a985b94289cd4d49afaac180 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Sep 2018 22:12:02 +0200 Subject: [PATCH 002/301] qt: refresh gui with "F5" --- electrum/gui/qt/main_window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index bad62890..95d97cd0 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -176,6 +176,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): QShortcut(QKeySequence("Ctrl+W"), self, self.close) QShortcut(QKeySequence("Ctrl+Q"), self, self.close) QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet) + QShortcut(QKeySequence("F5"), self, self.update_wallet) QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: wrtabs.setCurrentIndex((wrtabs.currentIndex() - 1)%wrtabs.count())) QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: wrtabs.setCurrentIndex((wrtabs.currentIndex() + 1)%wrtabs.count())) From 4c8103af3bd18bb7d2fee6fcc74706e4dcb06a90 Mon Sep 17 00:00:00 2001 From: Ilya Shalyapin Date: Sun, 23 Sep 2018 14:11:50 +0500 Subject: [PATCH 003/301] move get_default_language to gui.qt.util --- electrum/gui/qt/__init__.py | 2 +- electrum/gui/qt/util.py | 7 ++++++- electrum/i18n.py | 7 ------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 4fdb1b52..7557f0ec 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -38,7 +38,7 @@ from PyQt5.QtWidgets import * from PyQt5.QtCore import * import PyQt5.QtCore as QtCore -from electrum.i18n import _, set_language, get_default_language +from electrum.i18n import _, set_language from electrum.plugin import run_hook from electrum.storage import WalletStorage from electrum.base_wizard import GoBack diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 50ed0a5c..804a9907 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -10,7 +10,7 @@ from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import * -from electrum.i18n import _ +from electrum.i18n import _, languages from electrum.util import FileImportFailed, FileExportFailed from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED @@ -812,6 +812,11 @@ class IconCache: return self.__cache[file_name] +def get_default_language(): + name = QLocale.system().name() + return name if name in languages else 'en_UK' + + if __name__ == "__main__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done")) diff --git a/electrum/i18n.py b/electrum/i18n.py index bb4a2da5..9c6fad99 100644 --- a/electrum/i18n.py +++ b/electrum/i18n.py @@ -26,8 +26,6 @@ import os import gettext -from PyQt5.QtCore import QLocale - LOCALE_DIR = os.path.join(os.path.dirname(__file__), 'locale') language = gettext.translation('electrum', LOCALE_DIR, fallback=True) @@ -43,11 +41,6 @@ def set_language(x): language = gettext.translation('electrum', LOCALE_DIR, fallback=True, languages=[x]) -def get_default_language(): - system_locale = QLocale.system().name() - return languages.get(system_locale, 'en_UK') - - languages = { '': _('Default'), 'ar_SA': _('Arabic'), From 32d53052956bbbc60683cec0308afe127eed304e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 28 Sep 2018 16:43:25 +0200 Subject: [PATCH 004/301] fix daemon.load_wallet --- electrum/daemon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/daemon.py b/electrum/daemon.py index 2ce648bf..d31394da 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -163,6 +163,7 @@ class Daemon(DaemonThread): return True def run_daemon(self, config_options): + asyncio.set_event_loop(self.network.asyncio_loop) config = SimpleConfig(config_options) sub = config.get('subcommand') assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet'] From 5e4a4ae16bdd9e9c7a7ce056a352531a8870b74b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 28 Sep 2018 17:58:46 +0200 Subject: [PATCH 005/301] minor clean-up (prints/types/imports) --- electrum/daemon.py | 5 +++-- electrum/exchange_rate.py | 16 +++++++++------- electrum/interface.py | 10 +++++----- electrum/plugin.py | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index d31394da..cc52b845 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -29,6 +29,7 @@ import time import traceback import sys import threading +from typing import Dict import jsonrpclib @@ -37,7 +38,7 @@ from .version import ELECTRUM_VERSION from .network import Network from .util import json_decode, DaemonThread from .util import print_error, to_string -from .wallet import Wallet +from .wallet import Wallet, Abstract_Wallet from .storage import WalletStorage from .commands import known_commands, Commands from .simple_config import SimpleConfig @@ -131,7 +132,7 @@ class Daemon(DaemonThread): if self.network: self.network.start([self.fx.run]) self.gui = None - self.wallets = {} + self.wallets = {} # type: Dict[str, Abstract_Wallet] # Setup JSONRPC server self.init_server(config, fd) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 014fb23b..826a8775 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -1,6 +1,4 @@ import asyncio -import aiohttp -from aiohttp_socks import SocksConnector, SocksVer from datetime import datetime import inspect import sys @@ -12,6 +10,7 @@ import decimal from decimal import Decimal import concurrent.futures import traceback +from typing import Sequence from .bitcoin import COIN from .i18n import _ @@ -27,6 +26,7 @@ CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, 'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0, 'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0} + class ExchangeBase(PrintError): def __init__(self, on_quotes, on_history): @@ -65,7 +65,7 @@ class ExchangeBase(PrintError): self.quotes = await self.get_rates(ccy) self.print_error("received fx quotes") except BaseException as e: - self.print_error("failed fx quotes:", e) + self.print_error("failed fx quotes:", repr(e)) self.quotes = {} self.on_quotes() @@ -452,12 +452,14 @@ class FxThread(ThreadJob): def set_proxy(self, trigger_name, *args): self._trigger.set() - def get_currencies(self, h): - d = get_exchanges_by_ccy(h) + @staticmethod + def get_currencies(history: bool) -> Sequence[str]: + d = get_exchanges_by_ccy(history) return sorted(d.keys()) - def get_exchanges_by_ccy(self, ccy, h): - d = get_exchanges_by_ccy(h) + @staticmethod + def get_exchanges_by_ccy(ccy: str, history: bool) -> Sequence[str]: + d = get_exchanges_by_ccy(history) return d.get(ccy, []) def ccy_amount_str(self, amount, commas): diff --git a/electrum/interface.py b/electrum/interface.py index ac5541db..ef371138 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -63,7 +63,7 @@ class NotificationSession(ClientSession): for queue in self.subscriptions[key]: await queue.put(request.args) else: - assert False, request.method + raise Exception('unexpected request: {}'.format(repr(request))) async def send_request(self, *args, timeout=-1, **kwargs): # note: the timeout starts after the request touches the wire! @@ -255,12 +255,12 @@ class Interface(PrintError): try: ssl_context = await self._get_ssl_context() except (ErrorParsingSSLCert, ErrorGettingSSLCertFromServer) as e: - self.print_error('disconnecting due to: {} {}'.format(e, type(e))) + self.print_error('disconnecting due to: {}'.format(repr(e))) return try: - await self.open_session(ssl_context, exit_early=False) + await self.open_session(ssl_context) except (asyncio.CancelledError, OSError, aiorpcx.socks.SOCKSFailure) as e: - self.print_error('disconnecting due to: {} {}'.format(e, type(e))) + self.print_error('disconnecting due to: {}'.format(repr(e))) return def mark_ready(self): @@ -338,7 +338,7 @@ class Interface(PrintError): return conn, 0 return conn, res['count'] - async def open_session(self, sslc, exit_early): + async def open_session(self, sslc, exit_early=False): self.session = NotificationSession(self.host, self.port, ssl=sslc, proxy=self.proxy) async with self.session as session: try: diff --git a/electrum/plugin.py b/electrum/plugin.py index a13f7a72..a69082a0 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -134,7 +134,7 @@ class Plugins(DaemonThread): try: __import__(dep) except ImportError as e: - self.print_error('Plugin', name, 'unavailable:', type(e).__name__, ':', str(e)) + self.print_error('Plugin', name, 'unavailable:', repr(e)) return False requires = d.get('requires_wallet_type', []) return not requires or w.wallet_type in requires From 53fd6a2df590e2acd1117a05e87e66d65843005d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 28 Sep 2018 19:17:45 +0200 Subject: [PATCH 006/301] transaction: always sort i/o deterministically this was previously the caller's responsibility; now it's done implicitly when creating a txn --- electrum/transaction.py | 12 ++++++++---- electrum/wallet.py | 5 ----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index b32752f2..370f97c0 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -747,6 +747,7 @@ class Transaction: self._inputs = inputs self._outputs = outputs self.locktime = locktime + self.BIP69_sort() return self @classmethod @@ -981,10 +982,11 @@ class Transaction: for txin in self.inputs(): txin['sequence'] = nSequence - def BIP_LI01_sort(self): - # See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki - self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n'])) - self._outputs.sort(key = lambda o: (o[2], self.pay_script(o[0], o[1]))) + def BIP69_sort(self, inputs=True, outputs=True): + if inputs: + self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n'])) + if outputs: + self._outputs.sort(key = lambda o: (o[2], self.pay_script(o[0], o[1]))) def serialize_output(self, output): output_type, addr, amount = output @@ -1070,10 +1072,12 @@ class Transaction: def add_inputs(self, inputs): self._inputs.extend(inputs) self.raw = None + self.BIP69_sort(outputs=False) def add_outputs(self, outputs): self._outputs.extend(outputs) self.raw = None + self.BIP69_sort(inputs=False) def input_value(self): return sum(x['value'] for x in self.inputs()) diff --git a/electrum/wallet.py b/electrum/wallet.py index f918012a..011fd6ac 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -143,7 +143,6 @@ def sweep(privkeys, network, config, recipient, fee=None, imax=100): locktime = network.get_local_height() tx = Transaction.from_io(inputs, outputs, locktime=locktime) - tx.BIP_LI01_sort() tx.set_rbf(True) tx.sign(keypairs) return tx @@ -608,8 +607,6 @@ class Abstract_Wallet(AddressSynchronizer): outputs[i_max] = outputs[i_max]._replace(value=amount) tx = Transaction.from_io(inputs, outputs[:]) - # Sort the inputs and outputs deterministically - tx.BIP_LI01_sort() # Timelock tx to current height. tx.locktime = self.get_local_height() run_hook('make_unsigned_transaction', self, tx) @@ -714,7 +711,6 @@ class Abstract_Wallet(AddressSynchronizer): raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs')) locktime = self.get_local_height() tx_new = Transaction.from_io(inputs, outputs, locktime=locktime) - tx_new.BIP_LI01_sort() return tx_new def cpfp(self, tx, fee): @@ -733,7 +729,6 @@ class Abstract_Wallet(AddressSynchronizer): inputs = [item] outputs = [TxOutput(TYPE_ADDRESS, address, value - fee)] locktime = self.get_local_height() - # note: no need to call tx.BIP_LI01_sort() here - single input/output return Transaction.from_io(inputs, outputs, locktime=locktime) def add_input_sig_info(self, txin, address): From ce5cc135cd432552006c713d1d83d9fb51e1e7e2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 29 Sep 2018 19:47:55 +0200 Subject: [PATCH 007/301] transaction: make get_address_from_output_script safer closes #4743 --- electrum/ecc.py | 8 ++++ electrum/tests/test_transaction.py | 16 +++++++ electrum/transaction.py | 72 +++++++++++++++++++++--------- 3 files changed, 74 insertions(+), 22 deletions(-) diff --git a/electrum/ecc.py b/electrum/ecc.py index 1470618f..9c10e3b0 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -296,6 +296,14 @@ class ECPubkey(object): def is_at_infinity(self): return self == point_at_infinity() + @classmethod + def is_pubkey_bytes(cls, b: bytes): + try: + ECPubkey(b) + return True + except: + return False + def msg_magic(message: bytes) -> bytes: from .bitcoin import var_int diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py index c1212945..c7841c24 100644 --- a/electrum/tests/test_transaction.py +++ b/electrum/tests/test_transaction.py @@ -175,6 +175,8 @@ class TestTransaction(SequentialTestCase): # the inverse of this test is in test_bitcoin: test_address_to_script addr_from_script = lambda script: transaction.get_address_from_output_script(bfh(script)) ADDR = transaction.TYPE_ADDRESS + PUBKEY = transaction.TYPE_PUBKEY + SCRIPT = transaction.TYPE_SCRIPT # bech32 native segwit # test vectors from BIP-0173 @@ -182,14 +184,28 @@ class TestTransaction(SequentialTestCase): self.assertEqual((ADDR, 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6')) self.assertEqual((ADDR, 'bc1sw50qa3jx3s'), addr_from_script('6002751e')) self.assertEqual((ADDR, 'bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), addr_from_script('5210751e76e8199196d454941c45d1b3a323')) + # almost but not quite + self.assertEqual((SCRIPT, '0013751e76e8199196d454941c45d1b3a323f1433b'), addr_from_script('0013751e76e8199196d454941c45d1b3a323f1433b')) # base58 p2pkh self.assertEqual((ADDR, '14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac')) self.assertEqual((ADDR, '1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv'), addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac')) + # almost but not quite + self.assertEqual((SCRIPT, '76a9130000000000000000000000000000000000000088ac'), addr_from_script('76a9130000000000000000000000000000000000000088ac')) # base58 p2sh self.assertEqual((ADDR, '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487')) self.assertEqual((ADDR, '3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji'), addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387')) + # almost but not quite + self.assertEqual((SCRIPT, 'a912f47c8954e421031ad04ecd8e7752c947920687'), addr_from_script('a912f47c8954e421031ad04ecd8e7752c947920687')) + + # p2pk + self.assertEqual((PUBKEY, '0289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8b'), addr_from_script('210289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac')) + self.assertEqual((PUBKEY, '045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120'), addr_from_script('41045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120ac')) + # almost but not quite + self.assertEqual((SCRIPT, '200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac'), addr_from_script('200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac')) + self.assertEqual((SCRIPT, '210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'), addr_from_script('210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac')) + ##### diff --git a/electrum/transaction.py b/electrum/transaction.py index 370f97c0..bc0fe58f 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -27,7 +27,8 @@ # Note: The deserialization code originally comes from ABE. -from typing import Sequence, Union, NamedTuple, Tuple, Optional, Iterable +from typing import (Sequence, Union, NamedTuple, Tuple, Optional, Iterable, + Callable) from .util import print_error, profiler @@ -288,15 +289,39 @@ def script_GetOpName(opcode): return (opcodes.whatis(opcode)).replace("OP_", "") +class OPPushDataGeneric: + def __init__(self, pushlen: Callable=None): + if pushlen is not None: + self.check_data_len = pushlen + + @classmethod + def check_data_len(cls, datalen: int) -> bool: + # Opcodes below OP_PUSHDATA4 all just push data onto stack, and are equivalent. + return opcodes.OP_PUSHDATA4 >= datalen >= 0 + + @classmethod + def is_instance(cls, item): + # accept objects that are instances of this class + # or other classes that are subclasses + return isinstance(item, cls) \ + or (isinstance(item, type) and issubclass(item, cls)) + + +OPPushDataPubkey = OPPushDataGeneric(lambda x: x in (33, 65)) +# note that this does not include x_pubkeys ! + + def match_decoded(decoded, to_match): if decoded is None: return False if len(decoded) != len(to_match): return False for i in range(len(decoded)): - if to_match[i] == opcodes.OP_PUSHDATA4 and decoded[i][0] <= opcodes.OP_PUSHDATA4 and decoded[i][0]>0: - continue # Opcodes below OP_PUSHDATA4 all just push data onto stack, and are equivalent. - if to_match[i] != decoded[i][0]: + to_match_item = to_match[i] + decoded_item = decoded[i] + if OPPushDataGeneric.is_instance(to_match_item) and to_match_item.check_data_len(decoded_item[0]): + continue + if to_match_item != decoded_item[0]: return False return True @@ -319,7 +344,7 @@ def parse_scriptSig(d, _bytes): bh2u(_bytes)) return - match = [ opcodes.OP_PUSHDATA4 ] + match = [OPPushDataGeneric] if match_decoded(decoded, match): item = decoded[0][1] if item[0] == 0: @@ -350,7 +375,7 @@ def parse_scriptSig(d, _bytes): # p2pkh TxIn transactions push a signature # (71-73 bytes) and then their public key # (33 or 65 bytes) onto the stack: - match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ] + match = [OPPushDataGeneric, OPPushDataGeneric] if match_decoded(decoded, match): sig = bh2u(decoded[0][1]) x_pubkey = bh2u(decoded[1][1]) @@ -370,7 +395,7 @@ def parse_scriptSig(d, _bytes): return # p2sh transaction, m of n - match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1) + match = [opcodes.OP_0] + [OPPushDataGeneric] * (len(decoded) - 1) if match_decoded(decoded, match): x_sig = [bh2u(x[1]) for x in decoded[1:-1]] redeem_script_unsanitized = decoded[-1][1] # for partial multisig txn, this has x_pubkeys @@ -393,7 +418,7 @@ def parse_scriptSig(d, _bytes): return # custom partial format for imported addresses - match = [ opcodes.OP_INVALIDOPCODE, opcodes.OP_0, opcodes.OP_PUSHDATA4 ] + match = [opcodes.OP_INVALIDOPCODE, opcodes.OP_0, OPPushDataGeneric] if match_decoded(decoded, match): x_pubkey = bh2u(decoded[2][1]) pubkey, address = xpubkey_to_address(x_pubkey) @@ -421,7 +446,7 @@ def parse_redeemScript_multisig(redeem_script: bytes): raise NotRecognizedRedeemScript() op_m = opcodes.OP_1 + m - 1 op_n = opcodes.OP_1 + n - 1 - match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] + match_multisig = [op_m] + [OPPushDataGeneric] * n + [op_n, opcodes.OP_CHECKMULTISIG] if not match_decoded(dec2, match_multisig): raise NotRecognizedRedeemScript() x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] @@ -433,33 +458,36 @@ def parse_redeemScript_multisig(redeem_script: bytes): return m, n, x_pubkeys, pubkeys, redeem_script_sanitized -def get_address_from_output_script(_bytes, *, net=None): +def get_address_from_output_script(_bytes: bytes, *, net=None) -> Tuple[int, str]: try: decoded = [x for x in script_GetOp(_bytes)] except MalformedBitcoinScript: decoded = None - # The Genesis Block, self-payments, and pay-by-IP-address payments look like: - # 65 BYTES:... CHECKSIG - match = [ opcodes.OP_PUSHDATA4, opcodes.OP_CHECKSIG ] - if match_decoded(decoded, match): + # p2pk + match = [OPPushDataPubkey, opcodes.OP_CHECKSIG] + if match_decoded(decoded, match) and ecc.ECPubkey.is_pubkey_bytes(decoded[0][1]): return TYPE_PUBKEY, bh2u(decoded[0][1]) - # Pay-by-Bitcoin-address TxOuts look like: - # DUP HASH160 20 BYTES:... EQUALVERIFY CHECKSIG - match = [ opcodes.OP_DUP, opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG ] + # p2pkh + match = [opcodes.OP_DUP, opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG] if match_decoded(decoded, match): return TYPE_ADDRESS, hash160_to_p2pkh(decoded[2][1], net=net) # p2sh - match = [ opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUAL ] + match = [opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUAL] if match_decoded(decoded, match): return TYPE_ADDRESS, hash160_to_p2sh(decoded[1][1], net=net) - # segwit address - possible_witness_versions = [opcodes.OP_0] + list(range(opcodes.OP_1, opcodes.OP_16 + 1)) - for witver, opcode in enumerate(possible_witness_versions): - match = [ opcode, opcodes.OP_PUSHDATA4 ] + # segwit address (version 0) + match = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))] + if match_decoded(decoded, match): + return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1], witver=0, net=net) + + # segwit address (version 1-16) + future_witness_versions = list(range(opcodes.OP_1, opcodes.OP_16 + 1)) + for witver, opcode in enumerate(future_witness_versions, start=1): + match = [opcode, OPPushDataGeneric(lambda x: 2 <= x <= 40)] if match_decoded(decoded, match): return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1], witver=witver, net=net) From 70c32590a9bdfccc1ff6896d0fc558fb163c203a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 30 Sep 2018 00:25:36 +0200 Subject: [PATCH 008/301] hw plugins: fix only_hook_if_libraries_available follow-up f9a5f2e1835e8326d1d030a6e79bd148455ee6be --- electrum/plugins/coldcard/qt.py | 4 ++-- electrum/plugins/digitalbitbox/qt.py | 2 +- electrum/plugins/hw_wallet/plugin.py | 2 ++ electrum/plugins/keepkey/qt.py | 2 +- electrum/plugins/ledger/qt.py | 2 +- electrum/plugins/safe_t/qt.py | 2 +- electrum/plugins/trezor/qt.py | 2 +- 7 files changed, 9 insertions(+), 7 deletions(-) diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py index 90df1053..b7fd0e33 100644 --- a/electrum/plugins/coldcard/qt.py +++ b/electrum/plugins/coldcard/qt.py @@ -17,8 +17,8 @@ class Plugin(ColdcardPlugin, QtPluginBase): def create_handler(self, window): return Coldcard_Handler(window) - @hook @only_hook_if_libraries_available + @hook def receive_menu(self, menu, addrs, wallet): if type(wallet) is not Standard_Wallet: return @@ -28,8 +28,8 @@ class Plugin(ColdcardPlugin, QtPluginBase): keystore.thread.add(partial(self.show_address, wallet, addrs[0])) menu.addAction(_("Show on Coldcard"), show_address) - @hook @only_hook_if_libraries_available + @hook def transaction_dialog(self, dia): # see gui/qt/transaction_dialog.py diff --git a/electrum/plugins/digitalbitbox/qt.py b/electrum/plugins/digitalbitbox/qt.py index 451bff0c..292ebbbe 100644 --- a/electrum/plugins/digitalbitbox/qt.py +++ b/electrum/plugins/digitalbitbox/qt.py @@ -16,8 +16,8 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase): def create_handler(self, window): return DigitalBitbox_Handler(window) - @hook @only_hook_if_libraries_available + @hook def receive_menu(self, menu, addrs, wallet): if type(wallet) is not Standard_Wallet: return diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index ff77ba1c..67f86ad2 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -138,6 +138,8 @@ def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes: def only_hook_if_libraries_available(func): + # note: this decorator must wrap @hook, not the other way around, + # as 'hook' uses the name of the function it wraps def wrapper(self, *args, **kwargs): if not self.libraries_available: return None return func(self, *args, **kwargs) diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py index f879b2b9..ab335d41 100644 --- a/electrum/plugins/keepkey/qt.py +++ b/electrum/plugins/keepkey/qt.py @@ -195,8 +195,8 @@ class QtPlugin(QtPluginBase): def create_handler(self, window): return QtHandler(window, self.pin_matrix_widget_class(), self.device) - @hook @only_hook_if_libraries_available + @hook def receive_menu(self, menu, addrs, wallet): if type(wallet) is not Standard_Wallet: return diff --git a/electrum/plugins/ledger/qt.py b/electrum/plugins/ledger/qt.py index cfd33991..6be64988 100644 --- a/electrum/plugins/ledger/qt.py +++ b/electrum/plugins/ledger/qt.py @@ -17,8 +17,8 @@ class Plugin(LedgerPlugin, QtPluginBase): def create_handler(self, window): return Ledger_Handler(window) - @hook @only_hook_if_libraries_available + @hook def receive_menu(self, menu, addrs, wallet): if type(wallet) is not Standard_Wallet: return diff --git a/electrum/plugins/safe_t/qt.py b/electrum/plugins/safe_t/qt.py index 408df9df..ef9ce6d3 100644 --- a/electrum/plugins/safe_t/qt.py +++ b/electrum/plugins/safe_t/qt.py @@ -71,8 +71,8 @@ class QtPlugin(QtPluginBase): def create_handler(self, window): return QtHandler(window, self.pin_matrix_widget_class(), self.device) - @hook @only_hook_if_libraries_available + @hook def receive_menu(self, menu, addrs, wallet): if len(addrs) != 1: return diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index 95f68130..77b0ca23 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -166,8 +166,8 @@ class QtPlugin(QtPluginBase): def create_handler(self, window): return QtHandler(window, self.pin_matrix_widget_class(), self.device) - @hook @only_hook_if_libraries_available + @hook def receive_menu(self, menu, addrs, wallet): if len(addrs) != 1: return From 8aebb8249a0163cb2b1953b6aeaab9ff0c569f61 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 30 Sep 2018 01:29:27 +0200 Subject: [PATCH 009/301] keepkey: full segwit support ported from trezor plugin needs new fw to work (5.8??) fixes #3462 --- electrum/plugins/keepkey/keepkey.py | 110 ++++++++++++++++++---------- electrum/plugins/keepkey/qt.py | 13 ++-- electrum/plugins/trezor/trezor.py | 4 +- 3 files changed, 80 insertions(+), 47 deletions(-) diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 097aea33..75eba04a 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -3,9 +3,8 @@ import traceback import sys from electrum.util import bfh, bh2u, UserCancelled -from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, - TYPE_ADDRESS, TYPE_SCRIPT, - is_segwit_address) +from electrum.bitcoin import (xpub_from_pubkey, deserialize_xpub, + TYPE_ADDRESS, TYPE_SCRIPT) from electrum import constants from electrum.i18n import _ from electrum.plugin import BasePlugin @@ -29,9 +28,6 @@ class KeepKey_KeyStore(Hardware_KeyStore): def get_derivation(self): return self.derivation - def is_segwit(self): - return self.derivation.startswith("m/49'/") - def get_client(self, force_pair=True): return self.plugin.get_client(self, force_pair) @@ -79,7 +75,7 @@ class KeepKeyPlugin(HW_PluginBase): libraries_URL = 'https://github.com/keepkey/python-keepkey' minimum_firmware = (1, 0, 0) keystore_class = KeepKey_KeyStore - SUPPORTED_XTYPES = ('standard', ) + SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') MAX_LABEL_LEN = 32 @@ -232,6 +228,17 @@ class KeepKeyPlugin(HW_PluginBase): client.load_device_by_xprv(item, pin, passphrase_protection, label, language) + def _make_node_path(self, xpub, address_n): + _, depth, fingerprint, child_num, chain_code, key = deserialize_xpub(xpub) + node = self.types.HDNodeType( + depth=depth, + fingerprint=int.from_bytes(fingerprint, 'big'), + child_num=int.from_bytes(child_num, 'big'), + chain_code=chain_code, + public_key=key, + ) + return self.types.HDNodePathType(node=node, address_n=address_n) + def setup_device(self, device_info, wizard, purpose): devmgr = self.device_manager() device_id = device_info.device.id_ @@ -256,12 +263,34 @@ class KeepKeyPlugin(HW_PluginBase): client.used() return xpub + def get_keepkey_input_script_type(self, electrum_txin_type: str): + if electrum_txin_type in ('p2wpkh', 'p2wsh'): + return self.types.SPENDWITNESS + if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'): + return self.types.SPENDP2SHWITNESS + if electrum_txin_type in ('p2pkh', ): + return self.types.SPENDADDRESS + if electrum_txin_type in ('p2sh', ): + return self.types.SPENDMULTISIG + raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) + + def get_keepkey_output_script_type(self, electrum_txin_type: str): + if electrum_txin_type in ('p2wpkh', 'p2wsh'): + return self.types.PAYTOWITNESS + if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'): + return self.types.PAYTOP2SHWITNESS + if electrum_txin_type in ('p2pkh', ): + return self.types.PAYTOADDRESS + if electrum_txin_type in ('p2sh', ): + return self.types.PAYTOMULTISIG + raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) + def sign_transaction(self, keystore, tx, prev_tx, xpub_path): self.prev_tx = prev_tx self.xpub_path = xpub_path client = self.get_client(keystore) - inputs = self.tx_inputs(tx, True, keystore.is_segwit()) - outputs = self.tx_outputs(keystore.get_derivation(), tx, keystore.is_segwit()) + inputs = self.tx_inputs(tx, True) + outputs = self.tx_outputs(keystore.get_derivation(), tx) signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[0] signatures = [(bh2u(x) + '01') for x in signatures] tx.update_signatures(signatures) @@ -271,22 +300,34 @@ class KeepKeyPlugin(HW_PluginBase): keystore = wallet.get_keystore() if not self.show_address_helper(wallet, address, keystore): return - if type(wallet) is not Standard_Wallet: - keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) - return - client = self.get_client(wallet.keystore) + client = self.get_client(keystore) if not client.atleast_version(1, 3): - wallet.keystore.handler.show_error(_("Your device firmware is too old")) + keystore.handler.show_error(_("Your device firmware is too old")) return change, index = wallet.get_address_index(address) - derivation = wallet.keystore.derivation + derivation = keystore.derivation address_path = "%s/%d/%d"%(derivation, change, index) address_n = client.expand_path(address_path) - segwit = wallet.keystore.is_segwit() - script_type = self.types.SPENDP2SHWITNESS if segwit else self.types.SPENDADDRESS - client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) + xpubs = wallet.get_master_public_keys() + if len(xpubs) == 1: + script_type = self.get_keepkey_input_script_type(wallet.txin_type) + client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) + else: + def f(xpub): + return self._make_node_path(xpub, [change, index]) + pubkeys = wallet.get_public_keys(address) + # sort xpubs using the order of pubkeys + sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) + pubkeys = list(map(f, sorted_xpubs)) + multisig = self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=[b''] * wallet.n, + m=wallet.m, + ) + script_type = self.get_keepkey_input_script_type(wallet.txin_type) + client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type) - def tx_inputs(self, tx, for_sig=False, segwit=False): + def tx_inputs(self, tx, for_sig=False): inputs = [] for txin in tx.inputs(): txinputtype = self.types.TxInputType() @@ -301,7 +342,7 @@ class KeepKeyPlugin(HW_PluginBase): xpub, s = parse_xpubkey(x_pubkey) xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) txinputtype.address_n.extend(xpub_n + s) - txinputtype.script_type = self.types.SPENDP2SHWITNESS if segwit else self.types.SPENDADDRESS + txinputtype.script_type = self.get_keepkey_input_script_type(txin['type']) else: def f(x_pubkey): if is_xpubkey(x_pubkey): @@ -309,15 +350,14 @@ class KeepKeyPlugin(HW_PluginBase): else: xpub = xpub_from_pubkey(0, bfh(x_pubkey)) s = [] - node = self.ckd_public.deserialize(xpub) - return self.types.HDNodePathType(node=node, address_n=s) - pubkeys = map(f, x_pubkeys) + return self._make_node_path(xpub, s) + pubkeys = list(map(f, x_pubkeys)) multisig = self.types.MultisigRedeemScriptType( pubkeys=pubkeys, signatures=map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures')), m=txin.get('num_sig'), ) - script_type = self.types.SPENDP2SHWITNESS if segwit else self.types.SPENDMULTISIG + script_type = self.get_keepkey_input_script_type(txin['type']) txinputtype = self.types.TxInputType( script_type=script_type, multisig=multisig @@ -349,11 +389,11 @@ class KeepKeyPlugin(HW_PluginBase): return inputs - def tx_outputs(self, derivation, tx, segwit=False): + def tx_outputs(self, derivation, tx): def create_output_by_derivation(): + script_type = self.get_keepkey_output_script_type(info.script_type) if len(xpubs) == 1: - script_type = self.types.PAYTOP2SHWITNESS if segwit else self.types.PAYTOADDRESS address_n = self.client_class.expand_path(derivation + "/%d/%d" % index) txoutputtype = self.types.TxOutputType( amount=amount, @@ -361,10 +401,8 @@ class KeepKeyPlugin(HW_PluginBase): address_n=address_n, ) else: - script_type = self.types.PAYTOP2SHWITNESS if segwit else self.types.PAYTOMULTISIG address_n = self.client_class.expand_path("/%d/%d" % index) - nodes = map(self.ckd_public.deserialize, xpubs) - pubkeys = [self.types.HDNodePathType(node=node, address_n=address_n) for node in nodes] + pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs] multisig = self.types.MultisigRedeemScriptType( pubkeys=pubkeys, signatures=[b''] * len(pubkeys), @@ -383,16 +421,7 @@ class KeepKeyPlugin(HW_PluginBase): txoutputtype.script_type = self.types.PAYTOOPRETURN txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o) elif _type == TYPE_ADDRESS: - if is_segwit_address(address): - txoutputtype.script_type = self.types.PAYTOWITNESS - else: - addrtype, hash_160 = b58_address_to_hash160(address) - if addrtype == constants.net.ADDRTYPE_P2PKH: - txoutputtype.script_type = self.types.PAYTOADDRESS - elif addrtype == constants.net.ADDRTYPE_P2SH: - txoutputtype.script_type = self.types.PAYTOSCRIPTHASH - else: - raise Exception('addrtype: ' + str(addrtype)) + txoutputtype.script_type = self.types.PAYTOADDRESS txoutputtype.address = address return txoutputtype @@ -424,6 +453,9 @@ class KeepKeyPlugin(HW_PluginBase): def electrum_tx_to_txtype(self, tx): t = self.types.TransactionType() + if tx is None: + # probably for segwit input and we don't need this prev txn + return t d = deserialize(tx.raw) t.version = d['version'] t.lock_time = d['lockTime'] diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py index ab335d41..9c4a7480 100644 --- a/electrum/plugins/keepkey/qt.py +++ b/electrum/plugins/keepkey/qt.py @@ -198,13 +198,14 @@ class QtPlugin(QtPluginBase): @only_hook_if_libraries_available @hook def receive_menu(self, menu, addrs, wallet): - if type(wallet) is not Standard_Wallet: + if len(addrs) != 1: return - keystore = wallet.get_keystore() - if type(keystore) == self.keystore_class and len(addrs) == 1: - def show_address(): - keystore.thread.add(partial(self.show_address, wallet, addrs[0])) - menu.addAction(_("Show on {}").format(self.device), show_address) + for keystore in wallet.get_keystores(): + if type(keystore) == self.keystore_class: + def show_address(): + keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore)) + device_name = "{} ({})".format(self.device, keystore.label) + menu.addAction(_("Show on {}").format(device_name), show_address) def show_settings_dialog(self, window, keystore): device_id = self.choose_device(window, keystore) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 0c479437..dce06307 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -3,8 +3,8 @@ import traceback import sys from electrum.util import bfh, bh2u, versiontuple, UserCancelled -from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, deserialize_xpub, - TYPE_ADDRESS, TYPE_SCRIPT, is_address) +from electrum.bitcoin import (xpub_from_pubkey, deserialize_xpub, + TYPE_ADDRESS, TYPE_SCRIPT) from electrum import constants from electrum.i18n import _ from electrum.plugin import BasePlugin, Device From ab1ec574292d09855707d0feb031eb4149cc43b3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 30 Sep 2018 02:10:17 +0200 Subject: [PATCH 010/301] trezor and clones: rm dead code see Electron-Cash/Electron-Cash#872 see Electron-Cash/Electron-Cash#874 --- electrum/plugins/keepkey/keepkey.py | 6 +----- electrum/plugins/safe_t/safe_t.py | 6 +----- electrum/plugins/trezor/trezor.py | 6 +----- electrum/transaction.py | 1 - 4 files changed, 3 insertions(+), 16 deletions(-) diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 75eba04a..8827c5ed 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -345,11 +345,7 @@ class KeepKeyPlugin(HW_PluginBase): txinputtype.script_type = self.get_keepkey_input_script_type(txin['type']) else: def f(x_pubkey): - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - else: - xpub = xpub_from_pubkey(0, bfh(x_pubkey)) - s = [] + xpub, s = parse_xpubkey(x_pubkey) return self._make_node_path(xpub, s) pubkeys = list(map(f, x_pubkeys)) multisig = self.types.MultisigRedeemScriptType( diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index 320d2ad4..d5e3a047 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -354,11 +354,7 @@ class SafeTPlugin(HW_PluginBase): txinputtype.script_type = self.get_safet_input_script_type(txin['type']) else: def f(x_pubkey): - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - else: - xpub = xpub_from_pubkey(0, bfh(x_pubkey)) - s = [] + xpub, s = parse_xpubkey(x_pubkey) return self._make_node_path(xpub, s) pubkeys = list(map(f, x_pubkeys)) multisig = self.types.MultisigRedeemScriptType( diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index dce06307..b3e61768 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -365,11 +365,7 @@ class TrezorPlugin(HW_PluginBase): txinputtype.script_type = self.get_trezor_input_script_type(txin['type']) else: def f(x_pubkey): - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - else: - xpub = xpub_from_pubkey(0, bfh(x_pubkey)) - s = [] + xpub, s = parse_xpubkey(x_pubkey) return self._make_node_path(xpub, s) pubkeys = list(map(f, x_pubkeys)) multisig = self.types.MultisigRedeemScriptType( diff --git a/electrum/transaction.py b/electrum/transaction.py index bc0fe58f..547e96e4 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -742,7 +742,6 @@ class Transaction: j = pubkeys.index(pubkey_hex) print_error("adding sig", i, j, pubkey_hex, sig) self.add_signature_to_txin(i, j, sig) - #self._inputs[i]['x_pubkeys'][j] = pubkey break # redo raw self.raw = self.serialize() From 4d43d12abf5413bf24b18025510c1d14e4599078 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 1 Oct 2018 04:58:26 +0200 Subject: [PATCH 011/301] transaction: don't convert p2pk to p2pkh address when displaying also closes #4742 --- electrum/address_synchronizer.py | 10 ++++----- electrum/gui/kivy/uix/dialogs/tx_dialog.py | 2 +- electrum/gui/qt/transaction_dialog.py | 3 ++- electrum/plugins/greenaddress_instant/qt.py | 7 +++--- electrum/tests/test_transaction.py | 4 ++-- electrum/transaction.py | 24 ++++++++++----------- electrum/wallet.py | 3 ++- 7 files changed, 26 insertions(+), 27 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 7e425bff..e7e262dc 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -681,7 +681,7 @@ class AddressSynchronizer(PrintError): delta += v return delta - def get_wallet_delta(self, tx): + def get_wallet_delta(self, tx: Transaction): """ effect of tx on wallet """ is_relevant = False # "related to wallet?" is_mine = False @@ -708,10 +708,10 @@ class AddressSynchronizer(PrintError): is_partial = True if not is_mine: is_partial = False - for addr, value in tx.get_outputs(): - v_out += value - if self.is_mine(addr): - v_out_mine += value + for o in tx.outputs(): + v_out += o.value + if self.is_mine(o.address): + v_out_mine += o.value is_relevant = True if is_pruned: # some inputs are mine: diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py index a99acb94..8aacaa1e 100644 --- a/electrum/gui/kivy/uix/dialogs/tx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/tx_dialog.py @@ -130,7 +130,7 @@ class TxDialog(Factory.Popup): self.amount_str = format_amount(-amount) self.fee_str = format_amount(fee) if fee is not None else _('unknown') self.can_sign = self.wallet.can_sign(self.tx) - self.ids.output_list.update(self.tx.outputs()) + self.ids.output_list.update(self.tx.get_outputs_for_UI()) def do_rbf(self): from .bump_fee_dialog import BumpFeeDialog diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index d2affb6c..495bad31 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -319,7 +319,8 @@ class TxDialog(QDialog, MessageBoxMixin): o_text.setFont(QFont(MONOSPACE_FONT)) o_text.setReadOnly(True) cursor = o_text.textCursor() - for addr, v in self.tx.get_outputs(): + for o in self.tx.get_outputs_for_UI(): + addr, v = o.address, o.value cursor.insertText(addr, text_format(addr)) if v is not None: cursor.insertText('\t', ext) diff --git a/electrum/plugins/greenaddress_instant/qt.py b/electrum/plugins/greenaddress_instant/qt.py index 81e6686e..85977b86 100644 --- a/electrum/plugins/greenaddress_instant/qt.py +++ b/electrum/plugins/greenaddress_instant/qt.py @@ -34,7 +34,6 @@ from electrum.plugin import BasePlugin, hook from electrum.i18n import _ - class Plugin(BasePlugin): button_label = _("Verify GA instant") @@ -49,9 +48,9 @@ class Plugin(BasePlugin): def get_my_addr(self, d): """Returns the address for given tx which can be used to request instant confirmation verification from GreenAddress""" - for addr, _ in d.tx.get_outputs(): - if d.wallet.is_mine(addr): - return addr + for o in d.tx.outputs(): + if d.wallet.is_mine(o.address): + return o.address return None @hook diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py index c7841c24..609e4950 100644 --- a/electrum/tests/test_transaction.py +++ b/electrum/tests/test_transaction.py @@ -1,4 +1,5 @@ from electrum import transaction +from electrum.transaction import TxOutputForUI from electrum.bitcoin import TYPE_ADDRESS from electrum.keystore import xpubkey_to_address from electrum.util import bh2u, bfh @@ -86,8 +87,7 @@ class TestTransaction(SequentialTestCase): self.assertEqual(tx.deserialize(), None) self.assertEqual(tx.as_dict(), {'hex': unsigned_blob, 'complete': False, 'final': True}) - self.assertEqual(tx.get_outputs(), [('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', 1000000)]) - self.assertEqual(tx.get_output_addresses(), ['14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs']) + self.assertEqual(tx.get_outputs_for_UI(), [TxOutputForUI('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', 1000000)]) self.assertTrue(tx.has_address('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs')) self.assertTrue(tx.has_address('1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD')) diff --git a/electrum/transaction.py b/electrum/transaction.py index 547e96e4..ff6b7620 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -68,6 +68,9 @@ TxOutput = NamedTuple("TxOutput", [('type', int), ('address', str), ('value', Un # ^ value is str when the output is set to max: '!' +TxOutputForUI = NamedTuple("TxOutputForUI", [('address', str), ('value', int)]) + + TxOutputHwInfo = NamedTuple("TxOutputHwInfo", [('address_index', Tuple), ('sorted_xpubs', Iterable[str]), ('num_sig', Optional[int]), @@ -671,7 +674,7 @@ class Transaction: else: raise Exception("cannot initialize transaction", raw) self._inputs = None - self._outputs = None + self._outputs = None # type: List[TxOutput] self.locktime = 0 self.version = 1 # by default we assume this is a partial txn; @@ -689,7 +692,7 @@ class Transaction: self.deserialize() return self._inputs - def outputs(self): + def outputs(self) -> List[TxOutput]: if self._outputs is None: self.deserialize() return self._outputs @@ -1221,26 +1224,21 @@ class Transaction: sig = bh2u(sig) + '01' return sig - def get_outputs(self): - """convert pubkeys to addresses""" + def get_outputs_for_UI(self) -> Sequence[TxOutputForUI]: outputs = [] for o in self.outputs(): if o.type == TYPE_ADDRESS: addr = o.address elif o.type == TYPE_PUBKEY: - # TODO do we really want this conversion? it's not really that address after all - addr = bitcoin.public_key_to_p2pkh(bfh(o.address)) + addr = 'PUBKEY ' + o.address else: addr = 'SCRIPT ' + o.address - outputs.append((addr, o.value)) # consider using yield (addr, v) + outputs.append(TxOutputForUI(addr, o.value)) # consider using yield return outputs - def get_output_addresses(self): - return [addr for addr, val in self.get_outputs()] - - - def has_address(self, addr): - return (addr in self.get_output_addresses()) or (addr in (tx.get("address") for tx in self.inputs())) + def has_address(self, addr: str) -> bool: + return (addr in (o.address for o in self.outputs())) \ + or (addr in (txin.get("address") for txin in self.inputs())) def as_dict(self): if self.raw is None: diff --git a/electrum/wallet.py b/electrum/wallet.py index 011fd6ac..232f594c 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -412,7 +412,8 @@ class Abstract_Wallet(AddressSynchronizer): if show_addresses: tx = self.transactions.get(tx_hash) item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs())) - item['outputs'] = list(map(lambda x:{'address':x[0], 'value':Satoshis(x[1])}, tx.get_outputs())) + item['outputs'] = list(map(lambda x:{'address':x.address, 'value':Satoshis(x.value)}, + tx.get_outputs_for_UI())) # value may be None if wallet is not fully synchronized if value is None: continue From 626828e98071db132595b99460858e4e8c539497 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 1 Oct 2018 05:16:03 +0200 Subject: [PATCH 012/301] fix sweeping --- electrum/gui/qt/main_window.py | 2 +- electrum/tests/test_wallet_vertical.py | 6 +++++- electrum/wallet.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index f89b9e7f..2510663f 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2583,7 +2583,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.spend_max() self.payto_e.setFrozen(True) self.amount_e.setFrozen(True) - except BaseException as e: + except Exception as e: # FIXME too broad... self.show_message(str(e)) return self.warn_if_watching_only() diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 5162a78f..c6275886 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -2,6 +2,7 @@ from unittest import mock import shutil import tempfile from typing import Sequence +import asyncio from electrum import storage, bitcoin, keystore from electrum import Transaction @@ -997,7 +998,10 @@ class TestWalletSending(TestCaseForTestnet): class NetworkMock: relay_fee = 1000 def get_local_height(self): return 1325785 - def listunspent_for_scripthash(self, scripthash): + def run_from_another_thread(self, coro): + loop = asyncio.get_event_loop() + return loop.run_until_complete(coro) + async def listunspent_for_scripthash(self, scripthash): if scripthash == '460e4fb540b657d775d84ff4955c9b13bd954c2adc26a6b998331343f85b6a45': return [{'tx_hash': 'ac24de8b58e826f60bd7b9ba31670bdfc3e8aedb2f28d0e91599d741569e3429', 'tx_pos': 1, 'height': 1325785, 'value': 1000000}] else: diff --git a/electrum/wallet.py b/electrum/wallet.py index 232f594c..8df288bb 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -87,7 +87,7 @@ def append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax): scripthash = bitcoin.script_to_scripthash(script) address = '(pubkey)' - u = network.listunspent_for_scripthash(scripthash) + u = network.run_from_another_thread(network.listunspent_for_scripthash(scripthash)) for item in u: if len(inputs) >= imax: break From 3f4e632cc44fc132d484d8e9e8da4bbe48f6db96 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Mon, 1 Oct 2018 13:20:05 +0200 Subject: [PATCH 013/301] Travis: Fix crowdin upload --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4690db75..5f437297 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ cache: script: - tox after_success: - - if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi + - if [ "$TRAVIS_BRANCH" = "master" ]; then pip install requests && contrib/make_locale; fi - coveralls jobs: include: From 4653a1007cb8451c28461a326730900b285f38ad Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 1 Oct 2018 15:49:26 +0200 Subject: [PATCH 014/301] daemon: more convenient constructor for scripts --- electrum/daemon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index cc52b845..672295d3 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -121,9 +121,12 @@ def get_rpc_credentials(config): class Daemon(DaemonThread): - def __init__(self, config, fd): + def __init__(self, config, fd=None): DaemonThread.__init__(self) self.config = config + if fd is None: + fd, server = get_fd_or_server(config) + if fd is None: raise Exception('failed to lock daemon; already running?') if config.get('offline'): self.network = None else: From 7dd4032cce0100bc7e23c7b7fec84a7980fee274 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 1 Oct 2018 17:56:51 +0200 Subject: [PATCH 015/301] daemon: call self.start in __init__, and allow not to listen on jsonrpc --- electrum/daemon.py | 9 ++++++--- run_electrum | 2 -- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index 672295d3..0c0df6d7 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -121,10 +121,10 @@ def get_rpc_credentials(config): class Daemon(DaemonThread): - def __init__(self, config, fd=None): + def __init__(self, config, fd=None, *, listen_jsonrpc=True): DaemonThread.__init__(self) self.config = config - if fd is None: + if fd is None and listen_jsonrpc: fd, server = get_fd_or_server(config) if fd is None: raise Exception('failed to lock daemon; already running?') if config.get('offline'): @@ -137,7 +137,10 @@ class Daemon(DaemonThread): self.gui = None self.wallets = {} # type: Dict[str, Abstract_Wallet] # Setup JSONRPC server - self.init_server(config, fd) + self.server = None + if listen_jsonrpc: + self.init_server(config, fd) + self.start() def init_server(self, config, fd): host = config.get('rpchost', '127.0.0.1') diff --git a/run_electrum b/run_electrum index a254fabb..148c54ee 100755 --- a/run_electrum +++ b/run_electrum @@ -416,7 +416,6 @@ if __name__ == '__main__': if fd is not None: plugins = init_plugins(config, config.get('gui', 'qt')) d = daemon.Daemon(config, fd) - d.start() d.init_gui(config, plugins) sys.exit(0) else: @@ -437,7 +436,6 @@ if __name__ == '__main__': sys.exit(0) init_plugins(config, 'cmdline') d = daemon.Daemon(config, fd) - d.start() if config.get('websocket_server'): from electrum import websockets websockets.WebSocketServer(config, d.network).start() From da9d1e6001e3d1133901f5c2b4f16ba1b7a177e0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 1 Oct 2018 18:16:37 +0200 Subject: [PATCH 016/301] network: ensure there is a main interface scenario with previous code: auto_connect enabled, there is only one server in regtest environment. client started before server; client would not switch to server after it is started. --- electrum/network.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index 65d0774f..5eb7db4c 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -813,6 +813,22 @@ class Network(PrintError): def join(self): self._thread.join(1) + async def _ensure_there_is_a_main_interface(self): + if self.is_connected(): + return + now = time.time() + # if auto_connect is set, try a different server + if self.auto_connect and not self.is_connecting(): + await self._switch_to_random_interface() + # if auto_connect is not set, or still no main interface, retry current + if not self.is_connected() and not self.is_connecting(): + if self.default_server in self.disconnected_servers: + if now - self.server_retry_time > SERVER_RETRY_INTERVAL: + self.disconnected_servers.remove(self.default_server) + self.server_retry_time = now + else: + await self.switch_to_interface(self.default_server) + async def _maintain_sessions(self): while True: # launch already queued up new interfaces @@ -830,18 +846,8 @@ class Network(PrintError): self.nodes_retry_time = now # main interface - if not self.is_connected(): - if self.auto_connect: - if not self.is_connecting(): - await self._switch_to_random_interface() - else: - if self.default_server in self.disconnected_servers: - if now - self.server_retry_time > SERVER_RETRY_INTERVAL: - self.disconnected_servers.remove(self.default_server) - self.server_retry_time = now - else: - await self.switch_to_interface(self.default_server) - else: + await self._ensure_there_is_a_main_interface() + if self.is_connected(): if self.config.is_fee_estimates_update_required(): await self.interface.group.spawn(self._request_fee_estimates, self.interface) From a61953673a3503d780ee8a1135f21b9edbab8d33 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 2 Oct 2018 15:44:09 +0200 Subject: [PATCH 017/301] fees: add 1-2 s/b static options --- electrum/simple_config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 4bfd56f5..f637c529 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -22,7 +22,8 @@ FEERATE_MAX_DYNAMIC = 1500000 FEERATE_WARNING_HIGH_FEE = 600000 FEERATE_FALLBACK_STATIC_FEE = 150000 FEERATE_DEFAULT_RELAY = 1000 -FEERATE_STATIC_VALUES = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000] +FEERATE_STATIC_VALUES = [1000, 2000, 5000, 10000, 20000, 30000, + 50000, 70000, 100000, 150000, 200000, 300000] config = None @@ -443,7 +444,7 @@ class SimpleConfig(PrintError): else: fee_rate = self.fee_per_kb(dyn=False) pos = self.static_fee_index(fee_rate) - maxp = 9 + maxp = len(FEERATE_STATIC_VALUES) - 1 return maxp, pos, fee_rate def static_fee(self, i): From 788b5b04febf45da891b1a85990385d06c6f14bc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 2 Oct 2018 15:52:24 +0200 Subject: [PATCH 018/301] ledger: always use finalizeInput in sign_transaction related #4749 --- electrum/plugins/ledger/ledger.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index cdac591e..c5abdcef 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -318,7 +318,6 @@ class Ledger_KeyStore(Hardware_KeyStore): chipInputs = [] redeemScripts = [] signatures = [] - preparedTrustedInputs = [] changePath = "" output = None p2shTransaction = False @@ -377,8 +376,8 @@ class Ledger_KeyStore(Hardware_KeyStore): self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen txOutput = var_int(len(tx.outputs())) - for txout in tx.outputs(): - output_type, addr, amount = txout + for o in tx.outputs(): + output_type, addr, amount = o.type, o.address, o.value txOutput += int_to_hex(amount, 8) script = tx.pay_script(output_type, addr) txOutput += var_int(len(script)//2) @@ -442,14 +441,10 @@ class Ledger_KeyStore(Hardware_KeyStore): if segwitTransaction: self.get_client().startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex]) - if changePath: - # 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)) - else: - outputData = self.get_client().finalizeInputFull(txOutput) + # 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)) outputData['outputData'] = txOutput - transactionOutput = outputData['outputData'] if outputData['confirmationNeeded']: outputData['address'] = output self.handler.finished() @@ -469,16 +464,11 @@ class Ledger_KeyStore(Hardware_KeyStore): else: while inputIndex < len(inputs): self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, - chipInputs, redeemScripts[inputIndex]) - if changePath: - # 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)) - else: - outputData = self.get_client().finalizeInputFull(txOutput) + chipInputs, redeemScripts[inputIndex]) + # 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)) outputData['outputData'] = txOutput - if firstTransaction: - transactionOutput = outputData['outputData'] if outputData['confirmationNeeded']: outputData['address'] = output self.handler.finished() From 02f108d92786eec741a6311ed0b802391f97ffa2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Oct 2018 17:13:46 +0200 Subject: [PATCH 019/301] restructure synchronizer fix CLI notify cmd. fix merchant websockets. --- electrum/address_synchronizer.py | 49 +++------- electrum/commands.py | 20 ++--- electrum/interface.py | 12 +-- electrum/network.py | 51 +++++++++++ electrum/synchronizer.py | 148 ++++++++++++++++++++++--------- electrum/util.py | 11 ++- electrum/verifier.py | 25 ++++-- electrum/websockets.py | 109 ++++++++++------------- run_electrum | 2 +- 9 files changed, 249 insertions(+), 178 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index e7e262dc..cd58feed 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -56,11 +56,9 @@ class AddressSynchronizer(PrintError): def __init__(self, storage): self.storage = storage self.network = None - # verifier (SPV) and synchronizer are started in start_threads - self.synchronizer = None - self.verifier = None - self.sync_restart_lock = asyncio.Lock() - self.group = None + # verifier (SPV) and synchronizer are started in start_network + self.synchronizer = None # type: Synchronizer + self.verifier = None # type: SPV # locks: if you need to take multiple ones, acquire them in the order they are defined here! self.lock = threading.RLock() self.transaction_lock = threading.RLock() @@ -143,45 +141,20 @@ class AddressSynchronizer(PrintError): # add it in case it was previously unconfirmed self.add_unverified_tx(tx_hash, tx_height) - @aiosafe - async def on_default_server_changed(self, event): - async with self.sync_restart_lock: - self.stop_threads(write_to_disk=False) - await self._start_threads() - def start_network(self, network): self.network = network if self.network is not None: - self.network.register_callback(self.on_default_server_changed, ['default_server_changed']) - asyncio.run_coroutine_threadsafe(self._start_threads(), network.asyncio_loop) - - async def _start_threads(self): - interface = self.network.interface - if interface is None: - return # we should get called again soon - - self.verifier = SPV(self.network, self) - self.synchronizer = synchronizer = Synchronizer(self) - assert self.group is None, 'group already exists' - self.group = SilentTaskGroup() - - async def job(): - async with self.group as group: - await group.spawn(self.verifier.main(group)) - await group.spawn(self.synchronizer.send_subscriptions(group)) - await group.spawn(self.synchronizer.handle_status(group)) - await group.spawn(self.synchronizer.main()) - # we are being cancelled now - interface.session.unsubscribe(synchronizer.status_queue) - await interface.group.spawn(job) + self.synchronizer = Synchronizer(self) + self.verifier = SPV(self.network, self) def stop_threads(self, write_to_disk=True): if self.network: - self.synchronizer = None - self.verifier = None - if self.group: - asyncio.run_coroutine_threadsafe(self.group.cancel_remaining(), self.network.asyncio_loop) - self.group = None + if self.synchronizer: + asyncio.run_coroutine_threadsafe(self.synchronizer.stop(), self.network.asyncio_loop) + self.synchronizer = None + if self.verifier: + asyncio.run_coroutine_threadsafe(self.verifier.stop(), self.network.asyncio_loop) + self.verifier = None self.storage.put('stored_height', self.get_local_height()) if write_to_disk: self.save_transactions() diff --git a/electrum/commands.py b/electrum/commands.py index 03f7d33a..9ce251f7 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -40,7 +40,7 @@ from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS from .i18n import _ from .transaction import Transaction, multisig_script, TxOutput from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED -from .plugin import run_hook +from .synchronizer import Notifier known_commands = {} @@ -635,21 +635,11 @@ class Commands: self.wallet.remove_payment_request(k, self.config) @command('n') - def notify(self, address, URL): + def notify(self, address: str, URL: str): """Watch an address. Every time the address changes, a http POST is sent to the URL.""" - raise NotImplementedError() # TODO this method is currently broken - def callback(x): - import urllib.request - headers = {'content-type':'application/json'} - data = {'address':address, 'status':x.get('result')} - serialized_data = util.to_bytes(json.dumps(data)) - try: - req = urllib.request.Request(URL, serialized_data, headers) - response_stream = urllib.request.urlopen(req, timeout=5) - util.print_error('Got Response for %s' % address) - except BaseException as e: - util.print_error(str(e)) - self.network.subscribe_to_addresses([address], callback) + if not hasattr(self, "_notifier"): + self._notifier = Notifier(self.network) + self.network.run_from_another_thread(self._notifier.start_watching_queue.put((address, URL))) return True @command('wn') diff --git a/electrum/interface.py b/electrum/interface.py index ef371138..e1586a85 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -28,7 +28,7 @@ import ssl import sys import traceback import asyncio -from typing import Tuple, Union +from typing import Tuple, Union, List from collections import defaultdict import aiorpcx @@ -57,7 +57,7 @@ class NotificationSession(ClientSession): # will catch the exception, count errors, and at some point disconnect if isinstance(request, Notification): params, result = request.args[:-1], request.args[-1] - key = self.get_index(request.method, params) + key = self.get_hashable_key_for_rpc_call(request.method, params) if key in self.subscriptions: self.cache[key] = result for queue in self.subscriptions[key]: @@ -78,10 +78,10 @@ class NotificationSession(ClientSession): except asyncio.TimeoutError as e: raise RequestTimedOut('request timed out: {}'.format(args)) from e - async def subscribe(self, method, params, queue): + async def subscribe(self, method: str, params: List, queue: asyncio.Queue): # note: until the cache is written for the first time, # each 'subscribe' call might make a request on the network. - key = self.get_index(method, params) + key = self.get_hashable_key_for_rpc_call(method, params) self.subscriptions[key].append(queue) if key in self.cache: result = self.cache[key] @@ -99,7 +99,7 @@ class NotificationSession(ClientSession): v.remove(queue) @classmethod - def get_index(cls, method, params): + def get_hashable_key_for_rpc_call(cls, method, params): """Hashable index for subscriptions and cache""" return str(method) + repr(params) @@ -141,7 +141,7 @@ class Interface(PrintError): self._requested_chunks = set() self.network = network self._set_proxy(proxy) - self.session = None + self.session = None # type: NotificationSession self.tip_header = None self.tip = 0 diff --git a/electrum/network.py b/electrum/network.py index 5eb7db4c..d2d9a46b 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -852,3 +852,54 @@ class Network(PrintError): await self.interface.group.spawn(self._request_fee_estimates, self.interface) await asyncio.sleep(0.1) + + +class NetworkJobOnDefaultServer(PrintError): + """An abstract base class for a job that runs on the main network + interface. Every time the main interface changes, the job is + restarted, and some of its internals are reset. + """ + def __init__(self, network: Network): + asyncio.set_event_loop(network.asyncio_loop) + self.network = network + self.interface = None # type: Interface + self._restart_lock = asyncio.Lock() + self._reset() + asyncio.run_coroutine_threadsafe(self._restart(), network.asyncio_loop) + network.register_callback(self._restart, ['default_server_changed']) + + def _reset(self): + """Initialise fields. Called every time the underlying + server connection changes. + """ + self.group = SilentTaskGroup() + + async def _start(self, interface): + self.interface = interface + await interface.group.spawn(self._start_tasks) + + async def _start_tasks(self): + """Start tasks in self.group. Called every time the underlying + server connection changes. + """ + raise NotImplementedError() # implemented by subclasses + + async def stop(self): + await self.group.cancel_remaining() + + @aiosafe + async def _restart(self, *args): + interface = self.network.interface + if interface is None: + return # we should get called again soon + + async with self._restart_lock: + await self.stop() + self._reset() + await self._start(interface) + + @property + def session(self): + s = self.interface.session + assert s is not None + return s diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py index cb84f963..12bbb18c 100644 --- a/electrum/synchronizer.py +++ b/electrum/synchronizer.py @@ -24,12 +24,15 @@ # SOFTWARE. import asyncio import hashlib +from typing import Dict, List +from collections import defaultdict from aiorpcx import TaskGroup, run_in_thread from .transaction import Transaction -from .util import bh2u, PrintError +from .util import bh2u, make_aiohttp_session from .bitcoin import address_to_scripthash +from .network import NetworkJobOnDefaultServer def history_status(h): @@ -41,7 +44,68 @@ def history_status(h): return bh2u(hashlib.sha256(status.encode('ascii')).digest()) -class Synchronizer(PrintError): +class SynchronizerBase(NetworkJobOnDefaultServer): + """Subscribe over the network to a set of addresses, and monitor their statuses. + Every time a status changes, run a coroutine provided by the subclass. + """ + def __init__(self, network): + NetworkJobOnDefaultServer.__init__(self, network) + self.asyncio_loop = network.asyncio_loop + + def _reset(self): + super()._reset() + self.requested_addrs = set() + self.scripthash_to_address = {} + self._processed_some_notifications = False # so that we don't miss them + # Queues + self.add_queue = asyncio.Queue() + self.status_queue = asyncio.Queue() + + async def _start_tasks(self): + try: + async with self.group as group: + await group.spawn(self.send_subscriptions()) + await group.spawn(self.handle_status()) + await group.spawn(self.main()) + finally: + # we are being cancelled now + self.session.unsubscribe(self.status_queue) + + def add(self, addr): + asyncio.run_coroutine_threadsafe(self._add_address(addr), self.asyncio_loop) + + async def _add_address(self, addr): + if addr in self.requested_addrs: return + self.requested_addrs.add(addr) + await self.add_queue.put(addr) + + async def _on_address_status(self, addr, status): + """Handle the change of the status of an address.""" + raise NotImplementedError() # implemented by subclasses + + async def send_subscriptions(self): + async def subscribe_to_address(addr): + h = address_to_scripthash(addr) + self.scripthash_to_address[h] = addr + await self.session.subscribe('blockchain.scripthash.subscribe', [h], self.status_queue) + self.requested_addrs.remove(addr) + + while True: + addr = await self.add_queue.get() + await self.group.spawn(subscribe_to_address, addr) + + async def handle_status(self): + while True: + h, status = await self.status_queue.get() + addr = self.scripthash_to_address[h] + await self.group.spawn(self._on_address_status, addr, status) + self._processed_some_notifications = True + + async def main(self): + raise NotImplementedError() # implemented by subclasses + + +class Synchronizer(SynchronizerBase): '''The synchronizer keeps the wallet up-to-date with its set of addresses and their transactions. It subscribes over the network to wallet addresses, gets the wallet to generate new addresses @@ -51,16 +115,12 @@ class Synchronizer(PrintError): ''' def __init__(self, wallet): self.wallet = wallet - self.network = wallet.network - self.asyncio_loop = wallet.network.asyncio_loop + SynchronizerBase.__init__(self, wallet.network) + + def _reset(self): + super()._reset() self.requested_tx = {} self.requested_histories = {} - self.requested_addrs = set() - self.scripthash_to_address = {} - self._processed_some_notifications = False # so that we don't miss them - # Queues - self.add_queue = asyncio.Queue() - self.status_queue = asyncio.Queue() def diagnostic_name(self): return '{}:{}'.format(self.__class__.__name__, self.wallet.diagnostic_name()) @@ -70,14 +130,6 @@ class Synchronizer(PrintError): and not self.requested_histories and not self.requested_tx) - def add(self, addr): - asyncio.run_coroutine_threadsafe(self._add(addr), self.asyncio_loop) - - async def _add(self, addr): - if addr in self.requested_addrs: return - self.requested_addrs.add(addr) - await self.add_queue.put(addr) - async def _on_address_status(self, addr, status): history = self.wallet.history.get(addr, []) if history_status(history) == status: @@ -144,30 +196,6 @@ class Synchronizer(PrintError): # callbacks self.wallet.network.trigger_callback('new_transaction', self.wallet, tx) - async def send_subscriptions(self, group: TaskGroup): - async def subscribe_to_address(addr): - h = address_to_scripthash(addr) - self.scripthash_to_address[h] = addr - await self.session.subscribe('blockchain.scripthash.subscribe', [h], self.status_queue) - self.requested_addrs.remove(addr) - - while True: - addr = await self.add_queue.get() - await group.spawn(subscribe_to_address, addr) - - async def handle_status(self, group: TaskGroup): - while True: - h, status = await self.status_queue.get() - addr = self.scripthash_to_address[h] - await group.spawn(self._on_address_status, addr, status) - self._processed_some_notifications = True - - @property - def session(self): - s = self.wallet.network.interface.session - assert s is not None - return s - async def main(self): self.wallet.set_up_to_date(False) # request missing txns, if any @@ -178,7 +206,7 @@ class Synchronizer(PrintError): await self._request_missing_txs(history) # add addresses to bootstrap for addr in self.wallet.get_addresses(): - await self._add(addr) + await self._add_address(addr) # main loop while True: await asyncio.sleep(0.1) @@ -189,3 +217,37 @@ class Synchronizer(PrintError): self._processed_some_notifications = False self.wallet.set_up_to_date(up_to_date) self.wallet.network.trigger_callback('wallet_updated', self.wallet) + + +class Notifier(SynchronizerBase): + """Watch addresses. Every time the status of an address changes, + an HTTP POST is sent to the corresponding URL. + """ + def __init__(self, network): + SynchronizerBase.__init__(self, network) + self.watched_addresses = defaultdict(list) # type: Dict[str, List[str]] + self.start_watching_queue = asyncio.Queue() + + async def main(self): + # resend existing subscriptions if we were restarted + for addr in self.watched_addresses: + await self._add_address(addr) + # main loop + while True: + addr, url = await self.start_watching_queue.get() + self.watched_addresses[addr].append(url) + await self._add_address(addr) + + async def _on_address_status(self, addr, status): + self.print_error('new status for addr {}'.format(addr)) + headers = {'content-type': 'application/json'} + data = {'address': addr, 'status': status} + for url in self.watched_addresses[addr]: + try: + async with make_aiohttp_session(proxy=self.network.proxy, headers=headers) as session: + async with session.post(url, json=data, headers=headers) as resp: + await resp.text() + except Exception as e: + self.print_error(str(e)) + else: + self.print_error('Got Response for {}'.format(addr)) diff --git a/electrum/util.py b/electrum/util.py index 20af6e17..9ef3e516 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -869,7 +869,12 @@ VerifiedTxInfo = NamedTuple("VerifiedTxInfo", [("height", int), ("txpos", int), ("header_hash", str)]) -def make_aiohttp_session(proxy): + +def make_aiohttp_session(proxy: dict, headers=None, timeout=None): + if headers is None: + headers = {'User-Agent': 'Electrum'} + if timeout is None: + timeout = aiohttp.ClientTimeout(total=10) if proxy: connector = SocksConnector( socks_ver=SocksVer.SOCKS5 if proxy['mode'] == 'socks5' else SocksVer.SOCKS4, @@ -879,9 +884,9 @@ def make_aiohttp_session(proxy): password=proxy.get('password', None), rdns=True ) - return aiohttp.ClientSession(headers={'User-Agent' : 'Electrum'}, timeout=aiohttp.ClientTimeout(total=10), connector=connector) + return aiohttp.ClientSession(headers=headers, timeout=timeout, connector=connector) else: - return aiohttp.ClientSession(headers={'User-Agent' : 'Electrum'}, timeout=aiohttp.ClientTimeout(total=10)) + return aiohttp.ClientSession(headers=headers, timeout=timeout) class SilentTaskGroup(TaskGroup): diff --git a/electrum/verifier.py b/electrum/verifier.py index cb6bdfa6..d2c35759 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -25,14 +25,14 @@ import asyncio from typing import Sequence, Optional import aiorpcx -from aiorpcx import TaskGroup -from .util import PrintError, bh2u, VerifiedTxInfo +from .util import bh2u, VerifiedTxInfo from .bitcoin import Hash, hash_decode, hash_encode from .transaction import Transaction from .blockchain import hash_header from .interface import GracefulDisconnect from . import constants +from .network import NetworkJobOnDefaultServer class MerkleVerificationFailure(Exception): pass @@ -41,26 +41,33 @@ class MerkleRootMismatch(MerkleVerificationFailure): pass class InnerNodeOfSpvProofIsValidTx(MerkleVerificationFailure): pass -class SPV(PrintError): +class SPV(NetworkJobOnDefaultServer): """ Simple Payment Verification """ def __init__(self, network, wallet): + NetworkJobOnDefaultServer.__init__(self, network) self.wallet = wallet - self.network = network + + def _reset(self): + super()._reset() self.merkle_roots = {} # txid -> merkle root (once it has been verified) self.requested_merkle = set() # txid set of pending requests + async def _start_tasks(self): + async with self.group as group: + await group.spawn(self.main) + def diagnostic_name(self): return '{}:{}'.format(self.__class__.__name__, self.wallet.diagnostic_name()) - async def main(self, group: TaskGroup): + async def main(self): self.blockchain = self.network.blockchain() while True: await self._maybe_undo_verifications() - await self._request_proofs(group) + await self._request_proofs() await asyncio.sleep(0.1) - async def _request_proofs(self, group: TaskGroup): + async def _request_proofs(self): local_height = self.blockchain.height() unverified = self.wallet.get_unverified_txs() @@ -75,12 +82,12 @@ class SPV(PrintError): header = self.blockchain.read_header(tx_height) if header is None: if tx_height < constants.net.max_checkpoint(): - await group.spawn(self.network.request_chunk(tx_height, None, can_return_early=True)) + await self.group.spawn(self.network.request_chunk(tx_height, None, can_return_early=True)) continue # request now self.print_error('requested merkle', tx_hash) self.requested_merkle.add(tx_hash) - await group.spawn(self._request_and_verify_single_proof, tx_hash, tx_height) + await self.group.spawn(self._request_and_verify_single_proof, tx_hash, tx_height) async def _request_and_verify_single_proof(self, tx_hash, tx_height): try: diff --git a/electrum/websockets.py b/electrum/websockets.py index 4637f83e..b4c380b1 100644 --- a/electrum/websockets.py +++ b/electrum/websockets.py @@ -22,44 +22,49 @@ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import queue -import threading, os, json +import threading +import os +import json from collections import defaultdict +import asyncio +from typing import Dict, List +import traceback +import sys + try: from SimpleWebSocketServer import WebSocket, SimpleSSLWebSocketServer except ImportError: - import sys sys.exit("install SimpleWebSocketServer") -from . import util +from .util import PrintError from . import bitcoin +from .synchronizer import SynchronizerBase -request_queue = queue.Queue() +request_queue = asyncio.Queue() -class ElectrumWebSocket(WebSocket): + +class ElectrumWebSocket(WebSocket, PrintError): def handleMessage(self): assert self.data[0:3] == 'id:' - util.print_error("message received", self.data) + self.print_error("message received", self.data) request_id = self.data[3:] - request_queue.put((self, request_id)) + asyncio.run_coroutine_threadsafe( + request_queue.put((self, request_id)), asyncio.get_event_loop()) def handleConnected(self): - util.print_error("connected", self.address) + self.print_error("connected", self.address) def handleClose(self): - util.print_error("closed", self.address) + self.print_error("closed", self.address) - -class WsClientThread(util.DaemonThread): +class BalanceMonitor(SynchronizerBase): def __init__(self, config, network): - util.DaemonThread.__init__(self) - self.network = network + SynchronizerBase.__init__(self, network) self.config = config - self.response_queue = queue.Queue() - self.subscriptions = defaultdict(list) + self.expected_payments = defaultdict(list) # type: Dict[str, List[WebSocket, int]] def make_request(self, request_id): # read json file @@ -72,69 +77,47 @@ class WsClientThread(util.DaemonThread): amount = d.get('amount') return addr, amount - def reading_thread(self): - while self.is_running(): - try: - ws, request_id = request_queue.get() - except queue.Empty: - continue + async def main(self): + # resend existing subscriptions if we were restarted + for addr in self.expected_payments: + await self._add_address(addr) + # main loop + while True: + ws, request_id = await request_queue.get() try: addr, amount = self.make_request(request_id) - except: + except Exception: + traceback.print_exc(file=sys.stderr) continue - l = self.subscriptions.get(addr, []) - l.append((ws, amount)) - self.subscriptions[addr] = l - self.network.subscribe_to_addresses([addr], self.response_queue.put) - - def run(self): - threading.Thread(target=self.reading_thread).start() - while self.is_running(): - try: - r = self.response_queue.get(timeout=0.1) - except queue.Empty: - continue - util.print_error('response', r) - method = r.get('method') - result = r.get('result') - if result is None: - continue - if method == 'blockchain.scripthash.subscribe': - addr = r.get('params')[0] - scripthash = bitcoin.address_to_scripthash(addr) - self.network.get_balance_for_scripthash( - scripthash, self.response_queue.put) - elif method == 'blockchain.scripthash.get_balance': - scripthash = r.get('params')[0] - addr = self.network.h2addr.get(scripthash, None) - if addr is None: - util.print_error( - "can't find address for scripthash: %s" % scripthash) - l = self.subscriptions.get(addr, []) - for ws, amount in l: - if not ws.closed: - if sum(result.values()) >=amount: - ws.sendMessage('paid') + self.expected_payments[addr].append((ws, amount)) + await self._add_address(addr) + async def _on_address_status(self, addr, status): + self.print_error('new status for addr {}'.format(addr)) + sh = bitcoin.address_to_scripthash(addr) + balance = await self.network.get_balance_for_scripthash(sh) + for ws, amount in self.expected_payments[addr]: + if not ws.closed: + if sum(balance.values()) >= amount: + ws.sendMessage('paid') class WebSocketServer(threading.Thread): - def __init__(self, config, ns): + def __init__(self, config, network): threading.Thread.__init__(self) self.config = config - self.net_server = ns + self.network = network + asyncio.set_event_loop(network.asyncio_loop) self.daemon = True + self.balance_monitor = BalanceMonitor(self.config, self.network) + self.start() def run(self): - t = WsClientThread(self.config, self.net_server) - t.start() - + asyncio.set_event_loop(self.network.asyncio_loop) host = self.config.get('websocket_server') port = self.config.get('websocket_port', 9999) certfile = self.config.get('ssl_chain') keyfile = self.config.get('ssl_privkey') self.server = SimpleSSLWebSocketServer(host, port, ElectrumWebSocket, certfile, keyfile) self.server.serveforever() - - diff --git a/run_electrum b/run_electrum index 148c54ee..9e9f1a9c 100755 --- a/run_electrum +++ b/run_electrum @@ -438,7 +438,7 @@ if __name__ == '__main__': d = daemon.Daemon(config, fd) if config.get('websocket_server'): from electrum import websockets - websockets.WebSocketServer(config, d.network).start() + websockets.WebSocketServer(config, d.network) if config.get('requests_dir'): path = os.path.join(config.get('requests_dir'), 'index.html') if not os.path.exists(path): From d759546b321ac19e8b2c8e2ac8b46efd10b883de Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Oct 2018 18:26:09 +0200 Subject: [PATCH 020/301] qt console: fix word wrap --- electrum/gui/qt/console.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/console.py b/electrum/gui/qt/console.py index f378e580..a1928118 100644 --- a/electrum/gui/qt/console.py +++ b/electrum/gui/qt/console.py @@ -77,8 +77,9 @@ class Console(QtWidgets.QPlainTextEdit): self.messageOverlay = OverlayLabel(warning_text, self) def resizeEvent(self, e): - self.messageOverlay.on_resize(self.width() - self.verticalScrollBar().width()) - + super().resizeEvent(e) + vertical_scrollbar_width = self.verticalScrollBar().width() * self.verticalScrollBar().isVisible() + self.messageOverlay.on_resize(self.width() - vertical_scrollbar_width) def set_json(self, b): self.is_json = b From decb8bfd52619b54aaa18893434045e00fbc6b35 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Oct 2018 00:16:06 +0200 Subject: [PATCH 021/301] qt network status: display 'fork' in icon when chain split is detected --- electrum/gui/qt/main_window.py | 7 ++++--- icons.qrc | 3 +++ icons/status_connected_fork.png | Bin 0 -> 62949 bytes icons/status_connected_proxy_fork.png | Bin 0 -> 60879 bytes icons/status_lagging_fork.png | Bin 0 -> 63949 bytes 5 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 icons/status_connected_fork.png create mode 100644 icons/status_connected_proxy_fork.png create mode 100644 icons/status_lagging_fork.png diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2510663f..e5a0ae99 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -742,6 +742,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): elif self.network.is_connected(): server_height = self.network.get_server_height() server_lag = self.network.get_local_height() - server_height + num_chains = len(self.network.get_blockchains()) # Server height can be 0 after switching to a new server # until we get a headers subscription request response. # Display the synchronizing message in that case. @@ -750,7 +751,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): icon = QIcon(":icons/status_waiting.png") elif server_lag > 1: text = _("Server is lagging ({} blocks)").format(server_lag) - icon = QIcon(":icons/status_lagging.png") + icon = QIcon(":icons/status_lagging.png") if num_chains <= 1 else QIcon(":icons/status_lagging_fork.png") else: c, u, x = self.wallet.get_balance() text = _("Balance" ) + ": %s "%(self.format_amount_and_units(c)) @@ -764,9 +765,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): text += self.fx.get_fiat_status_text(c + u + x, self.base_unit(), self.get_decimal_point()) or '' if not self.network.proxy: - icon = QIcon(":icons/status_connected.png") + icon = QIcon(":icons/status_connected.png") if num_chains <= 1 else QIcon(":icons/status_connected_fork.png") else: - icon = QIcon(":icons/status_connected_proxy.png") + icon = QIcon(":icons/status_connected_proxy.png") if num_chains <= 1 else QIcon(":icons/status_connected_proxy_fork.png") else: if self.network.proxy: text = "{} ({})".format(_("Not connected"), _("proxy enabled")) diff --git a/icons.qrc b/icons.qrc index 1ff4ea57..19c298ad 100644 --- a/icons.qrc +++ b/icons.qrc @@ -35,10 +35,13 @@ icons/safe-t.png icons/seed.png icons/status_connected.png + icons/status_connected_fork.png icons/status_connected_proxy.png + icons/status_connected_proxy_fork.png icons/status_disconnected.png icons/status_waiting.png icons/status_lagging.png + icons/status_lagging_fork.png icons/seal.png icons/tab_addresses.png icons/tab_coins.png diff --git a/icons/status_connected_fork.png b/icons/status_connected_fork.png new file mode 100644 index 0000000000000000000000000000000000000000..a65c2a883ca8542a4497e0b85f59dbf554f2f178 GIT binary patch literal 62949 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 HH2wTPVi>Pl literal 0 HcmV?d00001 diff --git a/icons/status_connected_proxy_fork.png b/icons/status_connected_proxy_fork.png new file mode 100644 index 0000000000000000000000000000000000000000..f6b4541e1f3b8f2f90fe9798d360023bf045193e GIT binary patch literal 60879 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 literal 0 HcmV?d00001 diff --git a/icons/status_lagging_fork.png b/icons/status_lagging_fork.png new file mode 100644 index 0000000000000000000000000000000000000000..82826721b9265333e14b39e4c1b14794ac9d5ca4 GIT binary patch literal 63949 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 literal 0 HcmV?d00001 From 1d7bf698f2bbaa11f9d384b3838a19d98aacbf1c Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Fri, 5 Oct 2018 16:39:41 +0200 Subject: [PATCH 022/301] Windows: Update copyright notice in installed apps --- contrib/build-wine/electrum.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build-wine/electrum.nsi b/contrib/build-wine/electrum.nsi index 8d2482e2..ed7adcc3 100644 --- a/contrib/build-wine/electrum.nsi +++ b/contrib/build-wine/electrum.nsi @@ -58,7 +58,7 @@ VIAddVersionKey ProductName "${PRODUCT_NAME} Installer" VIAddVersionKey Comments "The installer for ${PRODUCT_NAME}" VIAddVersionKey CompanyName "${PRODUCT_NAME}" - VIAddVersionKey LegalCopyright "2013-2016 ${PRODUCT_PUBLISHER}" + VIAddVersionKey LegalCopyright "2013-2018 ${PRODUCT_PUBLISHER}" VIAddVersionKey FileDescription "${PRODUCT_NAME} Installer" VIAddVersionKey FileVersion ${PRODUCT_VERSION} VIAddVersionKey ProductVersion ${PRODUCT_VERSION} From b37695f9c8a6f3ea57c829209a14027c6ad11e60 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 6 Oct 2018 01:58:30 +0200 Subject: [PATCH 023/301] linux launcher madness see #4300 --- electrum.desktop | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum.desktop b/electrum.desktop index 4be21b7f..481f3b93 100644 --- a/electrum.desktop +++ b/electrum.desktop @@ -3,7 +3,7 @@ [Desktop Entry] Comment=Lightweight Bitcoin Client -Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\" electrum %u" +Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; electrum %u" GenericName[en_US]=Bitcoin Wallet GenericName=Bitcoin Wallet Icon=electrum @@ -17,5 +17,5 @@ MimeType=x-scheme-handler/bitcoin; Actions=Testnet; [Desktop Action Testnet] -Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\" electrum --testnet %u" +Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; electrum --testnet %u" Name=Testnet mode From 70cca3bad9a40b5b5230e13c0d2fce7ae1312b53 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 7 Oct 2018 17:50:52 +0200 Subject: [PATCH 024/301] fix #4759 --- electrum/util.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index 20af6e17..8b796d05 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -511,15 +511,21 @@ def format_satoshis(x, num_zeros=0, decimal_point=8, precision=None, is_diff=Fal return 'unknown' if precision is None: precision = decimal_point + # format string decimal_format = ".0" + str(precision) if precision > 0 else "" if is_diff: decimal_format = '+' + decimal_format - result = ("{:" + decimal_format + "f}").format(x / pow (10, decimal_point)).rstrip('0') + # initial result + scale_factor = pow(10, decimal_point) + result = ("{:" + decimal_format + "f}").format(Decimal(x) / scale_factor) + if "." not in result: result += "." + result = result.rstrip('0') + # extra decimal places integer_part, fract_part = result.split(".") - dp = DECIMAL_POINT if len(fract_part) < num_zeros: fract_part += "0" * (num_zeros - len(fract_part)) - result = integer_part + dp + fract_part + result = integer_part + DECIMAL_POINT + fract_part + # leading/trailing whitespaces if whitespaces: result += " " * (decimal_point - len(fract_part)) result = " " * (15 - len(result)) + result From f3f25348774255f1c3a3dc27822c48d761b1e87d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 7 Oct 2018 17:59:32 +0200 Subject: [PATCH 025/301] qt status: display "loading wallet" temporarily this will likely only be visible for large wallets; it gets overwritten by update_status() --- 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 2510663f..2659a726 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1962,7 +1962,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): sb.setFixedHeight(35) qtVersion = qVersion() - self.balance_label = QLabel("") + self.balance_label = QLabel("Loading wallet...") self.balance_label.setTextInteractionFlags(Qt.TextSelectableByMouse) self.balance_label.setStyleSheet("""QLabel { padding: 0 }""") sb.addWidget(self.balance_label) From dc1a31d80253df73dc5598cf1a255b31549da611 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 7 Oct 2018 18:43:35 +0200 Subject: [PATCH 026/301] fix tests follow-up 70cca3bad9a40b5b5230e13c0d2fce7ae1312b53 --- electrum/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/util.py b/electrum/util.py index 8b796d05..759fb740 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -517,7 +517,9 @@ def format_satoshis(x, num_zeros=0, decimal_point=8, precision=None, is_diff=Fal decimal_format = '+' + decimal_format # initial result scale_factor = pow(10, decimal_point) - result = ("{:" + decimal_format + "f}").format(Decimal(x) / scale_factor) + if not isinstance(x, Decimal): + x = Decimal(x).quantize(Decimal('1E-8')) + result = ("{:" + decimal_format + "f}").format(x / scale_factor) if "." not in result: result += "." result = result.rstrip('0') # extra decimal places From 508793b0101a2840283e21f8dba4390c3dd7624e Mon Sep 17 00:00:00 2001 From: Mark B Lundeberg <36528214+markblundeberg@users.noreply.github.com> Date: Mon, 8 Oct 2018 15:04:45 -0700 Subject: [PATCH 027/301] qt transaction_dialog: normal close if user presses Esc (Electron-Cash/Electron-Cash#890) --- electrum/gui/qt/transaction_dialog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 495bad31..9106979d 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -178,6 +178,10 @@ class TxDialog(QDialog, MessageBoxMixin): except ValueError: pass # was not in list already + def reject(self): + # Override escape-key to close normally (and invoke closeEvent) + self.close() + def show_qr(self): text = bfh(str(self.tx)) text = base_encode(text, base=43) From cc18f667930c8de094f12756a11659a43aa311d5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 9 Oct 2018 12:03:38 +0200 Subject: [PATCH 028/301] network: don't save negative ETA fee estimates -1 means bitcoind could not give an estimate --- electrum/gui/qt/main_window.py | 4 ++-- electrum/network.py | 3 ++- electrum/util.py | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2659a726..68f3a4bd 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1542,8 +1542,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): tx = self.wallet.make_unsigned_transaction( coins, outputs, self.config, fixed_fee=fee_estimator, is_sweep=is_sweep) - except NotEnoughFunds: - self.show_message(_("Insufficient funds")) + except (NotEnoughFunds, NoDynamicFeeEstimates) as e: + self.show_message(str(e)) return except BaseException as e: traceback.print_exc(file=sys.stdout) diff --git a/electrum/network.py b/electrum/network.py index 5eb7db4c..cc81259f 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -357,8 +357,9 @@ class Network(PrintError): self.notify('fee_histogram') for i, task in fee_tasks: fee = int(task.result() * COIN) - self.config.update_fee_estimates(i, fee) self.print_error("fee_estimates[%d]" % i, fee) + if fee < 0: continue + self.config.update_fee_estimates(i, fee) self.notify('fee') def get_status_value(self, key): diff --git a/electrum/util.py b/electrum/util.py index 759fb740..9384065b 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -77,7 +77,9 @@ def base_unit_name_to_decimal_point(unit_name: str) -> int: raise UnknownBaseUnit(unit_name) from None -class NotEnoughFunds(Exception): pass +class NotEnoughFunds(Exception): + def __str__(self): + return _("Insufficient funds") class NoDynamicFeeEstimates(Exception): From ad503daaca9ba3647c1d5d9933180fd90954da9b Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Tue, 9 Oct 2018 22:28:27 +0800 Subject: [PATCH 029/301] Fix some typos in RELEASE-NOTES (#4762) --- RELEASE-NOTES | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 74b68774..9350036e 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -223,7 +223,7 @@ issue #3374. Users should upgrade to 3.0.5. * Qt GUI: sweeping now uses the Send tab, allowing fees to be set * Windows: if using the installer binary, there is now a separate shortcut for "Electrum Testnet" - * Digital Bitbox: added suport for p2sh-segwit + * Digital Bitbox: added support for p2sh-segwit * OS notifications for incoming transactions * better transaction size estimation: - fees for segwit txns were somewhat underestimated (#3347) @@ -451,7 +451,7 @@ issue #3374. Users should upgrade to 3.0.5. # Release 2.7.7 * Fix utf8 encoding bug with old wallet seeds (issue #1967) - * Fix delete request from menu (isue #1968) + * Fix delete request from menu (issue #1968) # Release 2.7.6 * Fixes a critical bug with imported private keys (issue #1966). Keys @@ -814,7 +814,7 @@ issue #3374. Users should upgrade to 3.0.5. * New 'Receive' tab in the GUI: - create and manage payment requests, with QR Codes - the former 'Receive' tab was renamed to 'Addresses' - - the former Point of Sale plugin is replaced by a resizeable + - the former Point of Sale plugin is replaced by a resizable window that pops up if you click on the QR code * The 'Send' tab in the Qt GUI supports transactions with multiple @@ -837,7 +837,7 @@ issue #3374. Users should upgrade to 3.0.5. * The client accepts servers with a CA-signed SSL certificate. - * ECIES encrypt/decrypt methods, availabe in the GUI and using + * ECIES encrypt/decrypt methods, available in the GUI and using the command line: encrypt decrypt @@ -910,7 +910,7 @@ bugfixes: connection problems, transactions staying unverified # Release 1.8.1 -* Notification option when receiving new tranactions +* Notification option when receiving new transactions * Confirm dialogue before sending large amounts * Alternative datafile location for non-windows systems * Fix offline wallet creation From 87b05e1c9e1b7163e5f100765656b26b96c8a144 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 10 Oct 2018 15:56:41 +0200 Subject: [PATCH 030/301] network: change broadcast_transaction api raise exceptions instead of weird return values closes #4433 --- electrum/gui/kivy/main_window.py | 11 ++++++++--- electrum/gui/qt/main_window.py | 8 ++++++-- electrum/gui/stdio.py | 11 +++++------ electrum/gui/text.py | 12 +++++------- electrum/network.py | 12 +++--------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 27eab521..f2834727 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -887,9 +887,14 @@ class ElectrumWindow(App): Clock.schedule_once(lambda dt: on_success(tx)) def _broadcast_thread(self, tx, on_complete): - ok, txid = self.network.run_from_another_thread( - self.network.broadcast_transaction(tx)) - Clock.schedule_once(lambda dt: on_complete(ok, txid)) + + try: + self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) + except Exception as e: + ok, msg = False, repr(e) + else: + ok, msg = True, tx.txid() + Clock.schedule_once(lambda dt: on_complete(ok, msg)) def broadcast(self, tx, pr=None): def on_complete(ok, msg): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index e532ac9b..8e95862a 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1639,8 +1639,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if pr and pr.has_expired(): self.payment_request = None return False, _("Payment request has expired") - status, msg = self.network.run_from_another_thread( - self.network.broadcast_transaction(tx)) + try: + self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) + except Exception as e: + status, msg = False, repr(e) + else: + status, msg = True, tx.txid() if pr and status is True: self.invoices.set_paid(pr, tx.txid()) self.invoices.save() diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py index f4a86173..7b4a41b9 100644 --- a/electrum/gui/stdio.py +++ b/electrum/gui/stdio.py @@ -203,15 +203,14 @@ class ElectrumGui: self.wallet.labels[tx.txid()] = self.str_description print(_("Please wait...")) - status, msg = self.network.run_from_another_thread( - self.network.broadcast_transaction(tx)) - - if status: + try: + self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) + except Exception as e: + print(repr(e)) + else: print(_('Payment sent.')) #self.do_clear() #self.update_contacts_tab() - else: - print(_('Error')) def network_dialog(self): print("use 'electrum setconfig server/proxy' to change your network settings") diff --git a/electrum/gui/text.py b/electrum/gui/text.py index ed3faa0b..7d429ae0 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -367,16 +367,14 @@ class ElectrumGui: self.wallet.labels[tx.txid()] = self.str_description self.show_message(_("Please wait..."), getchar=False) - status, msg = self.network.run_from_another_thread( - self.network.broadcast_transaction(tx)) - - if status: + try: + self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) + except Exception as e: + self.show_message(repr(e)) + else: self.show_message(_('Payment sent.')) self.do_clear() #self.update_contacts_tab() - else: - self.show_message(_('Error')) - def show_message(self, message, getchar = True): w = self.w diff --git a/electrum/network.py b/electrum/network.py index cc81259f..7b5d41f3 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -676,16 +676,10 @@ class Network(PrintError): @best_effort_reliable async def broadcast_transaction(self, tx, timeout=10): - try: - out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout) - except RequestTimedOut as e: - return False, "error: operation timed out" - except Exception as e: - return False, "error: " + str(e) - + out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout) if out != tx.txid(): - return False, "error: " + out - return True, out + raise Exception(out) + return out # txid @best_effort_reliable async def request_chunk(self, height, tip=None, *, can_return_early=False): From bb9871ded7aadd20381461b5205a99799af52d48 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 10 Oct 2018 19:24:24 +0200 Subject: [PATCH 031/301] simplify prev commit --- electrum/gui/qt/main_window.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 8f58629f..0816c0a7 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -743,7 +743,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): elif self.network.is_connected(): server_height = self.network.get_server_height() server_lag = self.network.get_local_height() - server_height - num_chains = len(self.network.get_blockchains()) + fork_str = "_fork" if len(self.network.get_blockchains())>1 else "" # Server height can be 0 after switching to a new server # until we get a headers subscription request response. # Display the synchronizing message in that case. @@ -752,7 +752,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): icon = QIcon(":icons/status_waiting.png") elif server_lag > 1: text = _("Server is lagging ({} blocks)").format(server_lag) - icon = QIcon(":icons/status_lagging.png") if num_chains <= 1 else QIcon(":icons/status_lagging_fork.png") + icon = QIcon(":icons/status_lagging%s.png"%fork_str) else: c, u, x = self.wallet.get_balance() text = _("Balance" ) + ": %s "%(self.format_amount_and_units(c)) @@ -766,9 +766,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): text += self.fx.get_fiat_status_text(c + u + x, self.base_unit(), self.get_decimal_point()) or '' if not self.network.proxy: - icon = QIcon(":icons/status_connected.png") if num_chains <= 1 else QIcon(":icons/status_connected_fork.png") + icon = QIcon(":icons/status_connected%s.png"%fork_str) else: - icon = QIcon(":icons/status_connected_proxy.png") if num_chains <= 1 else QIcon(":icons/status_connected_proxy_fork.png") + icon = QIcon(":icons/status_connected_proxy%s.png%fork_str") else: if self.network.proxy: text = "{} ({})".format(_("Not connected"), _("proxy enabled")) From e975727075c048169572f67eed8e07cf40482120 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 10 Oct 2018 19:26:02 +0200 Subject: [PATCH 032/301] follow-up prev commit --- 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 0816c0a7..01b5391b 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -768,7 +768,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if not self.network.proxy: icon = QIcon(":icons/status_connected%s.png"%fork_str) else: - icon = QIcon(":icons/status_connected_proxy%s.png%fork_str") + icon = QIcon(":icons/status_connected_proxy%s.png"%fork_str) else: if self.network.proxy: text = "{} ({})".format(_("Not connected"), _("proxy enabled")) From 150e27608b2d5eaef438f32bc16adc3805b5e331 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 10 Oct 2018 20:26:12 +0200 Subject: [PATCH 033/301] wallet: rm electrum_version field --- electrum/gui/qt/main_window.py | 5 +++-- electrum/wallet.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 01b5391b..3308ccf7 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -57,6 +57,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, from electrum.transaction import Transaction, TxOutput from electrum.address_synchronizer import AddTransactionException from electrum.wallet import Multisig_Wallet, CannotBumpFee +from electrum.version import ELECTRUM_VERSION from .exception_window import Exception_Hook from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit @@ -399,7 +400,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def watching_only_changed(self): name = "Electrum Testnet" if constants.net.TESTNET else "Electrum" - title = '%s %s - %s' % (name, self.wallet.electrum_version, + title = '%s %s - %s' % (name, ELECTRUM_VERSION, self.wallet.basename()) extra = [self.wallet.storage.get('wallet_type', '?')] if self.wallet.is_watching_only(): @@ -584,7 +585,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def show_about(self): QMessageBox.about(self, "Electrum", - (_("Version")+" %s" % self.wallet.electrum_version + "\n\n" + + (_("Version")+" %s" % ELECTRUM_VERSION + "\n\n" + _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin.") + " " + _("You do not need to perform regular backups, because your wallet can be " "recovered from a secret phrase that you can memorize or write on paper.") + " " + diff --git a/electrum/wallet.py b/electrum/wallet.py index 8df288bb..756b98a7 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -167,7 +167,6 @@ class Abstract_Wallet(AddressSynchronizer): def __init__(self, storage): AddressSynchronizer.__init__(self, storage) - self.electrum_version = ELECTRUM_VERSION # saved fields self.use_change = storage.get('use_change', True) self.multiple_change = storage.get('multiple_change', False) From 1ef804c652991337dedfa3d8749eff2820549c52 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 11 Oct 2018 16:30:30 +0200 Subject: [PATCH 034/301] small import clean-up --- electrum/base_crash_reporter.py | 4 ++-- electrum/contacts.py | 4 +--- electrum/jsonrpc.py | 3 ++- electrum/mnemonic.py | 1 - electrum/websockets.py | 4 ++-- electrum/x509.py | 10 +++++++--- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py index cc63159c..48f84419 100644 --- a/electrum/base_crash_reporter.py +++ b/electrum/base_crash_reporter.py @@ -28,11 +28,11 @@ import sys import os from .version import ELECTRUM_VERSION -from .import constants +from . import constants from .i18n import _ - from .util import make_aiohttp_session + class BaseCrashReporter: report_server = "https://crashhub.electrum.org" config_key = "show_crash_reporter" diff --git a/electrum/contacts.py b/electrum/contacts.py index 03b8d3ec..8e20245b 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -21,11 +21,9 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re + import dns from dns.exception import DNSException -import json -import traceback -import sys from . import bitcoin from . import dnssec diff --git a/electrum/jsonrpc.py b/electrum/jsonrpc.py index 200b6e86..1640f529 100644 --- a/electrum/jsonrpc.py +++ b/electrum/jsonrpc.py @@ -23,10 +23,11 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler from base64 import b64decode import time +from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler + from . import util diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py index a22913ae..249c096c 100644 --- a/electrum/mnemonic.py +++ b/electrum/mnemonic.py @@ -23,7 +23,6 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os -import hmac import math import hashlib import unicodedata diff --git a/electrum/websockets.py b/electrum/websockets.py index b4c380b1..e87ad27c 100644 --- a/electrum/websockets.py +++ b/electrum/websockets.py @@ -27,7 +27,7 @@ import os import json from collections import defaultdict import asyncio -from typing import Dict, List +from typing import Dict, List, Tuple import traceback import sys @@ -64,7 +64,7 @@ class BalanceMonitor(SynchronizerBase): def __init__(self, config, network): SynchronizerBase.__init__(self, network) self.config = config - self.expected_payments = defaultdict(list) # type: Dict[str, List[WebSocket, int]] + self.expected_payments = defaultdict(list) # type: Dict[str, List[Tuple[WebSocket, int]]] def make_request(self, request_id): # read json file diff --git a/electrum/x509.py b/electrum/x509.py index f03d3051..f4b8ffca 100644 --- a/electrum/x509.py +++ b/electrum/x509.py @@ -22,12 +22,16 @@ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from . import util -from .util import profiler, bh2u -import ecdsa + import hashlib import time +import ecdsa + +from . import util +from .util import profiler, bh2u + + # algo OIDs ALGO_RSA_SHA1 = '1.2.840.113549.1.1.5' ALGO_RSA_SHA256 = '1.2.840.113549.1.1.11' From 37206ec08e37804813ff213ce48eb28eca0f38a3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 11 Oct 2018 19:42:38 +0200 Subject: [PATCH 035/301] network: auto-switch servers to preferred fork (or longest chain) If auto_connect is enabled, allow jumping between forks too. (Previously auto_connect was only switching servers on a given fork, not across forks) If there is a preferred fork set, jump to that (and stay); if there isn't, always jump to the longest fork. --- electrum/blockchain.py | 24 ++++-- electrum/gui/kivy/main_window.py | 4 +- electrum/gui/qt/network_dialog.py | 11 +-- electrum/interface.py | 1 + electrum/network.py | 120 +++++++++++++++++++----------- electrum/verifier.py | 2 +- 6 files changed, 103 insertions(+), 59 deletions(-) diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 09fd097f..da1a6ecb 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -22,7 +22,7 @@ # SOFTWARE. import os import threading -from typing import Optional +from typing import Optional, Dict from . import util from .bitcoin import Hash, hash_encode, int_to_hex, rev_hex @@ -73,7 +73,7 @@ def hash_header(header: dict) -> str: return hash_encode(Hash(bfh(serialize_header(header)))) -blockchains = {} +blockchains = {} # type: Dict[int, Blockchain] blockchains_lock = threading.Lock() @@ -100,7 +100,7 @@ class Blockchain(util.PrintError): Manages blockchain headers and their verification """ - def __init__(self, config, forkpoint: int, parent_id: int): + def __init__(self, config, forkpoint: int, parent_id: Optional[int]): self.config = config self.forkpoint = forkpoint self.checkpoints = constants.net.CHECKPOINTS @@ -124,22 +124,32 @@ class Blockchain(util.PrintError): children = list(filter(lambda y: y.parent_id==self.forkpoint, chains)) return max([x.forkpoint for x in children]) if children else None - def get_forkpoint(self) -> int: + def get_max_forkpoint(self) -> int: + """Returns the max height where there is a fork + related to this chain. + """ mc = self.get_max_child() return mc if mc is not None else self.forkpoint def get_branch_size(self) -> int: - return self.height() - self.get_forkpoint() + 1 + return self.height() - self.get_max_forkpoint() + 1 def get_name(self) -> str: - return self.get_hash(self.get_forkpoint()).lstrip('00')[0:10] + return self.get_hash(self.get_max_forkpoint()).lstrip('00')[0:10] def check_header(self, header: dict) -> bool: header_hash = hash_header(header) height = header.get('block_height') + return self.check_hash(height, header_hash) + + def check_hash(self, height: int, header_hash: str) -> bool: + """Returns whether the hash of the block at given height + is the given hash. + """ + assert isinstance(header_hash, str) and len(header_hash) == 64, header_hash # hex try: return header_hash == self.get_hash(height) - except MissingHeader: + except Exception: return False def fork(parent, header: dict) -> 'Blockchain': diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index f2834727..13f70e17 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -120,7 +120,7 @@ class ElectrumWindow(App): with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items()) for index, b in blockchain_items: if name == b.get_name(): - self.network.run_from_another_thread(self.network.follow_chain(index)) + self.network.run_from_another_thread(self.network.follow_chain_given_id(index)) names = [blockchain.blockchains[b].get_name() for b in chains] if len(names) > 1: cur_chain = self.network.blockchain().get_name() @@ -664,7 +664,7 @@ class ElectrumWindow(App): self.num_nodes = len(self.network.get_interfaces()) self.num_chains = len(self.network.get_blockchains()) chain = self.network.blockchain() - self.blockchain_forkpoint = chain.get_forkpoint() + self.blockchain_forkpoint = chain.get_max_forkpoint() self.blockchain_name = chain.get_name() interface = self.network.interface if interface: diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index afb288d8..bef85383 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -107,7 +107,7 @@ class NodesListWidget(QTreeWidget): b = blockchain.blockchains[k] name = b.get_name() if n_chains >1: - x = QTreeWidgetItem([name + '@%d'%b.get_forkpoint(), '%d'%b.height()]) + x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()]) x.setData(0, Qt.UserRole, 1) x.setData(1, Qt.UserRole, b.forkpoint) else: @@ -364,7 +364,7 @@ class NetworkChoiceLayout(object): chains = self.network.get_blockchains() if len(chains) > 1: chain = self.network.blockchain() - forkpoint = chain.get_forkpoint() + forkpoint = chain.get_max_forkpoint() name = chain.get_name() msg = _('Chain split detected at block {0}').format(forkpoint) + '\n' msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name @@ -411,14 +411,11 @@ class NetworkChoiceLayout(object): self.set_server() def follow_branch(self, index): - self.network.run_from_another_thread(self.network.follow_chain(index)) + self.network.run_from_another_thread(self.network.follow_chain_given_id(index)) self.update() def follow_server(self, server): - net_params = self.network.get_parameters() - host, port, protocol = deserialize_server(server) - net_params = net_params._replace(host=host, port=port, protocol=protocol) - self.network.run_from_another_thread(self.network.set_parameters(net_params)) + self.network.run_from_another_thread(self.network.follow_chain_given_server(server)) self.update() def server_changed(self, x): diff --git a/electrum/interface.py b/electrum/interface.py index e1586a85..3fa35527 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -384,6 +384,7 @@ class Interface(PrintError): self.mark_ready() await self._process_header_at_tip() self.network.trigger_callback('network_updated') + await self.network.switch_unwanted_fork_interface() await self.network.switch_lagging_interface() async def _process_header_at_tip(self): diff --git a/electrum/network.py b/electrum/network.py index 549f45ad..c513eced 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -32,7 +32,7 @@ import json import sys import ipaddress import asyncio -from typing import NamedTuple, Optional, Sequence, List +from typing import NamedTuple, Optional, Sequence, List, Dict import traceback import dns @@ -172,10 +172,9 @@ class Network(PrintError): self.config = SimpleConfig(config) if isinstance(config, dict) else config self.num_server = 10 if not self.config.get('oneserver') else 0 blockchain.blockchains = blockchain.read_blockchains(self.config) - self.print_error("blockchains", list(blockchain.blockchains.keys())) - self.blockchain_index = config.get('blockchain_index', 0) - if self.blockchain_index not in blockchain.blockchains.keys(): - self.blockchain_index = 0 + self.print_error("blockchains", list(blockchain.blockchains)) + self._blockchain_preferred_block = self.config.get('blockchain_preferred_block', None) # type: Optional[Dict] + self._blockchain_index = 0 # Server for addresses and transactions self.default_server = self.config.get('server', None) # Sanitize default server @@ -213,11 +212,10 @@ class Network(PrintError): # retry times self.server_retry_time = time.time() self.nodes_retry_time = time.time() - # kick off the network. interface is the main server we are currently - # communicating with. interfaces is the set of servers we are connecting - # to or have an ongoing connection with + # the main server we are currently communicating with self.interface = None # type: Interface - self.interfaces = {} + # set of servers we have an ongoing connection with + self.interfaces = {} # type: Dict[str, Interface] self.auto_connect = self.config.get('auto_connect', True) self.connecting = set() self.server_queue = None @@ -227,8 +225,8 @@ class Network(PrintError): #self.asyncio_loop.set_debug(1) self._run_forever = asyncio.Future() self._thread = threading.Thread(target=self.asyncio_loop.run_until_complete, - args=(self._run_forever,), - name='Network') + args=(self._run_forever,), + name='Network') self._thread.start() def run_from_another_thread(self, coro): @@ -523,20 +521,40 @@ class Network(PrintError): async def switch_lagging_interface(self): '''If auto_connect and lagging, switch interface''' - if await self._server_is_lagging() and self.auto_connect: + if self.auto_connect and await self._server_is_lagging(): # switch to one that has the correct header (not height) - header = self.blockchain().read_header(self.get_local_height()) - def filt(x): - a = x[1].tip_header - b = header - assert type(a) is type(b) - return a == b - - with self.interfaces_lock: interfaces_items = list(self.interfaces.items()) - filtered = list(map(lambda x: x[0], filter(filt, interfaces_items))) + best_header = self.blockchain().read_header(self.get_local_height()) + with self.interfaces_lock: interfaces = list(self.interfaces.values()) + filtered = list(filter(lambda iface: iface.tip_header == best_header, interfaces)) if filtered: - choice = random.choice(filtered) - await self.switch_to_interface(choice) + chosen_iface = random.choice(filtered) + await self.switch_to_interface(chosen_iface.server) + + async def switch_unwanted_fork_interface(self): + """If auto_connect and main interface is not on preferred fork, + try to switch to preferred fork. + """ + if not self.auto_connect: + return + with self.interfaces_lock: interfaces = list(self.interfaces.values()) + # try to switch to preferred fork + if self._blockchain_preferred_block: + pref_height = self._blockchain_preferred_block['height'] + pref_hash = self._blockchain_preferred_block['hash'] + filtered = list(filter(lambda iface: iface.blockchain.check_hash(pref_height, pref_hash), + interfaces)) + if filtered: + chosen_iface = random.choice(filtered) + await self.switch_to_interface(chosen_iface.server) + return + # try to switch to longest chain + if self.blockchain().parent_id is None: + return # already on longest chain + filtered = list(filter(lambda iface: iface.blockchain.parent_id is None, + interfaces)) + if filtered: + chosen_iface = random.choice(filtered) + await self.switch_to_interface(chosen_iface.server) async def switch_to_interface(self, server: str): """Switch to server as our main interface. If no connection exists, @@ -704,8 +722,8 @@ class Network(PrintError): def blockchain(self) -> Blockchain: interface = self.interface if interface and interface.blockchain is not None: - self.blockchain_index = interface.blockchain.forkpoint - return blockchain.blockchains[self.blockchain_index] + self._blockchain_index = interface.blockchain.forkpoint + return blockchain.blockchains[self._blockchain_index] def get_blockchains(self): out = {} # blockchain_id -> list(interfaces) @@ -724,24 +742,42 @@ class Network(PrintError): await self.connection_down(interface.server) return ifaces - async def follow_chain(self, chain_id): - bc = blockchain.blockchains.get(chain_id) - if bc: - self.blockchain_index = chain_id - self.config.set_key('blockchain_index', chain_id) - with self.interfaces_lock: interfaces_values = list(self.interfaces.values()) - for iface in interfaces_values: - if iface.blockchain == bc: - await self.switch_to_interface(iface.server) - break - else: - raise Exception('blockchain not found', chain_id) + def _set_preferred_chain(self, chain: Blockchain): + height = chain.get_max_forkpoint() + header_hash = chain.get_hash(height) + self._blockchain_preferred_block = { + 'height': height, + 'hash': header_hash, + } + self.config.set_key('blockchain_preferred_block', self._blockchain_preferred_block) - if self.interface: - net_params = self.get_parameters() - host, port, protocol = deserialize_server(self.interface.server) - net_params = net_params._replace(host=host, port=port, protocol=protocol) - await self.set_parameters(net_params) + async def follow_chain_given_id(self, chain_id: int) -> None: + bc = blockchain.blockchains.get(chain_id) + if not bc: + raise Exception('blockchain {} not found'.format(chain_id)) + self._set_preferred_chain(bc) + # select server on this chain + with self.interfaces_lock: interfaces = list(self.interfaces.values()) + interfaces_on_selected_chain = list(filter(lambda iface: iface.blockchain == bc, interfaces)) + if len(interfaces_on_selected_chain) == 0: return + chosen_iface = random.choice(interfaces_on_selected_chain) + # switch to server (and save to config) + net_params = self.get_parameters() + host, port, protocol = deserialize_server(chosen_iface.server) + net_params = net_params._replace(host=host, port=port, protocol=protocol) + await self.set_parameters(net_params) + + async def follow_chain_given_server(self, server_str: str) -> None: + # note that server_str should correspond to a connected interface + iface = self.interfaces.get(server_str) + if iface is None: + return + self._set_preferred_chain(iface.blockchain) + # switch to server (and save to config) + net_params = self.get_parameters() + host, port, protocol = deserialize_server(server_str) + net_params = net_params._replace(host=host, port=port, protocol=protocol) + await self.set_parameters(net_params) def get_local_height(self): return self.blockchain().height() diff --git a/electrum/verifier.py b/electrum/verifier.py index d2c35759..05a6e446 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -156,7 +156,7 @@ class SPV(NetworkJobOnDefaultServer): async def _maybe_undo_verifications(self): def undo_verifications(): - height = self.blockchain.get_forkpoint() + height = self.blockchain.get_max_forkpoint() self.print_error("undoing verifications back to height {}".format(height)) tx_hashes = self.wallet.undo_verifications(self.blockchain, height) for tx_hash in tx_hashes: From 1233309ebd0c62cfbb08c2d40d67bdf8f92c6c77 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 10 Oct 2018 20:29:51 +0200 Subject: [PATCH 036/301] cli/rpc: 'restore' and 'create' commands are now available via RPC --- electrum/commands.py | 89 +++++++++++++++++++++++++++---- electrum/daemon.py | 3 +- electrum/keystore.py | 17 +++--- electrum/mnemonic.py | 6 ++- electrum/tests/test_commands.py | 10 +++- run_electrum | 94 +++------------------------------ 6 files changed, 111 insertions(+), 108 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 9ce251f7..6bb18bb9 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -41,6 +41,10 @@ from .i18n import _ from .transaction import Transaction, multisig_script, TxOutput from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .synchronizer import Notifier +from .storage import WalletStorage +from . import keystore +from .wallet import Wallet, Imported_Wallet +from .mnemonic import Mnemonic known_commands = {} @@ -123,17 +127,73 @@ class Commands: return ' '.join(sorted(known_commands.keys())) @command('') - def create(self, segwit=False): + def create(self, passphrase=None, password=None, encrypt_file=True, segwit=False): """Create a new wallet""" - raise Exception('Not a JSON-RPC command') + storage = WalletStorage(self.config.get_wallet_path()) + if storage.file_exists(): + raise Exception("Remove the existing wallet first!") - @command('wn') - def restore(self, text): + seed_type = 'segwit' if segwit else 'standard' + seed = Mnemonic('en').make_seed(seed_type) + k = keystore.from_seed(seed, passphrase) + storage.put('keystore', k.dump()) + storage.put('wallet_type', 'standard') + wallet = Wallet(storage) + wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file) + wallet.synchronize() + msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet." + + wallet.storage.write() + return {'seed': seed, 'path': wallet.storage.path, 'msg': msg} + + @command('') + def restore(self, text, passphrase=None, password=None, encrypt_file=True): """Restore a wallet from text. Text can be a seed phrase, a master public key, a master private key, a list of bitcoin addresses or bitcoin private keys. If you want to be prompted for your seed, type '?' or ':' (concealed) """ - raise Exception('Not a JSON-RPC command') + storage = WalletStorage(self.config.get_wallet_path()) + if storage.file_exists(): + raise Exception("Remove the existing wallet first!") + + text = text.strip() + if keystore.is_address_list(text): + wallet = Imported_Wallet(storage) + for x in text.split(): + wallet.import_address(x) + elif keystore.is_private_key_list(text, allow_spaces_inside_key=False): + k = keystore.Imported_KeyStore({}) + storage.put('keystore', k.dump()) + wallet = Imported_Wallet(storage) + for x in text.split(): + wallet.import_private_key(x, password) + else: + if keystore.is_seed(text): + k = keystore.from_seed(text, passphrase) + elif keystore.is_master_key(text): + k = keystore.from_master_key(text) + else: + raise Exception("Seed or key not recognized") + storage.put('keystore', k.dump()) + storage.put('wallet_type', 'standard') + wallet = Wallet(storage) + + wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file) + wallet.synchronize() + + if self.network: + wallet.start_network(self.network) + print_error("Recovering wallet...") + wallet.wait_until_synchronized() + wallet.stop_threads() + # note: we don't wait for SPV + msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet" + else: + msg = ("This wallet was restored offline. It may contain more addresses than displayed. " + "Start a daemon (not offline) to sync history.") + + wallet.storage.write() + return {'path': wallet.storage.path, 'msg': msg} @command('wp') def password(self, password=None, new_password=None): @@ -419,7 +479,7 @@ class Commands: coins = self.wallet.get_spendable_coins(domain, self.config) tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr) - if locktime != None: + if locktime != None: tx.locktime = locktime if rbf is None: rbf = self.config.get('use_rbf', True) @@ -671,6 +731,16 @@ class Commands: # for the python console return sorted(known_commands.keys()) + +def eval_bool(x: str) -> bool: + if x == 'false': return False + if x == 'true': return True + try: + return bool(ast.literal_eval(x)) + except: + return bool(x) + + param_descriptions = { 'privkey': 'Private key. Type \'?\' to get a prompt.', 'destination': 'Bitcoin address, contact or alias', @@ -693,6 +763,7 @@ param_descriptions = { command_options = { 'password': ("-W", "Password"), 'new_password':(None, "New Password"), + 'encrypt_file':(None, "Whether the file on disk should be encrypted with the provided password"), 'receiving': (None, "Show only receiving addresses"), 'change': (None, "Show only change addresses"), 'frozen': (None, "Show only frozen addresses"), @@ -708,6 +779,7 @@ command_options = { 'nbits': (None, "Number of bits of entropy"), 'segwit': (None, "Create segwit seed"), 'language': ("-L", "Default language for wordlist"), + 'passphrase': (None, "Seed extension"), 'privkey': (None, "Private key. Set to '?' to get a prompt."), 'unsigned': ("-u", "Do not sign transaction"), 'rbf': (None, "Replace-by-fee transaction"), @@ -746,6 +818,7 @@ arg_types = { 'locktime': int, 'fee_method': str, 'fee_level': json_loads, + 'encrypt_file': eval_bool, } config_variables = { @@ -858,12 +931,10 @@ def get_parser(): cmd = known_commands[cmdname] p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description) add_global_options(p) - if cmdname == 'restore': - p.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline") for optname, default in zip(cmd.options, cmd.defaults): a, help = command_options[optname] b = '--' + optname - action = "store_true" if type(default) is bool else 'store' + action = "store_true" if default is False else 'store' args = (a, b) if a else (b,) if action == 'store': _type = arg_types.get(optname, str) diff --git a/electrum/daemon.py b/electrum/daemon.py index 0c0df6d7..70bd605a 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -170,7 +170,7 @@ class Daemon(DaemonThread): return True def run_daemon(self, config_options): - asyncio.set_event_loop(self.network.asyncio_loop) + asyncio.set_event_loop(self.network.asyncio_loop) # FIXME what if self.network is None? config = SimpleConfig(config_options) sub = config.get('subcommand') assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet'] @@ -264,6 +264,7 @@ class Daemon(DaemonThread): wallet.stop_threads() def run_cmdline(self, config_options): + asyncio.set_event_loop(self.network.asyncio_loop) # FIXME what if self.network is None? password = config_options.get('password') new_password = config_options.get('new_password') config = SimpleConfig(config_options) diff --git a/electrum/keystore.py b/electrum/keystore.py index 6769eefc..8eff2ee0 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -711,16 +711,19 @@ def is_address_list(text): return bool(parts) and all(bitcoin.is_address(x) for x in parts) -def get_private_keys(text): - parts = text.split('\n') - parts = map(lambda x: ''.join(x.split()), parts) - parts = list(filter(bool, parts)) +def get_private_keys(text, *, allow_spaces_inside_key=True): + if allow_spaces_inside_key: # see #1612 + parts = text.split('\n') + parts = map(lambda x: ''.join(x.split()), parts) + parts = list(filter(bool, parts)) + else: + parts = text.split() if bool(parts) and all(bitcoin.is_private_key(x) for x in parts): return parts -def is_private_key_list(text): - return bool(get_private_keys(text)) +def is_private_key_list(text, *, allow_spaces_inside_key=True): + return bool(get_private_keys(text, allow_spaces_inside_key=allow_spaces_inside_key)) is_mpk = lambda x: is_old_mpk(x) or is_xpub(x) @@ -746,7 +749,7 @@ def purpose48_derivation(account_id: int, xtype: str) -> str: return "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int) -def from_seed(seed, passphrase, is_p2sh): +def from_seed(seed, passphrase, is_p2sh=False): t = seed_type(seed) if t == 'old': keystore = Old_KeyStore({}) diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py index a22913ae..4b7fe1ab 100644 --- a/electrum/mnemonic.py +++ b/electrum/mnemonic.py @@ -113,9 +113,10 @@ filenames = { } - +# FIXME every time we instantiate this class, we read the wordlist from disk +# and store a new copy of it in memory class Mnemonic(object): - # Seed derivation no longer follows BIP39 + # Seed derivation does not follow BIP39 # Mnemonic phrase uses a hash based checksum, instead of a wordlist-dependent checksum def __init__(self, lang=None): @@ -129,6 +130,7 @@ class Mnemonic(object): def mnemonic_to_seed(self, mnemonic, passphrase): PBKDF2_ROUNDS = 2048 mnemonic = normalize_text(mnemonic) + passphrase = passphrase or '' passphrase = normalize_text(passphrase) return hashlib.pbkdf2_hmac('sha512', mnemonic.encode('utf-8'), b'electrum' + passphrase.encode('utf-8'), iterations = PBKDF2_ROUNDS) diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py index 6aa0ba71..a6d35c7b 100644 --- a/electrum/tests/test_commands.py +++ b/electrum/tests/test_commands.py @@ -1,7 +1,7 @@ import unittest from decimal import Decimal -from electrum.commands import Commands +from electrum.commands import Commands, eval_bool class TestCommands(unittest.TestCase): @@ -31,3 +31,11 @@ class TestCommands(unittest.TestCase): self.assertEqual("2asd", Commands._setconfig_normalize_value('rpcpassword', '2asd')) self.assertEqual("['file:///var/www/','https://electrum.org']", Commands._setconfig_normalize_value('rpcpassword', "['file:///var/www/','https://electrum.org']")) + + def test_eval_bool(self): + self.assertFalse(eval_bool("False")) + self.assertFalse(eval_bool("false")) + self.assertFalse(eval_bool("0")) + self.assertTrue(eval_bool("True")) + self.assertTrue(eval_bool("true")) + self.assertTrue(eval_bool("1")) diff --git a/run_electrum b/run_electrum index 9e9f1a9c..d814f1f2 100755 --- a/run_electrum +++ b/run_electrum @@ -65,18 +65,16 @@ if not is_android: check_imports() -from electrum import bitcoin, util +from electrum import util from electrum import constants -from electrum import SimpleConfig, Network -from electrum.wallet import Wallet, Imported_Wallet -from electrum import bitcoin, util, constants +from electrum import SimpleConfig +from electrum.wallet import Wallet from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled from electrum.util import set_verbosity, InvalidPassword from electrum.commands import get_parser, known_commands, Commands, config_variables from electrum import daemon from electrum import keystore -from electrum.mnemonic import Mnemonic # get password routine def prompt_password(prompt, confirm=True): @@ -91,80 +89,6 @@ def prompt_password(prompt, confirm=True): return password - -def run_non_RPC(config): - cmdname = config.get('cmd') - - storage = WalletStorage(config.get_wallet_path()) - if storage.file_exists(): - sys.exit("Error: Remove the existing wallet first!") - - def password_dialog(): - return prompt_password("Password (hit return if you do not wish to encrypt your wallet):") - - if cmdname == 'restore': - text = config.get('text').strip() - passphrase = config.get('passphrase', '') - password = password_dialog() if keystore.is_private(text) else None - if keystore.is_address_list(text): - wallet = Imported_Wallet(storage) - for x in text.split(): - wallet.import_address(x) - elif keystore.is_private_key_list(text): - k = keystore.Imported_KeyStore({}) - storage.put('keystore', k.dump()) - storage.put('use_encryption', bool(password)) - wallet = Imported_Wallet(storage) - for x in text.split(): - wallet.import_private_key(x, password) - storage.write() - else: - if keystore.is_seed(text): - k = keystore.from_seed(text, passphrase, False) - elif keystore.is_master_key(text): - k = keystore.from_master_key(text) - else: - sys.exit("Error: Seed or key not recognized") - if password: - k.update_password(None, password) - storage.put('keystore', k.dump()) - storage.put('wallet_type', 'standard') - storage.put('use_encryption', bool(password)) - storage.write() - wallet = Wallet(storage) - if not config.get('offline'): - network = Network(config) - network.start() - wallet.start_network(network) - print_msg("Recovering wallet...") - wallet.synchronize() - wallet.wait_until_synchronized() - wallet.stop_threads() - # note: we don't wait for SPV - msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet" - else: - msg = "This wallet was restored offline. It may contain more addresses than displayed." - print_msg(msg) - - elif cmdname == 'create': - password = password_dialog() - passphrase = config.get('passphrase', '') - seed_type = 'segwit' if config.get('segwit') else 'standard' - seed = Mnemonic('en').make_seed(seed_type) - k = keystore.from_seed(seed, passphrase, False) - storage.put('keystore', k.dump()) - storage.put('wallet_type', 'standard') - wallet = Wallet(storage) - wallet.update_password(None, password, True) - wallet.synchronize() - print_msg("Your wallet generation seed is:\n\"%s\"" % seed) - print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") - - wallet.storage.write() - print_msg("Wallet saved in '%s'" % wallet.storage.path) - sys.exit(0) - - def init_daemon(config_options): config = SimpleConfig(config_options) storage = WalletStorage(config.get_wallet_path()) @@ -233,14 +157,12 @@ def init_cmdline(config_options, server): else: password = None - config_options['password'] = password + config_options['password'] = config_options.get('password') or password if cmd.name == 'password': new_password = prompt_password('New password:') config_options['new_password'] = new_password - return cmd, password - def get_connected_hw_devices(plugins): support = plugins.get_hardware_support() @@ -297,7 +219,7 @@ def run_offline_command(config, config_options, plugins): # check password if cmd.requires_password and wallet.has_password(): try: - seed = wallet.check_password(password) + wallet.check_password(password) except InvalidPassword: print_msg("Error: This password does not decode this wallet.") sys.exit(1) @@ -320,6 +242,7 @@ def run_offline_command(config, config_options, plugins): wallet.storage.write() return result + def init_plugins(config, gui_name): from electrum.plugin import Plugins return Plugins(config, is_local or is_android, gui_name) @@ -406,11 +329,6 @@ if __name__ == '__main__': elif config.get('simnet'): constants.set_simnet() - # run non-RPC commands separately - if cmdname in ['create', 'restore']: - run_non_RPC(config) - sys.exit(0) - if cmdname == 'gui': fd, server = daemon.get_fd_or_server(config) if fd is not None: From 9ce3814d8b7163051142aa604d10f7bbad39fa2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=84=87=E5=B1=B1P?= Date: Fri, 12 Oct 2018 18:44:34 +0900 Subject: [PATCH 037/301] build-wine: update git version (#4769) --- contrib/build-wine/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build-wine/docker/Dockerfile b/contrib/build-wine/docker/Dockerfile index 2a2deff7..340bad00 100644 --- a/contrib/build-wine/docker/Dockerfile +++ b/contrib/build-wine/docker/Dockerfile @@ -20,7 +20,7 @@ RUN dpkg --add-architecture i386 && \ wine-stable-i386:i386=3.0.1~bionic \ wine-stable:amd64=3.0.1~bionic \ winehq-stable:amd64=3.0.1~bionic \ - git=1:2.17.1-1ubuntu0.1 \ + git=1:2.17.1-1ubuntu0.3 \ p7zip-full=16.02+dfsg-6 \ make=4.1-9.1ubuntu1 \ mingw-w64=5.0.3-1 \ From 372921b423a11af8ffc3de06f6d2da9e321c3c43 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 12 Oct 2018 16:09:41 +0200 Subject: [PATCH 038/301] mv NetworkJobOnDefaultServer to util break ref cycles --- electrum/network.py | 51 ---------------------------------------- electrum/synchronizer.py | 3 +-- electrum/util.py | 51 ++++++++++++++++++++++++++++++++++++++++ electrum/verifier.py | 3 +-- 4 files changed, 53 insertions(+), 55 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index c513eced..4900ccb3 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -883,54 +883,3 @@ class Network(PrintError): await self.interface.group.spawn(self._request_fee_estimates, self.interface) await asyncio.sleep(0.1) - - -class NetworkJobOnDefaultServer(PrintError): - """An abstract base class for a job that runs on the main network - interface. Every time the main interface changes, the job is - restarted, and some of its internals are reset. - """ - def __init__(self, network: Network): - asyncio.set_event_loop(network.asyncio_loop) - self.network = network - self.interface = None # type: Interface - self._restart_lock = asyncio.Lock() - self._reset() - asyncio.run_coroutine_threadsafe(self._restart(), network.asyncio_loop) - network.register_callback(self._restart, ['default_server_changed']) - - def _reset(self): - """Initialise fields. Called every time the underlying - server connection changes. - """ - self.group = SilentTaskGroup() - - async def _start(self, interface): - self.interface = interface - await interface.group.spawn(self._start_tasks) - - async def _start_tasks(self): - """Start tasks in self.group. Called every time the underlying - server connection changes. - """ - raise NotImplementedError() # implemented by subclasses - - async def stop(self): - await self.group.cancel_remaining() - - @aiosafe - async def _restart(self, *args): - interface = self.network.interface - if interface is None: - return # we should get called again soon - - async with self._restart_lock: - await self.stop() - self._reset() - await self._start(interface) - - @property - def session(self): - s = self.interface.session - assert s is not None - return s diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py index 12bbb18c..e5fd0f83 100644 --- a/electrum/synchronizer.py +++ b/electrum/synchronizer.py @@ -30,9 +30,8 @@ from collections import defaultdict from aiorpcx import TaskGroup, run_in_thread from .transaction import Transaction -from .util import bh2u, make_aiohttp_session +from .util import bh2u, make_aiohttp_session, NetworkJobOnDefaultServer from .bitcoin import address_to_scripthash -from .network import NetworkJobOnDefaultServer def history_status(h): diff --git a/electrum/util.py b/electrum/util.py index 9ce5e8aa..f17c23e5 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -906,3 +906,54 @@ class SilentTaskGroup(TaskGroup): if self._closed: raise asyncio.CancelledError() return super().spawn(*args, **kwargs) + + +class NetworkJobOnDefaultServer(PrintError): + """An abstract base class for a job that runs on the main network + interface. Every time the main interface changes, the job is + restarted, and some of its internals are reset. + """ + def __init__(self, network): + asyncio.set_event_loop(network.asyncio_loop) + self.network = network + self.interface = None + self._restart_lock = asyncio.Lock() + self._reset() + asyncio.run_coroutine_threadsafe(self._restart(), network.asyncio_loop) + network.register_callback(self._restart, ['default_server_changed']) + + def _reset(self): + """Initialise fields. Called every time the underlying + server connection changes. + """ + self.group = SilentTaskGroup() + + async def _start(self, interface): + self.interface = interface + await interface.group.spawn(self._start_tasks) + + async def _start_tasks(self): + """Start tasks in self.group. Called every time the underlying + server connection changes. + """ + raise NotImplementedError() # implemented by subclasses + + async def stop(self): + await self.group.cancel_remaining() + + @aiosafe + async def _restart(self, *args): + interface = self.network.interface + if interface is None: + return # we should get called again soon + + async with self._restart_lock: + await self.stop() + self._reset() + await self._start(interface) + + @property + def session(self): + s = self.interface.session + assert s is not None + return s diff --git a/electrum/verifier.py b/electrum/verifier.py index 05a6e446..3c070776 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -26,13 +26,12 @@ from typing import Sequence, Optional import aiorpcx -from .util import bh2u, VerifiedTxInfo +from .util import bh2u, VerifiedTxInfo, NetworkJobOnDefaultServer from .bitcoin import Hash, hash_decode, hash_encode from .transaction import Transaction from .blockchain import hash_header from .interface import GracefulDisconnect from . import constants -from .network import NetworkJobOnDefaultServer class MerkleVerificationFailure(Exception): pass From ab441a507abc160803bbb60e1b5d165f0de6289f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 12 Oct 2018 17:02:38 +0200 Subject: [PATCH 039/301] readme: use 'python3 -m pip install' to install --- README.rst | 4 ++-- electrum-env | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 42df1790..3f67724b 100644 --- a/README.rst +++ b/README.rst @@ -41,7 +41,7 @@ directory. To run Electrum from its root directory, just do:: You can also install Electrum on your system, by running this command:: sudo apt-get install python3-setuptools - pip3 install .[fast] + python3 -m pip install .[fast] This will download and install the Python dependencies used by Electrum, instead of using the 'packages' directory. @@ -64,7 +64,7 @@ Check out the code from GitHub:: Run install (this should install dependencies):: - pip3 install .[fast] + python3 -m pip install .[fast] Render the SVG icons to PNGs (optional):: diff --git a/electrum-env b/electrum-env index 71dfd595..177c69aa 100755 --- a/electrum-env +++ b/electrum-env @@ -17,7 +17,7 @@ if [ -e ./env/bin/activate ]; then else virtualenv env -p `which python3` source ./env/bin/activate - python3 setup.py install + python3 -m pip install .[fast] fi export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH" From e3b372946a6327f8786ae0e538692c817952a011 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 12 Oct 2018 18:29:59 +0200 Subject: [PATCH 040/301] rm aiosafe decorator. instead: log_exceptions and ignore_exceptions --- electrum/address_synchronizer.py | 2 +- electrum/exchange_rate.py | 6 ++--- electrum/interface.py | 8 +++--- electrum/network.py | 7 ++--- electrum/plugins/labels/labels.py | 8 +++--- electrum/util.py | 43 ++++++++++++++++++------------- 6 files changed, 41 insertions(+), 33 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index cd58feed..896e8f6c 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -28,7 +28,7 @@ from collections import defaultdict from . import bitcoin from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY -from .util import PrintError, profiler, bfh, VerifiedTxInfo, TxMinedStatus, aiosafe, SilentTaskGroup +from .util import PrintError, profiler, bfh, VerifiedTxInfo, TxMinedStatus from .transaction import Transaction, TxOutput from .synchronizer import Synchronizer from .verifier import SPV diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 826a8775..bfc9e692 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -14,7 +14,7 @@ from typing import Sequence from .bitcoin import COIN from .i18n import _ -from .util import PrintError, ThreadJob, make_dir, aiosafe +from .util import PrintError, ThreadJob, make_dir, log_exceptions from .util import make_aiohttp_session from .network import Network @@ -58,7 +58,7 @@ class ExchangeBase(PrintError): def name(self): return self.__class__.__name__ - @aiosafe + @log_exceptions async def update_safe(self, ccy): try: self.print_error("getting fx quotes for", ccy) @@ -89,7 +89,7 @@ class ExchangeBase(PrintError): self.on_history() return h - @aiosafe + @log_exceptions async def get_historical_rates_safe(self, ccy, cache_dir): try: self.print_error("requesting fx history for", ccy) diff --git a/electrum/interface.py b/electrum/interface.py index 3fa35527..2b5c4a6d 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -34,7 +34,7 @@ from collections import defaultdict import aiorpcx from aiorpcx import ClientSession, Notification -from .util import PrintError, aiosafe, bfh, AIOSafeSilentException, SilentTaskGroup +from .util import PrintError, ignore_exceptions, log_exceptions, bfh, SilentTaskGroup from . import util from . import x509 from . import pem @@ -146,9 +146,6 @@ class Interface(PrintError): self.tip_header = None self.tip = 0 - # note that an interface dying MUST NOT kill the whole network, - # hence exceptions raised by "run" need to be caught not to kill - # main_taskgroup! the aiosafe decorator does this. asyncio.run_coroutine_threadsafe( self.network.main_taskgroup.spawn(self.run()), self.network.asyncio_loop) self.group = SilentTaskGroup() @@ -249,7 +246,8 @@ class Interface(PrintError): self.got_disconnected.set_result(1) return wrapper_func - @aiosafe + @ignore_exceptions # do not kill main_taskgroup + @log_exceptions @handle_disconnect async def run(self): try: diff --git a/electrum/network.py b/electrum/network.py index 4900ccb3..9877907c 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -40,7 +40,7 @@ import dns.resolver from aiorpcx import TaskGroup from . import util -from .util import PrintError, print_error, aiosafe, bfh, SilentTaskGroup +from .util import PrintError, print_error, log_exceptions, ignore_exceptions, bfh, SilentTaskGroup from .bitcoin import COIN from . import constants from . import blockchain @@ -478,7 +478,7 @@ class Network(PrintError): addr = host return socket._getaddrinfo(addr, *args, **kwargs) - @aiosafe + @log_exceptions async def set_parameters(self, net_params: NetworkParameters): proxy = net_params.proxy proxy_str = serialize_proxy(proxy) @@ -619,7 +619,8 @@ class Network(PrintError): await self._close_interface(interface) self.trigger_callback('network_updated') - @aiosafe + @ignore_exceptions # do not kill main_taskgroup + @log_exceptions async def _run_new_interface(self, server): interface = Interface(self, server, self.config.path, self.proxy) timeout = 10 if not self.proxy else 20 diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py index 8ca63e6d..9405e70e 100644 --- a/electrum/plugins/labels/labels.py +++ b/electrum/plugins/labels/labels.py @@ -9,7 +9,7 @@ import base64 from electrum.plugin import BasePlugin, hook from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv from electrum.i18n import _ -from electrum.util import aiosafe, make_aiohttp_session +from electrum.util import log_exceptions, ignore_exceptions, make_aiohttp_session class LabelsPlugin(BasePlugin): @@ -58,7 +58,8 @@ class LabelsPlugin(BasePlugin): # Caller will write the wallet self.set_nonce(wallet, nonce + 1) - @aiosafe + @ignore_exceptions + @log_exceptions async def do_post_safe(self, *args): await self.do_post(*args) @@ -129,7 +130,8 @@ class LabelsPlugin(BasePlugin): self.set_nonce(wallet, response["nonce"] + 1) self.on_pulled(wallet) - @aiosafe + @ignore_exceptions + @log_exceptions async def pull_safe_thread(self, wallet, force): await self.pull_thread(wallet, force) diff --git a/electrum/util.py b/electrum/util.py index f17c23e5..9c85f16b 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -846,29 +846,36 @@ def make_dir(path, allow_symlink=True): os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) -class AIOSafeSilentException(Exception): pass - - -def aiosafe(f): - # save exception in object. - # f must be a method of a PrintError instance. - # aiosafe calls should not be nested - async def f2(*args, **kwargs): - self = args[0] +def log_exceptions(func): + """Decorator to log AND re-raise exceptions.""" + assert asyncio.iscoroutinefunction(func), 'func needs to be a coroutine' + async def wrapper(*args, **kwargs): + self = args[0] if len(args) > 0 else None try: - return await f(*args, **kwargs) - except AIOSafeSilentException as e: - self.exception = e + return await func(*args, **kwargs) except asyncio.CancelledError as e: - self.exception = e + raise except BaseException as e: - self.exception = e - self.print_error("Exception in", f.__name__, ":", e.__class__.__name__, str(e)) + print_ = self.print_error if hasattr(self, 'print_error') else print_error + print_("Exception in", func.__name__, ":", e.__class__.__name__, repr(e)) try: traceback.print_exc(file=sys.stderr) except BaseException as e2: - self.print_error("aiosafe:traceback.print_exc raised: {}... original exc: {}".format(e2, e)) - return f2 + print_error("traceback.print_exc raised: {}...".format(e2)) + raise + return wrapper + + +def ignore_exceptions(func): + """Decorator to silently swallow all exceptions.""" + assert asyncio.iscoroutinefunction(func), 'func needs to be a coroutine' + async def wrapper(*args, **kwargs): + try: + return await func(*args, **kwargs) + except BaseException as e: + pass + return wrapper + TxMinedStatus = NamedTuple("TxMinedStatus", [("height", int), ("conf", int), @@ -941,7 +948,7 @@ class NetworkJobOnDefaultServer(PrintError): async def stop(self): await self.group.cancel_remaining() - @aiosafe + @log_exceptions async def _restart(self, *args): interface = self.network.interface if interface is None: From 8fa6bd2aac4a289874cf29494081337796016055 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 12 Oct 2018 19:03:36 +0200 Subject: [PATCH 041/301] network: add_job --- electrum/network.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index 9877907c..78d43481 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -188,7 +188,6 @@ class Network(PrintError): self.default_server = pick_random_server() self.main_taskgroup = None - self._jobs = [] # locks self.restart_lock = asyncio.Lock() @@ -791,9 +790,7 @@ class Network(PrintError): with open(path, 'w', encoding='utf-8') as f: f.write(json.dumps(cp, indent=4)) - async def _start(self, jobs=None): - if jobs is None: jobs = self._jobs - self._jobs = jobs + async def _start(self): assert not self.main_taskgroup self.main_taskgroup = SilentTaskGroup() @@ -802,7 +799,7 @@ class Network(PrintError): await self._init_headers_file() async with self.main_taskgroup as group: await group.spawn(self._maintain_sessions()) - [await group.spawn(job) for job in jobs] + [await group.spawn(job) for job in self._jobs] except Exception as e: traceback.print_exc(file=sys.stderr) raise e @@ -818,8 +815,14 @@ class Network(PrintError): self._start_interface(self.default_server) self.trigger_callback('network_updated') - def start(self, jobs=None): - asyncio.run_coroutine_threadsafe(self._start(jobs=jobs), self.asyncio_loop) + def start(self, jobs: List=None): + self._jobs = jobs or [] + asyncio.run_coroutine_threadsafe(self._start(), self.asyncio_loop) + + async def add_job(self, job): + async with self.restart_lock: + self._jobs.append(job) + await self.main_taskgroup.spawn(job) async def _stop(self, full_shutdown=False): self.print_error("stopping network") From 5afdc14913c464643efe1bf65763e8285d5ac4b6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 13 Oct 2018 04:21:07 +0200 Subject: [PATCH 042/301] util: small clean-up re format_satoshis related #4771 --- electrum/gui/qt/main_window.py | 2 +- electrum/tests/test_util.py | 58 ++++++++++++---------------------- electrum/util.py | 7 ++-- 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 3308ccf7..5be84bec 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -688,7 +688,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): return text def format_fee_rate(self, fee_rate): - return format_fee_satoshis(fee_rate/1000, self.num_zeros) + ' sat/byte' + return format_fee_satoshis(fee_rate/1000, num_zeros=self.num_zeros) + ' sat/byte' def get_decimal_point(self): return self.decimal_point diff --git a/electrum/tests/test_util.py b/electrum/tests/test_util.py index a88bd8c3..b9f3d7e4 100644 --- a/electrum/tests/test_util.py +++ b/electrum/tests/test_util.py @@ -1,4 +1,6 @@ -from electrum.util import format_satoshis, parse_URI +from decimal import Decimal + +from electrum.util import format_satoshis, format_fee_satoshis, parse_URI from . import SequentialTestCase @@ -6,56 +8,38 @@ from . import SequentialTestCase class TestUtil(SequentialTestCase): def test_format_satoshis(self): - result = format_satoshis(1234) - expected = "0.00001234" - self.assertEqual(expected, result) + self.assertEqual("0.00001234", format_satoshis(1234)) def test_format_satoshis_negative(self): - result = format_satoshis(-1234) - expected = "-0.00001234" - self.assertEqual(expected, result) + self.assertEqual("-0.00001234", format_satoshis(-1234)) def test_format_fee(self): - result = format_satoshis(1700/1000, 0, 0) - expected = "1.7" - self.assertEqual(expected, result) + self.assertEqual("1.7", format_fee_satoshis(1700/1000)) def test_format_fee_precision(self): - result = format_satoshis(1666/1000, 0, 0, precision=6) - expected = "1.666" - self.assertEqual(expected, result) - - result = format_satoshis(1666/1000, 0, 0, precision=1) - expected = "1.7" - self.assertEqual(expected, result) + self.assertEqual("1.666", + format_fee_satoshis(1666/1000, precision=6)) + self.assertEqual("1.7", + format_fee_satoshis(1666/1000, precision=1)) def test_format_satoshis_whitespaces(self): - result = format_satoshis(12340, whitespaces=True) - expected = " 0.0001234 " - self.assertEqual(expected, result) - - result = format_satoshis(1234, whitespaces=True) - expected = " 0.00001234" - self.assertEqual(expected, result) + self.assertEqual(" 0.0001234 ", + format_satoshis(12340, whitespaces=True)) + self.assertEqual(" 0.00001234", + format_satoshis(1234, whitespaces=True)) def test_format_satoshis_whitespaces_negative(self): - result = format_satoshis(-12340, whitespaces=True) - expected = " -0.0001234 " - self.assertEqual(expected, result) - - result = format_satoshis(-1234, whitespaces=True) - expected = " -0.00001234" - self.assertEqual(expected, result) + self.assertEqual(" -0.0001234 ", + format_satoshis(-12340, whitespaces=True)) + self.assertEqual(" -0.00001234", + format_satoshis(-1234, whitespaces=True)) def test_format_satoshis_diff_positive(self): - result = format_satoshis(1234, is_diff=True) - expected = "+0.00001234" - self.assertEqual(expected, result) + self.assertEqual("+0.00001234", + format_satoshis(1234, is_diff=True)) def test_format_satoshis_diff_negative(self): - result = format_satoshis(-1234, is_diff=True) - expected = "-0.00001234" - self.assertEqual(expected, result) + self.assertEqual("-0.00001234", format_satoshis(-1234, is_diff=True)) def _do_test_parse_URI(self, uri, expected): result = parse_URI(uri) diff --git a/electrum/util.py b/electrum/util.py index 9c85f16b..e0f8ff6f 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -540,8 +540,11 @@ FEERATE_PRECISION = 1 # num fractional decimal places for sat/byte fee rates _feerate_quanta = Decimal(10) ** (-FEERATE_PRECISION) -def format_fee_satoshis(fee, num_zeros=0): - return format_satoshis(fee, num_zeros, 0, precision=FEERATE_PRECISION) +def format_fee_satoshis(fee, *, num_zeros=0, precision=None): + if precision is None: + precision = FEERATE_PRECISION + num_zeros = min(num_zeros, FEERATE_PRECISION) # no more zeroes than available prec + return format_satoshis(fee, num_zeros=num_zeros, decimal_point=0, precision=precision) def quantize_feerate(fee): From 7c4d6c68018beb16a2ec8d5def26276e50582b5b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 13 Oct 2018 04:22:53 +0200 Subject: [PATCH 043/301] fix #4771 --- electrum/tests/test_util.py | 5 ++++- electrum/util.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/tests/test_util.py b/electrum/tests/test_util.py index b9f3d7e4..b9436c7f 100644 --- a/electrum/tests/test_util.py +++ b/electrum/tests/test_util.py @@ -13,9 +13,12 @@ class TestUtil(SequentialTestCase): def test_format_satoshis_negative(self): self.assertEqual("-0.00001234", format_satoshis(-1234)) - def test_format_fee(self): + def test_format_fee_float(self): self.assertEqual("1.7", format_fee_satoshis(1700/1000)) + def test_format_fee_decimal(self): + self.assertEqual("1.7", format_fee_satoshis(Decimal("1.7"))) + def test_format_fee_precision(self): self.assertEqual("1.666", format_fee_satoshis(1666/1000, precision=6)) diff --git a/electrum/util.py b/electrum/util.py index e0f8ff6f..fd4494a8 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -514,7 +514,7 @@ def format_satoshis(x, num_zeros=0, decimal_point=8, precision=None, is_diff=Fal if precision is None: precision = decimal_point # format string - decimal_format = ".0" + str(precision) if precision > 0 else "" + decimal_format = "." + str(precision) if precision > 0 else "" if is_diff: decimal_format = '+' + decimal_format # initial result From 1af225015a19e0697f3ca31535d74f6f646ea7ac Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 13 Oct 2018 05:16:36 +0200 Subject: [PATCH 044/301] fix some type annotations involving tuples --- electrum/bitcoin.py | 4 ++-- electrum/ecc.py | 8 +++----- electrum/gui/qt/installwizard.py | 3 ++- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index df05fdd0..220d2870 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -24,7 +24,7 @@ # SOFTWARE. import hashlib -from typing import List +from typing import List, Tuple from .util import bfh, bh2u, BitcoinException, print_error, assert_bytes, to_bytes, inv_dict from . import version @@ -433,7 +433,7 @@ def serialize_privkey(secret: bytes, compressed: bool, txin_type: str, return '{}:{}'.format(txin_type, base58_wif) -def deserialize_privkey(key: str) -> (str, bytes, bool): +def deserialize_privkey(key: str) -> Tuple[str, bytes, bool]: if is_minikey(key): return 'p2pkh', minikey_to_private_key(key), False diff --git a/electrum/ecc.py b/electrum/ecc.py index 9c10e3b0..c5e77278 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -24,10 +24,8 @@ # SOFTWARE. import base64 -import hmac import hashlib -from typing import Union - +from typing import Union, Tuple import ecdsa from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1 @@ -110,7 +108,7 @@ def get_y_coord_from_x(x: int, odd: bool=True) -> int: raise InvalidECPointException() -def ser_to_point(ser: bytes) -> (int, int): +def ser_to_point(ser: bytes) -> Tuple[int, int]: if ser[0] not in (0x02, 0x03, 0x04): raise ValueError('Unexpected first byte: {}'.format(ser[0])) if ser[0] == 0x04: @@ -227,7 +225,7 @@ class ECPubkey(object): def get_public_key_hex(self, compressed=True): return bh2u(self.get_public_key_bytes(compressed)) - def point(self) -> (int, int): + def point(self) -> Tuple[int, int]: return self._pubkey.point.x(), self._pubkey.point.y() def __mul__(self, other: int): diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index 8d201663..58583a7a 100644 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -3,6 +3,7 @@ import os import sys import threading import traceback +from typing import Tuple from PyQt5.QtCore import * from PyQt5.QtGui import * @@ -506,7 +507,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): @wizard_dialog def choice_and_line_dialog(self, title, message1, choices, message2, - test_text, run_next) -> (str, str): + test_text, run_next) -> Tuple[str, str]: vbox = QVBoxLayout() c_values = [x[0] for x in choices] From 0e59bc1bc589222d00d53de3e2220ec06784a421 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 14 Oct 2018 04:23:10 +0200 Subject: [PATCH 045/301] network: "switch unwanted fork" should check what fork we are on.. follow-up #4767 --- electrum/network.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/network.py b/electrum/network.py index 78d43481..3e14b5fc 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -533,13 +533,15 @@ class Network(PrintError): """If auto_connect and main interface is not on preferred fork, try to switch to preferred fork. """ - if not self.auto_connect: + if not self.auto_connect or not self.interface: return with self.interfaces_lock: interfaces = list(self.interfaces.values()) # try to switch to preferred fork if self._blockchain_preferred_block: pref_height = self._blockchain_preferred_block['height'] pref_hash = self._blockchain_preferred_block['hash'] + if self.interface.blockchain.check_hash(pref_height, pref_hash): + return # already on preferred fork filtered = list(filter(lambda iface: iface.blockchain.check_hash(pref_height, pref_hash), interfaces)) if filtered: From 60f8cf665eb167ae315645c849fee81596d455e8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 14 Oct 2018 04:23:42 +0200 Subject: [PATCH 046/301] dnssec: trivial clean-up --- electrum/dnssec.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/electrum/dnssec.py b/electrum/dnssec.py index 6a8ac980..03d860d9 100644 --- a/electrum/dnssec.py +++ b/electrum/dnssec.py @@ -110,11 +110,9 @@ def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None): if rrsig.algorithm == ECDSAP256SHA256: curve = ecdsa.curves.NIST256p key_len = 32 - digest_len = 32 elif rrsig.algorithm == ECDSAP384SHA384: curve = ecdsa.curves.NIST384p key_len = 48 - digest_len = 48 else: # shouldn't happen raise ValidationFailure('unknown ECDSA curve') @@ -141,7 +139,7 @@ def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None): rrnamebuf = rrname.to_digestable(origin) rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass, rrsig.original_ttl) - rrlist = sorted(rdataset); + rrlist = sorted(rdataset) for rr in rrlist: hash.update(rrnamebuf) hash.update(rrfixed) From 2cc77c1c7dc45e07de7191409cd21a03895a695e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 14 Oct 2018 05:13:56 +0200 Subject: [PATCH 047/301] rm system config sample system-level configs are no longer supported since 04a1809969a24f7fc176aa505b7fed807e55ad4e --- electrum.conf.sample | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 electrum.conf.sample diff --git a/electrum.conf.sample b/electrum.conf.sample deleted file mode 100644 index 72a50b23..00000000 --- a/electrum.conf.sample +++ /dev/null @@ -1,16 +0,0 @@ -# Configuration file for the Electrum client -# Settings defined here are shared across wallets -# -# copy this file to /etc/electrum.conf if you want read-only settings - -[client] -server = electrum.novit.ro:50001:t -proxy = None -gap_limit = 5 -# booleans use python syntax -use_change = True -gui = qt -num_zeros = 2 -# default transaction fee is in Satoshis -fee = 10000 -winpos-qt = [799, 226, 877, 435] From 1526fd3722fa84a7f71f26fdfccc8b483ed9797d Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Sun, 14 Oct 2018 15:12:25 +0300 Subject: [PATCH 048/301] Removal of macOS Info.plist. It isn't being used by anything. (#4773) --- Info.plist | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 Info.plist diff --git a/Info.plist b/Info.plist deleted file mode 100644 index a8f58f73..00000000 --- a/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleURLTypes - - - CFBundleURLName - bitcoin - CFBundleURLSchemes - - bitcoin - - - - LSArchitecturePriority - - x86_64 - i386 - - - From 14b4955a6f21b466b612a0eb12da568100420d63 Mon Sep 17 00:00:00 2001 From: "Peter D. Gray" Date: Thu, 18 Oct 2018 12:38:47 -0400 Subject: [PATCH 049/301] Fix p2wpkh-p2sh support per issue #4729 --- electrum/plugins/coldcard/coldcard.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index 5327aa5e..e38012c1 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -423,13 +423,10 @@ class Coldcard_KeyStore(Hardware_KeyStore): for txin in inputs: if txin['type'] == 'coinbase': - self.give_error("Coinbase not supported") # but why not? + self.give_error("Coinbase not supported") - if txin['type'] in ['p2sh']: - self.give_error('Not ready for multisig transactions yet') - - #if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']: - #if txin['type'] in ['p2wpkh', 'p2wsh']: + if txin['type'] in ['p2sh', 'p2wsh-p2sh', 'p2wsh']: + self.give_error('No support yet for inputs of type: ' + txin['type']) # Construct PSBT from start to finish. out_fd = io.BytesIO() @@ -452,6 +449,7 @@ class Coldcard_KeyStore(Hardware_KeyStore): @classmethod def input_script(cls, txin, estimate_size=False): return '' + unsigned = bfh(CustomTXSerialization(tx.serialize()).serialize_to_network(witness=False)) write_kv(PSBT_GLOBAL_UNSIGNED_TX, unsigned) @@ -471,6 +469,12 @@ class Coldcard_KeyStore(Hardware_KeyStore): for k in pubkeys: write_kv(PSBT_IN_BIP32_DERIVATION, subkeys[k], k) + if txin['type'] == 'p2wpkh-p2sh': + assert len(pubkeys) == 1, 'can be only one redeem script per input' + pa = hash_160(k) + assert len(pa) == 20 + write_kv(PSBT_IN_REDEEM_SCRIPT, b'\x00\x14'+pa) + out_fd.write(b'\x00') # outputs section @@ -595,7 +599,7 @@ class ColdcardPlugin(HW_PluginBase): ] #SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') - SUPPORTED_XTYPES = ('standard', 'p2wpkh') + SUPPORTED_XTYPES = ('standard', 'p2wpkh', 'p2wpkh-p2sh') def __init__(self, parent, config, name): HW_PluginBase.__init__(self, parent, config, name) From e8bc025f5cbcd82590800bcbd195861a45d42edd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 19 Oct 2018 18:10:04 +0200 Subject: [PATCH 050/301] verifier: fix race in __init__ --- electrum/synchronizer.py | 2 +- electrum/verifier.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py index e5fd0f83..90160ae9 100644 --- a/electrum/synchronizer.py +++ b/electrum/synchronizer.py @@ -48,8 +48,8 @@ class SynchronizerBase(NetworkJobOnDefaultServer): Every time a status changes, run a coroutine provided by the subclass. """ def __init__(self, network): - NetworkJobOnDefaultServer.__init__(self, network) self.asyncio_loop = network.asyncio_loop + NetworkJobOnDefaultServer.__init__(self, network) def _reset(self): super()._reset() diff --git a/electrum/verifier.py b/electrum/verifier.py index 3c070776..c6648c7c 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -44,8 +44,8 @@ class SPV(NetworkJobOnDefaultServer): """ Simple Payment Verification """ def __init__(self, network, wallet): - NetworkJobOnDefaultServer.__init__(self, network) self.wallet = wallet + NetworkJobOnDefaultServer.__init__(self, network) def _reset(self): super()._reset() From 10a4c7a6ed929f108e347350cb8a3fbaf928c59b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 19 Oct 2018 20:48:48 +0200 Subject: [PATCH 051/301] wallet.mktx: add new args: rbf, nonlocal_only used on lightning branch --- electrum/address_synchronizer.py | 4 +++- electrum/wallet.py | 14 ++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 896e8f6c..a8eab38b 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -768,7 +768,7 @@ class AddressSynchronizer(PrintError): return c, u, x @with_local_height_cached - def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False): + def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False, nonlocal_only=False): coins = [] if domain is None: domain = self.get_addresses() @@ -780,6 +780,8 @@ class AddressSynchronizer(PrintError): for x in utxos.values(): if confirmed_only and x['height'] <= 0: continue + if nonlocal_only and x['height'] == TX_HEIGHT_LOCAL: + continue if mature and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height(): continue coins.append(x) diff --git a/electrum/wallet.py b/electrum/wallet.py index 756b98a7..4b0e87aa 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -363,9 +363,13 @@ class Abstract_Wallet(AddressSynchronizer): return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n - def get_spendable_coins(self, domain, config): + def get_spendable_coins(self, domain, config, *, nonlocal_only=False): confirmed_only = config.get('confirmed_only', False) - return self.get_utxos(domain, excluded=self.frozen_addresses, mature=True, confirmed_only=confirmed_only) + return self.get_utxos(domain, + excluded=self.frozen_addresses, + mature=True, + confirmed_only=confirmed_only, + nonlocal_only=nonlocal_only) def dummy_address(self): return self.get_receiving_addresses()[0] @@ -612,9 +616,11 @@ class Abstract_Wallet(AddressSynchronizer): run_hook('make_unsigned_transaction', self, tx) return tx - def mktx(self, outputs, password, config, fee=None, change_addr=None, domain=None): - coins = self.get_spendable_coins(domain, config) + def mktx(self, outputs, password, config, fee=None, change_addr=None, + domain=None, rbf=False, nonlocal_only=False): + coins = self.get_spendable_coins(domain, config, nonlocal_only=nonlocal_only) tx = self.make_unsigned_transaction(coins, outputs, config, fee, change_addr) + tx.set_rbf(rbf) self.sign_transaction(tx, password) return tx From bf18e2bbc9d469fead4701ce154ebaabbe2e8ee5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 19 Oct 2018 22:23:50 +0200 Subject: [PATCH 052/301] follow-up prev --- electrum/plugins/coldcard/coldcard.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index e38012c1..902d6f2a 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -31,7 +31,7 @@ try: from ckcc.constants import ( PSBT_GLOBAL_UNSIGNED_TX, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO, PSBT_IN_SIGHASH_TYPE, PSBT_IN_REDEEM_SCRIPT, PSBT_IN_WITNESS_SCRIPT, - PSBT_IN_BIP32_DERIVATION, PSBT_OUT_BIP32_DERIVATION) + PSBT_IN_BIP32_DERIVATION, PSBT_OUT_BIP32_DERIVATION, PSBT_OUT_REDEEM_SCRIPT) from ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID, CKCC_SIMULATOR_PATH @@ -497,9 +497,14 @@ class Coldcard_KeyStore(Hardware_KeyStore): assert 0 <= bb < 0x80000000 deriv = base_path + pack(' Date: Sat, 20 Oct 2018 23:17:10 +0200 Subject: [PATCH 053/301] network.stop: fix await --- electrum/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/network.py b/electrum/network.py index 3e14b5fc..3ea21c0f 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -829,7 +829,7 @@ class Network(PrintError): async def _stop(self, full_shutdown=False): self.print_error("stopping network") try: - asyncio.wait_for(await self.main_taskgroup.cancel_remaining(), timeout=2) + await asyncio.wait_for(self.main_taskgroup.cancel_remaining(), timeout=2) except asyncio.TimeoutError: pass self.main_taskgroup = None From ef2a6359e4521cef4302486a2b9844814938babf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 21 Oct 2018 03:09:47 +0200 Subject: [PATCH 054/301] fix SSL log spam on py3.7 based on kyuupichan/electrumx@83813ff1ac71da6030f7181d3c8fe961491a51f3 see pooler/electrum-ltc#191 --- electrum/network.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/electrum/network.py b/electrum/network.py index 3ea21c0f..d4623688 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -221,6 +221,7 @@ class Network(PrintError): self.proxy = None self.asyncio_loop = asyncio.get_event_loop() + self.asyncio_loop.set_exception_handler(self.on_event_loop_exception) #self.asyncio_loop.set_debug(1) self._run_forever = asyncio.Future() self._thread = threading.Thread(target=self.asyncio_loop.run_until_complete, @@ -237,6 +238,15 @@ class Network(PrintError): def get_instance(): return INSTANCE + def on_event_loop_exception(self, loop, context): + """Suppress spurious messages it appears we cannot control.""" + SUPPRESS_MESSAGE_REGEX = re.compile('SSL handshake|Fatal read error on|' + 'SSL error in data received') + message = context.get('message') + if message and SUPPRESS_MESSAGE_REGEX.match(message): + return + loop.default_exception_handler(context) + def with_recent_servers_lock(func): def func_wrapper(self, *args, **kwargs): with self.recent_servers_lock: From 6958c0ccc3d97edfb7f8504f27270182cb3abcfa Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 21 Oct 2018 14:58:55 +0200 Subject: [PATCH 055/301] config: reject non-json-serialisable writes see #4788 --- electrum/simple_config.py | 6 ++++++ electrum/storage.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index f637c529..bd12d789 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -140,6 +140,12 @@ class SimpleConfig(PrintError): if not self.is_modifiable(key): self.print_stderr("Warning: not changing config key '%s' set on the command line" % key) return + try: + json.dumps(key) + json.dumps(value) + except: + self.print_error(f"json error: cannot save {repr(key)} ({repr(value)})") + return self._set_key_in_user_config(key, value, save) def _set_key_in_user_config(self, key, value, save=True): diff --git a/electrum/storage.py b/electrum/storage.py index ac46b059..22b4fb61 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -90,7 +90,7 @@ class JsonDB(PrintError): json.dumps(key, cls=util.MyEncoder) json.dumps(value, cls=util.MyEncoder) except: - self.print_error("json error: cannot save", key) + self.print_error(f"json error: cannot save {repr(key)} ({repr(value)})") return with self.db_lock: if value is not None: From 81cc20039ea5d45204c00b75b283ea877029fbb0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 22 Oct 2018 16:41:25 +0200 Subject: [PATCH 056/301] more type annotations in core lib --- electrum/address_synchronizer.py | 10 ++++++++-- electrum/base_wizard.py | 13 +++++++++---- electrum/blockchain.py | 5 +++-- electrum/commands.py | 9 +++++++-- electrum/daemon.py | 18 +++++++++--------- electrum/exchange_rate.py | 4 +++- electrum/interface.py | 7 +++++-- electrum/network.py | 4 ++-- electrum/plugin.py | 8 +++++--- electrum/synchronizer.py | 10 +++++++--- electrum/util.py | 12 ++++++++---- electrum/verifier.py | 8 ++++++-- electrum/wallet.py | 20 ++++++++++---------- electrum/websockets.py | 11 ++++++++--- 14 files changed, 90 insertions(+), 49 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index a8eab38b..d39828cf 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -34,6 +34,9 @@ from .synchronizer import Synchronizer from .verifier import SPV from .blockchain import hash_header from .i18n import _ +from .storage import WalletStorage +from .network import Network + TX_HEIGHT_LOCAL = -2 TX_HEIGHT_UNCONF_PARENT = -1 @@ -53,9 +56,9 @@ class AddressSynchronizer(PrintError): inherited by wallet """ - def __init__(self, storage): + def __init__(self, storage: WalletStorage): self.storage = storage - self.network = None + self.network = None # type: Network # verifier (SPV) and synchronizer are started in start_network self.synchronizer = None # type: Synchronizer self.verifier = None # type: SPV @@ -807,3 +810,6 @@ class AddressSynchronizer(PrintError): def is_empty(self, address): c, u, x = self.get_addr_balance(address) return c+u+x == 0 + + def synchronize(self): + pass diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 0e2d45c6..fe91cb4b 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -31,10 +31,15 @@ from functools import partial from . import bitcoin from . import keystore from .keystore import bip44_derivation, purpose48_derivation -from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet -from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption +from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet, + wallet_types, Wallet, Abstract_Wallet) +from .storage import (WalletStorage, STO_EV_USER_PW, STO_EV_XPUB_PW, + get_derivation_used_for_hw_device_encryption) from .i18n import _ from .util import UserCancelled, InvalidPassword, WalletFileException +from .simple_config import SimpleConfig +from .plugin import Plugins + # hardware device setup purpose HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2) @@ -48,12 +53,12 @@ class GoBack(Exception): pass class BaseWizard(object): - def __init__(self, config, plugins, storage): + def __init__(self, config: SimpleConfig, plugins: Plugins, storage: WalletStorage): super(BaseWizard, self).__init__() self.config = config self.plugins = plugins self.storage = storage - self.wallet = None + self.wallet = None # type: Abstract_Wallet self.stack = [] self.plugin = None self.keystores = [] diff --git a/electrum/blockchain.py b/electrum/blockchain.py index da1a6ecb..ccc04138 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -28,6 +28,7 @@ from . import util from .bitcoin import Hash, hash_encode, int_to_hex, rev_hex from . import constants from .util import bfh, bh2u +from .simple_config import SimpleConfig HEADER_SIZE = 80 # bytes @@ -77,7 +78,7 @@ blockchains = {} # type: Dict[int, Blockchain] blockchains_lock = threading.Lock() -def read_blockchains(config): +def read_blockchains(config: 'SimpleConfig') -> Dict[int, 'Blockchain']: blockchains[0] = Blockchain(config, 0, None) fdir = os.path.join(util.get_headers_dir(config), 'forks') util.make_dir(fdir) @@ -100,7 +101,7 @@ class Blockchain(util.PrintError): Manages blockchain headers and their verification """ - def __init__(self, config, forkpoint: int, parent_id: Optional[int]): + def __init__(self, config: SimpleConfig, forkpoint: int, parent_id: Optional[int]): self.config = config self.forkpoint = forkpoint self.checkpoints = constants.net.CHECKPOINTS diff --git a/electrum/commands.py b/electrum/commands.py index 6bb18bb9..d7550ba2 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -32,6 +32,7 @@ import ast import base64 from functools import wraps from decimal import Decimal +from typing import Optional from .import util, ecc from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_encode @@ -43,8 +44,11 @@ from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .synchronizer import Notifier from .storage import WalletStorage from . import keystore -from .wallet import Wallet, Imported_Wallet +from .wallet import Wallet, Imported_Wallet, Abstract_Wallet from .mnemonic import Mnemonic +from .network import Network +from .simple_config import SimpleConfig + known_commands = {} @@ -95,7 +99,8 @@ def command(s): class Commands: - def __init__(self, config, wallet, network, callback = None): + def __init__(self, config: 'SimpleConfig', wallet: Abstract_Wallet, + network: Optional['Network'], callback=None): self.config = config self.wallet = wallet self.network = network diff --git a/electrum/daemon.py b/electrum/daemon.py index 70bd605a..e1fa82d7 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -29,7 +29,7 @@ import time import traceback import sys import threading -from typing import Dict +from typing import Dict, Optional, Tuple import jsonrpclib @@ -46,7 +46,7 @@ from .exchange_rate import FxThread from .plugin import run_hook -def get_lockfile(config): +def get_lockfile(config: SimpleConfig): return os.path.join(config.path, 'daemon') @@ -54,7 +54,7 @@ def remove_lockfile(lockfile): os.unlink(lockfile) -def get_fd_or_server(config): +def get_fd_or_server(config: SimpleConfig): '''Tries to create the lockfile, using O_EXCL to prevent races. If it succeeds it returns the FD. Otherwise try and connect to the server specified in the lockfile. @@ -73,7 +73,7 @@ def get_fd_or_server(config): remove_lockfile(lockfile) -def get_server(config): +def get_server(config: SimpleConfig) -> Optional[jsonrpclib.Server]: lockfile = get_lockfile(config) while True: create_time = None @@ -99,7 +99,7 @@ def get_server(config): time.sleep(1.0) -def get_rpc_credentials(config): +def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]: rpc_user = config.get('rpcuser', None) rpc_password = config.get('rpcpassword', None) if rpc_user is None or rpc_password is None: @@ -121,7 +121,7 @@ def get_rpc_credentials(config): class Daemon(DaemonThread): - def __init__(self, config, fd=None, *, listen_jsonrpc=True): + def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True): DaemonThread.__init__(self) self.config = config if fd is None and listen_jsonrpc: @@ -142,7 +142,7 @@ class Daemon(DaemonThread): self.init_server(config, fd) self.start() - def init_server(self, config, fd): + def init_server(self, config: SimpleConfig, fd): host = config.get('rpchost', '127.0.0.1') port = config.get('rpcport', 0) rpc_user, rpc_password = get_rpc_credentials(config) @@ -230,7 +230,7 @@ class Daemon(DaemonThread): response = "Error: Electrum is running in daemon mode. Please stop the daemon first." return response - def load_wallet(self, path, password): + def load_wallet(self, path, password) -> Optional[Abstract_Wallet]: # wizard will be launched if we return if path in self.wallets: wallet = self.wallets[path] @@ -251,7 +251,7 @@ class Daemon(DaemonThread): self.wallets[path] = wallet return wallet - def add_wallet(self, wallet): + def add_wallet(self, wallet: Abstract_Wallet): path = wallet.storage.path self.wallets[path] = wallet diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index bfc9e692..ae57cab7 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -17,6 +17,8 @@ from .i18n import _ from .util import PrintError, ThreadJob, make_dir, log_exceptions from .util import make_aiohttp_session from .network import Network +from .simple_config import SimpleConfig + # See https://en.wikipedia.org/wiki/ISO_4217 CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, @@ -434,7 +436,7 @@ def get_exchanges_by_ccy(history=True): class FxThread(ThreadJob): - def __init__(self, config, network): + def __init__(self, config: SimpleConfig, network: Network): self.config = config self.network = network if self.network: diff --git a/electrum/interface.py b/electrum/interface.py index 2b5c4a6d..491f60b6 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -28,7 +28,7 @@ import ssl import sys import traceback import asyncio -from typing import Tuple, Union, List +from typing import Tuple, Union, List, TYPE_CHECKING from collections import defaultdict import aiorpcx @@ -43,6 +43,9 @@ from . import blockchain from .blockchain import Blockchain from . import constants +if TYPE_CHECKING: + from .network import Network + class NotificationSession(ClientSession): @@ -129,7 +132,7 @@ def serialize_server(host: str, port: Union[str, int], protocol: str) -> str: class Interface(PrintError): - def __init__(self, network, server, config_path, proxy): + def __init__(self, network: 'Network', server: str, config_path, proxy: dict): self.ready = asyncio.Future() self.got_disconnected = asyncio.Future() self.server = server diff --git a/electrum/network.py b/electrum/network.py index d4623688..9be6813b 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -164,12 +164,12 @@ class Network(PrintError): """ verbosity_filter = 'n' - def __init__(self, config=None): + def __init__(self, config: SimpleConfig=None): global INSTANCE INSTANCE = self if config is None: config = {} # Do not use mutables as default values! - self.config = SimpleConfig(config) if isinstance(config, dict) else config + self.config = SimpleConfig(config) if isinstance(config, dict) else config # type: SimpleConfig self.num_server = 10 if not self.config.get('oneserver') else 0 blockchain.blockchains = blockchain.read_blockchains(self.config) self.print_error("blockchains", list(blockchain.blockchains)) diff --git a/electrum/plugin.py b/electrum/plugin.py index a69082a0..67a5c4e9 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -30,11 +30,13 @@ import pkgutil import time import threading -from .util import print_error from .i18n import _ -from .util import profiler, PrintError, DaemonThread, UserCancelled, ThreadJob +from .util import (profiler, PrintError, DaemonThread, UserCancelled, + ThreadJob, print_error) from . import bitcoin from . import plugins +from .simple_config import SimpleConfig + plugin_loaders = {} hook_names = set() @@ -45,7 +47,7 @@ class Plugins(DaemonThread): verbosity_filter = 'p' @profiler - def __init__(self, config, is_local, gui_name): + def __init__(self, config: SimpleConfig, is_local, gui_name): DaemonThread.__init__(self) self.setName('Plugins') self.pkgpath = os.path.dirname(plugins.__file__) diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py index 90160ae9..0a6ed687 100644 --- a/electrum/synchronizer.py +++ b/electrum/synchronizer.py @@ -24,7 +24,7 @@ # SOFTWARE. import asyncio import hashlib -from typing import Dict, List +from typing import Dict, List, TYPE_CHECKING from collections import defaultdict from aiorpcx import TaskGroup, run_in_thread @@ -33,6 +33,10 @@ from .transaction import Transaction from .util import bh2u, make_aiohttp_session, NetworkJobOnDefaultServer from .bitcoin import address_to_scripthash +if TYPE_CHECKING: + from .network import Network + from .address_synchronizer import AddressSynchronizer + def history_status(h): if not h: @@ -47,7 +51,7 @@ class SynchronizerBase(NetworkJobOnDefaultServer): """Subscribe over the network to a set of addresses, and monitor their statuses. Every time a status changes, run a coroutine provided by the subclass. """ - def __init__(self, network): + def __init__(self, network: 'Network'): self.asyncio_loop = network.asyncio_loop NetworkJobOnDefaultServer.__init__(self, network) @@ -112,7 +116,7 @@ class Synchronizer(SynchronizerBase): we don't have the full history of, and requests binary transaction data of any transactions the wallet doesn't have. ''' - def __init__(self, wallet): + def __init__(self, wallet: 'AddressSynchronizer'): self.wallet = wallet SynchronizerBase.__init__(self, wallet.network) diff --git a/electrum/util.py b/electrum/util.py index fd4494a8..b794bd1d 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -23,7 +23,7 @@ import binascii import os, sys, re, json from collections import defaultdict -from typing import NamedTuple, Union +from typing import NamedTuple, Union, TYPE_CHECKING from datetime import datetime import decimal from decimal import Decimal @@ -46,6 +46,10 @@ from aiorpcx import TaskGroup from .i18n import _ +if TYPE_CHECKING: + from .network import Network + from .interface import Interface + def inv_dict(d): return {v: k for k, v in d.items()} @@ -923,10 +927,10 @@ class NetworkJobOnDefaultServer(PrintError): interface. Every time the main interface changes, the job is restarted, and some of its internals are reset. """ - def __init__(self, network): + def __init__(self, network: 'Network'): asyncio.set_event_loop(network.asyncio_loop) self.network = network - self.interface = None + self.interface = None # type: Interface self._restart_lock = asyncio.Lock() self._reset() asyncio.run_coroutine_threadsafe(self._restart(), network.asyncio_loop) @@ -938,7 +942,7 @@ class NetworkJobOnDefaultServer(PrintError): """ self.group = SilentTaskGroup() - async def _start(self, interface): + async def _start(self, interface: 'Interface'): self.interface = interface await interface.group.spawn(self._start_tasks) diff --git a/electrum/verifier.py b/electrum/verifier.py index c6648c7c..714bd8f9 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -22,7 +22,7 @@ # SOFTWARE. import asyncio -from typing import Sequence, Optional +from typing import Sequence, Optional, TYPE_CHECKING import aiorpcx @@ -33,6 +33,10 @@ from .blockchain import hash_header from .interface import GracefulDisconnect from . import constants +if TYPE_CHECKING: + from .network import Network + from .address_synchronizer import AddressSynchronizer + class MerkleVerificationFailure(Exception): pass class MissingBlockHeader(MerkleVerificationFailure): pass @@ -43,7 +47,7 @@ class InnerNodeOfSpvProofIsValidTx(MerkleVerificationFailure): pass class SPV(NetworkJobOnDefaultServer): """ Simple Payment Verification """ - def __init__(self, network, wallet): + def __init__(self, network: 'Network', wallet: 'AddressSynchronizer'): self.wallet = wallet NetworkJobOnDefaultServer.__init__(self, network) diff --git a/electrum/wallet.py b/electrum/wallet.py index 4b0e87aa..e7a27315 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -48,7 +48,7 @@ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, from .bitcoin import * from .version import * from .keystore import load_keystore, Hardware_KeyStore -from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW +from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW, WalletStorage from . import transaction, bitcoin, coinchooser, paymentrequest, contacts from .transaction import Transaction, TxOutput, TxOutputHwInfo from .plugin import run_hook @@ -57,6 +57,9 @@ from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .paymentrequest import InvoiceStore from .contacts import Contacts +from .network import Network +from .simple_config import SimpleConfig + TX_STATUS = [ _('Unconfirmed'), @@ -67,18 +70,18 @@ TX_STATUS = [ -def relayfee(network): +def relayfee(network: Network): from .simple_config import FEERATE_DEFAULT_RELAY MAX_RELAY_FEE = 50000 f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY return min(f, MAX_RELAY_FEE) -def dust_threshold(network): +def dust_threshold(network: Network): # Change <= dust threshold is added to the tx fee return 182 * 3 * relayfee(network) / 1000 -def append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax): +def append_utxos_to_inputs(inputs, network: Network, pubkey, txin_type, imax): if txin_type != 'p2pk': address = bitcoin.pubkey_to_address(txin_type, pubkey) scripthash = bitcoin.address_to_scripthash(address) @@ -101,7 +104,7 @@ def append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax): item['num_sig'] = 1 inputs.append(item) -def sweep_preparations(privkeys, network, imax=100): +def sweep_preparations(privkeys, network: Network, imax=100): def find_utxos_for_privkey(txin_type, privkey, compressed): pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) @@ -127,7 +130,7 @@ def sweep_preparations(privkeys, network, imax=100): return inputs, keypairs -def sweep(privkeys, network, config, recipient, fee=None, imax=100): +def sweep(privkeys, network: Network, config: SimpleConfig, recipient, fee=None, imax=100): inputs, keypairs = sweep_preparations(privkeys, network, imax) total = sum(i.get('value') for i in inputs) if fee is None: @@ -164,7 +167,7 @@ class Abstract_Wallet(AddressSynchronizer): gap_limit_for_change = 6 verbosity_filter = 'w' - def __init__(self, storage): + def __init__(self, storage: WalletStorage): AddressSynchronizer.__init__(self, storage) # saved fields @@ -220,9 +223,6 @@ class Abstract_Wallet(AddressSynchronizer): if not bitcoin.is_address(addrs[0]): raise WalletFileException('The addresses in this wallet are not bitcoin addresses.') - def synchronize(self): - pass - def calc_unused_change_addresses(self): with self.lock: if hasattr(self, '_unused_change_addresses'): diff --git a/electrum/websockets.py b/electrum/websockets.py index e87ad27c..19dd8eca 100644 --- a/electrum/websockets.py +++ b/electrum/websockets.py @@ -27,7 +27,7 @@ import os import json from collections import defaultdict import asyncio -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, TYPE_CHECKING import traceback import sys @@ -40,6 +40,11 @@ from .util import PrintError from . import bitcoin from .synchronizer import SynchronizerBase +if TYPE_CHECKING: + from .network import Network + from .simple_config import SimpleConfig + + request_queue = asyncio.Queue() @@ -61,7 +66,7 @@ class ElectrumWebSocket(WebSocket, PrintError): class BalanceMonitor(SynchronizerBase): - def __init__(self, config, network): + def __init__(self, config: 'SimpleConfig', network: 'Network'): SynchronizerBase.__init__(self, network) self.config = config self.expected_payments = defaultdict(list) # type: Dict[str, List[Tuple[WebSocket, int]]] @@ -104,7 +109,7 @@ class BalanceMonitor(SynchronizerBase): class WebSocketServer(threading.Thread): - def __init__(self, config, network): + def __init__(self, config: 'SimpleConfig', network: 'Network'): threading.Thread.__init__(self) self.config = config self.network = network From c4e09fa8743b3f2ec01dfd50b9447f9c65636f74 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 22 Oct 2018 18:21:38 +0200 Subject: [PATCH 057/301] simplify Plugins constructor --- electrum/plugin.py | 2 +- electrum/tests/test_storage_upgrade.py | 2 +- run_electrum | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/plugin.py b/electrum/plugin.py index 67a5c4e9..8e90131a 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -47,7 +47,7 @@ class Plugins(DaemonThread): verbosity_filter = 'p' @profiler - def __init__(self, config: SimpleConfig, is_local, gui_name): + def __init__(self, config: SimpleConfig, gui_name): DaemonThread.__init__(self) self.setName('Plugins') self.pkgpath = os.path.dirname(plugins.__file__) diff --git a/electrum/tests/test_storage_upgrade.py b/electrum/tests/test_storage_upgrade.py index 8d24e279..c5586c7a 100644 --- a/electrum/tests/test_storage_upgrade.py +++ b/electrum/tests/test_storage_upgrade.py @@ -264,7 +264,7 @@ class TestStorageUpgrade(WalletTestCase): gui_name = 'cmdline' # TODO it's probably wasteful to load all plugins... only need Trezor - Plugins(config, True, gui_name) + Plugins(config, gui_name) @classmethod def tearDownClass(cls): diff --git a/run_electrum b/run_electrum index d814f1f2..d8b19701 100755 --- a/run_electrum +++ b/run_electrum @@ -245,7 +245,7 @@ def run_offline_command(config, config_options, plugins): def init_plugins(config, gui_name): from electrum.plugin import Plugins - return Plugins(config, is_local or is_android, gui_name) + return Plugins(config, gui_name) if __name__ == '__main__': From 2d352bc3f0a55674b8f040d340d185fa0d167076 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 22 Oct 2018 20:43:31 +0200 Subject: [PATCH 058/301] transaction.BIP69_sort: use namedtuple fields --- electrum/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index ff6b7620..95f9f08c 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1016,7 +1016,7 @@ class Transaction: if inputs: self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n'])) if outputs: - self._outputs.sort(key = lambda o: (o[2], self.pay_script(o[0], o[1]))) + self._outputs.sort(key = lambda o: (o.value, self.pay_script(o.type, o.address))) def serialize_output(self, output): output_type, addr, amount = output From 2a60a701bfb4bada026795fdb92cd1644d58bb48 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 22 Oct 2018 23:47:34 +0200 Subject: [PATCH 059/301] qt wallet information: show has_seed and watching_only --- electrum/gui/qt/main_window.py | 18 +++++++++++++----- electrum/wallet.py | 5 ++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 5be84bec..c63dede2 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -56,8 +56,11 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, UnknownBaseUnit, DECIMAL_POINT_DEFAULT) from electrum.transaction import Transaction, TxOutput from electrum.address_synchronizer import AddTransactionException -from electrum.wallet import Multisig_Wallet, CannotBumpFee +from electrum.wallet import Multisig_Wallet, CannotBumpFee, Abstract_Wallet from electrum.version import ELECTRUM_VERSION +from electrum.network import Network +from electrum.exchange_rate import FxThread +from electrum.simple_config import SimpleConfig from .exception_window import Exception_Hook from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit @@ -102,17 +105,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): computing_privkeys_signal = pyqtSignal() show_privkeys_signal = pyqtSignal() - def __init__(self, gui_object, wallet): + def __init__(self, gui_object, wallet: Abstract_Wallet): QMainWindow.__init__(self) self.gui_object = gui_object - self.config = config = gui_object.config + self.config = config = gui_object.config # type: SimpleConfig self.setup_exception_hook() - self.network = gui_object.daemon.network + self.network = gui_object.daemon.network # type: Network self.wallet = wallet - self.fx = gui_object.daemon.fx + self.fx = gui_object.daemon.fx # type: FxThread self.invoices = wallet.invoices self.contacts = wallet.contacts self.tray = gui_object.tray @@ -2079,6 +2082,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): mpk_list = self.wallet.get_master_public_keys() vbox = QVBoxLayout() wallet_type = self.wallet.storage.get('wallet_type', '') + if self.wallet.is_watching_only(): + wallet_type += ' [{}]'.format(_('watching-only')) + seed_available = _('True') if self.wallet.has_seed() else _('False') grid = QGridLayout() basename = os.path.basename(self.wallet.storage.path) grid.addWidget(QLabel(_("Wallet name")+ ':'), 0, 0) @@ -2087,6 +2093,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): grid.addWidget(QLabel(wallet_type), 1, 1) grid.addWidget(QLabel(_("Script type")+ ':'), 2, 0) grid.addWidget(QLabel(self.wallet.txin_type), 2, 1) + grid.addWidget(QLabel(_("Seed available") + ':'), 3, 0) + grid.addWidget(QLabel(str(seed_available)), 3, 1) vbox.addLayout(grid) if self.wallet.is_deterministic(): mpk_text = ShowQRTextEdit() diff --git a/electrum/wallet.py b/electrum/wallet.py index e7a27315..9cab5135 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1142,6 +1142,9 @@ class Abstract_Wallet(AddressSynchronizer): # overloaded for TrustedCoin wallets return False + def is_watching_only(self) -> bool: + raise NotImplementedError() + class Simple_Wallet(Abstract_Wallet): # wallet with a single keystore @@ -1602,7 +1605,7 @@ class Multisig_Wallet(Deterministic_Wallet): return self.keystore.has_seed() def is_watching_only(self): - return not any([not k.is_watching_only() for k in self.get_keystores()]) + return all([k.is_watching_only() for k in self.get_keystores()]) def get_master_public_key(self): return self.keystore.get_master_public_key() From b68729115aed62651da1f83c1106d5f24963a993 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 23 Oct 2018 02:54:54 +0200 Subject: [PATCH 060/301] qt wallet information: added keystore type --- electrum/gui/qt/main_window.py | 7 +++++- electrum/keystore.py | 42 ++++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index c63dede2..137bf6f6 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2085,6 +2085,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if self.wallet.is_watching_only(): wallet_type += ' [{}]'.format(_('watching-only')) seed_available = _('True') if self.wallet.has_seed() else _('False') + keystore_types = [k.get_type_text() for k in self.wallet.get_keystores()] grid = QGridLayout() basename = os.path.basename(self.wallet.storage.path) grid.addWidget(QLabel(_("Wallet name")+ ':'), 0, 0) @@ -2095,6 +2096,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): grid.addWidget(QLabel(self.wallet.txin_type), 2, 1) grid.addWidget(QLabel(_("Seed available") + ':'), 3, 0) grid.addWidget(QLabel(str(seed_available)), 3, 1) + if len(keystore_types) <= 1: + grid.addWidget(QLabel(_("Keystore type") + ':'), 4, 0) + ks_type = str(keystore_types[0]) if keystore_types else _('No keystore') + grid.addWidget(QLabel(ks_type), 4, 1) vbox.addLayout(grid) if self.wallet.is_deterministic(): mpk_text = ShowQRTextEdit() @@ -2106,7 +2111,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if len(mpk_list) > 1: def label(key): if isinstance(self.wallet, Multisig_Wallet): - return _("cosigner") + ' ' + str(key+1) + return f'{_("cosigner")} {key+1} ( keystore: {keystore_types[key]} )' return '' labels = [label(i) for i in range(len(mpk_list))] on_click = lambda clayout: show_mpk(clayout.selected_index()) diff --git a/electrum/keystore.py b/electrum/keystore.py index 8eff2ee0..0af59d1b 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -47,6 +47,9 @@ class KeyStore(PrintError): def can_import(self): return False + def get_type_text(self) -> str: + return f'{self.type}' + def may_have_password(self): """Returns whether the keystore can be encrypted with a password.""" raise NotImplementedError() @@ -117,6 +120,8 @@ class Software_KeyStore(KeyStore): class Imported_KeyStore(Software_KeyStore): # keystore for imported private keys + type = 'imported' + def __init__(self, d): Software_KeyStore.__init__(self) self.keypairs = d.get('keypairs', {}) @@ -129,7 +134,7 @@ class Imported_KeyStore(Software_KeyStore): def dump(self): return { - 'type': 'imported', + 'type': self.type, 'keypairs': self.keypairs, } @@ -200,6 +205,7 @@ class Deterministic_KeyStore(Software_KeyStore): d['seed'] = self.seed if self.passphrase: d['passphrase'] = self.passphrase + d['type'] = self.type return d def has_seed(self): @@ -282,6 +288,8 @@ class Xpub: class BIP32_KeyStore(Deterministic_KeyStore, Xpub): + type = 'bip32' + def __init__(self, d): Xpub.__init__(self) Deterministic_KeyStore.__init__(self, d) @@ -293,7 +301,6 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub): def dump(self): d = Deterministic_KeyStore.dump(self) - d['type'] = 'bip32' d['xpub'] = self.xpub d['xprv'] = self.xprv return d @@ -342,6 +349,8 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub): class Old_KeyStore(Deterministic_KeyStore): + type = 'old' + def __init__(self, d): Deterministic_KeyStore.__init__(self, d) self.mpk = d.get('mpk') @@ -352,7 +361,6 @@ class Old_KeyStore(Deterministic_KeyStore): def dump(self): d = Deterministic_KeyStore.dump(self) d['mpk'] = self.mpk - d['type'] = 'old' return d def add_seed(self, seedphrase): @@ -481,7 +489,7 @@ class Hardware_KeyStore(KeyStore, Xpub): # - DEVICE_IDS # - wallet_type - #restore_wallet_class = BIP32_RD_Wallet + type = 'hardware' max_change_outputs = 1 def __init__(self, d): @@ -505,9 +513,12 @@ class Hardware_KeyStore(KeyStore, Xpub): def is_deterministic(self): return True + def get_type_text(self) -> str: + return f'hw[{self.hw_type}]' + def dump(self): return { - 'type': 'hardware', + 'type': self.type, 'hw_type': self.hw_type, 'xpub': self.xpub, 'derivation':self.derivation, @@ -669,7 +680,8 @@ def hardware_keystore(d): if hw_type in hw_keystores: constructor = hw_keystores[hw_type] return constructor(d) - raise WalletFileException('unknown hardware type: {}. hw_keystores: {}'.format(hw_type, list(hw_keystores.keys()))) + raise WalletFileException(f'unknown hardware type: {hw_type}. ' + f'hw_keystores: {list(hw_keystores)}') def load_keystore(storage, name): d = storage.get(name, {}) @@ -678,17 +690,13 @@ def load_keystore(storage, name): raise WalletFileException( 'Wallet format requires update.\n' 'Cannot find keystore for name {}'.format(name)) - if t == 'old': - k = Old_KeyStore(d) - elif t == 'imported': - k = Imported_KeyStore(d) - elif t == 'bip32': - k = BIP32_KeyStore(d) - elif t == 'hardware': - k = hardware_keystore(d) - else: - raise WalletFileException( - 'Unknown type {} for keystore named {}'.format(t, name)) + keystore_constructors = {ks.type: ks for ks in [Old_KeyStore, Imported_KeyStore, BIP32_KeyStore]} + keystore_constructors['hardware'] = hardware_keystore + try: + ks_constructor = keystore_constructors[t] + except KeyError: + raise WalletFileException(f'Unknown type {t} for keystore named {name}') + k = ks_constructor(d) return k From 0e6160bf2d66a0590df034c426d88ffa68bf9af3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 23 Oct 2018 03:01:23 +0200 Subject: [PATCH 061/301] follow-up prev: bad idea to eval translated string --- 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 137bf6f6..b0d272e5 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2111,7 +2111,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if len(mpk_list) > 1: def label(key): if isinstance(self.wallet, Multisig_Wallet): - return f'{_("cosigner")} {key+1} ( keystore: {keystore_types[key]} )' + return _("cosigner") + f' {key+1} ( keystore: {keystore_types[key]} )' return '' labels = [label(i) for i in range(len(mpk_list))] on_click = lambda clayout: show_mpk(clayout.selected_index()) From 361ffc062053436f4625dbe54a6dd22d7adc8af7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 25 Oct 2018 00:18:14 +0200 Subject: [PATCH 062/301] correctly handle bitcoin URIs if GUI is already running see #4796 --- electrum/daemon.py | 1 + electrum/gui/qt/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index e1fa82d7..36e60872 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -221,6 +221,7 @@ class Daemon(DaemonThread): config = SimpleConfig(config_options) if self.gui: if hasattr(self.gui, 'new_window'): + config.open_last_wallet() path = config.get_wallet_path() self.gui.new_window(path, config.get('url')) response = "ok" diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 767de6e9..9e961224 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -237,9 +237,9 @@ class ElectrumGui(PrintError): try: for w in self.windows: if w.wallet.storage.path == wallet.storage.path: - w.bring_to_top() - return - w = self.create_window_for_wallet(wallet) + break + else: + w = self.create_window_for_wallet(wallet) except BaseException as e: traceback.print_exc(file=sys.stdout) d = QMessageBox(QMessageBox.Warning, _('Error'), From 07a06b5d158036bcda8f7a0be49abb06cf83ac10 Mon Sep 17 00:00:00 2001 From: Andrew Zhuk Date: Thu, 25 Oct 2018 18:09:52 +0300 Subject: [PATCH 063/301] Update util.py (#4797) Adding Bitupper Explorer to the list --- electrum/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/util.py b/electrum/util.py index b794bd1d..d4ab1534 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -622,6 +622,8 @@ def time_difference(distance_in_time, include_seconds): return "over %d years" % (round(distance_in_minutes / 525600)) mainnet_block_explorers = { + 'Bitupper Explorer': ('https://bitupper.com/en/explorer/bitcoin/', + {'tx': 'transactions/', 'addr': 'addresses/'}), 'Biteasy.com': ('https://www.biteasy.com/blockchain/', {'tx': 'transactions/', 'addr': 'addresses/'}), 'Bitflyer.jp': ('https://chainflyer.bitflyer.jp/', From c61e13c1e953c962a2083feecb3905186492fb81 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 25 Oct 2018 18:27:41 +0200 Subject: [PATCH 064/301] add more block explorers, and change defaults --- electrum/util.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index d4ab1534..b27ebdf9 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -23,7 +23,7 @@ import binascii import os, sys, re, json from collections import defaultdict -from typing import NamedTuple, Union, TYPE_CHECKING +from typing import NamedTuple, Union, TYPE_CHECKING, Tuple, Optional from datetime import datetime import decimal from decimal import Decimal @@ -49,6 +49,7 @@ from .i18n import _ if TYPE_CHECKING: from .network import Network from .interface import Interface + from .simple_config import SimpleConfig def inv_dict(d): @@ -652,6 +653,8 @@ mainnet_block_explorers = { {'tx': 'api/tx?txid=', 'addr': '#/search?q='}), 'OXT.me': ('https://oxt.me/', {'tx': 'transaction/', 'addr': 'address/'}), + 'smartbit.com.au': ('https://www.smartbit.com.au/', + {'tx': 'tx/', 'addr': 'address/'}), 'system default': ('blockchain:/', {'tx': 'tx/', 'addr': 'address/'}), } @@ -659,28 +662,41 @@ mainnet_block_explorers = { testnet_block_explorers = { 'Blocktrail.com': ('https://www.blocktrail.com/tBTC/', {'tx': 'tx/', 'addr': 'address/'}), + 'BlockCypher.com': ('https://live.blockcypher.com/btc-testnet/', + {'tx': 'tx/', 'addr': 'address/'}), + 'Blockchain.info': ('https://testnet.blockchain.info/', + {'tx': 'tx/', 'addr': 'address/'}), + 'BTC.com': ('https://tchain.btc.com/', + {'tx': '', 'addr': ''}), + 'smartbit.com.au': ('https://testnet.smartbit.com.au/', + {'tx': 'tx/', 'addr': 'address/'}), 'system default': ('blockchain://000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943/', {'tx': 'tx/', 'addr': 'address/'}), } def block_explorer_info(): from . import constants - return testnet_block_explorers if constants.net.TESTNET else mainnet_block_explorers + return mainnet_block_explorers if not constants.net.TESTNET else testnet_block_explorers -def block_explorer(config): - return config.get('block_explorer', 'Blocktrail.com') +def block_explorer(config: 'SimpleConfig') -> str: + from . import constants + default_ = 'Blockchair.com' if not constants.net.TESTNET else 'smartbit.com.au' + be_key = config.get('block_explorer', default_) + be = block_explorer_info().get(be_key) + return be_key if be is not None else default_ -def block_explorer_tuple(config): +def block_explorer_tuple(config: 'SimpleConfig') -> Optional[Tuple[str, dict]]: return block_explorer_info().get(block_explorer(config)) -def block_explorer_URL(config, kind, item): +def block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional[str]: be_tuple = block_explorer_tuple(config) if not be_tuple: return - kind_str = be_tuple[1].get(kind) - if not kind_str: + explorer_url, explorer_dict = be_tuple + kind_str = explorer_dict.get(kind) + if kind_str is None: return - url_parts = [be_tuple[0], kind_str, item] + url_parts = [explorer_url, kind_str, item] return ''.join(url_parts) # URL decode From a88a2dea8255532abdd20002c866b443237a509c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 25 Oct 2018 22:20:33 +0200 Subject: [PATCH 065/301] split bip32 from bitcoin.py --- electrum/base_wizard.py | 4 +- electrum/bip32.py | 269 +++++++++++++++++ electrum/bitcoin.py | 276 +----------------- electrum/keystore.py | 27 +- electrum/paymentrequest.py | 7 +- electrum/plugin.py | 4 +- electrum/plugins/coldcard/coldcard.py | 14 +- electrum/plugins/cosigner_pool/qt.py | 8 +- .../plugins/digitalbitbox/digitalbitbox.py | 5 +- electrum/plugins/keepkey/clientbase.py | 2 +- electrum/plugins/keepkey/keepkey.py | 6 +- electrum/plugins/keepkey/qt.py | 7 +- electrum/plugins/ledger/ledger.py | 10 +- electrum/plugins/safe_t/clientbase.py | 2 +- electrum/plugins/safe_t/qt.py | 7 +- electrum/plugins/safe_t/safe_t.py | 6 +- electrum/plugins/trezor/clientbase.py | 2 +- electrum/plugins/trezor/qt.py | 7 +- electrum/plugins/trezor/trezor.py | 6 +- electrum/plugins/trustedcoin/trustedcoin.py | 25 +- electrum/tests/test_bitcoin.py | 21 +- electrum/transaction.py | 29 +- electrum/verifier.py | 3 +- electrum/wallet.py | 16 +- 24 files changed, 391 insertions(+), 372 deletions(-) create mode 100644 electrum/bip32.py diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index fe91cb4b..94cf53f8 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -30,6 +30,7 @@ from functools import partial from . import bitcoin from . import keystore +from .bip32 import is_bip32_derivation, xpub_type from .keystore import bip44_derivation, purpose48_derivation from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet, Abstract_Wallet) @@ -339,7 +340,7 @@ class BaseWizard(object): try: self.choice_and_line_dialog( run_next=f, title=_('Script type and Derivation path'), message1=message1, - message2=message2, choices=choices, test_text=bitcoin.is_bip32_derivation) + message2=message2, choices=choices, test_text=is_bip32_derivation) return except ScriptTypeNotSupported as e: self.show_error(e) @@ -419,7 +420,6 @@ class BaseWizard(object): def on_keystore(self, k): has_xpub = isinstance(k, keystore.Xpub) if has_xpub: - from .bitcoin import xpub_type t1 = xpub_type(k.xpub) if self.wallet_type == 'standard': if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: diff --git a/electrum/bip32.py b/electrum/bip32.py new file mode 100644 index 00000000..967ab708 --- /dev/null +++ b/electrum/bip32.py @@ -0,0 +1,269 @@ +# Copyright (C) 2018 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php + +import hashlib +from typing import List + +from .util import bfh, bh2u, BitcoinException, print_error +from . import constants +from . import ecc +from .crypto import hash_160, hmac_oneshot +from .bitcoin import rev_hex, int_to_hex, EncodeBase58Check, DecodeBase58Check + + +BIP32_PRIME = 0x80000000 + + +def protect_against_invalid_ecpoint(func): + def func_wrapper(*args): + n = args[-1] + while True: + is_prime = n & BIP32_PRIME + try: + return func(*args[:-1], n=n) + except ecc.InvalidECPointException: + print_error('bip32 protect_against_invalid_ecpoint: skipping index') + n += 1 + is_prime2 = n & BIP32_PRIME + if is_prime != is_prime2: raise OverflowError() + return func_wrapper + + +# Child private key derivation function (from master private key) +# k = master private key (32 bytes) +# c = master chain code (extra entropy for key derivation) (32 bytes) +# n = the index of the key we want to derive. (only 32 bits will be used) +# If n is hardened (i.e. the 32nd bit is set), the resulting private key's +# corresponding public key can NOT be determined without the master private key. +# However, if n is not hardened, the resulting private key's corresponding +# public key can be determined without the master private key. +@protect_against_invalid_ecpoint +def CKD_priv(k, c, n): + if n < 0: raise ValueError('the bip32 index needs to be non-negative') + is_prime = n & BIP32_PRIME + return _CKD_priv(k, c, bfh(rev_hex(int_to_hex(n,4))), is_prime) + + +def _CKD_priv(k, c, s, is_prime): + try: + keypair = ecc.ECPrivkey(k) + except ecc.InvalidECPointException as e: + raise BitcoinException('Impossible xprv (not within curve order)') from e + cK = keypair.get_public_key_bytes(compressed=True) + data = bytes([0]) + k + s if is_prime else cK + s + I = hmac_oneshot(c, data, hashlib.sha512) + I_left = ecc.string_to_number(I[0:32]) + k_n = (I_left + ecc.string_to_number(k)) % ecc.CURVE_ORDER + if I_left >= ecc.CURVE_ORDER or k_n == 0: + raise ecc.InvalidECPointException() + k_n = ecc.number_to_string(k_n, ecc.CURVE_ORDER) + c_n = I[32:] + return k_n, c_n + +# Child public key derivation function (from public key only) +# K = master public key +# c = master chain code +# n = index of key we want to derive +# This function allows us to find the nth public key, as long as n is +# not hardened. If n is hardened, we need the master private key to find it. +@protect_against_invalid_ecpoint +def CKD_pub(cK, c, n): + if n < 0: raise ValueError('the bip32 index needs to be non-negative') + if n & BIP32_PRIME: raise Exception() + return _CKD_pub(cK, c, bfh(rev_hex(int_to_hex(n,4)))) + +# helper function, callable with arbitrary string. +# note: 's' does not need to fit into 32 bits here! (c.f. trustedcoin billing) +def _CKD_pub(cK, c, s): + I = hmac_oneshot(c, cK + s, hashlib.sha512) + pubkey = ecc.ECPrivkey(I[0:32]) + ecc.ECPubkey(cK) + if pubkey.is_at_infinity(): + raise ecc.InvalidECPointException() + cK_n = pubkey.get_public_key_bytes(compressed=True) + c_n = I[32:] + return cK_n, c_n + + +def xprv_header(xtype, *, net=None): + if net is None: + net = constants.net + return bfh("%08x" % net.XPRV_HEADERS[xtype]) + + +def xpub_header(xtype, *, net=None): + if net is None: + net = constants.net + return bfh("%08x" % net.XPUB_HEADERS[xtype]) + + +def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4, + child_number=b'\x00'*4, *, net=None): + if not ecc.is_secret_within_curve_range(k): + raise BitcoinException('Impossible xprv (not within curve order)') + xprv = xprv_header(xtype, net=net) \ + + bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k + return EncodeBase58Check(xprv) + + +def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4, + child_number=b'\x00'*4, *, net=None): + xpub = xpub_header(xtype, net=net) \ + + bytes([depth]) + fingerprint + child_number + c + cK + return EncodeBase58Check(xpub) + + +class InvalidMasterKeyVersionBytes(BitcoinException): pass + + +def deserialize_xkey(xkey, prv, *, net=None): + if net is None: + net = constants.net + xkey = DecodeBase58Check(xkey) + if len(xkey) != 78: + raise BitcoinException('Invalid length for extended key: {}' + .format(len(xkey))) + depth = xkey[4] + fingerprint = xkey[5:9] + child_number = xkey[9:13] + c = xkey[13:13+32] + header = int('0x' + bh2u(xkey[0:4]), 16) + headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS + if header not in headers.values(): + raise InvalidMasterKeyVersionBytes('Invalid extended key format: {}' + .format(hex(header))) + xtype = list(headers.keys())[list(headers.values()).index(header)] + n = 33 if prv else 32 + K_or_k = xkey[13+n:] + if prv and not ecc.is_secret_within_curve_range(K_or_k): + raise BitcoinException('Impossible xprv (not within curve order)') + return xtype, depth, fingerprint, child_number, c, K_or_k + + +def deserialize_xpub(xkey, *, net=None): + return deserialize_xkey(xkey, False, net=net) + +def deserialize_xprv(xkey, *, net=None): + return deserialize_xkey(xkey, True, net=net) + +def xpub_type(x): + return deserialize_xpub(x)[0] + + +def is_xpub(text): + try: + deserialize_xpub(text) + return True + except: + return False + + +def is_xprv(text): + try: + deserialize_xprv(text) + return True + except: + return False + + +def xpub_from_xprv(xprv): + xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv) + cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True) + return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) + + +def bip32_root(seed, xtype): + I = hmac_oneshot(b"Bitcoin seed", seed, hashlib.sha512) + master_k = I[0:32] + master_c = I[32:] + # create xprv first, as that will check if master_k is within curve order + xprv = serialize_xprv(xtype, master_c, master_k) + cK = ecc.ECPrivkey(master_k).get_public_key_bytes(compressed=True) + xpub = serialize_xpub(xtype, master_c, cK) + return xprv, xpub + + +def xpub_from_pubkey(xtype, cK): + if cK[0] not in (0x02, 0x03): + raise ValueError('Unexpected first byte: {}'.format(cK[0])) + return serialize_xpub(xtype, b'\x00'*32, cK) + + +def bip32_derivation(s): + if not s.startswith('m/'): + raise ValueError('invalid bip32 derivation path: {}'.format(s)) + s = s[2:] + for n in s.split('/'): + if n == '': continue + i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n) + yield i + +def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]: + """Convert bip32 path to list of uint32 integers with prime flags + m/0/-1/1' -> [0, 0x80000001, 0x80000001] + + based on code in trezorlib + """ + path = [] + for x in n.split('/')[1:]: + if x == '': continue + prime = 0 + if x.endswith("'"): + x = x.replace('\'', '') + prime = BIP32_PRIME + if x.startswith('-'): + prime = BIP32_PRIME + path.append(abs(int(x)) | prime) + return path + +def is_bip32_derivation(x): + try: + [ i for i in bip32_derivation(x)] + return True + except : + return False + +def bip32_private_derivation(xprv, branch, sequence): + if not sequence.startswith(branch): + raise ValueError('incompatible branch ({}) and sequence ({})' + .format(branch, sequence)) + if branch == sequence: + return xprv, xpub_from_xprv(xprv) + xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv) + sequence = sequence[len(branch):] + for n in sequence.split('/'): + if n == '': continue + i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n) + parent_k = k + k, c = CKD_priv(k, c, i) + depth += 1 + parent_cK = ecc.ECPrivkey(parent_k).get_public_key_bytes(compressed=True) + fingerprint = hash_160(parent_cK)[0:4] + child_number = bfh("%08X"%i) + cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True) + xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) + xprv = serialize_xprv(xtype, c, k, depth, fingerprint, child_number) + return xprv, xpub + + +def bip32_public_derivation(xpub, branch, sequence): + xtype, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub) + if not sequence.startswith(branch): + raise ValueError('incompatible branch ({}) and sequence ({})' + .format(branch, sequence)) + sequence = sequence[len(branch):] + for n in sequence.split('/'): + if n == '': continue + i = int(n) + parent_cK = cK + cK, c = CKD_pub(cK, c, i) + depth += 1 + fingerprint = hash_160(parent_cK)[0:4] + child_number = bfh("%08X"%i) + return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) + + +def bip32_private_key(sequence, k, chain): + for i in sequence: + k, chain = CKD_priv(k, chain, i) + return k diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 220d2870..3066692c 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -26,7 +26,7 @@ import hashlib from typing import List, Tuple -from .util import bfh, bh2u, BitcoinException, print_error, assert_bytes, to_bytes, inv_dict +from .util import bfh, bh2u, BitcoinException, assert_bytes, to_bytes, inv_dict from . import version from . import segwit_addr from . import constants @@ -152,6 +152,9 @@ hash_decode = lambda x: bfh(x)[::-1] hmac_sha_512 = lambda x, y: hmac_oneshot(x, y, hashlib.sha512) +################################## electrum seeds + + def is_new_seed(x, prefix=version.SEED_PREFIX): from . import mnemonic x = mnemonic.normalize_text(x) @@ -277,14 +280,14 @@ def address_to_script(addr, *, net=None): script = bh2u(bytes([OP_n])) script += push_script(bh2u(bytes(witprog))) return script - addrtype, hash_160 = b58_address_to_hash160(addr) + addrtype, hash_160_ = b58_address_to_hash160(addr) if addrtype == net.ADDRTYPE_P2PKH: script = '76a9' # op_dup, op_hash_160 - script += push_script(bh2u(hash_160)) + script += push_script(bh2u(hash_160_)) script += '88ac' # op_equalverify, op_checksig elif addrtype == net.ADDRTYPE_P2SH: script = 'a9' # op_hash_160 - script += push_script(bh2u(hash_160)) + script += push_script(bh2u(hash_160_)) script += '87' # op_equal else: raise BitcoinException('unknown address type: {}'.format(addrtype)) @@ -409,12 +412,6 @@ WIF_SCRIPT_TYPES = { WIF_SCRIPT_TYPES_INV = inv_dict(WIF_SCRIPT_TYPES) -PURPOSE48_SCRIPT_TYPES = { - 'p2wsh-p2sh': 1, # specifically multisig - 'p2wsh': 2, # specifically multisig -} -PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES) - def serialize_privkey(secret: bytes, compressed: bool, txin_type: str, internal_use: bool=False) -> str: @@ -521,262 +518,3 @@ def is_minikey(text): def minikey_to_private_key(text): return sha256(text) - - -###################################### BIP32 ############################## - -BIP32_PRIME = 0x80000000 - - -def protect_against_invalid_ecpoint(func): - def func_wrapper(*args): - n = args[-1] - while True: - is_prime = n & BIP32_PRIME - try: - return func(*args[:-1], n=n) - except ecc.InvalidECPointException: - print_error('bip32 protect_against_invalid_ecpoint: skipping index') - n += 1 - is_prime2 = n & BIP32_PRIME - if is_prime != is_prime2: raise OverflowError() - return func_wrapper - - -# Child private key derivation function (from master private key) -# k = master private key (32 bytes) -# c = master chain code (extra entropy for key derivation) (32 bytes) -# n = the index of the key we want to derive. (only 32 bits will be used) -# If n is hardened (i.e. the 32nd bit is set), the resulting private key's -# corresponding public key can NOT be determined without the master private key. -# However, if n is not hardened, the resulting private key's corresponding -# public key can be determined without the master private key. -@protect_against_invalid_ecpoint -def CKD_priv(k, c, n): - if n < 0: raise ValueError('the bip32 index needs to be non-negative') - is_prime = n & BIP32_PRIME - return _CKD_priv(k, c, bfh(rev_hex(int_to_hex(n,4))), is_prime) - - -def _CKD_priv(k, c, s, is_prime): - try: - keypair = ecc.ECPrivkey(k) - except ecc.InvalidECPointException as e: - raise BitcoinException('Impossible xprv (not within curve order)') from e - cK = keypair.get_public_key_bytes(compressed=True) - data = bytes([0]) + k + s if is_prime else cK + s - I = hmac_oneshot(c, data, hashlib.sha512) - I_left = ecc.string_to_number(I[0:32]) - k_n = (I_left + ecc.string_to_number(k)) % ecc.CURVE_ORDER - if I_left >= ecc.CURVE_ORDER or k_n == 0: - raise ecc.InvalidECPointException() - k_n = ecc.number_to_string(k_n, ecc.CURVE_ORDER) - c_n = I[32:] - return k_n, c_n - -# Child public key derivation function (from public key only) -# K = master public key -# c = master chain code -# n = index of key we want to derive -# This function allows us to find the nth public key, as long as n is -# not hardened. If n is hardened, we need the master private key to find it. -@protect_against_invalid_ecpoint -def CKD_pub(cK, c, n): - if n < 0: raise ValueError('the bip32 index needs to be non-negative') - if n & BIP32_PRIME: raise Exception() - return _CKD_pub(cK, c, bfh(rev_hex(int_to_hex(n,4)))) - -# helper function, callable with arbitrary string. -# note: 's' does not need to fit into 32 bits here! (c.f. trustedcoin billing) -def _CKD_pub(cK, c, s): - I = hmac_oneshot(c, cK + s, hashlib.sha512) - pubkey = ecc.ECPrivkey(I[0:32]) + ecc.ECPubkey(cK) - if pubkey.is_at_infinity(): - raise ecc.InvalidECPointException() - cK_n = pubkey.get_public_key_bytes(compressed=True) - c_n = I[32:] - return cK_n, c_n - - -def xprv_header(xtype, *, net=None): - if net is None: - net = constants.net - return bfh("%08x" % net.XPRV_HEADERS[xtype]) - - -def xpub_header(xtype, *, net=None): - if net is None: - net = constants.net - return bfh("%08x" % net.XPUB_HEADERS[xtype]) - - -def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4, - child_number=b'\x00'*4, *, net=None): - if not ecc.is_secret_within_curve_range(k): - raise BitcoinException('Impossible xprv (not within curve order)') - xprv = xprv_header(xtype, net=net) \ - + bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k - return EncodeBase58Check(xprv) - - -def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4, - child_number=b'\x00'*4, *, net=None): - xpub = xpub_header(xtype, net=net) \ - + bytes([depth]) + fingerprint + child_number + c + cK - return EncodeBase58Check(xpub) - - -class InvalidMasterKeyVersionBytes(BitcoinException): pass - - -def deserialize_xkey(xkey, prv, *, net=None): - if net is None: - net = constants.net - xkey = DecodeBase58Check(xkey) - if len(xkey) != 78: - raise BitcoinException('Invalid length for extended key: {}' - .format(len(xkey))) - depth = xkey[4] - fingerprint = xkey[5:9] - child_number = xkey[9:13] - c = xkey[13:13+32] - header = int('0x' + bh2u(xkey[0:4]), 16) - headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS - if header not in headers.values(): - raise InvalidMasterKeyVersionBytes('Invalid extended key format: {}' - .format(hex(header))) - xtype = list(headers.keys())[list(headers.values()).index(header)] - n = 33 if prv else 32 - K_or_k = xkey[13+n:] - if prv and not ecc.is_secret_within_curve_range(K_or_k): - raise BitcoinException('Impossible xprv (not within curve order)') - return xtype, depth, fingerprint, child_number, c, K_or_k - - -def deserialize_xpub(xkey, *, net=None): - return deserialize_xkey(xkey, False, net=net) - -def deserialize_xprv(xkey, *, net=None): - return deserialize_xkey(xkey, True, net=net) - -def xpub_type(x): - return deserialize_xpub(x)[0] - - -def is_xpub(text): - try: - deserialize_xpub(text) - return True - except: - return False - - -def is_xprv(text): - try: - deserialize_xprv(text) - return True - except: - return False - - -def xpub_from_xprv(xprv): - xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv) - cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True) - return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) - - -def bip32_root(seed, xtype): - I = hmac_oneshot(b"Bitcoin seed", seed, hashlib.sha512) - master_k = I[0:32] - master_c = I[32:] - # create xprv first, as that will check if master_k is within curve order - xprv = serialize_xprv(xtype, master_c, master_k) - cK = ecc.ECPrivkey(master_k).get_public_key_bytes(compressed=True) - xpub = serialize_xpub(xtype, master_c, cK) - return xprv, xpub - - -def xpub_from_pubkey(xtype, cK): - if cK[0] not in (0x02, 0x03): - raise ValueError('Unexpected first byte: {}'.format(cK[0])) - return serialize_xpub(xtype, b'\x00'*32, cK) - - -def bip32_derivation(s): - if not s.startswith('m/'): - raise ValueError('invalid bip32 derivation path: {}'.format(s)) - s = s[2:] - for n in s.split('/'): - if n == '': continue - i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n) - yield i - -def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]: - """Convert bip32 path to list of uint32 integers with prime flags - m/0/-1/1' -> [0, 0x80000001, 0x80000001] - - based on code in trezorlib - """ - path = [] - for x in n.split('/')[1:]: - if x == '': continue - prime = 0 - if x.endswith("'"): - x = x.replace('\'', '') - prime = BIP32_PRIME - if x.startswith('-'): - prime = BIP32_PRIME - path.append(abs(int(x)) | prime) - return path - -def is_bip32_derivation(x): - try: - [ i for i in bip32_derivation(x)] - return True - except : - return False - -def bip32_private_derivation(xprv, branch, sequence): - if not sequence.startswith(branch): - raise ValueError('incompatible branch ({}) and sequence ({})' - .format(branch, sequence)) - if branch == sequence: - return xprv, xpub_from_xprv(xprv) - xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv) - sequence = sequence[len(branch):] - for n in sequence.split('/'): - if n == '': continue - i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n) - parent_k = k - k, c = CKD_priv(k, c, i) - depth += 1 - parent_cK = ecc.ECPrivkey(parent_k).get_public_key_bytes(compressed=True) - fingerprint = hash_160(parent_cK)[0:4] - child_number = bfh("%08X"%i) - cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True) - xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) - xprv = serialize_xprv(xtype, c, k, depth, fingerprint, child_number) - return xprv, xpub - - -def bip32_public_derivation(xpub, branch, sequence): - xtype, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub) - if not sequence.startswith(branch): - raise ValueError('incompatible branch ({}) and sequence ({})' - .format(branch, sequence)) - sequence = sequence[len(branch):] - for n in sequence.split('/'): - if n == '': continue - i = int(n) - parent_cK = cK - cK, c = CKD_pub(cK, c, i) - depth += 1 - fingerprint = hash_160(parent_cK)[0:4] - child_number = bfh("%08X"%i) - return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) - - -def bip32_private_key(sequence, k, chain): - for i in sequence: - k, chain = CKD_priv(k, chain, i) - return k diff --git a/electrum/keystore.py b/electrum/keystore.py index 0af59d1b..f991a4f5 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -25,13 +25,19 @@ # SOFTWARE. from unicodedata import normalize +import hashlib -from . import bitcoin, ecc, constants -from .bitcoin import * +from . import bitcoin, ecc, constants, bip32 +from .bitcoin import (deserialize_privkey, serialize_privkey, + public_key_to_p2pkh, seed_type, is_seed) +from .bip32 import (bip32_public_derivation, deserialize_xpub, CKD_pub, + bip32_root, deserialize_xprv, bip32_private_derivation, + bip32_private_key, bip32_derivation, BIP32_PRIME, + is_xpub, is_xprv) from .ecc import string_to_number, number_to_string -from .crypto import pw_decode, pw_encode +from .crypto import pw_decode, pw_encode, Hash from .util import (PrintError, InvalidPassword, hfu, WalletFileException, - BitcoinException) + BitcoinException, bh2u, bfh, print_error, inv_dict) from .mnemonic import Mnemonic, load_wordlist from .plugin import run_hook @@ -332,7 +338,7 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub): def add_xprv(self, xprv): self.xprv = xprv - self.xpub = bitcoin.xpub_from_xprv(xprv) + self.xpub = bip32.xpub_from_xprv(xprv) def add_xprv_from_seed(self, bip32_seed, xtype, derivation): xprv, xpub = bip32_root(bip32_seed, xtype) @@ -614,6 +620,13 @@ def from_bip39_seed(seed, passphrase, derivation, xtype=None): return k +PURPOSE48_SCRIPT_TYPES = { + 'p2wsh-p2sh': 1, # specifically multisig + 'p2wsh': 2, # specifically multisig +} +PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES) + + def xtype_from_derivation(derivation: str) -> str: """Returns the script type to be used for this derivation.""" if derivation.startswith("m/84'"): @@ -781,7 +794,7 @@ def from_seed(seed, passphrase, is_p2sh=False): def from_private_key_list(text): keystore = Imported_KeyStore({}) for x in get_private_keys(text): - keystore.import_key(x, None) + keystore.import_privkey(x, None) return keystore def from_old_mpk(mpk): @@ -795,7 +808,7 @@ def from_xpub(xpub): return k def from_xprv(xprv): - xpub = bitcoin.xpub_from_xprv(xprv) + xpub = bip32.xpub_from_xprv(xprv) k = BIP32_KeyStore({}) k.xprv = xprv k.xpub = xpub diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 0cb15e47..ba1cd7ea 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -38,9 +38,8 @@ except ImportError: sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'protoc --proto_path=electrum/ --python_out=electrum/ electrum/paymentrequest.proto'") from . import bitcoin, ecc, util, transaction, x509, rsakey -from .util import print_error, bh2u, bfh -from .util import export_meta, import_meta - +from .util import print_error, bh2u, bfh, export_meta, import_meta +from .crypto import sha256 from .bitcoin import TYPE_ADDRESS from .transaction import TxOutput @@ -113,7 +112,7 @@ class PaymentRequest: def parse(self, r): if self.error: return - self.id = bh2u(bitcoin.sha256(r)[0:16]) + self.id = bh2u(sha256(r)[0:16]) try: self.data = pb2.PaymentRequest() self.data.ParseFromString(r) diff --git a/electrum/plugin.py b/electrum/plugin.py index 8e90131a..2e180f45 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -33,7 +33,7 @@ import threading from .i18n import _ from .util import (profiler, PrintError, DaemonThread, UserCancelled, ThreadJob, print_error) -from . import bitcoin +from . import bip32 from . import plugins from .simple_config import SimpleConfig @@ -432,7 +432,7 @@ class DeviceMgr(ThreadJob, PrintError): def force_pair_xpub(self, plugin, handler, info, xpub, derivation, devices): # The wallet has not been previously paired, so let the user # choose an unpaired device and compare its first address. - xtype = bitcoin.xpub_type(xpub) + xtype = bip32.xpub_type(xpub) client = self.client_lookup(info.device.id_) if client and client.is_pairable(): # See comment above for same code diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index 902d6f2a..e123f52b 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -3,25 +3,21 @@ # # from struct import pack, unpack -import hashlib import os, sys, time, io import traceback -from electrum import bitcoin -from electrum.bitcoin import serialize_xpub, deserialize_xpub, InvalidMasterKeyVersionBytes -from electrum import constants -from electrum.bitcoin import TYPE_ADDRESS, int_to_hex +from electrum.bip32 import serialize_xpub, deserialize_xpub, InvalidMasterKeyVersionBytes from electrum.i18n import _ -from electrum.plugin import BasePlugin, Device +from electrum.plugin import Device from electrum.keystore import Hardware_KeyStore, xpubkey_to_pubkey, Xpub from electrum.transaction import Transaction from electrum.wallet import Standard_Wallet from electrum.crypto import hash_160 -from ..hw_wallet import HW_PluginBase -from ..hw_wallet.plugin import is_any_tx_output_on_change_branch from electrum.util import print_error, bfh, bh2u, versiontuple from electrum.base_wizard import ScriptTypeNotSupported +from ..hw_wallet import HW_PluginBase + try: import hid from ckcc.protocol import CCProtocolPacker, CCProtocolUnpacker @@ -46,7 +42,7 @@ try: from electrum.ecc import ECPubkey xtype, depth, parent_fingerprint, child_number, chain_code, K_or_k \ - = bitcoin.deserialize_xpub(expect_xpub) + = deserialize_xpub(expect_xpub) pubkey = ECPubkey(K_or_k) try: diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py index 3db937ab..0f9d4fb3 100644 --- a/electrum/plugins/cosigner_pool/qt.py +++ b/electrum/plugins/cosigner_pool/qt.py @@ -30,7 +30,7 @@ from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import QPushButton -from electrum import bitcoin, util, keystore, ecc +from electrum import util, keystore, ecc, bip32, crypto from electrum import transaction from electrum.plugin import BasePlugin, hook from electrum.i18n import _ @@ -132,8 +132,8 @@ class Plugin(BasePlugin): self.cosigner_list = [] for key, keystore in wallet.keystores.items(): xpub = keystore.get_master_public_key() - K = bitcoin.deserialize_xpub(xpub)[-1] - _hash = bh2u(bitcoin.Hash(K)) + K = bip32.deserialize_xpub(xpub)[-1] + _hash = bh2u(crypto.Hash(K)) if not keystore.is_watching_only(): self.keys.append((key, _hash, window)) else: @@ -222,7 +222,7 @@ class Plugin(BasePlugin): if not xprv: return try: - k = bitcoin.deserialize_xprv(xprv)[-1] + k = bip32.deserialize_xprv(xprv)[-1] EC = ecc.ECPrivkey(k) message = bh2u(EC.decrypt_message(message)) except Exception as e: diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index c2ccdc4e..24f676e0 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -5,8 +5,9 @@ try: from electrum.crypto import Hash, EncodeAES, DecodeAES - from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, is_address, - serialize_xpub, deserialize_xpub) + from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, + is_address) + from electrum.bip32 import serialize_xpub, deserialize_xpub from electrum import ecc from electrum.ecc import msg_magic from electrum.wallet import Standard_Wallet diff --git a/electrum/plugins/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py index f4e46d15..7f9a0b58 100644 --- a/electrum/plugins/keepkey/clientbase.py +++ b/electrum/plugins/keepkey/clientbase.py @@ -4,7 +4,7 @@ from struct import pack from electrum.i18n import _ from electrum.util import PrintError, UserCancelled from electrum.keystore import bip39_normalize_passphrase -from electrum.bitcoin import serialize_xpub, convert_bip32_path_to_list_of_uint32 +from electrum.bip32 import serialize_xpub, convert_bip32_path_to_list_of_uint32 class GuiMixin(object): diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 8827c5ed..2498f495 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -3,14 +3,12 @@ import traceback import sys from electrum.util import bfh, bh2u, UserCancelled -from electrum.bitcoin import (xpub_from_pubkey, deserialize_xpub, - TYPE_ADDRESS, TYPE_SCRIPT) +from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT +from electrum.bip32 import deserialize_xpub from electrum import constants from electrum.i18n import _ -from electrum.plugin import BasePlugin from electrum.transaction import deserialize, Transaction from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey -from electrum.wallet import Standard_Wallet from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py index 9c4a7480..8496b1ed 100644 --- a/electrum/plugins/keepkey/qt.py +++ b/electrum/plugins/keepkey/qt.py @@ -7,9 +7,8 @@ from PyQt5.Qt import QVBoxLayout, QLabel from electrum.gui.qt.util import * from electrum.i18n import _ -from electrum.plugin import hook, DeviceMgr -from electrum.util import PrintError, UserCancelled, bh2u -from electrum.wallet import Wallet, Standard_Wallet +from electrum.plugin import hook +from electrum.util import bh2u from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available @@ -253,7 +252,7 @@ class QtPlugin(QtPluginBase): else: msg = _("Enter the master private key beginning with xprv:") def set_enabled(): - from keystore import is_xprv + from electrum.bip32 import is_xprv wizard.next_button.setEnabled(is_xprv(clean_text(text))) text.textChanged.connect(set_enabled) next_enabled = False diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index c5abdcef..a2665e74 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -3,18 +3,18 @@ import hashlib import sys import traceback -from electrum import bitcoin from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int +from electrum.bip32 import serialize_xpub from electrum.i18n import _ -from electrum.plugin import BasePlugin from electrum.keystore import Hardware_KeyStore from electrum.transaction import Transaction from electrum.wallet import Standard_Wallet -from ..hw_wallet import HW_PluginBase -from ..hw_wallet.plugin import is_any_tx_output_on_change_branch from electrum.util import print_error, bfh, bh2u, versiontuple from electrum.base_wizard import ScriptTypeNotSupported +from ..hw_wallet import HW_PluginBase +from ..hw_wallet.plugin import is_any_tx_output_on_change_branch + try: import hid from btchip.btchipComm import HIDDongleHIDAPI, DongleWait @@ -112,7 +112,7 @@ class Ledger_Client(): depth = len(splitPath) lastChild = splitPath[len(splitPath) - 1].split('\'') childnum = int(lastChild[0]) if len(lastChild) == 1 else 0x80000000 | int(lastChild[0]) - xpub = bitcoin.serialize_xpub(xtype, nodeData['chainCode'], publicKey, depth, self.i4b(fingerprint), self.i4b(childnum)) + xpub = serialize_xpub(xtype, nodeData['chainCode'], publicKey, depth, self.i4b(fingerprint), self.i4b(childnum)) return xpub def has_detached_pin_support(self, client): diff --git a/electrum/plugins/safe_t/clientbase.py b/electrum/plugins/safe_t/clientbase.py index da383809..6b0f9f2a 100644 --- a/electrum/plugins/safe_t/clientbase.py +++ b/electrum/plugins/safe_t/clientbase.py @@ -4,7 +4,7 @@ from struct import pack from electrum.i18n import _ from electrum.util import PrintError, UserCancelled from electrum.keystore import bip39_normalize_passphrase -from electrum.bitcoin import serialize_xpub, convert_bip32_path_to_list_of_uint32 +from electrum.bip32 import serialize_xpub, convert_bip32_path_to_list_of_uint32 class GuiMixin(object): diff --git a/electrum/plugins/safe_t/qt.py b/electrum/plugins/safe_t/qt.py index ef9ce6d3..1a764be9 100644 --- a/electrum/plugins/safe_t/qt.py +++ b/electrum/plugins/safe_t/qt.py @@ -7,9 +7,8 @@ from PyQt5.Qt import QVBoxLayout, QLabel from electrum.gui.qt.util import * from electrum.i18n import _ -from electrum.plugin import hook, DeviceMgr -from electrum.util import PrintError, UserCancelled, bh2u -from electrum.wallet import Wallet, Standard_Wallet +from electrum.plugin import hook +from electrum.util import bh2u from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available @@ -127,7 +126,7 @@ class QtPlugin(QtPluginBase): else: msg = _("Enter the master private key beginning with xprv:") def set_enabled(): - from electrum.keystore import is_xprv + from electrum.bip32 import is_xprv wizard.next_button.setEnabled(is_xprv(clean_text(text))) text.textChanged.connect(set_enabled) next_enabled = False diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index d5e3a047..48f4e386 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -3,11 +3,11 @@ import traceback import sys from electrum.util import bfh, bh2u, versiontuple, UserCancelled -from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, deserialize_xpub, - TYPE_ADDRESS, TYPE_SCRIPT, is_address) +from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT +from electrum.bip32 import deserialize_xpub from electrum import constants from electrum.i18n import _ -from electrum.plugin import BasePlugin, Device +from electrum.plugin import Device from electrum.transaction import deserialize, Transaction from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey from electrum.base_wizard import ScriptTypeNotSupported diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index cb6ccc5e..12718741 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -4,7 +4,7 @@ from struct import pack from electrum.i18n import _ from electrum.util import PrintError, UserCancelled from electrum.keystore import bip39_normalize_passphrase -from electrum.bitcoin import serialize_xpub, convert_bip32_path_to_list_of_uint32 +from electrum.bip32 import serialize_xpub, convert_bip32_path_to_list_of_uint32 class GuiMixin(object): diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index 77b0ca23..93f34d17 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -7,9 +7,8 @@ from PyQt5.Qt import QVBoxLayout, QLabel from electrum.gui.qt.util import * from electrum.i18n import _ -from electrum.plugin import hook, DeviceMgr -from electrum.util import PrintError, UserCancelled, bh2u -from electrum.wallet import Wallet, Standard_Wallet +from electrum.plugin import hook +from electrum.util import bh2u from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available @@ -222,7 +221,7 @@ class QtPlugin(QtPluginBase): else: msg = _("Enter the master private key beginning with xprv:") def set_enabled(): - from electrum.keystore import is_xprv + from electrum.bip32 import is_xprv wizard.next_button.setEnabled(is_xprv(clean_text(text))) text.textChanged.connect(set_enabled) next_enabled = False diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index b3e61768..e195b071 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -3,11 +3,11 @@ import traceback import sys from electrum.util import bfh, bh2u, versiontuple, UserCancelled -from electrum.bitcoin import (xpub_from_pubkey, deserialize_xpub, - TYPE_ADDRESS, TYPE_SCRIPT) +from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT +from electrum.bip32 import deserialize_xpub from electrum import constants from electrum.i18n import _ -from electrum.plugin import BasePlugin, Device +from electrum.plugin import Device from electrum.transaction import deserialize, Transaction from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey from electrum.base_wizard import ScriptTypeNotSupported diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 8227a3a8..84c4313a 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -24,14 +24,19 @@ # SOFTWARE. import asyncio import socket -import os import json import base64 +import time +import hashlib + from urllib.parse import urljoin from urllib.parse import quote -from electrum import bitcoin, ecc, constants, keystore, version -from electrum.bitcoin import * +from electrum import ecc, constants, keystore, version, bip32 +from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, public_key_to_p2pkh +from electrum.bip32 import (deserialize_xpub, deserialize_xprv, bip32_private_key, CKD_pub, + serialize_xpub, bip32_root, bip32_private_derivation) +from electrum.crypto import sha256 from electrum.transaction import TxOutput from electrum.mnemonic import Mnemonic from electrum.wallet import Multisig_Wallet, Deterministic_Wallet @@ -348,7 +353,7 @@ class Wallet_2fa(Multisig_Wallet): def get_user_id(storage): def make_long_id(xpub_hot, xpub_cold): - return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold]))) + return sha256(''.join(sorted([xpub_hot, xpub_cold]))) xpub1 = storage.get('x1/')['xpub'] xpub2 = storage.get('x2/')['xpub'] long_id = make_long_id(xpub1, xpub2) @@ -357,15 +362,15 @@ def get_user_id(storage): def make_xpub(xpub, s): version, _, _, _, c, cK = deserialize_xpub(xpub) - cK2, c2 = bitcoin._CKD_pub(cK, c, s) - return bitcoin.serialize_xpub(version, c2, cK2) + cK2, c2 = bip32._CKD_pub(cK, c, s) + return serialize_xpub(version, c2, cK2) def make_billing_address(wallet, num): long_id, short_id = wallet.get_user_id() xpub = make_xpub(get_billing_xpub(), long_id) version, _, _, _, c, cK = deserialize_xpub(xpub) - cK, c = bitcoin.CKD_pub(cK, c, num) - return bitcoin.public_key_to_p2pkh(cK) + cK, c = CKD_pub(cK, c, num) + return public_key_to_p2pkh(cK) class TrustedCoinPlugin(BasePlugin): @@ -379,7 +384,7 @@ class TrustedCoinPlugin(BasePlugin): @staticmethod def is_valid_seed(seed): - return bitcoin.is_new_seed(seed, SEED_PREFIX) + return is_new_seed(seed, SEED_PREFIX) def is_available(self): return True @@ -479,8 +484,6 @@ class TrustedCoinPlugin(BasePlugin): @classmethod def get_xkeys(self, seed, passphrase, derivation): - from electrum.mnemonic import Mnemonic - from electrum.keystore import bip32_root, bip32_private_derivation bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase) xprv, xpub = bip32_root(bip32_seed, 'standard') xprv, xpub = bip32_private_derivation(xprv, "m/", derivation) diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index f14fb63c..399ffc2e 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -1,16 +1,17 @@ import base64 import sys -from electrum.bitcoin import ( - public_key_to_p2pkh, - bip32_root, bip32_public_derivation, bip32_private_derivation, - Hash, address_from_private_key, - is_address, is_private_key, xpub_from_xprv, is_new_seed, is_old_seed, - var_int, op_push, address_to_script, - deserialize_privkey, serialize_privkey, is_segwit_address, - is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub, - xpub_type, is_xprv, is_bip32_derivation, seed_type, EncodeBase58Check, - script_num_to_hex, push_script, add_number_to_script, int_to_hex, convert_bip32_path_to_list_of_uint32) +from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key, + is_address, is_private_key, is_new_seed, is_old_seed, + var_int, op_push, address_to_script, + deserialize_privkey, serialize_privkey, is_segwit_address, + is_b58_address, address_to_scripthash, is_minikey, + is_compressed, seed_type, EncodeBase58Check, + script_num_to_hex, push_script, add_number_to_script, int_to_hex) +from electrum.bip32 import (bip32_root, bip32_public_derivation, bip32_private_derivation, + xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation, + is_xpub, convert_bip32_path_to_list_of_uint32) +from electrum.crypto import Hash from electrum import ecc, crypto, constants from electrum.ecc import number_to_string, string_to_number from electrum.transaction import opcodes diff --git a/electrum/transaction.py b/electrum/transaction.py index 95f9f08c..96bf6f99 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -27,23 +27,22 @@ # Note: The deserialization code originally comes from ABE. -from typing import (Sequence, Union, NamedTuple, Tuple, Optional, Iterable, - Callable) - -from .util import print_error, profiler - -from . import ecc -from . import bitcoin -from .bitcoin import * import struct import traceback import sys +from typing import (Sequence, Union, NamedTuple, Tuple, Optional, Iterable, + Callable, List) -# -# Workalike python implementation of Bitcoin's CDataStream class. -# +from . import ecc, bitcoin, constants, segwit_addr +from .util import print_error, profiler, to_bytes, bh2u, bfh +from .bitcoin import (TYPE_ADDRESS, TYPE_PUBKEY, TYPE_SCRIPT, hash_160, + hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, + hash_encode, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, + op_push, int_to_hex, push_script, b58_address_to_hash160) +from .crypto import Hash from .keystore import xpubkey_to_address, xpubkey_to_pubkey + NO_SIGNATURE = 'ff' PARTIAL_TXN_HEADER_MAGIC = b'EPTF\xff' @@ -78,6 +77,8 @@ TxOutputHwInfo = NamedTuple("TxOutputHwInfo", [('address_index', Tuple), class BCDataStream(object): + """Workalike python implementation of Bitcoin's CDataStream class.""" + def __init__(self): self.input = None self.read_cursor = 0 @@ -353,7 +354,7 @@ def parse_scriptSig(d, _bytes): if item[0] == 0: # segwit embedded into p2sh # witness version 0 - d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item)) + d['address'] = bitcoin.hash160_to_p2sh(hash_160(item)) if len(item) == 22: d['type'] = 'p2wpkh-p2sh' elif len(item) == 34: @@ -901,7 +902,7 @@ class Transaction: witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr) if witprog is not None: return 'p2wpkh' - addrtype, hash_160 = b58_address_to_hash160(addr) + addrtype, hash_160_ = b58_address_to_hash160(addr) if addrtype == constants.net.ADDRTYPE_P2PKH: return 'p2pkh' elif addrtype == constants.net.ADDRTYPE_P2SH: @@ -977,7 +978,7 @@ class Transaction: return multisig_script(pubkeys, txin['num_sig']) elif txin['type'] in ['p2wpkh', 'p2wpkh-p2sh']: pubkey = pubkeys[0] - pkh = bh2u(bitcoin.hash_160(bfh(pubkey))) + pkh = bh2u(hash_160(bfh(pubkey))) return '76a9' + push_script(pkh) + '88ac' elif txin['type'] == 'p2pk': pubkey = pubkeys[0] diff --git a/electrum/verifier.py b/electrum/verifier.py index 714bd8f9..32ea4aa4 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -27,7 +27,8 @@ from typing import Sequence, Optional, TYPE_CHECKING import aiorpcx from .util import bh2u, VerifiedTxInfo, NetworkJobOnDefaultServer -from .bitcoin import Hash, hash_decode, hash_encode +from .crypto import Hash +from .bitcoin import hash_decode, hash_encode from .transaction import Transaction from .blockchain import hash_header from .interface import GracefulDisconnect diff --git a/electrum/wallet.py b/electrum/wallet.py index 9cab5135..6379ef43 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -44,18 +44,20 @@ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, TimeoutException, WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, - Fiat) -from .bitcoin import * + Fiat, bfh, bh2u) +from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script, + is_minikey) from .version import * +from .crypto import Hash from .keystore import load_keystore, Hardware_KeyStore from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW, WalletStorage -from . import transaction, bitcoin, coinchooser, paymentrequest, contacts +from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32 from .transaction import Transaction, TxOutput, TxOutputHwInfo from .plugin import run_hook from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) -from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED -from .paymentrequest import InvoiceStore +from .paymentrequest import (PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, + InvoiceStore) from .contacts import Contacts from .network import Network from .simple_config import SimpleConfig @@ -1499,7 +1501,7 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet): def load_keystore(self): self.keystore = load_keystore(self.storage, 'keystore') try: - xtype = bitcoin.xpub_type(self.keystore.xpub) + xtype = bip32.xpub_type(self.keystore.xpub) except: xtype = 'standard' self.txin_type = 'p2pkh' if xtype == 'standard' else xtype @@ -1569,7 +1571,7 @@ class Multisig_Wallet(Deterministic_Wallet): name = 'x%d/'%(i+1) self.keystores[name] = load_keystore(self.storage, name) self.keystore = self.keystores['x1/'] - xtype = bitcoin.xpub_type(self.keystore.xpub) + xtype = bip32.xpub_type(self.keystore.xpub) self.txin_type = 'p2sh' if xtype == 'standard' else xtype def save_keystore(self): From 082a83dd85ae8aa8a3faf10ef12d360a7e22ac42 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 25 Oct 2018 22:28:24 +0200 Subject: [PATCH 066/301] rename crypto.Hash to sha256d --- electrum/bitcoin.py | 8 ++++---- electrum/blockchain.py | 5 +++-- electrum/crypto.py | 9 +++++---- electrum/ecc.py | 8 ++++---- electrum/keystore.py | 4 ++-- electrum/plugins/cosigner_pool/qt.py | 2 +- electrum/plugins/digitalbitbox/digitalbitbox.py | 10 +++++----- electrum/tests/test_bitcoin.py | 12 ++++-------- electrum/transaction.py | 16 ++++++++-------- electrum/verifier.py | 4 ++-- electrum/wallet.py | 4 ++-- 11 files changed, 40 insertions(+), 42 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 3066692c..41e83c53 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -31,7 +31,7 @@ from . import version from . import segwit_addr from . import constants from . import ecc -from .crypto import Hash, sha256, hash_160, hmac_oneshot +from .crypto import sha256d, sha256, hash_160, hmac_oneshot ################################## transactions @@ -199,7 +199,7 @@ is_seed = lambda x: bool(seed_type(x)) def hash160_to_b58_address(h160: bytes, addrtype): s = bytes([addrtype]) s += h160 - return base_encode(s+Hash(s)[0:4], base=58) + return base_encode(s+sha256d(s)[0:4], base=58) def b58_address_to_hash160(addr): @@ -382,7 +382,7 @@ class InvalidChecksum(Exception): def EncodeBase58Check(vchIn): - hash = Hash(vchIn) + hash = sha256d(vchIn) return base_encode(vchIn + hash[0:4], base=58) @@ -390,7 +390,7 @@ def DecodeBase58Check(psz): vchRet = base_decode(psz, None, base=58) key = vchRet[0:-4] csum = vchRet[-4:] - hash = Hash(key) + hash = sha256d(key) cs32 = hash[0:4] if cs32 != csum: raise InvalidChecksum('expected {}, actual {}'.format(bh2u(cs32), bh2u(csum))) diff --git a/electrum/blockchain.py b/electrum/blockchain.py index ccc04138..0f874905 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -25,7 +25,8 @@ import threading from typing import Optional, Dict from . import util -from .bitcoin import Hash, hash_encode, int_to_hex, rev_hex +from .bitcoin import hash_encode, int_to_hex, rev_hex +from .crypto import sha256d from . import constants from .util import bfh, bh2u from .simple_config import SimpleConfig @@ -71,7 +72,7 @@ def hash_header(header: dict) -> str: return '0' * 64 if header.get('prev_block_hash') is None: header['prev_block_hash'] = '00'*32 - return hash_encode(Hash(bfh(serialize_header(header)))) + return hash_encode(sha256d(bfh(serialize_header(header)))) blockchains = {} # type: Dict[int, Blockchain] diff --git a/electrum/crypto.py b/electrum/crypto.py index de8b6b5d..417a433b 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -27,6 +27,7 @@ import base64 import os import hashlib import hmac +from typing import Union import pyaes @@ -104,14 +105,14 @@ def DecodeAES(secret, e): def pw_encode(s, password): if password: - secret = Hash(password) + secret = sha256d(password) return EncodeAES(secret, to_bytes(s, "utf8")).decode('utf8') else: return s def pw_decode(s, password): if password is not None: - secret = Hash(password) + secret = sha256d(password) try: d = to_string(DecodeAES(secret, s), "utf8") except Exception: @@ -121,12 +122,12 @@ def pw_decode(s, password): return s -def sha256(x: bytes) -> bytes: +def sha256(x: Union[bytes, str]) -> bytes: x = to_bytes(x, 'utf8') return bytes(hashlib.sha256(x).digest()) -def Hash(x: bytes) -> bytes: +def sha256d(x: Union[bytes, str]) -> bytes: x = to_bytes(x, 'utf8') out = bytes(sha256(sha256(x))) return out diff --git a/electrum/ecc.py b/electrum/ecc.py index c5e77278..4a3cd2e9 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -34,7 +34,7 @@ from ecdsa.ellipticcurve import Point from ecdsa.util import string_to_number, number_to_string from .util import bfh, bh2u, assert_bytes, print_error, to_bytes, InvalidPassword, profiler -from .crypto import (Hash, aes_encrypt_with_iv, aes_decrypt_with_iv, hmac_oneshot) +from .crypto import (sha256d, aes_encrypt_with_iv, aes_decrypt_with_iv, hmac_oneshot) from .ecc_fast import do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1 from . import msqr @@ -252,7 +252,7 @@ class ECPubkey(object): def verify_message_for_address(self, sig65: bytes, message: bytes) -> None: assert_bytes(message) - h = Hash(msg_magic(message)) + h = sha256d(msg_magic(message)) public_key, compressed = self.from_signature65(sig65, h) # check public key if public_key != self: @@ -313,7 +313,7 @@ def verify_message_with_address(address: str, sig65: bytes, message: bytes): from .bitcoin import pubkey_to_address assert_bytes(sig65, message) try: - h = Hash(msg_magic(message)) + h = sha256d(msg_magic(message)) public_key, compressed = ECPubkey.from_signature65(sig65, h) # check public key using the address pubkey_hex = public_key.get_public_key_hex(compressed) @@ -403,7 +403,7 @@ class ECPrivkey(ECPubkey): raise Exception("error: cannot sign message. no recid fits..") message = to_bytes(message, 'utf8') - msg_hash = Hash(msg_magic(message)) + msg_hash = sha256d(msg_magic(message)) sig_string = self.sign(msg_hash, sigencode=sig_string_from_r_and_s, sigdecode=get_r_and_s_from_sig_string) diff --git a/electrum/keystore.py b/electrum/keystore.py index f991a4f5..a942d075 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -35,7 +35,7 @@ from .bip32 import (bip32_public_derivation, deserialize_xpub, CKD_pub, bip32_private_key, bip32_derivation, BIP32_PRIME, is_xpub, is_xprv) from .ecc import string_to_number, number_to_string -from .crypto import pw_decode, pw_encode, Hash +from .crypto import pw_decode, pw_encode, sha256d from .util import (PrintError, InvalidPassword, hfu, WalletFileException, BitcoinException, bh2u, bfh, print_error, inv_dict) from .mnemonic import Mnemonic, load_wordlist @@ -413,7 +413,7 @@ class Old_KeyStore(Deterministic_KeyStore): @classmethod def get_sequence(self, mpk, for_change, n): - return string_to_number(Hash(("%d:%d:"%(n, for_change)).encode('ascii') + bfh(mpk))) + return string_to_number(sha256d(("%d:%d:"%(n, for_change)).encode('ascii') + bfh(mpk))) @classmethod def get_pubkey_from_mpk(self, mpk, for_change, n): diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py index 0f9d4fb3..175e469c 100644 --- a/electrum/plugins/cosigner_pool/qt.py +++ b/electrum/plugins/cosigner_pool/qt.py @@ -133,7 +133,7 @@ class Plugin(BasePlugin): for key, keystore in wallet.keystores.items(): xpub = keystore.get_master_public_key() K = bip32.deserialize_xpub(xpub)[-1] - _hash = bh2u(crypto.Hash(K)) + _hash = bh2u(crypto.sha256d(K)) if not keystore.is_watching_only(): self.keys.append((key, _hash, window)) else: diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 24f676e0..bb9131c3 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -4,7 +4,7 @@ # try: - from electrum.crypto import Hash, EncodeAES, DecodeAES + from electrum.crypto import sha256d, EncodeAES, DecodeAES from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, is_address) from electrum.bip32 import serialize_xpub, deserialize_xpub @@ -395,7 +395,7 @@ class DigitalBitbox_Client(): def hid_send_encrypt(self, msg): reply = "" try: - secret = Hash(self.password) + secret = sha256d(self.password) msg = EncodeAES(secret, msg) reply = self.hid_send_plain(msg) if 'ciphertext' in reply: @@ -448,7 +448,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): try: message = message.encode('utf8') inputPath = self.get_derivation() + "/%d/%d" % sequence - msg_hash = Hash(msg_magic(message)) + msg_hash = sha256d(msg_magic(message)) inputHash = to_hexstr(msg_hash) hasharray = [] hasharray.append({'hash': inputHash, 'keypath': inputPath}) @@ -526,7 +526,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): if x_pubkey in derivations: index = derivations.get(x_pubkey) inputPath = "%s/%d/%d" % (self.get_derivation(), index[0], index[1]) - inputHash = Hash(binascii.unhexlify(tx.serialize_preimage(i))) + inputHash = sha256d(binascii.unhexlify(tx.serialize_preimage(i))) hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath} hasharray.append(hasharray_i) inputhasharray.append(inputHash) @@ -577,7 +577,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): }, } if tx_dbb_serialized is not None: - msg["sign"]["meta"] = to_hexstr(Hash(tx_dbb_serialized)) + msg["sign"]["meta"] = to_hexstr(sha256d(tx_dbb_serialized)) msg = json.dumps(msg).encode('ascii') dbb_client = self.plugin.get_client(self) diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index 399ffc2e..e11cee4e 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -11,7 +11,7 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key, from electrum.bip32 import (bip32_root, bip32_public_derivation, bip32_private_derivation, xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation, is_xpub, convert_bip32_path_to_list_of_uint32) -from electrum.crypto import Hash +from electrum.crypto import sha256d from electrum import ecc, crypto, constants from electrum.ecc import number_to_string, string_to_number from electrum.transaction import opcodes @@ -246,13 +246,9 @@ class Test_bitcoin(SequentialTestCase): enc = crypto.pw_encode(payload, password) self.assertRaises(Exception, crypto.pw_decode, enc, wrong_password) - def test_hash(self): - """Make sure the Hash function does sha256 twice""" - payload = u"test" - expected = b'\x95MZI\xfdp\xd9\xb8\xbc\xdb5\xd2R&x)\x95\x7f~\xf7\xfalt\xf8\x84\x19\xbd\xc5\xe8"\t\xf4' - - result = Hash(payload) - self.assertEqual(expected, result) + def test_sha256d(self): + self.assertEqual(b'\x95MZI\xfdp\xd9\xb8\xbc\xdb5\xd2R&x)\x95\x7f~\xf7\xfalt\xf8\x84\x19\xbd\xc5\xe8"\t\xf4', + sha256d(u"test")) def test_int_to_hex(self): self.assertEqual('00', int_to_hex(0, 1)) diff --git a/electrum/transaction.py b/electrum/transaction.py index 96bf6f99..b9134eaa 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -39,7 +39,7 @@ from .bitcoin import (TYPE_ADDRESS, TYPE_PUBKEY, TYPE_SCRIPT, hash_160, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, hash_encode, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, op_push, int_to_hex, push_script, b58_address_to_hash160) -from .crypto import Hash +from .crypto import sha256d from .keystore import xpubkey_to_address, xpubkey_to_pubkey @@ -728,7 +728,7 @@ class Transaction: sig = signatures[i] if sig in txin.get('signatures'): continue - pre_hash = Hash(bfh(self.serialize_preimage(i))) + pre_hash = sha256d(bfh(self.serialize_preimage(i))) sig_string = ecc.sig_string_from_der_sig(bfh(sig[:-2])) for recid in range(4): try: @@ -1036,9 +1036,9 @@ class Transaction: txin = inputs[i] # TODO: py3 hex if self.is_segwit_input(txin): - hashPrevouts = bh2u(Hash(bfh(''.join(self.serialize_outpoint(txin) for txin in inputs)))) - hashSequence = bh2u(Hash(bfh(''.join(int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) for txin in inputs)))) - hashOutputs = bh2u(Hash(bfh(''.join(self.serialize_output(o) for o in outputs)))) + hashPrevouts = bh2u(sha256d(bfh(''.join(self.serialize_outpoint(txin) for txin in inputs)))) + hashSequence = bh2u(sha256d(bfh(''.join(int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) for txin in inputs)))) + hashOutputs = bh2u(sha256d(bfh(''.join(self.serialize_output(o) for o in outputs)))) outpoint = self.serialize_outpoint(txin) preimage_script = self.get_preimage_script(txin) scriptCode = var_int(len(preimage_script) // 2) + preimage_script @@ -1091,14 +1091,14 @@ class Transaction: if not all_segwit and not self.is_complete(): return None ser = self.serialize_to_network(witness=False) - return bh2u(Hash(bfh(ser))[::-1]) + return bh2u(sha256d(bfh(ser))[::-1]) def wtxid(self): self.deserialize() if not self.is_complete(): return None ser = self.serialize_to_network(witness=True) - return bh2u(Hash(bfh(ser))[::-1]) + return bh2u(sha256d(bfh(ser))[::-1]) def add_inputs(self, inputs): self._inputs.extend(inputs) @@ -1219,7 +1219,7 @@ class Transaction: self.raw = self.serialize() def sign_txin(self, txin_index, privkey_bytes) -> str: - pre_hash = Hash(bfh(self.serialize_preimage(txin_index))) + pre_hash = sha256d(bfh(self.serialize_preimage(txin_index))) privkey = ecc.ECPrivkey(privkey_bytes) sig = privkey.sign_transaction(pre_hash) sig = bh2u(sig) + '01' diff --git a/electrum/verifier.py b/electrum/verifier.py index 32ea4aa4..27913ce1 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -27,7 +27,7 @@ from typing import Sequence, Optional, TYPE_CHECKING import aiorpcx from .util import bh2u, VerifiedTxInfo, NetworkJobOnDefaultServer -from .crypto import Hash +from .crypto import sha256d from .bitcoin import hash_decode, hash_encode from .transaction import Transaction from .blockchain import hash_header @@ -140,7 +140,7 @@ class SPV(NetworkJobOnDefaultServer): raise MerkleVerificationFailure(e) for i, item in enumerate(merkle_branch_bytes): - h = Hash(item + h) if ((leaf_pos_in_tree >> i) & 1) else Hash(h + item) + h = sha256d(item + h) if ((leaf_pos_in_tree >> i) & 1) else sha256d(h + item) cls._raise_if_valid_tx(bh2u(h)) return hash_encode(h) diff --git a/electrum/wallet.py b/electrum/wallet.py index 6379ef43..94486eb2 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -48,7 +48,7 @@ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script, is_minikey) from .version import * -from .crypto import Hash +from .crypto import sha256d from .keystore import load_keystore, Hardware_KeyStore from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW, WalletStorage from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32 @@ -931,7 +931,7 @@ class Abstract_Wallet(AddressSynchronizer): def make_payment_request(self, addr, amount, message, expiration): timestamp = int(time.time()) - _id = bh2u(Hash(addr + "%d"%timestamp))[0:10] + _id = bh2u(sha256d(addr + "%d"%timestamp))[0:10] r = {'time':timestamp, 'amount':amount, 'exp':expiration, 'address':addr, 'memo':message, 'id':_id} return r From 99d18a48f2e22604b8fdfbc236ce5d58e17db0c2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 25 Oct 2018 23:01:53 +0200 Subject: [PATCH 067/301] types: make some import conditional --- electrum/address_synchronizer.py | 9 ++++++--- electrum/commands.py | 8 +++++--- electrum/wallet.py | 17 ++++++++++------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index d39828cf..f74bb701 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -25,6 +25,7 @@ import threading import asyncio import itertools from collections import defaultdict +from typing import TYPE_CHECKING from . import bitcoin from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY @@ -34,8 +35,10 @@ from .synchronizer import Synchronizer from .verifier import SPV from .blockchain import hash_header from .i18n import _ -from .storage import WalletStorage -from .network import Network + +if TYPE_CHECKING: + from .storage import WalletStorage + from .network import Network TX_HEIGHT_LOCAL = -2 @@ -56,7 +59,7 @@ class AddressSynchronizer(PrintError): inherited by wallet """ - def __init__(self, storage: WalletStorage): + def __init__(self, storage: 'WalletStorage'): self.storage = storage self.network = None # type: Network # verifier (SPV) and synchronizer are started in start_network diff --git a/electrum/commands.py b/electrum/commands.py index d7550ba2..ddeb683f 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -32,7 +32,7 @@ import ast import base64 from functools import wraps from decimal import Decimal -from typing import Optional +from typing import Optional, TYPE_CHECKING from .import util, ecc from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_encode @@ -46,8 +46,10 @@ from .storage import WalletStorage from . import keystore from .wallet import Wallet, Imported_Wallet, Abstract_Wallet from .mnemonic import Mnemonic -from .network import Network -from .simple_config import SimpleConfig + +if TYPE_CHECKING: + from .network import Network + from .simple_config import SimpleConfig known_commands = {} diff --git a/electrum/wallet.py b/electrum/wallet.py index 94486eb2..9eb2617c 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -38,6 +38,7 @@ import traceback from functools import partial from numbers import Number from decimal import Decimal +from typing import TYPE_CHECKING from .i18n import _ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, @@ -59,8 +60,10 @@ from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, from .paymentrequest import (PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, InvoiceStore) from .contacts import Contacts -from .network import Network -from .simple_config import SimpleConfig + +if TYPE_CHECKING: + from .network import Network + from .simple_config import SimpleConfig TX_STATUS = [ @@ -72,18 +75,18 @@ TX_STATUS = [ -def relayfee(network: Network): +def relayfee(network: 'Network'): from .simple_config import FEERATE_DEFAULT_RELAY MAX_RELAY_FEE = 50000 f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY return min(f, MAX_RELAY_FEE) -def dust_threshold(network: Network): +def dust_threshold(network: 'Network'): # Change <= dust threshold is added to the tx fee return 182 * 3 * relayfee(network) / 1000 -def append_utxos_to_inputs(inputs, network: Network, pubkey, txin_type, imax): +def append_utxos_to_inputs(inputs, network: 'Network', pubkey, txin_type, imax): if txin_type != 'p2pk': address = bitcoin.pubkey_to_address(txin_type, pubkey) scripthash = bitcoin.address_to_scripthash(address) @@ -106,7 +109,7 @@ def append_utxos_to_inputs(inputs, network: Network, pubkey, txin_type, imax): item['num_sig'] = 1 inputs.append(item) -def sweep_preparations(privkeys, network: Network, imax=100): +def sweep_preparations(privkeys, network: 'Network', imax=100): def find_utxos_for_privkey(txin_type, privkey, compressed): pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) @@ -132,7 +135,7 @@ 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): inputs, keypairs = sweep_preparations(privkeys, network, imax) total = sum(i.get('value') for i in inputs) if fee is None: From 791e0e1a67fee73c8d1da7ca4c733a21ce1ea628 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 25 Oct 2018 23:08:59 +0200 Subject: [PATCH 068/301] move relayfee and dust_threshold to bitcoin.py --- electrum/bitcoin.py | 17 ++++++++++++++++- electrum/network.py | 2 +- electrum/wallet.py | 14 +------------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 41e83c53..abaa5ed2 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -24,7 +24,7 @@ # SOFTWARE. import hashlib -from typing import List, Tuple +from typing import List, Tuple, TYPE_CHECKING from .util import bfh, bh2u, BitcoinException, assert_bytes, to_bytes, inv_dict from . import version @@ -33,6 +33,9 @@ from . import constants from . import ecc from .crypto import sha256d, sha256, hash_160, hmac_oneshot +if TYPE_CHECKING: + from .network import Network + ################################## transactions @@ -147,6 +150,18 @@ def add_number_to_script(i: int) -> bytes: return bfh(push_script(script_num_to_hex(i))) +def relayfee(network: 'Network'=None) -> int: + from .simple_config import FEERATE_DEFAULT_RELAY + MAX_RELAY_FEE = 50000 + f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY + return min(f, MAX_RELAY_FEE) + + +def dust_threshold(network: 'Network'=None) -> int: + # Change <= dust threshold is added to the tx fee + return 182 * 3 * relayfee(network) // 1000 + + hash_encode = lambda x: bh2u(x[::-1]) hash_decode = lambda x: bfh(x)[::-1] hmac_sha_512 = lambda x, y: hmac_oneshot(x, y, hashlib.sha512) diff --git a/electrum/network.py b/electrum/network.py index 9be6813b..85fb3162 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -201,7 +201,7 @@ class Network(PrintError): self.banner = '' self.donation_address = '' - self.relay_fee = None + self.relay_fee = None # type: Optional[int] # callbacks set by the GUI self.callbacks = defaultdict(list) # note: needs self.callback_lock diff --git a/electrum/wallet.py b/electrum/wallet.py index 9eb2617c..f75e093c 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -47,7 +47,7 @@ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, Fiat, bfh, bh2u) from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script, - is_minikey) + is_minikey, relayfee, dust_threshold) from .version import * from .crypto import sha256d from .keystore import load_keystore, Hardware_KeyStore @@ -74,18 +74,6 @@ TX_STATUS = [ ] - -def relayfee(network: 'Network'): - from .simple_config import FEERATE_DEFAULT_RELAY - MAX_RELAY_FEE = 50000 - f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY - return min(f, MAX_RELAY_FEE) - -def dust_threshold(network: 'Network'): - # Change <= dust threshold is added to the tx fee - return 182 * 3 * relayfee(network) / 1000 - - def append_utxos_to_inputs(inputs, network: 'Network', pubkey, txin_type, imax): if txin_type != 'p2pk': address = bitcoin.pubkey_to_address(txin_type, pubkey) From 2aefc8440a8964c95df4cff4a311cf26c9eb1ae3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 Oct 2018 15:34:46 +0200 Subject: [PATCH 069/301] travis: make sure to have latest tag The Win/Mac build scripts name the binaries using "git describe --tags", so reproducibility requires git to find the latest tag. By default, Travis uses depth=50. --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5f437297..993a416c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,16 @@ language: python python: - 3.6 - 3.7 +git: + depth: false addons: apt: sources: - sourceline: 'ppa:tah83/secp256k1' packages: - libsecp256k1-0 +before_install: + - git tag install: - pip install -r contrib/requirements/requirements-travis.txt cache: From 92d16e8b103964d247561e7eb8f560a47b3f41db Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 Oct 2018 15:56:08 +0200 Subject: [PATCH 070/301] follow-up prev: unshallow no longer needed --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 993a416c..1c222c4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,14 +47,12 @@ jobs: python: false install: - git fetch --all --tags - - git fetch origin --unshallow script: ./contrib/build-osx/make_osx after_script: ls -lah dist && md5 dist/* after_success: true - stage: release check install: - git fetch --all --tags - - git fetch origin --unshallow script: - ./contrib/deterministic-build/check_submodules.sh after_success: true From 263c9265ae99012aae499fd5f4ffa27a616685be Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 Oct 2018 16:56:23 +0200 Subject: [PATCH 071/301] rerun freeze packages --- .../requirements-binaries.txt | 12 +-- .../deterministic-build/requirements-hw.txt | 92 ++++++++++--------- contrib/deterministic-build/requirements.txt | 48 +++++----- 3 files changed, 79 insertions(+), 73 deletions(-) diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt index 574c71a6..ebe156c8 100644 --- a/contrib/deterministic-build/requirements-binaries.txt +++ b/contrib/deterministic-build/requirements-binaries.txt @@ -1,6 +1,6 @@ -pip==18.0 \ - --hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \ - --hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76 +pip==18.1 \ + --hash=sha256:7909d0a0932e88ea53a7014dfd14522ffef91a464daaaf5c573343852ef98550 \ + --hash=sha256:c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1 pycryptodomex==3.6.6 \ --hash=sha256:0cf562fc5e5ddbe935bb6162d84a7e46e19edba8ac6609587ab9e78dc7c527d4 \ --hash=sha256:13b77b7a177a2fd0beb42db84b21d7d4ab646dfd223989a3b5fa6a6901075ae8 \ @@ -53,6 +53,6 @@ SIP==4.19.8 \ --hash=sha256:cf98150a99e43fda7ae22abe655b6f202e491d6291486548daa56cb15a2fcf85 \ --hash=sha256:d9023422127b94d11c1a84bfa94933e959c484f2c79553c1ef23c69fe00d25f8 \ --hash=sha256:e72955e12f4fccf27aa421be383453d697b8a44bde2cc26b08d876fd492d0174 -wheel==0.31.1 \ - --hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \ - --hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f +wheel==0.32.2 \ + --hash=sha256:196c9842d79262bb66fcf59faa4bd0deb27da911dbc7c6cdca931080eb1f0783 \ + --hash=sha256:c93e2d711f5f9841e17f53b0e6c0ff85593f3b416b6eec7a9452041a59a42688 diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 10ad43ca..d3c9c4e3 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -1,8 +1,8 @@ btchip-python==0.1.28 \ --hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83 -certifi==2018.8.24 \ - --hash=sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638 \ - --hash=sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a +certifi==2018.10.15 \ + --hash=sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c \ + --hash=sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a chardet==3.0.4 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 @@ -12,41 +12,43 @@ ckcc-protocol==0.7.2 \ click==7.0 \ --hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \ --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 -Cython==0.28.5 \ - --hash=sha256:022592d419fc754509d0e0461eb2958dbaa45fb60d51c8a61778c58994edbe36 \ - --hash=sha256:07659f4c57582104d9486c071de512fbd7e087a3a630535298442cc0e20a3f5a \ - --hash=sha256:13c73e2ffa93a615851e03fad97591954d143b5b62361b9adef81f46a31cd8ef \ - --hash=sha256:13eab5a2835a84ff62db343035603044c908d2b3b6eec09d67fdf9970acf7ac9 \ - --hash=sha256:183b35a48f58862c4ec1e821f07bb7b1156c8c8559c85c32ae086f28947474eb \ - --hash=sha256:2f526b0887128bf20ab2acc905a975f62b5a04ab2f63ecbe5a30fc28285d0e0c \ - --hash=sha256:32de8637f5e6c5a76667bc7c8fc644bd9314dc19af36db8ce30a0b92ada0f642 \ - --hash=sha256:4172c183ef4fb2ace6a29cdf7fc9200c5a471a7f775ff691975b774bd9ed3ad2 \ - --hash=sha256:553956ec06ecbd731ef0c538eb28a5b46bedea7ab89b18237ff28b4b99d65eee \ - --hash=sha256:660eeb6870687fd3eda91e00ba4e72220545c254c8c4d967fd0c910f4fbb8cbc \ - --hash=sha256:693a8619ef066ece055ed065a15cf440f9d3ebd1bca60e87ea19144833756433 \ - --hash=sha256:759c799e9ef418f163b5412e295e14c0a48fe3b4dcba9ab8aab69e9f511cfefd \ - --hash=sha256:827d3a91b7a7c31ce69e5974496fd9a8ba28eb498b988affb66d0d30de11d934 \ - --hash=sha256:87e57b5d730cfab225d95e7b23abbc0c6f77598bd66639e93c73ce8afbae6f38 \ - --hash=sha256:9400e5db8383346b0694a3e794d8bded18a27b21123516dcdf4b79d7ec28e98b \ - --hash=sha256:9ec27681c5b1b457aacb1cbda5db04aa28b76da2af6e1e1fd15f233eafe6a0b0 \ - --hash=sha256:ae4784f040a3313c8bd00c8d04934b7ade63dc59692d8f00a5235be8ed72a445 \ - --hash=sha256:b2ba8310ebd3c0e0b884d5e95bbd99d467d6af922acd1e44fe4b819839b2150e \ - --hash=sha256:b64575241f64f6ec005a4d4137339fb0ba5e156e826db2fdb5f458060d9979e0 \ - --hash=sha256:c78ad0df75a9fc03ab28ca1b950c893a208c451a18f76796c3e25817d6994001 \ - --hash=sha256:cdbb917e41220bd3812234dbe59d15391adbc2c5d91ae11a5273aab9e32ba7ec \ - --hash=sha256:d2223a80c623e2a8e97953ab945dfaa9385750a494438dcb55562eb1ddd9565a \ - --hash=sha256:e22f21cf92a9f8f007a280e3b3462c886d9068132a6c698dec10ad6125e3ca1e \ - --hash=sha256:ea5c16c48e561f4a6f6b8c24807494b77a79e156b8133521c400f22ca712101b \ - --hash=sha256:ee7a9614d51fe16e32ca5befe72e0808baff481791728449d0b17c8b0fe29eb9 \ - --hash=sha256:ef86de9299e4ab2ebb129fb84b886bf40b9aced9807c6d6d5f28b46fb905f82c \ - --hash=sha256:f3e4860f5458a9875caa3de65e255720c0ed2ce71f0bcdab02497b32104f9db8 \ - --hash=sha256:fc6c20a8ac22202a779ad4c59756647be0826993d2151a03c015e76d2368ae5f +Cython==0.29 \ + --hash=sha256:019008a69e6b7c102f2ed3d733a288d1784363802b437dd2b91e6256b12746da \ + --hash=sha256:1441fe19c56c90b8c2159d7b861c31a134d543ef7886fd82a5d267f9f11f35ac \ + --hash=sha256:1d1a5e9d6ed415e75a676b72200ad67082242ec4d2d76eb7446da255ae72d3f7 \ + --hash=sha256:339f5b985de3662b1d6c69991ab46fdbdc736feb4ac903ef6b8c00e14d87f4d8 \ + --hash=sha256:35bdf3f48535891fee2eaade70e91d5b2cc1ee9fc2a551847c7ec18bce55a92c \ + --hash=sha256:3d0afba0aec878639608f013045697fb0969ff60b3aea2daec771ea8d01ad112 \ + --hash=sha256:42c53786806e24569571a7a24ebe78ec6b364fe53e79a3f27eddd573cacd398f \ + --hash=sha256:48b919da89614d201e72fbd8247b5ae8881e296cf968feb5595a015a14c67f1f \ + --hash=sha256:49906e008eeb91912654a36c200566392bd448b87a529086694053a280f8af2d \ + --hash=sha256:49fc01a7c9c4e3c1784e9a15d162c2cac3990fcc28728227a6f8f0837aabda7c \ + --hash=sha256:501b671b639b9ca17ad303f8807deb1d0ff754d1dab106f2607d14b53cb0ff0b \ + --hash=sha256:5574574142364804423ab4428bd331a05c65f7ecfd31ac97c936f0c720fe6a53 \ + --hash=sha256:6092239a772b3c6604be9e94b9ab4f0dacb7452e8ad299fd97eae0611355b679 \ + --hash=sha256:71ff5c7632501c4f60edb8a24fd0a772e04c5bdca2856d978d04271b63666ef7 \ + --hash=sha256:7dcf2ad14e25b05eda8bdd104f8c03a642a384aeefd25a5b51deac0826e646fa \ + --hash=sha256:8ca3a99f5a7443a6a8f83a5d8fcc11854b44e6907e92ba8640d8a8f7b9085e21 \ + --hash=sha256:927da3b5710fb705aab173ad630b45a4a04c78e63dcd89411a065b2fe60e4770 \ + --hash=sha256:94916d1ede67682638d3cc0feb10648ff14dc51fb7a7f147f4fedce78eaaea97 \ + --hash=sha256:a3e5e5ca325527d312cdb12a4dab8b0459c458cad1c738c6f019d0d8d147081c \ + --hash=sha256:a7716a98f0b9b8f61ddb2bae7997daf546ac8fc594be6ba397f4bde7d76bfc62 \ + --hash=sha256:acf10d1054de92af8d5bfc6620bb79b85f04c98214b4da7db77525bfa9fc2a89 \ + --hash=sha256:de46ffb67e723975f5acab101c5235747af1e84fbbc89bf3533e2ea93fb26947 \ + --hash=sha256:df428969154a9a4cd9748c7e6efd18432111fbea3d700f7376046c38c5e27081 \ + --hash=sha256:f5ebf24b599caf466f9da8c4115398d663b2567b89e92f58a835e9da4f74669f \ + --hash=sha256:f79e45d5c122c4fb1fd54029bf1d475cecc05f4ed5b68136b0d6ec268bae68b6 \ + --hash=sha256:f7a43097d143bd7846ffba6d2d8cd1cc97f233318dbd0f50a235ea01297a096b \ + --hash=sha256:fceb8271bc2fd3477094ca157c824e8ea840a7b393e89e766eea9a3b9ce7e0c6 \ + --hash=sha256:ff919ceb40259f5332db43803aa6c22ff487e86036ce3921ae04b9185efc99a4 ecdsa==0.13 \ --hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \ --hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa hidapi==0.7.99.post21 \ --hash=sha256:1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24 \ + --hash=sha256:6424ad75da0021ce8c1bcd78056a04adada303eff3c561f8d132b85d0a914cb3 \ --hash=sha256:8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946 \ + --hash=sha256:92878bad7324dee619b7832fbfc60b5360d378aa7c5addbfef0a410d8fd342c7 \ --hash=sha256:b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87 \ --hash=sha256:bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660 \ --hash=sha256:c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7 \ @@ -65,15 +67,17 @@ mnemonic==0.18 \ --hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d pbkdf2==1.3 \ --hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979 -pip==18.0 \ - --hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \ - --hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76 +pip==18.1 \ + --hash=sha256:7909d0a0932e88ea53a7014dfd14522ffef91a464daaaf5c573343852ef98550 \ + --hash=sha256:c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1 protobuf==3.6.1 \ --hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \ --hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \ --hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \ --hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \ --hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \ + --hash=sha256:4b92e235a3afd42e7493b281c8b80c0c65cbef45de30f43d571d1ee40a1f77ef \ + --hash=sha256:574085a33ca0d2c67433e5f3e9a0965c487410d6cb3406c83bdaf549bfc2992e \ --hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \ --hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \ --hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \ @@ -96,9 +100,9 @@ pyblake2==1.1.2 \ --hash=sha256:baa2190bfe549e36163aa44664d4ee3a9080b236fc5d42f50dc6fd36bbdc749e \ --hash=sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4 \ --hash=sha256:fbc9fcde75713930bc2a91b149e97be2401f7c9c56d735b46a109210f58d7358 -requests==2.19.1 \ - --hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \ - --hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a +requests==2.20.0 \ + --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ + --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 safet==0.1.4 \ --hash=sha256:522c257910f9472e9c77c487425ed286f6721c314653e232bc41c6cedece1bb1 \ --hash=sha256:b152874acdc89ff0c8b2d680bfbf020b3e53527c2ad3404489dd61a548aa56a1 @@ -111,12 +115,12 @@ six==1.11.0 \ trezor==0.10.2 \ --hash=sha256:4dba4d5c53d3ca22884d79fb4aa68905fb8353a5da5f96c734645d8cf537138d \ --hash=sha256:d2b32f25982ab403758d870df1d0de86d0751c106ef1cd1289f452880ce68b84 -urllib3==1.23 \ - --hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \ - --hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5 +urllib3==1.24 \ + --hash=sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae \ + --hash=sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59 websocket-client==0.53.0 \ --hash=sha256:c42b71b68f9ef151433d6dcc6a7cb98ac72d2ad1e3a74981ca22bc5d9134f166 \ --hash=sha256:f5889b1d0a994258cfcbc8f2dc3e457f6fc7b32a8d74873033d12e4eab4bdf63 -wheel==0.31.1 \ - --hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \ - --hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f +wheel==0.32.2 \ + --hash=sha256:196c9842d79262bb66fcf59faa4bd0deb27da911dbc7c6cdca931080eb1f0783 \ + --hash=sha256:c93e2d711f5f9841e17f53b0e6c0ff85593f3b416b6eec7a9452041a59a42688 diff --git a/contrib/deterministic-build/requirements.txt b/contrib/deterministic-build/requirements.txt index 21d8086e..8648eb7e 100644 --- a/contrib/deterministic-build/requirements.txt +++ b/contrib/deterministic-build/requirements.txt @@ -21,20 +21,20 @@ aiohttp==3.4.4 \ --hash=sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6 \ --hash=sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0 \ --hash=sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07 -aiohttp_socks==0.1.6 \ - --hash=sha256:943148a3797ba9ffb6df6ddb006ffdd40538885b410589d589bda42a8e8bcd5a +aiohttp_socks==0.2 \ + --hash=sha256:eba0a6e198d9a69d254bf956d68cec7615c2a4cadd861b8da46464bd13c5641d aiorpcX==0.8.2 \ --hash=sha256:980d1d85a831688163ad087a1c1a88b6695a06e5e9914824676bab4251b2b1f2 \ --hash=sha256:e53ff8917a87843875526be1261d80171f5ad09187917ff29dfdc003c1526a65 -async_timeout==3.0.0 \ - --hash=sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c \ - --hash=sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287 +async_timeout==3.0.1 \ + --hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \ + --hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3 attrs==18.2.0 \ --hash=sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69 \ --hash=sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb -certifi==2018.8.24 \ - --hash=sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638 \ - --hash=sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a +certifi==2018.10.15 \ + --hash=sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c \ + --hash=sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a chardet==3.0.4 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 @@ -82,15 +82,17 @@ multidict==4.4.2 \ --hash=sha256:e8848ae3cd6a784c29fae5055028bee9bffcc704d8bcad09bd46b42b44a833e2 \ --hash=sha256:e8a048bfd7d5a280f27527d11449a509ddedf08b58a09a24314828631c099306 \ --hash=sha256:f6dd28a0ac60e2426a6918f36f1b4e2620fc785a0de7654cd206ba842eee57fd -pip==18.0 \ - --hash=sha256:070e4bf493c7c2c9f6a08dd797dd3c066d64074c38e9e8a0fb4e6541f266d96c \ - --hash=sha256:a0e11645ee37c90b40c46d607070c4fd583e2cd46231b1c06e389c5e814eed76 +pip==18.1 \ + --hash=sha256:7909d0a0932e88ea53a7014dfd14522ffef91a464daaaf5c573343852ef98550 \ + --hash=sha256:c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1 protobuf==3.6.1 \ --hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \ --hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \ --hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \ --hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \ --hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \ + --hash=sha256:4b92e235a3afd42e7493b281c8b80c0c65cbef45de30f43d571d1ee40a1f77ef \ + --hash=sha256:574085a33ca0d2c67433e5f3e9a0965c487410d6cb3406c83bdaf549bfc2992e \ --hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \ --hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \ --hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \ @@ -109,21 +111,21 @@ QDarkStyle==2.5.4 \ qrcode==6.0 \ --hash=sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf \ --hash=sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3 -requests==2.19.1 \ - --hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \ - --hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a +requests==2.20.0 \ + --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ + --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 setuptools==40.4.3 \ --hash=sha256:acbc5740dd63f243f46c2b4b8e2c7fd92259c2ddb55a4115b16418a2ed371b15 \ --hash=sha256:ce4137d58b444bac11a31d4e0c1805c69d89e8ed4e91fde1999674ecc2f6f9ff six==1.11.0 \ --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \ --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb -urllib3==1.23 \ - --hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \ - --hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5 -wheel==0.31.1 \ - --hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \ - --hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f +urllib3==1.24 \ + --hash=sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae \ + --hash=sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59 +wheel==0.32.2 \ + --hash=sha256:196c9842d79262bb66fcf59faa4bd0deb27da911dbc7c6cdca931080eb1f0783 \ + --hash=sha256:c93e2d711f5f9841e17f53b0e6c0ff85593f3b416b6eec7a9452041a59a42688 yarl==1.2.6 \ --hash=sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9 \ --hash=sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee \ @@ -134,6 +136,6 @@ yarl==1.2.6 \ --hash=sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1 \ --hash=sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4 \ --hash=sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7 -colorama==0.3.9 \ - --hash=sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda \ - --hash=sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1 +colorama==0.4.0 \ + --hash=sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3 \ + --hash=sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c From bcdb0c46fc13c00dcd3d6baf8045199aab54b4c5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 Oct 2018 17:06:42 +0200 Subject: [PATCH 072/301] update to aiorpcx 0.9 and require it --- contrib/deterministic-build/requirements.txt | 7 ++++--- contrib/requirements/requirements.txt | 2 +- electrum/interface.py | 16 ++++++++++------ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/contrib/deterministic-build/requirements.txt b/contrib/deterministic-build/requirements.txt index 8648eb7e..a54f984c 100644 --- a/contrib/deterministic-build/requirements.txt +++ b/contrib/deterministic-build/requirements.txt @@ -23,9 +23,10 @@ aiohttp==3.4.4 \ --hash=sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07 aiohttp_socks==0.2 \ --hash=sha256:eba0a6e198d9a69d254bf956d68cec7615c2a4cadd861b8da46464bd13c5641d -aiorpcX==0.8.2 \ - --hash=sha256:980d1d85a831688163ad087a1c1a88b6695a06e5e9914824676bab4251b2b1f2 \ - --hash=sha256:e53ff8917a87843875526be1261d80171f5ad09187917ff29dfdc003c1526a65 +aiorpcX==0.9.0 \ + --hash=sha256:4ad259076a3c94da5265505ef698d04a6d5a92d09e91d2296b5cc09d7d0f0c2c \ + --hash=sha256:71bfd014669bec0ffe2e1b82c1978b2c66330ce5adb3162529a6e066531703e7 \ + --hash=sha256:df621d8a434d4354554496c1e2db74056c88c7e9742cb3e343a22acca27dfc50 async_timeout==3.0.1 \ --hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \ --hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3 diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt index 854ecdae..ab033e67 100644 --- a/contrib/requirements/requirements.txt +++ b/contrib/requirements/requirements.txt @@ -6,6 +6,6 @@ protobuf dnspython jsonrpclib-pelix qdarkstyle<3.0 -aiorpcx>=0.8.2,<0.9 +aiorpcx>=0.9,<0.10 aiohttp aiohttp_socks diff --git a/electrum/interface.py b/electrum/interface.py index 491f60b6..3141fbec 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -32,7 +32,7 @@ from typing import Tuple, Union, List, TYPE_CHECKING from collections import defaultdict import aiorpcx -from aiorpcx import ClientSession, Notification +from aiorpcx import RPCSession, Notification from .util import PrintError, ignore_exceptions, log_exceptions, bfh, SilentTaskGroup from . import util @@ -47,7 +47,7 @@ if TYPE_CHECKING: from .network import Network -class NotificationSession(ClientSession): +class NotificationSession(RPCSession): def __init__(self, *args, **kwargs): super(NotificationSession, self).__init__(*args, **kwargs) @@ -71,7 +71,7 @@ class NotificationSession(ClientSession): async def send_request(self, *args, timeout=-1, **kwargs): # note: the timeout starts after the request touches the wire! if timeout == -1: - timeout = 20 if not self.proxy else 30 + timeout = 30 # note: the semaphore implementation guarantees no starvation async with self.in_flight_requests_semaphore: try: @@ -307,7 +307,9 @@ class Interface(PrintError): async def get_certificate(self): sslc = ssl.SSLContext() try: - async with aiorpcx.ClientSession(self.host, self.port, ssl=sslc, proxy=self.proxy) as session: + async with aiorpcx.Connector(RPCSession, + host=self.host, port=self.port, + ssl=sslc, proxy=self.proxy) as session: return session.transport._ssl_protocol._sslpipe._sslobj.getpeercert(True) except ValueError: return None @@ -340,8 +342,10 @@ class Interface(PrintError): return conn, res['count'] async def open_session(self, sslc, exit_early=False): - self.session = NotificationSession(self.host, self.port, ssl=sslc, proxy=self.proxy) - async with self.session as session: + async with aiorpcx.Connector(NotificationSession, + host=self.host, port=self.port, + ssl=sslc, proxy=self.proxy) as session: + self.session = session # type: NotificationSession try: ver = await session.send_request('server.version', [ELECTRUM_VERSION, PROTOCOL_VERSION]) except aiorpcx.jsonrpc.RPCError as e: From 78258a3a950921dd7e160eec7a8ced7055e8a7e6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 Oct 2018 18:45:36 +0200 Subject: [PATCH 073/301] fix #4802 --- electrum/transaction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/transaction.py b/electrum/transaction.py index b9134eaa..1f1271f4 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1067,6 +1067,7 @@ class Transaction: return network_ser def serialize_to_network(self, estimate_size=False, witness=True): + self.deserialize() nVersion = int_to_hex(self.version, 4) nLocktime = int_to_hex(self.locktime, 4) inputs = self.inputs() From 416b68705493a232818c28817129a126791147e8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 Oct 2018 19:31:20 +0200 Subject: [PATCH 074/301] storage: add a sanity check see #4803 --- electrum/storage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/storage.py b/electrum/storage.py index 22b4fb61..16a4cc90 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -175,6 +175,8 @@ class WalletStorage(JsonDB): self.print_error('Failed to convert label to json format', key) continue self.data[key] = value + if not isinstance(self.data, dict): + raise WalletFileException("Malformed wallet file (not dict)") # check here if I need to load a plugin t = self.get('wallet_type') From 917b7fa898479a80d19633279ced4d539819b8c2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 Oct 2018 22:43:33 +0200 Subject: [PATCH 075/301] network shutdown safety belts --- electrum/daemon.py | 1 - electrum/network.py | 33 ++++++++++++++++++--------------- electrum/util.py | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index 36e60872..dea5a8c1 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -302,7 +302,6 @@ class Daemon(DaemonThread): if self.network: self.print_error("shutting down network") self.network.stop() - self.network.join() self.on_stop() def stop(self): diff --git a/electrum/network.py b/electrum/network.py index 85fb3162..00c96a6f 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -836,29 +836,32 @@ class Network(PrintError): self._jobs.append(job) await self.main_taskgroup.spawn(job) + @log_exceptions async def _stop(self, full_shutdown=False): self.print_error("stopping network") try: await asyncio.wait_for(self.main_taskgroup.cancel_remaining(), timeout=2) - except asyncio.TimeoutError: pass - self.main_taskgroup = None - - assert self.interface is None - assert not self.interfaces - self.connecting.clear() - self.server_queue = None - self.trigger_callback('network_updated') - - if full_shutdown: - self._run_forever.set_result(1) + except (asyncio.TimeoutError, asyncio.CancelledError) as e: + self.print_error(f"exc during main_taskgroup cancellation: {repr(e)}") + try: + self.main_taskgroup = None + self.interface = None # type: Interface + self.interfaces = {} # type: Dict[str, Interface] + self.connecting.clear() + self.server_queue = None + if not full_shutdown: + self.trigger_callback('network_updated') + finally: + if full_shutdown: + self._run_forever.set_result(1) def stop(self): assert self._thread != threading.current_thread(), 'must not be called from network thread' fut = asyncio.run_coroutine_threadsafe(self._stop(full_shutdown=True), self.asyncio_loop) - fut.result() - - def join(self): - self._thread.join(1) + try: + fut.result(timeout=2) + except (asyncio.TimeoutError, asyncio.CancelledError): pass + self._thread.join(timeout=1) async def _ensure_there_is_a_main_interface(self): if self.is_connected(): diff --git a/electrum/util.py b/electrum/util.py index b27ebdf9..7ac77dfe 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -882,7 +882,7 @@ def log_exceptions(func): raise except BaseException as e: print_ = self.print_error if hasattr(self, 'print_error') else print_error - print_("Exception in", func.__name__, ":", e.__class__.__name__, repr(e)) + print_("Exception in", func.__name__, ":", repr(e)) try: traceback.print_exc(file=sys.stderr) except BaseException as e2: From 34569d172ff797047d11ae6f6bb6f95a9189879b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 27 Oct 2018 17:36:10 +0200 Subject: [PATCH 076/301] wallet: make importing thousands of addr/privkeys fast fixes #3101 closes #3106 closes #3113 --- electrum/base_wizard.py | 14 +++-- electrum/commands.py | 16 ++++-- electrum/gui/qt/main_window.py | 29 +++++----- electrum/tests/test_wallet_vertical.py | 6 +-- electrum/wallet.py | 73 ++++++++++++++++---------- 5 files changed, 84 insertions(+), 54 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 94cf53f8..485126d9 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -189,17 +189,23 @@ class BaseWizard(object): # will be reflected on self.storage if keystore.is_address_list(text): w = Imported_Wallet(self.storage) - for x in text.split(): - w.import_address(x) + addresses = text.split() + good_inputs, bad_inputs = w.import_addresses(addresses) elif keystore.is_private_key_list(text): k = keystore.Imported_KeyStore({}) self.storage.put('keystore', k.dump()) w = Imported_Wallet(self.storage) - for x in keystore.get_private_keys(text): - w.import_private_key(x, None) + keys = keystore.get_private_keys(text) + good_inputs, bad_inputs = w.import_private_keys(keys, None) self.keystores.append(w.keystore) else: return self.terminate() + if bad_inputs: + msg = "\n".join(f"{key[:10]}... ({msg})" for key, msg in bad_inputs[:10]) + if len(bad_inputs) > 10: msg += '\n...' + self.show_error(_("The following inputs could not be imported") + + f' ({len(bad_inputs)}):\n' + msg) + # FIXME what if len(good_inputs) == 0 ? return self.run('create_wallet') def restore_from_key(self): diff --git a/electrum/commands.py b/electrum/commands.py index ddeb683f..40a8142c 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -166,14 +166,20 @@ class Commands: text = text.strip() if keystore.is_address_list(text): wallet = Imported_Wallet(storage) - for x in text.split(): - wallet.import_address(x) + addresses = text.split() + good_inputs, bad_inputs = wallet.import_addresses(addresses) + # FIXME tell user about bad_inputs + if not good_inputs: + raise Exception("None of the given addresses can be imported") elif keystore.is_private_key_list(text, allow_spaces_inside_key=False): k = keystore.Imported_KeyStore({}) storage.put('keystore', k.dump()) wallet = Imported_Wallet(storage) - for x in text.split(): - wallet.import_private_key(x, password) + keys = keystore.get_private_keys(text) + good_inputs, bad_inputs = wallet.import_private_keys(keys, password) + # FIXME tell user about bad_inputs + if not good_inputs: + raise Exception("None of the given privkeys can be imported") else: if keystore.is_seed(text): k = keystore.from_seed(text, passphrase) @@ -435,7 +441,7 @@ class Commands: try: addr = self.wallet.import_private_key(privkey, password) out = "Keypair imported: " + addr - except BaseException as e: + except Exception as e: out = "Error: " + str(e) return out diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index b0d272e5..bade2a30 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2612,19 +2612,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True) if not text: return - bad = [] - good = [] - for key in str(text).split(): - try: - addr = func(key) - good.append(addr) - except BaseException as e: - bad.append(key) - continue - if good: - self.show_message(_("The following addresses were added") + ':\n' + '\n'.join(good)) - if bad: - self.show_critical(_("The following inputs could not be imported") + ':\n'+ '\n'.join(bad)) + keys = str(text).split() + good_inputs, bad_inputs = func(keys) + if good_inputs: + msg = '\n'.join(good_inputs[:10]) + if len(good_inputs) > 10: msg += '\n...' + self.show_message(_("The following addresses were added") + + f' ({len(good_inputs)}):\n' + msg) + if bad_inputs: + msg = "\n".join(f"{key[:10]}... ({msg})" for key, msg in bad_inputs[:10]) + if len(bad_inputs) > 10: msg += '\n...' + self.show_error(_("The following inputs could not be imported") + + f' ({len(bad_inputs)}):\n' + msg) self.address_list.update() self.history_list.update() @@ -2632,7 +2631,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if not self.wallet.can_import_address(): return title, msg = _('Import addresses'), _("Enter addresses")+':' - self._do_import(title, msg, self.wallet.import_address) + self._do_import(title, msg, self.wallet.import_addresses) @protected def do_import_privkey(self, password): @@ -2642,7 +2641,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): header_layout = QHBoxLayout() header_layout.addWidget(QLabel(_("Enter private keys")+':')) header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight) - self._do_import(title, header_layout, lambda x: self.wallet.import_private_key(x, password)) + self._do_import(title, header_layout, lambda x: self.wallet.import_private_keys(x, password)) def update_fiat(self): b = self.fx and self.fx.is_enabled() diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index c6275886..ba9eb90a 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -1158,7 +1158,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): @mock.patch.object(storage.WalletStorage, '_write') def test_sending_offline_wif_online_addr_p2pkh(self, mock_write): # compressed pubkey wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) - wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', pw=None) + wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG') @@ -1192,7 +1192,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): @mock.patch.object(storage.WalletStorage, '_write') def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_write): wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) - wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', pw=None) + wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8') @@ -1226,7 +1226,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): @mock.patch.object(storage.WalletStorage, '_write') def test_sending_offline_wif_online_addr_p2wpkh(self, mock_write): wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) - wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', pw=None) + wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False) wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529') diff --git a/electrum/wallet.py b/electrum/wallet.py index f75e093c..fc962daa 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -38,7 +38,7 @@ import traceback from functools import partial from numbers import Number from decimal import Decimal -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional, Tuple from .i18n import _ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, @@ -1227,16 +1227,29 @@ class Imported_Wallet(Simple_Wallet): def get_change_addresses(self): return [] - def import_address(self, address): - if not bitcoin.is_address(address): - return '' - if address in self.addresses: - return '' - self.addresses[address] = {} - self.add_address(address) + def import_addresses(self, addresses: List[str]) -> Tuple[List[str], List[Tuple[str, str]]]: + good_addr = [] # type: List[str] + bad_addr = [] # type: List[Tuple[str, str]] + for address in addresses: + if not bitcoin.is_address(address): + bad_addr.append((address, _('invalid address'))) + continue + if address in self.addresses: + bad_addr.append((address, _('address already in wallet'))) + continue + good_addr.append(address) + self.addresses[address] = {} + self.add_address(address) self.save_addresses() self.save_transactions(write=True) - return address + return good_addr, bad_addr + + def import_address(self, address: str) -> str: + good_addr, bad_addr = self.import_addresses([address]) + if good_addr and good_addr[0] == address: + return address + else: + raise BitcoinException(str(bad_addr[0][1])) def delete_address(self, address): if address not in self.addresses: @@ -1293,28 +1306,34 @@ class Imported_Wallet(Simple_Wallet): def get_public_key(self, address): return self.addresses[address].get('pubkey') - def import_private_key(self, sec, pw, redeem_script=None): - try: - txin_type, pubkey = self.keystore.import_privkey(sec, pw) - except Exception: - neutered_privkey = str(sec)[:3] + '..' + str(sec)[-2:] - raise BitcoinException('Invalid private key: {}'.format(neutered_privkey)) - if txin_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: - if redeem_script is not None: - raise BitcoinException('Cannot use redeem script with script type {}'.format(txin_type)) + def import_private_keys(self, keys: List[str], password: Optional[str]) -> Tuple[List[str], + List[Tuple[str, str]]]: + good_addr = [] # type: List[str] + bad_keys = [] # type: List[Tuple[str, str]] + for key in keys: + try: + txin_type, pubkey = self.keystore.import_privkey(key, password) + except Exception: + bad_keys.append((key, _('invalid private key'))) + continue + if txin_type not in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'): + bad_keys.append((key, _('not implemented type') + f': {txin_type}')) + continue addr = bitcoin.pubkey_to_address(txin_type, pubkey) - elif txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: - if redeem_script is None: - raise BitcoinException('Redeem script required for script type {}'.format(txin_type)) - addr = bitcoin.redeem_script_to_address(txin_type, redeem_script) - else: - raise NotImplementedError(txin_type) - self.addresses[addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':redeem_script} + good_addr.append(addr) + self.addresses[addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':None} + self.add_address(addr) self.save_keystore() - self.add_address(addr) self.save_addresses() self.save_transactions(write=True) - return addr + return good_addr, bad_keys + + def import_private_key(self, key: str, password: Optional[str]) -> str: + good_addr, bad_keys = self.import_private_keys([key], password=password) + if good_addr: + return good_addr[0] + else: + raise BitcoinException(str(bad_keys[0][1])) def get_redeem_script(self, address): d = self.addresses[address] From 9037f25da13873c751a1a333b43bae29296b0c13 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 28 Oct 2018 00:28:29 +0200 Subject: [PATCH 077/301] kill old-style namedtuples --- electrum/coinchooser.py | 18 ++++++++++-------- electrum/gui/qt/util.py | 11 ++++++++--- electrum/network.py | 11 ++++++----- electrum/plugin.py | 23 ++++++++++++++++------- electrum/plugins/coldcard/coldcard.py | 4 ++-- electrum/plugins/hw_wallet/plugin.py | 13 ++++++------- electrum/transaction.py | 22 ++++++++++++++-------- electrum/util.py | 20 ++++++++++++-------- 8 files changed, 74 insertions(+), 48 deletions(-) diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index 5a69cccf..29cfc719 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -22,8 +22,9 @@ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from collections import defaultdict, namedtuple +from collections import defaultdict from math import floor, log10 +from typing import NamedTuple, List from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address from .transaction import Transaction, TxOutput @@ -68,13 +69,14 @@ class PRNG: x[i], x[j] = x[j], x[i] -Bucket = namedtuple('Bucket', - ['desc', - 'weight', # as in BIP-141 - 'value', # in satoshis - 'coins', # UTXOs - 'min_height', # min block height where a coin was confirmed - 'witness']) # whether any coin uses segwit +class Bucket(NamedTuple): + desc: str + weight: int # as in BIP-141 + value: int # in satoshis + coins: List[dict] # UTXOs + min_height: int # min block height where a coin was confirmed + witness: bool # whether any coin uses segwit + def strip_unneeded(bkts, sufficient_funds): '''Remove buckets that are unnecessary in achieving the spend amount''' diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 50ed0a5c..5e1912a3 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -3,8 +3,8 @@ import time import sys import platform import queue -from collections import namedtuple from functools import partial +from typing import NamedTuple, Callable, Optional from PyQt5.QtGui import * from PyQt5.QtCore import * @@ -638,7 +638,12 @@ class TaskThread(QThread): '''Thread that runs background tasks. Callbacks are guaranteed to happen in the context of its parent.''' - Task = namedtuple("Task", "task cb_success cb_done cb_error") + class Task(NamedTuple): + task: Callable + cb_success: Optional[Callable] + cb_done: Optional[Callable] + cb_error: Optional[Callable] + doneSig = pyqtSignal(object, object, object) def __init__(self, parent, on_error=None): @@ -654,7 +659,7 @@ class TaskThread(QThread): def run(self): while True: - task = self.tasks.get() + task = self.tasks.get() # type: TaskThread.Task if not task: break try: diff --git a/electrum/network.py b/electrum/network.py index 00c96a6f..4b757c83 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -110,11 +110,12 @@ def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()): return random.choice(eligible) if eligible else None -NetworkParameters = NamedTuple("NetworkParameters", [("host", str), - ("port", str), - ("protocol", str), - ("proxy", Optional[dict]), - ("auto_connect", bool)]) +class NetworkParameters(NamedTuple): + host: str + port: str + protocol: str + proxy: Optional[dict] + auto_connect: bool proxy_modes = ['socks4', 'socks5'] diff --git a/electrum/plugin.py b/electrum/plugin.py index 2e180f45..cd9fbef1 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -22,13 +22,13 @@ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from collections import namedtuple import traceback import sys import os import pkgutil import time import threading +from typing import NamedTuple, Any, Union from .i18n import _ from .util import (profiler, PrintError, DaemonThread, UserCancelled, @@ -259,14 +259,23 @@ class BasePlugin(PrintError): pass -class DeviceNotFoundError(Exception): - pass +class DeviceNotFoundError(Exception): pass +class DeviceUnpairableError(Exception): pass -class DeviceUnpairableError(Exception): - pass -Device = namedtuple("Device", "path interface_number id_ product_key usage_page") -DeviceInfo = namedtuple("DeviceInfo", "device label initialized") +class Device(NamedTuple): + path: Union[str, bytes] + interface_number: int + id_: str + product_key: Any # when using hid, often Tuple[int, int] + usage_page: int + + +class DeviceInfo(NamedTuple): + device: Device + label: str + initialized: bool + class DeviceMgr(ThreadJob, PrintError): '''Manages hardware clients. A client communicates over a hardware diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index e123f52b..8a348f3f 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -374,7 +374,7 @@ class Coldcard_KeyStore(Hardware_KeyStore): # give empty bytes for error cases; it seems to clear the old signature box return b'' - def build_psbt(self, tx, wallet=None, xfp=None): + def build_psbt(self, tx: Transaction, wallet=None, xfp=None): # Render a PSBT file, for upload to Coldcard. # if xfp is None: @@ -390,7 +390,7 @@ class Coldcard_KeyStore(Hardware_KeyStore): wallet.add_hw_info(tx) # wallet.add_hw_info installs this attr - assert hasattr(tx, 'output_info'), 'need data about outputs' + assert tx.output_info, 'need data about outputs' # Build map of pubkey needed as derivation from master, in PSBT binary format # 1) binary version of the common subpath for all keys diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index 67f86ad2..a93b7a2b 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -28,7 +28,7 @@ from electrum.plugin import BasePlugin, hook from electrum.i18n import _ from electrum.bitcoin import is_address, TYPE_SCRIPT from electrum.util import bfh, versiontuple -from electrum.transaction import opcodes, TxOutput +from electrum.transaction import opcodes, TxOutput, Transaction class HW_PluginBase(BasePlugin): @@ -113,14 +113,13 @@ class HW_PluginBase(BasePlugin): return message -def is_any_tx_output_on_change_branch(tx): - if not hasattr(tx, 'output_info'): +def is_any_tx_output_on_change_branch(tx: Transaction): + if not tx.output_info: return False - for _type, address, amount in tx.outputs(): - info = tx.output_info.get(address) + for o in tx.outputs(): + info = tx.output_info.get(o.address) if info is not None: - index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig - if index[0] == 1: + if info.address_index[0] == 1: return True return False diff --git a/electrum/transaction.py b/electrum/transaction.py index 1f1271f4..2965d77c 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -31,7 +31,7 @@ import struct import traceback import sys from typing import (Sequence, Union, NamedTuple, Tuple, Optional, Iterable, - Callable, List) + Callable, List, Dict) from . import ecc, bitcoin, constants, segwit_addr from .util import print_error, profiler, to_bytes, bh2u, bfh @@ -63,17 +63,22 @@ class MalformedBitcoinScript(Exception): pass -TxOutput = NamedTuple("TxOutput", [('type', int), ('address', str), ('value', Union[int, str])]) -# ^ value is str when the output is set to max: '!' +class TxOutput(NamedTuple): + type: int + address: str + value: Union[int, str] # str when the output is set to max: '!' -TxOutputForUI = NamedTuple("TxOutputForUI", [('address', str), ('value', int)]) +class TxOutputForUI(NamedTuple): + address: str + value: int -TxOutputHwInfo = NamedTuple("TxOutputHwInfo", [('address_index', Tuple), - ('sorted_xpubs', Iterable[str]), - ('num_sig', Optional[int]), - ('script_type', str)]) +class TxOutputHwInfo(NamedTuple): + address_index: Tuple + sorted_xpubs: Iterable[str] + num_sig: Optional[int] + script_type: str class BCDataStream(object): @@ -682,6 +687,7 @@ class Transaction: # this value will get properly set when deserializing self.is_partial_originally = True self._segwit_ser = None # None means "don't know" + self.output_info = None # type: Optional[Dict[str, TxOutputHwInfo]] def update(self, raw): self.raw = raw diff --git a/electrum/util.py b/electrum/util.py index 7ac77dfe..7140893d 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -902,14 +902,18 @@ def ignore_exceptions(func): return wrapper -TxMinedStatus = NamedTuple("TxMinedStatus", [("height", int), - ("conf", int), - ("timestamp", int), - ("header_hash", str)]) -VerifiedTxInfo = NamedTuple("VerifiedTxInfo", [("height", int), - ("timestamp", int), - ("txpos", int), - ("header_hash", str)]) +class TxMinedStatus(NamedTuple): + height: int + conf: int + timestamp: int + header_hash: str + + +class VerifiedTxInfo(NamedTuple): + height: int + timestamp: int + txpos: int + header_hash: str def make_aiohttp_session(proxy: dict, headers=None, timeout=None): From 5e0179dac46a8c03f9795742171ba9bcf0407716 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Oct 2018 00:20:45 +0100 Subject: [PATCH 078/301] qt console: expose more refs, and fix auto-complete for >2 depth --- electrum/gui/qt/console.py | 25 ++++++++++++++----------- electrum/gui/qt/main_window.py | 19 +++++++++++++------ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/electrum/gui/qt/console.py b/electrum/gui/qt/console.py index a1928118..96564b0f 100644 --- a/electrum/gui/qt/console.py +++ b/electrum/gui/qt/console.py @@ -309,31 +309,34 @@ class Console(QtWidgets.QPlainTextEdit): super(Console, self).keyPressEvent(event) - - def completions(self): cmd = self.getCommand() lastword = re.split(' |\(|\)',cmd)[-1] beginning = cmd[0:-len(lastword)] path = lastword.split('.') + prefix = '.'.join(path[:-1]) + prefix = (prefix + '.') if prefix else prefix ns = self.namespace.keys() if len(path) == 1: ns = ns - prefix = '' else: + assert len(path) > 1 obj = self.namespace.get(path[0]) - prefix = path[0] + '.' - ns = dir(obj) - + try: + for attr in path[1:-1]: + obj = getattr(obj, attr) + except AttributeError: + ns = [] + else: + ns = dir(obj) completions = [] - for x in ns: - if x[0] == '_':continue - xx = prefix + x - if xx.startswith(lastword): - completions.append(xx) + for name in ns: + if name[0] == '_':continue + if name.startswith(path[-1]): + completions.append(prefix+name) completions.sort() if not completions: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index bade2a30..586789a9 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -42,6 +42,7 @@ from PyQt5.QtCore import * import PyQt5.QtCore as QtCore from PyQt5.QtWidgets import * +import electrum from electrum import (keystore, simple_config, ecc, constants, util, bitcoin, commands, coinchooser, paymentrequest) from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS @@ -1950,18 +1951,24 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): console.history = self.config.get("console-history",[]) console.history_index = len(console.history) - console.updateNamespace({'wallet' : self.wallet, - 'network' : self.network, - 'plugins' : self.gui_object.plugins, - 'window': self}) - console.updateNamespace({'util' : util, 'bitcoin':bitcoin}) + console.updateNamespace({ + 'wallet': self.wallet, + 'network': self.network, + 'plugins': self.gui_object.plugins, + 'window': self, + 'config': self.config, + 'electrum': electrum, + 'daemon': self.gui_object.daemon, + 'util': util, + 'bitcoin': bitcoin, + }) c = commands.Commands(self.config, self.wallet, self.network, lambda: self.console.set_json(True)) methods = {} def mkfunc(f, method): return lambda *args: f(method, args, self.password_dialog) for m in dir(c): - if m[0]=='_' or m in ['network','wallet']: continue + if m[0]=='_' or m in ['network','wallet','config']: continue methods[m] = mkfunc(c._run, m) console.updateNamespace(methods) From af232223ee4c7f5f34e9f21c5bec683b4c417d12 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Oct 2018 15:48:04 +0100 Subject: [PATCH 079/301] windows build script: add note to build from fresh clone --- contrib/build-wine/docker/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/contrib/build-wine/docker/README.md b/contrib/build-wine/docker/README.md index 0df96ea5..aba87018 100644 --- a/contrib/build-wine/docker/README.md +++ b/contrib/build-wine/docker/README.md @@ -27,6 +27,19 @@ folder. 3. Build Windows binaries + It's recommended to build from a fresh clone + (but you can skip this if reproducibility is not necessary). + + ``` + $ FRESH_CLONE=contrib/build-wine/fresh_clone && \ + rm -rf $FRESH_CLONE && \ + mkdir -p $FRESH_CLONE && \ + cd $FRESH_CLONE && \ + git clone https://github.com/spesmilo/electrum.git && \ + cd electrum + ``` + + And then build from this directory: ``` $ git checkout $REV $ sudo docker run \ From f819e9b6f4b8670329bf293bbcd9b37c57004a0e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Oct 2018 17:09:23 +0100 Subject: [PATCH 080/301] openalias: minor clean-up --- electrum/contacts.py | 2 +- electrum/gui/qt/paytoedit.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/electrum/contacts.py b/electrum/contacts.py index 8e20245b..c09b59e2 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -98,7 +98,7 @@ class Contacts(dict): try: records, validated = dnssec.query(url, dns.rdatatype.TXT) except DNSException as e: - print_error('Error resolving openalias: ', str(e)) + print_error(f'Error resolving openalias: {repr(e)}') return None prefix = 'btc' for record in records: diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index cbd9fde0..6235b85c 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -29,7 +29,7 @@ from decimal import Decimal from PyQt5.QtGui import * from electrum import bitcoin -from electrum.util import bfh +from electrum.util import bfh, PrintError from electrum.transaction import TxOutput from .qrtextedit import ScanQRTextEdit @@ -42,7 +42,7 @@ frozen_style = "QWidget { background-color:none; border:none;}" normal_style = "QPlainTextEdit { }" -class PayToEdit(CompletionTextEdit, ScanQRTextEdit): +class PayToEdit(CompletionTextEdit, ScanQRTextEdit, PrintError): def __init__(self, win): CompletionTextEdit.__init__(self) @@ -215,6 +215,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit): if self.is_pr: return key = str(self.toPlainText()) + key = key.strip() # strip whitespaces if key == self.previous_payto: return self.previous_payto = key @@ -225,7 +226,8 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit): return try: data = self.win.contacts.resolve(key) - except: + except Exception as e: + self.print_error(f'error resolving address/alias: {repr(e)}') return if not data: return From f53b480f1cd1b572e4c39b249b5c1ff2a0737466 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Oct 2018 21:34:44 +0100 Subject: [PATCH 081/301] wallet: more powerful add_input_info tangentially related: #4814 also recognise that input is_mine if tx was not fully parsed but we have the prevout UTXO --- electrum/gui/qt/transaction_dialog.py | 4 ++-- electrum/wallet.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 9106979d..b38430f1 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -39,7 +39,7 @@ from electrum.i18n import _ from electrum.plugin import run_hook from electrum import simple_config from electrum.util import bfh -from electrum.transaction import SerializationError +from electrum.transaction import SerializationError, Transaction from .util import * @@ -73,7 +73,7 @@ class TxDialog(QDialog, MessageBoxMixin): # Take a copy; it might get updated in the main window by # e.g. the FX plugin. If this happens during or after a long # sign operation the signatures are lost. - self.tx = tx = copy.deepcopy(tx) + self.tx = tx = copy.deepcopy(tx) # type: Transaction try: self.tx.deserialize() except BaseException as e: diff --git a/electrum/wallet.py b/electrum/wallet.py index fc962daa..62964486 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -734,7 +734,7 @@ class Abstract_Wallet(AddressSynchronizer): raise NotImplementedError() # implemented by subclasses def add_input_info(self, txin): - address = txin['address'] + address = self.get_txin_address(txin) if self.is_mine(address): txin['type'] = self.get_txin_type(address) # segwit needs value to sign From 5b4fada2a03f1ebfb156d7c4c7e5d468669ae8d6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 Oct 2018 19:07:37 +0100 Subject: [PATCH 082/301] fix some network.get_transaction calls see #4814 (issuecomment-434392195) --- electrum/gui/qt/main_window.py | 9 +++++---- electrum/interface.py | 12 +++++++++--- electrum/network.py | 7 ++++--- electrum/util.py | 10 ---------- electrum/wallet.py | 12 ++++++++---- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 586789a9..0729ca10 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2427,11 +2427,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if ok and txid: txid = str(txid).strip() try: - r = self.network.get_transaction(txid) - except BaseException as e: - self.show_message(str(e)) + raw_tx = self.network.run_from_another_thread( + self.network.get_transaction(txid, timeout=10)) + except Exception as e: + self.show_message(_("Error getting transaction from network") + ":\n" + str(e)) return - tx = transaction.Transaction(r) + tx = transaction.Transaction(raw_tx) self.show_transaction(tx) @protected diff --git a/electrum/interface.py b/electrum/interface.py index 3141fbec..5ae9beec 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -68,9 +68,9 @@ class NotificationSession(RPCSession): else: raise Exception('unexpected request: {}'.format(repr(request))) - async def send_request(self, *args, timeout=-1, **kwargs): + async def send_request(self, *args, timeout=None, **kwargs): # note: the timeout starts after the request touches the wire! - if timeout == -1: + if timeout is None: timeout = 30 # note: the semaphore implementation guarantees no starvation async with self.in_flight_requests_semaphore: @@ -108,7 +108,13 @@ class NotificationSession(RPCSession): class GracefulDisconnect(Exception): pass -class RequestTimedOut(GracefulDisconnect): pass + + +class RequestTimedOut(GracefulDisconnect): + def __str__(self): + return _("Network request timed out.") + + class ErrorParsingSSLCert(Exception): pass class ErrorGettingSSLCertFromServer(Exception): pass diff --git a/electrum/network.py b/electrum/network.py index 4b757c83..aa8db7e6 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -706,7 +706,7 @@ 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=10): + async def broadcast_transaction(self, tx, *, timeout=10): out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout) if out != tx.txid(): raise Exception(out) @@ -717,8 +717,9 @@ class Network(PrintError): return await self.interface.request_chunk(height, tip=tip, can_return_early=can_return_early) @best_effort_reliable - async def get_transaction(self, tx_hash: str) -> str: - return await self.interface.session.send_request('blockchain.transaction.get', [tx_hash]) + async def get_transaction(self, tx_hash: str, *, timeout=None) -> str: + return await self.interface.session.send_request('blockchain.transaction.get', [tx_hash], + timeout=timeout) @best_effort_reliable async def get_history_for_scripthash(self, sh: str) -> List[dict]: diff --git a/electrum/util.py b/electrum/util.py index 7140893d..f4cb8ddd 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -113,16 +113,6 @@ class FileExportFailed(Exception): return _("Failed to export to file.") + "\n" + self.message -class TimeoutException(Exception): - def __init__(self, message=''): - self.message = str(message) - - def __str__(self): - if not self.message: - return _("Operation timed out.") - return self.message - - class WalletFileException(Exception): pass diff --git a/electrum/wallet.py b/electrum/wallet.py index 62964486..1acca2a9 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -43,7 +43,7 @@ from typing import TYPE_CHECKING, List, Optional, Tuple from .i18n import _ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, - TimeoutException, WalletFileException, BitcoinException, + WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, Fiat, bfh, bh2u) from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script, @@ -60,6 +60,7 @@ from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, from .paymentrequest import (PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, InvoiceStore) from .contacts import Contacts +from .interface import RequestTimedOut if TYPE_CHECKING: from .network import Network @@ -768,11 +769,14 @@ class Abstract_Wallet(AddressSynchronizer): tx = self.transactions.get(tx_hash, None) if not tx and self.network: try: - tx = Transaction(self.network.get_transaction(tx_hash)) - except TimeoutException as e: - self.print_error('getting input txn from network timed out for {}'.format(tx_hash)) + raw_tx = self.network.run_from_another_thread( + self.network.get_transaction(tx_hash, timeout=10)) + except RequestTimedOut as e: + self.print_error(f'getting input txn from network timed out for {tx_hash}') if not ignore_timeout: raise e + else: + tx = Transaction(raw_tx) return tx def add_hw_info(self, tx): From 1c63bca2c70247e1db32e79058a51b11c641b9ed Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 Oct 2018 19:19:46 +0100 Subject: [PATCH 083/301] follow-up prev --- electrum/interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/interface.py b/electrum/interface.py index 5ae9beec..1b8511dc 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -42,6 +42,7 @@ from .version import ELECTRUM_VERSION, PROTOCOL_VERSION from . import blockchain from .blockchain import Blockchain from . import constants +from .i18n import _ if TYPE_CHECKING: from .network import Network From 4f7283a3b0b3301195f68cca915db2c49d410a15 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 Oct 2018 16:21:04 +0100 Subject: [PATCH 084/301] expose electrum version as __version__ --- electrum/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/__init__.py b/electrum/__init__.py index 556fc329..e7e06b08 100644 --- a/electrum/__init__.py +++ b/electrum/__init__.py @@ -12,3 +12,6 @@ from . import daemon from .transaction import Transaction from .plugin import BasePlugin from .commands import Commands, known_commands + + +__version__ = ELECTRUM_VERSION From 386e0d560eee1ad82380c4000d2afa8e6a07963c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 Oct 2018 17:58:47 +0100 Subject: [PATCH 085/301] wizard,hw: tell user about errors during plugin init see #4817 (issuecomment-434765570) --- electrum/base_wizard.py | 25 ++++++++++++++----------- electrum/plugin.py | 23 ++++++++++++++++++++--- run_electrum | 14 ++++++++------ 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 485126d9..a6776da6 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -228,14 +228,7 @@ class BaseWizard(object): def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET): title = _('Hardware Keystore') # check available plugins - support = self.plugins.get_hardware_support() - if not support: - msg = '\n'.join([ - _('No hardware wallet support found on your system.'), - _('Please install the relevant libraries (eg python-trezor for Trezor).'), - ]) - self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose)) - return + supported_plugins = self.plugins.get_hardware_support() # scan devices devices = [] devmgr = self.plugins.device_manager @@ -246,14 +239,24 @@ class BaseWizard(object): debug_msg = ' {}:\n {}'.format(_('Error scanning devices'), e) else: debug_msg = '' - for name, description, plugin in support: + for splugin in supported_plugins: + name, plugin = splugin.name, splugin.plugin + # plugin init errored? + if not plugin: + e = splugin.exception + indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True)) + debug_msg += f' {name}: (error during plugin init)\n' + debug_msg += ' {}\n'.format(_('You might have an incompatible library.')) + debug_msg += f'{indented_error_msg}\n' + continue + # 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) except BaseException as e: - devmgr.print_error('error getting device infos for {}: {}'.format(name, e)) + devmgr.print_error(f'error getting device infos for {name}: {e}') indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True)) - debug_msg += ' {}:\n{}\n'.format(plugin.name, indented_error_msg) + debug_msg += f' {name}: (error getting device infos)\n{indented_error_msg}\n' continue devices += list(map(lambda x: (name, x), u)) if not debug_msg: diff --git a/electrum/plugin.py b/electrum/plugin.py index cd9fbef1..b9521cfd 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -28,7 +28,7 @@ import os import pkgutil import time import threading -from typing import NamedTuple, Any, Union +from typing import NamedTuple, Any, Union, TYPE_CHECKING, Optional from .i18n import _ from .util import (profiler, PrintError, DaemonThread, UserCancelled, @@ -37,6 +37,9 @@ from . import bip32 from . import plugins from .simple_config import SimpleConfig +if TYPE_CHECKING: + from .plugins.hw_wallet import HW_PluginBase + plugin_loaders = {} hook_names = set() @@ -148,10 +151,17 @@ class Plugins(DaemonThread): try: p = self.get_plugin(name) if p.is_enabled(): - out.append([name, details[2], p]) - except: + out.append(HardwarePluginToScan(name=name, + description=details[2], + plugin=p, + exception=None)) + except Exception as e: traceback.print_exc() self.print_error("cannot load plugin for:", name) + out.append(HardwarePluginToScan(name=name, + description=details[2], + plugin=None, + exception=e)) return out def register_wallet_type(self, name, gui_good, wallet_type): @@ -277,6 +287,13 @@ class DeviceInfo(NamedTuple): initialized: bool +class HardwarePluginToScan(NamedTuple): + name: str + description: str + plugin: Optional['HW_PluginBase'] + exception: Optional[Exception] + + class DeviceMgr(ThreadJob, PrintError): '''Manages hardware clients. A client communicates over a hardware channel with the device. diff --git a/run_electrum b/run_electrum index d8b19701..dbb88d9c 100755 --- a/run_electrum +++ b/run_electrum @@ -165,18 +165,20 @@ def init_cmdline(config_options, server): def get_connected_hw_devices(plugins): - support = plugins.get_hardware_support() - if not support: - print_msg('No hardware wallet support found on your system.') - sys.exit(1) + supported_plugins = plugins.get_hardware_support() # scan devices devices = [] devmgr = plugins.device_manager - for name, description, plugin in support: + for splugin in supported_plugins: + name, plugin = splugin.name, splugin.plugin + if not plugin: + e = splugin.exception + print_stderr(f"{name}: error during plugin init: {repr(e)}") + continue try: u = devmgr.unpaired_device_infos(None, plugin) except: - devmgr.print_error("error", name) + devmgr.print_error(f'error getting device infos for {name}: {e}') continue devices += list(map(lambda x: (name, x), u)) return devices From 0862fdb9a9082f78a78dfb566e46b1428d6f5c26 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 Oct 2018 18:33:28 +0100 Subject: [PATCH 086/301] plugins: somewhat clearer exception is loading plugin fails see #4817 (issuecomment-434778055) --- electrum/plugin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/plugin.py b/electrum/plugin.py index b9521cfd..d8935d82 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -100,8 +100,11 @@ class Plugins(DaemonThread): if not loader: raise RuntimeError("%s implementation for %s plugin not found" % (self.gui_name, name)) - p = loader.load_module() - plugin = p.Plugin(self, self.config, name) + try: + p = loader.load_module() + plugin = p.Plugin(self, self.config, name) + except Exception as e: + raise Exception(f"Error loading {name} plugin: {e}") from e self.add_jobs(plugin.thread_jobs()) self.plugins[name] = plugin self.print_error("loaded", name) From ca8eae919f14f13de7e0a3a35ba544d100d4f5de Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 Oct 2018 19:59:07 +0100 Subject: [PATCH 087/301] daemon: clarify error print --- electrum/daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index dea5a8c1..5c83ee84 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -92,7 +92,7 @@ def get_server(config: SimpleConfig) -> Optional[jsonrpclib.Server]: server.ping() return server except Exception as e: - print_error("[get_server]", e) + print_error(f"failed to connect to JSON-RPC server: {e}") if not create_time or create_time < time.time() - 1.0: return None # Sleep a bit and try again; it might have just been started From c2ecfaf239b584e1a3ba9f10f03be0a5441ceac6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 1 Nov 2018 16:30:03 +0100 Subject: [PATCH 088/301] move event loop construction to daemon --- electrum/daemon.py | 30 ++++++++++++++++++++++++++++-- electrum/network.py | 45 +++++++++++++-------------------------------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index 5c83ee84..029231c9 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -30,6 +30,7 @@ import traceback import sys import threading from typing import Dict, Optional, Tuple +import re import jsonrpclib @@ -127,10 +128,12 @@ class Daemon(DaemonThread): if fd is None and listen_jsonrpc: fd, server = get_fd_or_server(config) if fd is None: raise Exception('failed to lock daemon; already running?') + self.create_and_start_event_loop() if config.get('offline'): self.network = None else: self.network = Network(config) + self.network._loop_thread = self._loop_thread self.fx = FxThread(config, self.network) if self.network: self.network.start([self.fx.run]) @@ -170,7 +173,7 @@ class Daemon(DaemonThread): return True def run_daemon(self, config_options): - asyncio.set_event_loop(self.network.asyncio_loop) # FIXME what if self.network is None? + asyncio.set_event_loop(self.asyncio_loop) config = SimpleConfig(config_options) sub = config.get('subcommand') assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet'] @@ -265,7 +268,7 @@ class Daemon(DaemonThread): wallet.stop_threads() def run_cmdline(self, config_options): - asyncio.set_event_loop(self.network.asyncio_loop) # FIXME what if self.network is None? + asyncio.set_event_loop(self.asyncio_loop) password = config_options.get('password') new_password = config_options.get('new_password') config = SimpleConfig(config_options) @@ -297,11 +300,15 @@ class Daemon(DaemonThread): def run(self): while self.is_running(): self.server.handle_request() if self.server else time.sleep(0.1) + # stop network/wallets for k, wallet in self.wallets.items(): wallet.stop_threads() if self.network: self.print_error("shutting down network") self.network.stop() + # stop event loop + self.asyncio_loop.call_soon_threadsafe(self._stop_loop.set_result, 1) + self._loop_thread.join(timeout=1) self.on_stop() def stop(self): @@ -323,3 +330,22 @@ class Daemon(DaemonThread): except BaseException as e: traceback.print_exc(file=sys.stdout) # app will exit now + + def create_and_start_event_loop(self): + def on_exception(loop, context): + """Suppress spurious messages it appears we cannot control.""" + SUPPRESS_MESSAGE_REGEX = re.compile('SSL handshake|Fatal read error on|' + 'SSL error in data received') + message = context.get('message') + if message and SUPPRESS_MESSAGE_REGEX.match(message): + return + loop.default_exception_handler(context) + + self.asyncio_loop = asyncio.get_event_loop() + self.asyncio_loop.set_exception_handler(on_exception) + # self.asyncio_loop.set_debug(1) + self._stop_loop = asyncio.Future() + self._loop_thread = threading.Thread(target=self.asyncio_loop.run_until_complete, + args=(self._stop_loop,), + name='EventLoop') + self._loop_thread.start() diff --git a/electrum/network.py b/electrum/network.py index aa8db7e6..617da45c 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -168,6 +168,10 @@ class Network(PrintError): def __init__(self, config: SimpleConfig=None): global INSTANCE INSTANCE = self + + self.asyncio_loop = asyncio.get_event_loop() + self._loop_thread = None # type: threading.Thread # set by caller; only used for sanity checks + if config is None: config = {} # Do not use mutables as default values! self.config = SimpleConfig(config) if isinstance(config, dict) else config # type: SimpleConfig @@ -221,17 +225,8 @@ class Network(PrintError): self.server_queue = None self.proxy = None - self.asyncio_loop = asyncio.get_event_loop() - self.asyncio_loop.set_exception_handler(self.on_event_loop_exception) - #self.asyncio_loop.set_debug(1) - self._run_forever = asyncio.Future() - self._thread = threading.Thread(target=self.asyncio_loop.run_until_complete, - args=(self._run_forever,), - name='Network') - self._thread.start() - def run_from_another_thread(self, coro): - assert self._thread != threading.current_thread(), 'must not be called from network thread' + assert self._loop_thread != threading.current_thread(), 'must not be called from network thread' fut = asyncio.run_coroutine_threadsafe(coro, self.asyncio_loop) return fut.result() @@ -239,15 +234,6 @@ class Network(PrintError): def get_instance(): return INSTANCE - def on_event_loop_exception(self, loop, context): - """Suppress spurious messages it appears we cannot control.""" - SUPPRESS_MESSAGE_REGEX = re.compile('SSL handshake|Fatal read error on|' - 'SSL error in data received') - message = context.get('message') - if message and SUPPRESS_MESSAGE_REGEX.match(message): - return - loop.default_exception_handler(context) - def with_recent_servers_lock(func): def func_wrapper(self, *args, **kwargs): with self.recent_servers_lock: @@ -845,25 +831,20 @@ class Network(PrintError): await asyncio.wait_for(self.main_taskgroup.cancel_remaining(), timeout=2) except (asyncio.TimeoutError, asyncio.CancelledError) as e: self.print_error(f"exc during main_taskgroup cancellation: {repr(e)}") - try: - self.main_taskgroup = None - self.interface = None # type: Interface - self.interfaces = {} # type: Dict[str, Interface] - self.connecting.clear() - self.server_queue = None - if not full_shutdown: - self.trigger_callback('network_updated') - finally: - if full_shutdown: - self._run_forever.set_result(1) + self.main_taskgroup = None + self.interface = None # type: Interface + self.interfaces = {} # type: Dict[str, Interface] + self.connecting.clear() + self.server_queue = None + if not full_shutdown: + self.trigger_callback('network_updated') def stop(self): - assert self._thread != threading.current_thread(), 'must not be called from network thread' + assert self._loop_thread != threading.current_thread(), 'must not be called from network thread' fut = asyncio.run_coroutine_threadsafe(self._stop(full_shutdown=True), self.asyncio_loop) try: fut.result(timeout=2) except (asyncio.TimeoutError, asyncio.CancelledError): pass - self._thread.join(timeout=1) async def _ensure_there_is_a_main_interface(self): if self.is_connected(): From 5c4a6c0f2b531bd84d4a0c5dad91ace4b8ad3596 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Nov 2018 16:06:47 +0100 Subject: [PATCH 089/301] rm network.add_job current implementation is prone to race, and is not used anyway --- electrum/network.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index 617da45c..cb34d638 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -819,11 +819,6 @@ class Network(PrintError): self._jobs = jobs or [] asyncio.run_coroutine_threadsafe(self._start(), self.asyncio_loop) - async def add_job(self, job): - async with self.restart_lock: - self._jobs.append(job) - await self.main_taskgroup.spawn(job) - @log_exceptions async def _stop(self, full_shutdown=False): self.print_error("stopping network") From e37da62a1ce68ed2374ddc18796a89bd21529b70 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Nov 2018 20:14:59 +0100 Subject: [PATCH 090/301] fix most "scripts" related: #4754 --- electrum/bitcoin.py | 24 +++--- electrum/blockchain.py | 6 +- electrum/daemon.py | 26 +------ electrum/network.py | 11 ++- electrum/scripts/bip70.py | 1 + electrum/scripts/block_headers.py | 38 +++++----- electrum/scripts/estimate_fee.py | 32 ++++++-- electrum/scripts/get_history.py | 26 +++++-- electrum/scripts/peers.py | 30 ++++++-- electrum/scripts/servers.py | 31 ++++++-- electrum/scripts/txradar.py | 38 +++++++--- electrum/scripts/util.py | 119 ++++++++++-------------------- electrum/scripts/watch_address.py | 48 +++++++----- electrum/synchronizer.py | 5 +- electrum/util.py | 25 ++++++- 15 files changed, 266 insertions(+), 194 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index abaa5ed2..6bebf498 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -284,13 +284,15 @@ def script_to_address(script, *, net=None): assert t == TYPE_ADDRESS return addr -def address_to_script(addr, *, net=None): +def address_to_script(addr: str, *, net=None) -> str: if net is None: net = constants.net + if not is_address(addr, net=net): + raise BitcoinException(f"invalid bitcoin address: {addr}") witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr) if witprog is not None: if not (0 <= witver <= 16): - raise BitcoinException('impossible witness version: {}'.format(witver)) + raise BitcoinException(f'impossible witness version: {witver}') OP_n = witver + 0x50 if witver > 0 else 0 script = bh2u(bytes([OP_n])) script += push_script(bh2u(bytes(witprog))) @@ -305,7 +307,7 @@ def address_to_script(addr, *, net=None): script += push_script(bh2u(hash_160_)) script += '87' # op_equal else: - raise BitcoinException('unknown address type: {}'.format(addrtype)) + raise BitcoinException(f'unknown address type: {addrtype}') return script def address_to_scripthash(addr): @@ -491,24 +493,28 @@ def address_from_private_key(sec): public_key = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) return pubkey_to_address(txin_type, public_key) -def is_segwit_address(addr): +def is_segwit_address(addr, *, net=None): + if net is None: net = constants.net try: - witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr) + witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr) except Exception as e: return False return witprog is not None -def is_b58_address(addr): +def is_b58_address(addr, *, net=None): + if net is None: net = constants.net try: addrtype, h = b58_address_to_hash160(addr) except Exception as e: return False - if addrtype not in [constants.net.ADDRTYPE_P2PKH, constants.net.ADDRTYPE_P2SH]: + if addrtype not in [net.ADDRTYPE_P2PKH, net.ADDRTYPE_P2SH]: return False return addr == hash160_to_b58_address(h, addrtype) -def is_address(addr): - return is_segwit_address(addr) or is_b58_address(addr) +def is_address(addr, *, net=None): + if net is None: net = constants.net + return is_segwit_address(addr, net=net) \ + or is_b58_address(addr, net=net) def is_private_key(key): diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 0f874905..2c72f6b3 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -72,7 +72,11 @@ def hash_header(header: dict) -> str: return '0' * 64 if header.get('prev_block_hash') is None: header['prev_block_hash'] = '00'*32 - return hash_encode(sha256d(bfh(serialize_header(header)))) + return hash_raw_header(serialize_header(header)) + + +def hash_raw_header(header: str) -> str: + return hash_encode(sha256d(bfh(header))) blockchains = {} # type: Dict[int, Blockchain] diff --git a/electrum/daemon.py b/electrum/daemon.py index 029231c9..b811905f 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -30,15 +30,14 @@ import traceback import sys import threading from typing import Dict, Optional, Tuple -import re import jsonrpclib from .jsonrpc import VerifyingJSONRPCServer from .version import ELECTRUM_VERSION from .network import Network -from .util import json_decode, DaemonThread -from .util import print_error, to_string +from .util import (json_decode, DaemonThread, print_error, to_string, + create_and_start_event_loop) from .wallet import Wallet, Abstract_Wallet from .storage import WalletStorage from .commands import known_commands, Commands @@ -128,7 +127,7 @@ class Daemon(DaemonThread): if fd is None and listen_jsonrpc: fd, server = get_fd_or_server(config) if fd is None: raise Exception('failed to lock daemon; already running?') - self.create_and_start_event_loop() + self.asyncio_loop, self._stop_loop, self._loop_thread = create_and_start_event_loop() if config.get('offline'): self.network = None else: @@ -330,22 +329,3 @@ class Daemon(DaemonThread): except BaseException as e: traceback.print_exc(file=sys.stdout) # app will exit now - - def create_and_start_event_loop(self): - def on_exception(loop, context): - """Suppress spurious messages it appears we cannot control.""" - SUPPRESS_MESSAGE_REGEX = re.compile('SSL handshake|Fatal read error on|' - 'SSL error in data received') - message = context.get('message') - if message and SUPPRESS_MESSAGE_REGEX.match(message): - return - loop.default_exception_handler(context) - - self.asyncio_loop = asyncio.get_event_loop() - self.asyncio_loop.set_exception_handler(on_exception) - # self.asyncio_loop.set_debug(1) - self._stop_loop = asyncio.Future() - self._loop_thread = threading.Thread(target=self.asyncio_loop.run_until_complete, - args=(self._stop_loop,), - name='EventLoop') - self._loop_thread.start() diff --git a/electrum/network.py b/electrum/network.py index cb34d638..36c957d8 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -32,7 +32,7 @@ import json import sys import ipaddress import asyncio -from typing import NamedTuple, Optional, Sequence, List, Dict +from typing import NamedTuple, Optional, Sequence, List, Dict, Tuple import traceback import dns @@ -53,7 +53,7 @@ NODES_RETRY_INTERVAL = 60 SERVER_RETRY_INTERVAL = 10 -def parse_servers(result): +def parse_servers(result: Sequence[Tuple[str, str, List[str]]]) -> Dict[str, dict]: """ parse servers list into dict format""" servers = {} for item in result: @@ -170,6 +170,7 @@ class Network(PrintError): INSTANCE = self self.asyncio_loop = asyncio.get_event_loop() + assert self.asyncio_loop.is_running(), "event loop not running" self._loop_thread = None # type: threading.Thread # set by caller; only used for sanity checks if config is None: @@ -225,6 +226,8 @@ class Network(PrintError): self.server_queue = None self.proxy = None + self._set_status('disconnected') + def run_from_another_thread(self, coro): assert self._loop_thread != threading.current_thread(), 'must not be called from network thread' fut = asyncio.run_coroutine_threadsafe(coro, self.asyncio_loop) @@ -411,10 +414,10 @@ class Network(PrintError): out = filter_noonion(out) return out - def _start_interface(self, server): + def _start_interface(self, server: str): if server not in self.interfaces and server not in self.connecting: if server == self.default_server: - self.print_error("connecting to %s as new interface" % server) + self.print_error(f"connecting to {server} as new interface") self._set_status('connecting') self.connecting.add(server) self.server_queue.put(server) diff --git a/electrum/scripts/bip70.py b/electrum/scripts/bip70.py index 2e04bfe7..7b3d0de2 100755 --- a/electrum/scripts/bip70.py +++ b/electrum/scripts/bip70.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 # create a BIP70 payment request signed with a certificate +# FIXME: the code here is outdated, and no longer working import tlslite diff --git a/electrum/scripts/block_headers.py b/electrum/scripts/block_headers.py index 649a0493..abf6a259 100755 --- a/electrum/scripts/block_headers.py +++ b/electrum/scripts/block_headers.py @@ -3,29 +3,33 @@ # A simple script that connects to a server and displays block headers import time -import sys +import asyncio -from .. import SimpleConfig, Network -from electrum.util import print_msg, json_encode +from electrum.network import Network +from electrum.util import print_msg, json_encode, create_and_start_event_loop, log_exceptions # start network -c = SimpleConfig() -network = Network(c) +loop, stopping_fut, loop_thread = create_and_start_event_loop() +network = Network() network.start() # wait until connected -while network.is_connecting(): - time.sleep(0.1) +while not network.is_connected(): + time.sleep(1) + print_msg("waiting for network to get connected...") -if not network.is_connected(): - print_msg("daemon is not connected") - sys.exit(1) +header_queue = asyncio.Queue() + +@log_exceptions +async def f(): + try: + await network.interface.session.subscribe('blockchain.headers.subscribe', [], header_queue) + # 3. wait for results + while network.is_connected(): + header = await header_queue.get() + print_msg(json_encode(header)) + finally: + stopping_fut.set_result(1) # 2. send the subscription -callback = lambda response: print_msg(json_encode(response.get('result'))) -network.send([('server.version',["block_headers script", "1.2"])], callback) -network.subscribe_to_headers(callback) - -# 3. wait for results -while network.is_connected(): - time.sleep(1) +asyncio.run_coroutine_threadsafe(f(), loop) diff --git a/electrum/scripts/estimate_fee.py b/electrum/scripts/estimate_fee.py index 85f63cef..bcb8c497 100755 --- a/electrum/scripts/estimate_fee.py +++ b/electrum/scripts/estimate_fee.py @@ -1,7 +1,29 @@ #!/usr/bin/env python3 -from . import util import json -from electrum.network import filter_protocol -peers = filter_protocol(util.get_peers()) -results = util.send_request(peers, 'blockchain.estimatefee', [2]) -print(json.dumps(results, indent=4)) +import asyncio +from statistics import median +from numbers import Number + +from electrum.network import filter_protocol, Network +from electrum.util import create_and_start_event_loop, log_exceptions + +import util + + +loop, stopping_fut, loop_thread = create_and_start_event_loop() +network = Network() +network.start() + +@log_exceptions +async def f(): + try: + peers = await util.get_peers(network) + peers = filter_protocol(peers) + results = await util.send_request(network, peers, 'blockchain.estimatefee', [2]) + print(json.dumps(results, indent=4)) + feerate_estimates = filter(lambda x: isinstance(x, Number), results.values()) + print(f"median feerate: {median(feerate_estimates)}") + finally: + stopping_fut.set_result(1) + +asyncio.run_coroutine_threadsafe(f(), loop) diff --git a/electrum/scripts/get_history.py b/electrum/scripts/get_history.py index c83f99d9..a9769828 100755 --- a/electrum/scripts/get_history.py +++ b/electrum/scripts/get_history.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 import sys -from .. import Network -from electrum.util import json_encode, print_msg +import asyncio + from electrum import bitcoin +from electrum.network import Network +from electrum.util import json_encode, print_msg, create_and_start_event_loop, log_exceptions + try: addr = sys.argv[1] @@ -11,8 +14,17 @@ except Exception: print("usage: get_history ") sys.exit(1) -n = Network() -n.start() -_hash = bitcoin.address_to_scripthash(addr) -h = n.get_history_for_scripthash(_hash) -print_msg(json_encode(h)) +loop, stopping_fut, loop_thread = create_and_start_event_loop() +network = Network() +network.start() + +@log_exceptions +async def f(): + try: + sh = bitcoin.address_to_scripthash(addr) + hist = await network.get_history_for_scripthash(sh) + print_msg(json_encode(hist)) + finally: + stopping_fut.set_result(1) + +asyncio.run_coroutine_threadsafe(f(), loop) diff --git a/electrum/scripts/peers.py b/electrum/scripts/peers.py index a887f079..e8ec5b03 100755 --- a/electrum/scripts/peers.py +++ b/electrum/scripts/peers.py @@ -1,14 +1,28 @@ #!/usr/bin/env python3 +import asyncio -from . import util +from electrum.network import filter_protocol, Network +from electrum.util import create_and_start_event_loop, log_exceptions +from electrum.blockchain import hash_raw_header -from electrum.network import filter_protocol -from electrum.blockchain import hash_header +import util -peers = util.get_peers() -peers = filter_protocol(peers, 's') -results = util.send_request(peers, 'blockchain.headers.subscribe', []) +loop, stopping_fut, loop_thread = create_and_start_event_loop() +network = Network() +network.start() -for n,v in sorted(results.items(), key=lambda x:x[1].get('block_height')): - print("%60s"%n, v.get('block_height'), hash_header(v)) +@log_exceptions +async def f(): + try: + peers = await util.get_peers(network) + peers = filter_protocol(peers, 's') + results = await util.send_request(network, peers, 'blockchain.headers.subscribe', []) + for server, header in sorted(results.items(), key=lambda x: x[1].get('height')): + height = header.get('height') + blockhash = hash_raw_header(header.get('hex')) + print("%60s" % server, height, blockhash) + finally: + stopping_fut.set_result(1) + +asyncio.run_coroutine_threadsafe(f(), loop) diff --git a/electrum/scripts/servers.py b/electrum/scripts/servers.py index c7201bc3..0c104f43 100755 --- a/electrum/scripts/servers.py +++ b/electrum/scripts/servers.py @@ -1,10 +1,27 @@ #!/usr/bin/env python3 - -from .. import set_verbosity -from electrum.network import filter_version -from . import util import json -set_verbosity(False) +import asyncio -servers = filter_version(util.get_peers()) -print(json.dumps(servers, sort_keys = True, indent = 4)) +from electrum.network import filter_version, Network +from electrum.util import create_and_start_event_loop, log_exceptions +from electrum import constants + +import util + + +#constants.set_testnet() + +loop, stopping_fut, loop_thread = create_and_start_event_loop() +network = Network() +network.start() + +@log_exceptions +async def f(): + try: + peers = await util.get_peers(network) + peers = filter_version(peers) + print(json.dumps(peers, sort_keys=True, indent=4)) + finally: + stopping_fut.set_result(1) + +asyncio.run_coroutine_threadsafe(f(), loop) diff --git a/electrum/scripts/txradar.py b/electrum/scripts/txradar.py index dda73227..8c150d8f 100755 --- a/electrum/scripts/txradar.py +++ b/electrum/scripts/txradar.py @@ -1,20 +1,38 @@ #!/usr/bin/env python3 -from . import util import sys +import asyncio + +from electrum.network import filter_protocol, Network +from electrum.util import create_and_start_event_loop, log_exceptions + +import util + + try: - tx = sys.argv[1] + txid = sys.argv[1] except: print("usage: txradar txid") sys.exit(1) -peers = util.get_peers() -results = util.send_request(peers, 'blockchain.transaction.get', [tx]) -r1 = [] -r2 = [] +loop, stopping_fut, loop_thread = create_and_start_event_loop() +network = Network() +network.start() -for k, v in results.items(): - (r1 if v else r2).append(k) +@log_exceptions +async def f(): + try: + peers = await util.get_peers(network) + peers = filter_protocol(peers, 's') + results = await util.send_request(network, peers, 'blockchain.transaction.get', [txid]) + r1, r2 = [], [] + for k, v in results.items(): + (r1 if not isinstance(v, Exception) else r2).append(k) + print(f"Received {len(results)} answers") + try: propagation = len(r1) * 100. / (len(r1) + len(r2)) + except ZeroDivisionError: propagation = 0 + print(f"Propagation rate: {propagation:.1f} percent") + finally: + stopping_fut.set_result(1) -print("Received %d answers"%len(results)) -print("Propagation rate: %.1f percent" % (len(r1) *100./(len(r1)+ len(r2)))) +asyncio.run_coroutine_threadsafe(f(), loop) diff --git a/electrum/scripts/util.py b/electrum/scripts/util.py index 266348f7..43a95d7d 100644 --- a/electrum/scripts/util.py +++ b/electrum/scripts/util.py @@ -1,87 +1,46 @@ -import select, time, queue -# import electrum -from .. import Connection, Interface, SimpleConfig +import asyncio +from typing import List, Sequence -from electrum.network import parse_servers -from collections import defaultdict +from aiorpcx import TaskGroup -# electrum.util.set_verbosity(1) -def get_interfaces(servers, timeout=10): - '''Returns a map of servers to connected interfaces. If any - connections fail or timeout, they will be missing from the map. - ''' - assert type(servers) is list - socket_queue = queue.Queue() - config = SimpleConfig() - connecting = {} - for server in servers: - if server not in connecting: - connecting[server] = Connection(server, socket_queue, config.path) - interfaces = {} - timeout = time.time() + timeout - count = 0 - while time.time() < timeout and count < len(servers): - try: - server, socket = socket_queue.get(True, 0.3) - except queue.Empty: - continue - if socket: - interfaces[server] = Interface(server, socket) - count += 1 - return interfaces +from electrum.network import parse_servers, Network +from electrum.interface import Interface -def wait_on_interfaces(interfaces, timeout=10): - '''Return a map of servers to a list of (request, response) tuples. - Waits timeout seconds, or until each interface has a response''' - result = defaultdict(list) - timeout = time.time() + timeout - while len(result) < len(interfaces) and time.time() < timeout: - rin = [i for i in interfaces.values()] - win = [i for i in interfaces.values() if i.unsent_requests] - rout, wout, xout = select.select(rin, win, [], 1) - for interface in wout: - interface.send_requests() - for interface in rout: - responses = interface.get_responses() - if responses: - result[interface.server].extend(responses) - return result -def get_peers(): - config = SimpleConfig() - peers = {} - # 1. get connected interfaces - server = config.get('server') - if server is None: - print("You need to set a secure server, for example (for mainnet): 'electrum setconfig server helicarrier.bauerj.eu:50002:s'") - return [] - interfaces = get_interfaces([server]) - if not interfaces: - print("No connection to", server) - return [] - # 2. get list of peers - interface = interfaces[server] - interface.queue_request('server.peers.subscribe', [], 0) - responses = wait_on_interfaces(interfaces).get(server) - if responses: - response = responses[0][1] # One response, (req, response) tuple - peers = parse_servers(response.get('result')) +#electrum.util.set_verbosity(True) + +async def get_peers(network: Network): + while not network.is_connected(): + await asyncio.sleep(1) + interface = network.interface + session = interface.session + print(f"asking server {interface.server} for its peers") + peers = parse_servers(await session.send_request('server.peers.subscribe')) + print(f"got {len(peers)} servers") return peers -def send_request(peers, method, params): - print("Contacting %d servers"%len(peers)) - interfaces = get_interfaces(peers) - print("%d servers could be reached" % len(interfaces)) - for peer in peers: - if not peer in interfaces: - print("Connection failed:", peer) - for msg_id, i in enumerate(interfaces.values()): - i.queue_request(method, params, msg_id) - responses = wait_on_interfaces(interfaces) - for peer in interfaces: - if not peer in responses: - print(peer, "did not answer") - results = dict(zip(responses.keys(), [t[0][1].get('result') for t in responses.values()])) - print("%d answers"%len(results)) - return results +async def send_request(network: Network, servers: List[str], method: str, params: Sequence): + print(f"contacting {len(servers)} servers") + num_connecting = len(network.connecting) + for server in servers: + network._start_interface(server) + # sleep a bit + for _ in range(10): + if len(network.connecting) < num_connecting: + break + await asyncio.sleep(1) + print(f"connected to {len(network.interfaces)} servers. sending request to all.") + responses = dict() + async def get_response(iface: Interface): + try: + res = await iface.session.send_request(method, params, timeout=10) + except Exception as e: + print(f"server {iface.server} errored or timed out: ({repr(e)})") + res = e + responses[iface.server] = res + async with TaskGroup() as group: + for interface in network.interfaces.values(): + await group.spawn(get_response(interface)) + print("%d answers" % len(responses)) + return responses diff --git a/electrum/scripts/watch_address.py b/electrum/scripts/watch_address.py index 8fd5d491..851160de 100755 --- a/electrum/scripts/watch_address.py +++ b/electrum/scripts/watch_address.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 import sys -import time -from electrum import bitcoin -from .. import SimpleConfig, Network -from electrum.util import print_msg, json_encode +import asyncio + +from electrum.network import Network +from electrum.util import print_msg, create_and_start_event_loop +from electrum.synchronizer import SynchronizerBase + try: addr = sys.argv[1] @@ -12,25 +14,31 @@ except Exception: print("usage: watch_address ") sys.exit(1) -sh = bitcoin.address_to_scripthash(addr) - # start network -c = SimpleConfig() -network = Network(c) +loop = create_and_start_event_loop()[0] +network = Network() network.start() -# wait until connected -while network.is_connecting(): - time.sleep(0.1) -if not network.is_connected(): - print_msg("daemon is not connected") - sys.exit(1) +class Notifier(SynchronizerBase): + def __init__(self, network): + SynchronizerBase.__init__(self, network) + self.watched_addresses = set() + self.watch_queue = asyncio.Queue() -# 2. send the subscription -callback = lambda response: print_msg(json_encode(response.get('result'))) -network.subscribe_to_address(addr, callback) + async def main(self): + # resend existing subscriptions if we were restarted + for addr in self.watched_addresses: + await self._add_address(addr) + # main loop + while True: + addr = await self.watch_queue.get() + self.watched_addresses.add(addr) + await self._add_address(addr) -# 3. wait for results -while network.is_connected(): - time.sleep(1) + async def _on_address_status(self, addr, status): + print_msg(f"addr {addr}, status {status}") + + +notifier = Notifier(network) +asyncio.run_coroutine_threadsafe(notifier.watch_queue.put(addr), loop) diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py index 0a6ed687..cdde6d24 100644 --- a/electrum/synchronizer.py +++ b/electrum/synchronizer.py @@ -31,7 +31,7 @@ from aiorpcx import TaskGroup, run_in_thread from .transaction import Transaction from .util import bh2u, make_aiohttp_session, NetworkJobOnDefaultServer -from .bitcoin import address_to_scripthash +from .bitcoin import address_to_scripthash, is_address if TYPE_CHECKING: from .network import Network @@ -77,7 +77,8 @@ class SynchronizerBase(NetworkJobOnDefaultServer): def add(self, addr): asyncio.run_coroutine_threadsafe(self._add_address(addr), self.asyncio_loop) - async def _add_address(self, addr): + async def _add_address(self, addr: str): + if not is_address(addr): raise ValueError(f"invalid bitcoin address {addr}") if addr in self.requested_addrs: return self.requested_addrs.add(addr) await self.add_queue.put(addr) diff --git a/electrum/util.py b/electrum/util.py index f4cb8ddd..08ea73d4 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -278,7 +278,7 @@ class DaemonThread(threading.Thread, PrintError): self.print_error("stopped") -verbosity = '*' +verbosity = '' def set_verbosity(filters: Union[str, bool]): global verbosity if type(filters) is bool: # backwards compat @@ -983,3 +983,26 @@ class NetworkJobOnDefaultServer(PrintError): s = self.interface.session assert s is not None return s + + +def create_and_start_event_loop() -> Tuple[asyncio.AbstractEventLoop, + asyncio.Future, + threading.Thread]: + def on_exception(loop, context): + """Suppress spurious messages it appears we cannot control.""" + SUPPRESS_MESSAGE_REGEX = re.compile('SSL handshake|Fatal read error on|' + 'SSL error in data received') + message = context.get('message') + if message and SUPPRESS_MESSAGE_REGEX.match(message): + return + loop.default_exception_handler(context) + + loop = asyncio.get_event_loop() + loop.set_exception_handler(on_exception) + # loop.set_debug(1) + stopping_fut = asyncio.Future() + loop_thread = threading.Thread(target=loop.run_until_complete, + args=(stopping_fut,), + name='EventLoop') + loop_thread.start() + return loop, stopping_fut, loop_thread From 1a5c77aa82d0ddc1a2b47276ea94e17b2aa04e80 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Nov 2018 20:37:37 +0100 Subject: [PATCH 091/301] follow-up prev --- electrum/scripts/servers.py | 6 ++++-- electrum/scripts/util.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/scripts/servers.py b/electrum/scripts/servers.py index 0c104f43..ace1798f 100755 --- a/electrum/scripts/servers.py +++ b/electrum/scripts/servers.py @@ -2,17 +2,19 @@ import json import asyncio +from electrum.simple_config import SimpleConfig from electrum.network import filter_version, Network from electrum.util import create_and_start_event_loop, log_exceptions from electrum import constants import util - +# testnet? #constants.set_testnet() +config = SimpleConfig({'testnet': False}) loop, stopping_fut, loop_thread = create_and_start_event_loop() -network = Network() +network = Network(config) network.start() @log_exceptions diff --git a/electrum/scripts/util.py b/electrum/scripts/util.py index 43a95d7d..0fe8663d 100644 --- a/electrum/scripts/util.py +++ b/electrum/scripts/util.py @@ -12,6 +12,7 @@ from electrum.interface import Interface async def get_peers(network: Network): while not network.is_connected(): await asyncio.sleep(1) + print("waiting for network to get connected...") interface = network.interface session = interface.session print(f"asking server {interface.server} for its peers") From 908c9793385650aefe1f82b3c6a46e3be32b96cc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Nov 2018 20:38:26 +0100 Subject: [PATCH 092/301] network: update hardcoded mainnet servers --- electrum/servers.json | 322 ++++++++++++++++++++++++++++-------------- 1 file changed, 215 insertions(+), 107 deletions(-) diff --git a/electrum/servers.json b/electrum/servers.json index 05029bde..2677b814 100644 --- a/electrum/servers.json +++ b/electrum/servers.json @@ -1,304 +1,412 @@ { - "207.154.223.80": { + "3smoooajg7qqac2y.onion": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "4cii7ryno5j3axe4.onion": { - "pruning": "-", - "t": "50001", - "version": "1.2" - }, - "74.222.1.20": { + "81-7-10-251.blue.kundencontroller.de": { "pruning": "-", "s": "50002", - "t": "50001", - "version": "1.2" - }, - "88.198.43.231": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" + "version": "1.4" }, "E-X.not.fyi": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" + }, + "MEADS.hopto.org": { + "pruning": "-", + "s": "50002", + "version": "1.4" }, "VPS.hsmiths.com": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "arihancckjge66iv.onion": { + "b.ooze.cc": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" - }, - "aspinall.io": { - "pruning": "-", - "s": "50002", - "version": "1.2" + "version": "1.4" }, "bauerjda5hnedjam.onion": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "bauerjhejlv6di7s.onion": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "btc.asis.io": { + "bitcoin.corgi.party": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" + }, + "bitcoin3nqy3db7c.onion": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "bitcoins.sk": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" }, "btc.cihar.com": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "btc.smsys.me": { "pruning": "-", "s": "995", - "version": "1.2" + "version": "1.4" + }, + "btc.xskyx.net": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "cashyes.zapto.org": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "currentlane.lovebitco.in": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" }, "daedalus.bauerj.eu": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "de.hamster.science": { + "dedi.jochen-hoenicke.de": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" + }, + "dragon085.startdedicated.de": { + "pruning": "-", + "s": "50002", + "version": "1.4" + }, + "e-1.claudioboxx.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" }, "e.keff.org": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "elec.luggs.co": { "pruning": "-", "s": "443", - "version": "1.2" + "version": "1.4" }, "electrum-server.ninja": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "electrum.achow101.com": { + "electrum-unlimited.criptolayer.net": { + "pruning": "-", + "s": "50002", + "version": "1.4" + }, + "electrum.eff.ro": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "electrum.cutie.ga": { + "electrum.festivaldelhumor.org": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "electrum.hsmiths.com": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "electrum.leblancnet.us": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "electrum.meltingice.net": { + "electrum.mindspot.org": { "pruning": "-", "s": "50002", - "t": "50001", - "version": "1.2" - }, - "electrum.nute.net": { - "pruning": "-", - "s": "50002", - "version": "1.2" - }, - "electrum.poorcoding.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" + "version": "1.4" }, "electrum.qtornado.com": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "electrum.vom-stausee.de": { + "electrum.taborsky.cz": { + "pruning": "-", + "s": "50002", + "version": "1.4" + }, + "electrum.villocq.com": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "electrum0.snel.it": { + "electrum2.eff.ro": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "electrumx-core.1209k.com": { + "electrum2.villocq.com": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" + }, + "electrum3.hachre.de": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" }, "electrumx.bot.nu": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" + }, + "electrumx.ddns.net": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "electrumx.ftp.sh": { + "pruning": "-", + "s": "50002", + "version": "1.4" + }, + "electrumx.ml": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" }, "electrumx.nmdps.net": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "electrumx.westeurope.cloudapp.azure.com": { + "electrumx.soon.it": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "electrumxhqdsmlu.onion": { "pruning": "-", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "elx2018.mooo.com": { + "elx01.knas.systems": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" + }, + "enode.duckdns.org": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "erbium1.sytes.net": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "fedaykin.goip.de": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "fn.48.org": { + "pruning": "-", + "s": "50002", + "t": "50003", + "version": "1.4" }, "helicarrier.bauerj.eu": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "hsmiths4fyqlw5xw.onion": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "hsmiths5mjk6uijs.onion": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "j5jfrdthqt5g25xz.onion": { + "icarus.tetradrachm.net": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "kirsche.emzy.de": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "luggscoqbymhvnkp.onion": { "pruning": "-", "t": "80", - "version": "1.2" + "version": "1.4" }, "ndnd.selfhost.eu": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "ndndword5lpb7eex.onion": { "pruning": "-", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "node.arihanc.com": { + "oneweek.duckdns.org": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, - "node.erratic.space": { + "orannis.com": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "ozahtqwp25chjdjd.onion": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "qtornadoklbgdyww.onion": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" }, "rbx.curalle.ovh": { "pruning": "-", "s": "50002", - "version": "1.2" - }, - "ruuxwv74pjxms3ws.onion": { - "pruning": "-", - "s": "10042", - "t": "50001", - "version": "1.2" + "version": "1.4" }, "s7clinmo4cazmhul.onion": { "pruning": "-", "t": "50001", - "version": "1.2" - }, - "songbird.bauerj.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.2" - }, - "spv.48.org": { - "pruning": "-", - "s": "50002", - "t": "50003", - "version": "1.2" + "version": "1.4" }, "tardis.bauerj.eu": { "pruning": "-", "s": "50002", "t": "50001", - "version": "1.2" + "version": "1.4" + }, + "technetium.network": { + "pruning": "-", + "s": "50002", + "version": "1.4" + }, + "tomscryptos.com": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "ulrichard.ch": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "us.electrum.be": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "vmd27610.contaboserver.net": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "vmd30612.contaboserver.net": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "wsw6tua3xl24gsmi264zaep6seppjyrkyucpsmuxnjzyt3f3j6swshad.onion": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" + }, + "xray587.startdedicated.de": { + "pruning": "-", + "s": "50002", + "version": "1.4" + }, + "yuio.top": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" } } From 7a46bd10893b30779ba180442ab60495a416fd6c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 3 Nov 2018 17:11:08 +0100 Subject: [PATCH 093/301] network: minor clean-up --- electrum/network.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index 36c957d8..1dd2654f 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -382,7 +382,11 @@ class Network(PrintError): def get_parameters(self) -> NetworkParameters: host, port, protocol = deserialize_server(self.default_server) - return NetworkParameters(host, port, protocol, self.proxy, self.auto_connect) + return NetworkParameters(host=host, + port=port, + protocol=protocol, + proxy=self.proxy, + auto_connect=self.auto_connect) def get_donation_address(self): if self.is_connected(): @@ -487,15 +491,16 @@ class Network(PrintError): try: deserialize_server(serialize_server(host, port, protocol)) if proxy: - proxy_modes.index(proxy["mode"]) + 1 + proxy_modes.index(proxy['mode']) + 1 int(proxy['port']) except: return self.config.set_key('auto_connect', net_params.auto_connect, False) - self.config.set_key("proxy", proxy_str, False) - self.config.set_key("server", server_str, True) + self.config.set_key('proxy', proxy_str, False) + self.config.set_key('server', server_str, True) # abort if changes were not allowed by config - if self.config.get('server') != server_str or self.config.get('proxy') != proxy_str: + if self.config.get('server') != server_str \ + or self.config.get('proxy') != proxy_str: return async with self.restart_lock: @@ -796,6 +801,14 @@ class Network(PrintError): async def _start(self): assert not self.main_taskgroup self.main_taskgroup = SilentTaskGroup() + assert not self.interface and not self.interfaces + assert not self.connecting and not self.server_queue + self.print_error('starting network') + self.disconnected_servers = set([]) + self.protocol = deserialize_server(self.default_server)[2] + self.server_queue = queue.Queue() + self._set_proxy(deserialize_proxy(self.config.get('proxy'))) + self._start_interface(self.default_server) async def main(): try: @@ -808,14 +821,6 @@ class Network(PrintError): raise e asyncio.run_coroutine_threadsafe(main(), self.asyncio_loop) - assert not self.interface and not self.interfaces - assert not self.connecting and not self.server_queue - self.print_error('starting network') - self.disconnected_servers = set([]) - self.protocol = deserialize_server(self.default_server)[2] - self.server_queue = queue.Queue() - self._set_proxy(deserialize_proxy(self.config.get('proxy'))) - self._start_interface(self.default_server) self.trigger_callback('network_updated') def start(self, jobs: List=None): From 160bc93e269984cdab8e2d11ee383a52b79e4b60 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 3 Nov 2018 17:19:51 +0100 Subject: [PATCH 094/301] implement oneserver option for kivy closes #4826 --- electrum/gui/kivy/main_window.py | 9 +++++++++ electrum/gui/kivy/uix/ui_screens/network.kv | 7 +++++++ electrum/network.py | 16 ++++++++++++---- setup.py | 2 +- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 13f70e17..c34e5ef9 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -101,6 +101,14 @@ class ElectrumWindow(App): def toggle_auto_connect(self, x): self.auto_connect = not self.auto_connect + oneserver = BooleanProperty(False) + def on_oneserver(self, instance, x): + net_params = self.network.get_parameters() + net_params = net_params._replace(oneserver=self.oneserver) + self.network.run_from_another_thread(self.network.set_parameters(net_params)) + def toggle_oneserver(self, x): + self.oneserver = not self.oneserver + def choose_server_dialog(self, popup): from .uix.dialogs.choice_dialog import ChoiceDialog protocol = 's' @@ -275,6 +283,7 @@ class ElectrumWindow(App): self.server_host = net_params.host self.server_port = net_params.port self.auto_connect = net_params.auto_connect + self.oneserver = net_params.oneserver self.proxy_config = net_params.proxy if net_params.proxy else {} self.plugins = kwargs.get('plugins', []) diff --git a/electrum/gui/kivy/uix/ui_screens/network.kv b/electrum/gui/kivy/uix/ui_screens/network.kv index 99fb8366..31363ce2 100644 --- a/electrum/gui/kivy/uix/ui_screens/network.kv +++ b/electrum/gui/kivy/uix/ui_screens/network.kv @@ -37,6 +37,13 @@ Popup: description: _("Select your server automatically") action: app.toggle_auto_connect + CardSeparator + SettingsItem: + title: _("One-server mode") + ': ' + ('ON' if app.oneserver else 'OFF') + description: _("Only connect to a single server") + action: app.toggle_oneserver + disabled: app.auto_connect and not app.oneserver + CardSeparator SettingsItem: value: "%d blocks" % app.num_blocks diff --git a/electrum/network.py b/electrum/network.py index 1dd2654f..1dec8bc0 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -116,6 +116,7 @@ class NetworkParameters(NamedTuple): protocol: str proxy: Optional[dict] auto_connect: bool + oneserver: bool = False proxy_modes = ['socks4', 'socks5'] @@ -176,7 +177,6 @@ class Network(PrintError): if config is None: config = {} # Do not use mutables as default values! self.config = SimpleConfig(config) if isinstance(config, dict) else config # type: SimpleConfig - self.num_server = 10 if not self.config.get('oneserver') else 0 blockchain.blockchains = blockchain.read_blockchains(self.config) self.print_error("blockchains", list(blockchain.blockchains)) self._blockchain_preferred_block = self.config.get('blockchain_preferred_block', None) # type: Optional[Dict] @@ -386,7 +386,8 @@ class Network(PrintError): port=port, protocol=protocol, proxy=self.proxy, - auto_connect=self.auto_connect) + auto_connect=self.auto_connect, + oneserver=self.oneserver) def get_donation_address(self): if self.is_connected(): @@ -496,16 +497,18 @@ class Network(PrintError): except: return self.config.set_key('auto_connect', net_params.auto_connect, False) + self.config.set_key('oneserver', net_params.oneserver, False) self.config.set_key('proxy', proxy_str, False) self.config.set_key('server', server_str, True) # abort if changes were not allowed by config if self.config.get('server') != server_str \ - or self.config.get('proxy') != proxy_str: + or self.config.get('proxy') != proxy_str \ + or self.config.get('oneserver') != net_params.oneserver: return async with self.restart_lock: self.auto_connect = net_params.auto_connect - if self.proxy != proxy or self.protocol != protocol: + if self.proxy != proxy or self.protocol != protocol or self.oneserver != net_params.oneserver: # Restart the network defaulting to the given server await self._stop() self.default_server = server_str @@ -515,6 +518,10 @@ class Network(PrintError): else: await self.switch_lagging_interface() + def _set_oneserver(self, oneserver: bool): + self.num_server = 10 if not oneserver else 0 + self.oneserver = oneserver + async def _switch_to_random_interface(self): '''Switch to a random connected server other than the current one''' servers = self.get_interfaces() # Those in connected state @@ -808,6 +815,7 @@ class Network(PrintError): self.protocol = deserialize_server(self.default_server)[2] self.server_queue = queue.Queue() self._set_proxy(deserialize_proxy(self.config.get('proxy'))) + self._set_oneserver(self.config.get('oneserver')) self._start_interface(self.default_server) async def main(): diff --git a/setup.py b/setup.py index 363e419a..3e71caab 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ import subprocess from setuptools import setup, find_packages from setuptools.command.install import install -MIN_PYTHON_VERSION = "3.6" +MIN_PYTHON_VERSION = "3.6.1" _min_python_version_tuple = tuple(map(int, (MIN_PYTHON_VERSION.split(".")))) From a89e67eeedd29bd1a33cc6b0b39af377f62674b6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 4 Nov 2018 19:25:23 +0100 Subject: [PATCH 095/301] network: trivial clean-up --- electrum/daemon.py | 6 +++--- electrum/network.py | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index b811905f..a2f80c62 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -194,18 +194,18 @@ class Daemon(DaemonThread): response = False elif sub == 'status': if self.network: - p = self.network.get_parameters() + net_params = self.network.get_parameters() current_wallet = self.cmd_runner.wallet current_wallet_path = current_wallet.storage.path \ if current_wallet else None response = { 'path': self.network.config.path, - 'server': p[0], + 'server': net_params.host, 'blockchain_height': self.network.get_local_height(), 'server_height': self.network.get_server_height(), 'spv_nodes': len(self.network.get_interfaces()), 'connected': self.network.is_connected(), - 'auto_connect': p[4], + 'auto_connect': net_params.auto_connect, 'version': ELECTRUM_VERSION, 'wallets': {k: w.is_up_to_date() for k, w in self.wallets.items()}, diff --git a/electrum/network.py b/electrum/network.py index 1dd2654f..0000b25f 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -299,7 +299,7 @@ class Network(PrintError): lh = self.get_local_height() result = (lh - sh) > 1 if result: - self.print_error('%s is lagging (%d vs %d)' % (self.default_server, sh, lh)) + self.print_error(f'{self.default_server} is lagging ({sh} vs {lh})') return result def _set_status(self, status): @@ -350,13 +350,15 @@ class Network(PrintError): for i in FEE_ETA_TARGETS: fee_tasks.append((i, await group.spawn(session.send_request('blockchain.estimatefee', [i])))) self.config.mempool_fees = histogram = histogram_task.result() - self.print_error('fee_histogram', histogram) + self.print_error(f'fee_histogram {histogram}') self.notify('fee_histogram') - for i, task in fee_tasks: + fee_estimates_eta = {} + for nblock_target, task in fee_tasks: fee = int(task.result() * COIN) - self.print_error("fee_estimates[%d]" % i, fee) + fee_estimates_eta[nblock_target] = fee if fee < 0: continue - self.config.update_fee_estimates(i, fee) + self.config.update_fee_estimates(nblock_target, fee) + self.print_error(f'fee_estimates {fee_estimates_eta}') self.notify('fee') def get_status_value(self, key): From 1b46866e34b7b95ff1c5e0df59b5b6337b4ff68f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 5 Nov 2018 01:53:35 +0100 Subject: [PATCH 096/301] qt: re sweeping, minor clean-up --- electrum/gui/qt/main_window.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 0729ca10..dead3da3 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -57,7 +57,8 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, UnknownBaseUnit, DECIMAL_POINT_DEFAULT) from electrum.transaction import Transaction, TxOutput from electrum.address_synchronizer import AddTransactionException -from electrum.wallet import Multisig_Wallet, CannotBumpFee, Abstract_Wallet +from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, + sweep_preparations) from electrum.version import ELECTRUM_VERSION from electrum.network import Network from electrum.exchange_rate import FxThread @@ -2601,19 +2602,20 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): address_e.textChanged.connect(on_address) if not d.exec_(): return - from electrum.wallet import sweep_preparations + # user pressed "sweep" try: - self.do_clear() coins, keypairs = sweep_preparations(get_pk(), self.network) - self.tx_external_keypairs = keypairs - self.spend_coins(coins) - self.payto_e.setText(get_address()) - self.spend_max() - self.payto_e.setFrozen(True) - self.amount_e.setFrozen(True) except Exception as e: # FIXME too broad... + #traceback.print_exc(file=sys.stderr) self.show_message(str(e)) return + self.do_clear() + self.tx_external_keypairs = keypairs + self.spend_coins(coins) + self.payto_e.setText(get_address()) + self.spend_max() + self.payto_e.setFrozen(True) + self.amount_e.setFrozen(True) self.warn_if_watching_only() def _do_import(self, title, header_layout, func): From 1686a97ece31aaa1d2c0e6c7042137a5e8a04943 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 5 Nov 2018 19:31:17 +0100 Subject: [PATCH 097/301] bip70 PRs: use aiohttp instead of requests. use proxy. small fixes. --- electrum/ecc.py | 2 +- electrum/gui/qt/main_window.py | 10 ++-- electrum/paymentrequest.py | 84 +++++++++++++++++++--------------- electrum/transaction.py | 3 +- electrum/util.py | 13 +++--- 5 files changed, 62 insertions(+), 50 deletions(-) diff --git a/electrum/ecc.py b/electrum/ecc.py index 4a3cd2e9..7c2cab92 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -327,7 +327,7 @@ def verify_message_with_address(address: str, sig65: bytes, message: bytes): public_key.verify_message_hash(sig65[1:], h) return True except Exception as e: - print_error("Verification error: {0}".format(e)) + print_error(f"Verification error: {repr(e)}") return False diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index dead3da3..2d214dce 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -36,6 +36,7 @@ from decimal import Decimal import base64 from functools import partial import queue +import asyncio from PyQt5.QtGui import * from PyQt5.QtCore import * @@ -1656,10 +1657,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.invoices.set_paid(pr, tx.txid()) self.invoices.save() self.payment_request = None - refund_address = self.wallet.get_receiving_addresses()[0] - ack_status, ack_msg = pr.send_ack(str(tx), refund_address) - if ack_status: - msg = ack_msg + refund_address = self.wallet.get_receiving_address() + coro = pr.send_payment_and_receive_paymentack(str(tx), refund_address) + fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) + ack_status, ack_msg = fut.result(timeout=20) + msg += f"\n\nPayment ACK: {ack_status}.\nAck message: {ack_msg}" return status, msg # Capture current TL window; override might be removed on return diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index ba1cd7ea..b50c399b 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -27,9 +27,10 @@ import sys import time import traceback import json -import requests +import requests import urllib.parse +import aiohttp try: @@ -38,15 +39,17 @@ except ImportError: sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'protoc --proto_path=electrum/ --python_out=electrum/ electrum/paymentrequest.proto'") from . import bitcoin, ecc, util, transaction, x509, rsakey -from .util import print_error, bh2u, bfh, export_meta, import_meta +from .util import print_error, bh2u, bfh, export_meta, import_meta, make_aiohttp_session from .crypto import sha256 from .bitcoin import TYPE_ADDRESS from .transaction import TxOutput +from .network import Network + REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'} ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'} -ca_path = requests.certs.where() +ca_path = requests.certs.where() # FIXME do we need to depend on requests here? ca_list = None ca_keyID = None @@ -64,25 +67,31 @@ PR_UNKNOWN = 2 # sent but not propagated PR_PAID = 3 # send and propagated - -def get_payment_request(url): +async def get_payment_request(url: str) -> 'PaymentRequest': u = urllib.parse.urlparse(url) error = None - if u.scheme in ['http', 'https']: + if u.scheme in ('http', 'https'): + resp_content = None try: - response = requests.request('GET', url, headers=REQUEST_HEADERS) - response.raise_for_status() - # Guard against `bitcoin:`-URIs with invalid payment request URLs - if "Content-Type" not in response.headers \ - or response.headers["Content-Type"] != "application/bitcoin-paymentrequest": - data = None - error = "payment URL not pointing to a payment request handling server" - else: - data = response.content - print_error('fetched payment request', url, len(response.content)) - except requests.exceptions.RequestException: + proxy = Network.get_instance().proxy + async with make_aiohttp_session(proxy, headers=REQUEST_HEADERS) as session: + async with session.get(url) as response: + resp_content = await response.read() + response.raise_for_status() + # Guard against `bitcoin:`-URIs with invalid payment request URLs + if "Content-Type" not in response.headers \ + or response.headers["Content-Type"] != "application/bitcoin-paymentrequest": + data = None + error = "payment URL not pointing to a payment request handling server" + else: + data = resp_content + data_len = len(data) if data is not None else None + print_error('fetched payment request', url, data_len) + except aiohttp.ClientError as e: + error = f"Error while contacting payment URL:\n{repr(e)}" + if isinstance(e, aiohttp.ClientResponseError) and e.status == 400 and resp_content: + error += "\n" + resp_content.decode("utf8") data = None - error = "payment URL not pointing to a valid server" elif u.scheme == 'file': try: with open(u.path, 'r', encoding='utf-8') as f: @@ -92,7 +101,7 @@ def get_payment_request(url): error = "payment URL not pointing to a valid file" else: data = None - error = "Unknown scheme for payment request. URL: {}".format(url) + error = f"Unknown scheme for payment request. URL: {url}" pr = PaymentRequest(data, error) return pr @@ -255,7 +264,7 @@ class PaymentRequest: def get_outputs(self): return self.outputs[:] - def send_ack(self, raw_tx, refund_addr): + async def send_payment_and_receive_paymentack(self, raw_tx, refund_addr): pay_det = self.details if not self.details.payment_url: return False, "no url" @@ -267,24 +276,25 @@ class PaymentRequest: paymnt.memo = "Paid using Electrum" pm = paymnt.SerializeToString() payurl = urllib.parse.urlparse(pay_det.payment_url) + resp_content = None try: - r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=ca_path) - except requests.exceptions.SSLError: - print("Payment Message/PaymentACK verify Failed") - try: - r = requests.post(payurl.geturl(), data=pm, headers=ACK_HEADERS, verify=False) - except Exception as e: - print(e) - return False, "Payment Message/PaymentACK Failed" - if r.status_code >= 500: - return False, r.reason - try: - paymntack = pb2.PaymentACK() - paymntack.ParseFromString(r.content) - except Exception: - return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received." - print("PaymentACK message received: %s" % paymntack.memo) - return True, paymntack.memo + proxy = Network.get_instance().proxy + async with make_aiohttp_session(proxy, headers=ACK_HEADERS) as session: + async with session.post(payurl.geturl(), data=pm) as response: + resp_content = await response.read() + response.raise_for_status() + try: + paymntack = pb2.PaymentACK() + paymntack.ParseFromString(resp_content) + except Exception: + return False, "PaymentACK could not be processed. Payment was sent; please manually verify that payment was received." + print(f"PaymentACK message received: {paymntack.memo}") + return True, paymntack.memo + except aiohttp.ClientError as e: + error = f"Payment Message/PaymentACK Failed:\n{repr(e)}" + if isinstance(e, aiohttp.ClientResponseError) and e.status == 400 and resp_content: + error += "\n" + resp_content.decode("utf8") + return False, error def make_unsigned_request(req): diff --git a/electrum/transaction.py b/electrum/transaction.py index 2965d77c..7a4bb8cf 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -788,7 +788,8 @@ class Transaction: return self @classmethod - def pay_script(self, output_type, addr): + def pay_script(self, output_type, addr: str) -> str: + """Returns scriptPubKey in hex form.""" if output_type == TYPE_SCRIPT: return addr elif output_type == TYPE_ADDRESS: diff --git a/electrum/util.py b/electrum/util.py index 08ea73d4..22a5aac5 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -23,7 +23,7 @@ import binascii import os, sys, re, json from collections import defaultdict -from typing import NamedTuple, Union, TYPE_CHECKING, Tuple, Optional +from typing import NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable from datetime import datetime import decimal from decimal import Decimal @@ -693,7 +693,7 @@ def block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional #_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) #urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) -def parse_URI(uri, on_pr=None): +def parse_URI(uri: str, on_pr: Callable=None) -> dict: from . import bitcoin from .bitcoin import COIN @@ -746,18 +746,17 @@ def parse_URI(uri, on_pr=None): sig = out.get('sig') name = out.get('name') if on_pr and (r or (name and sig)): - def get_payment_request_thread(): + async def get_payment_request(): from . import paymentrequest as pr if name and sig: s = pr.serialize_request(out).SerializeToString() request = pr.PaymentRequest(s) else: - request = pr.get_payment_request(r) + request = await pr.get_payment_request(r) if on_pr: on_pr(request) - t = threading.Thread(target=get_payment_request_thread) - t.setDaemon(True) - t.start() + loop = asyncio.get_event_loop() + asyncio.run_coroutine_threadsafe(get_payment_request(), loop) return out From 5d52dc204cc85a6660d2790769ecd7128d228646 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 6 Nov 2018 16:17:18 +0100 Subject: [PATCH 098/301] coldcard: fix spending when there is no change follow-up 9037f25da13873c751a1a333b43bae29296b0c13 --- electrum/plugins/coldcard/coldcard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index 8a348f3f..444a3436 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -390,7 +390,7 @@ class Coldcard_KeyStore(Hardware_KeyStore): wallet.add_hw_info(tx) # wallet.add_hw_info installs this attr - assert tx.output_info, 'need data about outputs' + assert tx.output_info is not None, 'need data about outputs' # Build map of pubkey needed as derivation from master, in PSBT binary format # 1) binary version of the common subpath for all keys From 8b61d18a9f252ee69f270c84738dbe82b73b6c31 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 6 Nov 2018 17:04:12 +0100 Subject: [PATCH 099/301] transaction.serialize_output: use namedtuple fields --- electrum/transaction.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index 7a4bb8cf..d91de2b5 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1026,10 +1026,9 @@ class Transaction: if outputs: self._outputs.sort(key = lambda o: (o.value, self.pay_script(o.type, o.address))) - def serialize_output(self, output): - output_type, addr, amount = output - s = int_to_hex(amount, 8) - script = self.pay_script(output_type, addr) + def serialize_output(self, output: TxOutput) -> str: + s = int_to_hex(output.value, 8) + script = self.pay_script(output.type, output.address) s += var_int(len(script)//2) s += script return s From 6d5b28a9c5228e49ab173c7e034196ae12293b84 Mon Sep 17 00:00:00 2001 From: neoCogent Date: Wed, 7 Nov 2018 11:18:00 +1000 Subject: [PATCH 100/301] add blockstream.info as explorer option (#4829) --- electrum/util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/util.py b/electrum/util.py index 22a5aac5..54ed08d6 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -625,6 +625,8 @@ mainnet_block_explorers = { {'tx': 'tx/', 'addr': 'address/'}), 'Blockr.io': ('https://btc.blockr.io/', {'tx': 'tx/info/', 'addr': 'address/info/'}), + 'Blockstream.info': ('https://blockstream.info/', + {'tx': 'tx/', 'addr': 'address/'}), 'Blocktrail.com': ('https://www.blocktrail.com/BTC/', {'tx': 'tx/', 'addr': 'address/'}), 'BTC.com': ('https://chain.btc.com/', @@ -656,6 +658,8 @@ testnet_block_explorers = { {'tx': 'tx/', 'addr': 'address/'}), 'Blockchain.info': ('https://testnet.blockchain.info/', {'tx': 'tx/', 'addr': 'address/'}), + 'Blockstream.info': ('https://blockstream.info/testnet/', + {'tx': 'tx/', 'addr': 'address/'}), 'BTC.com': ('https://tchain.btc.com/', {'tx': '', 'addr': ''}), 'smartbit.com.au': ('https://testnet.smartbit.com.au/', From 39fb5b8f581742c9909646ae3e78db968a0c9ee1 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 7 Nov 2018 12:38:29 +0100 Subject: [PATCH 101/301] use blockstream.info as default block explorer --- electrum/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/util.py b/electrum/util.py index 54ed08d6..064659f7 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -674,7 +674,7 @@ def block_explorer_info(): def block_explorer(config: 'SimpleConfig') -> str: from . import constants - default_ = 'Blockchair.com' if not constants.net.TESTNET else 'smartbit.com.au' + default_ = 'Blockstream.info' be_key = config.get('block_explorer', default_) be = block_explorer_info().get(be_key) return be_key if be is not None else default_ From 7d114ff32d18f87240c63c2edb866e62873bdb75 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 7 Nov 2018 14:48:33 +0100 Subject: [PATCH 102/301] cpfp: don't reuse address --- electrum/tests/test_wallet_vertical.py | 12 ++++++------ electrum/wallet.py | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index ba9eb90a..da428593 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -898,10 +898,10 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual(tx.txid(), tx_copy.txid()) self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - self.assertEqual('010000000168368aeb2fba618b62c8b64d03513b6185f58623433439b649a3af1889bf7399000000006a47304402203a0b369e46c5fbacb83044b7ab9d69ff7998774041d6870993504915bc495d210220272833b870d8abca516adb7dc4cb27892b1b6e4b52fbfeb592a72c3e795eb213012102a7536f0bfbc60c5a8e86e2b9df26431fc062f9f454016dbc26f2467e0bc98b3ffdffffff01f0874b00000000001976a9141df43441a3a3ee563e560d3ddc7e07cc9f9c3cdb88acbe391400', + self.assertEqual('010000000168368aeb2fba618b62c8b64d03513b6185f58623433439b649a3af1889bf7399000000006b483045022100d58301d9e7543b29776f5f1a4410cdf964fa35547713bd1c7d852b58b1c1100602202fcf71a1f2a80055db321adc4c73488ba624425cf00cca38960f737c3b6667d2012102a7536f0bfbc60c5a8e86e2b9df26431fc062f9f454016dbc26f2467e0bc98b3ffdffffff01f0874b00000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88acbe391400', str(tx_copy)) - self.assertEqual('47500a425518b5542d94db1157f473b8cf322d31ea97a1a642fec19386cdb761', tx_copy.txid()) - self.assertEqual('47500a425518b5542d94db1157f473b8cf322d31ea97a1a642fec19386cdb761', tx_copy.wtxid()) + self.assertEqual('d3c24f7a2315d3294a04a059761014815ea6832645e92a1c18a4c7c617c3803c', tx_copy.txid()) + self.assertEqual('d3c24f7a2315d3294a04a059761014815ea6832645e92a1c18a4c7c617c3803c', tx_copy.wtxid()) wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) @@ -984,10 +984,10 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual(tx.txid(), tx_copy.txid()) self.assertEqual(tx.wtxid(), tx_copy.wtxid()) - self.assertEqual('010000000001014a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff01f0874b000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab900248304502210098fbe458a9f1c595d6bf63962fad00300a7b60c6dd8b2e7625f3804a3bf1086602204bc8a46fb162be8f85a23644eccf9f4223fa092f5c861144676a34dc83a7c39d012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbd391400', + self.assertEqual('010000000001014a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff01f0874b0000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b70247304402200a0855f38f3f5015e78c5d2161c1d881e16ea8169b375ef423feb0233ed0402d0220238c48d56eb846e3d71945b856554f2665ff55dfb7d52249fca6de0b7cecb338012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbd391400', str(tx_copy)) - self.assertEqual('38a21c67336232c88ae15311f329197c69ee70e872f8acb5bc9c2b6417c35ad8', tx_copy.txid()) - self.assertEqual('b5b8264ed5f3e03d48ef82fa2a25278cd9c0563fa78e557f370b7e0558293172', tx_copy.wtxid()) + self.assertEqual('92fe0029019e8f7476fbee38a684c40c2d726bc769ea064e9cb044d09e715be1', tx_copy.txid()) + self.assertEqual('5ab92fa14ffecc3c510a77f994bdf6bb5aa810e74ddf41b8a03da088d5a96326', tx_copy.wtxid()) wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) diff --git a/electrum/wallet.py b/electrum/wallet.py index 1acca2a9..70b4b1c9 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -727,7 +727,8 @@ class Abstract_Wallet(AddressSynchronizer): return self.add_input_info(item) inputs = [item] - outputs = [TxOutput(TYPE_ADDRESS, address, value - fee)] + out_address = self.get_unused_address() or address + outputs = [TxOutput(TYPE_ADDRESS, out_address, value - fee)] locktime = self.get_local_height() return Transaction.from_io(inputs, outputs, locktime=locktime) From 3d4773b1618cb8364b5a9a36018b305552258ad7 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 7 Nov 2018 15:03:54 +0100 Subject: [PATCH 103/301] wizard: make segwit/bech32 the default choice during wallet creation --- electrum/base_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index a6776da6..921aee93 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -538,8 +538,8 @@ class BaseWizard(object): _("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.") ]) choices = [ - ('create_standard_seed', _('Standard')), ('create_segwit_seed', _('Segwit')), + ('create_standard_seed', _('Standard')), ] self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) From 47b6d3c52c863e9f677731920817f1237527a028 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 8 Nov 2018 13:01:40 +0100 Subject: [PATCH 104/301] wizard: make native segwit (bech32) the default for bip39/hw --- electrum/base_wizard.py | 7 +++++-- electrum/bip32.py | 4 ++-- electrum/gui/qt/installwizard.py | 13 +++++++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 921aee93..213af567 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -334,12 +334,14 @@ class BaseWizard(object): # There is no general standard for HD multisig. # For legacy, this is partially compatible with BIP45; assumes index=0 # For segwit, a custom path is used, as there is no standard at all. + default_choice_idx = 2 choices = [ ('standard', 'legacy multisig (p2sh)', "m/45'/0"), ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')), ('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')), ] else: + default_choice_idx = 2 choices = [ ('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)), ('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)), @@ -349,7 +351,8 @@ class BaseWizard(object): try: self.choice_and_line_dialog( run_next=f, title=_('Script type and Derivation path'), message1=message1, - message2=message2, choices=choices, test_text=is_bip32_derivation) + message2=message2, choices=choices, test_text=is_bip32_derivation, + default_choice_idx=default_choice_idx) return except ScriptTypeNotSupported as e: self.show_error(e) @@ -539,7 +542,7 @@ class BaseWizard(object): ]) choices = [ ('create_segwit_seed', _('Segwit')), - ('create_standard_seed', _('Standard')), + ('create_standard_seed', _('Legacy')), ] self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) diff --git a/electrum/bip32.py b/electrum/bip32.py index 967ab708..0f671b28 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -189,7 +189,7 @@ def xpub_from_pubkey(xtype, cK): return serialize_xpub(xtype, b'\x00'*32, cK) -def bip32_derivation(s): +def bip32_derivation(s: str) -> int: if not s.startswith('m/'): raise ValueError('invalid bip32 derivation path: {}'.format(s)) s = s[2:] @@ -216,7 +216,7 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]: path.append(abs(int(x)) | prime) return path -def is_bip32_derivation(x): +def is_bip32_derivation(x: str) -> bool: try: [ i for i in bip32_derivation(x)] return True diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index 58583a7a..dc4ce28e 100644 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -1,9 +1,12 @@ +# Copyright (C) 2018 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php import os import sys import threading import traceback -from typing import Tuple +from typing import Tuple, List, Callable from PyQt5.QtCore import * from PyQt5.QtGui import * @@ -506,8 +509,9 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): return clayout.selected_index() @wizard_dialog - def choice_and_line_dialog(self, title, message1, choices, message2, - test_text, run_next) -> Tuple[str, str]: + def choice_and_line_dialog(self, title: str, message1: str, choices: List[Tuple[str, str, str]], + message2: str, test_text: Callable[[str], int], + run_next, default_choice_idx: int=0) -> Tuple[str, str]: vbox = QVBoxLayout() c_values = [x[0] for x in choices] @@ -516,7 +520,8 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): def on_choice_click(clayout): idx = clayout.selected_index() line.setText(c_default_text[idx]) - clayout = ChoicesLayout(message1, c_titles, on_choice_click) + clayout = ChoicesLayout(message1, c_titles, on_choice_click, + checked_index=default_choice_idx) vbox.addLayout(clayout.layout()) vbox.addSpacing(50) From dace2e5495165a1a6ff186442966dde1934b69c0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 8 Nov 2018 17:07:05 +0100 Subject: [PATCH 105/301] trezor: don't let bridge transport failing block all other transports [trezor] connecting to device at bridge:hid... [trezor] connected to device at bridge:hid... Traceback (most recent call last): File "...\electrum\electrum\base_wizard.py", line 255, in choose_hw_device u = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices) File "...\electrum\electrum\plugin.py", line 501, in unpaired_device_infos client = self.create_client(device, handler, plugin) File "...\electrum\electrum\plugin.py", line 374, in create_client client = plugin.create_client(device, handler) File "...\electrum\electrum\plugins\trezor\trezor.py", line 124, in create_client client = self.client_class(transport, handler, self) File "...\electrum\electrum\plugins\trezor\client.py", line 7, in __init__ ProtocolMixin.__init__(self, transport=transport) File "...\Python36-32\lib\site-packages\trezorlib\client.py", line 444, in __init__ self.init_device() File "...\Python36-32\lib\site-packages\trezorlib\client.py", line 454, in init_device self.features = expect(proto.Features)(self.call)(init_msg) File "...\Python36-32\lib\site-packages\trezorlib\client.py", line 115, in wrapped_f ret = f(*args, **kwargs) File "...\Python36-32\lib\site-packages\trezorlib\client.py", line 129, in wrapped_f client.transport.session_begin() File "...\Python36-32\lib\site-packages\trezorlib\transport\__init__.py", line 42, in session_begin self.open() File "...\Python36-32\lib\site-packages\trezorlib\transport\bridge.py", line 69, in open raise TransportException('trezord: Could not acquire session' + get_error(r)) trezorlib.transport.TransportException: trezord: Could not acquire session (error=400 str=wrong previous session) [DeviceMgr] error getting device infos for trezor: trezord: Could not acquire session (error=400 str=wrong previous session) --- electrum/base_wizard.py | 10 ++++++++-- electrum/plugin.py | 8 ++++++-- electrum/plugins/trezor/trezor.py | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 213af567..431e72e0 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -27,6 +27,7 @@ import os import sys import traceback from functools import partial +from typing import List, TYPE_CHECKING, Tuple from . import bitcoin from . import keystore @@ -41,6 +42,9 @@ from .util import UserCancelled, InvalidPassword, WalletFileException from .simple_config import SimpleConfig from .plugin import Plugins +if TYPE_CHECKING: + from .plugin import DeviceInfo + # hardware device setup purpose HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2) @@ -230,7 +234,7 @@ class BaseWizard(object): # check available plugins supported_plugins = self.plugins.get_hardware_support() # scan devices - devices = [] + devices = [] # type: List[Tuple[str, DeviceInfo]] devmgr = self.plugins.device_manager try: scanned_devices = devmgr.scan_devices() @@ -254,6 +258,7 @@ class BaseWizard(object): # FIXME: side-effect: unpaired_device_info sets client.handler u = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices) 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' @@ -278,7 +283,8 @@ class BaseWizard(object): for name, info in devices: state = _("initialized") if info.initialized else _("wiped") label = info.label or _("An unnamed {}").format(name) - descr = "%s [%s, %s]" % (label, name, state) + descr = f"{label} [{name}, {state}]" + # TODO maybe expose info.device.path (mainly for transport type) choices.append(((name, info), descr)) msg = _('Select a device') + ':' self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose)) diff --git a/electrum/plugin.py b/electrum/plugin.py index d8935d82..df2b3719 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -485,7 +485,7 @@ 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, devices=None): + def unpaired_device_infos(self, handler, plugin: 'HW_PluginBase', devices=None): '''Returns a list of DeviceInfo objects: one for each connected, unpaired device accepted by the plugin.''' if not plugin.libraries_available: @@ -498,7 +498,11 @@ class DeviceMgr(ThreadJob, PrintError): for device in devices: if device.product_key not in plugin.DEVICE_IDS: continue - client = self.create_client(device, handler, plugin) + try: + client = self.create_client(device, handler, plugin) + except BaseException as e: + self.print_error(f'failed to create client for {plugin.name} at {device.path}: {repr(e)}') + continue if not client: continue infos.append(DeviceInfo(device, client.label(), client.is_initialized())) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index e195b071..91b5aab3 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -121,6 +121,7 @@ class TrezorPlugin(HW_PluginBase): return self.print_error("connected to device at", device.path) + # note that this call can still raise! client = self.client_class(transport, handler, self) # Try a ping for device sanity From bd32b88f625a698bcc466faa17a3adb58ed229a4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 8 Nov 2018 19:46:15 +0100 Subject: [PATCH 106/301] introduce UserFacingException we should not raise generic Exception when wanting to communicate with the user. it makes distinguishing programming errors and messages hard, as the caller will necessarily need to catch all Exceptions then --- electrum/gui/qt/main_window.py | 13 +++++--- electrum/plugin.py | 4 ++- electrum/plugins/coldcard/coldcard.py | 14 ++++---- .../plugins/digitalbitbox/digitalbitbox.py | 20 ++++++------ electrum/plugins/hw_wallet/plugin.py | 6 ++-- electrum/plugins/keepkey/keepkey.py | 12 +++---- electrum/plugins/ledger/ledger.py | 32 +++++++++---------- electrum/plugins/safe_t/safe_t.py | 12 +++---- electrum/plugins/trezor/trezor.py | 12 +++---- electrum/util.py | 4 +++ 10 files changed, 70 insertions(+), 59 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2d214dce..d4d28500 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -55,7 +55,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, export_meta, import_meta, bh2u, bfh, InvalidPassword, base_units, base_units_list, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, quantize_feerate, - UnknownBaseUnit, DECIMAL_POINT_DEFAULT) + UnknownBaseUnit, DECIMAL_POINT_DEFAULT, UserFacingException) from electrum.transaction import Transaction, TxOutput from electrum.address_synchronizer import AddTransactionException from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, @@ -300,12 +300,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.raise_() def on_error(self, exc_info): - if not isinstance(exc_info[1], UserCancelled): + e = exc_info[1] + if isinstance(e, UserCancelled): + pass + elif isinstance(e, UserFacingException): + self.show_error(str(e)) + else: try: traceback.print_exception(*exc_info) except OSError: - pass # see #4418; try to at least show popup: - self.show_error(str(exc_info[1])) + pass # see #4418 + self.show_error(str(e)) def on_network(self, event, *args): if event == 'wallet_updated': diff --git a/electrum/plugin.py b/electrum/plugin.py index df2b3719..46d4fca9 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -32,7 +32,7 @@ from typing import NamedTuple, Any, Union, TYPE_CHECKING, Optional from .i18n import _ from .util import (profiler, PrintError, DaemonThread, UserCancelled, - ThreadJob, print_error) + ThreadJob, print_error, UserFacingException) from . import bip32 from . import plugins from .simple_config import SimpleConfig @@ -500,6 +500,8 @@ class DeviceMgr(ThreadJob, PrintError): continue try: client = self.create_client(device, handler, plugin) + except UserFacingException: + raise except BaseException as e: self.print_error(f'failed to create client for {plugin.name} at {device.path}: {repr(e)}') continue diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index 444a3436..f1558005 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -13,7 +13,7 @@ from electrum.keystore import Hardware_KeyStore, xpubkey_to_pubkey, Xpub from electrum.transaction import Transaction from electrum.wallet import Standard_Wallet from electrum.crypto import hash_160 -from electrum.util import print_error, bfh, bh2u, versiontuple +from electrum.util import print_error, bfh, bh2u, versiontuple, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase @@ -190,8 +190,8 @@ class CKCCClient: try: __, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub) except InvalidMasterKeyVersionBytes: - raise Exception(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.') - .format(self.device)) from None + raise UserFacingException(_('Invalid xpub magic. Make sure your {} device is set to the correct chain.') + .format(self.device)) from None if xtype != 'standard': xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) return xpub @@ -305,7 +305,7 @@ class Coldcard_KeyStore(Hardware_KeyStore): self.ux_busy = False if clear_client: self.client = None - raise Exception(message) + raise UserFacingException(message) def wrap_busy(func): # decorator: function takes over the UX on the device. @@ -318,7 +318,7 @@ class Coldcard_KeyStore(Hardware_KeyStore): return wrapper def decrypt_message(self, pubkey, message, password): - raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) + raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device)) @wrap_busy def sign_message(self, sequence, message, password): @@ -650,8 +650,8 @@ class ColdcardPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) + raise UserFacingException(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) client.handler = self.create_handler(wizard) def get_xpub(self, device_id, derivation, xtype, wizard): diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index bb9131c3..25e69e27 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -16,7 +16,7 @@ try: from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore from ..hw_wallet import HW_PluginBase - from electrum.util import print_error, to_string, UserCancelled + from electrum.util import print_error, to_string, UserCancelled, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET import time @@ -114,7 +114,7 @@ class DigitalBitbox_Client(): def dbb_has_password(self): reply = self.hid_send_plain(b'{"ping":""}') if 'ping' not in reply: - raise Exception(_('Device communication error. Please unplug and replug your Digital Bitbox.')) + raise UserFacingException(_('Device communication error. Please unplug and replug your Digital Bitbox.')) if reply['ping'] == 'password': return True return False @@ -221,7 +221,7 @@ class DigitalBitbox_Client(): return else: if self.hid_send_encrypt(b'{"device":"info"}')['device']['lock']: - raise Exception(_("Full 2FA enabled. This is not supported yet.")) + raise UserFacingException(_("Full 2FA enabled. This is not supported yet.")) # Use existing seed self.isInitialized = True @@ -294,7 +294,7 @@ class DigitalBitbox_Client(): msg = ('{"seed":{"source": "create", "key": "%s", "filename": "%s", "entropy": "%s"}}' % (key, filename, 'Digital Bitbox Electrum Plugin')).encode('utf8') reply = self.hid_send_encrypt(msg) if 'error' in reply: - raise Exception(reply['error']['message']) + raise UserFacingException(reply['error']['message']) def dbb_erase(self): @@ -304,16 +304,16 @@ class DigitalBitbox_Client(): hid_reply = self.hid_send_encrypt(b'{"reset":"__ERASE__"}') self.handler.finished() if 'error' in hid_reply: - raise Exception(hid_reply['error']['message']) + raise UserFacingException(hid_reply['error']['message']) else: self.password = None - raise Exception('Device erased') + raise UserFacingException('Device erased') def dbb_load_backup(self, show_msg=True): backups = self.hid_send_encrypt(b'{"backup":"list"}') if 'error' in backups: - raise Exception(backups['error']['message']) + raise UserFacingException(backups['error']['message']) try: f = self.handler.win.query_choice(_("Choose a backup file:"), backups['backup']) except Exception: @@ -330,7 +330,7 @@ class DigitalBitbox_Client(): hid_reply = self.hid_send_encrypt(msg) self.handler.finished() if 'error' in hid_reply: - raise Exception(hid_reply['error']['message']) + raise UserFacingException(hid_reply['error']['message']) return True @@ -388,7 +388,7 @@ class DigitalBitbox_Client(): r = to_string(r, 'utf8') reply = json.loads(r) except Exception as e: - print_error('Exception caught ' + str(e)) + print_error('Exception caught ' + repr(e)) return reply @@ -405,7 +405,7 @@ class DigitalBitbox_Client(): if 'error' in reply: self.password = None except Exception as e: - print_error('Exception caught ' + str(e)) + print_error('Exception caught ' + repr(e)) return reply diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index a93b7a2b..140c8447 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -27,7 +27,7 @@ from electrum.plugin import BasePlugin, hook from electrum.i18n import _ from electrum.bitcoin import is_address, TYPE_SCRIPT -from electrum.util import bfh, versiontuple +from electrum.util import bfh, versiontuple, UserFacingException from electrum.transaction import opcodes, TxOutput, Transaction @@ -130,9 +130,9 @@ def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes: script = bfh(output.address) if not (script[0] == opcodes.OP_RETURN and script[1] == len(script) - 2 and script[1] <= 75): - raise Exception(_("Only OP_RETURN scripts, with one constant push, are supported.")) + raise UserFacingException(_("Only OP_RETURN scripts, with one constant push, are supported.")) if output.value != 0: - raise Exception(_("Amount for OP_RETURN output must be zero.")) + raise UserFacingException(_("Amount for OP_RETURN output must be zero.")) return script[2:] diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 2498f495..699ea9d7 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -2,7 +2,7 @@ from binascii import hexlify, unhexlify import traceback import sys -from electrum.util import bfh, bh2u, UserCancelled +from electrum.util import bfh, bh2u, UserCancelled, UserFacingException from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT from electrum.bip32 import deserialize_xpub from electrum import constants @@ -30,7 +30,7 @@ class KeepKey_KeyStore(Hardware_KeyStore): return self.plugin.get_client(self, force_pair) def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) + raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device)) def sign_message(self, sequence, message, password): client = self.get_client() @@ -50,7 +50,7 @@ class KeepKey_KeyStore(Hardware_KeyStore): pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) tx_hash = txin['prevout_hash'] if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) prev_tx[tx_hash] = txin['prev_tx'] for x_pubkey in x_pubkeys: if not is_xpubkey(x_pubkey): @@ -138,7 +138,7 @@ class KeepKeyPlugin(HW_PluginBase): if handler: handler.show_error(msg) else: - raise Exception(msg) + raise UserFacingException(msg) return None return client @@ -242,8 +242,8 @@ class KeepKeyPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) + raise UserFacingException(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index a2665e74..11ff152c 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -9,7 +9,7 @@ from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore from electrum.transaction import Transaction from electrum.wallet import Standard_Wallet -from electrum.util import print_error, bfh, bh2u, versiontuple +from electrum.util import print_error, bfh, bh2u, versiontuple, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase @@ -46,7 +46,7 @@ def test_pin_unlocked(func): return func(self, *args, **kwargs) except BTChipException as e: if e.sw == 0x6982: - raise Exception(_('Your Ledger is locked. Please unlock it.')) + raise UserFacingException(_('Your Ledger is locked. Please unlock it.')) else: raise return catch_exception @@ -92,9 +92,9 @@ class Ledger_Client(): #self.get_client() # prompt for the PIN before displaying the dialog if necessary #self.handler.show_message("Computing master public key") if xtype in ['p2wpkh', 'p2wsh'] and not self.supports_native_segwit(): - raise Exception(MSG_NEEDS_FW_UPDATE_SEGWIT) + raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): - raise Exception(MSG_NEEDS_FW_UPDATE_SEGWIT) + raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) splitPath = bip32_path.split('/') if splitPath[0] == 'm': splitPath = splitPath[1:] @@ -154,7 +154,7 @@ class Ledger_Client(): if not checkFirmware(firmwareInfo): self.dongleObject.dongle.close() - raise Exception(MSG_NEEDS_FW_UPDATE_GENERIC) + raise UserFacingException(MSG_NEEDS_FW_UPDATE_GENERIC) try: self.dongleObject.getOperationMode() except BTChipException as e: @@ -172,18 +172,18 @@ class Ledger_Client(): msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." confirmed, p, pin = self.password_dialog(msg) if not confirmed: - raise Exception('Aborted by user - please unplug the dongle and plug it again before retrying') + raise UserFacingException('Aborted by user - please unplug the dongle and plug it again before retrying') pin = pin.encode() self.dongleObject.verifyPin(pin) except BTChipException as e: if (e.sw == 0x6faa): - raise Exception("Dongle is temporarily locked - please unplug it and replug it again") + raise UserFacingException("Dongle is temporarily locked - please unplug it and replug it again") if ((e.sw & 0xFFF0) == 0x63c0): - raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") + raise UserFacingException("Invalid PIN - please unplug the dongle and plug it again before retrying") if e.sw == 0x6f00 and e.message == 'Invalid channel': # based on docs 0x6f00 might be a more general error, hence we also compare message to be sure - raise Exception("Invalid channel.\n" - "Please make sure that 'Browser support' is disabled on your device.") + raise UserFacingException("Invalid channel.\n" + "Please make sure that 'Browser support' is disabled on your device.") raise e def checkDevice(self): @@ -192,7 +192,7 @@ class Ledger_Client(): self.perform_hw1_preflight() except BTChipException as e: if (e.sw == 0x6d00 or e.sw == 0x6700): - raise Exception(_("Device not in Bitcoin mode")) from e + raise UserFacingException(_("Device not in Bitcoin mode")) from e raise e self.preflightDone = True @@ -238,7 +238,7 @@ class Ledger_KeyStore(Hardware_KeyStore): self.signing = False if clear_client: self.client = None - raise Exception(message) + raise UserFacingException(message) def set_and_unset_signing(func): """Function decorator to set and unset self.signing.""" @@ -258,7 +258,7 @@ class Ledger_KeyStore(Hardware_KeyStore): return address_path[2:] def decrypt_message(self, pubkey, message, password): - raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) + raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device)) @test_pin_unlocked @set_and_unset_signing @@ -357,7 +357,7 @@ class Ledger_KeyStore(Hardware_KeyStore): redeemScript = Transaction.get_preimage_script(txin) txin_prev_tx = txin.get('prev_tx') if txin_prev_tx is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) txin_prev_tx_raw = txin_prev_tx.raw if txin_prev_tx else None inputs.append([txin_prev_tx_raw, txin['prevout_n'], @@ -586,8 +586,8 @@ class LedgerPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) + raise UserFacingException(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) client.handler = self.create_handler(wizard) client.get_xpub("m/44'/0'", 'standard') # TODO replace by direct derivation once Nano S > 1.1 diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index 48f4e386..c5a1031c 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -2,7 +2,7 @@ from binascii import hexlify, unhexlify import traceback import sys -from electrum.util import bfh, bh2u, versiontuple, UserCancelled +from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT from electrum.bip32 import deserialize_xpub from electrum import constants @@ -31,7 +31,7 @@ class SafeTKeyStore(Hardware_KeyStore): return self.plugin.get_client(self, force_pair) def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) + raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device)) def sign_message(self, sequence, message, password): client = self.get_client() @@ -51,7 +51,7 @@ class SafeTKeyStore(Hardware_KeyStore): pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) tx_hash = txin['prevout_hash'] if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) prev_tx[tx_hash] = txin['prev_tx'] for x_pubkey in x_pubkeys: if not is_xpubkey(x_pubkey): @@ -137,7 +137,7 @@ class SafeTPlugin(HW_PluginBase): if handler: handler.show_error(msg) else: - raise Exception(msg) + raise UserFacingException(msg) return None return client @@ -253,8 +253,8 @@ class SafeTPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) + raise UserFacingException(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 91b5aab3..a9ea1328 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -2,7 +2,7 @@ from binascii import hexlify, unhexlify import traceback import sys -from electrum.util import bfh, bh2u, versiontuple, UserCancelled +from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT from electrum.bip32 import deserialize_xpub from electrum import constants @@ -32,7 +32,7 @@ class TrezorKeyStore(Hardware_KeyStore): return self.plugin.get_client(self, force_pair) def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) + raise UserFacingException(_('Encryption and decryption are not implemented by {}').format(self.device)) def sign_message(self, sequence, message, password): client = self.get_client() @@ -52,7 +52,7 @@ class TrezorKeyStore(Hardware_KeyStore): pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) tx_hash = txin['prevout_hash'] if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) + raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) prev_tx[tx_hash] = txin['prev_tx'] for x_pubkey in x_pubkeys: if not is_xpubkey(x_pubkey): @@ -139,7 +139,7 @@ class TrezorPlugin(HW_PluginBase): if handler: handler.show_error(msg) else: - raise Exception(msg) + raise UserFacingException(msg) return None return client @@ -265,8 +265,8 @@ class TrezorPlugin(HW_PluginBase): device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) if client is None: - raise Exception(_('Failed to create a client for this device.') + '\n' + - _('Make sure it is in the correct state.')) + raise UserFacingException(_('Failed to create a client for this device.') + '\n' + + _('Make sure it is in the correct state.')) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: diff --git a/electrum/util.py b/electrum/util.py index 064659f7..fec507eb 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -119,6 +119,10 @@ class WalletFileException(Exception): pass class BitcoinException(Exception): pass +class UserFacingException(Exception): + """Exception that contains information intended to be shown to the user.""" + + # Throw this exception to unwind the stack like when an error occurs. # However unlike other exceptions the user won't be informed. class UserCancelled(Exception): From 2b8d801b366a12a6eb84f49ce2ca23b772d9d045 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 9 Nov 2018 16:33:29 +0100 Subject: [PATCH 107/301] if possible, batch new transaction with existing rbf transaction --- electrum/address_synchronizer.py | 6 +++ electrum/coinchooser.py | 8 ++-- electrum/gui/qt/transaction_dialog.py | 2 +- electrum/transaction.py | 13 ++++++- electrum/wallet.py | 55 ++++++++++++++++----------- 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index f74bb701..49af8067 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -484,6 +484,12 @@ class AddressSynchronizer(PrintError): self.threadlocal_cache.local_height = orig_val return f + def get_unconfirmed_tx(self): + for tx_hash, tx_mined_status, delta, balance in self.get_history(): + if tx_mined_status.conf <= 0 and delta < 0: + tx = self.transactions.get(tx_hash) + return tx + @with_local_height_cached def get_history(self, domain=None): # get domain diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index 29cfc719..ae6b639f 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -187,7 +187,7 @@ class CoinChooserBase(PrintError): self.print_error('not keeping dust', dust) return change - def make_tx(self, coins, outputs, change_addrs, fee_estimator, + def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator, dust_threshold): """Select unspent coins to spend to pay outputs. If the change is greater than dust_threshold (after adding the change output to @@ -202,7 +202,9 @@ class CoinChooserBase(PrintError): self.p = PRNG(''.join(sorted(utxos))) # Copy the outputs so when adding change we don't modify "outputs" - tx = Transaction.from_io([], outputs[:]) + tx = Transaction.from_io(inputs[:], outputs[:]) + v = tx.input_value() + # Weight of the transaction with no inputs and no change # Note: this will use legacy tx serialization as the need for "segwit" # would be detected from inputs. The only side effect should be that the @@ -230,7 +232,7 @@ class CoinChooserBase(PrintError): def sufficient_funds(buckets): '''Given a list of buckets, return True if it has enough value to pay for the transaction''' - total_input = sum(bucket.value for bucket in buckets) + total_input = v + sum(bucket.value for bucket in buckets) total_weight = get_tx_weight(buckets) return total_input >= spent_amount + fee_estimator_w(total_weight) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index b38430f1..84a9e701 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -87,7 +87,7 @@ class TxDialog(QDialog, MessageBoxMixin): # if the wallet can populate the inputs with more info, do it now. # as a result, e.g. we might learn an imported address tx is segwit, # in which case it's ok to display txid - self.wallet.add_input_info_to_all_inputs(tx) + tx.add_inputs_info(self.wallet) self.setMinimumWidth(950) self.setWindowTitle(_("Transaction")) diff --git a/electrum/transaction.py b/electrum/transaction.py index d91de2b5..ba477c92 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -763,6 +763,17 @@ class Transaction: txin['witness'] = None # force re-serialization self.raw = None + def add_inputs_info(self, wallet): + if self.is_complete(): + return + for txin in self.inputs(): + wallet.add_input_info(txin) + + def remove_signatures(self): + for txin in self.inputs(): + txin['signatures'] = [None] * len(txin['signatures']) + assert not self.is_complete() + def deserialize(self, force_full_parse=False): if self.raw is None: return @@ -1199,8 +1210,6 @@ class Transaction: return s, r def is_complete(self): - if not self.is_partial_originally: - return True s, r = self.signature_count() return r == s diff --git a/electrum/wallet.py b/electrum/wallet.py index 70b4b1c9..00b40610 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -536,7 +536,7 @@ class Abstract_Wallet(AddressSynchronizer): def dust_threshold(self): return dust_threshold(self.network) - def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None, + def make_unsigned_transaction(self, coins, outputs, config, fixed_fee=None, change_addr=None, is_sweep=False): # check outputs i_max = None @@ -550,13 +550,13 @@ class Abstract_Wallet(AddressSynchronizer): i_max = i # Avoid index-out-of-range with inputs[0] below - if not inputs: + if not coins: raise NotEnoughFunds() if fixed_fee is None and config.fee_per_kb() is None: raise NoDynamicFeeEstimates() - for item in inputs: + for item in coins: self.add_input_info(item) # change address @@ -591,19 +591,34 @@ class Abstract_Wallet(AddressSynchronizer): # Let the coin chooser select the coins to spend max_change = self.max_change_outputs if self.multiple_change else 1 coin_chooser = coinchooser.get_coin_chooser(config) - tx = coin_chooser.make_tx(inputs, outputs, change_addrs[:max_change], + # If there is an unconfirmed RBF tx, merge with it + base_tx = self.get_unconfirmed_tx() + if base_tx and not base_tx.is_final(): + base_tx = Transaction(base_tx.serialize()) + base_tx.deserialize(force_full_parse=True) + base_tx.remove_signatures() + base_tx.add_inputs_info(self) + base_fee = base_tx.get_fee() + fee_per_byte = Decimal(base_fee) / base_tx.estimated_size() + fee_estimator = lambda size: base_fee + round(fee_per_byte * size) + txi = base_tx.inputs() + txo = list(filter(lambda x: not self.is_change(x[1]), base_tx.outputs())) + else: + txi = [] + txo = [] + tx = coin_chooser.make_tx(coins, txi, outputs[:] + txo, change_addrs[:max_change], fee_estimator, self.dust_threshold()) else: # FIXME?? this might spend inputs with negative effective value... - sendable = sum(map(lambda x:x['value'], inputs)) + sendable = sum(map(lambda x:x['value'], coins)) outputs[i_max] = outputs[i_max]._replace(value=0) - tx = Transaction.from_io(inputs, outputs[:]) + tx = Transaction.from_io(coins, outputs[:]) fee = fee_estimator(tx.estimated_size()) amount = sendable - tx.output_value() - fee if amount < 0: raise NotEnoughFunds() outputs[i_max] = outputs[i_max]._replace(value=amount) - tx = Transaction.from_io(inputs, outputs[:]) + tx = Transaction.from_io(coins, outputs[:]) # Timelock tx to current height. tx.locktime = self.get_local_height() @@ -679,11 +694,10 @@ class Abstract_Wallet(AddressSynchronizer): raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final')) tx = Transaction(tx.serialize()) tx.deserialize(force_full_parse=True) # need to parse inputs - inputs = copy.deepcopy(tx.inputs()) - outputs = copy.deepcopy(tx.outputs()) - for txin in inputs: - txin['signatures'] = [None] * len(txin['signatures']) - self.add_input_info(txin) + tx.remove_signatures() + tx.add_inputs_info(self) + inputs = tx.inputs() + outputs = tx.outputs() # use own outputs s = list(filter(lambda x: self.is_mine(x[1]), outputs)) # ... unless there is none @@ -738,26 +752,21 @@ class Abstract_Wallet(AddressSynchronizer): def add_input_info(self, txin): address = self.get_txin_address(txin) if self.is_mine(address): + txin['address'] = address txin['type'] = self.get_txin_type(address) # segwit needs value to sign - if txin.get('value') is None and Transaction.is_input_value_needed(txin): + if txin.get('value') is None: received, spent = self.get_addr_io(address) item = received.get(txin['prevout_hash']+':%d'%txin['prevout_n']) - tx_height, value, is_cb = item - txin['value'] = value + if item: + txin['value'] = item[1] self.add_input_sig_info(txin, address) - def add_input_info_to_all_inputs(self, tx): - if tx.is_complete(): - return - for txin in tx.inputs(): - self.add_input_info(txin) - def can_sign(self, tx): if tx.is_complete(): return False # add info to inputs if we can; otherwise we might return a false negative: - self.add_input_info_to_all_inputs(tx) # though note that this is a side-effect + tx.add_inputs_info(self) for k in self.get_keystores(): if k.can_sign(tx): return True @@ -804,7 +813,7 @@ class Abstract_Wallet(AddressSynchronizer): def sign_transaction(self, tx, password): if self.is_watching_only(): return - self.add_input_info_to_all_inputs(tx) + tx.add_inputs_info(self) # hardware wallets require extra info if any([(isinstance(k, Hardware_KeyStore) and k.can_sign(tx)) for k in self.get_keystores()]): self.add_hw_info(tx) From f55db2f90b7ad0114deea178ac9d3fc2dee4fe1a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 9 Nov 2018 17:09:23 +0100 Subject: [PATCH 108/301] add batch_rbf option to Qt GUI --- electrum/gui/qt/main_window.py | 17 +++++++++++++++-- electrum/wallet.py | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index d4d28500..29921cd0 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2746,17 +2746,30 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): feebox_cb.stateChanged.connect(on_feebox) fee_widgets.append((feebox_cb, None)) + use_rbf = self.config.get('use_rbf', True) use_rbf_cb = QCheckBox(_('Use Replace-By-Fee')) - use_rbf_cb.setChecked(self.config.get('use_rbf', True)) + use_rbf_cb.setChecked(use_rbf) use_rbf_cb.setToolTip( _('If you check this box, your transactions will be marked as non-final,') + '\n' + \ _('and you will have the possibility, while they are unconfirmed, to replace them with transactions that pay higher fees.') + '\n' + \ _('Note that some merchants do not accept non-final transactions until they are confirmed.')) def on_use_rbf(x): - self.config.set_key('use_rbf', x == Qt.Checked) + self.config.set_key('use_rbf', bool(x)) + batch_rbf_cb.setEnabled(bool(x)) use_rbf_cb.stateChanged.connect(on_use_rbf) fee_widgets.append((use_rbf_cb, None)) + batch_rbf_cb = QCheckBox(_('Batch RBF transactions')) + batch_rbf_cb.setChecked(self.config.get('batch_rbf', False)) + batch_rbf_cb.setEnabled(use_rbf) + batch_rbf_cb.setToolTip( + _('If you check this box, your unconfirmed transactios will be consolidated in a single transaction') + '\n' + \ + _('This will save fees.')) + def on_batch_rbf(x): + self.config.set_key('batch_rbf', bool(x)) + batch_rbf_cb.stateChanged.connect(on_batch_rbf) + fee_widgets.append((batch_rbf_cb, None)) + msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\ + _('The following alias providers are available:') + '\n'\ + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\ diff --git a/electrum/wallet.py b/electrum/wallet.py index 00b40610..61f51cd1 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -593,7 +593,7 @@ class Abstract_Wallet(AddressSynchronizer): coin_chooser = coinchooser.get_coin_chooser(config) # If there is an unconfirmed RBF tx, merge with it base_tx = self.get_unconfirmed_tx() - if base_tx and not base_tx.is_final(): + if config.get('batch_rbf', False) and base_tx and not base_tx.is_final(): base_tx = Transaction(base_tx.serialize()) base_tx.deserialize(force_full_parse=True) base_tx.remove_signatures() From 71ac3bb305c6622f8224323c56cba6d3a88a1dc6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 9 Nov 2018 17:56:42 +0100 Subject: [PATCH 109/301] RBF batching: some fixes --- electrum/address_synchronizer.py | 10 ++-------- electrum/coinchooser.py | 9 +++++---- electrum/gui/qt/main_window.py | 2 +- electrum/transaction.py | 6 +----- electrum/wallet.py | 29 ++++++++++++++++++++++++++--- 5 files changed, 35 insertions(+), 21 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 49af8067..b876daaa 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -25,7 +25,7 @@ import threading import asyncio import itertools from collections import defaultdict -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict from . import bitcoin from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY @@ -457,7 +457,7 @@ class AddressSynchronizer(PrintError): self.spent_outpoints = defaultdict(dict) self.history = {} self.verified_tx = {} - self.transactions = {} + self.transactions = {} # type: Dict[str, Transaction] self.save_transactions() def get_txpos(self, tx_hash): @@ -484,12 +484,6 @@ class AddressSynchronizer(PrintError): self.threadlocal_cache.local_height = orig_val return f - def get_unconfirmed_tx(self): - for tx_hash, tx_mined_status, delta, balance in self.get_history(): - if tx_mined_status.conf <= 0 and delta < 0: - tx = self.transactions.get(tx_hash) - return tx - @with_local_height_cached def get_history(self, domain=None): # get domain diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index ae6b639f..9c64d68b 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -84,8 +84,8 @@ def strip_unneeded(bkts, sufficient_funds): for i in range(len(bkts)): if not sufficient_funds(bkts[i + 1:]): return bkts[i:] - # Shouldn't get here - return bkts + # none of the buckets are needed + return [] class CoinChooserBase(PrintError): @@ -203,12 +203,13 @@ class CoinChooserBase(PrintError): # Copy the outputs so when adding change we don't modify "outputs" tx = Transaction.from_io(inputs[:], outputs[:]) - v = tx.input_value() + input_value = tx.input_value() # Weight of the transaction with no inputs and no change # Note: this will use legacy tx serialization as the need for "segwit" # would be detected from inputs. The only side effect should be that the # marker and flag are excluded, which is compensated in get_tx_weight() + # FIXME calculation will be off by this (2 wu) in case of RBF batching base_weight = tx.estimated_weight() spent_amount = tx.output_value() @@ -232,7 +233,7 @@ class CoinChooserBase(PrintError): def sufficient_funds(buckets): '''Given a list of buckets, return True if it has enough value to pay for the transaction''' - total_input = v + sum(bucket.value for bucket in buckets) + total_input = input_value + sum(bucket.value for bucket in buckets) total_weight = get_tx_weight(buckets) return total_input >= spent_amount + fee_estimator_w(total_weight) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 29921cd0..d900fe65 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2763,7 +2763,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): batch_rbf_cb.setChecked(self.config.get('batch_rbf', False)) batch_rbf_cb.setEnabled(use_rbf) batch_rbf_cb.setToolTip( - _('If you check this box, your unconfirmed transactios will be consolidated in a single transaction') + '\n' + \ + _('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' + \ _('This will save fees.')) def on_batch_rbf(x): self.config.set_key('batch_rbf', bool(x)) diff --git a/electrum/transaction.py b/electrum/transaction.py index ba477c92..12e3f3f6 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -864,7 +864,7 @@ class Transaction: @classmethod def serialize_witness(self, txin, estimate_size=False): _type = txin['type'] - if not self.is_segwit_input(txin) and not self.is_input_value_needed(txin): + if not self.is_segwit_input(txin) and not txin['type'] == 'address': return '00' if _type == 'coinbase': return txin['witness'] @@ -902,10 +902,6 @@ class Transaction: def is_segwit_inputtype(cls, txin_type): return txin_type in ('p2wpkh', 'p2wpkh-p2sh', 'p2wsh', 'p2wsh-p2sh') - @classmethod - def is_input_value_needed(cls, txin): - return cls.is_segwit_input(txin) or txin['type'] == 'address' - @classmethod def guess_txintype_from_address(cls, addr): # It's not possible to tell the script type in general diff --git a/electrum/wallet.py b/electrum/wallet.py index 61f51cd1..bd353981 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -536,6 +536,29 @@ class Abstract_Wallet(AddressSynchronizer): def dust_threshold(self): return dust_threshold(self.network) + def get_unconfirmed_base_tx_for_batching(self) -> Optional[Transaction]: + candidate = None + for tx_hash, tx_mined_status, delta, balance in self.get_history(): + # tx should not be mined yet + if tx_mined_status.conf > 0: continue + # tx should be "outgoing" from wallet + if delta >= 0: continue + tx = self.transactions.get(tx_hash) + if not tx: continue + # is_mine outputs should not be spent yet + # to avoid cancelling our own dependent transactions + for output_idx, o in enumerate(tx.outputs()): + if self.is_mine(o.address) and self.spent_outpoints[tx.txid()].get(output_idx): + continue + # prefer txns already in mempool (vs local) + if tx_mined_status.height == TX_HEIGHT_LOCAL: + candidate = tx + continue + # tx must have opted-in for RBF + if tx.is_final(): continue + return tx + return candidate + def make_unsigned_transaction(self, coins, outputs, config, fixed_fee=None, change_addr=None, is_sweep=False): # check outputs @@ -592,8 +615,8 @@ class Abstract_Wallet(AddressSynchronizer): max_change = self.max_change_outputs if self.multiple_change else 1 coin_chooser = coinchooser.get_coin_chooser(config) # If there is an unconfirmed RBF tx, merge with it - base_tx = self.get_unconfirmed_tx() - if config.get('batch_rbf', False) and base_tx and not base_tx.is_final(): + base_tx = self.get_unconfirmed_base_tx_for_batching() + if config.get('batch_rbf', False) and base_tx: base_tx = Transaction(base_tx.serialize()) base_tx.deserialize(force_full_parse=True) base_tx.remove_signatures() @@ -602,7 +625,7 @@ class Abstract_Wallet(AddressSynchronizer): fee_per_byte = Decimal(base_fee) / base_tx.estimated_size() fee_estimator = lambda size: base_fee + round(fee_per_byte * size) txi = base_tx.inputs() - txo = list(filter(lambda x: not self.is_change(x[1]), base_tx.outputs())) + txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs())) else: txi = [] txo = [] From 436f6a4870df4411c69dbca9f2bdacd5f63ba514 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 9 Nov 2018 18:48:12 +0100 Subject: [PATCH 110/301] qt history export: include fiat value in csv --- electrum/gui/qt/history_list.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index d12903c7..b6e2e185 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -414,26 +414,29 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): if not filename: return try: - self.do_export_history(self.wallet, filename, csv_button.isChecked()) + self.do_export_history(filename, csv_button.isChecked()) except (IOError, os.error) as reason: export_error_label = _("Electrum was unable to produce a transaction export.") self.parent.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history")) return self.parent.show_message(_("Your wallet history has been successfully exported.")) - def do_export_history(self, wallet, fileName, is_csv): + def do_export_history(self, file_name, is_csv): history = self.transactions lines = [] - for item in history: - if is_csv: - lines.append([item['txid'], item.get('label', ''), item['confirmations'], item['value'], item['date']]) - else: - lines.append(item) - with open(fileName, "w+", encoding='utf-8') as f: + if is_csv: + for item in history: + lines.append([item['txid'], + item.get('label', ''), + item['confirmations'], + item['value'], + item.get('fiat_value', ''), + item['date']]) + with open(file_name, "w+", encoding='utf-8') as f: if is_csv: import csv transaction = csv.writer(f, lineterminator='\n') - transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"]) + transaction.writerow(["transaction_hash", "label", "confirmations", "value", "fiat_value", "timestamp"]) for line in lines: transaction.writerow(line) else: From d905f0e55ef95e321f5e986dbdc62be50c629181 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 9 Nov 2018 19:15:46 +0100 Subject: [PATCH 111/301] RBF batching: for now, let user deal with fee problems (honour slider) --- electrum/wallet.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index bd353981..7d0dda2d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -572,10 +572,6 @@ class Abstract_Wallet(AddressSynchronizer): raise Exception("More than one output set to spend max") i_max = i - # Avoid index-out-of-range with inputs[0] below - if not coins: - raise NotEnoughFunds() - if fixed_fee is None and config.fee_per_kb() is None: raise NoDynamicFeeEstimates() @@ -621,9 +617,6 @@ class Abstract_Wallet(AddressSynchronizer): base_tx.deserialize(force_full_parse=True) base_tx.remove_signatures() base_tx.add_inputs_info(self) - base_fee = base_tx.get_fee() - fee_per_byte = Decimal(base_fee) / base_tx.estimated_size() - fee_estimator = lambda size: base_fee + round(fee_per_byte * size) txi = base_tx.inputs() txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs())) else: From 2ab8234e9c08b5d35f6bf8d07ba587975cf86e53 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 9 Nov 2018 20:04:06 +0100 Subject: [PATCH 112/301] RBF batching: smarter fee handling --- electrum/gui/qt/main_window.py | 3 ++- electrum/wallet.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index d900fe65..fa71b5a9 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1203,7 +1203,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): _('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' + _('At most 100 satoshis might be lost due to this rounding.') + ' ' + _("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' + - _('Also, dust is not kept as change, but added to the fee.')) + _('Also, dust is not kept as change, but added to the fee.') + '\n' + + _('Also, when batching RBF transactions, BIP 125 imposes a lower bound on the fee.')) QMessageBox.information(self, 'Fee rounding', text) self.feerounding_icon = QPushButton(QIcon(':icons/info.png'), '') diff --git a/electrum/wallet.py b/electrum/wallet.py index 7d0dda2d..ca9af202 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -550,6 +550,9 @@ class Abstract_Wallet(AddressSynchronizer): for output_idx, o in enumerate(tx.outputs()): if self.is_mine(o.address) and self.spent_outpoints[tx.txid()].get(output_idx): continue + # all inputs should be is_mine + if not all([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()]): + continue # prefer txns already in mempool (vs local) if tx_mined_status.height == TX_HEIGHT_LOCAL: candidate = tx @@ -613,10 +616,18 @@ class Abstract_Wallet(AddressSynchronizer): # If there is an unconfirmed RBF tx, merge with it base_tx = self.get_unconfirmed_base_tx_for_batching() if config.get('batch_rbf', False) and base_tx: + is_local = self.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL base_tx = Transaction(base_tx.serialize()) base_tx.deserialize(force_full_parse=True) base_tx.remove_signatures() base_tx.add_inputs_info(self) + base_tx_fee = base_tx.get_fee() + relayfeerate = self.relayfee() / 1000 + original_fee_estimator = fee_estimator + def fee_estimator(size: int) -> int: + lower_bound = base_tx_fee + round(size * relayfeerate) + lower_bound = lower_bound if not is_local else 0 + return max(lower_bound, original_fee_estimator(size)) txi = base_tx.inputs() txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs())) else: From a6a003a345e2fc2e8de709190a4a6d0ecd531c64 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 9 Nov 2018 22:47:41 +0100 Subject: [PATCH 113/301] RBF batching: fix logic bug --- electrum/wallet.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index ca9af202..a60eedce 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -547,9 +547,10 @@ class Abstract_Wallet(AddressSynchronizer): if not tx: continue # is_mine outputs should not be spent yet # to avoid cancelling our own dependent transactions - for output_idx, o in enumerate(tx.outputs()): - if self.is_mine(o.address) and self.spent_outpoints[tx.txid()].get(output_idx): - continue + txid = tx.txid() + if any([self.is_mine(o.address) and self.spent_outpoints[txid].get(output_idx) + for output_idx, o in enumerate(tx.outputs())]): + continue # all inputs should be is_mine if not all([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()]): continue From aceb022f9df7c778856debecc53eaeaa49dfbfaa Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 10 Nov 2018 13:30:34 +0100 Subject: [PATCH 114/301] crypto: more type annotations --- electrum/crypto.py | 52 ++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/electrum/crypto.py b/electrum/crypto.py index 417a433b..345fbd85 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -44,13 +44,13 @@ class InvalidPadding(Exception): pass -def append_PKCS7_padding(data): +def append_PKCS7_padding(data: bytes) -> bytes: assert_bytes(data) padlen = 16 - (len(data) % 16) return data + bytes([padlen]) * padlen -def strip_PKCS7_padding(data): +def strip_PKCS7_padding(data: bytes) -> bytes: assert_bytes(data) if len(data) % 16 != 0 or len(data) == 0: raise InvalidPadding("invalid length") @@ -63,7 +63,7 @@ def strip_PKCS7_padding(data): return data[0:-padlen] -def aes_encrypt_with_iv(key, iv, data): +def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: assert_bytes(key, iv, data) data = append_PKCS7_padding(data) if AES: @@ -75,7 +75,7 @@ def aes_encrypt_with_iv(key, iv, data): return e -def aes_decrypt_with_iv(key, iv, data): +def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: assert_bytes(key, iv, data) if AES: cipher = AES.new(key, AES.MODE_CBC, iv) @@ -90,36 +90,38 @@ def aes_decrypt_with_iv(key, iv, data): raise InvalidPassword() -def EncodeAES(secret, s): - assert_bytes(s) +def EncodeAES(secret: bytes, msg: bytes) -> bytes: + """Returns base64 encoded ciphertext.""" + assert_bytes(msg) iv = bytes(os.urandom(16)) - ct = aes_encrypt_with_iv(secret, iv, s) + ct = aes_encrypt_with_iv(secret, iv, msg) e = iv + ct return base64.b64encode(e) -def DecodeAES(secret, e): - e = bytes(base64.b64decode(e)) + +def DecodeAES(secret: bytes, ciphertext_b64: Union[bytes, str]) -> bytes: + e = bytes(base64.b64decode(ciphertext_b64)) iv, e = e[:16], e[16:] s = aes_decrypt_with_iv(secret, iv, e) return s -def pw_encode(s, password): - if password: - secret = sha256d(password) - return EncodeAES(secret, to_bytes(s, "utf8")).decode('utf8') - else: - return s -def pw_decode(s, password): - if password is not None: - secret = sha256d(password) - try: - d = to_string(DecodeAES(secret, s), "utf8") - except Exception: - raise InvalidPassword() - return d - else: - return s +def pw_encode(data: str, password: Union[bytes, str]) -> str: + if not password: + return data + secret = sha256d(password) + return EncodeAES(secret, to_bytes(data, "utf8")).decode('utf8') + + +def pw_decode(data: str, password: Union[bytes, str]) -> str: + if password is None: + return data + secret = sha256d(password) + try: + d = to_string(DecodeAES(secret, data), "utf8") + except Exception: + raise InvalidPassword() + return d def sha256(x: Union[bytes, str]) -> bytes: From 48b0de78719dd349a096c037870ee801e9bd9ac0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 10 Nov 2018 15:30:41 +0100 Subject: [PATCH 115/301] keystore: stronger pbkdf for encryption --- electrum/crypto.py | 99 ++++++++++++++++--- electrum/keystore.py | 58 +++++++---- .../plugins/digitalbitbox/digitalbitbox.py | 8 +- electrum/tests/test_bitcoin.py | 27 ++--- 4 files changed, 140 insertions(+), 52 deletions(-) diff --git a/electrum/crypto.py b/electrum/crypto.py index 345fbd85..752dfec8 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -32,6 +32,7 @@ from typing import Union import pyaes from .util import assert_bytes, InvalidPassword, to_bytes, to_string +from .i18n import _ try: @@ -90,37 +91,103 @@ def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: raise InvalidPassword() -def EncodeAES(secret: bytes, msg: bytes) -> bytes: +def EncodeAES_base64(secret: bytes, msg: bytes) -> bytes: """Returns base64 encoded ciphertext.""" - assert_bytes(msg) - iv = bytes(os.urandom(16)) - ct = aes_encrypt_with_iv(secret, iv, msg) - e = iv + ct + e = EncodeAES_bytes(secret, msg) return base64.b64encode(e) -def DecodeAES(secret: bytes, ciphertext_b64: Union[bytes, str]) -> bytes: - e = bytes(base64.b64decode(ciphertext_b64)) - iv, e = e[:16], e[16:] +def EncodeAES_bytes(secret: bytes, msg: bytes) -> bytes: + assert_bytes(msg) + iv = bytes(os.urandom(16)) + ct = aes_encrypt_with_iv(secret, iv, msg) + return iv + ct + + +def DecodeAES_base64(secret: bytes, ciphertext_b64: Union[bytes, str]) -> bytes: + ciphertext = bytes(base64.b64decode(ciphertext_b64)) + return DecodeAES_bytes(secret, ciphertext) + + +def DecodeAES_bytes(secret: bytes, ciphertext: bytes) -> bytes: + assert_bytes(ciphertext) + iv, e = ciphertext[:16], ciphertext[16:] s = aes_decrypt_with_iv(secret, iv, e) return s -def pw_encode(data: str, password: Union[bytes, str]) -> str: +PW_HASH_VERSION_LATEST = 2 +KNOWN_PW_HASH_VERSIONS = (1, 2) +assert PW_HASH_VERSION_LATEST in KNOWN_PW_HASH_VERSIONS + + +class UnexpectedPasswordHashVersion(InvalidPassword): + def __init__(self, version): + self.version = version + + def __str__(self): + return "{unexpected}: {version}\n{please_update}".format( + unexpected=_("Unexpected password hash version"), + version=self.version, + please_update=_('You are most likely using an outdated version of Electrum. Please update.')) + + +def _hash_password(password: Union[bytes, str], *, version: int, salt: bytes) -> bytes: + pw = to_bytes(password, 'utf8') + if version == 1: + return sha256d(pw) + elif version == 2: + if not isinstance(salt, bytes) or len(salt) < 16: + raise Exception('too weak salt', salt) + return hashlib.pbkdf2_hmac(hash_name='sha256', + password=pw, + salt=b'ELECTRUM_PW_HASH_V2'+salt, + iterations=50_000) + else: + assert version not in KNOWN_PW_HASH_VERSIONS + raise UnexpectedPasswordHashVersion(version) + + +def pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> str: if not password: return data - secret = sha256d(password) - return EncodeAES(secret, to_bytes(data, "utf8")).decode('utf8') + if version not in KNOWN_PW_HASH_VERSIONS: + raise UnexpectedPasswordHashVersion(version) + # derive key from password + if version == 1: + salt = b'' + elif version == 2: + salt = bytes(os.urandom(16)) + else: + assert False, version + secret = _hash_password(password, version=version, salt=salt) + # encrypt given data + e = EncodeAES_bytes(secret, to_bytes(data, "utf8")) + # return base64(salt + encrypted data) + ciphertext = salt + e + ciphertext_b64 = base64.b64encode(ciphertext) + return ciphertext_b64.decode('utf8') -def pw_decode(data: str, password: Union[bytes, str]) -> str: +def pw_decode(data: str, password: Union[bytes, str, None], *, version: int) -> str: if password is None: return data - secret = sha256d(password) + if version not in KNOWN_PW_HASH_VERSIONS: + raise UnexpectedPasswordHashVersion(version) + data_bytes = bytes(base64.b64decode(data)) + # derive key from password + if version == 1: + salt = b'' + elif version == 2: + salt, data_bytes = data_bytes[:16], data_bytes[16:] + else: + assert False, version + secret = _hash_password(password, version=version, salt=salt) + # decrypt given data try: - d = to_string(DecodeAES(secret, data), "utf8") - except Exception: - raise InvalidPassword() + d = to_string(DecodeAES_bytes(secret, data_bytes), "utf8") + except Exception as e: + raise InvalidPassword() from e return d diff --git a/electrum/keystore.py b/electrum/keystore.py index a942d075..e0e21fa6 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -35,7 +35,7 @@ from .bip32 import (bip32_public_derivation, deserialize_xpub, CKD_pub, bip32_private_key, bip32_derivation, BIP32_PRIME, is_xpub, is_xprv) from .ecc import string_to_number, number_to_string -from .crypto import pw_decode, pw_encode, sha256d +from .crypto import (pw_decode, pw_encode, sha256d, PW_HASH_VERSION_LATEST) from .util import (PrintError, InvalidPassword, hfu, WalletFileException, BitcoinException, bh2u, bfh, print_error, inv_dict) from .mnemonic import Mnemonic, load_wordlist @@ -92,8 +92,9 @@ class KeyStore(PrintError): class Software_KeyStore(KeyStore): - def __init__(self): + def __init__(self, d): KeyStore.__init__(self) + self.pw_hash_version = d.get('pw_hash_version', 1) def may_have_password(self): return not self.is_watching_only() @@ -122,6 +123,12 @@ class Software_KeyStore(KeyStore): if keypairs: tx.sign(keypairs) + def update_password(self, old_password, new_password): + raise NotImplementedError() # implemented by subclasses + + def check_password(self, password): + raise NotImplementedError() # implemented by subclasses + class Imported_KeyStore(Software_KeyStore): # keystore for imported private keys @@ -129,7 +136,7 @@ class Imported_KeyStore(Software_KeyStore): type = 'imported' def __init__(self, d): - Software_KeyStore.__init__(self) + Software_KeyStore.__init__(self, d) self.keypairs = d.get('keypairs', {}) def is_deterministic(self): @@ -142,6 +149,7 @@ class Imported_KeyStore(Software_KeyStore): return { 'type': self.type, 'keypairs': self.keypairs, + 'pw_hash_version': self.pw_hash_version, } def can_import(self): @@ -161,14 +169,14 @@ class Imported_KeyStore(Software_KeyStore): # there will only be one pubkey-privkey pair for it in self.keypairs, # and the privkey will encode a txin_type but that txin_type cannot be trusted. # Removing keys complicates this further. - self.keypairs[pubkey] = pw_encode(serialized_privkey, password) + self.keypairs[pubkey] = pw_encode(serialized_privkey, password, version=self.pw_hash_version) return txin_type, pubkey def delete_imported_key(self, key): self.keypairs.pop(key) def get_private_key(self, pubkey, password): - sec = pw_decode(self.keypairs[pubkey], password) + sec = pw_decode(self.keypairs[pubkey], password, version=self.pw_hash_version) txin_type, privkey, compressed = deserialize_privkey(sec) # this checks the password if pubkey != ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed): @@ -189,16 +197,17 @@ class Imported_KeyStore(Software_KeyStore): if new_password == '': new_password = None for k, v in self.keypairs.items(): - b = pw_decode(v, old_password) - c = pw_encode(b, new_password) + b = pw_decode(v, old_password, version=self.pw_hash_version) + c = pw_encode(b, new_password, version=PW_HASH_VERSION_LATEST) self.keypairs[k] = c + self.pw_hash_version = PW_HASH_VERSION_LATEST class Deterministic_KeyStore(Software_KeyStore): def __init__(self, d): - Software_KeyStore.__init__(self) + Software_KeyStore.__init__(self, d) self.seed = d.get('seed', '') self.passphrase = d.get('passphrase', '') @@ -206,12 +215,14 @@ class Deterministic_KeyStore(Software_KeyStore): return True def dump(self): - d = {} + d = { + 'type': self.type, + 'pw_hash_version': self.pw_hash_version, + } if self.seed: d['seed'] = self.seed if self.passphrase: d['passphrase'] = self.passphrase - d['type'] = self.type return d def has_seed(self): @@ -226,10 +237,13 @@ class Deterministic_KeyStore(Software_KeyStore): self.seed = self.format_seed(seed) def get_seed(self, password): - return pw_decode(self.seed, password) + return pw_decode(self.seed, password, version=self.pw_hash_version) def get_passphrase(self, password): - return pw_decode(self.passphrase, password) if self.passphrase else '' + if self.passphrase: + return pw_decode(self.passphrase, password, version=self.pw_hash_version) + else: + return '' class Xpub: @@ -312,10 +326,10 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub): return d def get_master_private_key(self, password): - return pw_decode(self.xprv, password) + return pw_decode(self.xprv, password, version=self.pw_hash_version) def check_password(self, password): - xprv = pw_decode(self.xprv, password) + xprv = pw_decode(self.xprv, password, version=self.pw_hash_version) if deserialize_xprv(xprv)[4] != deserialize_xpub(self.xpub)[4]: raise InvalidPassword() @@ -325,13 +339,14 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub): new_password = None if self.has_seed(): decoded = self.get_seed(old_password) - self.seed = pw_encode(decoded, new_password) + self.seed = pw_encode(decoded, new_password, version=PW_HASH_VERSION_LATEST) if self.passphrase: decoded = self.get_passphrase(old_password) - self.passphrase = pw_encode(decoded, new_password) + self.passphrase = pw_encode(decoded, new_password, version=PW_HASH_VERSION_LATEST) if self.xprv is not None: - b = pw_decode(self.xprv, old_password) - self.xprv = pw_encode(b, new_password) + b = pw_decode(self.xprv, old_password, version=self.pw_hash_version) + self.xprv = pw_encode(b, new_password, version=PW_HASH_VERSION_LATEST) + self.pw_hash_version = PW_HASH_VERSION_LATEST def is_watching_only(self): return self.xprv is None @@ -362,7 +377,7 @@ class Old_KeyStore(Deterministic_KeyStore): self.mpk = d.get('mpk') def get_hex_seed(self, password): - return pw_decode(self.seed, password).encode('utf8') + return pw_decode(self.seed, password, version=self.pw_hash_version).encode('utf8') def dump(self): d = Deterministic_KeyStore.dump(self) @@ -484,8 +499,9 @@ class Old_KeyStore(Deterministic_KeyStore): if new_password == '': new_password = None if self.has_seed(): - decoded = pw_decode(self.seed, old_password) - self.seed = pw_encode(decoded, new_password) + decoded = pw_decode(self.seed, old_password, version=self.pw_hash_version) + self.seed = pw_encode(decoded, new_password, version=PW_HASH_VERSION_LATEST) + self.pw_hash_version = PW_HASH_VERSION_LATEST diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 25e69e27..dd93f77f 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -4,7 +4,7 @@ # try: - from electrum.crypto import sha256d, EncodeAES, DecodeAES + from electrum.crypto import sha256d, EncodeAES_base64, DecodeAES_base64 from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, is_address) from electrum.bip32 import serialize_xpub, deserialize_xpub @@ -396,10 +396,10 @@ class DigitalBitbox_Client(): reply = "" try: secret = sha256d(self.password) - msg = EncodeAES(secret, msg) + msg = EncodeAES_base64(secret, msg) reply = self.hid_send_plain(msg) if 'ciphertext' in reply: - reply = DecodeAES(secret, ''.join(reply["ciphertext"])) + reply = DecodeAES_base64(secret, ''.join(reply["ciphertext"])) reply = to_string(reply, 'utf8') reply = json.loads(reply) if 'error' in reply: @@ -716,7 +716,7 @@ class DigitalBitboxPlugin(HW_PluginBase): key_s = base64.b64decode(self.digitalbitbox_config['encryptionprivkey']) args = 'c=data&s=0&dt=0&uuid=%s&pl=%s' % ( self.digitalbitbox_config['comserverchannelid'], - EncodeAES(key_s, json.dumps(payload).encode('ascii')).decode('ascii'), + EncodeAES_base64(key_s, json.dumps(payload).encode('ascii')).decode('ascii'), ) try: requests.post(url, args) diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index e11cee4e..6217ed5b 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -11,11 +11,11 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key, from electrum.bip32 import (bip32_root, bip32_public_derivation, bip32_private_derivation, xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation, is_xpub, convert_bip32_path_to_list_of_uint32) -from electrum.crypto import sha256d +from electrum.crypto import sha256d, KNOWN_PW_HASH_VERSIONS from electrum import ecc, crypto, constants from electrum.ecc import number_to_string, string_to_number from electrum.transaction import opcodes -from electrum.util import bfh, bh2u +from electrum.util import bfh, bh2u, InvalidPassword from electrum.storage import WalletStorage from electrum.keystore import xtype_from_derivation @@ -219,23 +219,26 @@ class Test_bitcoin(SequentialTestCase): """Make sure AES is homomorphic.""" payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' password = u'secret' - enc = crypto.pw_encode(payload, password) - dec = crypto.pw_decode(enc, password) - self.assertEqual(dec, payload) + for version in KNOWN_PW_HASH_VERSIONS: + enc = crypto.pw_encode(payload, password, version=version) + dec = crypto.pw_decode(enc, password, version=version) + self.assertEqual(dec, payload) @needs_test_with_all_aes_implementations def test_aes_encode_without_password(self): """When not passed a password, pw_encode is noop on the payload.""" payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' - enc = crypto.pw_encode(payload, None) - self.assertEqual(payload, enc) + for version in KNOWN_PW_HASH_VERSIONS: + enc = crypto.pw_encode(payload, None, version=version) + self.assertEqual(payload, enc) @needs_test_with_all_aes_implementations def test_aes_deencode_without_password(self): """When not passed a password, pw_decode is noop on the payload.""" payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' - enc = crypto.pw_decode(payload, None) - self.assertEqual(payload, enc) + for version in KNOWN_PW_HASH_VERSIONS: + enc = crypto.pw_decode(payload, None, version=version) + self.assertEqual(payload, enc) @needs_test_with_all_aes_implementations def test_aes_decode_with_invalid_password(self): @@ -243,8 +246,10 @@ class Test_bitcoin(SequentialTestCase): payload = u"blah" password = u"uber secret" wrong_password = u"not the password" - enc = crypto.pw_encode(payload, password) - self.assertRaises(Exception, crypto.pw_decode, enc, wrong_password) + for version in KNOWN_PW_HASH_VERSIONS: + enc = crypto.pw_encode(payload, password, version=version) + with self.assertRaises(InvalidPassword): + crypto.pw_decode(enc, wrong_password, version=version) def test_sha256d(self): self.assertEqual(b'\x95MZI\xfdp\xd9\xb8\xbc\xdb5\xd2R&x)\x95\x7f~\xf7\xfalt\xf8\x84\x19\xbd\xc5\xe8"\t\xf4', From e04e8d236573d5ad5cd40bcca8c6d613b6ee31a4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 11 Nov 2018 23:55:34 +0100 Subject: [PATCH 116/301] plugins: when loading plugins, use newer importlib mechanism fixes #4842 --- electrum/plugin.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/electrum/plugin.py b/electrum/plugin.py index 46d4fca9..e2c58d3a 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -26,6 +26,7 @@ import traceback import sys import os import pkgutil +import importlib.util import time import threading from typing import NamedTuple, Any, Union, TYPE_CHECKING, Optional @@ -66,9 +67,16 @@ class Plugins(DaemonThread): def load_plugins(self): for loader, name, ispkg in pkgutil.iter_modules([self.pkgpath]): - mod = pkgutil.find_loader('electrum.plugins.' + name) - m = mod.load_module() - d = m.__dict__ + full_name = f'electrum.plugins.{name}' + spec = importlib.util.find_spec(full_name) + if spec is None: # pkgutil found it but importlib can't ?! + raise Exception(f"Error pre-loading {full_name}: no spec") + try: + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + except Exception as e: + raise Exception(f"Error pre-loading {full_name}: {repr(e)}") from e + d = module.__dict__ gui_good = self.gui_name in d.get('available_for', []) if not gui_good: continue @@ -95,16 +103,17 @@ class Plugins(DaemonThread): def load_plugin(self, name): if name in self.plugins: return self.plugins[name] - full_name = 'electrum.plugins.' + name + '.' + self.gui_name - loader = pkgutil.find_loader(full_name) - if not loader: + full_name = f'electrum.plugins.{name}.{self.gui_name}' + spec = importlib.util.find_spec(full_name) + if spec is None: raise RuntimeError("%s implementation for %s plugin not found" % (self.gui_name, name)) try: - p = loader.load_module() - plugin = p.Plugin(self, self.config, name) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + plugin = module.Plugin(self, self.config, name) except Exception as e: - raise Exception(f"Error loading {name} plugin: {e}") from e + raise Exception(f"Error loading {name} plugin: {repr(e)}") from e self.add_jobs(plugin.thread_jobs()) self.plugins[name] = plugin self.print_error("loaded", name) From e1b85327bed94f99fb239c6cb6bff672e392b6aa Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Nov 2018 00:37:03 +0100 Subject: [PATCH 117/301] transaction: clean-up multisig_script --- electrum/network.py | 2 +- electrum/transaction.py | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index 0000b25f..99dd7065 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -636,7 +636,7 @@ class Network(PrintError): await asyncio.wait_for(interface.ready, timeout) except BaseException as e: #traceback.print_exc() - self.print_error(server, "couldn't launch because", str(e), str(type(e))) + self.print_error(f"couldn't launch iface {server} -- {repr(e)}") await interface.close() return else: diff --git a/electrum/transaction.py b/electrum/transaction.py index 12e3f3f6..04e2519d 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -38,7 +38,7 @@ from .util import print_error, profiler, to_bytes, bh2u, bfh from .bitcoin import (TYPE_ADDRESS, TYPE_PUBKEY, TYPE_SCRIPT, hash_160, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, hash_encode, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, - op_push, int_to_hex, push_script, b58_address_to_hash160) + push_script, int_to_hex, push_script, b58_address_to_hash160) from .crypto import sha256d from .keystore import xpubkey_to_address, xpubkey_to_pubkey @@ -649,15 +649,12 @@ def deserialize(raw: str, force_full_parse=False) -> dict: # pay & redeem scripts - - def multisig_script(public_keys: Sequence[str], m: int) -> str: n = len(public_keys) - assert n <= 15 - assert m <= n - op_m = format(opcodes.OP_1 + m - 1, 'x') - op_n = format(opcodes.OP_1 + n - 1, 'x') - keylist = [op_push(len(k)//2) + k for k in public_keys] + assert 1 <= m <= n <= 15, f'm {m}, n {n}' + op_m = bh2u(bytes([opcodes.OP_1 - 1 + m])) + op_n = bh2u(bytes([opcodes.OP_1 - 1 + n])) + keylist = [push_script(k) for k in public_keys] return op_m + ''.join(keylist) + op_n + 'ae' From a266de6735c5be059f78ae42e23af3c07fe80c22 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 14 Nov 2018 13:16:08 +0100 Subject: [PATCH 118/301] PrintError: display verbosity filter --- electrum/interface.py | 1 + electrum/util.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index 1b8511dc..68ede755 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -138,6 +138,7 @@ def serialize_server(host: str, port: Union[str, int], protocol: str) -> str: class Interface(PrintError): + verbosity_filter = 'i' def __init__(self, network: 'Network', server: str, config_path, proxy: dict): self.ready = asyncio.Future() diff --git a/electrum/util.py b/electrum/util.py index fec507eb..30e71826 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -183,17 +183,23 @@ class PrintError(object): verbosity_filter = '' def diagnostic_name(self): - return self.__class__.__name__ + return '' + + def log_name(self): + msg = self.verbosity_filter or self.__class__.__name__ + d = self.diagnostic_name() + if d: msg += "][" + d + return "[%s]" % msg def print_error(self, *msg): if self.verbosity_filter in verbosity or verbosity == '*': - print_error("[%s]" % self.diagnostic_name(), *msg) + print_error(self.log_name(), *msg) def print_stderr(self, *msg): - print_stderr("[%s]" % self.diagnostic_name(), *msg) + print_stderr(self.log_name(), *msg) def print_msg(self, *msg): - print_msg("[%s]" % self.diagnostic_name(), *msg) + print_msg(self.log_name(), *msg) class ThreadJob(PrintError): """A job that is run periodically from a thread's main loop. run() is From e059867314ad8cdc8f29b33cfa179f2df77b8fc2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Nov 2018 16:04:43 +0100 Subject: [PATCH 119/301] paymentrequest: be explicit about only allowing "addresses" --- electrum/paymentrequest.py | 11 +++++++++-- electrum/transaction.py | 5 +++-- electrum/util.py | 11 ++++------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index b50c399b..071e7bb8 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -132,8 +132,12 @@ class PaymentRequest: self.details.ParseFromString(self.data.serialized_payment_details) self.outputs = [] for o in self.details.outputs: - addr = transaction.get_address_from_output_script(o.script)[1] - self.outputs.append(TxOutput(TYPE_ADDRESS, addr, o.amount)) + type_, addr = transaction.get_address_from_output_script(o.script) + if type_ != TYPE_ADDRESS: + # TODO maybe rm restriction but then get_requestor and get_id need changes + self.error = "only addresses are allowed as outputs" + return + self.outputs.append(TxOutput(type_, addr, o.amount)) self.memo = self.details.memo self.payment_url = self.details.payment_url @@ -195,6 +199,9 @@ class PaymentRequest: verify = pubkey0.verify(sigBytes, x509.PREFIX_RSA_SHA256 + hashBytes) elif paymntreq.pki_type == "x509+sha1": verify = pubkey0.hashAndVerify(sigBytes, msgBytes) + else: + self.error = f"ERROR: unknown pki_type {paymntreq.pki_type} in Payment Request" + return False if not verify: self.error = "ERROR: Invalid Signature for Payment Request Data" return False diff --git a/electrum/transaction.py b/electrum/transaction.py index 04e2519d..9222b528 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1030,9 +1030,10 @@ class Transaction: if outputs: self._outputs.sort(key = lambda o: (o.value, self.pay_script(o.type, o.address))) - def serialize_output(self, output: TxOutput) -> str: + @classmethod + def serialize_output(cls, output: TxOutput) -> str: s = int_to_hex(output.value, 8) - script = self.pay_script(output.type, output.address) + script = cls.pay_script(output.type, output.address) s += var_int(len(script)//2) s += script return s diff --git a/electrum/util.py b/electrum/util.py index 30e71826..d5ea9c28 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -444,8 +444,7 @@ def assert_str(*args): assert isinstance(x, str) - -def to_string(x, enc): +def to_string(x, enc) -> str: if isinstance(x, (bytes, bytearray)): return x.decode(enc) if isinstance(x, str): @@ -453,7 +452,8 @@ def to_string(x, enc): else: raise TypeError("Not a string or bytes like object") -def to_bytes(something, encoding='utf8'): + +def to_bytes(something, encoding='utf8') -> bytes: """ cast string to bytes() like object, but for python2 support it's bytearray copy """ @@ -471,16 +471,13 @@ bfh = bytes.fromhex hfu = binascii.hexlify -def bh2u(x): +def bh2u(x: bytes) -> str: """ str with hex representation of a bytes-like object >>> x = bytes((1, 2, 10)) >>> bh2u(x) '01020A' - - :param x: bytes - :rtype: str """ return hfu(x).decode('ascii') From e1c66488b1c5d81346d5bcd74d307b314964061b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Nov 2018 16:33:41 +0100 Subject: [PATCH 120/301] paymentrequest: don't show PaymentAck to user mainly because the main "merchant" using bip70 is bitpay, and they are failing all the PaymentAcks due to the tx is using RBF... no need to confuse users. follow-up 1686a97ece31aaa1d2c0e6c7042137a5e8a04943 --- 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 fa71b5a9..11dad7b6 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1667,7 +1667,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): coro = pr.send_payment_and_receive_paymentack(str(tx), refund_address) fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) ack_status, ack_msg = fut.result(timeout=20) - msg += f"\n\nPayment ACK: {ack_status}.\nAck message: {ack_msg}" + self.print_error(f"Payment ACK: {ack_status}. Ack message: {ack_msg}") return status, msg # Capture current TL window; override might be removed on return From 75e30ddc9dd97f11db19d609d0d1d4780dc3885a Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Wed, 14 Nov 2018 17:43:58 +0200 Subject: [PATCH 121/301] Show description (label) in TxDialog screen when opened from History (#4775) --- electrum/gui/qt/history_list.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index b6e2e185..fd73aaf6 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -297,10 +297,14 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): super(HistoryList, self).on_doubleclick(item, column) else: tx_hash = item.data(0, Qt.UserRole) - tx = self.wallet.transactions.get(tx_hash) - if not tx: - return - self.parent.show_transaction(tx) + self.show_transaction(tx_hash) + + def show_transaction(self, tx_hash): + tx = self.wallet.transactions.get(tx_hash) + if not tx: + return + label = self.wallet.get_label(tx_hash) or None # prefer 'None' if not defined (force tx dialog to hide Description field if missing) + self.parent.show_transaction(tx, label) def update_labels(self): root = self.invisibleRootItem() @@ -354,7 +358,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): for c in self.editable_columns: menu.addAction(_("Edit {}").format(self.headerItem().text(c)), lambda bound_c=c: self.editItem(item, bound_c)) - menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) + menu.addAction(_("Details"), lambda: self.show_transaction(tx_hash)) if is_unconfirmed and tx: # note: the current implementation of RBF *needs* the old tx fee rbf = is_mine and not tx.is_final() and fee is not None From f767d41409ad20e60bf535fb78468f276679b6fb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Nov 2018 18:58:27 +0100 Subject: [PATCH 122/301] tests: spanish test case for mnemonic.py, and refactoring --- electrum/tests/test_mnemonic.py | 142 +++++++++++++++++-------- electrum/tests/test_wallet_vertical.py | 4 +- 2 files changed, 99 insertions(+), 47 deletions(-) diff --git a/electrum/tests/test_mnemonic.py b/electrum/tests/test_mnemonic.py index 1f5f18f1..7b207c8b 100644 --- a/electrum/tests/test_mnemonic.py +++ b/electrum/tests/test_mnemonic.py @@ -1,68 +1,120 @@ +from typing import NamedTuple, Optional + from electrum import keystore from electrum import mnemonic from electrum import old_mnemonic from electrum.util import bh2u, bfh from electrum.bitcoin import is_new_seed -from electrum.version import SEED_PREFIX_SW +from electrum.version import SEED_PREFIX_SW, SEED_PREFIX from . import SequentialTestCase -from .test_wallet_vertical import UNICODE_HORROR +from .test_wallet_vertical import UNICODE_HORROR, UNICODE_HORROR_HEX -SEED_WORDS_JAPANESE = 'なのか ひろい しなん まなぶ つぶす さがす おしゃれ かわく おいかける けさき かいとう さたん' -assert bh2u(SEED_WORDS_JAPANESE.encode('utf8')) == 'e381aae381aee3818b20e381b2e3828de3818420e38197e381aae3829320e381bee381aae381b5e3829920e381a4e381b5e38299e3819920e38195e3818be38299e3819920e3818ae38197e38283e3828c20e3818be3828fe3818f20e3818ae38184e3818be38191e3828b20e38191e38195e3818d20e3818be38184e381a8e3818620e38195e3819fe38293' +class SeedTestCase(NamedTuple): + words: str + bip32_seed: str + lang: Optional[str] = 'en' + words_hex: Optional[str] = None + entropy: Optional[int] = None + passphrase: Optional[str] = None + passphrase_hex: Optional[str] = None + seed_version: str = SEED_PREFIX -SEED_WORDS_CHINESE = '眼 悲 叛 改 节 跃 衡 响 疆 股 遂 冬' -assert bh2u(SEED_WORDS_CHINESE.encode('utf8')) == 'e79cbc20e682b220e58f9b20e694b920e88a8220e8b78320e8a1a120e5938d20e7968620e882a120e9818220e586ac' -PASSPHRASE_CHINESE = '给我一些测试向量谷歌' -assert bh2u(PASSPHRASE_CHINESE.encode('utf8')) == 'e7bb99e68891e4b880e4ba9be6b58be8af95e59091e9878fe8b0b7e6ad8c' +SEED_TEST_CASES = { + 'english': SeedTestCase( + words='wild father tree among universe such mobile favorite target dynamic credit identify', + seed_version=SEED_PREFIX_SW, + bip32_seed='aac2a6302e48577ab4b46f23dbae0774e2e62c796f797d0a1b5faeb528301e3064342dafb79069e7c4c6b8c38ae11d7a973bec0d4f70626f8cc5184a8d0b0756'), + 'english_with_passphrase': SeedTestCase( + words='wild father tree among universe such mobile favorite target dynamic credit identify', + seed_version=SEED_PREFIX_SW, + passphrase='Did you ever hear the tragedy of Darth Plagueis the Wise?', + bip32_seed='4aa29f2aeb0127efb55138ab9e7be83b36750358751906f86c662b21a1ea1370f949e6d1a12fa56d3d93cadda93038c76ac8118597364e46f5156fde6183c82f'), + 'japanese': SeedTestCase( + lang='ja', + words='なのか ひろい しなん まなぶ つぶす さがす おしゃれ かわく おいかける けさき かいとう さたん', + words_hex='e381aae381aee3818b20e381b2e3828de3818420e38197e381aae3829320e381bee381aae381b5e3829920e381a4e381b5e38299e3819920e38195e3818be38299e3819920e3818ae38197e38283e3828c20e3818be3828fe3818f20e3818ae38184e3818be38191e3828b20e38191e38195e3818d20e3818be38184e381a8e3818620e38195e3819fe38293', + entropy=1938439226660562861250521787963972783469, + bip32_seed='d3eaf0e44ddae3a5769cb08a26918e8b308258bcb057bb704c6f69713245c0b35cb92c03df9c9ece5eff826091b4e74041e010b701d44d610976ce8bfb66a8ad'), + 'japanese_with_passphrase': SeedTestCase( + lang='ja', + words='なのか ひろい しなん まなぶ つぶす さがす おしゃれ かわく おいかける けさき かいとう さたん', + words_hex='e381aae381aee3818b20e381b2e3828de3818420e38197e381aae3829320e381bee381aae381b5e3829920e381a4e381b5e38299e3819920e38195e3818be38299e3819920e3818ae38197e38283e3828c20e3818be3828fe3818f20e3818ae38184e3818be38191e3828b20e38191e38195e3818d20e3818be38184e381a8e3818620e38195e3819fe38293', + entropy=1938439226660562861250521787963972783469, + passphrase=UNICODE_HORROR, + passphrase_hex=UNICODE_HORROR_HEX, + bip32_seed='251ee6b45b38ba0849e8f40794540f7e2c6d9d604c31d68d3ac50c034f8b64e4bc037c5e1e985a2fed8aad23560e690b03b120daf2e84dceb1d7857dda042457'), + 'chinese': SeedTestCase( + lang='zh', + words='眼 悲 叛 改 节 跃 衡 响 疆 股 遂 冬', + words_hex='e79cbc20e682b220e58f9b20e694b920e88a8220e8b78320e8a1a120e5938d20e7968620e882a120e9818220e586ac', + seed_version=SEED_PREFIX_SW, + entropy=3083737086352778425940060465574397809099, + bip32_seed='0b9077db7b5a50dbb6f61821e2d35e255068a5847e221138048a20e12d80b673ce306b6fe7ac174ebc6751e11b7037be6ee9f17db8040bb44f8466d519ce2abf'), + 'chinese_with_passphrase': SeedTestCase( + lang='zh', + words='眼 悲 叛 改 节 跃 衡 响 疆 股 遂 冬', + words_hex='e79cbc20e682b220e58f9b20e694b920e88a8220e8b78320e8a1a120e5938d20e7968620e882a120e9818220e586ac', + seed_version=SEED_PREFIX_SW, + entropy=3083737086352778425940060465574397809099, + passphrase='给我一些测试向量谷歌', + passphrase_hex='e7bb99e68891e4b880e4ba9be6b58be8af95e59091e9878fe8b0b7e6ad8c', + bip32_seed='6c03dd0615cf59963620c0af6840b52e867468cc64f20a1f4c8155705738e87b8edb0fc8a6cee4085776cb3a629ff88bb1a38f37085efdbf11ce9ec5a7fa5f71'), + 'spanish': SeedTestCase( + lang='es', + words='almíbar tibio superar vencer hacha peatón príncipe matar consejo polen vehículo odisea', + words_hex='616c6d69cc8162617220746962696f20737570657261722076656e63657220686163686120706561746fcc816e20707269cc816e63697065206d6174617220636f6e73656a6f20706f6c656e2076656869cc8163756c6f206f6469736561', + entropy=3423992296655289706780599506247192518735, + bip32_seed='18bffd573a960cc775bbd80ed60b7dc00bc8796a186edebe7fc7cf1f316da0fe937852a969c5c79ded8255cdf54409537a16339fbe33fb9161af793ea47faa7a'), + 'spanish_with_passphrase': SeedTestCase( + lang='es', + words='almíbar tibio superar vencer hacha peatón príncipe matar consejo polen vehículo odisea', + words_hex='616c6d69cc8162617220746962696f20737570657261722076656e63657220686163686120706561746fcc816e20707269cc816e63697065206d6174617220636f6e73656a6f20706f6c656e2076656869cc8163756c6f206f6469736561', + entropy=3423992296655289706780599506247192518735, + passphrase='araña difícil solución término cárcel', + passphrase_hex='6172616ecc83612064696669cc8163696c20736f6c7563696fcc816e207465cc81726d696e6f206361cc817263656c', + bip32_seed='363dec0e575b887cfccebee4c84fca5a3a6bed9d0e099c061fa6b85020b031f8fe3636d9af187bf432d451273c625e20f24f651ada41aae2c4ea62d87e9fa44c'), + 'spanish2': SeedTestCase( + lang='es', + words='equipo fiar auge langosta hacha calor trance cubrir carro pulmón oro áspero', + words_hex='65717569706f20666961722061756765206c616e676f7374612068616368612063616c6f72207472616e63652063756272697220636172726f2070756c6d6fcc816e206f726f2061cc81737065726f', + seed_version=SEED_PREFIX_SW, + entropy=448346710104003081119421156750490206837, + bip32_seed='001ebce6bfde5851f28a0d44aae5ae0c762b600daf3b33fc8fc630aee0d207646b6f98b18e17dfe3be0a5efe2753c7cdad95860adbbb62cecad4dedb88e02a64'), + 'spanish3': SeedTestCase( + lang='es', + words='vidrio jabón muestra pájaro capucha eludir feliz rotar fogata pez rezar oír', + words_hex='76696472696f206a61626fcc816e206d756573747261207061cc816a61726f206361707563686120656c756469722066656c697a20726f74617220666f676174612070657a2072657a6172206f69cc8172', + seed_version=SEED_PREFIX_SW, + entropy=3444792611339130545499611089352232093648, + passphrase='¡Viva España! repiten veinte pueblos y al hablar dan fe del ánimo español... ¡Marquen arado martillo y clarín', + passphrase_hex='c2a1566976612045737061c3b16121207265706974656e207665696e746520707565626c6f73207920616c206861626c61722064616e2066652064656c20c3a16e696d6f2065737061c3b16f6c2e2e2e20c2a14d61727175656e20617261646f206d617274696c6c6f207920636c6172c3ad6e', + bip32_seed='c274665e5453c72f82b8444e293e048d700c59bf000cacfba597629d202dcf3aab1cf9c00ba8d3456b7943428541fed714d01d8a0a4028fc3a9bb33d981cb49f'), +} class Test_NewMnemonic(SequentialTestCase): def test_mnemonic_to_seed_basic(self): + # note: not a valid electrum seed seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic='foobar', passphrase='none') self.assertEqual('741b72fd15effece6bfe5a26a52184f66811bd2be363190e07a42cca442b1a5bb22b3ad0eb338197287e6d314866c7fba863ac65d3f156087a5052ebc7157fce', bh2u(seed)) - def test_mnemonic_to_seed_japanese(self): - words = SEED_WORDS_JAPANESE - self.assertTrue(is_new_seed(words)) - - m = mnemonic.Mnemonic(lang='ja') - self.assertEqual(1938439226660562861250521787963972783469, m.mnemonic_decode(words)) - - seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=words, passphrase='') - self.assertEqual('d3eaf0e44ddae3a5769cb08a26918e8b308258bcb057bb704c6f69713245c0b35cb92c03df9c9ece5eff826091b4e74041e010b701d44d610976ce8bfb66a8ad', - bh2u(seed)) - - def test_mnemonic_to_seed_japanese_with_unicode_horror(self): - words = SEED_WORDS_JAPANESE - self.assertTrue(is_new_seed(words)) - - seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=words, passphrase=UNICODE_HORROR) - self.assertEqual('251ee6b45b38ba0849e8f40794540f7e2c6d9d604c31d68d3ac50c034f8b64e4bc037c5e1e985a2fed8aad23560e690b03b120daf2e84dceb1d7857dda042457', - bh2u(seed)) - - def test_mnemonic_to_seed_chinese(self): - words = SEED_WORDS_CHINESE - self.assertTrue(is_new_seed(words, prefix=SEED_PREFIX_SW)) - - m = mnemonic.Mnemonic(lang='zh') - self.assertEqual(3083737086352778425940060465574397809099, m.mnemonic_decode(words)) - - seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=words, passphrase='') - self.assertEqual('0b9077db7b5a50dbb6f61821e2d35e255068a5847e221138048a20e12d80b673ce306b6fe7ac174ebc6751e11b7037be6ee9f17db8040bb44f8466d519ce2abf', - bh2u(seed)) - - def test_mnemonic_to_seed_chinese_with_passphrase(self): - words = SEED_WORDS_CHINESE - passphrase = PASSPHRASE_CHINESE - self.assertTrue(is_new_seed(words, prefix=SEED_PREFIX_SW)) - seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=words, passphrase=passphrase) - self.assertEqual('6c03dd0615cf59963620c0af6840b52e867468cc64f20a1f4c8155705738e87b8edb0fc8a6cee4085776cb3a629ff88bb1a38f37085efdbf11ce9ec5a7fa5f71', - bh2u(seed)) + def test_mnemonic_to_seed(self): + for test_name, test in SEED_TEST_CASES.items(): + if test.words_hex is not None: + self.assertEqual(test.words_hex, bh2u(test.words.encode('utf8')), msg=test_name) + self.assertTrue(is_new_seed(test.words, prefix=test.seed_version), msg=test_name) + m = mnemonic.Mnemonic(lang=test.lang) + if test.entropy is not None: + self.assertEqual(test.entropy, m.mnemonic_decode(test.words), msg=test_name) + if test.passphrase_hex is not None: + self.assertEqual(test.passphrase_hex, bh2u(test.passphrase.encode('utf8')), msg=test_name) + seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=test.words, passphrase=test.passphrase) + self.assertEqual(test.bip32_seed, bh2u(seed), msg=test_name) def test_random_seeds(self): iters = 10 diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index da428593..2f4d336d 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -19,8 +19,8 @@ from . import SequentialTestCase from .test_bitcoin import needs_test_with_all_ecc_implementations -_UNICODE_HORROR_HEX = 'e282bf20f09f988020f09f98882020202020e3818620e38191e3819fe381be20e3828fe3828b2077cda2cda2cd9d68cda16fcda2cda120ccb8cda26bccb5cd9f6eccb4cd98c7ab77ccb8cc9b73cd9820cc80cc8177cd98cda2e1b8a9ccb561d289cca1cda27420cca7cc9568cc816fccb572cd8fccb5726f7273cca120ccb6cda1cda06cc4afccb665cd9fcd9f20ccb6cd9d696ecda220cd8f74cc9568ccb7cca1cd9f6520cd9fcd9f64cc9b61cd9c72cc95cda16bcca2cca820cda168ccb465cd8f61ccb7cca2cca17274cc81cd8f20ccb4ccb7cda0c3b2ccb5ccb666ccb82075cca7cd986ec3adcc9bcd9c63cda2cd8f6fccb7cd8f64ccb8cda265cca1cd9d3fcd9e' -UNICODE_HORROR = bfh(_UNICODE_HORROR_HEX).decode('utf-8') +UNICODE_HORROR_HEX = 'e282bf20f09f988020f09f98882020202020e3818620e38191e3819fe381be20e3828fe3828b2077cda2cda2cd9d68cda16fcda2cda120ccb8cda26bccb5cd9f6eccb4cd98c7ab77ccb8cc9b73cd9820cc80cc8177cd98cda2e1b8a9ccb561d289cca1cda27420cca7cc9568cc816fccb572cd8fccb5726f7273cca120ccb6cda1cda06cc4afccb665cd9fcd9f20ccb6cd9d696ecda220cd8f74cc9568ccb7cca1cd9f6520cd9fcd9f64cc9b61cd9c72cc95cda16bcca2cca820cda168ccb465cd8f61ccb7cca2cca17274cc81cd8f20ccb4ccb7cda0c3b2ccb5ccb666ccb82075cca7cd986ec3adcc9bcd9c63cda2cd8f6fccb7cd8f64ccb8cda265cca1cd9d3fcd9e' +UNICODE_HORROR = bfh(UNICODE_HORROR_HEX).decode('utf-8') assert UNICODE_HORROR == '₿ 😀 😈 う けたま わる w͢͢͝h͡o͢͡ ̸͢k̵͟n̴͘ǫw̸̛s͘ ̀́w͘͢ḩ̵a҉̡͢t ̧̕h́o̵r͏̵rors̡ ̶͡͠lį̶e͟͟ ̶͝in͢ ͏t̕h̷̡͟e ͟͟d̛a͜r̕͡k̢̨ ͡h̴e͏a̷̢̡rt́͏ ̴̷͠ò̵̶f̸ u̧͘ní̛͜c͢͏o̷͏d̸͢e̡͝?͞' From 4d62963efe79d2224a5a273b3eaac207def68a7b Mon Sep 17 00:00:00 2001 From: ghost43 Date: Wed, 14 Nov 2018 22:39:49 +0100 Subject: [PATCH 123/301] qt: count wizards in progress (#4349) fixes #4348 --- electrum/gui/qt/__init__.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 9e961224..75011ae7 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -26,6 +26,7 @@ import signal import sys import traceback +import threading try: @@ -105,6 +106,8 @@ class ElectrumGui(PrintError): self.timer = Timer() self.nd = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() + self._num_wizards_in_progress = 0 + self._num_wizards_lock = threading.Lock() # init tray self.dark_icon = self.config.get("dark_icon", False) self.tray = QSystemTrayIcon(self.tray_icon(), None) @@ -195,6 +198,18 @@ class ElectrumGui(PrintError): run_hook('on_new_window', w) return w + def count_wizards_in_progress(func): + def wrapper(self: 'ElectrumGui', *args, **kwargs): + with self._num_wizards_lock: + self._num_wizards_in_progress += 1 + try: + return func(self, *args, **kwargs) + finally: + with self._num_wizards_lock: + self._num_wizards_in_progress -= 1 + return wrapper + + @count_wizards_in_progress def start_new_window(self, path, uri, app_is_starting=False): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it''' @@ -291,10 +306,15 @@ class ElectrumGui(PrintError): signal.signal(signal.SIGINT, lambda *args: self.app.quit()) def quit_after_last_window(): - # on some platforms, not only does exec_ not return but not even - # aboutToQuit is emitted (but following this, it should be emitted) - if self.app.quitOnLastWindowClosed(): - self.app.quit() + # keep daemon running after close + if self.config.get('daemon'): + return + # check if a wizard is in progress + with self._num_wizards_lock: + if self._num_wizards_in_progress > 0 or len(self.windows) > 0: + return + self.app.quit() + self.app.setQuitOnLastWindowClosed(False) # so _we_ can decide whether to quit self.app.lastWindowClosed.connect(quit_after_last_window) def clean_up(): @@ -306,10 +326,6 @@ class ElectrumGui(PrintError): self.tray.hide() self.app.aboutToQuit.connect(clean_up) - # keep daemon running after close - if self.config.get('daemon'): - self.app.setQuitOnLastWindowClosed(False) - # main loop self.app.exec_() # on some platforms the exec_ call may not return, so use clean_up() From eba97f74b41b0e647aaf600f7cdd3bf643a40fee Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 16 Nov 2018 14:39:22 +0100 Subject: [PATCH 124/301] decorate some methods with @profiler to debug slow startup --- electrum/daemon.py | 3 ++- electrum/gui/qt/__init__.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index a2f80c62..ad1c2a85 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -37,7 +37,7 @@ from .jsonrpc import VerifyingJSONRPCServer from .version import ELECTRUM_VERSION from .network import Network from .util import (json_decode, DaemonThread, print_error, to_string, - create_and_start_event_loop) + create_and_start_event_loop, profiler) from .wallet import Wallet, Abstract_Wallet from .storage import WalletStorage from .commands import known_commands, Commands @@ -121,6 +121,7 @@ def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]: class Daemon(DaemonThread): + @profiler def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True): DaemonThread.__init__(self) self.config = config diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 75011ae7..9da968f3 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -43,7 +43,7 @@ from electrum.i18n import _, set_language from electrum.plugin import run_hook from electrum.storage import WalletStorage from electrum.base_wizard import GoBack -from electrum.util import (UserCancelled, PrintError, +from electrum.util import (UserCancelled, PrintError, profiler, WalletFileException, BitcoinException) from .installwizard import InstallWizard @@ -85,6 +85,7 @@ class QNetworkUpdatedSignalObject(QObject): class ElectrumGui(PrintError): + @profiler def __init__(self, config, daemon, plugins): set_language(config.get('language')) # Uncomment this call to verify objects are being properly @@ -190,6 +191,7 @@ class ElectrumGui(PrintError): self.network_updated_signal_obj) self.nd.show() + @profiler def create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) From 32af83b7aede02f0b9bb3e8294ef2dc0481fb6de Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 16 Nov 2018 19:03:25 +0100 Subject: [PATCH 125/301] wizard/hw: show transport type when listing HWDs --- electrum/base_wizard.py | 3 +-- electrum/plugin.py | 9 +++++++-- electrum/plugins/coldcard/coldcard.py | 8 ++++++-- electrum/plugins/safe_t/safe_t.py | 8 +++++++- electrum/plugins/trezor/trezor.py | 8 +++++++- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 431e72e0..84323226 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -283,8 +283,7 @@ class BaseWizard(object): for name, info in devices: state = _("initialized") if info.initialized else _("wiped") label = info.label or _("An unnamed {}").format(name) - descr = f"{label} [{name}, {state}]" - # TODO maybe expose info.device.path (mainly for transport type) + descr = f"{label} [{name}, {state}, {info.device.transport_ui_string}]" choices.append(((name, info), descr)) msg = _('Select a device') + ':' self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose)) diff --git a/electrum/plugin.py b/electrum/plugin.py index e2c58d3a..f24206c6 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -291,6 +291,7 @@ class Device(NamedTuple): id_: str product_key: Any # when using hid, often Tuple[int, int] usage_page: int + transport_ui_string: str class DeviceInfo(NamedTuple): @@ -576,8 +577,12 @@ class DeviceMgr(ThreadJob, PrintError): if len(id_) == 0: id_ = str(d['path']) id_ += str(interface_number) + str(usage_page) - devices.append(Device(d['path'], interface_number, - id_, product_key, usage_page)) + devices.append(Device(path=d['path'], + interface_number=interface_number, + id_=id_, + product_key=product_key, + usage_page=usage_page, + transport_ui_string='hid')) return devices def scan_devices(self): diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index f1558005..afbb1384 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -625,10 +625,14 @@ class ColdcardPlugin(HW_PluginBase): fn = CKCC_SIMULATOR_PATH if os.path.exists(fn): - return [Device(fn, -1, fn, (COINKITE_VID, CKCC_SIMULATED_PID), 0)] + return [Device(path=fn, + interface_number=-1, + id_=fn, + product_key=(COINKITE_VID, CKCC_SIMULATED_PID), + usage_page=0, + transport_ui_string='simulator')] return [] - def create_client(self, device, handler): if handler: diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index c5a1031c..f153e6ad 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -105,7 +105,13 @@ class SafeTPlugin(HW_PluginBase): def enumerate(self): devices = self.transport_handler.enumerate_devices() - return [Device(d.get_path(), -1, d.get_path(), 'Safe-T mini', 0) for d in devices] + return [Device(path=d.get_path(), + interface_number=-1, + id_=d.get_path(), + product_key='Safe-T mini', + usage_page=0, + transport_ui_string=d.get_path()) + for d in devices] def create_client(self, device, handler): try: diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index a9ea1328..3f2c938d 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -106,7 +106,13 @@ class TrezorPlugin(HW_PluginBase): def enumerate(self): devices = self.transport_handler.enumerate_devices() - return [Device(d.get_path(), -1, d.get_path(), 'TREZOR', 0) for d in devices] + return [Device(path=d.get_path(), + interface_number=-1, + id_=d.get_path(), + product_key='TREZOR', + usage_page=0, + transport_ui_string=d.get_path()) + for d in devices] def create_client(self, device, handler): try: From 5376d37c24b5d60f4e8e7423326e6a42a92e2b90 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 18 Nov 2018 16:46:07 +0100 Subject: [PATCH 126/301] history export: include tx fee closes #3504 --- electrum/address_synchronizer.py | 15 ++++++++++++++- electrum/gui/qt/history_list.py | 17 +++++++++++++++-- electrum/util.py | 2 +- electrum/wallet.py | 9 +++++++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index b876daaa..e0d1ec24 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -25,7 +25,7 @@ import threading import asyncio import itertools from collections import defaultdict -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING, Dict, Optional from . import bitcoin from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY @@ -712,6 +712,19 @@ class AddressSynchronizer(PrintError): fee = None return is_relevant, is_mine, v, fee + def get_tx_fee(self, tx: Transaction) -> Optional[int]: + if not tx: + return None + if hasattr(tx, '_cached_fee'): + return tx._cached_fee + is_relevant, is_mine, v, fee = self.get_wallet_delta(tx) + if fee is None: + txid = tx.txid() + fee = self.tx_fees.get(txid) + if fee is not None: + tx._cached_fee = fee + return fee + def get_addr_io(self, address): with self.lock, self.transaction_lock: h = self.get_address_history(address) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index fd73aaf6..eb410d95 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -26,6 +26,7 @@ import webbrowser import datetime from datetime import date +from typing import TYPE_CHECKING from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ @@ -33,6 +34,9 @@ from electrum.util import block_explorer_URL, profiler, print_error, TxMinedStat from .util import * +if TYPE_CHECKING: + from electrum.wallet import Abstract_Wallet + try: from electrum.plot import plot_history, NothingToPlotException except: @@ -216,7 +220,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): @profiler def on_update(self): - self.wallet = self.parent.wallet + self.wallet = self.parent.wallet # type: Abstract_Wallet fx = self.parent.fx r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=self.start_timestamp, to_timestamp=self.end_timestamp, fx=fx) self.transactions = r['transactions'] @@ -435,12 +439,21 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): item['confirmations'], item['value'], item.get('fiat_value', ''), + item.get('fee', ''), + item.get('fiat_fee', ''), item['date']]) with open(file_name, "w+", encoding='utf-8') as f: if is_csv: import csv transaction = csv.writer(f, lineterminator='\n') - transaction.writerow(["transaction_hash", "label", "confirmations", "value", "fiat_value", "timestamp"]) + transaction.writerow(["transaction_hash", + "label", + "confirmations", + "value", + "fiat_value", + "fee", + "fiat_fee", + "timestamp"]) for line in lines: transaction.writerow(line) else: diff --git a/electrum/util.py b/electrum/util.py index d5ea9c28..92526980 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -156,7 +156,7 @@ class Fiat(object): return 'Fiat(%s)'% self.__str__() def __str__(self): - if self.value.is_nan(): + if self.value is None or self.value.is_nan(): return _('No Data') else: return "{:.2f}".format(self.value) + ' ' + self.ccy diff --git a/electrum/wallet.py b/electrum/wallet.py index a60eedce..c104d7be 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -396,6 +396,7 @@ class Abstract_Wallet(AddressSynchronizer): continue if to_timestamp and (timestamp or now) >= to_timestamp: continue + tx = self.transactions.get(tx_hash) item = { 'txid': tx_hash, 'height': tx_mined_status.height, @@ -406,8 +407,9 @@ class Abstract_Wallet(AddressSynchronizer): 'date': timestamp_to_datetime(timestamp), 'label': self.get_label(tx_hash), } + tx_fee = self.get_tx_fee(tx) + item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None if show_addresses: - tx = self.transactions.get(tx_hash) item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs())) item['outputs'] = list(map(lambda x:{'address':x.address, 'value':Satoshis(x.value)}, tx.get_outputs_for_UI())) @@ -423,8 +425,11 @@ class Abstract_Wallet(AddressSynchronizer): if fx and fx.is_enabled() and fx.get_history_config(): fiat_value = self.get_fiat_value(tx_hash, fx.ccy) fiat_default = fiat_value is None - fiat_value = fiat_value if fiat_value is not None else value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) # + fiat_rate = self.price_at_timestamp(tx_hash, fx.timestamp_rate) + fiat_value = fiat_value if fiat_value is not None else value / Decimal(COIN) * fiat_rate + fiat_fee = tx_fee / Decimal(COIN) * fiat_rate if tx_fee is not None else None item['fiat_value'] = Fiat(fiat_value, fx.ccy) + item['fiat_fee'] = Fiat(fiat_fee, fx.ccy) if fiat_fee else None item['fiat_default'] = fiat_default if value < 0: acquisition_price = - value / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy) From 36f64d1ad98487df6ea3d9d29dc320ae14799c03 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 18 Nov 2018 22:07:27 +0100 Subject: [PATCH 127/301] bitcoin/ecc: some more type annotations --- electrum/bitcoin.py | 98 ++++++++++++++++++---------------- electrum/ecc.py | 16 +++--- electrum/mnemonic.py | 2 +- electrum/paymentrequest.py | 2 +- electrum/tests/test_bitcoin.py | 6 +-- 5 files changed, 64 insertions(+), 60 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 6bebf498..56d07163 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -24,7 +24,7 @@ # SOFTWARE. import hashlib -from typing import List, Tuple, TYPE_CHECKING +from typing import List, Tuple, TYPE_CHECKING, Optional, Union from .util import bfh, bh2u, BitcoinException, assert_bytes, to_bytes, inv_dict from . import version @@ -49,7 +49,7 @@ TYPE_PUBKEY = 1 TYPE_SCRIPT = 2 -def rev_hex(s): +def rev_hex(s: str) -> str: return bh2u(bfh(s)[::-1]) @@ -162,22 +162,25 @@ def dust_threshold(network: 'Network'=None) -> int: return 182 * 3 * relayfee(network) // 1000 -hash_encode = lambda x: bh2u(x[::-1]) -hash_decode = lambda x: bfh(x)[::-1] -hmac_sha_512 = lambda x, y: hmac_oneshot(x, y, hashlib.sha512) +def hash_encode(x: bytes) -> str: + return bh2u(x[::-1]) + + +def hash_decode(x: str) -> bytes: + return bfh(x)[::-1] ################################## electrum seeds -def is_new_seed(x, prefix=version.SEED_PREFIX): +def is_new_seed(x: str, prefix=version.SEED_PREFIX) -> bool: from . import mnemonic x = mnemonic.normalize_text(x) - s = bh2u(hmac_sha_512(b"Seed version", x.encode('utf8'))) + s = bh2u(hmac_oneshot(b"Seed version", x.encode('utf8'), hashlib.sha512)) return s.startswith(prefix) -def is_old_seed(seed): +def is_old_seed(seed: str) -> bool: from . import old_mnemonic, mnemonic seed = mnemonic.normalize_text(seed) words = seed.split() @@ -195,7 +198,7 @@ def is_old_seed(seed): return is_hex or (uses_electrum_words and (len(words) == 12 or len(words) == 24)) -def seed_type(x): +def seed_type(x: str) -> str: if is_old_seed(x): return 'old' elif is_new_seed(x): @@ -206,29 +209,31 @@ def seed_type(x): return '2fa' return '' -is_seed = lambda x: bool(seed_type(x)) + +def is_seed(x: str) -> bool: + return bool(seed_type(x)) ############ functions from pywallet ##################### -def hash160_to_b58_address(h160: bytes, addrtype): - s = bytes([addrtype]) - s += h160 - return base_encode(s+sha256d(s)[0:4], base=58) +def hash160_to_b58_address(h160: bytes, addrtype: int) -> str: + s = bytes([addrtype]) + h160 + s = s + sha256d(s)[0:4] + return base_encode(s, base=58) -def b58_address_to_hash160(addr): +def b58_address_to_hash160(addr: str) -> Tuple[int, bytes]: addr = to_bytes(addr, 'ascii') _bytes = base_decode(addr, 25, base=58) return _bytes[0], _bytes[1:21] -def hash160_to_p2pkh(h160, *, net=None): +def hash160_to_p2pkh(h160: bytes, *, net=None) -> str: if net is None: net = constants.net return hash160_to_b58_address(h160, net.ADDRTYPE_P2PKH) -def hash160_to_p2sh(h160, *, net=None): +def hash160_to_p2sh(h160: bytes, *, net=None) -> str: if net is None: net = constants.net return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH) @@ -236,26 +241,26 @@ def hash160_to_p2sh(h160, *, net=None): def public_key_to_p2pkh(public_key: bytes) -> str: return hash160_to_p2pkh(hash_160(public_key)) -def hash_to_segwit_addr(h, witver, *, net=None): +def hash_to_segwit_addr(h: bytes, witver: int, *, net=None) -> str: if net is None: net = constants.net return segwit_addr.encode(net.SEGWIT_HRP, witver, h) -def public_key_to_p2wpkh(public_key): +def public_key_to_p2wpkh(public_key: bytes) -> str: return hash_to_segwit_addr(hash_160(public_key), witver=0) -def script_to_p2wsh(script): +def script_to_p2wsh(script: str) -> str: return hash_to_segwit_addr(sha256(bfh(script)), witver=0) -def p2wpkh_nested_script(pubkey): +def p2wpkh_nested_script(pubkey: str) -> str: pkh = bh2u(hash_160(bfh(pubkey))) return '00' + push_script(pkh) -def p2wsh_nested_script(witness_script): +def p2wsh_nested_script(witness_script: str) -> str: wsh = bh2u(sha256(bfh(witness_script))) return '00' + push_script(wsh) -def pubkey_to_address(txin_type, pubkey): +def pubkey_to_address(txin_type: str, pubkey: str) -> str: if txin_type == 'p2pkh': return public_key_to_p2pkh(bfh(pubkey)) elif txin_type == 'p2wpkh': @@ -266,7 +271,7 @@ def pubkey_to_address(txin_type, pubkey): else: raise NotImplementedError(txin_type) -def redeem_script_to_address(txin_type, redeem_script): +def redeem_script_to_address(txin_type: str, redeem_script: str) -> str: if txin_type == 'p2sh': return hash160_to_p2sh(hash_160(bfh(redeem_script))) elif txin_type == 'p2wsh': @@ -278,7 +283,7 @@ def redeem_script_to_address(txin_type, redeem_script): raise NotImplementedError(txin_type) -def script_to_address(script, *, net=None): +def script_to_address(script: str, *, net=None) -> str: from .transaction import get_address_from_output_script t, addr = get_address_from_output_script(bfh(script), net=net) assert t == TYPE_ADDRESS @@ -310,15 +315,15 @@ def address_to_script(addr: str, *, net=None) -> str: raise BitcoinException(f'unknown address type: {addrtype}') return script -def address_to_scripthash(addr): +def address_to_scripthash(addr: str) -> str: script = address_to_script(addr) return script_to_scripthash(script) -def script_to_scripthash(script): - h = sha256(bytes.fromhex(script))[0:32] +def script_to_scripthash(script: str) -> str: + h = sha256(bfh(script))[0:32] return bh2u(bytes(reversed(h))) -def public_key_to_p2pk_script(pubkey): +def public_key_to_p2pk_script(pubkey: str) -> str: script = push_script(pubkey) script += 'ac' # op_checksig return script @@ -360,7 +365,7 @@ def base_encode(v: bytes, base: int) -> str: return result.decode('ascii') -def base_decode(v, length, base): +def base_decode(v: Union[bytes, str], length: Optional[int], base: int) -> Optional[bytes]: """ decode v into a string of len bytes.""" # assert_bytes(v) v = to_bytes(v, 'ascii') @@ -398,21 +403,20 @@ class InvalidChecksum(Exception): pass -def EncodeBase58Check(vchIn): +def EncodeBase58Check(vchIn: bytes) -> str: hash = sha256d(vchIn) return base_encode(vchIn + hash[0:4], base=58) -def DecodeBase58Check(psz): +def DecodeBase58Check(psz: Union[bytes, str]) -> bytes: vchRet = base_decode(psz, None, base=58) - key = vchRet[0:-4] - csum = vchRet[-4:] - hash = sha256d(key) - cs32 = hash[0:4] - if cs32 != csum: - raise InvalidChecksum('expected {}, actual {}'.format(bh2u(cs32), bh2u(csum))) + payload = vchRet[0:-4] + csum_found = vchRet[-4:] + csum_calculated = sha256d(payload)[0:4] + if csum_calculated != csum_found: + raise InvalidChecksum(f'calculated {bh2u(csum_calculated)}, found {bh2u(csum_found)}') else: - return key + return payload # backwards compat @@ -484,16 +488,16 @@ def deserialize_privkey(key: str) -> Tuple[str, bytes, bool]: return txin_type, secret_bytes, compressed -def is_compressed(sec): +def is_compressed_privkey(sec: str) -> bool: return deserialize_privkey(sec)[2] -def address_from_private_key(sec): +def address_from_private_key(sec: str) -> str: txin_type, privkey, compressed = deserialize_privkey(sec) public_key = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) return pubkey_to_address(txin_type, public_key) -def is_segwit_address(addr, *, net=None): +def is_segwit_address(addr: str, *, net=None) -> bool: if net is None: net = constants.net try: witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr) @@ -501,7 +505,7 @@ def is_segwit_address(addr, *, net=None): return False return witprog is not None -def is_b58_address(addr, *, net=None): +def is_b58_address(addr: str, *, net=None) -> bool: if net is None: net = constants.net try: addrtype, h = b58_address_to_hash160(addr) @@ -511,13 +515,13 @@ def is_b58_address(addr, *, net=None): return False return addr == hash160_to_b58_address(h, addrtype) -def is_address(addr, *, net=None): +def is_address(addr: str, *, net=None) -> bool: if net is None: net = constants.net return is_segwit_address(addr, net=net) \ or is_b58_address(addr, net=net) -def is_private_key(key): +def is_private_key(key: str) -> bool: try: k = deserialize_privkey(key) return k is not False @@ -527,7 +531,7 @@ def is_private_key(key): ########### end pywallet functions ####################### -def is_minikey(text): +def is_minikey(text: str) -> bool: # Minikeys are typically 22 or 30 characters, but this routine # permits any length of 20 or more provided the minikey is valid. # A valid minikey must begin with an 'S', be in base58, and when @@ -537,5 +541,5 @@ def is_minikey(text): and all(ord(c) in __b58chars for c in text) and sha256(text + '?')[0] == 0x00) -def minikey_to_private_key(text): +def minikey_to_private_key(text: str) -> bytes: return sha256(text) diff --git a/electrum/ecc.py b/electrum/ecc.py index 7c2cab92..feb56ce7 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -52,31 +52,31 @@ def point_at_infinity(): return ECPubkey(None) -def sig_string_from_der_sig(der_sig, order=CURVE_ORDER): +def sig_string_from_der_sig(der_sig: bytes, order=CURVE_ORDER) -> bytes: r, s = ecdsa.util.sigdecode_der(der_sig, order) return ecdsa.util.sigencode_string(r, s, order) -def der_sig_from_sig_string(sig_string, order=CURVE_ORDER): +def der_sig_from_sig_string(sig_string: bytes, order=CURVE_ORDER) -> bytes: r, s = ecdsa.util.sigdecode_string(sig_string, order) return ecdsa.util.sigencode_der_canonize(r, s, order) -def der_sig_from_r_and_s(r, s, order=CURVE_ORDER): +def der_sig_from_r_and_s(r: int, s: int, order=CURVE_ORDER) -> bytes: return ecdsa.util.sigencode_der_canonize(r, s, order) -def get_r_and_s_from_der_sig(der_sig, order=CURVE_ORDER): +def get_r_and_s_from_der_sig(der_sig: bytes, order=CURVE_ORDER) -> Tuple[int, int]: r, s = ecdsa.util.sigdecode_der(der_sig, order) return r, s -def get_r_and_s_from_sig_string(sig_string, order=CURVE_ORDER): +def get_r_and_s_from_sig_string(sig_string: bytes, order=CURVE_ORDER) -> Tuple[int, int]: r, s = ecdsa.util.sigdecode_string(sig_string, order) return r, s -def sig_string_from_r_and_s(r, s, order=CURVE_ORDER): +def sig_string_from_r_and_s(r: int, s: int, order=CURVE_ORDER) -> bytes: return ecdsa.util.sigencode_string_canonize(r, s, order) @@ -410,7 +410,7 @@ class ECPrivkey(ECPubkey): sig65, recid = bruteforce_recid(sig_string) return sig65 - def decrypt_message(self, encrypted, magic=b'BIE1'): + def decrypt_message(self, encrypted: Tuple[str, bytes], magic: bytes=b'BIE1') -> bytes: encrypted = base64.b64decode(encrypted) if len(encrypted) < 85: raise Exception('invalid ciphertext: length') @@ -435,6 +435,6 @@ class ECPrivkey(ECPubkey): return aes_decrypt_with_iv(key_e, iv, ciphertext) -def construct_sig65(sig_string, recid, is_compressed): +def construct_sig65(sig_string: bytes, recid: int, is_compressed: bool) -> bytes: comp = 4 if is_compressed else 0 return bytes([27 + recid + comp]) + sig_string diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py index 6de21750..97889c0a 100644 --- a/electrum/mnemonic.py +++ b/electrum/mnemonic.py @@ -74,7 +74,7 @@ def is_CJK(c): return False -def normalize_text(seed): +def normalize_text(seed: str) -> str: # normalize seed = unicodedata.normalize('NFKD', seed) # lower diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 071e7bb8..8b0d4bee 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -336,7 +336,7 @@ def sign_request_with_alias(pr, alias, alias_privkey): pr.pki_data = str(alias) message = pr.SerializeToString() ec_key = ecc.ECPrivkey(alias_privkey) - compressed = bitcoin.is_compressed(alias_privkey) + compressed = bitcoin.is_compressed_privkey(alias_privkey) pr.signature = ec_key.sign_message(message, compressed) diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index e11cee4e..cdf1257f 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -6,7 +6,7 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key, var_int, op_push, address_to_script, deserialize_privkey, serialize_privkey, is_segwit_address, is_b58_address, address_to_scripthash, is_minikey, - is_compressed, seed_type, EncodeBase58Check, + is_compressed_privkey, seed_type, EncodeBase58Check, script_num_to_hex, push_script, add_number_to_script, int_to_hex) from electrum.bip32 import (bip32_root, bip32_public_derivation, bip32_private_derivation, xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation, @@ -710,10 +710,10 @@ class Test_keyImport(SequentialTestCase): self.assertEqual(minikey, is_minikey(priv)) @needs_test_with_all_ecc_implementations - def test_is_compressed(self): + def test_is_compressed_privkey(self): for priv_details in self.priv_pub_addr: self.assertEqual(priv_details['compressed'], - is_compressed(priv_details['priv'])) + is_compressed_privkey(priv_details['priv'])) class Test_seeds(SequentialTestCase): From 55963bd092dc8be9d597f8edcc08a721a9234c3c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 20 Nov 2018 11:59:06 +0100 Subject: [PATCH 128/301] network: oneserver should be bool fix #4858 --- electrum/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index a9df3494..c0218f9c 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -522,7 +522,7 @@ class Network(PrintError): def _set_oneserver(self, oneserver: bool): self.num_server = 10 if not oneserver else 0 - self.oneserver = oneserver + self.oneserver = bool(oneserver) async def _switch_to_random_interface(self): '''Switch to a random connected server other than the current one''' @@ -817,7 +817,7 @@ class Network(PrintError): self.protocol = deserialize_server(self.default_server)[2] self.server_queue = queue.Queue() self._set_proxy(deserialize_proxy(self.config.get('proxy'))) - self._set_oneserver(self.config.get('oneserver')) + self._set_oneserver(self.config.get('oneserver', False)) self._start_interface(self.default_server) async def main(): From a8e6eaa247a7a21cba1d23bf72ae9799cbc0cd73 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 22 Nov 2018 16:52:51 +0100 Subject: [PATCH 129/301] blockchain: fix difficulty retarget "target" is a 256 bit int, but the "bits" field in the block headers that is used to represent target is only 32 bits. We were checking PoW against the untruncated target value, which is a slightly larger value than the one that can actually be represented, and hence we would have accepted a slightly lower difficulty chain than what the consensus requires. --- electrum/blockchain.py | 2 + electrum/checkpoints.json | 502 +++++++++++++++++++------------------- 2 files changed, 253 insertions(+), 251 deletions(-) diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 2c72f6b3..5609c41e 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -367,6 +367,8 @@ class Blockchain(util.PrintError): nActualTimespan = max(nActualTimespan, nTargetTimespan // 4) nActualTimespan = min(nActualTimespan, nTargetTimespan * 4) new_target = min(MAX_TARGET, (target * nActualTimespan) // nTargetTimespan) + # not any target can be represented in 32 bits: + new_target = self.bits_to_target(self.target_to_bits(new_target)) return new_target def bits_to_target(self, bits: int) -> int: diff --git a/electrum/checkpoints.json b/electrum/checkpoints.json index 58fd65f9..765ba8fe 100644 --- a/electrum/checkpoints.json +++ b/electrum/checkpoints.json @@ -61,75 +61,75 @@ ], [ "00000000984f962134a7291e3693075ae03e521f0ee33378ec30a334d860034b", - 22791193517536179595645637622052884930882401463536451358196587084939 + 22791060871177364286867400663010583169263383106957897897309909286912 ], [ "000000005e36047e39452a7beaaa6721048ac408a3e75bb60a8b0008713653ce", - 20657752757787447339704949573503817091559730029795210136290380062967 + 20657664212610420653213483117824978239553266057163961604478437687296 ], [ "00000000128d789579ffbec00203a371cbb39cee27df35d951fd66e62ed59258", - 20055900442607493419304231885070612388653052033693203212369143515380 + 20055820920770189543295303139304627292355830414308479769458683936768 ], [ "000000008dde642fb80481bb5e1671cb04c6716de5b7f783aa3388456d5c8a85", - 14823964236757567892443379740509603561300486961438335652879209691748 + 14823939180767414932263578623363531361763221729526512593941781544960 ], [ "000000008135b689ad1557d4e148a8b9e58e2c4a67240fc87962abb69710231a", - 10665478642773714533594093039651282002301533435475036254747899885223 + 10665477591887247494381404907447500979192021944764506987270680608768 ], [ "00000000308496ef3e4f9fa542a772df637b4aaf1dcce404424611feacfc09e7", - 7129928201274994723790235748908587989251132236328748923672922318604 + 7129927859545590787920041835044506526699926406309469412482969763840 ], [ "000000001a2e0c63d7d012003c9173acfd04ccd6372027718979228c461b5ed5", - 5949911830998722926292643443014583571932577723103865087785236463581 + 5949911473257063494842414979623989957440207170696926280907451531264 ], [ "000000002e0c0ac26ccde91b51ab018576b3a126b413e9f6f787b36637f1b174", - 5905493731356012500002445562241380310188483401887904088185399375735 + 5905492491837656485645884063467495540781288435542782321354050895872 ], [ "00000000103226f85fe2b68795f087dcec345e523363f18017e60b5c94175355", - 4430144627991408624040948791361640318006240855899368474057439916851 + 4430143390146946405787502162943966061454423600514874825749833973760 ], [ "000000001ae6f66fd4de47f8d6f357e798943bbfc4f39ebf14b0975fab059173", - 3447600873975070077932488290376750731396138937686397230467460081722 + 3447600406241317909690675945127070282093452846402311540118831235072 ], [ "000000000a3f22690162744d3bc0b674c92e661a25afb3d2ac8b39b27ac14373", - 2351604930866654632766829472567920383958332390561025111996712740267 + 2351604382534916182160036119666703740669209516522695514729880748032 ], [ "0000000006dc436c3c515a97446af858c1203a501c85d26c4a30afa380aba4a1", - 2098151743855439919137531366951071713579837678345159724749870973527 + 2098151686442211199940455690614286210348997571531298297574806519808 ], [ "000000000943fe1680ffcc498ce50790ff8e842a8af2c157664e4fbc1cb7cb46", - 2275792073644785018721128646741518076327875870388847727099387795022 + 2275790652544821279950241890112140030244814501479017131553197129728 ], [ "000000000847b2144376c1fb057ea1d5a027d5a6004277ed4c72422e93df04e9", - 1622204498754365521718764766072378227544231556364276849425436764228 + 1622203955679450683159610732218403647246163922223729367236739072000 ], [ "00000000094505954deb1d31382b86d0510fd280a34143400b1856a4d52b4c93", - 1551050141962082184940599235022157265046848054947355206102386866143 + 1551048739079662593758612650769536967206480773659027300489594142720 ], [ "000000000109272cecb3f7e98ac12cf149fa8a1b2aaab248e1b006b0dc595a3a", - 1389323441362281405504133894690662702230469716601985716313296951861 + 1389323280429349294447518501872137680563441219958739463959193059328 ], [ "0000000009e6aa0fe39b790625ffeb18a2d6ff5060a5bd14e699e83c54109977", - 1147154217026336014073920869620380692430705543951348139504758384216 + 1147152896345386682952518188670047452875537662186691235300769792000 ], [ "0000000000d14af55c4eae0121184919baba2deb8bf89c3af6b8e4c4f35c8e4e", - 594008212391331743177258641174232971084553374243271275697110908234 + 594007861936424273334637371358095438347537381057796937154824241152 ], [ "0000000003dfbfa2b33707e691ab2ab7cda7503be2c2cce43d1b21cd1cc757fb", @@ -137,934 +137,934 @@ ], [ "0000000000c169d181d66d242901f70d006f3e088c1ae9cacb88b94b8266e9c3", - 110393704409292953137636253955510629068475916699790368077242928142 + 110393429764504113949181711819653188468070301266890302199533928448 ], [ "000000000009f7d1439d6a2fc1a456db8e843674275bf0133fc7b43b5f45b96e", - 76555780966028280774274008956877300222068246662708272689770207398 + 76554528428498296726819074079132986384157750623812250673757552640 ], [ "000000000011b8a8fad7973548b50f6d4b2ba1690f7487c374e43248c576354f", - 52679970922643127683947083904801524368866887307161543562595547363 + 52678642966898219212816601311127992435882858542187514726849708032 ], [ "000000000077e856b6cc475d9cf784119811214c9cac8d7b674ec24faa7c2c0c", - 43246875121342569218488803557695204365585581295709263857216301849 + 43246870766561725070861386869077695524372774526710079316876591104 ], [ "00000000004cbb474f2cbf3a65f690efa09804512af3351ba3a0888c806c6625", - 37817522176947171595261355763110820847417850236612020028828535138 + 37817516728945957090904676150631917288430706594442690521085247488 ], [ "0000000000235b1ec6656d8e91f3dde3b6ab9ad7e75b332e4da9355ce60d860e", - 29373105354589651513503064535568195122478342070358205617825458296 + 29373101246077110899697012205905070265841442578602225419818106880 ], [ "00000000002a153a2c95a8e5493db93086b0e3fe590b636a5871ace57523ef93", - 20444489530085161064085987129079503334049188267661948259198215487 + 20444488966645742314409346972440253478913291170842138088329707520 ], [ "00000000000e9550e084908cf91a4e8b74f9f1315d1bc4020709f9e7f261bb18", - 19563851393374294635996921207472450463857223702361327968607284610 + 19563849255781403323327768731100757126732627316116500830377476096 ], [ "00000000002c2cfef3bb85b463d3fcd39b73a6d3d5ae11c1e2a8113e3794f28d", - 12545027206560661467344001226069385793869578030934168709550533072 + 12545026348036226200394850922278603223904369245268262607334146048 ], [ "00000000000fa92b757ee29674aa97e98a49ba3ad340d2baa94155d71648dfe1", - 8719871918647905191685831001181973300414533694245757905046274783 + 8719867261221084516486306056196045840260667577454435863762042880 ], [ "0000000000030571601dbc8e13d00d45004eee6ea8b6ab3cdfb38d2546fee21c", - 5942997561411541711563156602531385577600077786198627208704997014 + 5942996718418989293499865695368015163438891473576991811912597504 ], [ "00000000000bb6adef42e63082b20fd2b1dc1b324c51973512a4c31f29a9986e", - 3926018509229572344313816286588613965571477415700629866143917555 + 3926013280397599483741094494745234959951218212740030386090803200 ], [ "000000000000765094788a98dbb8adac30d248b7129b59b1441ee2b7ef9e332f", - 3337325505332425700040650320729095537310516946108490809993884103 + 3337321571246095014985518819479127172783474909736415373333364736 ], [ "00000000000431a0aa9625f82975709f3c6f4f64d04c559512af051599872084", - 2200422254731939804709388022233205762025354383380152145148334197 + 2200419182034594781720344474937177839165432393990533906392154112 ], [ "00000000000292b850b8f8578e6b4d03cbb4a78ada44afbb4d2f80a16490e8f9", - 1861317049673577272902795125376526066826651733332976503154178702 + 1861311314983800126815643622927230076368334845814253369901973504 ], [ "0000000000025afe84e27423011af25f777e5a94545dbd00fd04bebe9050f7dd", - 1653210985697702096268217038408538100642620147117674184232799239 + 1653206561150525499452195696179626311675293455763937233695932416 ], [ "0000000000000e389cccae2a40437be574fd806909e24136711e7f8bce671d65", - 1462202160246170142640486657710301628879951515428353771159991652 + 1462200632444444190489436459820840230299714881944341127503020032 ], [ "0000000000030510bf6bc1649726cf2e6e4010c64a2c8fd3fde5dc92535ca40e", - 1224747759897877506274637367000463834699323352769332185408382770 + 1224744150896501443874292381730317417444978877835711165914677248 ], [ "00000000000082648057f14fc835779c6ce46a407bafb2e5c2ac1d20d9f4e822", - 1036993586117576703268996282150397585765576605730719362190807632 + 1036989760889350435547200084292752907272941324136347429599444992 ], [ "000000000000f38accd6b22959010471a6d9f159d43bf2a9d4c53c220201254e", - 739430452756642306146040915925451887239493960335784687377022899 + 739430030225080220618328322475016688484025266646974337550123008 ], [ "0000000000004ed7a73133678b5eb883cd8882bf14dfb26c104ae0c3f94cf4ee", - 484980150867459464772730739859302095672636271057575743647282522 + 484975157177710342494716926626447514974484083994735770500857856 ], [ "00000000000037bb3ff4cf649a1757d4028ecc10f893529b4a2214792c981f96", - 353834202080594446847490995785168095798368734611949601937470709 + 353833947722011807976659613996792948209273674048993161457434624 ], [ "0000000000008008f46559fe7f181e9dc0648f213472a1e576e8bf506b88f22f", - 390846686979010943280302753017141998917705716027679590623447523 + 390843739553851677760235428436025349398613161749553108945469440 ], [ "000000000000691d0c2444db713bf6c088844cc95a37cdc55cc269bb0a31d8c8", - 327399809810416473497219170054754564569687652741316499001410264 + 327394795212563108599383268946242257264650552916910648089116672 ], [ "00000000000071153b0afcc64a425f8442c29749610797119e732dd4b723f675", - 291937852278662838074813817696277197987476923260730675453803937 + 291935447509363748964474894494542149680088347011133317125767168 ], [ "000000000000a384acb522e4e5935ad2bc31366ecf1f16f1f11023e967ef033d", - 245829147781851502645710488124949429684812753873220896184598139 + 245823858161213192073337185391658632187400443916100519594033152 ], [ "0000000000002e532093d43e901292121fb7c6583caf2d13b666fe7e194b4a97", - 171262571764606989041741296999128813297927395580615685573053946 + 171262555713783851185422181139260521316022447660158187451973632 ], [ "00000000000033e435c4bbddc7eb255146aa7f18e61a832983af3a9ee5dd144d", - 110439004522135981410957929709803254805947931106765020063637821 + 110438984653392107399822606842181601255647711092336854093004800 ], [ "00000000000028ff4b0bd45f0e3e713f91fa1821d28a276a1a1f32f786662f13", - 61993466854134149454140006024796140857619052825495269156061184 + 61993465896324436412959469550829248888675813063783317791309824 ], [ "0000000000001ef9c75318e116a607af4de68fb4f67c788677ee6779fb5fa0d5", - 47525095027499967685539085016596651791271838150303471592202567 + 47525089675259291211422247200069659468817014361857087365971968 ], [ "0000000000000e6e98694ccb8247aad63aaa1e2bec5a7be14329407e4cea6223", - 30742242324775075538370115437091356458943450412845263377277862 + 30742228348699538311994447367921718297595975288392383715082240 ], [ "000000000000000a2153574b2523a6d1844c3cb82d085e2575846dd8c5d4ebb4", - 19547340168280248765311813293333293631817970001494998481269884 + 19547336162709893274575855467812492508787617050928192350584832 ], [ "00000000000002a92c1b1ffb2a8388979cf30798e312335ae2a1b922927ee83d", - 17248294060755457364687620800167145237577978222086136949668577 + 17248274092338559882155796390905381469049315669915374897332224 ], [ "00000000000004d54b1422ce733922e7672a4e2ecc86dcf96c0de06565cddaa6", - 15943944661534740097945584046599407470739618287604834836788345 + 15943936487596784557029840069157210316687734428242467413295104 ], [ "00000000000009dd91ae96cbbf67af42340b0bc715b3606aa725f630b470262d", - 14273487520109069190865495135324295912393888045891964854360837 + 14273467308195657992975774342458504496649432985410431166185472 ], [ "00000000000007d33d78522fa95bdcd4a25072aeac844cbe9b6bc5d0cc885d0a", - 14930240326912220437232591181374307607822146395712988852898063 + 14930233597189143322113827544414041000381079823613435714732032 ], [ "00000000000003dd57f5dd1228f68390b586700063225d26bac972bd120546d2", - 15164777495018002532932947047554711971850359981358394796619712 + 15164766714763258952996988973449124317842091658872414191747072 ], [ "000000000000076bdeca878b47c392f51fbda543b1e69612cf7d305deb537604", - 15357836661363254148000422860842573817259062733233058353910518 + 15357836632983707094928406965317628870031114888441593128288256 ], [ "00000000000008eb1bb7e18d9dfe62210d761cbf114d59ca08e4f638b8563e30", - 15958691676650473757098043151847631737628132481844875166319930 + 15958672964717750944291813934170287689797412223641384931819520 ], [ "00000000000001b0d8d885e4d77d7c51e8f1fdaba68f229ac04d191915845f09", - 18362382113291570192217962968958993778167022285180280072455374 + 18362361570655080300849714079315004638119732162003921272832000 ], [ "000000000000081baa3a716d5f9ab072c9fc3b798900234c9be23ab02a287c30", - 22401656061533210580918575951901358551917227873474367195418168 + 22401652017447755518156310839596703571934659990690572544245760 ], [ "00000000000005b88d0224b9b0d4b65d3de9a61d93609bb91c9297440f1c4657", - 22607630170339665188190152183146632918104515553204180801386220 + 22607619418140130980719672680045705126213018528712048676700160 ], [ "000000000000027d6a6870403fa43a650b7d9a6e61243f375a79ea935ad9ef1f", - 24717308784096979165831027254372357786209337057535982141051915 + 24717289559589094364468373797949472355802981654048927838633984 ], [ "0000000000000810a3490b86e4f302f6557f9621c5c8620c2b09ec8f0cf72794", - 23340837323532611728563455098354667083079032543420012677249737 + 23340814324747679919001773364939281849550099124416593832968192 ], [ "000000000000073833bca8d0ea909fde717e251576b7b3ccaaa58ad5d39eed60", - 23242403153239567181248045649858932694926499996163845297462125 + 23242391331131109072962566885467580392541369223033474166816768 ], [ "000000000000031b7fd2ed1f28ff74e969aa891297706c38bd2e1d3bc48183c4", - 21554570223215082708991040006621195807471559921461022664387024 + 21554562042243053719921017803645315870071034703425342074257408 ], [ "0000000000000b0738bcba382983811d40b531f2e68cd57126092755f1be4ba6", - 20615559358036706401988446678345142325284830029403352655769482 + 20615546854515052444405957679617344022137222968655050411343872 ], [ "000000000000000664cbfd5e3fa497c07614c33a0934b83e01fbe980634a9aa4", - 19540900118929245469513784022598005389554682908250308721002538 + 19540887421473929614259883543522244007742949396702043752628224 ], [ "000000000000021eb520df39289a70e40c59822a8c47924dc4940e7d0c1455c4", - 19588382633956678748738987427134971684150657954263472331193639 + 19588382523276445241758125434587686389961661359576757951266816 ], [ "0000000000000275e0c41b11bc250fe887c5e60c8ebaaa449f5c28c67133d496", - 18009312093826905807996061071987479121278814437031313100845126 + 18009299117968233362105684657812007807160912568078774269116416 ], [ "000000000000097fb0fdbeee0cee7e8f4e1a4ef8fad49f3d549624b0d47abed0", - 17993487382135493395314550202532083574115934981151443202421804 + 17993483763986497389087426516491816616385967180337839494660096 ], [ "000000000000053f199ae19d34365277e534f978ea2f6c69cd4757a4fc099af5", - 16574652347477707606538518827054821354422596190208356086094719 + 16574638092431222848464934504874974361824393751455373256032256 ], [ "0000000000000217b2e7b4f61682d24b9357d62ad29f27ed45ea2a32dc1f32f6", - 17085566110414426392074980811822124799183310889195548936089857 + 17085559845791583266730740536950670241169412424878408752693248 ], [ "000000000000039c1d77acd4702393f48ca61983c64fc0209ade141c694b2359", - 17870696125576904989516147458864032514115346444088781066283239 + 17870687961287995446644888885900316642120964851955511819501568 ], [ "0000000000000ae53f0c78330f6c2fbece2752909bc3742823e4fab29c5fd2b0", - 15554723035590620381978382489682684584827446061258013409024347 + 15554707140145502641228553657813466188995512591033787398225920 ], [ "00000000000004b4d72b8631a85ec7d226dc696f1913ba1bf735b7c8dec207b8", - 16944240402989056240270048857919858304172512515419325535711617 + 16944226977030767532657500340718760127019357828074148225613824 ], [ "00000000000006e06735bffb7d2f215dcadd8311fc33f4a46661fdca3dc0560e", - 17028766006301915583302001014128348187011555103613522799474256 + 17028747171100603034973679895960153979114298528140818252824576 ], [ "000000000000055fc0110d4a38ffb338eabc30c8b0aef355d4643d21b5b6a860", - 15614541816377627606833566623846498830327983334155710863946027 + 15614535766060906942258863525753414259523988166363835227176960 ], [ "000000000000081b69cb4de006c14084c4861f0e4a140c37200117a738733fe8", - 15392658582903619517884239396883829533752908215468116311928350 + 15392654931672180089790308609774483894682932641297604569726976 ], [ "00000000000009920770f2d40b5b6a8aba33d969b855c91b0f56e3db9c27e41a", - 14444739202621038642296525467957270513966223272539123613709315 + 14444739009842829731785903206212823051010663269705670545375232 ], [ "0000000000000791dd1cb7a684a54c72ccde51f459fff0fc3e6e051641b1e941", - 13237069982010980053565410157895773782534548540484990599728904 + 13237058963854547748734324548161076199478283141947127217782784 ], [ "000000000000019da474a1a598b5cf28534b7fd9b214eed0f36c67c203a9b449", - 12305441845260052457400411036992507599992679866354285875870526 + 12305424274651356593961118223415860240572779254789271782948864 ], [ "000000000000074333e888bac730f9772b65e4cc9d07edb122c6e3c6606bc8ab", - 11046102047853392984991332456419807063224677592114743703633836 + 11046080738989403765716562970384822165842244193743674858799104 ], [ "000000000000067080669115c445f378f3dec19787558d0e03263b9dec5d7720", - 10007086165511791816771124848728462094811571795311807624126594 + 10007073282210984973971337419529346944295676968729147521105920 ], [ "0000000000000304760bf583f4ac241c5ffe77312fa213634eba252c720530f1", - 9412804029559050886132126846183090289448911866201243978830721 + 9412783771427520201810837309176674245361798887059324066070528 ], [ "000000000000041fb61665c8a31b8b5c3ae8fe81903ea81530c979d5094e6f9d", - 8825807680257895657479991196220989276506275995152177228848553 + 8825801199382903987726989797449454220615414953524072026210304 ], [ "000000000000022fc7f2a5c87b2bab742d71c4eb662d572df33f18193d6abf0e", - 8774981337152660993121733114298631263731662998207194412401974 + 8774971387283464186072960143252932765613148614319486309236736 ], [ "000000000000013c6d43ba38bc5f24e699515b9d78602694112fefdc64606640", - 8158793708965770005321748925786317683564827171691288121295309 + 8158785580212107593904235970576336449063725988071903546310656 ], [ "00000000000001665176b9a810fddf27cca60dfcfd80bf113289fcc8ffed0284", - 8002813558257060656072356380146767001272597020026124199745768 + 8002789794116287035234223109988652176644807295346590313611264 ], [ "00000000000002dc6ef80f56a00f1091471d942ce9bfb656ebdab4ea0b77eb0b", - 7839578136187174365862370390163660393786299729896106652527867 + 7839560629067579481152758851432818444879208153964570478641152 ], [ "00000000000002a1fa5546ec48ca88b9e5710e2c6d895bb3675004fdacd6ab13", - 7999436853933517849738304697453936802516675338771116464559736 + 7999430563890709006856701613305138698914315019190763857641472 ], [ "00000000000000f517517c11e649b98feca7da84ae44fb643de5a86798fe3c31", - 9047933968943662429055854851798411859479270438104123361452456 + 9047927233058169382412882048952728634925849476849852060008448 ], [ "0000000000000299cab92a923348acf9251f656bcbacdb641fd0a66d895a6e8f", - 8296401729498848716200066027575181804609215798824798623774115 + 8296391419817537486273948666838217011279219811331013552898048 ], [ "000000000000027508b977f72c3a0f06f1f36e311ad079536630661880934501", - 9081043763417525999805054818818176389840193708186237826596038 + 9081029136740872581753422344739175313292014241889017867010048 ], [ "00000000000001925959229452cc6fbfef0104ebed7ccd6f584f2439c5dd1f1b", - 8230756222604082728916412296377630357556635887892965869189316 + 8230751570811169734692743946971314968326461977249645504495616 ], [ "00000000000003b34ca89509da5f558af468c194afaa8d458bbeb07c50cc7c74", - 7384132762576773456261468151764493698188252321818593178380086 + 7384127474250891166670391848516180960454656786677558849568768 ], [ "0000000000000076559e314ab0c86cc552e34fd79488415d3d17f6ea3c01adb3", - 6172235633712067451972497618887145940241016806561805162089236 + 6172230000534146257480611019445716458048957888854766248787968 ], [ "000000000000003a58043252cdc30ed2f37fb17e6ef1658324b1478f16c1463b", - 5561375174290806544537887055854541186367445945410171525594428 + 5561365017980676031428107027647386014985059524839404952616960 ], [ "000000000000011babf767e60240658195b693711c217d7da0d9215ccab45333", - 4026320687087602082485484360946232153393536063582206994825059 + 4026319404534786334009451711043898716884778820756489262596096 ], [ "000000000000027579d28fb480ccad8e2516d1219d4c1919e3fd4fc0c882955d", - 3513562835129894943437236119628516496362458327482173263945837 + 3513558656525386849113615662535622466519417660386833443323904 ], [ "0000000000000074546fe07f80ba15fc81897ec56a5535de727df9fda9dab500", - 3004086841873755151847218915251583968757589997419002536446958 + 3004083578955603829930099910053556479043735076695139267117056 ], [ "00000000000000b6c55833b80c07894f4c4d3bb686e5ddbc1b1d162e22752ca3", - 2675564091736135973597987074403776057837198839748912144832848 + 2675541054922611112919804040984964595022815308724929898217472 ], [ "00000000000001326f2f970753122e35bfdf3358d046ddf5ea22e57f5d82b00d", - 2409853811740497723006216754124060157774336072925654369402748 + 2409843108029446766213067266805752590003732794677225687351296 ], [ "00000000000000641084745613912464ff73c974bafd0bf6dd306295f019d306", - 2218270940716371747904935551989691447849649677886077648624174 + 2218268905456883731807407021635746739577921454491297946533888 ], [ "000000000000011ae105ddb1a5bbac6931a6578d95c201525f3a945276a64559", - 1727570438327407251342043828017904756815782584333725141104066 + 1727551573307299192250197436766000536509732237655131060961280 ], [ "00000000000000d9b66fee19af89eaaf3f3933d1acd2617924c107f0abbe0a41", - 1394050998377933499722472690026032322818492088393319462766728 + 1394031503757574068227953656553224448260418805016069352194048 ], [ "0000000000000011956d42670c2f75eeb344ac0657a806775998e2c58fa4b157", - 1263613033940095470462619539828531085609177044392029609988618 + 1263610003247723462826224891154624535497729630761756072607744 ], [ "00000000000000959b1ea990368fd16d494e68ee13bd7245ddd9cdfba3330100", - 1030471032625362817908252078771570487808270046919474202776261 + 1030450001678223668360152541055867895065240185756254103142400 ], [ "0000000000000091f86b1e423e24fe358c72db181cfcc2738c85f2f51871a960", - 862536742724199235179104073167840532858949484653681168904647 + 862513010327976103705811440432628413487564277790886242287616 ], [ "0000000000000055e146e473b49fe656a1f2f4b8c33e72b80acc18f84d9fcc26", - 720982725653754866133106184196823339064064188411714396293721 + 720982641204331278205950312227594303241470815982254303477760 ], [ "000000000000004f6a191a3261274735292bc30a1f79f23a143e4ee7dd2f64c1", - 530591605956209005375408931042036763612094286954585940489028 + 530591525189316709998942710962548491505413142398652303540224 ], [ "000000000000005327c8e714272803c60277333362e74ec88b9ffab5410c2358", - 410030655694725315191023225682702558843537088229871225194892 + 410030579894253754102159787320079652501746816512444002729984 ], [ "0000000000000002e2a62b8705564c38d6a746fc8e971a450a69989152b5ee97", - 310118507134852270764417655876559284597214440570539833833949 + 310118479516817784682897231521434079438159381558537557639168 ], [ "00000000000000202bf3ff30109538bfd9b5075c6438ab5ef64ebe2cf9b61404", - 239366804613626989118705458454015500681551595998816410136871 + 239366800071949252578530950352093786414793290792735831228416 ], [ "000000000000001c997105893f5991cb45765ff856b6e503f8466cb22cdd330a", - 181156300891423147840813581996669801683959668074714341556907 + 181156297885756721946540202079438048595571151633323613224960 ], [ "0000000000000010c13ce182a3d8fc6748b75640447eb360d7739a5fe984ffc1", - 142431143903518058663503832095902619444236806543928975891292 + 142431093377788751676361246670241704468765375727695350988800 ], [ "000000000000000bbb49db68b79ecc8393376d78272d237bb612288af64c1de8", - 100696286705944192804288311731154032278221074156374274573154 + 100696259189502783924473792493100546893980348528488767029248 ], [ "0000000000000001bbfd0973c367d30eef2416d9e94bdddea53bccf541a4858f", - 68962785458117760598328072539715155134139124175836033018875 + 68962778243821519216393853205209897734463141354237780295680 ], [ "0000000000000004ee5b6ace996ab746f1e6dd952cdbc74c0b4f8b9ac51c7335", - 52765647417137724306257751915372504293019655403366801103482 + 52765641310467331636297188681879886184148735229489015947264 ], [ "0000000000000002f2f23b515085d0c9f37a2824304ccb7ca1546a48548d0dac", - 44233494692117781485772218913793271750746093635349642503033 + 44233472386696495417387091608220539804351405166731810832384 ], [ "00000000000000045590c3fdeca1753d148a87614a70fa0897a17f90bb321654", - 38110303308616451367971130315102755539751527244002747835354 + 38110290672195532365762668664552282566878756832852091863040 ], [ "0000000000000002b704edc0bf1435fe2116040b547adb1bc2d196eb81779834", - 29679712134953944285822600537404275892101515173751373902643 + 29679649578007061283718812081441644170496168236939550392320 ], [ "00000000000000038cc59dc6dd68ae0fbe2ded8a3de65dbd9a2f9a36d26772df", - 22829284162675848134182694598477416531051323480214451851537 + 22829202948393929850749706076701368331072452018388575715328 ], [ "0000000000000000a979bc50075e7cdf0da5274f7314910b2d798b1aeaf6543f", - 19005972021752888554737867279515830726136655207276613952446 + 19005913916847449503306572434028937600915626422125897711616 ], [ "0000000000000001dd8e548c8cf5b77cde6e5631cd542e39f42c41952e5e7085", - 15065030752662243106668159124876133476723125447787423397009 + 15065005852539512185984435657022720640916062598235628240896 ], [ "0000000000000002513542a461de351a5a94f96b4bcd3e324a48d2d71b403fe0", - 12288777851891587151373320769563000373599628572350950946294 + 12288698618318346282960995223961541766142764336009759948800 ], [ "000000000000000150cc07163e78d599a7e56c0d1040641bffb382705ac17df0", - 10284450072667651845630380921900049634274231900711580829901 + 10284386012808371892335572105827331142617405906583881252864 ], [ "00000000000000009051d83d276dad5c547612f67c2907acf6a143039bddb1bb", - 8614457133517962240383077577277860009688882364333357498735 + 8614444778121073626993210829679478604092861119379437256704 ], [ "00000000000000000b83d3947d2790ab0bcbbb61eba1eb8d8f0f0eb3e9d461e0", - 7065404376960081064548050202734411051432779994036264291865 + 7065379129219572345353864175298106702426244380437224882176 ], [ "00000000000000005a4fbbaeffee6d52fa329dd8c559f90c9b30264c46ad33fd", - 6343128691613752139911564815777925738673759990853012864417 + 6343094824615218102798845742064326605321937397913065881600 ], [ "00000000000000006b6834bae83e895a78c5026a8c8141388040d90506cf3148", - 5384566985902468539838947745491317290501351277582100625895 + 5384518863803604621895699676581808210968416076987222720512 ], [ "0000000000000000bf3c066c9acdb008e7fff3672f1391b35c8877b76b9e295e", - 4405445424268587912774001698765643657938467054813941696357 + 4405349994161605759458363322921957536960017949107037405184 ], [ "00000000000000006bcf448b771c8f4db4e2ca653474e3b29504ec08422b3fba", - 3863116091606416844204395924633339211949472882692642434091 + 3863038134637689339706803268689141874606936642244315185152 ], [ "000000000000000098686ab04cc22fec77e4fa2d76d5a3cc0eb8cbf4ed800cdc", - 3369644874471976788888364569461031006144821186115339704344 + 3369574570478873127315415525946742317481702644901195284480 ], [ "000000000000000036cc637d80982595b1fa30f877efe8904965e6fd70aeae1a", - 3045099804940836864917455634208357232827311736852711219052 + 3045099693687311168583241534842989903432036285033490677760 ], [ "00000000000000000ee9b585e0a707347d7c80f3a905f48fa32d448917335366", - 2578448738892556035161639572550297683334908085589209042124 + 2578448441038522347123624842639328775756428679710156783616 ], [ "00000000000000000401800189014bad6a3ca1af029e19b362d6ef3c5425a8dc", - 2293150027595934059742111263510686973492486336734191444857 + 2293149852232440455888971398133692017055281498246925516800 ], [ "00000000000000001b44d4645ac00773be676f3de8a8bff1a5fdd1fb04d2b3b2", - 2002553394643609738890838973561169711471353898661293921361 + 2002553378451099534811946324256852041059202347552707969024 ], [ "00000000000000003ff2a53152ee98910d7383c0177459ad258c4b2d2c4d4610", - 1602973121906621623499825176001242504910089450561449296745 + 1602972750958019380418919163663316163747908621623690788864 ], [ "00000000000000001bb242c9463b511b9e6a99a6d48bd783acb070ca27861c2b", - 1555090301026128543569302441423333574769288057539276771351 + 1555090122338762644529309082074529684497336694348804259840 ], [ "000000000000000019d43247356b848a7ef8b1c786d8c833b76e382608cb59e9", - 1438882618901096676077751337424466243540231648216042671672 + 1438882362326364789097016808333128944459434864174551793664 ], [ "00000000000000003711b624fbde8c77d4c7e25334cfa8bc176b7248ca67b24b", - 1366448148696423482270218240630565379904190231445288559686 + 1366448002777625511026173062127977611952455397852592472064 ], [ "0000000000000000092c1f996e0b6d07fd0e73dfe6409a5c2adc1206e997c3a2", - 1130631792721554272454999472203133803635779505498977249380 + 1130631509982695295834811811892052032638591596239280668672 ], [ "000000000000000020ce180d66df9d3c28aee9fcec7896071ec67091a9753283", - 982897902661444504749094486748895114762769275663213548760 + 982897592923314645728937741958820396011314229953349812224 ], [ "000000000000000018d37d53ae02e13634eefb8d9246253e99c1bdf65ac293ea", - 903780674822307262725136466127288858430591999464421319774 + 903780639904017349860452775965599807564731663176966340608 ], [ "00000000000000001607d1a21507dea1c0e5f398daf94d35fb7e0a3238f96a0f", - 777796786715545142990933608995805126717575855757223448283 + 777796486219054632155478957346406689849105796561635377152 ], [ "00000000000000001acae244523061f650ddab9c3271d13c0cd86071ae6e8a5f", - 770217857427240993023051315984564139215374347389780685886 + 770217816864616291160628694313702426464491250746461782016 ], [ "0000000000000000104430189dba1219b0e3dd90824e8c2271609aca5b71250f", - 749175002550855564826315453191856424408132088739667533908 + 749174812297985386116525053725808178560617045558724395008 ], [ "00000000000000001aa260733b6d8f8faa2092af35e55973278bb17f8eaeca6b", - 680733332917879088904702563202563546480869669564659182916 + 680733321990486529407107157001552378184394215934016880640 ], [ "000000000000000009925ad5866a9cb3a1d83d9399137bccc7b5470b38b1db2b", - 668970749931191589798031473561994304229010598616526068121 + 668970595596618687654683311252875969389523722950049529856 ], [ "00000000000000001133acacb92e43e24af63a487923361a4a98c87a5550dffe", - 673862885517789065391946314370719009092913047398806257816 + 673862533877092685902494685124943911912916060357898797056 ], [ "000000000000000018c66b4a76ca69204e24ee069da9368c7a9883adb36c24af", - 683252375980679323816587400004061743952674823748550569728 + 683252062220249508849116041812776958610205092831121375232 ], [ "000000000000000010b13aed220b96c35ccd5f07125b51308db976eefcd718f9", - 663358898259210531333699235628449595078182768956016850932 + 663358803453687177159928221638562617962497973903752691712 ], [ "0000000000000000031b14ece1cfda0e23774e473cd2676834f73155e4f46a2b", - 613111677421249032126095464155766633549817788831841702233 + 613111582105360026820898034285227810088764320248934432768 ], [ "000000000000000010bfa427c8d305d861ab5ee4776d87d6d911f5fb3045c754", - 653202571346946874804858789924935228771775905822751784751 + 653202279051259096361833571150520065936493508031976308736 ], [ "000000000000000005d1e9e192a43a19e2fbd933ffb27df2623187ad5ce10adc", - 606440210473080582646260971729051700700295823810315465086 + 606439838822957553646521558653356639834299145437709336576 ], [ "00000000000000000f9e30784bd647e91f6923263a674c9c5c18084fe79a41f8", - 577485545195557219124205162278233745767078209386685370301 + 577485176368838834686684127480472050622611986764206702592 ], [ "00000000000000000036d3e1c36e4b959a3e4ad6376ce9ae65961e60350c86e8", - 568436189899844976161013318161470010900802307864463999350 + 568436119447114618883887501211268589217582000336195813376 ], [ "00000000000000000b3ec9df7aebc319bb12491ba651337f9b3541e78446eca8", - 577075446183156083131210077122535091982277790261940376730 + 577075114085443079269506210404847846798089003835028668416 ], [ "000000000000000012d24ce222e3c81d4c148f2bce88f752c0dba184c3bc6844", - 545227685810993878908530774661151072647124692119579479626 + 545227566982404669720599751103563308707559049533419683840 ], [ "000000000000000000c4ccbdd98c267bd16bda12b63b648c47af3ac51c1cc574", - 566251462633192796874293710752184671013063323002614261298 + 566251116039239425785056264238964437451875594947144974336 ], [ "00000000000000000056bfec1dca8e82710f411af64b1d3b04a2d2364a81993f", - 565861163013726292152715860908846169118213713027013549266 + 565860883410058976058672534759150528155363303710710038528 ], [ "00000000000000001275d1cadce690546f74f77f6d4a6190e2137a8a819946f6", - 552365082628398268882484833076555675653086455208105645421 + 552364745922238091561919045022000637317595931246011088896 ], [ "000000000000000003816ae80c6413b84cbee2f639ba497ab5872ec9711eb256", - 566500826506537696689556913703962485638366020240431987761 + 566500670366816952120145379831520408210047884740723212288 ], [ "00000000000000000d92953224570f521b09553194da1ca3c4b31a09a238f4f6", - 542528831070582225190358970054175523872885764221168055524 + 542528489142608155505707877213460200687386787807972294656 ], [ "000000000000000006721943f23cfacf20c17c2ad6ea4e902af36b01f92e3c06", - 545717458684443426657861963694104795617022469075593560376 + 545717322027080804612101478705745866012577831152301113344 ], [ "0000000000000000031d9af2fe38cc02410361fb213181fdb667c74e210d54c4", - 527828116295419256939747768525818422990809696098687485908 + 527827980769521817826567786138322798799309668948178370560 ], [ "0000000000000000142e8a13ef6994961655c8e86aece3f0abebd2ee05473e75", - 515692649961651115318501607126660466594771968970128733915 + 515692606534173891771672037645739723025219384908133171200 ], [ "00000000000000000c7a8db37a746d6637ef6a6eab28735608fd715ee2f394e7", - 511567833081612605062932845380344111401319750691048028647 + 511567664312971151375333957573881285830542480898837708800 ], [ "000000000000000007854877c66c71a49af40d20f2d6f817becfe4d66d5e5a81", - 496889275651173623472900330204902534352929519684753746862 + 496889230460615059653870414954457230681194245244172894208 ], [ "000000000000000005ce1d2d10aeb9def4d38233e859d98a4a168ea3fa36687a", - 473326016878892721329791660926511941983191613711888666872 + 473325989086544548323169648982069700877697035484407005184 ], [ "000000000000000007c71decfe74855ad99dc2aa4a2e713165db5a8d6da5f32a", - 454358905739145490120646206475613103265889121292141221496 + 454358737757395076722955683517864397151243915416267915264 ], [ "000000000000000008ce4f34161be6760569877c685e37ebebce3546ea42a767", - 443317174350997401226699663083830316501226707336190868827 + 443316987659242217350916733941384923365365929826941140992 ], [ "0000000000000000086233f4843682eb47bacb58930a5577fbfd5c9ebd57ddf9", - 442803156296231091698861521258691618419467911445974398697 + 442802913227320896234856097023585967110900073490544590848 ], [ "000000000000000010a904eee4fc763c6b88d378884f368fd652f63c1af71580", - 433057295538880306866830023102486508102611067408810729986 + 433057199397126884276233483897801969646324654385408245760 ], [ "00000000000000000c114754749d622d4fa2f78c84d7147c345b2b99a8e83d2e", - 409419135913169127551416754586994781281659818649795994250 + 409419129139225030716120689261979366152221060879441985536 ], [ "000000000000000000a5039e32cc9a89aeffbde1391e8bc9ae9724127904f01d", - 370716565562591807409073645534324134138902968133741824826 + 370716507988397359530778284103407727265240291588416995328 ], [ "000000000000000003b0b73d9b3259c318cca48a6335b5d64545583f7f3773fa", - 340818601652590375722654926010534269909167221015231774473 + 340818253309165415058055171484606858815006633875327680512 ], [ "00000000000000000198bcc5bd65fd0ccd1c7e3b49e0170ea80296cbfee05042", - 288495776454828940814130957501183806179235220269688957284 + 288495652867775987986282369150900282132304927019642126336 ], [ "00000000000000000a60f379d3dc1413491f360809a97cbb02c81442c613dce7", - 259524927038954052049842432960406271327041356520946780931 + 259524902203633530447121351815377152077137395840706412544 ], [ "0000000000000000038973a5f8ba8cdc7e371dcc8f4b24337ef695f24b962907", - 237834533496394499560421837048697627284447080833665891069 + 237834253647442358407456603145452341381064939329604812800 ], [ "000000000000000004b8ec471974913d052a3af7dc2a8c6f01c2ac2f3d1f7b19", - 224600594221399775791208366807237501899705336368643295004 + 224600391397450328424792273873642383828872941895338164224 ], [ "0000000000000000075d572eef1c4210adc7abf4e40986d7f0a80003853bfec4", - 187068024570118295326670137055767916260683809649859998591 + 187067719845325692996306936867878122094522982476155977728 ], [ "0000000000000000074f9edbfc07648dc74392ba8248f0983ffea63431b3bc20", - 164898586657174446766450284432249324933473312757247241703 + 164898540577033087399552264895286015147022701908103004160 ], [ "000000000000000003c4a4d9c62b3a7f4893afe14eef8a6a377229d23ad4b1ea", - 170169949941312779383320359289276524103458774855674537695 + 170169861298531990750482624090969781281789404909188153344 ], [ "00000000000000000404b6939e6c35a5448386e5d58f318c82ce2fefb7d73e47", - 162900642628594452312926252009782198966469183066378413701 + 162900609378736249874251099581569547607832255884553093120 ], [ "0000000000000000034656c96781091b5fbc799c881ea85b41cba0b88128eff7", - 161578253985639514393501040432436419806938319938347383115 + 161578008857017275969393492955354620126364423170461532160 ], [ "0000000000000000045645e2acd740a88d2b3a09369e9f0f80d5376e4b6c5189", - 150883217088565412406283744917586302541065882485692466643 + 150883090635422687830679296233896712896447026244773478400 ], [ "00000000000000000381e6a138308c6547d6fe3eb3437250ffefdebbf71eefd1", - 150899431314054665651533974629900879951167127567886958331 + 150899178845446426410002882396535253739927398750206558208 ], [ "0000000000000000012100ddbb2102e65fb1ebbf104ead754a4110abffc4b8bc", - 138784704342716220538434620238263807017514526920482840730 + 138784382553152119468195441786396823230753870240366460928 ], [ "0000000000000000046f56e59b9b1293b5e7c1587aa6d29c4f3f79b98cf22ee6", - 135263027158857483473983812897618462696878980167989570177 + 135262935280049154152065372885142255350817451144176992256 ], [ "000000000000000001bd1c291e91f4476f93454d4542d2ed7e44fc86902c93bb", - 137505575960473580232190762314053902119220761315057010096 + 137505556928474480767543871928291413858290772017802117120 ], [ "000000000000000001c37a483375ff6fd6ed7c5b79d80167b027a8fdb0721dcd", - 128714000003724620550017796842876174875520737762229396938 + 128713911367130082233924624261304605948946745676720504832 ], [ "0000000000000000051804b4c2da5298c4573386bf1d4242bf0e26a49ec32e42", - 126334257597368896694079008874105899845411447996852366067 + 126333978716874242627475052620752087219210710628817698816 ], [ "0000000000000000034bff7888f1f7294311f0199322f77c1457018c875bd9e1", - 126278728489740292169183109579386034099056145098127681816 + 126278605342839049377710151409810132688161986656629424128 ], [ "00000000000000000506b43c9283ccbc40f583e0c734e4a8af2ce6a4262c6221", - 133533674521328301805375468020445677637867523414815983180 + 133533639774706835230353390473157702360903922769486413824 ], [ "000000000000000003937068e19a0750a33978050f019d2b60f430e3da707db9", - 124023231761354306172598997090326962528984683316222123922 + 124022888639743237872084547350559836284832548627419234304 ], [ "000000000000000002e2f6ec3c9eb965aa706c788da7dede201b6b4b8fae3971", - 122123890689597169329897975011373560881532793639713851004 + 122123731568103772089607259872577666017242529148853813248 ], [ "000000000000000000b3076636b13562bb4315f895bcb324e0c962763c2196b1", - 119378471659813172166584350643745606396975629669615648535 + 119378259820331825692479928211144812308894309500762193920 ], [ "00000000000000000025b8961d1d0cfba33b0205ec10b3ce541618e352b0bbd5", - 111760099061575845238587552104542233599456594020708180600 + 111759931157462873316041289986819959868258380300102402048 ], [ "00000000000000000421d58b78b9f063a4b20e181d55c9c79082f9e4b8b30925", - 104283398725864083874296861096497976441886465506877958948 + 104283029085035157753191385936387396702868516379761311744 ], [ "0000000000000000027fd968d41741f31c73c4a3b304472da0165245278e2ea3", - 106299891835047816880570816560226555729378855394467112113 + 106299667504289830835845558415962632664710558339861315584 ], [ "00000000000000000364a23184b8a2c009d13172094421c22e4d9bc85dcf90a5", - 105881534387569087602448606393026827269357803018613746024 + 105881374043672627773432318187360570734220873198601240576 ], [ "0000000000000000042a2ed4a504424060407825d774a54f2e148fa769ee72ff", - 95668758377605096786059344838386233938948428360571473100 + 95668727978371040303278646201741713440261619517174579200 ], [ "0000000000000000025f769f13f2806fed19d9948b1a7ef19048177789afc5d3", - 94012478943487551583874745631213709785208280748731165788 + 94012390634764280055243391736606357298689315295029362688 ], [ "000000000000000000b3ff31d54e9e83515ee18360c7dc59e30697d083c745ff", - 86923144448447518913809103136679872784564523201770836515 + 86923102180582917240747796162767475850640519180006195200 ], [ "0000000000000000021ecdcb2368ce66c23efd8bd8ab6a88a8bb70571c6e67f0", - 84861696667064232085350895302379622169877065200841464945 + 84861566431029438820446406485131195674434646972185968640 ], [ "000000000000000001972cb33b862b27c1dc3f3a723f7d1cfd69aebe0409126c", - 80022436630974307725804284020086214397285337936510125904 + 80022382513656536844370512820784980102919810105407963136 ], [ "000000000000000000cb26d2b1018d80670ccc41d89c7da92175bd6b00f27a3e", - 68605895635350324123887563889758158648405285708846995220 + 68605739707508652902977299640495787127103841947617329152 ], [ "00000000000000000276deb4022f66cacd929c690cd6b4f7e740836b614b21f4", - 63859488458993656960329361157926368758742149072401957675 + 63859343606086615291372321518809062931940920926127783936 ], [ "000000000000000000587912ced677698c86eec8b1d70144dccb1c6b0bad0f17", - 61163588147080336562860372542789363550797760125590468374 + 61163258921643354765656928775243357859392914550528409600 ], [ "0000000000000000009f989a246ac4221ebdced8ccebae9b8d5c83b69bb5e7c8", - 58509968837817799412963215131374851975666125194369450244 + 58509826700983959310706392369835644790490546910263246848 ], [ "000000000000000000038bed8b89c4e82c13076dd64dc5f7a349c39d3921d607", - 56672978024443644437306289406994921596646228103740151166 + 56672777602924507578641088682504585686103825941044133888 ], [ "00000000000000000122f47d580700a3a5b4b6cb46669a36e4fa974c720ab6cd", - 53958706289281806789111061412993899806784528297928389354 + 53958359841942568206719748916397287559357255547625668608 ], [ "00000000000000000172ad9ea56a90bdfed0f364a902500e9ff4d74f000ced99", - 51765097045688608012424287693701763884232488530834902033 + 51764751112426770751506128647798102319231116027761786880 ], [ "00000000000000000201d7429db233c7055e9699c5bfb57b167ca8d0c710dc71", - 51649247587912518226490987244672765779747315777961084943 + 51649140486907347007064544362790913467244253139882213376 ], [ "000000000000000000c0549b2a8adbefbf6c909f61fdc4d6087c44a549cf8201", - 48144761676638685568393252844604229390549310101321306353 + 48144529712666433692552181910809237167694270386587828224 ], [ "0000000000000000015b6789cdc5dc13766f58b38f16d5b35bf79ce4b040f7fd", - 45240056525891956455575817517143990421796325617308336169 + 45240046586752885057924289339576851866807485277820420096 ], [ "0000000000000000013a31b29f845d97465bff53f901027f8ab4b1a2f59118a8", - 39719085345888042233262788103506269388987831055953076236 + 39718797393257298660757754408019939605415460564426031104 ], [ "00000000000000000088cdeaa7389a7de9f09e3a28b3647630fea3bd1b107134", - 37880653743061241847157755785329340895782894371522587986 + 37880625861940376795251270290737354395669643839013912576 ], [ "000000000000000001389446206ebcd378c32cd00b4920a8a1ba7b540ca7d699", - 38043253251243498799796359449649225329347481521269202959 + 38043004539854389433075372490391464304285496568268718080 ], [ "000000000000000000f41e2b7f056b6edef47477d0d0f5833d5d4a047151f2dc", - 33510049713200839962002052974605137446441531580345905745 + 33509870757351677175294676059494700127350769223450230784 ], [ "0000000000000000010e0373719b7538e713e47d8d7189826dce4264d85a79b8", - 31340511093499215382498875631096178729473407545556119324 + 31340207270661909233492904963194738468218672502370467840 ], [ "00000000000000000053e2d10bd703ad5b7787614965711d6170b69b133aa366", - 29201554221106481014362444600779904393001928219662824381 + 29201223626342991605750065618903157022235193117232857088 ], [ "000000000000000000cbeff0b533f8e1189cf09dfbebf57a8ebe349362811b80", - 30354232589320643409720162249214362116926806095467115096 + 30353962581764818649842367179120467226026534727449575424 ], [ "000000000000000000d0ad638ad61e7c4c3113618b8b26b2044347c00c042278", - 29217445580005453044145144287633722880237231025559536344 + 29217311836366730185073651781541697865715565622665936896 ], [ "000000000000000000a7bda943639876a2d7a8caf4cac45678fb237d59c28ba1", - 24433315186493117547015353728839494165411420867297244659 + 24433127148609864747615599184820261456796420809345204224 ], [ "000000000000000000fb6c6a307c8363e923873499ba6299597769c10a438e61", - 23988337581966024451862874735374376736823985966238572778 + 23988269434232535193761088780698748366141469438183997440 ], [ "0000000000000000006f408147ffbcaa0fb1dcf1f199c527ffdaf159d86e5cd9", - 22526603255015707503680924025827203599625190615869254262 + 22526487188587264742197108840494583820145762956159746048 ], [ "000000000000000000e3be3cf7343d7792c0d47d3c39ddb9ceaf19961e9eeab4", - 18556473167918062248854389700869820348727762534776424137 + 18556440756915402760741928101946749165024073301499052032 ], [ "000000000000000000b3fb09d6def197657e20f9c1d5e9680cfcac1e1f9aa269", - 19759157687224108664379003516351943599373215433413919905 + 19758940920085072387393228723348383373068660102939017216 ], [ "000000000000000000bfe71f044145e1b42fdfb3a523ee2a215e80fa6afc2a98", - 20014601621424565995143800336070874732337755340431658220 + 20014481558369106100835306608979160026489460596213284864 ], [ "000000000000000000cee3bff56ee49c0f96d1cbd17fa17dc6f84b3f48aed765", - 16946223147907286639275870228581142863500004051737247938 + 16946123176864917983795071264823963343174695083267063808 ], [ "00000000000000000089ef13654974b8896b0b0909dd9ae8e350b8a8a7807ce3", - 14393235111671584691995228944147692615427239344048996539 + 14392961660539521116256653268419249019684881662910398464 ], [ "0000000000000000003105a067417c318dab31e25ae1583fa2b27be226945fdd", - 13960554065678404881662765388314788446457906960835196843 + 13960450711994363030255127593764523087979983609872252928 ], [ "000000000000000000720da39f66f29337b9a29223e1ce05fd5ee57bb72a9223", - 12101157814506873037325199894442204023473205005719474983 + 12101157559014734955774763823279522156034099347349045248 ], [ "0000000000000000006a8957cbd52c2038861514f106f7f9f76392d5cb83fd4c", - 10356794104728254122144804026855362068260936623802026210 + 10356793971791534424976101420669664288187918308140384256 ], [ "0000000000000000006b68e55432541794388c94fe9e805652038e7b3cac0681", - 9378292707998313412116380171862644964293497276319204201 + 9378292318569022964986206758839123913433917663832178688 ], [ "00000000000000000001c9deea9f0302eadb1250df1ad53da802dfb40d47face", - 8964448809071454563198782190249047923745291487292520783 + 8964447668935855171055978546867850348456065181232922624 ], [ "00000000000000000013aaa8778111530a626a3fe57e4e6f4a878c92669b04d1", - 8192879673885498125868612536191253209089530007606798637 + 8192878571041388924351625416816775770172128369752145920 ], [ "0000000000000000002f67aa98789b98304a32e54bffbb34c8693eb0acac4c30", - 7786052299140735828822313223500239047404586439857310526 + 7786052052270684126234611299412205796254663675224260608 ], [ "0000000000000000002e5f072398ee27b25b6cdcf69051bcdbbece417093c979", - 7678459575374433725135064732059630548334362167840519275 + 7678459224733657715202292429397298472913633233275453440 ], [ "00000000000000000028d7447c20ade2053bbaf49e8a16eb5fb1bc74335d0d18", - 7021962387297538215162191668190420818507007046641761211 + 7021961458254440109762706424650140438182306270565892096 ], [ "00000000000000000042d89446b9043387be2d4c09aa9e9524176c5754616510", - 6702919661112618417011103564460059073928318846609410368 + 6702918573828378664524678433037841287557455508299317248 ], [ "00000000000000000018ec4d369bab2c13174834a02138decea7c85685d46bd6", - 6505870722004004229230504292495188676492429416682571694 + 6505870154073602347674948421782035713149324747260035072 ], [ "0000000000000000000d4a6c2237c6c46b963b17f60d9c850c4915518deb6678", - 6259544227059496163548942066110099807402951752241008049 + 6259542822111302646229226565336702507884435252736688128 ], [ "00000000000000000031adb986da21237ce06b57ae5390b7f0f890ab8e21b66a", - 5456617768214852996771856909727813546477083066109605574 + 5456617206587901877414813377199700077413780408546361344 ], [ "000000000000000000031df41201cd3789559333cd9529f99834a805014c9b13", - 5309609731779463925710783996961452866664143937023784605 + 5309609141393698345581459330931267317315649121846034432 ], [ "00000000000000000020c68bfc8de14bc9dd2d6cf45161a67e0c6455cf28cfd8", - 5026315858682664529586803062495716614500737280720290905 + 5026314587016750785722693470327208449351582469580652544 ], [ "00000000000000000009dce52e227d46a6bdf38a8c1f2e88c6044893289c2bf0", - 5205879841852030921059527756813029926469497427631238471 + 5205879062684137510961952799929229129995569309608312832 ], [ "0000000000000000002eca92f4e44dcf144115851689ace0ff4ce271792f16fe", - 4531443141490318780034113235264455891508287828291522257 + 4531442825108320403104334767545311437480985430866264064 ], [ "00000000000000000000943de85f4495f053ff55f27d135edc61c27990c2eec5", - 4219471567912784283817862725463546097477954228203549938 + 4219470685603665866184576203153693664105230070242607104 ], [ "0000000000000000001d9d48d93793aaa85b5f6d17c176d4ef905c7e7112b1cf", - 4007527398636149719774488173266778882653753905718962657 + 4007526641161212986792514236082843733160766044725313536 ] ] \ No newline at end of file From 141ff99580192c920bc6bb7f6bbc9d35449daea8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 20 Nov 2018 18:57:16 +0100 Subject: [PATCH 130/301] blockchain.py: generalise fork ids to get rid of conflicts --- electrum/blockchain.py | 248 ++++++++++++++++++++---------- electrum/gui/kivy/main_window.py | 8 +- electrum/gui/qt/network_dialog.py | 21 +-- electrum/interface.py | 65 ++------ electrum/network.py | 38 ++--- electrum/storage.py | 2 +- electrum/tests/test_blockchain.py | 239 ++++++++++++++++++++++++++++ electrum/tests/test_network.py | 20 ++- electrum/tests/test_wallet.py | 1 - 9 files changed, 472 insertions(+), 170 deletions(-) create mode 100644 electrum/tests/test_blockchain.py diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 5609c41e..c7c07531 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -79,26 +79,67 @@ def hash_raw_header(header: str) -> str: return hash_encode(sha256d(bfh(header))) -blockchains = {} # type: Dict[int, Blockchain] -blockchains_lock = threading.Lock() +# key: blockhash hex at forkpoint +# the chain at some key is the best chain that includes the given hash +blockchains = {} # type: Dict[str, Blockchain] +blockchains_lock = threading.RLock() -def read_blockchains(config: 'SimpleConfig') -> Dict[int, 'Blockchain']: - blockchains[0] = Blockchain(config, 0, None) +def read_blockchains(config: 'SimpleConfig'): + blockchains[constants.net.GENESIS] = Blockchain(config=config, + forkpoint=0, + parent=None, + forkpoint_hash=constants.net.GENESIS, + prev_hash=None) fdir = os.path.join(util.get_headers_dir(config), 'forks') util.make_dir(fdir) - l = filter(lambda x: x.startswith('fork_'), os.listdir(fdir)) - l = sorted(l, key = lambda x: int(x.split('_')[1])) - for filename in l: - forkpoint = int(filename.split('_')[2]) - parent_id = int(filename.split('_')[1]) - b = Blockchain(config, forkpoint, parent_id) - h = b.read_header(b.forkpoint) - if b.parent().can_connect(h, check_height=False): - blockchains[b.forkpoint] = b + # files are named as: fork2_{forkpoint}_{prev_hash}_{first_hash} + l = filter(lambda x: x.startswith('fork2_') and '.' not in x, os.listdir(fdir)) + l = sorted(l, key=lambda x: int(x.split('_')[1])) # sort by forkpoint + + def delete_chain(filename, reason): + util.print_error("[blockchain]", reason, filename) + os.unlink(os.path.join(fdir, filename)) + + def instantiate_chain(filename): + __, forkpoint, prev_hash, first_hash = filename.split('_') + forkpoint = int(forkpoint) + prev_hash = (64-len(prev_hash)) * "0" + prev_hash # left-pad with zeroes + first_hash = (64-len(first_hash)) * "0" + first_hash + # forks below the max checkpoint are not allowed + if forkpoint <= constants.net.max_checkpoint(): + delete_chain(filename, "deleting fork below max checkpoint") + return + # find parent (sorting by forkpoint guarantees it's already instantiated) + for parent in blockchains.values(): + if parent.check_hash(forkpoint - 1, prev_hash): + break else: - util.print_error("cannot connect", filename) - return blockchains + delete_chain(filename, "cannot find parent for chain") + return + b = Blockchain(config=config, + forkpoint=forkpoint, + parent=parent, + forkpoint_hash=first_hash, + prev_hash=prev_hash) + # consistency checks + h = b.read_header(b.forkpoint) + if first_hash != hash_header(h): + delete_chain(filename, "incorrect first hash for chain") + return + if not b.parent.can_connect(h, check_height=False): + delete_chain(filename, "cannot connect chain to parent") + return + chain_id = b.get_id() + assert first_hash == chain_id, (first_hash, chain_id) + blockchains[chain_id] = b + + for filename in l: + instantiate_chain(filename) + + +def get_best_chain() -> 'Blockchain': + return blockchains[constants.net.GENESIS] class Blockchain(util.PrintError): @@ -106,15 +147,20 @@ class Blockchain(util.PrintError): Manages blockchain headers and their verification """ - def __init__(self, config: SimpleConfig, forkpoint: int, parent_id: Optional[int]): + def __init__(self, config: SimpleConfig, forkpoint: int, parent: Optional['Blockchain'], + forkpoint_hash: str, prev_hash: Optional[str]): + assert isinstance(forkpoint_hash, str) and len(forkpoint_hash) == 64, forkpoint_hash + assert (prev_hash is None) or (isinstance(prev_hash, str) and len(prev_hash) == 64), prev_hash + # assert (parent is None) == (forkpoint == 0) + if 0 < forkpoint <= constants.net.max_checkpoint(): + raise Exception(f"cannot fork below max checkpoint. forkpoint: {forkpoint}") self.config = config - self.forkpoint = forkpoint - self.checkpoints = constants.net.CHECKPOINTS - self.parent_id = parent_id - assert parent_id != forkpoint + self.forkpoint = forkpoint # height of first header + self.parent = parent + self._forkpoint_hash = forkpoint_hash # blockhash at forkpoint. "first hash" + self._prev_hash = prev_hash # blockhash immediately before forkpoint self.lock = threading.RLock() - with self.lock: - self.update_size() + self.update_size() def with_lock(func): def func_wrapper(self, *args, **kwargs): @@ -122,12 +168,13 @@ class Blockchain(util.PrintError): return func(self, *args, **kwargs) return func_wrapper - def parent(self) -> 'Blockchain': - return blockchains[self.parent_id] + @property + def checkpoints(self): + return constants.net.CHECKPOINTS def get_max_child(self) -> Optional[int]: with blockchains_lock: chains = list(blockchains.values()) - children = list(filter(lambda y: y.parent_id==self.forkpoint, chains)) + children = list(filter(lambda y: y.parent==self, chains)) return max([x.forkpoint for x in children]) if children else None def get_max_forkpoint(self) -> int: @@ -137,11 +184,12 @@ class Blockchain(util.PrintError): mc = self.get_max_child() return mc if mc is not None else self.forkpoint + @with_lock def get_branch_size(self) -> int: return self.height() - self.get_max_forkpoint() + 1 def get_name(self) -> str: - return self.get_hash(self.get_max_forkpoint()).lstrip('00')[0:10] + return self.get_hash(self.get_max_forkpoint()).lstrip('0')[0:10] def check_header(self, header: dict) -> bool: header_hash = hash_header(header) @@ -159,24 +207,38 @@ class Blockchain(util.PrintError): return False def fork(parent, header: dict) -> 'Blockchain': + if not parent.can_connect(header, check_height=False): + raise Exception("forking header does not connect to parent chain") forkpoint = header.get('block_height') - self = Blockchain(parent.config, forkpoint, parent.forkpoint) + self = Blockchain(config=parent.config, + forkpoint=forkpoint, + parent=parent, + forkpoint_hash=hash_header(header), + prev_hash=parent.get_hash(forkpoint-1)) open(self.path(), 'w+').close() self.save_header(header) + # put into global dict + chain_id = self.get_id() + with blockchains_lock: + assert chain_id not in blockchains, (chain_id, list(blockchains)) + blockchains[chain_id] = self return self + @with_lock def height(self) -> int: return self.forkpoint + self.size() - 1 + @with_lock def size(self) -> int: - with self.lock: - return self._size + return self._size + @with_lock def update_size(self) -> None: p = self.path() self._size = os.path.getsize(p)//HEADER_SIZE if os.path.exists(p) else 0 - def verify_header(self, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None: + @classmethod + def verify_header(cls, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None: _hash = hash_header(header) if expected_header_hash and expected_header_hash != _hash: raise Exception("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash)) @@ -184,7 +246,7 @@ class Blockchain(util.PrintError): raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))) if constants.net.TESTNET: return - bits = self.target_to_bits(target) + bits = cls.target_to_bits(target) if bits != header.get('bits'): raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits'))) if int('0x' + _hash, 16) > target: @@ -206,21 +268,26 @@ class Blockchain(util.PrintError): self.verify_header(header, prev_hash, target, expected_header_hash) prev_hash = hash_header(header) + @with_lock def path(self): d = util.get_headers_dir(self.config) - if self.parent_id is None: + if self.parent is None: filename = 'blockchain_headers' else: - basename = 'fork_%d_%d' % (self.parent_id, self.forkpoint) + assert self.forkpoint > 0, self.forkpoint + prev_hash = self._prev_hash.lstrip('0') + first_hash = self._forkpoint_hash.lstrip('0') + basename = f'fork2_{self.forkpoint}_{prev_hash}_{first_hash}' filename = os.path.join('forks', basename) return os.path.join(d, filename) @with_lock def save_chunk(self, index: int, chunk: bytes): + assert index >= 0, index chunk_within_checkpoint_region = index < len(self.checkpoints) # chunks in checkpoint region are the responsibility of the 'main chain' - if chunk_within_checkpoint_region and self.parent_id is not None: - main_chain = blockchains[0] + if chunk_within_checkpoint_region and self.parent is not None: + main_chain = get_best_chain() main_chain.save_chunk(index, chunk) return @@ -235,18 +302,36 @@ class Blockchain(util.PrintError): self.write(chunk, delta_bytes, truncate) self.swap_with_parent() - @with_lock def swap_with_parent(self) -> None: - if self.parent_id is None: - return - parent_branch_size = self.parent().height() - self.forkpoint + 1 - if parent_branch_size >= self.size(): - return - self.print_error("swap", self.forkpoint, self.parent_id) - parent_id = self.parent_id - forkpoint = self.forkpoint - parent = self.parent() + parent_lock = self.parent.lock if self.parent is not None else threading.Lock() + with parent_lock, self.lock, blockchains_lock: # this order should not deadlock + # do the swap; possibly multiple ones + cnt = 0 + while self._swap_with_parent(): + cnt += 1 + if cnt > len(blockchains): # make sure we are making progress + raise Exception(f'swapping fork with parent too many times: {cnt}') + + def _swap_with_parent(self) -> bool: + """Check if this chain became stronger than its parent, and swap + the underlying files if so. The Blockchain instances will keep + 'containing' the same headers, but their ids change and so + they will be stored in different files.""" + if self.parent is None: + return False + parent_branch_size = self.parent.height() - self.forkpoint + 1 + if parent_branch_size >= self.size(): # FIXME most work, not length + return False + self.print_error("swap", self.forkpoint, self.parent.forkpoint) + forkpoint = self.forkpoint # type: Optional[int] + parent = self.parent # type: Optional[Blockchain] + child_old_id = self.get_id() + parent_old_id = parent.get_id() + # swap files + # child takes parent's name + # parent's new name will be something new (not child's old name) self.assert_headers_file_available(self.path()) + child_old_name = self.path() with open(self.path(), 'rb') as f: my_data = f.read() self.assert_headers_file_available(parent.path()) @@ -255,24 +340,28 @@ class Blockchain(util.PrintError): parent_data = f.read(parent_branch_size*HEADER_SIZE) self.write(parent_data, 0) parent.write(my_data, (forkpoint - parent.forkpoint)*HEADER_SIZE) - # store file path - with blockchains_lock: chains = list(blockchains.values()) - for b in chains: - b.old_path = b.path() # swap parameters - self.parent_id = parent.parent_id; parent.parent_id = parent_id - self.forkpoint = parent.forkpoint; parent.forkpoint = forkpoint - self._size = parent._size; parent._size = parent_branch_size - # move files - for b in chains: - if b in [self, parent]: continue - if b.old_path != b.path(): - self.print_error("renaming", b.old_path, b.path()) - os.rename(b.old_path, b.path()) + self.parent, parent.parent = parent.parent, self # type: Optional[Blockchain], Optional[Blockchain] + self.forkpoint, parent.forkpoint = parent.forkpoint, self.forkpoint + self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(bh2u(parent_data[:HEADER_SIZE])) + self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash + # parent's new name + try: + os.rename(child_old_name, parent.path()) + except OSError: + os.remove(parent.path()) + os.rename(child_old_name, parent.path()) + self.update_size() + parent.update_size() # update pointers - with blockchains_lock: - blockchains[self.forkpoint] = self - blockchains[parent.forkpoint] = parent + blockchains.pop(child_old_id, None) + blockchains.pop(parent_old_id, None) + blockchains[self.get_id()] = self + blockchains[parent.get_id()] = parent + return True + + def get_id(self) -> str: + return self._forkpoint_hash def assert_headers_file_available(self, path): if os.path.exists(path): @@ -282,19 +371,19 @@ class Blockchain(util.PrintError): else: raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path)) + @with_lock def write(self, data: bytes, offset: int, truncate: bool=True) -> None: filename = self.path() - with self.lock: - self.assert_headers_file_available(filename) - with open(filename, 'rb+') as f: - if truncate and offset != self._size * HEADER_SIZE: - f.seek(offset) - f.truncate() + self.assert_headers_file_available(filename) + with open(filename, 'rb+') as f: + if truncate and offset != self._size * HEADER_SIZE: f.seek(offset) - f.write(data) - f.flush() - os.fsync(f.fileno()) - self.update_size() + f.truncate() + f.seek(offset) + f.write(data) + f.flush() + os.fsync(f.fileno()) + self.update_size() @with_lock def save_header(self, header: dict) -> None: @@ -306,12 +395,12 @@ class Blockchain(util.PrintError): self.write(data, delta*HEADER_SIZE) self.swap_with_parent() + @with_lock def read_header(self, height: int) -> Optional[dict]: - assert self.parent_id != self.forkpoint if height < 0: return if height < self.forkpoint: - return self.parent().read_header(height) + return self.parent.read_header(height) if height > self.height(): return delta = height - self.forkpoint @@ -371,16 +460,18 @@ class Blockchain(util.PrintError): new_target = self.bits_to_target(self.target_to_bits(new_target)) return new_target - def bits_to_target(self, bits: int) -> int: + @classmethod + def bits_to_target(cls, bits: int) -> int: bitsN = (bits >> 24) & 0xff - if not (bitsN >= 0x03 and bitsN <= 0x1d): + if not (0x03 <= bitsN <= 0x1d): raise Exception("First part of bits should be in [0x03, 0x1d]") bitsBase = bits & 0xffffff - if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff): + if not (0x8000 <= bitsBase <= 0x7fffff): raise Exception("Second part of bits should be in [0x8000, 0x7fffff]") return bitsBase << (8 * (bitsN-3)) - def target_to_bits(self, target: int) -> int: + @classmethod + def target_to_bits(cls, target: int) -> int: c = ("%064x" % target)[2:] while c[:2] == '00' and len(c) > 6: c = c[2:] @@ -416,6 +507,7 @@ class Blockchain(util.PrintError): return True def connect_chunk(self, idx: int, hexdata: str) -> bool: + assert idx >= 0, idx try: data = bfh(hexdata) self.verify_chunk(idx, data) @@ -423,7 +515,7 @@ class Blockchain(util.PrintError): self.save_chunk(idx, data) return True except BaseException as e: - self.print_error('verify_chunk %d failed'%idx, str(e)) + self.print_error(f'verify_chunk idx {idx} failed: {repr(e)}') return False def get_checkpoints(self): diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index c34e5ef9..73379bb2 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -126,10 +126,12 @@ class ElectrumWindow(App): chains = self.network.get_blockchains() def cb(name): with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items()) - for index, b in blockchain_items: + for chain_id, b in blockchain_items: if name == b.get_name(): - self.network.run_from_another_thread(self.network.follow_chain_given_id(index)) - names = [blockchain.blockchains[b].get_name() for b in chains] + self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id)) + chain_objects = [blockchain.blockchains.get(chain_id) for chain_id in chains] + chain_objects = filter(lambda b: b is not None, chain_objects) + names = [b.get_name() for b in chain_objects] if len(names) > 1: cur_chain = self.network.blockchain().get_name() ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open() diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index bef85383..94ae7773 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -82,8 +82,8 @@ class NodesListWidget(QTreeWidget): server = item.data(1, Qt.UserRole) menu.addAction(_("Use as server"), lambda: self.parent.follow_server(server)) else: - index = item.data(1, Qt.UserRole) - menu.addAction(_("Follow this branch"), lambda: self.parent.follow_branch(index)) + chain_id = item.data(1, Qt.UserRole) + menu.addAction(_("Follow this branch"), lambda: self.parent.follow_branch(chain_id)) menu.exec_(self.viewport().mapToGlobal(position)) def keyPressEvent(self, event): @@ -103,22 +103,23 @@ class NodesListWidget(QTreeWidget): self.addChild = self.addTopLevelItem chains = network.get_blockchains() n_chains = len(chains) - for k, items in chains.items(): - b = blockchain.blockchains[k] + for chain_id, interfaces in chains.items(): + b = blockchain.blockchains.get(chain_id) + if b is None: continue name = b.get_name() - if n_chains >1: + if n_chains > 1: x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()]) x.setData(0, Qt.UserRole, 1) - x.setData(1, Qt.UserRole, b.forkpoint) + x.setData(1, Qt.UserRole, b.get_id()) else: x = self - for i in items: + for i in interfaces: star = ' *' if i == network.interface else '' item = QTreeWidgetItem([i.host + star, '%d'%i.tip]) item.setData(0, Qt.UserRole, 0) item.setData(1, Qt.UserRole, i.server) x.addChild(item) - if n_chains>1: + if n_chains > 1: self.addTopLevelItem(x) x.setExpanded(True) @@ -410,8 +411,8 @@ class NetworkChoiceLayout(object): self.set_protocol(p) self.set_server() - def follow_branch(self, index): - self.network.run_from_another_thread(self.network.follow_chain_given_id(index)) + def follow_branch(self, chain_id): + self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id)) self.update() def follow_server(self, server): diff --git a/electrum/interface.py b/electrum/interface.py index 68ede755..99e2349e 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -28,7 +28,7 @@ import ssl import sys import traceback import asyncio -from typing import Tuple, Union, List, TYPE_CHECKING +from typing import Tuple, Union, List, TYPE_CHECKING, Optional from collections import defaultdict import aiorpcx @@ -140,14 +140,14 @@ def serialize_server(host: str, port: Union[str, int], protocol: str) -> str: class Interface(PrintError): verbosity_filter = 'i' - def __init__(self, network: 'Network', server: str, config_path, proxy: dict): + def __init__(self, network: 'Network', server: str, proxy: Optional[dict]): self.ready = asyncio.Future() self.got_disconnected = asyncio.Future() self.server = server self.host, self.port, self.protocol = deserialize_server(self.server) self.port = int(self.port) - self.config_path = config_path - self.cert_path = os.path.join(self.config_path, 'certs', self.host) + assert network.config.path + self.cert_path = os.path.join(network.config.path, 'certs', self.host) self.blockchain = None self._requested_chunks = set() self.network = network @@ -281,7 +281,7 @@ class Interface(PrintError): assert self.tip_header chain = blockchain.check_header(self.tip_header) if not chain: - self.blockchain = blockchain.blockchains[0] + self.blockchain = blockchain.get_best_chain() else: self.blockchain = chain assert self.blockchain is not None @@ -502,7 +502,7 @@ class Interface(PrintError): # bad_header connects to good_header; bad_header itself is NOT in self.blockchain. bh = self.blockchain.height() - assert bh >= good + assert bh >= good, (bh, good) if bh == good: height = good + 1 self.print_error("catching up from {}".format(height)) @@ -510,53 +510,12 @@ class Interface(PrintError): # this is a new fork we don't yet have height = bad + 1 - branch = blockchain.blockchains.get(bad) - if branch is not None: - # Conflict!! As our fork handling is not completely general, - # we need to delete another fork to save this one. - # Note: This could be a potential DOS vector against Electrum. - # However, mining blocks that satisfy the difficulty requirements - # is assumed to be expensive; especially as forks below the max - # checkpoint are ignored. - self.print_error("new fork at bad height {}. conflict!!".format(bad)) - assert self.blockchain != branch - ismocking = type(branch) is dict - if ismocking: - self.print_error("TODO replace blockchain") - return 'fork_conflict', height - self.print_error('forkpoint conflicts with existing fork', branch.path()) - self._raise_if_fork_conflicts_with_default_server(branch) - await self._disconnect_from_interfaces_on_conflicting_blockchain(branch) - branch.write(b'', 0) - branch.save_header(bad_header) - self.blockchain = branch - return 'fork_conflict', height - else: - # No conflict. Just save the new fork. - self.print_error("new fork at bad height {}. NO conflict.".format(bad)) - forkfun = self.blockchain.fork if 'mock' not in bad_header else bad_header['mock']['fork'] - b = forkfun(bad_header) - with blockchain.blockchains_lock: - assert bad not in blockchain.blockchains, (bad, list(blockchain.blockchains)) - blockchain.blockchains[bad] = b - self.blockchain = b - assert b.forkpoint == bad - return 'fork_noconflict', height - - def _raise_if_fork_conflicts_with_default_server(self, chain_to_delete: Blockchain) -> None: - main_interface = self.network.interface - if not main_interface: return - if main_interface == self: return - chain_of_default_server = main_interface.blockchain - if not chain_of_default_server: return - if chain_to_delete == chain_of_default_server: - raise GracefulDisconnect('refusing to overwrite blockchain of default server') - - async def _disconnect_from_interfaces_on_conflicting_blockchain(self, chain: Blockchain) -> None: - ifaces = await self.network.disconnect_from_interfaces_on_given_blockchain(chain) - if not ifaces: return - servers = [interface.server for interface in ifaces] - self.print_error("forcing disconnect of other interfaces: {}".format(servers)) + self.print_error(f"new fork at bad height {bad}") + forkfun = self.blockchain.fork if 'mock' not in bad_header else bad_header['mock']['fork'] + b = forkfun(bad_header) # type: Blockchain + self.blockchain = b + assert b.forkpoint == bad + return 'fork', height async def _search_headers_backwards(self, height, header): async def iterate(): diff --git a/electrum/network.py b/electrum/network.py index c0218f9c..85c8dbee 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -177,10 +177,10 @@ class Network(PrintError): if config is None: config = {} # Do not use mutables as default values! self.config = SimpleConfig(config) if isinstance(config, dict) else config # type: SimpleConfig - blockchain.blockchains = blockchain.read_blockchains(self.config) - self.print_error("blockchains", list(blockchain.blockchains)) + blockchain.read_blockchains(self.config) + self.print_error("blockchains", list(map(lambda b: b.forkpoint, blockchain.blockchains.values()))) self._blockchain_preferred_block = self.config.get('blockchain_preferred_block', None) # type: Optional[Dict] - self._blockchain_index = 0 + self._blockchain = blockchain.get_best_chain() # Server for addresses and transactions self.default_server = self.config.get('server', None) # Sanitize default server @@ -559,17 +559,24 @@ class Network(PrintError): filtered = list(filter(lambda iface: iface.blockchain.check_hash(pref_height, pref_hash), interfaces)) if filtered: + self.print_error("switching to preferred fork") chosen_iface = random.choice(filtered) await self.switch_to_interface(chosen_iface.server) return - # try to switch to longest chain - if self.blockchain().parent_id is None: - return # already on longest chain - filtered = list(filter(lambda iface: iface.blockchain.parent_id is None, + else: + self.print_error("tried to switch to preferred fork but no interfaces are on it") + # try to switch to best chain + if self.blockchain().parent is None: + return # already on best chain + filtered = list(filter(lambda iface: iface.blockchain.parent is None, interfaces)) if filtered: + self.print_error("switching to best chain") chosen_iface = random.choice(filtered) await self.switch_to_interface(chosen_iface.server) + else: + # FIXME switch to best available? + self.print_error("tried to switch to best chain but no interfaces are on it") async def switch_to_interface(self, server: str): """Switch to server as our main interface. If no connection exists, @@ -637,7 +644,7 @@ class Network(PrintError): @ignore_exceptions # do not kill main_taskgroup @log_exceptions async def _run_new_interface(self, server): - interface = Interface(self, server, self.config.path, self.proxy) + interface = Interface(self, server, self.proxy) timeout = 10 if not self.proxy else 20 try: await asyncio.wait_for(interface.ready, timeout) @@ -661,7 +668,7 @@ class Network(PrintError): self.trigger_callback('network_updated') async def _init_headers_file(self): - b = blockchain.blockchains[0] + b = blockchain.get_best_chain() filename = b.path() length = HEADER_SIZE * len(constants.net.CHECKPOINTS) * 2016 if not os.path.exists(filename) or os.path.getsize(filename) < length: @@ -739,8 +746,8 @@ class Network(PrintError): def blockchain(self) -> Blockchain: interface = self.interface if interface and interface.blockchain is not None: - self._blockchain_index = interface.blockchain.forkpoint - return blockchain.blockchains[self._blockchain_index] + self._blockchain = interface.blockchain + return self._blockchain def get_blockchains(self): out = {} # blockchain_id -> list(interfaces) @@ -752,13 +759,6 @@ class Network(PrintError): out[chain_id] = r return out - async def disconnect_from_interfaces_on_given_blockchain(self, chain: Blockchain) -> Sequence[Interface]: - chain_id = chain.forkpoint - ifaces = self.get_blockchains().get(chain_id) or [] - for interface in ifaces: - await self.connection_down(interface.server) - return ifaces - def _set_preferred_chain(self, chain: Blockchain): height = chain.get_max_forkpoint() header_hash = chain.get_hash(height) @@ -768,7 +768,7 @@ class Network(PrintError): } self.config.set_key('blockchain_preferred_block', self._blockchain_preferred_block) - async def follow_chain_given_id(self, chain_id: int) -> None: + async def follow_chain_given_id(self, chain_id: str) -> None: bc = blockchain.blockchains.get(chain_id) if not bc: raise Exception('blockchain {} not found'.format(chain_id)) diff --git a/electrum/storage.py b/electrum/storage.py index 16a4cc90..ad3de4c6 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -125,7 +125,7 @@ class JsonDB(PrintError): # perform atomic write on POSIX systems try: os.rename(temp_path, self.path) - except: + except OSError: os.remove(self.path) os.rename(temp_path, self.path) os.chmod(self.path, mode) diff --git a/electrum/tests/test_blockchain.py b/electrum/tests/test_blockchain.py new file mode 100644 index 00000000..be29c1b0 --- /dev/null +++ b/electrum/tests/test_blockchain.py @@ -0,0 +1,239 @@ +import shutil +import tempfile +import os + +from electrum import constants, blockchain +from electrum.simple_config import SimpleConfig +from electrum.blockchain import Blockchain, deserialize_header, hash_header +from electrum.util import bh2u, bfh, make_dir + +from . import SequentialTestCase + + +class TestBlockchain(SequentialTestCase): + + HEADERS = { + 'A': deserialize_header(bfh("0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f2002000000"), 0), + 'B': deserialize_header(bfh("0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f186c8dfd970a4545f79916bc1d75c9d00432f57c89209bf3bb115b7612848f509c25f45bffff7f2000000000"), 1), + 'C': deserialize_header(bfh("00000020686bdfc6a3db73d5d93e8c9663a720a26ecb1ef20eb05af11b36cdbc57c19f7ebf2cbf153013a1c54abaf70e95198fcef2f3059cc6b4d0f7e876808e7d24d11cc825f45bffff7f2000000000"), 2), + 'D': deserialize_header(bfh("00000020122baa14f3ef54985ae546d1611559e3f487bd2a0f46e8dbb52fbacc9e237972e71019d7feecd9b8596eca9a67032c5f4641b23b5d731dc393e37de7f9c2f299e725f45bffff7f2000000000"), 3), + 'E': deserialize_header(bfh("00000020f8016f7ef3a17d557afe05d4ea7ab6bde1b2247b7643896c1b63d43a1598b747a3586da94c71753f27c075f57f44faf913c31177a0957bbda42e7699e3a2141aed25f45bffff7f2001000000"), 4), + 'F': deserialize_header(bfh("000000201d589c6643c1d121d73b0573e5ee58ab575b8fdf16d507e7e915c5fbfbbfd05e7aee1d692d1615c3bdf52c291032144ce9e3b258a473c17c745047f3431ff8e2ee25f45bffff7f2000000000"), 5), + 'O': deserialize_header(bfh("00000020b833ed46eea01d4c980f59feee44a66aa1162748b6801029565d1466790c405c3a141ce635cbb1cd2b3a4fcdd0a3380517845ba41736c82a79cab535d31128066526f45bffff7f2001000000"), 6), + 'P': deserialize_header(bfh("00000020abe8e119d1877c9dc0dc502d1a253fb9a67967c57732d2f71ee0280e8381ff0a9690c2fe7c1a4450c74dc908fe94dd96c3b0637d51475e9e06a78e944a0c7fe28126f45bffff7f2000000000"), 7), + 'Q': deserialize_header(bfh("000000202ce41d94eb70e1518bc1f72523f84a903f9705d967481e324876e1f8cf4d3452148be228a4c3f2061bafe7efdfc4a8d5a94759464b9b5c619994d45dfcaf49e1a126f45bffff7f2000000000"), 8), + 'R': deserialize_header(bfh("00000020552755b6c59f3d51e361d16281842a4e166007799665b5daed86a063dd89857415681cb2d00ff889193f6a68a93f5096aeb2d84ca0af6185a462555822552221a626f45bffff7f2000000000"), 9), + 'S': deserialize_header(bfh("00000020a13a491cbefc93cd1bb1938f19957e22a134faf14c7dee951c45533e2c750f239dc087fc977b06c24a69c682d1afd1020e6dc1f087571ccec66310a786e1548fab26f45bffff7f2000000000"), 10), + 'T': deserialize_header(bfh("00000020dbf3a9b55dfefbaf8b6e43a89cf833fa2e208bbc0c1c5d76c0d71b9e4a65337803b243756c25053253aeda309604363460a3911015929e68705bd89dff6fe064b026f45bffff7f2002000000"), 11), + 'U': deserialize_header(bfh("000000203d0932b3b0c78eccb39a595a28ae4a7c966388648d7783fd1305ec8d40d4fe5fd67cb902a7d807cee7676cb543feec3e053aa824d5dfb528d5b94f9760313d9db726f45bffff7f2001000000"), 12), + 'G': deserialize_header(bfh("00000020b833ed46eea01d4c980f59feee44a66aa1162748b6801029565d1466790c405c3a141ce635cbb1cd2b3a4fcdd0a3380517845ba41736c82a79cab535d31128066928f45bffff7f2001000000"), 6), + 'H': deserialize_header(bfh("00000020e19e687f6e7f83ca394c114144dbbbc4f3f9c9450f66331a125413702a2e1a719690c2fe7c1a4450c74dc908fe94dd96c3b0637d51475e9e06a78e944a0c7fe26a28f45bffff7f2002000000"), 7), + 'I': deserialize_header(bfh("0000002009dcb3b158293c89d7cf7ceeb513add122ebc3880a850f47afbb2747f5e48c54148be228a4c3f2061bafe7efdfc4a8d5a94759464b9b5c619994d45dfcaf49e16a28f45bffff7f2000000000"), 8), + 'J': deserialize_header(bfh("000000206a65f3bdd3374a5a6c4538008ba0b0a560b8566291f9ef4280ab877627a1742815681cb2d00ff889193f6a68a93f5096aeb2d84ca0af6185a462555822552221c928f45bffff7f2000000000"), 9), + 'K': deserialize_header(bfh("00000020bb3b421653548991998f96f8ba486b652fdb07ca16e9cee30ece033547cd1a6e9dc087fc977b06c24a69c682d1afd1020e6dc1f087571ccec66310a786e1548fca28f45bffff7f2000000000"), 10), + 'L': deserialize_header(bfh("00000020c391d74d37c24a130f4bf4737932bdf9e206dd4fad22860ec5408978eb55d46303b243756c25053253aeda309604363460a3911015929e68705bd89dff6fe064ca28f45bffff7f2000000000"), 11), + 'M': deserialize_header(bfh("000000206a65f3bdd3374a5a6c4538008ba0b0a560b8566291f9ef4280ab877627a1742815681cb2d00ff889193f6a68a93f5096aeb2d84ca0af6185a4625558225522214229f45bffff7f2000000000"), 9), + 'N': deserialize_header(bfh("00000020383dab38b57f98aa9b4f0d5ff868bc674b4828d76766bf048296f4c45fff680a9dc087fc977b06c24a69c682d1afd1020e6dc1f087571ccec66310a786e1548f4329f45bffff7f2003000000"), 10), + 'X': deserialize_header(bfh("0000002067f1857f54b7fef732cb4940f7d1b339472b3514660711a820330fd09d8fba6b03b243756c25053253aeda309604363460a3911015929e68705bd89dff6fe0649b29f45bffff7f2002000000"), 11), + 'Y': deserialize_header(bfh("00000020db33c9768a9e5f7c37d0f09aad88d48165946c87d08f7d63793f07b5c08c527fd67cb902a7d807cee7676cb543feec3e053aa824d5dfb528d5b94f9760313d9d9b29f45bffff7f2000000000"), 12), + 'Z': deserialize_header(bfh("0000002047822b67940e337fda38be6f13390b3596e4dea2549250256879722073824e7f0f2596c29203f8a0f71ae94193092dc8f113be3dbee4579f1e649fa3d6dcc38c622ef45bffff7f2003000000"), 13), + } + # tree of headers: + # - M <- N <- X <- Y <- Z + # / + # - G <- H <- I <- J <- K <- L + # / + # A <- B <- C <- D <- E <- F <- O <- P <- Q <- R <- S <- T <- U + + @classmethod + def setUpClass(cls): + super().setUpClass() + constants.set_regtest() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + constants.set_mainnet() + + def setUp(self): + super().setUp() + self.data_dir = tempfile.mkdtemp() + make_dir(os.path.join(self.data_dir, 'forks')) + self.config = SimpleConfig({'electrum_path': self.data_dir}) + blockchain.blockchains = {} + + def tearDown(self): + super().tearDown() + shutil.rmtree(self.data_dir) + + def _append_header(self, chain: Blockchain, header: dict): + self.assertTrue(chain.can_connect(header)) + chain.save_header(header) + + def test_forking_and_swapping(self): + blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain( + config=self.config, forkpoint=0, parent=None, + forkpoint_hash=constants.net.GENESIS, prev_hash=None) + open(chain_u.path(), 'w+').close() + + self._append_header(chain_u, self.HEADERS['A']) + self._append_header(chain_u, self.HEADERS['B']) + self._append_header(chain_u, self.HEADERS['C']) + self._append_header(chain_u, self.HEADERS['D']) + self._append_header(chain_u, self.HEADERS['E']) + self._append_header(chain_u, self.HEADERS['F']) + self._append_header(chain_u, self.HEADERS['O']) + self._append_header(chain_u, self.HEADERS['P']) + self._append_header(chain_u, self.HEADERS['Q']) + self._append_header(chain_u, self.HEADERS['R']) + + chain_l = chain_u.fork(self.HEADERS['G']) + self._append_header(chain_l, self.HEADERS['H']) + self._append_header(chain_l, self.HEADERS['I']) + self._append_header(chain_l, self.HEADERS['J']) + + # do checks + self.assertEqual(2, len(blockchain.blockchains)) + self.assertEqual(1, len(os.listdir(os.path.join(self.data_dir, "forks")))) + self.assertEqual(0, chain_u.forkpoint) + self.assertEqual(None, chain_u.parent) + self.assertEqual(constants.net.GENESIS, chain_u._forkpoint_hash) + self.assertEqual(None, chain_u._prev_hash) + self.assertEqual(os.path.join(self.data_dir, "blockchain_headers"), chain_u.path()) + self.assertEqual(10 * 80, os.stat(chain_u.path()).st_size) + self.assertEqual(6, chain_l.forkpoint) + self.assertEqual(chain_u, chain_l.parent) + self.assertEqual(hash_header(self.HEADERS['G']), chain_l._forkpoint_hash) + self.assertEqual(hash_header(self.HEADERS['F']), chain_l._prev_hash) + self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_711a2e2a701354121a33660f45c9f9f3c4bbdb4441114c39ca837f6e7f689ee1"), chain_l.path()) + self.assertEqual(4 * 80, os.stat(chain_l.path()).st_size) + + self._append_header(chain_l, self.HEADERS['K']) + + # chains were swapped, do checks + self.assertEqual(2, len(blockchain.blockchains)) + self.assertEqual(1, len(os.listdir(os.path.join(self.data_dir, "forks")))) + self.assertEqual(6, chain_u.forkpoint) + self.assertEqual(chain_l, chain_u.parent) + self.assertEqual(hash_header(self.HEADERS['O']), chain_u._forkpoint_hash) + self.assertEqual(hash_header(self.HEADERS['F']), chain_u._prev_hash) + self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_aff81830e28e01ef7d23277c56779a6b93f251a2d50dcc09d7c87d119e1e8ab"), chain_u.path()) + self.assertEqual(4 * 80, os.stat(chain_u.path()).st_size) + self.assertEqual(0, chain_l.forkpoint) + self.assertEqual(None, chain_l.parent) + self.assertEqual(constants.net.GENESIS, chain_l._forkpoint_hash) + self.assertEqual(None, chain_l._prev_hash) + self.assertEqual(os.path.join(self.data_dir, "blockchain_headers"), chain_l.path()) + self.assertEqual(11 * 80, os.stat(chain_l.path()).st_size) + for b in (chain_u, chain_l): + self.assertTrue(all([b.can_connect(b.read_header(i), False) for i in range(b.height())])) + + self._append_header(chain_u, self.HEADERS['S']) + self._append_header(chain_u, self.HEADERS['T']) + self._append_header(chain_u, self.HEADERS['U']) + self._append_header(chain_l, self.HEADERS['L']) + + chain_z = chain_l.fork(self.HEADERS['M']) + self._append_header(chain_z, self.HEADERS['N']) + self._append_header(chain_z, self.HEADERS['X']) + self._append_header(chain_z, self.HEADERS['Y']) + self._append_header(chain_z, self.HEADERS['Z']) + + # chain_z became best chain, do checks + self.assertEqual(3, len(blockchain.blockchains)) + self.assertEqual(2, len(os.listdir(os.path.join(self.data_dir, "forks")))) + self.assertEqual(0, chain_z.forkpoint) + self.assertEqual(None, chain_z.parent) + self.assertEqual(constants.net.GENESIS, chain_z._forkpoint_hash) + self.assertEqual(None, chain_z._prev_hash) + self.assertEqual(os.path.join(self.data_dir, "blockchain_headers"), chain_z.path()) + self.assertEqual(14 * 80, os.stat(chain_z.path()).st_size) + self.assertEqual(9, chain_l.forkpoint) + self.assertEqual(chain_z, chain_l.parent) + self.assertEqual(hash_header(self.HEADERS['J']), chain_l._forkpoint_hash) + self.assertEqual(hash_header(self.HEADERS['I']), chain_l._prev_hash) + self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_9_2874a1277687ab8042eff9916256b860a5b0a08b0038456c5a4a37d3bdf3656a_6e1acd473503ce0ee3cee916ca07db2f656b48baf8968f999189545316423bbb"), chain_l.path()) + self.assertEqual(3 * 80, os.stat(chain_l.path()).st_size) + self.assertEqual(6, chain_u.forkpoint) + self.assertEqual(chain_z, chain_u.parent) + self.assertEqual(hash_header(self.HEADERS['O']), chain_u._forkpoint_hash) + self.assertEqual(hash_header(self.HEADERS['F']), chain_u._prev_hash) + self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_aff81830e28e01ef7d23277c56779a6b93f251a2d50dcc09d7c87d119e1e8ab"), chain_u.path()) + self.assertEqual(7 * 80, os.stat(chain_u.path()).st_size) + for b in (chain_u, chain_l, chain_z): + self.assertTrue(all([b.can_connect(b.read_header(i), False) for i in range(b.height())])) + + self.assertEqual(constants.net.GENESIS, chain_z.get_hash(0)) + self.assertEqual(hash_header(self.HEADERS['F']), chain_z.get_hash(5)) + self.assertEqual(hash_header(self.HEADERS['G']), chain_z.get_hash(6)) + self.assertEqual(hash_header(self.HEADERS['I']), chain_z.get_hash(8)) + self.assertEqual(hash_header(self.HEADERS['M']), chain_z.get_hash(9)) + self.assertEqual(hash_header(self.HEADERS['Z']), chain_z.get_hash(13)) + + def test_doing_multiple_swaps_after_single_new_header(self): + blockchain.blockchains[constants.net.GENESIS] = chain_u = Blockchain( + config=self.config, forkpoint=0, parent=None, + forkpoint_hash=constants.net.GENESIS, prev_hash=None) + open(chain_u.path(), 'w+').close() + + self._append_header(chain_u, self.HEADERS['A']) + self._append_header(chain_u, self.HEADERS['B']) + self._append_header(chain_u, self.HEADERS['C']) + self._append_header(chain_u, self.HEADERS['D']) + self._append_header(chain_u, self.HEADERS['E']) + self._append_header(chain_u, self.HEADERS['F']) + self._append_header(chain_u, self.HEADERS['O']) + self._append_header(chain_u, self.HEADERS['P']) + self._append_header(chain_u, self.HEADERS['Q']) + self._append_header(chain_u, self.HEADERS['R']) + self._append_header(chain_u, self.HEADERS['S']) + + self.assertEqual(1, len(blockchain.blockchains)) + self.assertEqual(0, len(os.listdir(os.path.join(self.data_dir, "forks")))) + + chain_l = chain_u.fork(self.HEADERS['G']) + self._append_header(chain_l, self.HEADERS['H']) + self._append_header(chain_l, self.HEADERS['I']) + self._append_header(chain_l, self.HEADERS['J']) + self._append_header(chain_l, self.HEADERS['K']) + # now chain_u is best chain, but it's tied with chain_l + + self.assertEqual(2, len(blockchain.blockchains)) + self.assertEqual(1, len(os.listdir(os.path.join(self.data_dir, "forks")))) + + chain_z = chain_l.fork(self.HEADERS['M']) + self._append_header(chain_z, self.HEADERS['N']) + self._append_header(chain_z, self.HEADERS['X']) + + self.assertEqual(3, len(blockchain.blockchains)) + self.assertEqual(2, len(os.listdir(os.path.join(self.data_dir, "forks")))) + + # chain_z became best chain, do checks + self.assertEqual(0, chain_z.forkpoint) + self.assertEqual(None, chain_z.parent) + self.assertEqual(constants.net.GENESIS, chain_z._forkpoint_hash) + self.assertEqual(None, chain_z._prev_hash) + self.assertEqual(os.path.join(self.data_dir, "blockchain_headers"), chain_z.path()) + self.assertEqual(12 * 80, os.stat(chain_z.path()).st_size) + self.assertEqual(9, chain_l.forkpoint) + self.assertEqual(chain_z, chain_l.parent) + self.assertEqual(hash_header(self.HEADERS['J']), chain_l._forkpoint_hash) + self.assertEqual(hash_header(self.HEADERS['I']), chain_l._prev_hash) + self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_9_2874a1277687ab8042eff9916256b860a5b0a08b0038456c5a4a37d3bdf3656a_6e1acd473503ce0ee3cee916ca07db2f656b48baf8968f999189545316423bbb"), chain_l.path()) + self.assertEqual(2 * 80, os.stat(chain_l.path()).st_size) + self.assertEqual(6, chain_u.forkpoint) + self.assertEqual(chain_z, chain_u.parent) + self.assertEqual(hash_header(self.HEADERS['O']), chain_u._forkpoint_hash) + self.assertEqual(hash_header(self.HEADERS['F']), chain_u._prev_hash) + self.assertEqual(os.path.join(self.data_dir, "forks", "fork2_6_5c400c7966145d56291080b6482716a16aa644eefe590f984c1da0ee46ed33b8_aff81830e28e01ef7d23277c56779a6b93f251a2d50dcc09d7c87d119e1e8ab"), chain_u.path()) + self.assertEqual(5 * 80, os.stat(chain_u.path()).st_size) + + self.assertEqual(constants.net.GENESIS, chain_z.get_hash(0)) + self.assertEqual(hash_header(self.HEADERS['F']), chain_z.get_hash(5)) + self.assertEqual(hash_header(self.HEADERS['G']), chain_z.get_hash(6)) + self.assertEqual(hash_header(self.HEADERS['I']), chain_z.get_hash(8)) + self.assertEqual(hash_header(self.HEADERS['M']), chain_z.get_hash(9)) + self.assertEqual(hash_header(self.HEADERS['X']), chain_z.get_hash(11)) + + for b in (chain_u, chain_l, chain_z): + self.assertTrue(all([b.can_connect(b.read_header(i), False) for i in range(b.height())])) diff --git a/electrum/tests/test_network.py b/electrum/tests/test_network.py index c69375bd..ece54056 100644 --- a/electrum/tests/test_network.py +++ b/electrum/tests/test_network.py @@ -6,6 +6,9 @@ from electrum import constants from electrum.simple_config import SimpleConfig from electrum import blockchain from electrum.interface import Interface +from electrum.crypto import sha256 +from electrum.util import bh2u + class MockTaskGroup: async def spawn(self, x): return @@ -17,10 +20,14 @@ class MockNetwork: class MockInterface(Interface): def __init__(self, config): self.config = config - super().__init__(MockNetwork(), 'mock-server:50000:t', self.config.electrum_path(), None) + network = MockNetwork() + network.config = config + super().__init__(network, 'mock-server:50000:t', None) self.q = asyncio.Queue() - self.blockchain = blockchain.Blockchain(self.config, 2002, None) + self.blockchain = blockchain.Blockchain(config=self.config, forkpoint=0, + parent=None, forkpoint_hash=constants.net.GENESIS, prev_hash=None) self.tip = 12 + self.blockchain._size = self.tip + 1 async def get_block_header(self, height, assert_mode): assert self.q.qsize() > 0, (height, assert_mode) item = await self.q.get() @@ -56,7 +63,7 @@ class TestNetwork(unittest.TestCase): self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) ifa = self.interface - self.assertEqual(('fork_noconflict', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7))) + self.assertEqual(('fork', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7))) self.assertEqual(self.interface.q.qsize(), 0) def test_fork_conflict(self): @@ -70,7 +77,7 @@ class TestNetwork(unittest.TestCase): self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) ifa = self.interface - self.assertEqual(('fork_conflict', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7))) + self.assertEqual(('fork', 8), asyncio.get_event_loop().run_until_complete(ifa.sync_until(8, next_height=7))) self.assertEqual(self.interface.q.qsize(), 0) def test_can_connect_during_backward(self): @@ -87,7 +94,10 @@ class TestNetwork(unittest.TestCase): self.assertEqual(self.interface.q.qsize(), 0) def mock_fork(self, bad_header): - return blockchain.Blockchain(self.config, bad_header['block_height'], None) + forkpoint = bad_header['block_height'] + b = blockchain.Blockchain(config=self.config, forkpoint=forkpoint, parent=None, + forkpoint_hash=bh2u(sha256(str(forkpoint))), prev_hash=bh2u(sha256(str(forkpoint-1)))) + return b def test_chain_false_during_binary(self): blockchain.blockchains = {} diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py index 9117392e..c6366f3e 100644 --- a/electrum/tests/test_wallet.py +++ b/electrum/tests/test_wallet.py @@ -64,7 +64,6 @@ class TestWalletStorage(WalletTestCase): storage.put(key, value) storage.write() - contents = "" with open(self.wallet_path, "r") as f: contents = f.read() self.assertEqual(some_dict, json.loads(contents)) From 65ce3deeaa33828407cf3a873c7ce5c48fa0b6d4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 22 Nov 2018 17:13:43 +0100 Subject: [PATCH 131/301] blockchain: chain hierarchy based on most work, not length --- electrum/blockchain.py | 43 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/electrum/blockchain.py b/electrum/blockchain.py index c7c07531..d1238a2e 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -141,6 +141,11 @@ def read_blockchains(config: 'SimpleConfig'): def get_best_chain() -> 'Blockchain': return blockchains[constants.net.GENESIS] +# block hash -> chain work; up to and including that block +_CHAINWORK_CACHE = { + "0000000000000000000000000000000000000000000000000000000000000000": 0, # virtual block at height -1 +} # type: Dict[str, int] + class Blockchain(util.PrintError): """ @@ -319,10 +324,10 @@ class Blockchain(util.PrintError): they will be stored in different files.""" if self.parent is None: return False - parent_branch_size = self.parent.height() - self.forkpoint + 1 - if parent_branch_size >= self.size(): # FIXME most work, not length + if self.parent.get_chainwork() >= self.get_chainwork(): return False self.print_error("swap", self.forkpoint, self.parent.forkpoint) + parent_branch_size = self.parent.height() - self.forkpoint + 1 forkpoint = self.forkpoint # type: Optional[int] parent = self.parent # type: Optional[Blockchain] child_old_id = self.get_id() @@ -481,6 +486,40 @@ class Blockchain(util.PrintError): bitsBase >>= 8 return bitsN << 24 | bitsBase + def chainwork_of_header_at_height(self, height: int) -> int: + """work done by single header at given height""" + chunk_idx = height // 2016 - 1 + target = self.get_target(chunk_idx) + work = ((2 ** 256 - target - 1) // (target + 1)) + 1 + return work + + @with_lock + def get_chainwork(self, height=None) -> int: + if height is None: + height = max(0, self.height()) + if constants.net.TESTNET: + # On testnet/regtest, difficulty works somewhat different. + # It's out of scope to properly implement that. + return height + last_retarget = height // 2016 * 2016 - 1 + cached_height = last_retarget + while _CHAINWORK_CACHE.get(self.get_hash(cached_height)) is None: + if cached_height <= -1: + break + cached_height -= 2016 + assert cached_height >= -1, cached_height + running_total = _CHAINWORK_CACHE[self.get_hash(cached_height)] + while cached_height < last_retarget: + cached_height += 2016 + work_in_single_header = self.chainwork_of_header_at_height(cached_height) + work_in_chunk = 2016 * work_in_single_header + running_total += work_in_chunk + _CHAINWORK_CACHE[self.get_hash(cached_height)] = running_total + cached_height += 2016 + work_in_single_header = self.chainwork_of_header_at_height(cached_height) + work_in_last_partial_chunk = (height % 2016 + 1) * work_in_single_header + return running_total + work_in_last_partial_chunk + def can_connect(self, header: dict, check_height: bool=True) -> bool: if header is None: return False From f04e5fbed6a572bb68482f757d76332918df2070 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 22 Nov 2018 18:21:19 +0100 Subject: [PATCH 132/301] crypto: fix pkcs7 padding check related: ricmoo/pyaes#22 in practice, the only strings we would incorrectly accept are (certain length of) all zero bytes --- electrum/crypto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/crypto.py b/electrum/crypto.py index 345fbd85..038caafc 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -55,8 +55,8 @@ def strip_PKCS7_padding(data: bytes) -> bytes: if len(data) % 16 != 0 or len(data) == 0: raise InvalidPadding("invalid length") padlen = data[-1] - if padlen > 16: - raise InvalidPadding("invalid padding byte (large)") + if not (0 < padlen <= 16): + raise InvalidPadding("invalid padding byte (out of range)") for i in data[-padlen:]: if i != padlen: raise InvalidPadding("invalid padding byte (inconsistent)") From 0dd3a58a6375fd6d5e415c2f4bbccfc0a7ecd381 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 22 Nov 2018 19:37:56 +0100 Subject: [PATCH 133/301] requirements: also accept aiorpcx 0.10.x --- contrib/requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt index ab033e67..f4f458c3 100644 --- a/contrib/requirements/requirements.txt +++ b/contrib/requirements/requirements.txt @@ -6,6 +6,6 @@ protobuf dnspython jsonrpclib-pelix qdarkstyle<3.0 -aiorpcx>=0.9,<0.10 +aiorpcx>=0.9,<0.11 aiohttp aiohttp_socks From 67abea567f656e395c7a1f10f75f5eb002c92760 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 22 Nov 2018 19:41:06 +0100 Subject: [PATCH 134/301] rerun freeze packages --- .../requirements-binaries.txt | 72 ++++++------ .../deterministic-build/requirements-hw.txt | 30 ++--- contrib/deterministic-build/requirements.txt | 103 +++++++++--------- 3 files changed, 101 insertions(+), 104 deletions(-) diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt index ebe156c8..ea3a1d3f 100644 --- a/contrib/deterministic-build/requirements-binaries.txt +++ b/contrib/deterministic-build/requirements-binaries.txt @@ -1,45 +1,43 @@ pip==18.1 \ --hash=sha256:7909d0a0932e88ea53a7014dfd14522ffef91a464daaaf5c573343852ef98550 \ --hash=sha256:c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1 -pycryptodomex==3.6.6 \ - --hash=sha256:0cf562fc5e5ddbe935bb6162d84a7e46e19edba8ac6609587ab9e78dc7c527d4 \ - --hash=sha256:13b77b7a177a2fd0beb42db84b21d7d4ab646dfd223989a3b5fa6a6901075ae8 \ - --hash=sha256:1baf0d485853cfa8f87c969148dc040ae81a8de3dcaaeb07b3f40a90ab2ac637 \ - --hash=sha256:1dac67655206c92eabf827cc3c9f9efd35d699057f12185f01245b22e02db5f8 \ - --hash=sha256:1dc3abcb853e8b2188d0c8d624dbdc575a1f23ae82feb9a99e4a69b7064df81a \ - --hash=sha256:24bf22fffba3a7c1132a039d95256ca9e5016798111c7bf7d0091bfddb0e0245 \ - --hash=sha256:26fde532deaed9781643be654b25fc899b97866b68e50bc015aadd442387c42b \ - --hash=sha256:2958eceb671795ec4e5f2164e4ec5ddc1788fdf4f72b18390f548de5e11c9dcc \ - --hash=sha256:34b3218fcdcd59c26de75c4ff8a15e6472e7b88f9e5b3b193cb1c001e7a0e815 \ - --hash=sha256:3f4d96cfa963960d190d3d6ef5e8f7c2c14815ff816999dff5efaa6bf7cd1134 \ - --hash=sha256:40063250911af6ec5b3ccc80108e7a3be9d1695dd3d17104a1930861cbdb78b7 \ - --hash=sha256:4b8669f4b3fa386696b9b0366e69898dd626b119eaead50ee3b2c59c56222272 \ - --hash=sha256:5144ad0df1009cce6d03b71c2e5843ed96be91ab746e58e8e64ef9bca5209bfe \ - --hash=sha256:549d0a0884e60d876b975e18e791cf2aece077cb174f0d7d042982b5d36a6279 \ - --hash=sha256:5b365bc3da05fbc15acd6600d41f84fe8ed903400566bc0d1a0fadc862b41c8e \ - --hash=sha256:6ab64112924be1905a22656166ec0eeeba275fb15c15adba6470bd030362e160 \ - --hash=sha256:a06e0e0965145cc0d625e1a031b49705aa2c6b4e16aa0044d4719fb1d639977f \ - --hash=sha256:a472d2271cfb37940ad80c4698affeaf39552e4c19ceeac4d5290fe71519e3f4 \ - --hash=sha256:bfc315eef8cdae280e404df64cf71f8673abcc81e646699b15722f41b82a4ed7 \ - --hash=sha256:c1d4ea2382c1726ff65548f41dc79bd9d0345f3a47ba128768242eea470c6515 \ - --hash=sha256:c54d3d2e3b099dae61124d7476311ab594df7eeec305f612a5eb79226cf36b53 \ - --hash=sha256:d0d448484e161786922b41e112b5d7cf76ef5f0c725ea5107ef866bb14a38b12 \ - --hash=sha256:ddf10a2a071ae749158d8fdad0cbc78f6197219721afa91e0752dd999fddf422 \ - --hash=sha256:e0cc6ec20e6dfce1a6715ce60d71df3e9491461d525980821f71223026950b0e \ - --hash=sha256:e5fcb23f936912b6cfb475c7b115aa2e31b7fed409bc947c6190503d9f7d0338 \ - --hash=sha256:e76d0148ffa5b86f1bcb111bd3950bc61fd50bed8b6b1cf9f5e56784378b55b6 \ - --hash=sha256:ea09a059e4cf3d99a446f0a80ac9be54802542226cdbf8ffeea79a6ddc77f310 \ - --hash=sha256:f5844faf085ad3d92fb94d07347c69f193c0bd1f32843329c2a422addeb042c9 \ - --hash=sha256:f8478a443bb747c2ef1d508a09eeb57ff8c2ebbd8ea0e6e2351d483c23e725b3 \ - --hash=sha256:ff31cb6cd69c6683bd44e99bc4f36cb74a338cee58a3a9c5c19a8ee4bdd2f3b2 +pycryptodomex==3.7.0 \ + --hash=sha256:02c358fa2445821d110857266e4e400f110054694636efe678dc60ba22a1aaef \ + --hash=sha256:09989c8a1b83e576d02ad77b9b019648648c569febca41f58fa04b9d9fdd1e8f \ + --hash=sha256:0f8fe28aec591d1b86af596c9fc5f75fc0204fb1026188a44e5e1b199780f1e5 \ + --hash=sha256:0fb58c2065030a5381f3c466aaa7c4de707901badad0d6a0575952bb10e6c35f \ + --hash=sha256:0fb9f3e6b28a280436afe9192a9957c7f56e20ceecb73f2d7db807368fdf3aaf \ + --hash=sha256:12ff38a68bbd743407018f9dd87d4cc21f9cb28fe2d8ba93beca004ada9a09ff \ + --hash=sha256:1650143106383bae79cbbda3701fd9979d0a624dba2ec2fa63f88cae29dd7174 \ + --hash=sha256:20a646cd0e690b07b7da619bc5b3ee1467243b2e32dfff579825c3ad5d7637ab \ + --hash=sha256:284779f0908682657adb8c60d8484174baa0d2620fb1df49183be6e2e06e73ce \ + --hash=sha256:2f3ce5bfe81d975c45e4a3cbe2bef15b809acc24f952f5f6aa67c2ae3c1a6808 \ + --hash=sha256:30ac12f0c9ac8332cc76832fea88a547b49ef60c31f74697ee2584f215723d4f \ + --hash=sha256:4f038b815d66dea0b1d4286515d96474204e137eb5d883229616781865902789 \ + --hash=sha256:57199a867b9991b1950f438b788e818338cee8ed8698e2eebdc5664521ad92a9 \ + --hash=sha256:5c5349385e9863e3bba6804899f4125c8335f66d090e892d6a5bb915f5c89d4c \ + --hash=sha256:5d546fac597b5007d5ff38c50c9031945057a6a6fa1ab7585058165d370ea202 \ + --hash=sha256:614eddfa0cf325e49b5b803fcb41c9334de79c4b18bf8de07e7737e1efc1d2b9 \ + --hash=sha256:82ae66244824d50b2b657c32e5912fde70a6e36f41e61f2869151f226204430d \ + --hash=sha256:96a733f3be325fb17c2ba79648e85ab446767af3dc3b396f1404b9397aa28fe5 \ + --hash=sha256:9c3834d27c1cff84e2a5c218e373d80ebbb3edca81912656f16137f7f97e58e0 \ + --hash=sha256:9f11823636128acbe4e17c35ff668f4d0a9f3133450753a0675525b6413aa1b0 \ + --hash=sha256:a3f9ad4e3f90f14707776f13b886fbac491ebe65d96a64f3ce0b378e167c3bbf \ + --hash=sha256:a89dee72a0f5024cc1cbaf85535eee8d14e891384513145d2f368b5c481dcd54 \ + --hash=sha256:ccadde651e712093052286ad9ee27f5aa5f657ca688a1bf6d5c41ade709467f3 \ + --hash=sha256:ced9ea10977dd52cb1b936a92119fc38fcdc5eaa4148f925ef22bbf0f0d4a5bd \ + --hash=sha256:eb0c6d3b91d55e3481158ecf77f3963c1725454fdcf5b79302c27c1c9c0d2c2a \ + --hash=sha256:f6714569a4039287972c672a8bd4b8d7dc78a601def8b31ffa39cd2fec00cb4b \ + --hash=sha256:fa4036582c8755259d4b8f4fe203ae534b7b187dcea143ab53a24e0f3931d547 \ + --hash=sha256:fb31bb0c8301e5a43d8d7aad22acabef65f28f7ab057eaeb2c21433309cc41e8 PyQt5==5.10.1 \ --hash=sha256:1e652910bd1ffd23a3a48c510ecad23a57a853ed26b782cd54b16658e6f271ac \ --hash=sha256:4db7113f464c733a99fcb66c4c093a47cf7204ad3f8b3bda502efcc0839ac14b \ --hash=sha256:9c17ab3974c1fc7bbb04cc1c9dae780522c0ebc158613f3025fccae82227b5f7 \ --hash=sha256:f6035baa009acf45e5f460cf88f73580ad5dc0e72330029acd99e477f20a5d61 -setuptools==40.4.3 \ - --hash=sha256:acbc5740dd63f243f46c2b4b8e2c7fd92259c2ddb55a4115b16418a2ed371b15 \ - --hash=sha256:ce4137d58b444bac11a31d4e0c1805c69d89e8ed4e91fde1999674ecc2f6f9ff +setuptools==40.6.2 \ + --hash=sha256:86bb4d8e1b0fabad1f4642b64c335b673e53e7a381de03c9a89fe678152c4c64 \ + --hash=sha256:88ee6bcd5decec9bd902252e02e641851d785c6e5e75677d2744a9d13fed0b0a SIP==4.19.8 \ --hash=sha256:09f9a4e6c28afd0bafedb26ffba43375b97fe7207bd1a0d3513f79b7d168b331 \ --hash=sha256:105edaaa1c8aa486662226360bd3999b4b89dd56de3e314d82b83ed0587d8783 \ @@ -53,6 +51,6 @@ SIP==4.19.8 \ --hash=sha256:cf98150a99e43fda7ae22abe655b6f202e491d6291486548daa56cb15a2fcf85 \ --hash=sha256:d9023422127b94d11c1a84bfa94933e959c484f2c79553c1ef23c69fe00d25f8 \ --hash=sha256:e72955e12f4fccf27aa421be383453d697b8a44bde2cc26b08d876fd492d0174 -wheel==0.32.2 \ - --hash=sha256:196c9842d79262bb66fcf59faa4bd0deb27da911dbc7c6cdca931080eb1f0783 \ - --hash=sha256:c93e2d711f5f9841e17f53b0e6c0ff85593f3b416b6eec7a9452041a59a42688 +wheel==0.32.3 \ + --hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \ + --hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44 diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index d3c9c4e3..e61cb344 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -100,27 +100,27 @@ pyblake2==1.1.2 \ --hash=sha256:baa2190bfe549e36163aa44664d4ee3a9080b236fc5d42f50dc6fd36bbdc749e \ --hash=sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4 \ --hash=sha256:fbc9fcde75713930bc2a91b149e97be2401f7c9c56d735b46a109210f58d7358 -requests==2.20.0 \ - --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ - --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 +requests==2.20.1 \ + --hash=sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54 \ + --hash=sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263 safet==0.1.4 \ --hash=sha256:522c257910f9472e9c77c487425ed286f6721c314653e232bc41c6cedece1bb1 \ --hash=sha256:b152874acdc89ff0c8b2d680bfbf020b3e53527c2ad3404489dd61a548aa56a1 -setuptools==40.4.3 \ - --hash=sha256:acbc5740dd63f243f46c2b4b8e2c7fd92259c2ddb55a4115b16418a2ed371b15 \ - --hash=sha256:ce4137d58b444bac11a31d4e0c1805c69d89e8ed4e91fde1999674ecc2f6f9ff +setuptools==40.6.2 \ + --hash=sha256:86bb4d8e1b0fabad1f4642b64c335b673e53e7a381de03c9a89fe678152c4c64 \ + --hash=sha256:88ee6bcd5decec9bd902252e02e641851d785c6e5e75677d2744a9d13fed0b0a six==1.11.0 \ --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \ --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb trezor==0.10.2 \ --hash=sha256:4dba4d5c53d3ca22884d79fb4aa68905fb8353a5da5f96c734645d8cf537138d \ --hash=sha256:d2b32f25982ab403758d870df1d0de86d0751c106ef1cd1289f452880ce68b84 -urllib3==1.24 \ - --hash=sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae \ - --hash=sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59 -websocket-client==0.53.0 \ - --hash=sha256:c42b71b68f9ef151433d6dcc6a7cb98ac72d2ad1e3a74981ca22bc5d9134f166 \ - --hash=sha256:f5889b1d0a994258cfcbc8f2dc3e457f6fc7b32a8d74873033d12e4eab4bdf63 -wheel==0.32.2 \ - --hash=sha256:196c9842d79262bb66fcf59faa4bd0deb27da911dbc7c6cdca931080eb1f0783 \ - --hash=sha256:c93e2d711f5f9841e17f53b0e6c0ff85593f3b416b6eec7a9452041a59a42688 +urllib3==1.24.1 \ + --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \ + --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 +websocket-client==0.54.0 \ + --hash=sha256:8c8bf2d4f800c3ed952df206b18c28f7070d9e3dcbd6ca6291127574f57ee786 \ + --hash=sha256:e51562c91ddb8148e791f0155fdb01325d99bb52c4cdbb291aee7a3563fd0849 +wheel==0.32.3 \ + --hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \ + --hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44 diff --git a/contrib/deterministic-build/requirements.txt b/contrib/deterministic-build/requirements.txt index a54f984c..99fb37e9 100644 --- a/contrib/deterministic-build/requirements.txt +++ b/contrib/deterministic-build/requirements.txt @@ -23,10 +23,9 @@ aiohttp==3.4.4 \ --hash=sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07 aiohttp_socks==0.2 \ --hash=sha256:eba0a6e198d9a69d254bf956d68cec7615c2a4cadd861b8da46464bd13c5641d -aiorpcX==0.9.0 \ - --hash=sha256:4ad259076a3c94da5265505ef698d04a6d5a92d09e91d2296b5cc09d7d0f0c2c \ - --hash=sha256:71bfd014669bec0ffe2e1b82c1978b2c66330ce5adb3162529a6e066531703e7 \ - --hash=sha256:df621d8a434d4354554496c1e2db74056c88c7e9742cb3e343a22acca27dfc50 +aiorpcX==0.10.1 \ + --hash=sha256:0c0a3342a43d939f00af84684fd08c0c5e7de4fa3eb21740063bea98f6070798 \ + --hash=sha256:58fe42b3695bc4e761b61b9a61416b0c6d69b220630be222b9b129b96ac9c331 async_timeout==3.0.1 \ --hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \ --hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3 @@ -50,39 +49,39 @@ idna==2.7 \ --hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16 idna_ssl==1.1.0 \ --hash=sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c -jsonrpclib-pelix==0.3.1 \ - --hash=sha256:5417b1508d5a50ec64f6e5b88907f111155d52607b218ff3ba9a777afb2e49e3 \ - --hash=sha256:bd89a6093bc4d47dc8a096197aacb827359944a4533be5193f3845f57b9f91b4 -multidict==4.4.2 \ - --hash=sha256:05eeab69bf2b0664644c62bd92fabb045163e5b8d4376a31dfb52ce0210ced7b \ - --hash=sha256:0c85880efa7cadb18e3b5eef0aa075dc9c0a3064cbbaef2e20be264b9cf47a64 \ - --hash=sha256:136f5a4a6a4adeacc4dc820b8b22f0a378fb74f326e259c54d1817639d1d40a0 \ - --hash=sha256:14906ad3347c7d03e9101749b16611cf2028547716d0840838d3c5e2b3b0f2d3 \ - --hash=sha256:1ade4a3b71b1bf9e90c5f3d034a87fe4949c087ef1f6cd727fdd766fe8bbd121 \ - --hash=sha256:22939a00a511a59f9ecc0158b8db728afef57975ce3782b3a265a319d05b9b12 \ - --hash=sha256:2b86b02d872bc5ba5b3a4530f6a7ba0b541458ab4f7c1429a12ac326231203f7 \ - --hash=sha256:3c11e92c3dfc321014e22fb442bc9eb70e01af30d6ce442026b0c35723448c66 \ - --hash=sha256:4ba3bd26f282b201fdbce351f1c5d17ceb224cbedb73d6e96e6ce391b354aacc \ - --hash=sha256:4c6e78d042e93751f60672989efbd6a6bc54213ed7ff695fff82784bbb9ea035 \ - --hash=sha256:4d80d1901b89cc935a6cf5b9fd89df66565272722fe2e5473168927a9937e0ca \ - --hash=sha256:4fcf71d33178a00cc34a57b29f5dab1734b9ce0f1c97fb34666deefac6f92037 \ - --hash=sha256:52f7670b41d4b4d97866ebc38121de8bcb9813128b7c4942b07794d08193c0ab \ - --hash=sha256:5368e2b7649a26b7253c6c9e53241248aab9da49099442f5be238fde436f18c9 \ - --hash=sha256:5bb65fbb48999044938f0c0508e929b14a9b8bf4939d8263e9ea6691f7b54663 \ - --hash=sha256:60672bb5577472800fcca1ac9dae232d1461db9f20f055184be8ce54b0052572 \ - --hash=sha256:669e9be6d148fc0283f53e17dd140cde4dc7c87edac8319147edd5aa2a830771 \ - --hash=sha256:6a0b7a804e8d1716aa2c72e73210b48be83d25ba9ec5cf52cf91122285707bb1 \ - --hash=sha256:79034ea3da3cf2a815e3e52afdc1f6c1894468c98bdce5d2546fa2342585497f \ - --hash=sha256:79247feeef6abcc11137ad17922e865052f23447152059402fc320f99ff544bb \ - --hash=sha256:81671c2049e6bf42c7fd11a060f8bc58f58b7b3d6f3f951fc0b15e376a6a5a98 \ - --hash=sha256:82ac4a5cb56cc9280d4ae52c2d2ebcd6e0668dd0f9ef17f0a9d7c82bd61e24fa \ - --hash=sha256:9436267dbbaa49dad18fbbb54f85386b0f5818d055e7b8e01d219661b6745279 \ - --hash=sha256:94e4140bb1343115a1afd6d84ebf8fca5fb7bfb50e1c2cbd6f2fb5d3117ef102 \ - --hash=sha256:a2cab366eae8a0ffe0813fd8e335cf0d6b9bb6c5227315f53bb457519b811537 \ - --hash=sha256:a596019c3eafb1b0ae07db9f55a08578b43c79adb1fe1ab1fd818430ae59ee6f \ - --hash=sha256:e8848ae3cd6a784c29fae5055028bee9bffcc704d8bcad09bd46b42b44a833e2 \ - --hash=sha256:e8a048bfd7d5a280f27527d11449a509ddedf08b58a09a24314828631c099306 \ - --hash=sha256:f6dd28a0ac60e2426a6918f36f1b4e2620fc785a0de7654cd206ba842eee57fd +jsonrpclib-pelix==0.3.2 \ + --hash=sha256:14d288d1b3d3273cf96a729dd21a2470851c4962be8509f3dd62f0137ff90339 \ + --hash=sha256:27fcd919d3dbf6179bcce587f73e1bad006922ae23c83c308e01227b8533178c +multidict==4.5.1 \ + --hash=sha256:013eb6591ab95173fd3deb7667d80951abac80100335b3e97b5fa778c1bb4b91 \ + --hash=sha256:0bffbbbb48db35f57dfb4733e943ac8178efb31aab5601cb7b303ee228ce96af \ + --hash=sha256:1a34aab1dfba492407c757532f665ba3282ec4a40b0d2f678bda828ef422ebb7 \ + --hash=sha256:1b4b46a33f459a2951b0fd26c2d80639810631eb99b3d846d298b02d28a3e31d \ + --hash=sha256:1d616d80c37a388891bf760d64bc50cac7c61dbb7d7013f2373aa4b44936e9f0 \ + --hash=sha256:225aefa7befbe05bd0116ef87e8cd76cbf4ac39457a66faf7fb5f3c2d7bea19a \ + --hash=sha256:2c9b28985ef7c830d5c7ea344d068bcdee22f8b6c251369dea98c3a814713d44 \ + --hash=sha256:39e0600f8dd72acb011d09960da560ba3451b1eca8de5557c15705afc9d35f0e \ + --hash=sha256:3c642c40ea1ca074397698446893a45cd6059d5d071fc3ba3915c430c125320f \ + --hash=sha256:42357c90b488fac38852bcd7b31dcd36b1e2325413960304c28b8d98e6ff5fd4 \ + --hash=sha256:6ac668f27dbdf8a69c31252f501e128a69a60b43a44e43d712fb58ce3e5dfcca \ + --hash=sha256:713683da2e3f1dd81a920c995df5dda51f1fff2b3995f5864c3ee782fcdcb96c \ + --hash=sha256:73b6e7853b6d3bc0eac795044e700467631dff37a5a33d3230122b03076ac2f9 \ + --hash=sha256:77534c1b9f4a5d0962392cad3f668d1a04036b807618e3357eb2c50d8b05f7f7 \ + --hash=sha256:77b579ef57e27457064bb6bb4c8e5ede866af071af60fe3576226136048c6dfa \ + --hash=sha256:82cf28f18c935d66c15a6f82fda766a4138d21e78532a1946b8ec603019ba0b8 \ + --hash=sha256:937e8f12f9edc0d2e351c09fc3e7335a65eefb75406339d488ee46ef241f75d8 \ + --hash=sha256:985dbf59e92f475573a04598f9a00f92b4fdb64fc41f1df2ea6f33b689319537 \ + --hash=sha256:9c4fab7599ba8c0dbf829272c48c519625c2b7f5630b49925802f1af3a77f1f4 \ + --hash=sha256:9e8772be8455b49a85ad6dbf6ce433da7856ba481d6db36f53507ae540823b15 \ + --hash=sha256:a06d6d88ce3be4b54deabd078810e3c077a8b2e20f0ce541c979b5dd49337031 \ + --hash=sha256:a1da0cdc3bc45315d313af976dab900888dbb477d812997ee0e6e4ea43d325e5 \ + --hash=sha256:a6652466a4800e9fde04bf0252e914fff5f05e2a40ee1453db898149624dfe04 \ + --hash=sha256:a7f23523ea6a01f77e0c6da8aae37ab7943e35630a8d2eda7e49502f36b51b46 \ + --hash=sha256:a87429da49f4c9fb37a6a171fa38b59a99efdeabffb34b4255a7a849ffd74a20 \ + --hash=sha256:c26bb81d0d19619367a96593a097baec2d5a7b3a0cfd1e3a9470277505a465c2 \ + --hash=sha256:d4f4545edb4987f00fde44241cef436bf6471aaac7d21c6bbd497cca6049f613 \ + --hash=sha256:daabc2766a2b76b3bec2086954c48d5f215f75a335eaee1e89c8357922a3c4d5 \ + --hash=sha256:f08c1dcac70b558183b3b755b92f1135a76fd1caa04009b89ddea57a815599aa pip==18.1 \ --hash=sha256:7909d0a0932e88ea53a7014dfd14522ffef91a464daaaf5c573343852ef98550 \ --hash=sha256:c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1 @@ -106,27 +105,27 @@ protobuf==3.6.1 \ --hash=sha256:fcfc907746ec22716f05ea96b7f41597dfe1a1c088f861efb8a0d4f4196a6f10 pyaes==1.6.1 \ --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f -QDarkStyle==2.5.4 \ - --hash=sha256:3eb60922b8c4d9cedecb6897ca4c9f8a259d81bdefe5791976ccdf12432de1f0 \ - --hash=sha256:51331fc6490b38c376e6ba8d8c814320c8d2d1c2663055bc396321a7c28fa8be +QDarkStyle==2.6.4 \ + --hash=sha256:99a21e27405850b4e49610bb7f1720e7f756a9e7b461a4ee54cb6b35cfed3b15 \ + --hash=sha256:e16eae2c3d448b7e0dd13e24b26183bbaae9b1e8fcb2c819c858a3d4bd4caf44 qrcode==6.0 \ --hash=sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf \ --hash=sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3 -requests==2.20.0 \ - --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ - --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 -setuptools==40.4.3 \ - --hash=sha256:acbc5740dd63f243f46c2b4b8e2c7fd92259c2ddb55a4115b16418a2ed371b15 \ - --hash=sha256:ce4137d58b444bac11a31d4e0c1805c69d89e8ed4e91fde1999674ecc2f6f9ff +requests==2.20.1 \ + --hash=sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54 \ + --hash=sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263 +setuptools==40.6.2 \ + --hash=sha256:86bb4d8e1b0fabad1f4642b64c335b673e53e7a381de03c9a89fe678152c4c64 \ + --hash=sha256:88ee6bcd5decec9bd902252e02e641851d785c6e5e75677d2744a9d13fed0b0a six==1.11.0 \ --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \ --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb -urllib3==1.24 \ - --hash=sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae \ - --hash=sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59 -wheel==0.32.2 \ - --hash=sha256:196c9842d79262bb66fcf59faa4bd0deb27da911dbc7c6cdca931080eb1f0783 \ - --hash=sha256:c93e2d711f5f9841e17f53b0e6c0ff85593f3b416b6eec7a9452041a59a42688 +urllib3==1.24.1 \ + --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \ + --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 +wheel==0.32.3 \ + --hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \ + --hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44 yarl==1.2.6 \ --hash=sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9 \ --hash=sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee \ From d7c5949365cf061020e5dfb82ca26171c0692eb6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 26 Nov 2018 01:16:26 +0100 Subject: [PATCH 135/301] prefer int.from_bytes over int('0x'+hex, 16) --- electrum/bip32.py | 2 +- electrum/blockchain.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/electrum/bip32.py b/electrum/bip32.py index 0f671b28..22b0fbce 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -127,7 +127,7 @@ def deserialize_xkey(xkey, prv, *, net=None): fingerprint = xkey[5:9] child_number = xkey[9:13] c = xkey[13:13+32] - header = int('0x' + bh2u(xkey[0:4]), 16) + header = int.from_bytes(xkey[0:4], byteorder='big') headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS if header not in headers.values(): raise InvalidMasterKeyVersionBytes('Invalid extended key format: {}' diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 5609c41e..9f1e0c01 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -56,7 +56,7 @@ def deserialize_header(s: bytes, height: int) -> dict: raise InvalidHeader('Invalid header: {}'.format(s)) if len(s) != HEADER_SIZE: raise InvalidHeader('Invalid header length: {}'.format(len(s))) - hex_to_int = lambda s: int('0x' + bh2u(s[::-1]), 16) + hex_to_int = lambda s: int.from_bytes(s, byteorder='little') h = {} h['version'] = hex_to_int(s[0:4]) h['prev_block_hash'] = hash_encode(s[4:36]) @@ -187,8 +187,9 @@ class Blockchain(util.PrintError): bits = self.target_to_bits(target) if bits != header.get('bits'): raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits'))) - if int('0x' + _hash, 16) > target: - raise Exception("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target)) + block_hash_as_num = int.from_bytes(bfh(_hash), byteorder='big') + if block_hash_as_num > target: + raise Exception(f"insufficient proof of work: {block_hash_as_num} vs target {target}") def verify_chunk(self, index: int, data: bytes) -> None: num = len(data) // HEADER_SIZE @@ -384,7 +385,7 @@ class Blockchain(util.PrintError): c = ("%064x" % target)[2:] while c[:2] == '00' and len(c) > 6: c = c[2:] - bitsN, bitsBase = len(c) // 2, int('0x' + c[:6], 16) + bitsN, bitsBase = len(c) // 2, int.from_bytes(bfh(c[:6]), byteorder='big') if bitsBase >= 0x800000: bitsN += 1 bitsBase >>= 8 From a53dded50fad595dce34be609c59fddad61d66be Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 26 Nov 2018 01:34:23 +0100 Subject: [PATCH 136/301] bitcoin: avoid floating point in int_to_hex --- electrum/bitcoin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 56d07163..194ccfa7 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -60,7 +60,7 @@ def int_to_hex(i: int, length: int=1) -> str: if not isinstance(i, int): raise TypeError('{} instead of int'.format(i)) range_size = pow(256, length) - if i < -range_size/2 or i >= range_size: + if i < -(range_size//2) or i >= range_size: raise OverflowError('cannot convert int {} to hex ({} bytes)'.format(i, length)) if i < 0: # two's complement From d296a1be65d2553bce9faaf448c3f3b8ff459bbf Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Mon, 26 Nov 2018 13:36:51 +0200 Subject: [PATCH 137/301] [macOS] Added optional code signing capability to the OSX build scripts. --- contrib/build-osx/base.sh | 23 +++++++++++++++++++++++ contrib/build-osx/make_osx | 29 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/contrib/build-osx/base.sh b/contrib/build-osx/base.sh index c5a5c0d6..e7454f7a 100644 --- a/contrib/build-osx/base.sh +++ b/contrib/build-osx/base.sh @@ -2,6 +2,7 @@ RED='\033[0;31m' BLUE='\033[0,34m' +YELLOW='\033[0;33m' NC='\033[0m' # No Color function info { printf "\r💬 ${BLUE}INFO:${NC} ${1}\n" @@ -10,3 +11,25 @@ function fail { printf "\r🗯 ${RED}ERROR:${NC} ${1}\n" exit 1 } +function warn { + printf "\r⚠️ ${YELLOW}WARNING:${NC} ${1}\n" +} + +function DoCodeSignMaybe { # ARGS: infoName fileOrDirName codesignIdentity + infoName="$1" + file="$2" + identity="$3" + deep="" + if [ -z "$identity" ]; then + # we are ok with them not passing anything -- master script calls us always even if no identity is specified + return + fi + if [ -d "$file" ]; then + deep="--deep" + fi + if [ -z "$infoName" ] || [ -z "$file" ] || [ -z "$identity" ] || [ ! -e "$file" ]; then + fail "Argument error to internal function DoCodeSignMaybe()" + fi + info "Code signing ${infoName}..." + codesign -f -v $deep -s "$identity" "$file" || fail "Could not code sign ${infoName}" +} diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx index 599480e2..ecccbef3 100755 --- a/contrib/build-osx/make_osx +++ b/contrib/build-osx/make_osx @@ -17,6 +17,24 @@ VERSION=`git describe --tags --dirty --always` which brew > /dev/null 2>&1 || fail "Please install brew from https://brew.sh/ to continue" +# Code Signing: See https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html +APP_SIGN="" +if [ -n "$1" ]; then + # Test the identity is valid for signing by doing this hack. There is no other way to do this. + cp -f /bin/ls ./CODESIGN_TEST + codesign -s "$1" --dryrun -f ./CODESIGN_TEST > /dev/null 2>&1 + res=$? + rm -f ./CODESIGN_TEST + if ((res)); then + fail "Code signing identity \"$1\" appears to be invalid." + fi + unset res + APP_SIGN="$1" + info "Code signing enabled using identity \"$APP_SIGN\"" +else + warn "Code signing DISABLED. Specify a valid macOS Developer identity installed on the system as the first argument to this script to enable signing." +fi + info "Installing Python $PYTHON_VERSION" export PATH="~/.pyenv/bin:~/.pyenv/shims:~/Library/Python/3.6/bin:$PATH" if [ -d "~/.pyenv" ]; then @@ -54,6 +72,7 @@ info "Downloading libusb..." curl https://homebrew.bintray.com/bottles/libusb-1.0.22.el_capitan.bottle.tar.gz | \ tar xz --directory $BUILDDIR cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/build-osx +DoCodeSignMaybe "libusb" "contrib/build-osx/libusb-1.0.dylib" "$APP_SIGN" # If APP_SIGN is empty will be a noop info "Building libsecp256k1" brew install autoconf automake libtool @@ -66,6 +85,7 @@ git clean -f -x -q make popd cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/build-osx +DoCodeSignMaybe "libsecp256k1" "contrib/build-osx/libsecp256k1.0.dylib" "$APP_SIGN" # If APP_SIGN is empty will be a noop info "Installing requirements..." @@ -96,5 +116,14 @@ plutil -insert 'CFBundleURLTypes' \ -- dist/$PACKAGE.app/Contents/Info.plist \ || fail "Could not add keys to Info.plist. Make sure the program 'plutil' exists and is installed." +DoCodeSignMaybe "app bundle" "dist/${PACKAGE}.app" "$APP_SIGN" # If APP_SIGN is empty will be a noop + info "Creating .DMG" hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG" + +DoCodeSignMaybe ".DMG" "dist/electrum-${VERSION}.dmg" "$APP_SIGN" # If APP_SIGN is empty will be a noop + +if [ -z "$APP_SIGN" ]; then + warn "App was built successfully but was not code signed. Users may get security warnings from macOS." + warn "Specify a valid code signing identity as the first argument to this script to enable code signing." +fi From f095b35663f857e07ac97797b739dfe6920256bb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 23 Nov 2018 23:17:08 +0100 Subject: [PATCH 138/301] android: build apk using new python3 p4a toolchain --- electrum/gui/kivy/Readme.md | 85 ++++++++++++++++---------- electrum/gui/kivy/tools/buildozer.spec | 12 ++-- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/electrum/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md index 4c151acc..cbafb938 100644 --- a/electrum/gui/kivy/Readme.md +++ b/electrum/gui/kivy/Readme.md @@ -1,6 +1,9 @@ # Kivy GUI -The Kivy GUI is used with Electrum on Android devices. To generate an APK file, follow these instructions. +The Kivy GUI is used with Electrum on Android devices. +To generate an APK file, follow these instructions. + +Recommended env: Ubuntu 18.04 ## 1. Preliminaries @@ -21,10 +24,7 @@ sudo apt-get install python3-kivy ## 3. Install python-for-android (p4a) p4a is used to package Electrum, Python, SDL and a bootstrap Java app into an APK file. -We patched p4a to add some functionality we need for Electrum. Until those changes are -merged into p4a, you need to merge them locally (into the master branch): - -3.1 [kivy/python-for-android#1217](https://github.com/kivy/python-for-android/pull/1217) +We need some functionality not in p4a master, so for the time being we have our own fork. Something like this should work: @@ -32,12 +32,9 @@ Something like this should work: cd /opt git clone https://github.com/kivy/python-for-android cd python-for-android -git remote add agilewalker https://github.com/agilewalker/python-for-android git remote add sombernight https://github.com/SomberNight/python-for-android git fetch --all -git checkout 93759f36ba45c7bbe0456a4b3e6788622924cbac -git cherry-pick a2fb5ecbc09c4847adbcfd03c6b1ca62b3d09b8d # openssl-fix -git cherry-pick a0ef2007bc60ed642fbd8b61937995dbed0ddd24 # disable backups +git checkout f74226666af69f9915afaee9ef9292db85a6c617 ``` ## 4. Install buildozer @@ -51,31 +48,57 @@ sudo python3 setup.py install ``` 4.2 Install additional dependencies: + ```sh sudo apt-get install python-pip ``` -and the ones listed -[here](https://buildozer.readthedocs.io/en/latest/installation.html#targeting-android). -You will also need +(from [buildozer docs](https://buildozer.readthedocs.io/en/latest/installation.html#targeting-android)) ```sh -python3 -m pip install colorama appdirs sh jinja2 +sudo pip install --upgrade cython==0.21 +sudo dpkg --add-architecture i386 +sudo apt-get update +sudo apt-get install build-essential ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 +``` + +4.3 Download Android NDK +```sh +cd /opt +wget https://dl.google.com/android/repository/android-ndk-r14b-linux-x86_64.zip +unzip android-ndk-r14b-linux-x86_64.zip +``` + +## 5. Some more dependencies + +```sh +python3 -m pip install colorama appdirs sh jinja2 cython==0.29 +sudo apt-get install autotools-dev autoconf libtool pkg-config python3.7 ``` -4.3 Download the [Crystax NDK](https://www.crystax.net/en/download) manually. -Extract into `/opt/crystax-ndk-10.3.2` +## 6. Create the UI Atlas +In the `electrum/gui/kivy` directory of Electrum, run `make theming`. - -## 5. Create the UI Atlas -In the `gui/kivy` directory of Electrum, run `make theming`. - -## 6. Download Electrum dependencies +## 7. Download Electrum dependencies ```sh sudo contrib/make_packages ``` -## 7. Try building the APK and fail +## 8. Try building the APK and fail + +### 1. Try and fail: + +```sh +contrib/make_apk +``` + +Symlink android tools: + +```sh +ln -sf ~/.buildozer/android/platform/android-sdk-24/tools ~/.buildozer/android/platform/android-sdk-24/tools.save +``` + +### 2. Try and fail: ```sh contrib/make_apk @@ -84,47 +107,43 @@ contrib/make_apk During this build attempt, buildozer downloaded some tools, e.g. those needed in the next step. -## 8. Update the Android SDK build tools +## 9. Update the Android SDK build tools ### Method 1: Using the GUI Start the Android SDK manager in GUI mode: - ~/.buildozer/android/platform/android-sdk-20/tools/android + ~/.buildozer/android/platform/android-sdk-24/tools/android Check the latest SDK available and install it ("Android SDK Tools" and "Android SDK Platform-tools"). Close the SDK manager. Repeat until there is no newer version. Reopen the SDK manager, and install the latest build tools - ("Android SDK Build-tools"), 27.0.3 at the time of writing. + ("Android SDK Build-tools"), 28.0.3 at the time of writing. + Install "Android 9">"SDK Platform". Install "Android Support Repository" from the SDK manager (under "Extras"). ### Method 2: Using the command line: Repeat the following command until there is nothing to install: - ~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t tools,platform-tools + ~/.buildozer/android/platform/android-sdk-24/tools/android update sdk -u -t tools,platform-tools Install Build Tools, android API 19 and Android Support Library: - ~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t build-tools-27.0.3,android-19,extra-android-m2repository + ~/.buildozer/android/platform/android-sdk-24/tools/android update sdk -u -t build-tools-28.0.3,android-28,extra-android-m2repository + (FIXME: build-tools is not getting installed?! use GUI for now.) -## 9. Build the APK +## 10. Build the APK ```sh contrib/make_apk ``` # FAQ -## Why do I get errors like `package me.dm7.barcodescanner.zxing does not exist` while compiling? -Update your Android build tools to version 27 like described above. - -## Why do I get errors like `(use -source 7 or higher to enable multi-catch statement)` while compiling? -Make sure that your p4a installation includes commit a3cc78a6d1a107cd3b6bd28db8b80f89e3ecddd2. -Also make sure you have recent SDK tools and platform-tools ## I changed something but I don't see any differences on the phone. What did I do wrong? You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}` diff --git a/electrum/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec index ceaba777..5b8dfb11 100644 --- a/electrum/gui/kivy/tools/buildozer.spec +++ b/electrum/gui/kivy/tools/buildozer.spec @@ -31,7 +31,7 @@ version.filename = %(source.dir)s/electrum/version.py #version = 1.9.8 # (list) Application requirements -requirements = python3crystax==3.6, android, openssl, plyer, kivy==master, libsecp256k1 +requirements = python3, android, openssl, plyer, kivy==master, libffi, libsecp256k1 # (str) Presplash of the application #presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png @@ -55,22 +55,22 @@ fullscreen = False android.permissions = INTERNET, WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE, CAMERA # (int) Android API to use -#android.api = 14 +android.api = 28 # (int) Minimum API required (8 = Android 2.2 devices) -#android.minapi = 8 +android.minapi = 21 # (int) Android SDK version to use -#android.sdk = 21 +android.sdk = 24 # (str) Android NDK version to use -#android.ndk = 9 +android.ndk = 14b # (bool) Use --private data storage (True) or --dir public storage (False) android.private_storage = True # (str) Android NDK directory (if empty, it will be automatically downloaded.) -android.ndk_path = /opt/crystax-ndk-10.3.2 +android.ndk_path = /opt/android-ndk-r14b # (str) Android SDK directory (if empty, it will be automatically downloaded.) #android.sdk_path = From 29b697df1a1db73c513d10022a7e2d95c4035c95 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 24 Nov 2018 03:29:50 +0100 Subject: [PATCH 139/301] android: runtime permission dialog for camera --- .../electrum/qr/SimpleScannerActivity.java | 65 +++++++++++++++---- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/electrum/gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java b/electrum/gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java index 8f471462..2d29d7f1 100644 --- a/electrum/gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java +++ b/electrum/gui/kivy/data/java-classes/org/electrum/qr/SimpleScannerActivity.java @@ -4,6 +4,9 @@ import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.content.Intent; +import android.support.v4.app.ActivityCompat; +import android.Manifest; +import android.content.pm.PackageManager; import java.util.Arrays; @@ -13,28 +16,35 @@ import com.google.zxing.Result; import com.google.zxing.BarcodeFormat; public class SimpleScannerActivity extends Activity implements ZXingScannerView.ResultHandler { - private ZXingScannerView mScannerView; - final String TAG = "org.electrum.SimpleScannerActivity"; + private static final int MY_PERMISSIONS_CAMERA = 1002; - @Override - public void onCreate(Bundle state) { - super.onCreate(state); - mScannerView = new ZXingScannerView(this); // Programmatically initialize the scanner view - mScannerView.setFormats(Arrays.asList(BarcodeFormat.QR_CODE)); - setContentView(mScannerView); // Set the scanner view as the content view - } + private ZXingScannerView mScannerView = null; + final String TAG = "org.electrum.SimpleScannerActivity"; @Override public void onResume() { super.onResume(); - mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results. - mScannerView.startCamera(); // Start camera on resume + if (this.hasPermission()) { + this.startCamera(); + } else { + this.requestPermission(); + } } @Override public void onPause() { super.onPause(); - mScannerView.stopCamera(); // Stop camera on pause + if (null != mScannerView) { + mScannerView.stopCamera(); // Stop camera on pause + } + } + + private void startCamera() { + mScannerView = new ZXingScannerView(this); // Programmatically initialize the scanner view + mScannerView.setFormats(Arrays.asList(BarcodeFormat.QR_CODE)); + setContentView(mScannerView); // Set the scanner view as the content view + mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results. + mScannerView.startCamera(); // Start camera on resume } @Override @@ -45,4 +55,35 @@ public class SimpleScannerActivity extends Activity implements ZXingScannerView. setResult(Activity.RESULT_OK, resultIntent); this.finish(); } + + private boolean hasPermission() { + return (ActivityCompat.checkSelfPermission(this, + Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED); + } + + private void requestPermission() { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.CAMERA}, + MY_PERMISSIONS_CAMERA); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + switch (requestCode) { + case MY_PERMISSIONS_CAMERA: { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // permission was granted, yay! + this.startCamera(); + } else { + // permission denied + this.finish(); + } + return; + } + } + } + } From b21064f16f49b554c64a7c2e9fc895f686f863f9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 26 Nov 2018 17:52:30 +0100 Subject: [PATCH 140/301] android: don't use external storage so that we don't need the extra permission. also because phones these days have enough internal storage for the headers; and maybe it's better even for security reasons to store it there. no upgrade path is provided for the headers stored on external storage, we will litter the filesystem and leave them there. they will be downloaded again into internal storage. --- electrum/gui/kivy/tools/buildozer.spec | 2 +- electrum/util.py | 34 ++------------------------ 2 files changed, 3 insertions(+), 33 deletions(-) diff --git a/electrum/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec index 5b8dfb11..dca6daab 100644 --- a/electrum/gui/kivy/tools/buildozer.spec +++ b/electrum/gui/kivy/tools/buildozer.spec @@ -52,7 +52,7 @@ fullscreen = False # # (list) Permissions -android.permissions = INTERNET, WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE, CAMERA +android.permissions = INTERNET, CAMERA # (int) Android API to use android.api = 28 diff --git a/electrum/util.py b/electrum/util.py index 92526980..9a0e81d2 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -354,41 +354,11 @@ def profiler(func): return lambda *args, **kw_args: do_profile(args, kw_args) -def android_ext_dir(): - import jnius - env = jnius.autoclass('android.os.Environment') - return env.getExternalStorageDirectory().getPath() - def android_data_dir(): import jnius PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity') return PythonActivity.mActivity.getFilesDir().getPath() + '/data' -def android_headers_dir(): - d = android_ext_dir() + '/org.electrum.electrum' - if not os.path.exists(d): - try: - os.mkdir(d) - except FileExistsError: - pass # in case of race - return d - -def android_check_data_dir(): - """ if needed, move old directory to sandbox """ - ext_dir = android_ext_dir() - data_dir = android_data_dir() - old_electrum_dir = ext_dir + '/electrum' - if not os.path.exists(data_dir) and os.path.exists(old_electrum_dir): - import shutil - new_headers_path = android_headers_dir() + '/blockchain_headers' - old_headers_path = old_electrum_dir + '/blockchain_headers' - if not os.path.exists(new_headers_path) and os.path.exists(old_headers_path): - print_error("Moving headers file to", new_headers_path) - shutil.move(old_headers_path, new_headers_path) - print_error("Moving data to", data_dir) - shutil.move(old_electrum_dir, data_dir) - return data_dir - def ensure_sparse_file(filename): # On modern Linux, no need to do anything. @@ -401,7 +371,7 @@ def ensure_sparse_file(filename): def get_headers_dir(config): - return android_headers_dir() if 'ANDROID_DATA' in os.environ else config.path + return config.path def assert_datadir_available(config_path): @@ -484,7 +454,7 @@ def bh2u(x: bytes) -> str: def user_dir(): if 'ANDROID_DATA' in os.environ: - return android_check_data_dir() + return android_data_dir() elif os.name == 'posix': return os.path.join(os.environ["HOME"], ".electrum") elif "APPDATA" in os.environ: From a34d42492def4f173094afe54e142941bfe3ddba Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 26 Nov 2018 03:47:41 +0100 Subject: [PATCH 141/301] android docker build --- electrum/gui/kivy/Makefile | 2 +- electrum/gui/kivy/Readme.md | 177 ++++++------------------- electrum/gui/kivy/tools/Dockerfile | 142 ++++++++++++++++++++ electrum/gui/kivy/tools/build.sh | 9 ++ electrum/gui/kivy/tools/buildozer.spec | 4 +- 5 files changed, 197 insertions(+), 137 deletions(-) create mode 100644 electrum/gui/kivy/tools/Dockerfile create mode 100755 electrum/gui/kivy/tools/build.sh diff --git a/electrum/gui/kivy/Makefile b/electrum/gui/kivy/Makefile index 7e87afa6..868a774a 100644 --- a/electrum/gui/kivy/Makefile +++ b/electrum/gui/kivy/Makefile @@ -11,7 +11,7 @@ prepare: @cp tools/buildozer.spec ../../../buildozer.spec # copy electrum to main.py @cp ../../../run_electrum ../../../main.py - @-if [ ! -d "../../.buildozer" ];then \ + @-if [ ! -d "../../../.buildozer" ];then \ cd ../../..; buildozer android debug;\ cp -f electrum/gui/kivy/tools/blacklist.txt .buildozer/android/platform/python-for-android/src/blacklist.txt;\ rm -rf ./.buildozer/android/platform/python-for-android/dist;\ diff --git a/electrum/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md index cbafb938..83bc01a3 100644 --- a/electrum/gui/kivy/Readme.md +++ b/electrum/gui/kivy/Readme.md @@ -3,147 +3,56 @@ The Kivy GUI is used with Electrum on Android devices. To generate an APK file, follow these instructions. -Recommended env: Ubuntu 18.04 +## Android binary with Docker -## 1. Preliminaries +This assumes an Ubuntu host, but it should not be too hard to adapt to another +similar system. The docker commands should be executed in the project's root +folder. -Make sure the current user can write `/opt` (e.g. `sudo chown username: /opt`). +1. Install Docker -We assume that you already got Electrum to run from source on this machine, -hence have e.g. `git`, `python3-pip` and `python3-setuptools`. + ``` + $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + $ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + $ sudo apt-get update + $ sudo apt-get install -y docker-ce + ``` -## 2. Install kivy +2. Build image -Install kivy for python3 as described [here](https://kivy.org/docs/installation/installation-linux.html). -So for example: -```sh -sudo add-apt-repository ppa:kivy-team/kivy -sudo apt-get install python3-kivy -``` + ``` + $ sudo docker build -t electrum-android-builder-img electrum/gui/kivy/tools + ``` + +3. Build binaries + + ``` + $ sudo docker run \ + --name electrum-android-builder-cont \ + --rm \ + -v $PWD:/home/user/wspace/electrum \ + --workdir /home/user/wspace/electrum \ + electrum-android-builder-img \ + ./electrum/gui/kivy/tools/build.sh + ``` + This mounts the project dir inside the container, + and so the modifications will affect it, e.g. `.buildozer` folder + will be created. + +4. The generated binary is in `./bin`. -## 3. Install python-for-android (p4a) -p4a is used to package Electrum, Python, SDL and a bootstrap Java app into an APK file. -We need some functionality not in p4a master, so for the time being we have our own fork. -Something like this should work: +## FAQ -```sh -cd /opt -git clone https://github.com/kivy/python-for-android -cd python-for-android -git remote add sombernight https://github.com/SomberNight/python-for-android -git fetch --all -git checkout f74226666af69f9915afaee9ef9292db85a6c617 -``` - -## 4. Install buildozer -4.1 Buildozer is a frontend to p4a. Luckily we don't need to patch it: - -```sh -cd /opt -git clone https://github.com/kivy/buildozer -cd buildozer -sudo python3 setup.py install -``` - -4.2 Install additional dependencies: - -```sh -sudo apt-get install python-pip -``` - -(from [buildozer docs](https://buildozer.readthedocs.io/en/latest/installation.html#targeting-android)) -```sh -sudo pip install --upgrade cython==0.21 -sudo dpkg --add-architecture i386 -sudo apt-get update -sudo apt-get install build-essential ccache git libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 python2.7 python2.7-dev openjdk-8-jdk unzip zlib1g-dev zlib1g:i386 -``` - -4.3 Download Android NDK -```sh -cd /opt -wget https://dl.google.com/android/repository/android-ndk-r14b-linux-x86_64.zip -unzip android-ndk-r14b-linux-x86_64.zip -``` - -## 5. Some more dependencies - -```sh -python3 -m pip install colorama appdirs sh jinja2 cython==0.29 -sudo apt-get install autotools-dev autoconf libtool pkg-config python3.7 -``` - - -## 6. Create the UI Atlas -In the `electrum/gui/kivy` directory of Electrum, run `make theming`. - -## 7. Download Electrum dependencies -```sh -sudo contrib/make_packages -``` - -## 8. Try building the APK and fail - -### 1. Try and fail: - -```sh -contrib/make_apk -``` - -Symlink android tools: - -```sh -ln -sf ~/.buildozer/android/platform/android-sdk-24/tools ~/.buildozer/android/platform/android-sdk-24/tools.save -``` - -### 2. Try and fail: - -```sh -contrib/make_apk -``` - -During this build attempt, buildozer downloaded some tools, -e.g. those needed in the next step. - -## 9. Update the Android SDK build tools - -### Method 1: Using the GUI - - Start the Android SDK manager in GUI mode: - - ~/.buildozer/android/platform/android-sdk-24/tools/android - - Check the latest SDK available and install it - ("Android SDK Tools" and "Android SDK Platform-tools"). - Close the SDK manager. Repeat until there is no newer version. - - Reopen the SDK manager, and install the latest build tools - ("Android SDK Build-tools"), 28.0.3 at the time of writing. - - Install "Android 9">"SDK Platform". - Install "Android Support Repository" from the SDK manager (under "Extras"). - -### Method 2: Using the command line: - - Repeat the following command until there is nothing to install: - - ~/.buildozer/android/platform/android-sdk-24/tools/android update sdk -u -t tools,platform-tools - - Install Build Tools, android API 19 and Android Support Library: - - ~/.buildozer/android/platform/android-sdk-24/tools/android update sdk -u -t build-tools-28.0.3,android-28,extra-android-m2repository - - (FIXME: build-tools is not getting installed?! use GUI for now.) - -## 10. Build the APK - -```sh -contrib/make_apk -``` - -# FAQ - -## I changed something but I don't see any differences on the phone. What did I do wrong? +### I changed something but I don't see any differences on the phone. What did I do wrong? You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}` + + +### How do I get an interactive shell inside docker? +``` +$ sudo docker run -it --rm \ + -v $PWD:/home/user/wspace/electrum \ + --workdir /home/user/wspace/electrum \ + electrum-android-builder-img +``` diff --git a/electrum/gui/kivy/tools/Dockerfile b/electrum/gui/kivy/tools/Dockerfile new file mode 100644 index 00000000..0300d862 --- /dev/null +++ b/electrum/gui/kivy/tools/Dockerfile @@ -0,0 +1,142 @@ +# based on https://github.com/kivy/python-for-android/blob/master/Dockerfile + +FROM ubuntu:18.04 + +ENV ANDROID_HOME="/opt/android" + +RUN apt -y update -qq \ + && apt -y install -qq --no-install-recommends curl unzip git python3-pip python3-setuptools \ + && apt -y autoremove \ + && apt -y clean + + +ENV ANDROID_NDK_HOME="${ANDROID_HOME}/android-ndk" +ENV ANDROID_NDK_VERSION="14b" +ENV ANDROID_NDK_HOME_V="${ANDROID_NDK_HOME}-r${ANDROID_NDK_VERSION}" + +# get the latest version from https://developer.android.com/ndk/downloads/index.html +ENV ANDROID_NDK_ARCHIVE="android-ndk-r${ANDROID_NDK_VERSION}-linux-x86_64.zip" +ENV ANDROID_NDK_DL_URL="https://dl.google.com/android/repository/${ANDROID_NDK_ARCHIVE}" + +# download and install Android NDK +RUN curl --location --progress-bar \ + "${ANDROID_NDK_DL_URL}" \ + --output "${ANDROID_NDK_ARCHIVE}" \ + && mkdir --parents "${ANDROID_NDK_HOME_V}" \ + && unzip -q "${ANDROID_NDK_ARCHIVE}" -d "${ANDROID_HOME}" \ + && ln -sfn "${ANDROID_NDK_HOME_V}" "${ANDROID_NDK_HOME}" \ + && rm -rf "${ANDROID_NDK_ARCHIVE}" + + +ENV ANDROID_SDK_HOME="${ANDROID_HOME}/android-sdk" + +# get the latest version from https://developer.android.com/studio/index.html +ENV ANDROID_SDK_TOOLS_VERSION="4333796" +ENV ANDROID_SDK_TOOLS_ARCHIVE="sdk-tools-linux-${ANDROID_SDK_TOOLS_VERSION}.zip" +ENV ANDROID_SDK_TOOLS_DL_URL="https://dl.google.com/android/repository/${ANDROID_SDK_TOOLS_ARCHIVE}" + +# download and install Android SDK +RUN curl --location --progress-bar \ + "${ANDROID_SDK_TOOLS_DL_URL}" \ + --output "${ANDROID_SDK_TOOLS_ARCHIVE}" \ + && mkdir --parents "${ANDROID_SDK_HOME}" \ + && unzip -q "${ANDROID_SDK_TOOLS_ARCHIVE}" -d "${ANDROID_SDK_HOME}" \ + && rm -rf "${ANDROID_SDK_TOOLS_ARCHIVE}" + +# update Android SDK, install Android API, Build Tools... +RUN mkdir --parents "${ANDROID_SDK_HOME}/.android/" \ + && echo '### User Sources for Android SDK Manager' \ + > "${ANDROID_SDK_HOME}/.android/repositories.cfg" + +# accept Android licenses (JDK necessary!) +RUN apt -y update -qq \ + && apt -y install -qq --no-install-recommends openjdk-8-jdk \ + && apt -y autoremove \ + && apt -y clean +RUN yes | "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" --licenses > /dev/null + +# download platforms, API, build tools +RUN "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-24" && \ + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-28" && \ + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;28.0.3" && \ + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "extras;android;m2repository" && \ + chmod +x "${ANDROID_SDK_HOME}/tools/bin/avdmanager" + + +ENV USER="user" +ENV HOME_DIR="/home/${USER}" +ENV WORK_DIR="${HOME_DIR}/wspace" \ + PATH="${HOME_DIR}/.local/bin:${PATH}" + +# install system dependencies +RUN apt -y update -qq \ + && apt -y install -qq --no-install-recommends \ + python virtualenv python-pip wget lbzip2 patch sudo \ + software-properties-common + +# install kivy +RUN add-apt-repository ppa:kivy-team/kivy \ + && apt -y update -qq \ + && apt -y install -qq --no-install-recommends python3-kivy \ + && apt -y autoremove \ + && apt -y clean +RUN python3 -m pip install image + +# build dependencies +# https://buildozer.readthedocs.io/en/latest/installation.html#android-on-ubuntu-16-04-64bit +RUN dpkg --add-architecture i386 \ + && apt -y update -qq \ + && apt -y install -qq --no-install-recommends \ + build-essential ccache git python2.7 python2.7-dev \ + libncurses5:i386 libstdc++6:i386 libgtk2.0-0:i386 \ + libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386 \ + zip zlib1g-dev zlib1g:i386 \ + && apt -y autoremove \ + && apt -y clean + +# specific recipes dependencies (e.g. libffi requires autoreconf binary) +RUN apt -y update -qq \ + && apt -y install -qq --no-install-recommends \ + autoconf automake cmake gettext libltdl-dev libtool pkg-config \ + python3.7 \ + && apt -y autoremove \ + && apt -y clean + + +# prepare non root env +RUN useradd --create-home --shell /bin/bash ${USER} + +# with sudo access and no password +RUN usermod -append --groups sudo ${USER} +RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + + +WORKDIR ${WORK_DIR} + +# user needs ownership/write access to these directories +RUN chown --recursive ${USER} ${WORK_DIR} ${ANDROID_SDK_HOME} +RUN chown ${USER} /opt +USER ${USER} + + +RUN pip install --upgrade cython==0.29 +RUN python3 -m pip install --upgrade cython==0.29 + +# install buildozer +RUN cd /opt \ + && git clone https://github.com/kivy/buildozer \ + && cd buildozer \ + && python3 -m pip install -e . + +# install python-for-android +RUN cd /opt \ + && git clone https://github.com/kivy/python-for-android \ + && cd python-for-android \ + && git remote add sombernight https://github.com/SomberNight/python-for-android \ + && git fetch --all \ + && git checkout f74226666af69f9915afaee9ef9292db85a6c617 \ + && python3 -m pip install -e . + +# build env vars +ENV USE_SDK_WRAPPER=1 +ENV GRADLE_OPTS="-Xmx1536M -Dorg.gradle.jvmargs='-Xmx1536M'" diff --git a/electrum/gui/kivy/tools/build.sh b/electrum/gui/kivy/tools/build.sh new file mode 100755 index 00000000..fa8de30b --- /dev/null +++ b/electrum/gui/kivy/tools/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +pushd electrum/gui/kivy +make theming +popd + +sudo ./contrib/make_packages + +./contrib/make_apk diff --git a/electrum/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec index dca6daab..2c4e7745 100644 --- a/electrum/gui/kivy/tools/buildozer.spec +++ b/electrum/gui/kivy/tools/buildozer.spec @@ -70,10 +70,10 @@ android.ndk = 14b android.private_storage = True # (str) Android NDK directory (if empty, it will be automatically downloaded.) -android.ndk_path = /opt/android-ndk-r14b +android.ndk_path = /opt/android/android-ndk # (str) Android SDK directory (if empty, it will be automatically downloaded.) -#android.sdk_path = +android.sdk_path = /opt/android/android-sdk # (str) Android entry point, default is ok for Kivy-based app #android.entrypoint = org.renpy.android.PythonActivity From 5411ad96335e4acf67de8249ff076e707e362027 Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 27 Nov 2018 15:31:40 +0100 Subject: [PATCH 142/301] plugins can also check maximum library version --- electrum/plugins/hw_wallet/plugin.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index 140c8447..b1eadbd5 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -90,6 +90,9 @@ class HW_PluginBase(BasePlugin): raise NotImplementedError() def check_libraries_available(self) -> bool: + def version_str(t): + return ".".join(str(i) for i in t) + try: library_version = self.get_library_version() except ImportError: @@ -99,9 +102,18 @@ class HW_PluginBase(BasePlugin): self.libraries_available_message = ( _("Library version for '{}' is too old.").format(self.name) + '\nInstalled: {}, Needed: {}' - .format(library_version, self.minimum_library)) + .format(library_version, version_str(self.minimum_library))) self.print_stderr(self.libraries_available_message) return False + elif hasattr(self, "maximum_library") and \ + versiontuple(library_version) >= self.maximum_library: + self.libraries_available_message = ( + _("Library version for '{}' is incompatible.").format(self.name) + + '\nInstalled: {}, Needed: less than {}' + .format(library_version, version_str(self.maximum_library))) + self.print_stderr(self.libraries_available_message) + return False + return True def get_library_not_available_message(self) -> str: From c33c90733008fd8f88a374dfe9098dad9c50b6c9 Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 27 Nov 2018 15:31:55 +0100 Subject: [PATCH 143/301] trezor: update to trezor 0.11.0 --- electrum/plugins/trezor/client.py | 11 - electrum/plugins/trezor/clientbase.py | 346 ++++++++++++++------------ electrum/plugins/trezor/qt.py | 73 ++---- electrum/plugins/trezor/transport.py | 95 ------- electrum/plugins/trezor/trezor.py | 288 ++++++++++----------- 5 files changed, 339 insertions(+), 474 deletions(-) delete mode 100644 electrum/plugins/trezor/client.py delete mode 100644 electrum/plugins/trezor/transport.py diff --git a/electrum/plugins/trezor/client.py b/electrum/plugins/trezor/client.py deleted file mode 100644 index 89b5c292..00000000 --- a/electrum/plugins/trezor/client.py +++ /dev/null @@ -1,11 +0,0 @@ -from trezorlib.client import proto, BaseClient, ProtocolMixin -from .clientbase import TrezorClientBase - -class TrezorClient(TrezorClientBase, ProtocolMixin, BaseClient): - def __init__(self, transport, handler, plugin): - BaseClient.__init__(self, transport=transport) - ProtocolMixin.__init__(self, transport=transport) - TrezorClientBase.__init__(self, handler, plugin, proto) - - -TrezorClientBase.wrap_methods(TrezorClient) diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index 12718741..037afeca 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -4,118 +4,76 @@ from struct import pack from electrum.i18n import _ from electrum.util import PrintError, UserCancelled from electrum.keystore import bip39_normalize_passphrase -from electrum.bip32 import serialize_xpub, convert_bip32_path_to_list_of_uint32 +from electrum.bip32 import serialize_xpub, convert_bip32_path_to_list_of_uint32 as parse_path + +from trezorlib.client import TrezorClient +from trezorlib.exceptions import TrezorFailure, Cancelled +from trezorlib.messages import WordRequestType, FailureType, RecoveryDeviceType +import trezorlib.btc +import trezorlib.device + +MESSAGES = { + 3: _("Confirm the transaction output on your {} device"), + 4: _("Confirm internal entropy on your {} device to begin"), + 5: _("Write down the seed word shown on your {}"), + 6: _("Confirm on your {} that you want to wipe it clean"), + 7: _("Confirm on your {} device the message to sign"), + 8: _("Confirm the total amount spent and the transaction fee on your {} device"), + 10: _("Confirm wallet address on your {} device"), + 14: _("Choose on your {} device where to enter your passphrase"), + 'default': _("Check your {} device to continue"), +} -class GuiMixin(object): - # Requires: self.proto, self.device - - # ref: https://github.com/trezor/trezor-common/blob/44dfb07cfaafffada4b2ce0d15ba1d90d17cf35e/protob/types.proto#L89 - messages = { - 3: _("Confirm the transaction output on your {} device"), - 4: _("Confirm internal entropy on your {} device to begin"), - 5: _("Write down the seed word shown on your {}"), - 6: _("Confirm on your {} that you want to wipe it clean"), - 7: _("Confirm on your {} device the message to sign"), - 8: _("Confirm the total amount spent and the transaction fee on your " - "{} device"), - 10: _("Confirm wallet address on your {} device"), - 14: _("Choose on your {} device where to enter your passphrase"), - 'default': _("Check your {} device to continue"), - } - - def callback_Failure(self, msg): - # BaseClient's unfortunate call() implementation forces us to - # raise exceptions on failure in order to unwind the stack. - # However, making the user acknowledge they cancelled - # gets old very quickly, so we suppress those. The NotInitialized - # one is misnamed and indicates a passphrase request was cancelled. - if msg.code in (self.types.FailureType.PinCancelled, - self.types.FailureType.ActionCancelled, - self.types.FailureType.NotInitialized): - raise UserCancelled() - raise RuntimeError(msg.message) - - def callback_ButtonRequest(self, msg): - message = self.msg - if not message: - message = self.messages.get(msg.code, self.messages['default']) - self.handler.show_message(message.format(self.device), self.cancel) - return self.proto.ButtonAck() - - def callback_PinMatrixRequest(self, msg): - if msg.type == 2: - msg = _("Enter a new PIN for your {}:") - elif msg.type == 3: - msg = (_("Re-enter the new PIN for your {}.\n\n" - "NOTE: the positions of the numbers have changed!")) - else: - msg = _("Enter your current {} PIN:") - pin = self.handler.get_pin(msg.format(self.device)) - if len(pin) > 9: - self.handler.show_error(_('The PIN cannot be longer than 9 characters.')) - pin = '' # to cancel below - if not pin: - return self.proto.Cancel() - return self.proto.PinMatrixAck(pin=pin) - - def callback_PassphraseRequest(self, req): - if req and hasattr(req, 'on_device') and req.on_device is True: - return self.proto.PassphraseAck() - - if self.creating_wallet: - msg = _("Enter a passphrase to generate this wallet. Each time " - "you use this wallet your {} will prompt you for the " - "passphrase. If you forget the passphrase you cannot " - "access the bitcoins in the wallet.").format(self.device) - else: - msg = _("Enter the passphrase to unlock this wallet:") - passphrase = self.handler.get_passphrase(msg, self.creating_wallet) - if passphrase is None: - return self.proto.Cancel() - passphrase = bip39_normalize_passphrase(passphrase) - - ack = self.proto.PassphraseAck(passphrase=passphrase) - length = len(ack.passphrase) - if length > 50: - self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length)) - return self.proto.Cancel() - return ack - - def callback_PassphraseStateRequest(self, msg): - return self.proto.PassphraseStateAck() - - def callback_WordRequest(self, msg): - if (msg.type is not None - and msg.type in (self.types.WordRequestType.Matrix9, - self.types.WordRequestType.Matrix6)): - num = 9 if msg.type == self.types.WordRequestType.Matrix9 else 6 - char = self.handler.get_matrix(num) - if char == 'x': - return self.proto.Cancel() - return self.proto.WordAck(word=char) - - self.step += 1 - msg = _("Step {}/24. Enter seed word as explained on " - "your {}:").format(self.step, self.device) - word = self.handler.get_word(msg) - # Unfortunately the device can't handle self.proto.Cancel() - return self.proto.WordAck(word=word) - - -class TrezorClientBase(GuiMixin, PrintError): - - def __init__(self, handler, plugin, proto): - assert hasattr(self, 'tx_api') # ProtocolMixin already constructed? - self.proto = proto +class TrezorClientBase(PrintError): + def __init__(self, transport, handler, plugin): + self.client = TrezorClient(transport, ui=self) + self.plugin = plugin self.device = plugin.device self.handler = handler - self.tx_api = plugin - self.types = plugin.types + self.msg = None self.creating_wallet = False + + self.in_flow = False + self.used() + def run_flow(self, message=None, creating_wallet=False): + if self.in_flow: + raise RuntimeError("Overlapping call to run_flow") + + self.in_flow = True + self.msg = message + self.creating_wallet = creating_wallet + self.prevent_timeouts() + return self + + def end_flow(self): + self.in_flow = False + self.msg = None + self.creating_wallet = False + self.handler.finished() + self.used() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.end_flow() + if exc_value is not None: + if issubclass(exc_type, Cancelled): + raise UserCancelled from exc_value + elif issubclass(exc_type, TrezorFailure): + raise RuntimeError(exc_value.message) from exc_value + else: + return False + return True + + @property + def features(self): + return self.client.features + def __str__(self): return "%s/%s" % (self.label(), self.features.device_id) @@ -131,8 +89,11 @@ class TrezorClientBase(GuiMixin, PrintError): return not self.features.bootloader_mode def has_usable_connection_with_device(self): + if self.in_flow: + return True + try: - res = self.ping("electrum pinging device") + res = self.client.ping("electrum pinging device") assert res == "electrum pinging device" except BaseException: return False @@ -150,47 +111,41 @@ class TrezorClientBase(GuiMixin, PrintError): self.print_error("timed out") self.clear_session() - @staticmethod - def expand_path(n): - return convert_bip32_path_to_list_of_uint32(n) - - def cancel(self): - '''Provided here as in keepkeylib but not trezorlib.''' - self.transport.write(self.proto.Cancel()) - def i4b(self, x): return pack('>I', x) - def get_xpub(self, bip32_path, xtype): - address_n = self.expand_path(bip32_path) - creating = False - node = self.get_public_node(address_n, creating).node + def get_xpub(self, bip32_path, xtype, creating=False): + address_n = parse_path(bip32_path) + with self.run_flow(creating_wallet=creating): + node = trezorlib.btc.get_public_node(self.client, address_n).node return serialize_xpub(xtype, node.chain_code, node.public_key, node.depth, self.i4b(node.fingerprint), self.i4b(node.child_num)) def toggle_passphrase(self): if self.features.passphrase_protection: - self.msg = _("Confirm on your {} device to disable passphrases") + msg = _("Confirm on your {} device to disable passphrases") else: - self.msg = _("Confirm on your {} device to enable passphrases") + msg = _("Confirm on your {} device to enable passphrases") enabled = not self.features.passphrase_protection - self.apply_settings(use_passphrase=enabled) + with self.run_flow(msg): + trezorlib.device.apply_settings(self.client, use_passphrase=enabled) def change_label(self, label): - self.msg = _("Confirm the new label on your {} device") - self.apply_settings(label=label) + with self.run_flow(_("Confirm the new label on your {} device")): + trezorlib.device.apply_settings(self.client, label=label) def change_homescreen(self, homescreen): - self.msg = _("Confirm on your {} device to change your home screen") - self.apply_settings(homescreen=homescreen) + with self.run_flow(_("Confirm on your {} device to change your home screen")): + trezorlib.device.apply_settings(self.client, homescreen=homescreen) def set_pin(self, remove): if remove: - self.msg = _("Confirm on your {} device to disable PIN protection") + msg = _("Confirm on your {} device to disable PIN protection") elif self.features.pin_protection: - self.msg = _("Confirm on your {} device to change your PIN") + msg = _("Confirm on your {} device to change your PIN") else: - self.msg = _("Confirm on your {} device to set a PIN") - self.change_pin(remove) + msg = _("Confirm on your {} device to set a PIN") + with self.run_flow(msg): + trezorlib.device.change_pin(remove) def clear_session(self): '''Clear the session to force pin (and passphrase if enabled) @@ -198,21 +153,15 @@ class TrezorClientBase(GuiMixin, PrintError): self.print_error("clear session:", self) self.prevent_timeouts() try: - super(TrezorClientBase, self).clear_session() + self.client.clear_session() except BaseException as e: # If the device was removed it has the same effect... self.print_error("clear_session: ignoring error", str(e)) - def get_public_node(self, address_n, creating): - self.creating_wallet = creating - return super(TrezorClientBase, self).get_public_node(address_n) - def close(self): '''Called when Our wallet was closed or the device removed.''' self.print_error("closing client") self.clear_session() - # Release the device - self.transport.close() def firmware_version(self): f = self.features @@ -225,27 +174,112 @@ class TrezorClientBase(GuiMixin, PrintError): """Returns '1' for Trezor One, 'T' for Trezor T.""" return self.features.model - @staticmethod - def wrapper(func): - '''Wrap methods to clear any message box they opened.''' + def show_address(self, address_str, script_type, multisig=None): + coin_name = self.plugin.get_coin_name() + address_n = parse_path(address_str) + with self.run_flow(): + return trezorlib.btc.get_address( + self.client, + coin_name, + address_n, + show_display=True, + script_type=script_type, + multisig=multisig) - def wrapped(self, *args, **kwargs): - try: - self.prevent_timeouts() - return func(self, *args, **kwargs) - finally: - self.used() - self.handler.finished() - self.creating_wallet = False - self.msg = None + def sign_message(self, address_str, message): + coin_name = self.plugin.get_coin_name() + address_n = parse_path(address_str) + with self.run_flow(): + return trezorlib.btc.sign_message( + self.client, + coin_name, + address_n, + message) - return wrapped + def recover_device(self, recovery_type, *args, **kwargs): + input_callback = self.mnemonic_callback(recovery_type) + with self.run_flow(): + return trezorlib.device.recover( + self.client, + *args, + input_callback=input_callback, + **kwargs) - @staticmethod - def wrap_methods(cls): - for method in ['apply_settings', 'change_pin', - 'get_address', 'get_public_node', - 'load_device_by_mnemonic', 'load_device_by_xprv', - 'recovery_device', 'reset_device', 'sign_message', - 'sign_tx', 'wipe_device']: - setattr(cls, method, cls.wrapper(getattr(cls, method))) + # ========= Unmodified trezorlib methods ========= + + def sign_tx(self, *args, **kwargs): + with self.run_flow(): + return trezorlib.btc.sign_tx(self.client, *args, **kwargs) + + def reset_device(self, *args, **kwargs): + with self.run_flow(): + return trezorlib.device.reset(self.client, *args, **kwargs) + + def wipe_device(self, *args, **kwargs): + with self.run_flow(): + return trezorlib.device.wipe(self.client, *args, **kwargs) + + # ========= UI methods ========== + + def button_request(self, code): + message = self.msg or MESSAGES.get(code) or MESSAGES['default'] + self.handler.show_message(message.format(self.device), self.client.cancel) + + def get_pin(self, code=None): + if code == 2: + msg = _("Enter a new PIN for your {}:") + elif code == 3: + msg = (_("Re-enter the new PIN for your {}.\n\n" + "NOTE: the positions of the numbers have changed!")) + else: + msg = _("Enter your current {} PIN:") + pin = self.handler.get_pin(msg.format(self.device)) + if not pin: + raise Cancelled + if len(pin) > 9: + self.handler.show_error(_('The PIN cannot be longer than 9 characters.')) + raise Cancelled + return pin + + def get_passphrase(self): + if self.creating_wallet: + msg = _("Enter a passphrase to generate this wallet. Each time " + "you use this wallet your {} will prompt you for the " + "passphrase. If you forget the passphrase you cannot " + "access the bitcoins in the wallet.").format(self.device) + else: + msg = _("Enter the passphrase to unlock this wallet:") + passphrase = self.handler.get_passphrase(msg, self.creating_wallet) + if passphrase is None: + raise Cancelled + passphrase = bip39_normalize_passphrase(passphrase) + length = len(passphrase) + if length > 50: + self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length)) + raise Cancelled + return passphrase + + def _matrix_char(self, matrix_type): + num = 9 if matrix_type == WordRequestType.Matrix9 else 6 + char = self.handler.get_matrix(num) + if char == 'x': + raise Cancelled + return char + + def mnemonic_callback(self, recovery_type): + if recovery_type is None: + return None + + if recovery_type == RecoveryDeviceType.Matrix: + return self._matrix_char + + step = 0 + def word_callback(_ignored): + nonlocal step + step += 1 + msg = _("Step {}/24. Enter seed word as explained on your {}:").format(step, self.device) + word = self.handler.get_word(msg) + if not word: + raise Cancelled + return word + return word_callback diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index 93f34d17..aaa6aa72 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -12,7 +12,7 @@ from electrum.util import bh2u from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available -from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, +from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX) @@ -197,50 +197,24 @@ class QtPlugin(QtPluginBase): text = widget.toPlainText().strip() return ' '.join(text.split()) - if method in [TIM_NEW, TIM_RECOVER]: - gb = QGroupBox() - hbox1 = QHBoxLayout() - gb.setLayout(hbox1) - vbox.addWidget(gb) - gb.setTitle(_("Select your seed length:")) - bg_numwords = QButtonGroup() - for i, count in enumerate([12, 18, 24]): - rb = QRadioButton(gb) - rb.setText(_("%d words") % count) - bg_numwords.addButton(rb) - bg_numwords.setId(rb, i) - hbox1.addWidget(rb) - rb.setChecked(True) - cb_pin = QCheckBox(_('Enable PIN protection')) - cb_pin.setChecked(True) - else: - text = QTextEdit() - text.setMaximumHeight(60) - if method == TIM_MNEMONIC: - msg = _("Enter your BIP39 mnemonic:") - else: - msg = _("Enter the master private key beginning with xprv:") - def set_enabled(): - from electrum.bip32 import is_xprv - wizard.next_button.setEnabled(is_xprv(clean_text(text))) - text.textChanged.connect(set_enabled) - next_enabled = False + gb = QGroupBox() + hbox1 = QHBoxLayout() + gb.setLayout(hbox1) + vbox.addWidget(gb) + gb.setTitle(_("Select your seed length:")) + bg_numwords = QButtonGroup() + for i, count in enumerate([12, 18, 24]): + rb = QRadioButton(gb) + rb.setText(_("%d words") % count) + bg_numwords.addButton(rb) + bg_numwords.setId(rb, i) + hbox1.addWidget(rb) + rb.setChecked(True) + cb_pin = QCheckBox(_('Enable PIN protection')) + cb_pin.setChecked(True) - vbox.addWidget(QLabel(msg)) - vbox.addWidget(text) - pin = QLineEdit() - pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}'))) - pin.setMaximumWidth(100) - hbox_pin = QHBoxLayout() - hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):"))) - hbox_pin.addWidget(pin) - hbox_pin.addStretch(1) - - if method in [TIM_NEW, TIM_RECOVER]: - vbox.addWidget(WWLabel(RECOMMEND_PIN)) - vbox.addWidget(cb_pin) - else: - vbox.addLayout(hbox_pin) + vbox.addWidget(WWLabel(RECOMMEND_PIN)) + vbox.addWidget(cb_pin) passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT) passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN) @@ -277,14 +251,9 @@ class QtPlugin(QtPluginBase): wizard.exec_layout(vbox, next_enabled=next_enabled) - if method in [TIM_NEW, TIM_RECOVER]: - item = bg_numwords.checkedId() - pin = cb_pin.isChecked() - recovery_type = bg_rectype.checkedId() if bg_rectype else None - else: - item = ' '.join(str(clean_text(text)).split()) - pin = str(pin.text()) - recovery_type = None + item = bg_numwords.checkedId() + pin = cb_pin.isChecked() + recovery_type = bg_rectype.checkedId() if bg_rectype else None return (item, name.text(), pin, cb_phrase.isChecked(), recovery_type) diff --git a/electrum/plugins/trezor/transport.py b/electrum/plugins/trezor/transport.py deleted file mode 100644 index 78c1dd20..00000000 --- a/electrum/plugins/trezor/transport.py +++ /dev/null @@ -1,95 +0,0 @@ -from electrum.util import PrintError - - -class TrezorTransport(PrintError): - - @staticmethod - def all_transports(): - """Reimplemented trezorlib.transport.all_transports so that we can - enable/disable specific transports. - """ - try: - # only to detect trezorlib version - from trezorlib.transport import all_transports - except ImportError: - # old trezorlib. compat for trezorlib < 0.9.2 - transports = [] - try: - from trezorlib.transport_bridge import BridgeTransport - transports.append(BridgeTransport) - except BaseException: - pass - try: - from trezorlib.transport_hid import HidTransport - transports.append(HidTransport) - except BaseException: - pass - try: - from trezorlib.transport_udp import UdpTransport - transports.append(UdpTransport) - except BaseException: - pass - try: - from trezorlib.transport_webusb import WebUsbTransport - transports.append(WebUsbTransport) - except BaseException: - pass - else: - # new trezorlib. - transports = [] - try: - from trezorlib.transport.bridge import BridgeTransport - transports.append(BridgeTransport) - except BaseException: - pass - try: - from trezorlib.transport.hid import HidTransport - transports.append(HidTransport) - except BaseException: - pass - try: - from trezorlib.transport.udp import UdpTransport - transports.append(UdpTransport) - except BaseException: - pass - try: - from trezorlib.transport.webusb import WebUsbTransport - transports.append(WebUsbTransport) - except BaseException: - pass - return transports - return transports - - def enumerate_devices(self): - """Just like trezorlib.transport.enumerate_devices, - but with exception catching, so that transports can fail separately. - """ - devices = [] - for transport in self.all_transports(): - try: - new_devices = transport.enumerate() - except BaseException as e: - self.print_error('enumerate failed for {}. error {}' - .format(transport.__name__, str(e))) - else: - devices.extend(new_devices) - return devices - - def get_transport(self, path=None): - """Reimplemented trezorlib.transport.get_transport, - (1) for old trezorlib - (2) to be able to disable specific transports - (3) to call our own enumerate_devices that catches exceptions - """ - if path is None: - try: - return self.enumerate_devices()[0] - except IndexError: - raise Exception("No TREZOR device found") from None - - def match_prefix(a, b): - return a.startswith(b) or b.startswith(a) - transports = [t for t in self.all_transports() if match_prefix(path, t.PATH_PREFIX)] - if transports: - return transports[0].find_by_path(path) - raise Exception("Unknown path prefix '%s'" % path) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 3f2c938d..1dc858ca 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -1,10 +1,9 @@ -from binascii import hexlify, unhexlify import traceback import sys from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT -from electrum.bip32 import deserialize_xpub +from electrum.bip32 import deserialize_xpub, convert_bip32_path_to_list_of_uint32 as parse_path from electrum import constants from electrum.i18n import _ from electrum.plugin import Device @@ -15,10 +14,31 @@ from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data +try: + import trezorlib + import trezorlib.transport + + from .clientbase import TrezorClientBase + + from trezorlib.messages import ( + RecoveryDeviceType, HDNodeType, HDNodePathType, + InputScriptType, OutputScriptType, MultisigRedeemScriptType, + TxInputType, TxOutputType, TxOutputBinType, TransactionType, SignTx) + + RECOVERY_TYPE_SCRAMBLED_WORDS = RecoveryDeviceType.ScrambledWords + RECOVERY_TYPE_MATRIX = RecoveryDeviceType.Matrix + + TREZORLIB = True +except Exception as e: + import traceback + traceback.print_exc() + TREZORLIB = False + + RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX = range(2) + # TREZOR initialization methods -TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) -RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX = range(0, 2) +TIM_NEW, TIM_RECOVER = range(2) class TrezorKeyStore(Hardware_KeyStore): @@ -37,8 +57,7 @@ class TrezorKeyStore(Hardware_KeyStore): def sign_message(self, sequence, message, password): client = self.get_client() address_path = self.get_derivation() + "/%d/%d"%sequence - address_n = client.expand_path(address_path) - msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) + msg_sig = client.sign_message(address_path, message) return msg_sig.signature def sign_transaction(self, tx, password): @@ -75,37 +94,31 @@ class TrezorPlugin(HW_PluginBase): libraries_URL = 'https://github.com/trezor/python-trezor' minimum_firmware = (1, 5, 2) keystore_class = TrezorKeyStore - minimum_library = (0, 9, 0) + minimum_library = (0, 11, 0) + maximum_library = (0, 12) SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') + DEVICE_IDS = ('TREZOR',) MAX_LABEL_LEN = 32 def __init__(self, parent, config, name): - HW_PluginBase.__init__(self, parent, config, name) + super().__init__(parent, config, name) self.libraries_available = self.check_libraries_available() if not self.libraries_available: return - - from . import client - from . import transport - import trezorlib.messages - self.client_class = client.TrezorClient - self.types = trezorlib.messages - self.DEVICE_IDS = ('TREZOR',) - - self.transport_handler = transport.TrezorTransport() self.device_manager().register_enumerate_func(self.enumerate) def get_library_version(self): - import trezorlib + if not TREZORLIB: + raise ImportError try: return trezorlib.__version__ - except AttributeError: + except Exception: return 'unknown' def enumerate(self): - devices = self.transport_handler.enumerate_devices() + devices = trezorlib.transport.enumerate_devices() return [Device(path=d.get_path(), interface_number=-1, id_=d.get_path(), @@ -117,7 +130,7 @@ class TrezorPlugin(HW_PluginBase): def create_client(self, device, handler): try: self.print_error("connecting to device at", device.path) - transport = self.transport_handler.get_transport(device.path) + transport = trezorlib.transport.get_transport(device.path) except BaseException as e: self.print_error("cannot connect at", device.path, str(e)) return None @@ -128,14 +141,7 @@ class TrezorPlugin(HW_PluginBase): self.print_error("connected to device at", device.path) # note that this call can still raise! - client = self.client_class(transport, handler, self) - - # Try a ping for device sanity - try: - client.ping('t') - except BaseException as e: - self.print_error("ping failed", str(e)) - return None + client = TrezorClientBase(transport, handler, self) if not client.atleast_version(*self.minimum_firmware): msg = (_('Outdated {} firmware for device labelled {}. Please ' @@ -177,8 +183,6 @@ class TrezorPlugin(HW_PluginBase): # Must be short as QT doesn't word-wrap radio button text (TIM_NEW, _("Let the device generate a completely new seed randomly")), (TIM_RECOVER, _("Recover from a seed you have previously written down")), - (TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")), - (TIM_PRIVKEY, _("Upload a master private key")) ] devmgr = self.device_manager() client = devmgr.client_by_id(device_id) @@ -222,49 +226,37 @@ class TrezorPlugin(HW_PluginBase): "the words carefully!"), blocking=True) - language = 'english' devmgr = self.device_manager() client = devmgr.client_by_id(device_id) if method == TIM_NEW: - strength = 64 * (item + 2) # 128, 192 or 256 - u2f_counter = 0 - skip_backup = False - client.reset_device(True, strength, passphrase_protection, - pin_protection, label, language, - u2f_counter, skip_backup) + client.reset_device( + strength=64 * (item + 2), # 128, 192 or 256 + passphrase_protection=passphrase_protection, + pin_protection=pin_protection, + label=label) elif method == TIM_RECOVER: - word_count = 6 * (item + 2) # 12, 18 or 24 - client.step = 0 - if recovery_type == RECOVERY_TYPE_SCRAMBLED_WORDS: - recovery_type_trezor = self.types.RecoveryDeviceType.ScrambledWords - else: - recovery_type_trezor = self.types.RecoveryDeviceType.Matrix - client.recovery_device(word_count, passphrase_protection, - pin_protection, label, language, - type=recovery_type_trezor) + client.recover_device( + recovery_type=recovery_type, + word_count=6 * (item + 2), # 12, 18 or 24 + passphrase_protection=passphrase_protection, + pin_protection=pin_protection, + label=label) if recovery_type == RECOVERY_TYPE_MATRIX: handler.close_matrix_dialog() - elif method == TIM_MNEMONIC: - pin = pin_protection # It's the pin, not a boolean - client.load_device_by_mnemonic(str(item), pin, - passphrase_protection, - label, language) else: - pin = pin_protection # It's the pin, not a boolean - client.load_device_by_xprv(item, pin, passphrase_protection, - label, language) + raise RuntimeError("Unsupported recovery method") def _make_node_path(self, xpub, address_n): _, depth, fingerprint, child_num, chain_code, key = deserialize_xpub(xpub) - node = self.types.HDNodeType( + node = HDNodeType( depth=depth, fingerprint=int.from_bytes(fingerprint, 'big'), child_num=int.from_bytes(child_num, 'big'), chain_code=chain_code, public_key=key, ) - return self.types.HDNodePathType(node=node, address_n=address_n) + return HDNodePathType(node=node, address_n=address_n) def setup_device(self, device_info, wizard, purpose): devmgr = self.device_manager() @@ -275,9 +267,10 @@ class TrezorPlugin(HW_PluginBase): _('Make sure it is in the correct state.')) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) - if not device_info.initialized: + creating = not device_info.initialized + if creating: self.initialize_device(device_id, wizard, client.handler) - client.get_xpub('m', 'standard') + client.get_xpub('m', 'standard', creating) client.used() def get_xpub(self, device_id, derivation, xtype, wizard): @@ -292,33 +285,33 @@ class TrezorPlugin(HW_PluginBase): def get_trezor_input_script_type(self, electrum_txin_type: str): if electrum_txin_type in ('p2wpkh', 'p2wsh'): - return self.types.InputScriptType.SPENDWITNESS + return InputScriptType.SPENDWITNESS if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'): - return self.types.InputScriptType.SPENDP2SHWITNESS + return InputScriptType.SPENDP2SHWITNESS if electrum_txin_type in ('p2pkh', ): - return self.types.InputScriptType.SPENDADDRESS + return InputScriptType.SPENDADDRESS if electrum_txin_type in ('p2sh', ): - return self.types.InputScriptType.SPENDMULTISIG + return InputScriptType.SPENDMULTISIG raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) def get_trezor_output_script_type(self, electrum_txin_type: str): if electrum_txin_type in ('p2wpkh', 'p2wsh'): - return self.types.OutputScriptType.PAYTOWITNESS + return OutputScriptType.PAYTOWITNESS if electrum_txin_type in ('p2wpkh-p2sh', 'p2wsh-p2sh'): - return self.types.OutputScriptType.PAYTOP2SHWITNESS + return OutputScriptType.PAYTOP2SHWITNESS if electrum_txin_type in ('p2pkh', ): - return self.types.OutputScriptType.PAYTOADDRESS + return OutputScriptType.PAYTOADDRESS if electrum_txin_type in ('p2sh', ): - return self.types.OutputScriptType.PAYTOMULTISIG + return OutputScriptType.PAYTOMULTISIG raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) def sign_transaction(self, keystore, tx, prev_tx, xpub_path): - self.prev_tx = prev_tx - self.xpub_path = xpub_path + prev_tx = { txhash: self.electrum_tx_to_txtype(tx, xpub_path) for txhash, tx in prev_tx.items() } client = self.get_client(keystore) - inputs = self.tx_inputs(tx, True) + inputs = self.tx_inputs(tx, xpub_path, True) outputs = self.tx_outputs(keystore.get_derivation(), tx) - signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[0] + details = SignTx(lock_time=tx.locktime) + signatures, _ = client.sign_tx(self.get_coin_name(), inputs, outputs, details=details, prev_txes=prev_tx) signatures = [(bh2u(x) + '01') for x in signatures] tx.update_signatures(signatures) @@ -327,74 +320,50 @@ class TrezorPlugin(HW_PluginBase): keystore = wallet.get_keystore() if not self.show_address_helper(wallet, address, keystore): return - client = self.get_client(keystore) - if not client.atleast_version(1, 3): - keystore.handler.show_error(_("Your device firmware is too old")) - return - change, index = wallet.get_address_index(address) + deriv_suffix = wallet.get_address_index(address) derivation = keystore.derivation - address_path = "%s/%d/%d"%(derivation, change, index) - address_n = client.expand_path(address_path) + address_path = "%s/%d/%d"%(derivation, *deriv_suffix) + script_type = self.get_trezor_input_script_type(wallet.txin_type) + + # prepare multisig, if available: xpubs = wallet.get_master_public_keys() - if len(xpubs) == 1: - script_type = self.get_trezor_input_script_type(wallet.txin_type) - client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) - else: - def f(xpub): - return self._make_node_path(xpub, [change, index]) + if len(xpubs) > 1: pubkeys = wallet.get_public_keys(address) # sort xpubs using the order of pubkeys - sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) - pubkeys = list(map(f, sorted_xpubs)) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=[b''] * wallet.n, - m=wallet.m, - ) - script_type = self.get_trezor_input_script_type(wallet.txin_type) - client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type) + sorted_pairs = sorted(zip(pubkeys, xpubs)) + multisig = self._make_multisig( + wallet.m, + [(xpub, deriv_suffix) for _, xpub in sorted_pairs]) + else: + multisig = None - def tx_inputs(self, tx, for_sig=False): + client = self.get_client(keystore) + client.show_address(address_path, script_type, multisig) + + def tx_inputs(self, tx, xpub_path, for_sig=False): inputs = [] for txin in tx.inputs(): - txinputtype = self.types.TxInputType() + txinputtype = TxInputType() if txin['type'] == 'coinbase': prev_hash = b"\x00"*32 prev_index = 0xffffffff # signed int -1 else: if for_sig: x_pubkeys = txin['x_pubkeys'] - if len(x_pubkeys) == 1: - x_pubkey = x_pubkeys[0] - xpub, s = parse_xpubkey(x_pubkey) - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype._extend_address_n(xpub_n + s) - txinputtype.script_type = self.get_trezor_input_script_type(txin['type']) - else: - def f(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - return self._make_node_path(xpub, s) - pubkeys = list(map(f, x_pubkeys)) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))), - m=txin.get('num_sig'), - ) - script_type = self.get_trezor_input_script_type(txin['type']) - txinputtype = self.types.TxInputType( - script_type=script_type, - multisig=multisig - ) - # find which key is mine - for x_pubkey in x_pubkeys: - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - if xpub in self.xpub_path: - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype._extend_address_n(xpub_n + s) - break + xpubs = [parse_xpubkey(x) for x in x_pubkeys] + multisig = self._make_multisig(txin.get('num_sig'), xpubs, txin.get('signatures')) + script_type = self.get_trezor_input_script_type(txin['type']) + txinputtype = TxInputType( + script_type=script_type, + multisig=multisig) + # find which key is mine + for xpub, deriv in xpubs: + if xpub in xpub_path: + xpub_n = parse_path(xpub_path[xpub]) + txinputtype.address_n = xpub_n + deriv + break - prev_hash = unhexlify(txin['prevout_hash']) + prev_hash = bfh(txin['prevout_hash']) prev_index = txin['prevout_n'] if 'value' in txin: @@ -412,39 +381,44 @@ class TrezorPlugin(HW_PluginBase): return inputs + def _make_multisig(self, m, xpubs, signatures=None): + if len(xpubs) == 1: + return None + + pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] + if signatures is None: + signatures = [b''] * len(pubkeys) + elif len(signatures) != len(pubkeys): + raise RuntimeError('Mismatched number of signatures') + else: + signatures = [bfh(x)[:-1] if x else b'' for x in signatures] + + return MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=signatures, + m=m) + def tx_outputs(self, derivation, tx): def create_output_by_derivation(): script_type = self.get_trezor_output_script_type(info.script_type) - if len(xpubs) == 1: - address_n = self.client_class.expand_path(derivation + "/%d/%d" % index) - txoutputtype = self.types.TxOutputType( - amount=amount, - script_type=script_type, - address_n=address_n, - ) - else: - address_n = self.client_class.expand_path("/%d/%d" % index) - pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs] - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=[b''] * len(pubkeys), - m=m) - txoutputtype = self.types.TxOutputType( - multisig=multisig, - amount=amount, - address_n=self.client_class.expand_path(derivation + "/%d/%d" % index), - script_type=script_type) + deriv = parse_path("/%d/%d" % index) + multisig = self._make_multisig(m, [(xpub, deriv) for xpub in xpubs]) + txoutputtype = TxOutputType( + multisig=multisig, + amount=amount, + address_n=parse_path(derivation + "/%d/%d" % index), + script_type=script_type) return txoutputtype def create_output_by_address(): - txoutputtype = self.types.TxOutputType() + txoutputtype = TxOutputType() txoutputtype.amount = amount if _type == TYPE_SCRIPT: - txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN + txoutputtype.script_type = OutputScriptType.PAYTOOPRETURN txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o) elif _type == TYPE_ADDRESS: - txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS + txoutputtype.script_type = OutputScriptType.PAYTOADDRESS txoutputtype.address = address return txoutputtype @@ -476,23 +450,17 @@ class TrezorPlugin(HW_PluginBase): return outputs - def electrum_tx_to_txtype(self, tx): - t = self.types.TransactionType() + def electrum_tx_to_txtype(self, tx, xpub_path): + t = TransactionType() if tx is None: # probably for segwit input and we don't need this prev txn return t d = deserialize(tx.raw) t.version = d['version'] t.lock_time = d['lockTime'] - inputs = self.tx_inputs(tx) - t._extend_inputs(inputs) - for vout in d['outputs']: - o = t._add_bin_outputs() - o.amount = vout['value'] - o.script_pubkey = bfh(vout['scriptPubKey']) + t.inputs = self.tx_inputs(tx, xpub_path) + t.bin_outputs = [ + TxOutputBinType(amount=vout['value'], script_pubkey=bfh(vout['scriptPubKey'])) + for vout in d['outputs'] + ] return t - - # This function is called from the TREZOR libraries (via tx_api) - def get_tx(self, tx_hash): - tx = self.prev_tx[tx_hash] - return self.electrum_tx_to_txtype(tx) From b040db26a7a8e02c12c8b0aa498e5457fcad54f2 Mon Sep 17 00:00:00 2001 From: matejcik Date: Tue, 27 Nov 2018 16:51:49 +0100 Subject: [PATCH 144/301] drop trezor/client.py from build specs --- contrib/build-osx/osx.spec | 1 - contrib/build-wine/deterministic.spec | 1 - 2 files changed, 2 deletions(-) diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec index 501df88e..86254816 100644 --- a/contrib/build-osx/osx.spec +++ b/contrib/build-osx/osx.spec @@ -60,7 +60,6 @@ a = Analysis([electrum+ MAIN_SCRIPT, electrum+'electrum/commands.py', electrum+'electrum/plugins/cosigner_pool/qt.py', electrum+'electrum/plugins/email_requests/qt.py', - electrum+'electrum/plugins/trezor/client.py', electrum+'electrum/plugins/trezor/qt.py', electrum+'electrum/plugins/safe_t/client.py', electrum+'electrum/plugins/safe_t/qt.py', diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index 875a091a..dafda1c9 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -57,7 +57,6 @@ a = Analysis([home+'run_electrum', home+'electrum/commands.py', home+'electrum/plugins/cosigner_pool/qt.py', home+'electrum/plugins/email_requests/qt.py', - home+'electrum/plugins/trezor/client.py', home+'electrum/plugins/trezor/qt.py', home+'electrum/plugins/safe_t/client.py', home+'electrum/plugins/safe_t/qt.py', From 37b009a342b7a815bb31c63d3d496e019d9b1efa Mon Sep 17 00:00:00 2001 From: Janus Date: Mon, 26 Nov 2018 21:21:02 +0100 Subject: [PATCH 145/301] qt history view custom fiat input fixes previously, when you submitted a fiat value with thousands separator, it would be discarded. --- electrum/exchange_rate.py | 6 ++- electrum/gui/qt/history_list.py | 6 ++- electrum/tests/test_wallet.py | 71 +++++++++++++++++++++++++++++++++ electrum/util.py | 27 +++---------- electrum/wallet.py | 70 +++++++++++++++++++++----------- 5 files changed, 133 insertions(+), 47 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index ae57cab7..4f3e9cb6 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -464,9 +464,13 @@ class FxThread(ThreadJob): d = get_exchanges_by_ccy(history) return d.get(ccy, []) + @staticmethod + def remove_thousands_separator(text): + return text.replace(',', '') # FIXME use THOUSAND_SEPARATOR in util + def ccy_amount_str(self, amount, commas): prec = CCY_PRECISIONS.get(self.ccy, 2) - fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) + fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) # FIXME use util.THOUSAND_SEPARATOR and util.DECIMAL_POINT try: rounded_amount = round(amount, prec) except decimal.InvalidOperation: diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index eb410d95..18e7d1dd 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -275,10 +275,11 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): if value and value < 0: item.setForeground(3, red_brush) item.setForeground(4, red_brush) - if fiat_value and not tx_item['fiat_default']: + if fiat_value is not None and not tx_item['fiat_default']: item.setForeground(6, blue_brush) if tx_hash: item.setData(0, Qt.UserRole, tx_hash) + item.setData(0, Qt.UserRole+1, value) self.insertTopLevelItem(0, item) if current_tx == tx_hash: self.setCurrentItem(item) @@ -286,6 +287,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): def on_edited(self, item, column, prior): '''Called only when the text actually changes''' key = item.data(0, Qt.UserRole) + value = item.data(0, Qt.UserRole+1) text = item.text(column) # fixme if column == 3: @@ -293,7 +295,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): self.update_labels() self.parent.update_completions() elif column == 6: - self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text) + self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, value) self.on_update() def on_doubleclick(self, item, column): diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py index 9117392e..ecfefae6 100644 --- a/electrum/tests/test_wallet.py +++ b/electrum/tests/test_wallet.py @@ -3,9 +3,16 @@ import tempfile import sys import os import json +from decimal import Decimal +from unittest import TestCase +import time from io import StringIO from electrum.storage import WalletStorage, FINAL_SEED_VERSION +from electrum.wallet import Abstract_Wallet +from electrum.exchange_rate import ExchangeBase, FxThread +from electrum.util import TxMinedStatus +from electrum.bitcoin import COIN from . import SequentialTestCase @@ -68,3 +75,67 @@ class TestWalletStorage(WalletTestCase): with open(self.wallet_path, "r") as f: contents = f.read() self.assertEqual(some_dict, json.loads(contents)) + +class FakeExchange(ExchangeBase): + def __init__(self, rate): + super().__init__(lambda self: None, lambda self: None) + self.quotes = {'TEST': rate} + +class FakeFxThread: + def __init__(self, exchange): + self.exchange = exchange + self.ccy = 'TEST' + + remove_thousands_separator = staticmethod(FxThread.remove_thousands_separator) + timestamp_rate = FxThread.timestamp_rate + ccy_amount_str = FxThread.ccy_amount_str + history_rate = FxThread.history_rate + +class FakeWallet: + def __init__(self, fiat_value): + super().__init__() + self.fiat_value = fiat_value + self.transactions = self.verified_tx = {'abc': 'Tx'} + + def get_tx_height(self, txid): + # because we use a current timestamp, and history is empty, + # FxThread.history_rate will use spot prices + return TxMinedStatus(height=10, conf=10, timestamp=time.time(), header_hash='def') + + default_fiat_value = Abstract_Wallet.default_fiat_value + price_at_timestamp = Abstract_Wallet.price_at_timestamp + class storage: + put = lambda self, x: None + +txid = 'abc' +ccy = 'TEST' + +class TestFiat(TestCase): + def setUp(self): + self.value_sat = COIN + self.fiat_value = {} + self.wallet = FakeWallet(fiat_value=self.fiat_value) + self.fx = FakeFxThread(FakeExchange(Decimal('1000.001'))) + default_fiat = Abstract_Wallet.default_fiat_value(self.wallet, txid, self.fx, self.value_sat) + self.assertEqual(Decimal('1000.001'), default_fiat) + self.assertEqual('1,000.00', self.fx.ccy_amount_str(default_fiat, commas=True)) + + def test_save_fiat_and_reset(self): + self.assertEqual(False, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1000.01', self.fx, self.value_sat)) + saved = self.fiat_value[ccy][txid] + self.assertEqual('1,000.01', self.fx.ccy_amount_str(Decimal(saved), commas=True)) + self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '', self.fx, self.value_sat)) + self.assertNotIn(txid, self.fiat_value[ccy]) + # even though we are not setting it to the exact fiat value according to the exchange rate, precision is truncated away + self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1,000.002', self.fx, self.value_sat)) + + def test_too_high_precision_value_resets_with_no_saved_value(self): + self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1,000.001', self.fx, self.value_sat)) + + def test_empty_resets(self): + self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '', self.fx, self.value_sat)) + self.assertNotIn(ccy, self.fiat_value) + + def test_save_garbage(self): + self.assertEqual(False, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, 'garbage', self.fx, self.value_sat)) + self.assertNotIn(ccy, self.fiat_value) diff --git a/electrum/util.py b/electrum/util.py index 92526980..4f06d573 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -39,6 +39,7 @@ import urllib.request, urllib.parse, urllib.error import builtins import json import time +from typing import NamedTuple, Optional import aiohttp from aiohttp_socks import SocksConnector, SocksVer @@ -129,31 +130,15 @@ class UserCancelled(Exception): '''An exception that is suppressed from the user''' pass -class Satoshis(object): - __slots__ = ('value',) - - def __new__(cls, value): - self = super(Satoshis, cls).__new__(cls) - self.value = value - return self - - def __repr__(self): - return 'Satoshis(%d)'%self.value +class Satoshis(NamedTuple): + value: int def __str__(self): return format_satoshis(self.value) + " BTC" -class Fiat(object): - __slots__ = ('value', 'ccy') - - def __new__(cls, value, ccy): - self = super(Fiat, cls).__new__(cls) - self.ccy = ccy - self.value = value - return self - - def __repr__(self): - return 'Fiat(%s)'% self.__str__() +class Fiat(NamedTuple): + value: Optional[Decimal] + ccy: str def __str__(self): if self.value is None or self.value.is_nan(): diff --git a/electrum/wallet.py b/electrum/wallet.py index c104d7be..a7634721 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -247,24 +247,37 @@ class Abstract_Wallet(AddressSynchronizer): self.storage.put('labels', self.labels) return changed - def set_fiat_value(self, txid, ccy, text): + def set_fiat_value(self, txid, ccy, text, fx, value): if txid not in self.transactions: return - if not text: + # since fx is inserting the thousands separator, + # and not util, also have fx remove it + text = fx.remove_thousands_separator(text) + def_fiat = self.default_fiat_value(txid, fx, value) + formatted = fx.ccy_amount_str(def_fiat, commas=False) + def_fiat_rounded = Decimal(formatted) + reset = not text + if not reset: + try: + text_dec = Decimal(text) + text_dec_rounded = Decimal(fx.ccy_amount_str(text_dec, commas=False)) + reset = text_dec_rounded == def_fiat_rounded + except: + # garbage. not resetting, but not saving either + return False + if reset: d = self.fiat_value.get(ccy, {}) if d and txid in d: d.pop(txid) else: - return - else: - try: - Decimal(text) - except: - return + # avoid saving empty dict + return True if ccy not in self.fiat_value: self.fiat_value[ccy] = {} - self.fiat_value[ccy][txid] = text + if not reset: + self.fiat_value[ccy][txid] = text self.storage.put('fiat_value', self.fiat_value) + return reset def get_fiat_value(self, txid, ccy): fiat_value = self.fiat_value.get(ccy, {}).get(txid) @@ -423,21 +436,11 @@ class Abstract_Wallet(AddressSynchronizer): income += value # fiat computations if fx and fx.is_enabled() and fx.get_history_config(): - fiat_value = self.get_fiat_value(tx_hash, fx.ccy) - fiat_default = fiat_value is None - fiat_rate = self.price_at_timestamp(tx_hash, fx.timestamp_rate) - fiat_value = fiat_value if fiat_value is not None else value / Decimal(COIN) * fiat_rate - fiat_fee = tx_fee / Decimal(COIN) * fiat_rate if tx_fee is not None else None - item['fiat_value'] = Fiat(fiat_value, fx.ccy) - item['fiat_fee'] = Fiat(fiat_fee, fx.ccy) if fiat_fee else None - item['fiat_default'] = fiat_default + fiat_fields = self.get_tx_item_fiat(tx_hash, value, fx, tx_fee) + fiat_value = fiat_fields['fiat_value'].value + item.update(fiat_fields) if value < 0: - acquisition_price = - value / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy) - liquidation_price = - fiat_value - item['acquisition_price'] = Fiat(acquisition_price, fx.ccy) - cg = liquidation_price - acquisition_price - item['capital_gain'] = Fiat(cg, fx.ccy) - capital_gains += cg + capital_gains += fiat_fields['capital_gain'].value fiat_expenditures += -fiat_value else: fiat_income += fiat_value @@ -478,6 +481,27 @@ class Abstract_Wallet(AddressSynchronizer): 'summary': summary } + def default_fiat_value(self, tx_hash, fx, value): + return value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) + + def get_tx_item_fiat(self, tx_hash, value, fx, tx_fee): + item = {} + fiat_value = self.get_fiat_value(tx_hash, fx.ccy) + fiat_default = fiat_value is None + fiat_rate = self.price_at_timestamp(tx_hash, fx.timestamp_rate) + fiat_value = fiat_value if fiat_value is not None else self.default_fiat_value(tx_hash, fx, value) + fiat_fee = tx_fee / Decimal(COIN) * fiat_rate if tx_fee is not None else None + item['fiat_value'] = Fiat(fiat_value, fx.ccy) + item['fiat_fee'] = Fiat(fiat_fee, fx.ccy) if fiat_fee else None + item['fiat_default'] = fiat_default + if value < 0: + acquisition_price = - value / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy) + liquidation_price = - fiat_value + item['acquisition_price'] = Fiat(acquisition_price, fx.ccy) + cg = liquidation_price - acquisition_price + item['capital_gain'] = Fiat(cg, fx.ccy) + return item + def get_label(self, tx_hash): label = self.labels.get(tx_hash, '') if label is '': From c5b8706225ad028f3fd306928088b48a7b830ecb Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 27 Nov 2018 18:34:36 +0100 Subject: [PATCH 146/301] simplify test --- electrum/wallet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index a7634721..cc71079d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -272,9 +272,9 @@ class Abstract_Wallet(AddressSynchronizer): else: # avoid saving empty dict return True - if ccy not in self.fiat_value: - self.fiat_value[ccy] = {} - if not reset: + else: + if ccy not in self.fiat_value: + self.fiat_value[ccy] = {} self.fiat_value[ccy][txid] = text self.storage.put('fiat_value', self.fiat_value) return reset From d4d5e32c91788115c754b933a1e8e6e0338fb196 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 27 Nov 2018 21:15:31 +0100 Subject: [PATCH 147/301] qt history list: fix Qt.UserRole collision --- electrum/gui/qt/history_list.py | 30 ++++++++++++++++-------------- electrum/gui/qt/util.py | 2 +- electrum/wallet.py | 8 ++++---- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 18e7d1dd..83e93d06 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -60,6 +60,8 @@ TX_ICONS = [ class HistoryList(MyTreeWidget, AcceptFileDragDrop): filter_columns = [2, 3, 4] # Date, Description, Amount + TX_HASH_ROLE = Qt.UserRole + TX_VALUE_ROLE = Qt.UserRole + 1 def __init__(self, parent=None): MyTreeWidget.__init__(self, parent, self.create_menu, [], 3) @@ -231,7 +233,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): self.years = [str(i) for i in range(start_date.year, end_date.year + 1)] self.period_combo.insertItems(1, self.years) item = self.currentItem() - current_tx = item.data(0, Qt.UserRole) if item else None + current_tx = item.data(0, self.TX_HASH_ROLE) if item else None self.clear() if fx: fx.history_used_spot = False blue_brush = QBrush(QColor("#1E1EFF")) @@ -242,23 +244,23 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): height = tx_item['height'] conf = tx_item['confirmations'] timestamp = tx_item['timestamp'] - value = tx_item['value'].value + value_sat = tx_item['value'].value balance = tx_item['balance'].value label = tx_item['label'] tx_mined_status = TxMinedStatus(height, conf, timestamp, None) status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) has_invoice = self.wallet.invoices.paid.get(tx_hash) icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) - v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) + v_str = self.parent.format_amount(value_sat, is_diff=True, whitespaces=True) balance_str = self.parent.format_amount(balance, whitespaces=True) entry = ['', tx_hash, status_str, label, v_str, balance_str] fiat_value = None - if value is not None and fx and fx.show_history(): + if value_sat is not None and fx and fx.show_history(): fiat_value = tx_item['fiat_value'].value value_str = fx.format_fiat(fiat_value) entry.append(value_str) # fixme: should use is_mine - if value < 0: + if value_sat < 0: entry.append(fx.format_fiat(tx_item['acquisition_price'].value)) entry.append(fx.format_fiat(tx_item['capital_gain'].value)) item = SortableTreeWidgetItem(entry) @@ -272,22 +274,22 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): item.setTextAlignment(i, Qt.AlignRight | Qt.AlignVCenter) if i!=2: item.setFont(i, monospace_font) - if value and value < 0: + if value_sat and value_sat < 0: item.setForeground(3, red_brush) item.setForeground(4, red_brush) if fiat_value is not None and not tx_item['fiat_default']: item.setForeground(6, blue_brush) if tx_hash: - item.setData(0, Qt.UserRole, tx_hash) - item.setData(0, Qt.UserRole+1, value) + item.setData(0, self.TX_HASH_ROLE, tx_hash) + item.setData(0, self.TX_VALUE_ROLE, value_sat) self.insertTopLevelItem(0, item) if current_tx == tx_hash: self.setCurrentItem(item) def on_edited(self, item, column, prior): '''Called only when the text actually changes''' - key = item.data(0, Qt.UserRole) - value = item.data(0, Qt.UserRole+1) + key = item.data(0, self.TX_HASH_ROLE) + value_sat = item.data(0, self.TX_VALUE_ROLE) text = item.text(column) # fixme if column == 3: @@ -295,14 +297,14 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): self.update_labels() self.parent.update_completions() elif column == 6: - self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, value) + self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, value_sat) self.on_update() def on_doubleclick(self, item, column): if self.permit_edit(item, column): super(HistoryList, self).on_doubleclick(item, column) else: - tx_hash = item.data(0, Qt.UserRole) + tx_hash = item.data(0, self.TX_HASH_ROLE) self.show_transaction(tx_hash) def show_transaction(self, tx_hash): @@ -317,7 +319,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): child_count = root.childCount() for i in range(child_count): item = root.child(i) - txid = item.data(0, Qt.UserRole) + txid = item.data(0, self.TX_HASH_ROLE) label = self.wallet.get_label(txid) item.setText(3, label) @@ -340,7 +342,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): if not item: return column = self.currentColumn() - tx_hash = item.data(0, Qt.UserRole) + tx_hash = item.data(0, self.TX_HASH_ROLE) if not tx_hash: return tx = self.wallet.transactions.get(tx_hash) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 5e1912a3..02a73683 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -791,7 +791,7 @@ def get_parent_main_window(widget): return None class SortableTreeWidgetItem(QTreeWidgetItem): - DataRole = Qt.UserRole + 1 + DataRole = Qt.UserRole + 100 def __lt__(self, other): column = self.treeWidget().sortColumn() diff --git a/electrum/wallet.py b/electrum/wallet.py index cc71079d..9a60e180 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -247,13 +247,13 @@ class Abstract_Wallet(AddressSynchronizer): self.storage.put('labels', self.labels) return changed - def set_fiat_value(self, txid, ccy, text, fx, value): + def set_fiat_value(self, txid, ccy, text, fx, value_sat): if txid not in self.transactions: return # since fx is inserting the thousands separator, # and not util, also have fx remove it text = fx.remove_thousands_separator(text) - def_fiat = self.default_fiat_value(txid, fx, value) + def_fiat = self.default_fiat_value(txid, fx, value_sat) formatted = fx.ccy_amount_str(def_fiat, commas=False) def_fiat_rounded = Decimal(formatted) reset = not text @@ -481,8 +481,8 @@ class Abstract_Wallet(AddressSynchronizer): 'summary': summary } - def default_fiat_value(self, tx_hash, fx, value): - return value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) + def default_fiat_value(self, tx_hash, fx, value_sat): + return value_sat / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) def get_tx_item_fiat(self, tx_hash, value, fx, tx_fee): item = {} From 4a7ce238fd312d5ed78fdcce92beec7f2f37ebb4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 27 Nov 2018 21:32:55 +0100 Subject: [PATCH 148/301] qt history list: fix sort order of fiat columns --- electrum/gui/qt/history_list.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 83e93d06..9d05ac74 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -266,7 +266,6 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): item = SortableTreeWidgetItem(entry) item.setIcon(0, icon) item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else "")) - item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf)) if has_invoice: item.setIcon(3, self.icon_cache.get(":icons/seal")) for i in range(len(entry)): @@ -279,6 +278,15 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): item.setForeground(4, red_brush) if fiat_value is not None and not tx_item['fiat_default']: item.setForeground(6, blue_brush) + # sort orders + item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf)) + item.setData(4, SortableTreeWidgetItem.DataRole, value_sat) + item.setData(5, SortableTreeWidgetItem.DataRole, balance) + if fiat_value is not None: + item.setData(6, SortableTreeWidgetItem.DataRole, fiat_value) + if value_sat < 0: + item.setData(7, SortableTreeWidgetItem.DataRole, tx_item['acquisition_price'].value) + item.setData(8, SortableTreeWidgetItem.DataRole, tx_item['capital_gain'].value) if tx_hash: item.setData(0, self.TX_HASH_ROLE, tx_hash) item.setData(0, self.TX_VALUE_ROLE, value_sat) From e12af33626622809640fc29804e0ef14ae3dfb9b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 28 Nov 2018 12:35:53 +0100 Subject: [PATCH 149/301] wallet: cache more in get_tx_fee closes #4879 --- electrum/address_synchronizer.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index e0d1ec24..909ed028 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -717,12 +717,15 @@ class AddressSynchronizer(PrintError): return None if hasattr(tx, '_cached_fee'): return tx._cached_fee - is_relevant, is_mine, v, fee = self.get_wallet_delta(tx) - if fee is None: - txid = tx.txid() - fee = self.tx_fees.get(txid) - if fee is not None: - tx._cached_fee = fee + with self.lock, self.transaction_lock: + is_relevant, is_mine, v, fee = self.get_wallet_delta(tx) + if fee is None: + txid = tx.txid() + fee = self.tx_fees.get(txid) + # cache fees. if wallet is synced, cache all; + # otherwise only cache non-None, as None can still change while syncing + if self.up_to_date or fee is not None: + tx._cached_fee = fee return fee def get_addr_io(self, address): From 99325618a6c6cf27e29fa6015359342d1b5268d0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 28 Nov 2018 15:52:38 +0100 Subject: [PATCH 150/301] wallet: add FIXME re fiat coin_price calculation --- electrum/wallet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/wallet.py b/electrum/wallet.py index 9a60e180..f3f3efe4 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1185,6 +1185,9 @@ class Abstract_Wallet(AddressSynchronizer): """ if txin_value is None: return Decimal('NaN') + # FIXME: this mutual recursion will be really slow and might even reach + # max recursion depth if there are no FX rates available as then + # nothing will be cached. cache_key = "{}:{}:{}".format(str(txid), str(ccy), str(txin_value)) result = self.coin_price_cache.get(cache_key, None) if result is not None: From 505cb2f65db2ef83d02e082acabcd8819dd347b2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 28 Nov 2018 16:43:44 +0100 Subject: [PATCH 151/301] build-wine: update git version --- contrib/build-wine/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build-wine/docker/Dockerfile b/contrib/build-wine/docker/Dockerfile index 340bad00..8ed7a546 100644 --- a/contrib/build-wine/docker/Dockerfile +++ b/contrib/build-wine/docker/Dockerfile @@ -20,7 +20,7 @@ RUN dpkg --add-architecture i386 && \ wine-stable-i386:i386=3.0.1~bionic \ wine-stable:amd64=3.0.1~bionic \ winehq-stable:amd64=3.0.1~bionic \ - git=1:2.17.1-1ubuntu0.3 \ + git=1:2.17.1-1ubuntu0.4 \ p7zip-full=16.02+dfsg-6 \ make=4.1-9.1ubuntu1 \ mingw-w64=5.0.3-1 \ From 243a0e3cf1f3953b2c6c9796fe739613153e191e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 27 Nov 2018 21:35:26 +0100 Subject: [PATCH 152/301] android docker: make_apk optionally takes "release" as arg --- contrib/make_apk | 2 ++ electrum/gui/kivy/Readme.md | 16 +++++++++++----- electrum/gui/kivy/tools/build.sh | 9 --------- 3 files changed, 13 insertions(+), 14 deletions(-) delete mode 100755 electrum/gui/kivy/tools/build.sh diff --git a/contrib/make_apk b/contrib/make_apk index 773aeab5..6940222c 100755 --- a/contrib/make_apk +++ b/contrib/make_apk @@ -2,6 +2,8 @@ pushd ./electrum/gui/kivy/ +make theming + if [[ -n "$1" && "$1" == "release" ]] ; then echo -n Keystore Password: read -s password diff --git a/electrum/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md index 83bc01a3..f394ddf1 100644 --- a/electrum/gui/kivy/Readme.md +++ b/electrum/gui/kivy/Readme.md @@ -24,22 +24,28 @@ folder. $ sudo docker build -t electrum-android-builder-img electrum/gui/kivy/tools ``` -3. Build binaries +3. Prepare pure python dependencies ``` - $ sudo docker run \ + $ sudo ./contrib/make_packages + ``` + +4. Build binaries + + ``` + $ sudo docker run -it --rm \ --name electrum-android-builder-cont \ - --rm \ -v $PWD:/home/user/wspace/electrum \ + -v ~/.keystore:/home/user/.keystore \ --workdir /home/user/wspace/electrum \ electrum-android-builder-img \ - ./electrum/gui/kivy/tools/build.sh + ./contrib/make_apk ``` This mounts the project dir inside the container, and so the modifications will affect it, e.g. `.buildozer` folder will be created. -4. The generated binary is in `./bin`. +5. The generated binary is in `./bin`. diff --git a/electrum/gui/kivy/tools/build.sh b/electrum/gui/kivy/tools/build.sh deleted file mode 100755 index fa8de30b..00000000 --- a/electrum/gui/kivy/tools/build.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -pushd electrum/gui/kivy -make theming -popd - -sudo ./contrib/make_packages - -./contrib/make_apk From d0e6b8c89dee017e4ad790d63e4f642a7e28e67c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 28 Nov 2018 20:54:57 +0100 Subject: [PATCH 153/301] hw: fix passphrase dialog with confirmation closes #4876 --- electrum/gui/qt/installwizard.py | 4 ++-- electrum/gui/qt/password_dialog.py | 17 ++++++++--------- electrum/plugins/hw_wallet/qt.py | 14 ++++++++++---- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index dc4ce28e..ffd18867 100644 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -432,7 +432,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): return slayout.is_ext def pw_layout(self, msg, kind, force_disable_encrypt_cb): - playout = PasswordLayout(None, msg, kind, self.next_button, + playout = PasswordLayout(msg=msg, kind=kind, OK_button=self.next_button, force_disable_encrypt_cb=force_disable_encrypt_cb) playout.encrypt_cb.setChecked(True) self.exec_layout(playout.layout()) @@ -446,7 +446,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): @wizard_dialog def request_storage_encryption(self, run_next): - playout = PasswordLayoutForHW(None, MSG_HW_STORAGE_ENCRYPTION, PW_NEW, self.next_button) + playout = PasswordLayoutForHW(MSG_HW_STORAGE_ENCRYPTION) playout.encrypt_cb.setChecked(True) self.exec_layout(playout.layout()) return playout.encrypt_cb.isChecked() diff --git a/electrum/gui/qt/password_dialog.py b/electrum/gui/qt/password_dialog.py index 66b3f51b..3202618e 100644 --- a/electrum/gui/qt/password_dialog.py +++ b/electrum/gui/qt/password_dialog.py @@ -60,7 +60,7 @@ class PasswordLayout(object): titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")] - def __init__(self, wallet, msg, kind, OK_button, force_disable_encrypt_cb=False): + def __init__(self, msg, kind, OK_button, wallet=None, force_disable_encrypt_cb=False): self.wallet = wallet self.pw = QLineEdit() @@ -169,12 +169,9 @@ class PasswordLayout(object): class PasswordLayoutForHW(object): - def __init__(self, wallet, msg, kind, OK_button): + def __init__(self, msg, wallet=None): self.wallet = wallet - self.kind = kind - self.OK_button = OK_button - vbox = QVBoxLayout() label = QLabel(msg + "\n") label.setWordWrap(True) @@ -254,9 +251,11 @@ class ChangePasswordDialogForSW(ChangePasswordDialogBase): else: msg = _('Your wallet is password protected and encrypted.') msg += ' ' + _('Use this dialog to change your password.') - self.playout = PasswordLayout( - wallet, msg, PW_CHANGE, OK_button, - force_disable_encrypt_cb=not wallet.can_have_keystore_encryption()) + self.playout = PasswordLayout(msg=msg, + kind=PW_CHANGE, + OK_button=OK_button, + wallet=wallet, + force_disable_encrypt_cb=not wallet.can_have_keystore_encryption()) def run(self): if not self.exec_(): @@ -276,7 +275,7 @@ class ChangePasswordDialogForHW(ChangePasswordDialogBase): msg = _('Your wallet file is encrypted.') msg += '\n' + _('Note: If you enable this setting, you will need your hardware device to open your wallet.') msg += '\n' + _('Use this dialog to toggle encryption.') - self.playout = PasswordLayoutForHW(wallet, msg, PW_CHANGE, OK_button) + self.playout = PasswordLayoutForHW(msg) def run(self): if not self.exec_(): diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index 2b5215eb..fc188ad0 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -27,7 +27,8 @@ import threading from PyQt5.Qt import QVBoxLayout, QLabel -from electrum.gui.qt.password_dialog import PasswordDialog, PW_PASSPHRASE + +from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE from electrum.gui.qt.util import * from electrum.i18n import _ @@ -114,11 +115,16 @@ class QtHandlerBase(QObject, PrintError): def passphrase_dialog(self, msg, confirm): # If confirm is true, require the user to enter the passphrase twice parent = self.top_level_window() + d = WindowModalDialog(parent, _("Enter Passphrase")) if confirm: - d = PasswordDialog(parent, None, msg, PW_PASSPHRASE) - confirmed, p, passphrase = d.run() + OK_button = OkButton(d) + playout = PasswordLayout(msg=msg, kind=PW_PASSPHRASE, OK_button=OK_button) + vbox = QVBoxLayout() + vbox.addLayout(playout.layout()) + vbox.addLayout(Buttons(CancelButton(d), OK_button)) + d.setLayout(vbox) + passphrase = playout.new_password() if d.exec_() else None else: - d = WindowModalDialog(parent, _("Enter Passphrase")) pw = QLineEdit() pw.setEchoMode(2) pw.setMinimumWidth(200) From db89286ec3fa19156b7a85628e93469ef1616546 Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Thu, 29 Nov 2018 00:09:06 +0200 Subject: [PATCH 154/301] [macOS] Added QR scanner facility using platform-native helper app. --- .gitmodules | 3 +++ contrib/CalinsQRReader | 1 + contrib/build-osx/base.sh | 2 +- contrib/build-osx/make_osx | 9 +++++++++ contrib/build-osx/osx.spec | 3 +++ electrum/qrscanner.py | 25 ++++++++++++++++++++++++- 6 files changed, 41 insertions(+), 2 deletions(-) create mode 160000 contrib/CalinsQRReader diff --git a/.gitmodules b/.gitmodules index 5a0f914f..34cfeafb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "contrib/deterministic-build/electrum-locale"] path = contrib/deterministic-build/electrum-locale url = https://github.com/spesmilo/electrum-locale +[submodule "contrib/CalinsQRReader"] + path = contrib/CalinsQRReader + url = https://github.com/spesmilo/CalinsQRReader diff --git a/contrib/CalinsQRReader b/contrib/CalinsQRReader new file mode 160000 index 00000000..20189155 --- /dev/null +++ b/contrib/CalinsQRReader @@ -0,0 +1 @@ +Subproject commit 20189155a461cf7fbad14357e58fbc8e7c964608 diff --git a/contrib/build-osx/base.sh b/contrib/build-osx/base.sh index e7454f7a..2c22ca9c 100644 --- a/contrib/build-osx/base.sh +++ b/contrib/build-osx/base.sh @@ -21,7 +21,7 @@ function DoCodeSignMaybe { # ARGS: infoName fileOrDirName codesignIdentity identity="$3" deep="" if [ -z "$identity" ]; then - # we are ok with them not passing anything -- master script calls us always even if no identity is specified + # we are ok with them not passing anything; master script calls us unconditionally even if no identity is specified return fi if [ -d "$file" ]; then diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx index ecccbef3..ff836fd6 100755 --- a/contrib/build-osx/make_osx +++ b/contrib/build-osx/make_osx @@ -16,6 +16,7 @@ export PYTHONHASHSEED=22 VERSION=`git describe --tags --dirty --always` which brew > /dev/null 2>&1 || fail "Please install brew from https://brew.sh/ to continue" +which xcodebuild > /dev/null 2>&1 || fail "Please install Xcode and xcode command line tools to continue" # Code Signing: See https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html APP_SIGN="" @@ -87,6 +88,14 @@ popd cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/build-osx DoCodeSignMaybe "libsecp256k1" "contrib/build-osx/libsecp256k1.0.dylib" "$APP_SIGN" # If APP_SIGN is empty will be a noop +info "Building CalinsQRReader..." +d=contrib/CalinsQRReader +pushd $d +rm -fr build +xcodebuild || fail "Could not build CalinsQRReader" +popd +DoCodeSignMaybe "CalinsQRReader.app" "${d}/build/Release/CalinsQRReader.app" "$APP_SIGN" # If APP_SIGN is empty will be a noop + info "Installing requirements..." python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \ diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec index 501df88e..e48ba97c 100644 --- a/contrib/build-osx/osx.spec +++ b/contrib/build-osx/osx.spec @@ -41,6 +41,9 @@ datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') datas += collect_data_files('ckcc') +# Add the QR Scanner helper app +datas += [(electrum + "contrib/CalinsQRReader/build/Release/CalinsQRReader.app", "./contrib/CalinsQRReader/build/Release/CalinsQRReader.app")] + # Add libusb so Trezor and Safe-T mini will work binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")] binaries += [(electrum + "contrib/build-osx/libsecp256k1.0.dylib", ".")] diff --git a/electrum/qrscanner.py b/electrum/qrscanner.py index 463d5ec6..5d15aad9 100644 --- a/electrum/qrscanner.py +++ b/electrum/qrscanner.py @@ -40,7 +40,7 @@ except BaseException: libzbar = None -def scan_barcode(device='', timeout=-1, display=True, threaded=False, try_again=True): +def scan_barcode_ctypes(device='', timeout=-1, display=True, threaded=False, try_again=True): if libzbar is None: raise RuntimeError("Cannot start QR scanner; zbar not available.") libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p @@ -69,6 +69,29 @@ def scan_barcode(device='', timeout=-1, display=True, threaded=False, try_again= data = libzbar.zbar_symbol_get_data(symbol) return data.decode('utf8') +def scan_barcode_osx(*args_ignored, **kwargs_ignored): + import subprocess + # NOTE: This code needs to be modified if the positions of this file changes with respect to the helper app! + # This assumes the built macOS .app bundle which ends up putting the helper app in + # .app/contrib/CalinsQRReader/build/Release/CalinsQRReader.app. + root_ec_dir = os.path.abspath(os.path.dirname(__file__) + "/../") + prog = root_ec_dir + "/" + "contrib/CalinsQRReader/build/Release/CalinsQRReader.app/Contents/MacOS/CalinsQRReader" + if not os.path.exists(prog): + raise RuntimeError("Cannot start QR scanner; helper app not found.") + data = '' + try: + # This will run the "CalinsQRReader" helper app (which also gets bundled with the built .app) + # Just like the zbar implementation -- the main app will hang until the QR window returns a QR code + # (or is closed). Communication with the subprocess is done via stdout. + # See contrib/CalinsQRReader for the helper app source code. + with subprocess.Popen([prog], stdout=subprocess.PIPE) as p: + data = p.stdout.read().decode('utf-8').strip() + return data + except OSError as e: + raise RuntimeError("Cannot start camera helper app; {}".format(e.strerror)) + +scan_barcode = scan_barcode_osx if sys.platform == 'darwin' else scan_barcode_ctypes + def _find_system_cameras(): device_root = "/sys/class/video4linux" devices = {} # Name -> device From d7bf8826fc1e8c3692504edbe60523dd1327b8a0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 29 Nov 2018 11:39:57 +0100 Subject: [PATCH 155/301] rename contrib/build-osx as contrib/osx. Move QRReader submodule there. --- .gitmodules | 4 ++-- README.rst | 2 +- contrib/{build-osx => osx}/README.md | 4 ++-- contrib/{build-osx => osx}/base.sh | 0 .../{build-osx => osx}/cdrkit-deterministic.patch | 0 contrib/{build-osx => osx}/make_osx | 12 ++++++------ contrib/{build-osx => osx}/osx.spec | 6 +++--- contrib/{build-osx => osx}/package.sh | 0 electrum/qrscanner.py | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) rename contrib/{build-osx => osx}/README.md (93%) rename contrib/{build-osx => osx}/base.sh (100%) rename contrib/{build-osx => osx}/cdrkit-deterministic.patch (100%) rename contrib/{build-osx => osx}/make_osx (91%) rename contrib/{build-osx => osx}/osx.spec (92%) rename contrib/{build-osx => osx}/package.sh (100%) diff --git a/.gitmodules b/.gitmodules index 34cfeafb..c6788ecf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,6 @@ [submodule "contrib/deterministic-build/electrum-locale"] path = contrib/deterministic-build/electrum-locale url = https://github.com/spesmilo/electrum-locale -[submodule "contrib/CalinsQRReader"] - path = contrib/CalinsQRReader +[submodule "contrib/osx/CalinsQRReader"] + path = contrib/osx/CalinsQRReader url = https://github.com/spesmilo/CalinsQRReader diff --git a/README.rst b/README.rst index 3f67724b..1bb140d8 100644 --- a/README.rst +++ b/README.rst @@ -101,7 +101,7 @@ This directory contains the python dependencies used by Electrum. Mac OS X / macOS -------- -See `contrib/build-osx/`. +See `contrib/osx/`. Windows ------- diff --git a/contrib/build-osx/README.md b/contrib/osx/README.md similarity index 93% rename from contrib/build-osx/README.md rename to contrib/osx/README.md index c1e96d90..056d9fb8 100644 --- a/contrib/build-osx/README.md +++ b/contrib/osx/README.md @@ -14,7 +14,7 @@ Before starting, make sure that the Xcode command line tools are installed (e.g. cd electrum - ./contrib/build-osx/make_osx + ./contrib/osx/make_osx This creates a folder named Electrum.app. @@ -33,4 +33,4 @@ Copy the Electrum.app directory over and install the dependencies, e.g.: Then you can just invoke `package.sh` with the path to the app: cd electrum - ./contrib/build-osx/package.sh ~/Electrum.app/ \ No newline at end of file + ./contrib/osx/package.sh ~/Electrum.app/ \ No newline at end of file diff --git a/contrib/build-osx/base.sh b/contrib/osx/base.sh similarity index 100% rename from contrib/build-osx/base.sh rename to contrib/osx/base.sh diff --git a/contrib/build-osx/cdrkit-deterministic.patch b/contrib/osx/cdrkit-deterministic.patch similarity index 100% rename from contrib/build-osx/cdrkit-deterministic.patch rename to contrib/osx/cdrkit-deterministic.patch diff --git a/contrib/build-osx/make_osx b/contrib/osx/make_osx similarity index 91% rename from contrib/build-osx/make_osx rename to contrib/osx/make_osx index ff836fd6..b1cf4b72 100755 --- a/contrib/build-osx/make_osx +++ b/contrib/osx/make_osx @@ -72,8 +72,8 @@ cp ./contrib/deterministic-build/electrum-icons/icons_rc.py ./electrum/gui/qt info "Downloading libusb..." curl https://homebrew.bintray.com/bottles/libusb-1.0.22.el_capitan.bottle.tar.gz | \ tar xz --directory $BUILDDIR -cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/build-osx -DoCodeSignMaybe "libusb" "contrib/build-osx/libusb-1.0.dylib" "$APP_SIGN" # If APP_SIGN is empty will be a noop +cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/osx +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 @@ -85,11 +85,11 @@ git clean -f -x -q ./configure --enable-module-recovery --enable-experimental --enable-module-ecdh --disable-jni make popd -cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/build-osx -DoCodeSignMaybe "libsecp256k1" "contrib/build-osx/libsecp256k1.0.dylib" "$APP_SIGN" # If APP_SIGN is empty will be a noop +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/CalinsQRReader +d=contrib/osx/CalinsQRReader pushd $d rm -fr build xcodebuild || fail "Could not build CalinsQRReader" @@ -117,7 +117,7 @@ for d in ~/Library/Python/ ~/.pyenv .; do done info "Building binary" -pyinstaller --noconfirm --ascii --clean --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary" +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/build-osx/osx.spec b/contrib/osx/osx.spec similarity index 92% rename from contrib/build-osx/osx.spec rename to contrib/osx/osx.spec index e48ba97c..28ac336e 100644 --- a/contrib/build-osx/osx.spec +++ b/contrib/osx/osx.spec @@ -42,11 +42,11 @@ datas += collect_data_files('keepkeylib') datas += collect_data_files('ckcc') # Add the QR Scanner helper app -datas += [(electrum + "contrib/CalinsQRReader/build/Release/CalinsQRReader.app", "./contrib/CalinsQRReader/build/Release/CalinsQRReader.app")] +datas += [(electrum + "contrib/osx/CalinsQRReader/build/Release/CalinsQRReader.app", "./contrib/osx/CalinsQRReader/build/Release/CalinsQRReader.app")] # Add libusb so Trezor and Safe-T mini will work -binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")] -binaries += [(electrum + "contrib/build-osx/libsecp256k1.0.dylib", ".")] +binaries = [(electrum + "contrib/osx/libusb-1.0.dylib", ".")] +binaries += [(electrum + "contrib/osx/libsecp256k1.0.dylib", ".")] # Workaround for "Retro Look": binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]] diff --git a/contrib/build-osx/package.sh b/contrib/osx/package.sh similarity index 100% rename from contrib/build-osx/package.sh rename to contrib/osx/package.sh diff --git a/electrum/qrscanner.py b/electrum/qrscanner.py index 5d15aad9..ab1f1341 100644 --- a/electrum/qrscanner.py +++ b/electrum/qrscanner.py @@ -73,9 +73,9 @@ def scan_barcode_osx(*args_ignored, **kwargs_ignored): import subprocess # NOTE: This code needs to be modified if the positions of this file changes with respect to the helper app! # This assumes the built macOS .app bundle which ends up putting the helper app in - # .app/contrib/CalinsQRReader/build/Release/CalinsQRReader.app. + # .app/contrib/osx/CalinsQRReader/build/Release/CalinsQRReader.app. root_ec_dir = os.path.abspath(os.path.dirname(__file__) + "/../") - prog = root_ec_dir + "/" + "contrib/CalinsQRReader/build/Release/CalinsQRReader.app/Contents/MacOS/CalinsQRReader" + prog = root_ec_dir + "/" + "contrib/osx/CalinsQRReader/build/Release/CalinsQRReader.app/Contents/MacOS/CalinsQRReader" if not os.path.exists(prog): raise RuntimeError("Cannot start QR scanner; helper app not found.") data = '' From f0a59f06cd25fbe4450f720693597967f1d150ba Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 29 Nov 2018 11:46:34 +0100 Subject: [PATCH 156/301] fix module path --- .gitmodules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index c6788ecf..34cfeafb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,6 @@ [submodule "contrib/deterministic-build/electrum-locale"] path = contrib/deterministic-build/electrum-locale url = https://github.com/spesmilo/electrum-locale -[submodule "contrib/osx/CalinsQRReader"] - path = contrib/osx/CalinsQRReader +[submodule "contrib/CalinsQRReader"] + path = contrib/CalinsQRReader url = https://github.com/spesmilo/CalinsQRReader From f4513c12ebd20fe224af14b1279bdfb520e6bba3 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 29 Nov 2018 11:47:02 +0100 Subject: [PATCH 157/301] follow-up --- .gitmodules | 2 +- contrib/{ => osx}/CalinsQRReader | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename contrib/{ => osx}/CalinsQRReader (100%) diff --git a/.gitmodules b/.gitmodules index 34cfeafb..f95b1ebc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,5 +5,5 @@ path = contrib/deterministic-build/electrum-locale url = https://github.com/spesmilo/electrum-locale [submodule "contrib/CalinsQRReader"] - path = contrib/CalinsQRReader + path = contrib/osx/CalinsQRReader url = https://github.com/spesmilo/CalinsQRReader diff --git a/contrib/CalinsQRReader b/contrib/osx/CalinsQRReader similarity index 100% rename from contrib/CalinsQRReader rename to contrib/osx/CalinsQRReader From 124d2e23b7f46863cafb47f2b69b6ba4e07d6791 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 29 Nov 2018 13:24:44 +0100 Subject: [PATCH 158/301] fix travis macOS build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1c222c4f..bfcef023 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,7 +47,7 @@ jobs: python: false install: - git fetch --all --tags - script: ./contrib/build-osx/make_osx + script: ./contrib/osx/make_osx after_script: ls -lah dist && md5 dist/* after_success: true - stage: release check From ee287740a7eeed9b40cfdd85d967009bbbb882ee Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 29 Nov 2018 20:28:27 +0100 Subject: [PATCH 159/301] coldcard: fix p2pkh signing for new fw (1.1.0) PSBT was serialised incorrectly but old fw did not complain --- electrum/plugins/coldcard/coldcard.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index afbb1384..96d80777 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -118,6 +118,8 @@ class CKCCClient: or (self.dev.master_fingerprint != expected_xfp) or (self.dev.master_xpub != expected_xpub)): # probably indicating programing error, not hacking + print_error("[coldcard]", f"xpubs. reported by device: {self.dev.master_xpub}. " + f"stored in file: {expected_xpub}") raise RuntimeError("Expecting 0x%08x but that's not whats connected?!" % expected_xfp) @@ -454,9 +456,12 @@ class Coldcard_KeyStore(Hardware_KeyStore): # inputs section for txin in inputs: - utxo = txin['prev_tx'].outputs()[txin['prevout_n']] - spendable = txin['prev_tx'].serialize_output(utxo) - write_kv(PSBT_IN_WITNESS_UTXO, spendable) + if Transaction.is_segwit_input(txin): + utxo = txin['prev_tx'].outputs()[txin['prevout_n']] + spendable = txin['prev_tx'].serialize_output(utxo) + write_kv(PSBT_IN_WITNESS_UTXO, spendable) + else: + write_kv(PSBT_IN_NON_WITNESS_UTXO, str(txin['prev_tx'])) pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) From 863ee984fee7931c94467c92934caf19f2ec1949 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 29 Nov 2018 20:47:26 +0100 Subject: [PATCH 160/301] wallet: cache NaN coin prices, clear cache on new history --- electrum/gui/kivy/main_window.py | 1 + electrum/gui/qt/main_window.py | 1 + electrum/wallet.py | 13 ++++++------- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 73379bb2..811dda6f 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -173,6 +173,7 @@ class ElectrumWindow(App): def on_history(self, d): Logger.info("on_history") + self.wallet.clear_coin_price_cache() self._trigger_update_history() def on_fee_histogram(self, *args): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 11dad7b6..7c3f5e2e 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -222,6 +222,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.fetch_alias() def on_history(self, b): + self.wallet.clear_coin_price_cache() self.new_fx_history_signal.emit() def setup_exception_hook(self): diff --git a/electrum/wallet.py b/electrum/wallet.py index f3f3efe4..a14bf4f9 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -182,7 +182,7 @@ class Abstract_Wallet(AddressSynchronizer): self.invoices = InvoiceStore(self.storage) self.contacts = Contacts(self.storage) - self.coin_price_cache = {} + self._coin_price_cache = {} def load_and_cleanup(self): self.load_keystore() @@ -1178,6 +1178,9 @@ class Abstract_Wallet(AddressSynchronizer): total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v) return total_price / (input_value/Decimal(COIN)) + def clear_coin_price_cache(self): + self._coin_price_cache = {} + def coin_price(self, txid, price_func, ccy, txin_value): """ Acquisition price of a coin. @@ -1185,17 +1188,13 @@ class Abstract_Wallet(AddressSynchronizer): """ if txin_value is None: return Decimal('NaN') - # FIXME: this mutual recursion will be really slow and might even reach - # max recursion depth if there are no FX rates available as then - # nothing will be cached. cache_key = "{}:{}:{}".format(str(txid), str(ccy), str(txin_value)) - result = self.coin_price_cache.get(cache_key, None) + result = self._coin_price_cache.get(cache_key, None) if result is not None: return result if self.txi.get(txid, {}) != {}: result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN) - if not result.is_nan(): - self.coin_price_cache[cache_key] = result + self._coin_price_cache[cache_key] = result return result else: fiat_value = self.get_fiat_value(txid, ccy) From bddea809ecb66f310a27edf906f38776bf30e02a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 30 Nov 2018 04:08:02 +0100 Subject: [PATCH 161/301] storage/blockchain: use os.replace --- electrum/blockchain.py | 6 +----- electrum/storage.py | 7 +------ 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 0f59211e..018b3adb 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -352,11 +352,7 @@ class Blockchain(util.PrintError): self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(bh2u(parent_data[:HEADER_SIZE])) self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash # parent's new name - try: - os.rename(child_old_name, parent.path()) - except OSError: - os.remove(parent.path()) - os.rename(child_old_name, parent.path()) + os.replace(child_old_name, parent.path()) self.update_size() parent.update_size() # update pointers diff --git a/electrum/storage.py b/electrum/storage.py index ad3de4c6..d526000e 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -122,12 +122,7 @@ class JsonDB(PrintError): os.fsync(f.fileno()) mode = os.stat(self.path).st_mode if os.path.exists(self.path) else stat.S_IREAD | stat.S_IWRITE - # perform atomic write on POSIX systems - try: - os.rename(temp_path, self.path) - except OSError: - os.remove(self.path) - os.rename(temp_path, self.path) + os.replace(temp_path, self.path) os.chmod(self.path, mode) self.print_error("saved", self.path) self.modified = False From 86e42a9081f6d0ca27dc93e8e7d6b2625ca8384f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 30 Nov 2018 11:22:40 +0100 Subject: [PATCH 162/301] release notes for 3.3 --- RELEASE-NOTES | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 9350036e..43486f91 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,15 @@ +# Release 3.3 - (Hodler's Edition) + + * The network layer has been rewritten using asyncio. + * Follow blockchain that has the most work, not length. + * New wallet creation defaults to native segwit (bech32). + * RBF batching (option): If the wallet has an unconfirmed RBF + transaction, new payments will be added to that transaction, + instead of creating new transactions. + * OSX: support QR code scanner. + * Android APK: Use API 28, and do not use external storage. + + # Release 3.2.3 - (September 3, 2018) * hardware wallet: the Safe-T mini from Archos is now supported. From 1165d3f330c9be0318a262856af89938a1257641 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 30 Nov 2018 11:23:01 +0100 Subject: [PATCH 163/301] update version number --- electrum/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/version.py b/electrum/version.py index 53387af3..5866941f 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '3.2.3' # version of the client package -APK_VERSION = '3.2.3.1' # read by buildozer.spec +ELECTRUM_VERSION = '3.3.0' # version of the client package +APK_VERSION = '3.3.0.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From 73e2b09ba82fcc34f2762500b7ab3d7b839b5162 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 30 Nov 2018 16:36:37 +0100 Subject: [PATCH 164/301] blockchain: check best chain on disk is consistent with checkpoints had a corrupted mainnet datadir that had testnet blockchain_headers file (I had probably corrupted it myself but electrum could not recover from it) --- electrum/blockchain.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 018b3adb..92a58723 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -86,11 +86,20 @@ blockchains_lock = threading.RLock() def read_blockchains(config: 'SimpleConfig'): - blockchains[constants.net.GENESIS] = Blockchain(config=config, - forkpoint=0, - parent=None, - forkpoint_hash=constants.net.GENESIS, - prev_hash=None) + best_chain = Blockchain(config=config, + forkpoint=0, + parent=None, + forkpoint_hash=constants.net.GENESIS, + prev_hash=None) + blockchains[constants.net.GENESIS] = best_chain + # consistency checks + if best_chain.height() > constants.net.max_checkpoint(): + header_after_cp = best_chain.read_header(constants.net.max_checkpoint()+1) + if not header_after_cp or not best_chain.can_connect(header_after_cp, check_height=False): + util.print_error("[blockchain] deleting best chain. cannot connect header after last cp to last cp.") + os.unlink(best_chain.path()) + best_chain.update_size() + # forks fdir = os.path.join(util.get_headers_dir(config), 'forks') util.make_dir(fdir) # files are named as: fork2_{forkpoint}_{prev_hash}_{first_hash} @@ -98,7 +107,7 @@ def read_blockchains(config: 'SimpleConfig'): l = sorted(l, key=lambda x: int(x.split('_')[1])) # sort by forkpoint def delete_chain(filename, reason): - util.print_error("[blockchain]", reason, filename) + util.print_error(f"[blockchain] deleting chain {filename}: {reason}") os.unlink(os.path.join(fdir, filename)) def instantiate_chain(filename): @@ -222,10 +231,10 @@ class Blockchain(util.PrintError): prev_hash=parent.get_hash(forkpoint-1)) open(self.path(), 'w+').close() self.save_header(header) - # put into global dict + # put into global dict. note that in some cases + # save_header might have already put it there but that's OK chain_id = self.get_id() with blockchains_lock: - assert chain_id not in blockchains, (chain_id, list(blockchains)) blockchains[chain_id] = self return self @@ -392,7 +401,7 @@ class Blockchain(util.PrintError): delta = header.get('block_height') - self.forkpoint data = bfh(serialize_header(header)) # headers are only _appended_ to the end: - assert delta == self.size() + assert delta == self.size(), (delta, self.size()) assert len(data) == HEADER_SIZE self.write(data, delta*HEADER_SIZE) self.swap_with_parent() From ed22f968f980ddc8d3bfcb203adf32f554493bcd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 30 Nov 2018 17:18:06 +0100 Subject: [PATCH 165/301] text gui: fix network event handler --- electrum/gui/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/text.py b/electrum/gui/text.py index 7d429ae0..ec5ba5c9 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -91,7 +91,7 @@ class ElectrumGui: self.set_cursor(0) return s - def update(self, event): + def update(self, event, *args): self.update_history() if self.tab == 0: self.print_history() From fe6367cbcd91fc10664417feb88ec0ff8d3f74b2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 30 Nov 2018 18:56:35 +0100 Subject: [PATCH 166/301] network: validate donation address for server --- electrum/network.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/electrum/network.py b/electrum/network.py index 85c8dbee..9fe78a5e 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -44,6 +44,7 @@ from .util import PrintError, print_error, log_exceptions, ignore_exceptions, bf from .bitcoin import COIN from . import constants from . import blockchain +from . import bitcoin from .blockchain import Blockchain, HEADER_SIZE from .interface import Interface, serialize_server, deserialize_server, RequestTimedOut from .version import PROTOCOL_VERSION @@ -321,7 +322,11 @@ class Network(PrintError): self.banner = await session.send_request('server.banner') self.notify('banner') async def get_donation_address(): - self.donation_address = await session.send_request('server.donation_address') + addr = await session.send_request('server.donation_address') + if not bitcoin.is_address(addr): + self.print_error(f"invalid donation address from server: {addr}") + addr = '' + self.donation_address = addr async def get_server_peers(): self.server_peers = parse_servers(await session.send_request('server.peers.subscribe')) self.notify('servers') From ec5f406f4904c1b53bdb55584da760141c502cad Mon Sep 17 00:00:00 2001 From: Janus Date: Fri, 30 Nov 2018 19:16:07 +0100 Subject: [PATCH 167/301] plugins: labels: dump response if malformed sync server response --- electrum/plugins/labels/labels.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py index 9405e70e..3c5ff206 100644 --- a/electrum/plugins/labels/labels.py +++ b/electrum/plugins/labels/labels.py @@ -73,7 +73,10 @@ class LabelsPlugin(BasePlugin): url = 'https://' + self.target_host + url async with make_aiohttp_session(self.proxy) as session: async with session.post(url, json=data) as result: - return await result.json() + try: + return await result.json() + except Exception as e: + raise Exception('Could not decode: ' + await result.text()) from e async def push_thread(self, wallet): wallet_data = self.wallets.get(wallet, None) From 74f6ac27af74b5e2a81bd98e3f883a946720fdef Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 30 Nov 2018 20:45:54 +0100 Subject: [PATCH 168/301] wizard/hw: cap transport string follow-up 32af83b7aede02f0b9bb3e8294ef2dc0481fb6de --- electrum/base_wizard.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 84323226..4f390979 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -283,7 +283,9 @@ class BaseWizard(object): for name, info in devices: state = _("initialized") if info.initialized else _("wiped") label = info.label or _("An unnamed {}").format(name) - descr = f"{label} [{name}, {state}, {info.device.transport_ui_string}]" + try: transport_str = info.device.transport_ui_string[:20] + except: transport_str = 'unknown transport' + descr = f"{label} [{name}, {state}, {transport_str}]" choices.append(((name, info), descr)) msg = _('Select a device') + ':' self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose)) From 2f4b9aa1f009dd2e3cdcb571d901e5ae0aa62689 Mon Sep 17 00:00:00 2001 From: Ken <41596906+preserveddarnell@users.noreply.github.com> Date: Sat, 1 Dec 2018 21:28:46 -0500 Subject: [PATCH 169/301] Update README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 1bb140d8..6dfb6994 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ Qt interface, install the Qt dependencies:: sudo apt-get install python3-pyqt5 If you downloaded the official package (tar.gz), you can run -Electrum from its root directory, without installing it on your +Electrum from its root directory without installing it on your system; all the python dependencies are included in the 'packages' directory. To run Electrum from its root directory, just do:: @@ -44,7 +44,7 @@ You can also install Electrum on your system, by running this command:: python3 -m pip install .[fast] This will download and install the Python dependencies used by -Electrum, instead of using the 'packages' directory. +Electrum instead of using the 'packages' directory. The 'fast' extra contains some optional dependencies that we think are often useful but they are not strictly needed. From d2374d62aad751733999f188495703e3522b01ef Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Sun, 2 Dec 2018 14:53:44 +0200 Subject: [PATCH 170/301] UI Pet Peeve: Make Coins Tab -> Details pop up a tx dialog that actually includes the tx description as seen in UTXOList (if available) --- electrum/gui/qt/utxo_list.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 5985d9c8..64735de1 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -71,7 +71,8 @@ class UTXOList(MyTreeWidget): txid = selected[0].split(':')[0] tx = self.wallet.transactions.get(txid) if tx: - menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) + label = self.wallet.get_label(txid) + menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label)) menu.exec_(self.viewport().mapToGlobal(position)) From 4386799fb0f02b785ed5d5c7a225d31a33eafc08 Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Sun, 2 Dec 2018 15:20:32 +0200 Subject: [PATCH 171/301] follow-up --- electrum/gui/qt/utxo_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 64735de1..0a6dc334 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -71,7 +71,7 @@ class UTXOList(MyTreeWidget): txid = selected[0].split(':')[0] tx = self.wallet.transactions.get(txid) if tx: - label = self.wallet.get_label(txid) + label = self.wallet.get_label(txid) or None # Prefer None if empty (None hides the Description: field in the window) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label)) menu.exec_(self.viewport().mapToGlobal(position)) From ff454ab29dd374fb2998ef748ab3969e13e0f172 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 3 Dec 2018 12:46:12 +0100 Subject: [PATCH 172/301] cli restore: fix imported privkeys with password closes #4894 --- electrum/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/commands.py b/electrum/commands.py index 40a8142c..3acf7952 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -176,7 +176,7 @@ class Commands: storage.put('keystore', k.dump()) wallet = Imported_Wallet(storage) keys = keystore.get_private_keys(text) - good_inputs, bad_inputs = wallet.import_private_keys(keys, password) + good_inputs, bad_inputs = wallet.import_private_keys(keys, None) # FIXME tell user about bad_inputs if not good_inputs: raise Exception("None of the given privkeys can be imported") From 9350709f13bc7e3d79b8e0f1515a3fdba4f2cbff Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 3 Dec 2018 13:02:14 +0100 Subject: [PATCH 173/301] wallet creation: take care not to write plaintext keys to disk when creating imported privkey wallets the privkeys were written to disk unencrypted first, then overwritten with ciphertext --- electrum/base_wizard.py | 3 ++- electrum/commands.py | 3 ++- electrum/wallet.py | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 4f390979..7efd8229 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -200,7 +200,7 @@ class BaseWizard(object): self.storage.put('keystore', k.dump()) w = Imported_Wallet(self.storage) keys = keystore.get_private_keys(text) - good_inputs, bad_inputs = w.import_private_keys(keys, None) + good_inputs, bad_inputs = w.import_private_keys(keys, None, write_to_disk=False) self.keystores.append(w.keystore) else: return self.terminate() @@ -510,6 +510,7 @@ class BaseWizard(object): def on_password(self, password, *, encrypt_storage, storage_enc_version=STO_EV_USER_PW, encrypt_keystore): + assert not self.storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk" self.storage.set_keystore_encryption(bool(password) and encrypt_keystore) if encrypt_storage: self.storage.set_password(password, enc_version=storage_enc_version) diff --git a/electrum/commands.py b/electrum/commands.py index 3acf7952..2192d992 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -176,7 +176,7 @@ class Commands: storage.put('keystore', k.dump()) wallet = Imported_Wallet(storage) keys = keystore.get_private_keys(text) - good_inputs, bad_inputs = wallet.import_private_keys(keys, None) + good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False) # FIXME tell user about bad_inputs if not good_inputs: raise Exception("None of the given privkeys can be imported") @@ -191,6 +191,7 @@ class Commands: storage.put('wallet_type', 'standard') wallet = Wallet(storage) + assert not storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk" wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file) wallet.synchronize() diff --git a/electrum/wallet.py b/electrum/wallet.py index a14bf4f9..b38f441d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1379,8 +1379,8 @@ class Imported_Wallet(Simple_Wallet): def get_public_key(self, address): return self.addresses[address].get('pubkey') - def import_private_keys(self, keys: List[str], password: Optional[str]) -> Tuple[List[str], - List[Tuple[str, str]]]: + def import_private_keys(self, keys: List[str], password: Optional[str], + write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]: good_addr = [] # type: List[str] bad_keys = [] # type: List[Tuple[str, str]] for key in keys: @@ -1398,7 +1398,7 @@ class Imported_Wallet(Simple_Wallet): self.add_address(addr) self.save_keystore() self.save_addresses() - self.save_transactions(write=True) + self.save_transactions(write=write_to_disk) return good_addr, bad_keys def import_private_key(self, key: str, password: Optional[str]) -> str: From 5473320ce459b3076d60f71dab490ed3a07b86a5 Mon Sep 17 00:00:00 2001 From: Janus Date: Tue, 27 Nov 2018 21:32:55 +0100 Subject: [PATCH 174/301] qt: use QStandardItemModel --- electrum/contacts.py | 3 +- electrum/gui/qt/address_list.py | 82 +++--- electrum/gui/qt/contact_list.py | 80 +++--- electrum/gui/qt/history_list.py | 404 ++++++++++++++++++++---------- electrum/gui/qt/invoice_list.py | 28 ++- electrum/gui/qt/main_window.py | 10 +- electrum/gui/qt/network_dialog.py | 3 +- electrum/gui/qt/request_list.py | 68 ++--- electrum/gui/qt/util.py | 230 +++++++++-------- electrum/gui/qt/utxo_list.py | 61 +++-- 10 files changed, 569 insertions(+), 400 deletions(-) diff --git a/electrum/contacts.py b/electrum/contacts.py index c09b59e2..49c8087f 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -65,8 +65,9 @@ class Contacts(dict): def pop(self, key): if key in self.keys(): - dict.pop(self, key) + res = dict.pop(self, key) self.save() + return res def resolve(self, k): if bitcoin.is_address(k): diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index fce1004c..01958810 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -31,13 +31,11 @@ from electrum.bitcoin import is_address from .util import * - -class AddressList(MyTreeWidget): +class AddressList(MyTreeView): filter_columns = [0, 1, 2, 3] # Type, Address, Label, Balance def __init__(self, parent=None): - MyTreeWidget.__init__(self, parent, self.create_menu, [], 2) - self.refresh_headers() + super().__init__(parent, self.create_menu, 2) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSortingEnabled(True) self.show_change = 0 @@ -50,6 +48,8 @@ class AddressList(MyTreeWidget): self.used_button.currentIndexChanged.connect(self.toggle_used) for t in [_('All'), _('Unused'), _('Funded'), _('Used')]: self.used_button.addItem(t) + self.setModel(QStandardItemModel(self)) + self.update() def get_toolbar_buttons(self): return QLabel(_("Filter:")), self.change_button, self.used_button @@ -82,18 +82,19 @@ class AddressList(MyTreeWidget): self.show_used = state self.update() - def on_update(self): + def update(self): self.wallet = self.parent.wallet - item = self.currentItem() - current_address = item.data(0, Qt.UserRole) if item else None + current_address = self.current_item_user_role(col=2) if self.show_change == 1: addr_list = self.wallet.get_receiving_addresses() elif self.show_change == 2: addr_list = self.wallet.get_change_addresses() else: addr_list = self.wallet.get_addresses() - self.clear() + self.model().clear() + self.refresh_headers() fx = self.parent.fx + set_address = None for address in addr_list: num = self.wallet.get_address_history_len(address) label = self.wallet.labels.get(address, '') @@ -111,61 +112,66 @@ class AddressList(MyTreeWidget): if fx and fx.get_fiat_address_config(): rate = fx.exchange_rate() fiat_balance = fx.value_str(balance, rate) - address_item = SortableTreeWidgetItem(['', address, label, balance_text, fiat_balance, "%d"%num]) + labels = ['', address, label, balance_text, fiat_balance, "%d"%num] + address_item = [QStandardItem(e) for e in labels] else: - address_item = SortableTreeWidgetItem(['', address, label, balance_text, "%d"%num]) + labels = ['', address, label, balance_text, "%d"%num] + address_item = [QStandardItem(e) for e in labels] # align text and set fonts - for i in range(address_item.columnCount()): - address_item.setTextAlignment(i, Qt.AlignVCenter) + for i, item in enumerate(address_item): + item.setTextAlignment(Qt.AlignVCenter) if i not in (0, 2): - address_item.setFont(i, QFont(MONOSPACE_FONT)) + item.setFont(QFont(MONOSPACE_FONT)) + item.setEditable(i in self.editable_columns) if fx and fx.get_fiat_address_config(): - address_item.setTextAlignment(4, Qt.AlignRight | Qt.AlignVCenter) + address_item[4].setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) # setup column 0 if self.wallet.is_change(address): - address_item.setText(0, _('change')) - address_item.setBackground(0, ColorScheme.YELLOW.as_color(True)) + address_item[0].setText(_('change')) + address_item[0].setBackground(ColorScheme.YELLOW.as_color(True)) else: - address_item.setText(0, _('receiving')) - address_item.setBackground(0, ColorScheme.GREEN.as_color(True)) - address_item.setData(0, Qt.UserRole, address) # column 0; independent from address column + address_item[0].setText(_('receiving')) + address_item[0].setBackground(ColorScheme.GREEN.as_color(True)) + address_item[2].setData(address, Qt.UserRole) # setup column 1 if self.wallet.is_frozen(address): - address_item.setBackground(1, ColorScheme.BLUE.as_color(True)) + address_item[1].setBackground(ColorScheme.BLUE.as_color(True)) if self.wallet.is_beyond_limit(address): - address_item.setBackground(1, ColorScheme.RED.as_color(True)) + address_item[1].setBackground(ColorScheme.RED.as_color(True)) # add item - self.addChild(address_item) + count = self.model().rowCount() + self.model().insertRow(count, address_item) + address_idx = self.model().index(count, 2) if address == current_address: - self.setCurrentItem(address_item) + set_address = QPersistentModelIndex(address_idx) + self.set_current_idx(set_address) def create_menu(self, position): from electrum.wallet import Multisig_Wallet is_multisig = isinstance(self.wallet, Multisig_Wallet) can_delete = self.wallet.can_delete_address() - selected = self.selectedItems() + selected = self.selected_in_column(1) multi_select = len(selected) > 1 - addrs = [item.text(1) for item in selected] - if not addrs: - return + addrs = [self.model().itemFromIndex(item).text() for item in selected] if not multi_select: - item = self.itemAt(position) - col = self.currentColumn() + idx = self.indexAt(position) + col = idx.column() + item = self.model().itemFromIndex(idx) if not item: return addr = addrs[0] - if not is_address(addr): - item.setExpanded(not item.isExpanded()) - return menu = QMenu() if not multi_select: - column_title = self.headerItem().text(col) - copy_text = item.text(col) + addr_column_title = self.model().horizontalHeaderItem(2).text() + addr_idx = idx.sibling(idx.row(), 2) + + column_title = self.model().horizontalHeaderItem(col).text() + copy_text = self.model().itemFromIndex(idx).text() menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(copy_text)) menu.addAction(_('Details'), lambda: self.parent.show_address(addr)) - if col in self.editable_columns: - menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, col)) + persistent = QPersistentModelIndex(addr_idx) + menu.addAction(_("Edit {}").format(addr_column_title), lambda p=persistent: self.edit(QModelIndex(p))) menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr)) if self.wallet.can_export(): menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr)) @@ -189,7 +195,3 @@ class AddressList(MyTreeWidget): run_hook('receive_menu', menu, addrs, self.wallet) menu.exec_(self.viewport().mapToGlobal(position)) - - def on_permit_edit(self, item, column): - # labels for headings, e.g. "receiving" or "used" should not be editable - return item.childCount() == 0 diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index d85c6df5..e1915b1b 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -34,67 +34,81 @@ from electrum.bitcoin import is_address from electrum.util import block_explorer_URL from electrum.plugin import run_hook -from .util import MyTreeWidget, import_meta_gui, export_meta_gui +from .util import MyTreeView, import_meta_gui, export_meta_gui -class ContactList(MyTreeWidget): +class ContactList(MyTreeView): filter_columns = [0, 1] # Key, Value def __init__(self, parent): - MyTreeWidget.__init__(self, parent, self.create_menu, [_('Name'), _('Address')], 0, [0]) + super().__init__(parent, self.create_menu, stretch_column=0, editable_columns=[0]) + self.setModel(QStandardItemModel(self)) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSortingEnabled(True) + self.update() - def on_permit_edit(self, item, column): - # openalias items shouldn't be editable - return item.text(1) != "openalias" + def on_edited(self, idx, user_role, text): + _type, prior_name = self.parent.contacts.pop(user_role) - def on_edited(self, item, column, prior): - if column == 0: # Remove old contact if renamed - self.parent.contacts.pop(prior) - self.parent.set_contact(item.text(0), item.text(1)) + # TODO when min Qt >= 5.11, use siblingAtColumn + col_1_sibling = idx.sibling(idx.row(), 1) + col_1_item = self.model().itemFromIndex(col_1_sibling) + + self.parent.set_contact(text, col_1_item.text()) def import_contacts(self): - import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.on_update) + import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.update) def export_contacts(self): export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file) def create_menu(self, position): menu = QMenu() - selected = self.selectedItems() - if not selected: + 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) + 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: - names = [item.text(0) for item in selected] - keys = [item.text(1) for item in selected] - column = self.currentColumn() - column_title = self.headerItem().text(column) - column_data = '\n'.join([item.text(column) for item in selected]) + column = idx.column() + column_title = self.model().horizontalHeaderItem(column).text() + column_data = '\n'.join(self.model().itemFromIndex(idx).text() for 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.currentItem() - menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column)) - menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(keys)) - menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(keys)) - URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, keys)] + item = self.model().itemFromIndex(idx) + if item.isEditable(): + # would not be editable if openalias + persistent = QPersistentModelIndex(idx) + menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p))) + menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(selected_keys)) + menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(selected_keys)) + URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, selected_keys)] if URLs: menu.addAction(_("View on block explorer"), lambda: map(webbrowser.open, URLs)) - run_hook('create_contact_menu', menu, selected) + run_hook('create_contact_menu', menu, selected_keys) menu.exec_(self.viewport().mapToGlobal(position)) - def on_update(self): - item = self.currentItem() - current_key = item.data(0, Qt.UserRole) if item else None - self.clear() + def update(self): + current_key = self.current_item_user_role(col=0) + self.model().clear() + self.update_headers([_('Name'), _('Address')]) + set_current = None for key in sorted(self.parent.contacts.keys()): - _type, name = self.parent.contacts[key] - item = QTreeWidgetItem([name, key]) - item.setData(0, Qt.UserRole, key) - self.addTopLevelItem(item) + contact_type, name = self.parent.contacts[key] + items = [QStandardItem(x) for x in (name, key)] + items[0].setEditable(contact_type != 'openalias') + items[1].setEditable(False) + items[0].setData(key, Qt.UserRole) + row_count = self.model().rowCount() + self.model().insertRow(row_count, items) if key == current_key: - self.setCurrentItem(item) + idx = self.model().index(row_count, 0) + set_current = QPersistentModelIndex(idx) + self.set_current_idx(set_current) run_hook('update_contacts_tab', self) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 9d05ac74..807484f9 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -27,10 +27,11 @@ import webbrowser import datetime from datetime import date from typing import TYPE_CHECKING +from collections import OrderedDict from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ -from electrum.util import block_explorer_URL, profiler, print_error, TxMinedStatus +from electrum.util import block_explorer_URL, profiler, print_error, TxMinedStatus, Fiat from .util import * @@ -57,40 +58,111 @@ TX_ICONS = [ "confirmed.png", ] +class HistorySortModel(QSortFilterProxyModel): + def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): + item1 = self.sourceModel().itemFromIndex(source_left) + item2 = self.sourceModel().itemFromIndex(source_right) + data1 = item1.data(HistoryList.SORT_ROLE) + data2 = item2.data(HistoryList.SORT_ROLE) + if data1 is not None and data2 is not None: + return data1 < data2 + return item1.text() < item2.text() -class HistoryList(MyTreeWidget, AcceptFileDragDrop): - filter_columns = [2, 3, 4] # Date, Description, Amount +class HistoryList(MyTreeView, AcceptFileDragDrop): + filter_columns = [1, 2, 3] # Date, Description, Amount TX_HASH_ROLE = Qt.UserRole - TX_VALUE_ROLE = Qt.UserRole + 1 + SORT_ROLE = Qt.UserRole + 1 + + def should_hide(self, proxy_row): + if self.start_timestamp and self.end_timestamp: + source_idx = self.proxy.mapToSource(self.proxy.index(proxy_row, 0)) + item = self.std_model.itemFromIndex(source_idx) + txid = item.data(self.TX_HASH_ROLE) + date = self.transactions[txid]['date'] + if date: + in_interval = self.start_timestamp <= date <= self.end_timestamp + if not in_interval: + return True + return False def __init__(self, parent=None): - MyTreeWidget.__init__(self, parent, self.create_menu, [], 3) + super().__init__(parent, self.create_menu, 2) + self.std_model = QStandardItemModel(self) + self.proxy = HistorySortModel(self) + self.proxy.setSourceModel(self.std_model) + self.setModel(self.proxy) + + self.txid_to_items = {} + self.transactions = OrderedDict() + self.summary = {} + self.blue_brush = QBrush(QColor("#1E1EFF")) + self.red_brush = QBrush(QColor("#BC1E1E")) + self.monospace_font = QFont(MONOSPACE_FONT) + self.default_color = self.parent.app.palette().text().color() + self.config = parent.config AcceptFileDragDrop.__init__(self, ".txn") - self.refresh_headers() - self.setColumnHidden(1, True) self.setSortingEnabled(True) - self.sortByColumn(0, Qt.AscendingOrder) self.start_timestamp = None self.end_timestamp = None self.years = [] self.create_toolbar_buttons() self.wallet = None + root = self.std_model.invisibleRootItem() + + self.wallet = self.parent.wallet # type: Abstract_Wallet + fx = self.parent.fx + r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) + self.transactions.update([(x['txid'], x) for x in r['transactions']]) + self.summary = r['summary'] + if not self.years and self.transactions: + start_date = next(iter(self.transactions.values())).get('date') or date.today() + end_date = next(iter(reversed(self.transactions.values()))).get('date') or date.today() + self.years = [str(i) for i in range(start_date.year, end_date.year + 1)] + self.period_combo.insertItems(1, self.years) + if fx: fx.history_used_spot = False + self.refresh_headers() + for tx_item in self.transactions.values(): + self.insert_tx(tx_item) + self.sortByColumn(0, Qt.AscendingOrder) + + #def on_activated(self, idx: QModelIndex): + # # TODO use siblingAtColumn when min Qt version is >=5.11 + # self.edit(idx.sibling(idx.row(), 2)) + def format_date(self, d): return str(datetime.date(d.year, d.month, d.day)) if d else _('None') def refresh_headers(self): - headers = ['', '', _('Date'), _('Description'), _('Amount'), _('Balance')] + headers = ['', _('Date'), _('Description'), _('Amount'), _('Balance')] fx = self.parent.fx if fx and fx.show_history(): headers.extend(['%s '%fx.ccy + _('Value')]) - self.editable_columns |= {6} + self.editable_columns |= {5} if fx.get_history_capital_gains_config(): headers.extend(['%s '%fx.ccy + _('Acquisition price')]) headers.extend(['%s '%fx.ccy + _('Capital Gains')]) else: - self.editable_columns -= {6} - self.update_headers(headers) + self.editable_columns -= {5} + col_count = self.std_model.columnCount() + diff = col_count-len(headers) + grew = False + if col_count > len(headers): + if diff == 2: + self.std_model.removeColumns(6, diff) + else: + assert diff in [1, 3] + self.std_model.removeColumns(5, diff) + for items in self.txid_to_items.values(): + while len(items) > col_count: + items.pop() + elif col_count < len(headers): + grew = True + self.std_model.clear() + self.txid_to_items.clear() + self.transactions.clear() + self.summary.clear() + self.update_headers(headers, self.std_model) def get_domain(self): '''Replaced in address_dialog.py''' @@ -111,13 +183,11 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): year = int(s) except: return - start_date = datetime.datetime(year, 1, 1) - end_date = datetime.datetime(year+1, 1, 1) - self.start_timestamp = time.mktime(start_date.timetuple()) - self.end_timestamp = time.mktime(end_date.timetuple()) + self.start_timestamp = start_date = datetime.datetime(year, 1, 1) + self.end_timestamp = end_date = datetime.datetime(year+1, 1, 1) self.start_button.setText(_('From') + ' ' + self.format_date(start_date)) self.end_button.setText(_('To') + ' ' + self.format_date(end_date)) - self.update() + self.hide_rows() def create_toolbar_buttons(self): self.period_combo = QComboBox() @@ -136,18 +206,18 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): def on_hide_toolbar(self): self.start_timestamp = None self.end_timestamp = None - self.update() + self.hide_rows() def save_toolbar_state(self, state, config): config.set_key('show_toolbar_history', state) def select_start_date(self): self.start_timestamp = self.select_date(self.start_button) - self.update() + self.hide_rows() def select_end_date(self): self.end_timestamp = self.select_date(self.end_button) - self.update() + self.hide_rows() def select_date(self, button): d = WindowModalDialog(self, _("Select date")) @@ -167,7 +237,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): return None date = d.date.toPyDate() button.setText(self.format_date(date)) - return time.mktime(date.timetuple()) + return datetime.datetime(date.year, date.month, date.day) def show_summary(self): h = self.summary @@ -215,104 +285,167 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): _("Perhaps some dependencies are missing...") + " (matplotlib?)") return try: - plt = plot_history(self.transactions) + plt = plot_history(list(self.transactions.values())) plt.show() except NothingToPlotException as e: self.parent.show_message(str(e)) + def insert_tx(self, tx_item): + fx = self.parent.fx + tx_hash = tx_item['txid'] + height = tx_item['height'] + conf = tx_item['confirmations'] + timestamp = tx_item['timestamp'] + value = tx_item['value'].value + balance = tx_item['balance'].value + label = tx_item['label'] + tx_mined_status = TxMinedStatus(height, conf, timestamp, None) + status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) + has_invoice = self.wallet.invoices.paid.get(tx_hash) + icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) + v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) + balance_str = self.parent.format_amount(balance, whitespaces=True) + entry = ['', status_str, label, v_str, balance_str] + fiat_value = None + item = [QStandardItem(e) for e in entry] + item[3].setData(value, self.SORT_ROLE) + item[4].setData(balance, self.SORT_ROLE) + if has_invoice: + item[2].setIcon(self.icon_cache.get(":icons/seal")) + for i in range(len(entry)): + self.set_item_properties(item[i], i, tx_hash) + if value and value < 0: + item[2].setForeground(self.red_brush) + item[3].setForeground(self.red_brush) + self.txid_to_items[tx_hash] = item + self.update_item(tx_hash, self.parent.wallet.get_tx_height(tx_hash)) + source_row_idx = self.std_model.rowCount() + self.std_model.insertRow(source_row_idx, item) + new_idx = self.std_model.index(source_row_idx, 0) + history = self.parent.fx.show_history() + if history: + self.update_fiat(tx_hash, tx_item) + self.hide_row(self.proxy.mapFromSource(new_idx).row()) + + def set_item_properties(self, item, i, tx_hash): + if i>2: + item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) + if i!=1: + item.setFont(self.monospace_font) + item.setEditable(i in self.editable_columns) + item.setData(tx_hash, self.TX_HASH_ROLE) + + def ensure_fields_available(self, items, idx, txid): + while len(items) < idx + 1: + row = list(self.transactions.keys()).index(txid) + qidx = self.std_model.index(row, len(items)) + assert qidx.isValid(), (self.std_model.columnCount(), idx) + item = self.std_model.itemFromIndex(qidx) + self.set_item_properties(item, len(items), txid) + items.append(item) + @profiler - def on_update(self): + def update(self): self.wallet = self.parent.wallet # type: Abstract_Wallet fx = self.parent.fx - r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=self.start_timestamp, to_timestamp=self.end_timestamp, fx=fx) - self.transactions = r['transactions'] - self.summary = r['summary'] - if not self.years and self.transactions: - start_date = self.transactions[0].get('date') or date.today() - end_date = self.transactions[-1].get('date') or date.today() - self.years = [str(i) for i in range(start_date.year, end_date.year + 1)] - self.period_combo.insertItems(1, self.years) - item = self.currentItem() - current_tx = item.data(0, self.TX_HASH_ROLE) if item else None - self.clear() - if fx: fx.history_used_spot = False - blue_brush = QBrush(QColor("#1E1EFF")) - red_brush = QBrush(QColor("#BC1E1E")) - monospace_font = QFont(MONOSPACE_FONT) - for tx_item in self.transactions: - tx_hash = tx_item['txid'] - height = tx_item['height'] - conf = tx_item['confirmations'] - timestamp = tx_item['timestamp'] - value_sat = tx_item['value'].value - balance = tx_item['balance'].value - label = tx_item['label'] - tx_mined_status = TxMinedStatus(height, conf, timestamp, None) - status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) - has_invoice = self.wallet.invoices.paid.get(tx_hash) - icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) - v_str = self.parent.format_amount(value_sat, is_diff=True, whitespaces=True) - balance_str = self.parent.format_amount(balance, whitespaces=True) - entry = ['', tx_hash, status_str, label, v_str, balance_str] - fiat_value = None - if value_sat is not None and fx and fx.show_history(): - fiat_value = tx_item['fiat_value'].value - value_str = fx.format_fiat(fiat_value) - entry.append(value_str) - # fixme: should use is_mine - if value_sat < 0: - entry.append(fx.format_fiat(tx_item['acquisition_price'].value)) - entry.append(fx.format_fiat(tx_item['capital_gain'].value)) - item = SortableTreeWidgetItem(entry) - item.setIcon(0, icon) - item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else "")) - if has_invoice: - item.setIcon(3, self.icon_cache.get(":icons/seal")) - for i in range(len(entry)): - if i>3: - item.setTextAlignment(i, Qt.AlignRight | Qt.AlignVCenter) - if i!=2: - item.setFont(i, monospace_font) - if value_sat and value_sat < 0: - item.setForeground(3, red_brush) - item.setForeground(4, red_brush) - if fiat_value is not None and not tx_item['fiat_default']: - item.setForeground(6, blue_brush) - # sort orders - item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf)) - item.setData(4, SortableTreeWidgetItem.DataRole, value_sat) - item.setData(5, SortableTreeWidgetItem.DataRole, balance) - if fiat_value is not None: - item.setData(6, SortableTreeWidgetItem.DataRole, fiat_value) - if value_sat < 0: - item.setData(7, SortableTreeWidgetItem.DataRole, tx_item['acquisition_price'].value) - item.setData(8, SortableTreeWidgetItem.DataRole, tx_item['capital_gain'].value) - if tx_hash: - item.setData(0, self.TX_HASH_ROLE, tx_hash) - item.setData(0, self.TX_VALUE_ROLE, value_sat) - self.insertTopLevelItem(0, item) - if current_tx == tx_hash: - self.setCurrentItem(item) + r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) + seen = set() + history = fx.show_history() + tx_list = list(self.transactions.values()) + if r['transactions'] == tx_list: + return + if r['transactions'][:-1] == tx_list: + print_error('history_list: one new transaction') + row = r['transactions'][-1] + txid = row['txid'] + if txid not in self.transactions: + self.transactions[txid] = row + self.transactions.move_to_end(txid, last=True) + self.insert_tx(row) + return + else: + print_error('history_list: tx added but txid is already in list (weird), txid: ', txid) + for idx, row in enumerate(r['transactions']): + txid = row['txid'] + seen.add(txid) + if txid not in self.transactions: + self.transactions[txid] = row + self.transactions.move_to_end(txid, last=True) + self.insert_tx(row) + continue + old = self.transactions[txid] + if old == row: + continue + self.update_item(txid, self.parent.wallet.get_tx_height(txid)) + if history: + self.update_fiat(txid, row) + balance_str = self.parent.format_amount(row['balance'].value, whitespaces=True) + self.txid_to_items[txid][4].setText(balance_str) + self.txid_to_items[txid][4].setData(row['balance'].value, self.SORT_ROLE) + old.clear() + old.update(**row) + removed = 0 + l = list(enumerate(self.transactions.keys())) + for idx, txid in l: + if txid not in seen: + del self.transactions[txid] + del self.txid_to_items[txid] + items = self.std_model.takeRow(idx - removed) + removed_txid = items[0].data(self.TX_HASH_ROLE) + assert removed_txid == txid, (idx, removed) + removed += 1 + self.apply_filter() - def on_edited(self, item, column, prior): - '''Called only when the text actually changes''' - key = item.data(0, self.TX_HASH_ROLE) - value_sat = item.data(0, self.TX_VALUE_ROLE) - text = item.text(column) + def update_fiat(self, txid, row): + cap_gains = self.parent.fx.get_history_capital_gains_config() + items = self.txid_to_items[txid] + self.ensure_fields_available(items, 7 if cap_gains else 5, txid) + items[5].setForeground(self.blue_brush if not row['fiat_default'] and row['fiat_value'] else self.default_color) + value_str = self.parent.fx.format_fiat(row['fiat_value'].value) + items[5].setText(value_str) + items[5].setData(row['fiat_value'].value, self.SORT_ROLE) + # fixme: should use is_mine + if row['value'].value < 0 and cap_gains: + acq = row['acquisition_price'].value + items[6].setText(self.parent.fx.format_fiat(acq)) + items[6].setData(acq, self.SORT_ROLE) + cg = row['capital_gain'].value + items[7].setText(self.parent.fx.format_fiat(cg)) + items[7].setData(cg, self.SORT_ROLE) + + def update_on_new_fee_histogram(self): + pass + # TODO update unconfirmed tx'es + + def on_edited(self, index, user_role, text): + column = index.column() + index = self.proxy.mapToSource(index) + item = self.std_model.itemFromIndex(index) + key = item.data(self.TX_HASH_ROLE) # fixme - if column == 3: + if column == 2: self.parent.wallet.set_label(key, text) self.update_labels() self.parent.update_completions() - elif column == 6: - self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, value_sat) - self.on_update() - - def on_doubleclick(self, item, column): - if self.permit_edit(item, column): - super(HistoryList, self).on_doubleclick(item, column) + elif column == 5: + tx_item = self.transactions[key] + self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, tx_item['value'].value) + value = tx_item['value'].value + if value is not None: + fee = tx_item['fee'] + fiat_fields = self.parent.wallet.get_tx_item_fiat(key, value, self.parent.fx, fee.value if fee else None) + tx_item.update(fiat_fields) + self.update_fiat(key, tx_item) else: - tx_hash = item.data(0, self.TX_HASH_ROLE) + assert False + + def mouseDoubleClickEvent(self, event: QMouseEvent): + idx = self.indexAt(event.pos()) + item = self.std_model.itemFromIndex(self.proxy.mapToSource(idx)) + if not item or item.isEditable(): + super().mouseDoubleClickEvent(event) + elif item: + tx_hash = item.data(self.TX_HASH_ROLE) self.show_transaction(tx_hash) def show_transaction(self, tx_hash): @@ -323,13 +456,13 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): self.parent.show_transaction(tx, label) def update_labels(self): - root = self.invisibleRootItem() - child_count = root.childCount() + root = self.std_model.invisibleRootItem() + child_count = root.rowCount() for i in range(child_count): - item = root.child(i) - txid = item.data(0, self.TX_HASH_ROLE) + item = root.child(i, 2) + txid = item.data(self.TX_HASH_ROLE) label = self.wallet.get_label(txid) - item.setText(3, label) + item.setText(label) def update_item(self, tx_hash, tx_mined_status): if self.wallet is None: @@ -337,31 +470,30 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): conf = tx_mined_status.conf status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) - items = self.findItems(tx_hash, Qt.MatchExactly, column=1) - if items: - item = items[0] - item.setIcon(0, icon) - item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf)) - item.setText(2, status_str) + if tx_hash not in self.txid_to_items: + return + items = self.txid_to_items[tx_hash] + items[0].setIcon(icon) + items[0].setToolTip(str(conf) + _(" confirmation" + ("s" if conf != 1 else ""))) + items[0].setData((status, conf), self.SORT_ROLE) + items[1].setText(status_str) - def create_menu(self, position): - self.selectedIndexes() - item = self.currentItem() - if not item: - return - column = self.currentColumn() - tx_hash = item.data(0, self.TX_HASH_ROLE) - if not tx_hash: - return + def create_menu(self, position: QPoint): + org_idx: QModelIndex = self.indexAt(position) + idx = self.proxy.mapToSource(org_idx) + item: QStandardItem = self.std_model.itemFromIndex(idx) + assert item, 'create_menu: index not found in model' + tx_hash = idx.data(self.TX_HASH_ROLE) + column = idx.column() + assert tx_hash, "create_menu: no tx hash" tx = self.wallet.transactions.get(tx_hash) - if not tx: - return - if column is 0: - column_title = "ID" + assert tx, "create_menu: no tx" + if column == 0: + column_title = _('Transaction ID') column_data = tx_hash else: - column_title = self.headerItem().text(column) - column_data = item.text(column) + column_title = self.std_model.horizontalHeaderItem(column).text() + column_data = item.text() tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) height = self.wallet.get_tx_height(tx_hash).height is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) @@ -372,8 +504,10 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) for c in self.editable_columns: - menu.addAction(_("Edit {}").format(self.headerItem().text(c)), - lambda bound_c=c: self.editItem(item, bound_c)) + label = self.std_model.horizontalHeaderItem(c).text() + # TODO use siblingAtColumn when min Qt version is >=5.11 + persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c)) + menu.addAction(_("Edit {}").format(label), lambda p=persistent: self.edit(QModelIndex(p))) menu.addAction(_("Details"), lambda: self.show_transaction(tx_hash)) if is_unconfirmed and tx: # note: the current implementation of RBF *needs* the old tx fee @@ -442,7 +576,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): self.parent.show_message(_("Your wallet history has been successfully exported.")) def do_export_history(self, file_name, is_csv): - history = self.transactions + history = self.transactions.values() lines = [] if is_csv: for item in history: diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 462aadd8..4789aa06 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -29,36 +29,40 @@ from electrum.util import format_time from .util import * -class InvoiceList(MyTreeWidget): +class InvoiceList(MyTreeView): filter_columns = [0, 1, 2, 3] # Date, Requestor, Description, Amount def __init__(self, parent): - MyTreeWidget.__init__(self, parent, self.create_menu, [_('Expires'), _('Requestor'), _('Description'), _('Amount'), _('Status')], 2) + super().__init__(parent, self.create_menu, 2) self.setSortingEnabled(True) - self.header().setSectionResizeMode(1, QHeaderView.Interactive) self.setColumnWidth(1, 200) + self.setModel(QStandardItemModel(self)) + self.update() - def on_update(self): + def update(self): inv_list = self.parent.invoices.unpaid_invoices() - self.clear() + self.model().clear() + self.update_headers([_('Expires'), _('Requestor'), _('Description'), _('Amount'), _('Status')]) + self.header().setSectionResizeMode(1, QHeaderView.Interactive) for pr in inv_list: key = pr.get_id() status = self.parent.invoices.get_status(key) requestor = pr.get_requestor() exp = pr.get_expiration_date() date_str = format_time(exp) if exp else _('Never') - item = QTreeWidgetItem([date_str, requestor, pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')]) - item.setIcon(4, self.icon_cache.get(pr_icons.get(status))) - item.setData(0, Qt.UserRole, key) - item.setFont(1, QFont(MONOSPACE_FONT)) - item.setFont(3, QFont(MONOSPACE_FONT)) + labels = [date_str, requestor, pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')] + item = [QStandardItem(e) for e in labels] + item[4].setIcon(self.icon_cache.get(pr_icons.get(status))) + item[0].setData(Qt.UserRole, key) + item[1].setFont(QFont(MONOSPACE_FONT)) + item[3].setFont(QFont(MONOSPACE_FONT)) self.addTopLevelItem(item) - self.setCurrentItem(self.topLevelItem(0)) + self.selectionModel().select(self.model().index(0,0), QItemSelectionModel.SelectCurrent) self.setVisible(len(inv_list)) self.parent.invoices_label.setVisible(len(inv_list)) def import_invoices(self): - import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.on_update) + import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.update) def export_invoices(self): export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 7c3f5e2e..db0e3065 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -353,8 +353,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if self.config.is_dynfee(): self.fee_slider.update() self.do_update_fee() - # todo: update only unconfirmed tx - self.history_list.update() + self.history_list.update_on_new_fee_histogram() else: self.print_error("unexpected network_qt signal:", event, args) @@ -379,9 +378,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def load_wallet(self, wallet): wallet.thread = TaskThread(self, self.on_error) self.update_recently_visited(wallet.storage.path) - # update(==init) all tabs; expensive for large wallets.. - # so delay it somewhat, hence __init__ can finish and the window can appear sooner - QTimer.singleShot(50, self.update_tabs) self.need_update.set() # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized # update menus @@ -1111,9 +1107,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.from_label = QLabel(_('From')) grid.addWidget(self.from_label, 3, 0) - self.from_list = MyTreeWidget(self, self.from_list_menu, ['','']) - self.from_list.setHeaderHidden(True) - self.from_list.setMaximumHeight(80) + self.from_list = FromList(self, self.from_list_menu) grid.addWidget(self.from_list, 3, 1, 1, -1) self.set_pay_from([]) diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 94ae7773..a1f2dace 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -100,7 +100,6 @@ class NodesListWidget(QTreeWidget): def update(self, network: Network): self.clear() - self.addChild = self.addTopLevelItem chains = network.get_blockchains() n_chains = len(chains) for chain_id, interfaces in chains.items(): @@ -118,7 +117,7 @@ class NodesListWidget(QTreeWidget): item = QTreeWidgetItem([i.host + star, '%d'%i.tip]) item.setData(0, Qt.UserRole, 0) item.setData(1, Qt.UserRole, i.server) - x.addChild(item) + x.addTopLevelItem(item) if n_chains > 1: self.addTopLevelItem(x) x.setExpanded(True) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 19ec5970..8c6567fc 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -23,43 +23,39 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from PyQt5.QtGui import * -from PyQt5.QtCore import * -from PyQt5.QtWidgets import QTreeWidgetItem, QMenu +from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtWidgets import QMenu +from PyQt5.QtCore import Qt from electrum.i18n import _ from electrum.util import format_time, age from electrum.plugin import run_hook from electrum.paymentrequest import PR_UNKNOWN -from .util import MyTreeWidget, pr_tooltips, pr_icons +from .util import MyTreeView, pr_tooltips, pr_icons - -class RequestList(MyTreeWidget): +class RequestList(MyTreeView): filter_columns = [0, 1, 2, 3, 4] # Date, Account, Address, Description, Amount def __init__(self, parent): - MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 3) - self.currentItemChanged.connect(self.item_changed) - self.itemClicked.connect(self.item_changed) + super().__init__(parent, self.create_menu, 3, editable_columns=[]) + self.setModel(QStandardItemModel(self)) self.setSortingEnabled(True) self.setColumnWidth(0, 180) - self.hideColumn(1) + self.update() + self.selectionModel().currentRowChanged.connect(self.item_changed) - def item_changed(self, item): - if item is None: - return - if not item.isSelected(): - return - addr = str(item.text(1)) + def item_changed(self, idx): + # TODO use siblingAtColumn when min Qt version is >=5.11 + addr = self.model().itemFromIndex(idx.sibling(idx.row(), 1)).text() req = self.wallet.receive_requests.get(addr) if req is None: self.update() return expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never') amount = req['amount'] - message = self.wallet.labels.get(addr, '') + message = req['memo'] self.parent.receive_address_e.setText(addr) self.parent.receive_message_e.setText(message) self.parent.receive_amount_e.setAmount(amount) @@ -68,7 +64,7 @@ class RequestList(MyTreeWidget): self.parent.expires_label.setText(expires) self.parent.new_request_button.setEnabled(True) - def on_update(self): + def update(self): self.wallet = self.parent.wallet # hide receive tab if no receive requests available b = len(self.wallet.receive_requests) > 0 @@ -86,8 +82,9 @@ class RequestList(MyTreeWidget): self.parent.set_receive_address(addr) self.parent.new_request_button.setEnabled(addr != current_address) - # clear the list and fill it again - self.clear() + self.model().clear() + self.update_headers([_('Date'), _('Address'), '', _('Description'), _('Amount'), _('Status')]) + self.hideColumn(1) # hide address column for req in self.wallet.get_sorted_requests(self.config): address = req['address'] if address not in domain: @@ -95,35 +92,40 @@ class RequestList(MyTreeWidget): timestamp = req.get('time', 0) amount = req.get('amount') expiration = req.get('exp', None) - message = req.get('memo', '') + message = req['memo'] date = format_time(timestamp) status = req.get('status') signature = req.get('sig') requestor = req.get('name', '') amount_str = self.parent.format_amount(amount) if amount else "" - item = QTreeWidgetItem([date, address, '', message, amount_str, pr_tooltips.get(status,'')]) + labels = [date, address, '', message, amount_str, pr_tooltips.get(status,'')] + items = [QStandardItem(e) for e in labels] + self.set_editability(items) if signature is not None: - item.setIcon(2, self.icon_cache.get(":icons/seal.png")) - item.setToolTip(2, 'signed by '+ requestor) + items[2].setIcon(self.icon_cache.get(":icons/seal.png")) + items[2].setToolTip('signed by '+ requestor) if status is not PR_UNKNOWN: - item.setIcon(6, self.icon_cache.get(pr_icons.get(status))) - self.addTopLevelItem(item) - + items[5].setIcon(self.icon_cache.get(pr_icons.get(status))) + items[3].setData(address, Qt.UserRole) + self.model().insertRow(self.model().rowCount(), items) def create_menu(self, position): - item = self.itemAt(position) + idx = self.indexAt(position) + # TODO use siblingAtColumn when min Qt version is >=5.11 + item = self.model().itemFromIndex(idx.sibling(idx.row(), 1)) if not item: return - addr = str(item.text(1)) + addr = item.text() req = self.wallet.receive_requests.get(addr) if req is None: self.update() return - column = self.currentColumn() - column_title = self.headerItem().text(column) - column_data = item.text(column) + column = idx.column() + column_title = self.model().horizontalHeaderItem(column).text() + column_data = item.text() menu = QMenu(self) - menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) + if column != 2: + menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Copy URI"), lambda: self.parent.view_and_paste('URI', '', self.parent.get_request_URI(addr))) menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr)) menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr)) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index b6db61c1..e320715a 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -5,6 +5,7 @@ import platform import queue from functools import partial from typing import NamedTuple, Callable, Optional +from abc import abstractmethod from PyQt5.QtGui import * from PyQt5.QtCore import * @@ -398,20 +399,16 @@ class ElectrumItemDelegate(QStyledItemDelegate): def createEditor(self, parent, option, index): return self.parent().createEditor(parent, option, index) -class MyTreeWidget(QTreeWidget): +class MyTreeView(QTreeView): - def __init__(self, parent, create_menu, headers, stretch_column=None, - editable_columns=None): - QTreeWidget.__init__(self, parent) + def __init__(self, parent, create_menu, stretch_column=None, editable_columns=None): + super().__init__(parent) self.parent = parent self.config = self.parent.config self.stretch_column = stretch_column self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(create_menu) self.setUniformRowHeights(True) - # extend the syntax for consistency - self.addChild = self.addTopLevelItem - self.insertChild = self.insertTopLevelItem self.icon_cache = IconCache() @@ -424,127 +421,143 @@ class MyTreeWidget(QTreeWidget): editable_columns = set(editable_columns) self.editable_columns = editable_columns self.setItemDelegate(ElectrumItemDelegate(self)) - self.itemDoubleClicked.connect(self.on_doubleclick) - self.update_headers(headers) self.current_filter = "" self.setRootIsDecorated(False) # remove left margin self.toolbar_shown = False - def update_headers(self, headers): - self.setColumnCount(len(headers)) - self.setHeaderLabels(headers) + def set_editability(self, items): + for idx, i in enumerate(items): + i.setEditable(idx in self.editable_columns) + + def selected_in_column(self, column: int): + items = self.selectionModel().selectedIndexes() + return list(x for x in items if x.column() == column) + + def current_item_user_role(self, col) -> Optional[QStandardItem]: + idx = self.selectionModel().currentIndex() + idx = idx.sibling(idx.row(), col) + item = self.model().itemFromIndex(idx) + if item: + return item.data(Qt.UserRole) + + def set_current_idx(self, set_current: QPersistentModelIndex): + if set_current: + assert isinstance(set_current, QPersistentModelIndex) + assert set_current.isValid() + self.selectionModel().select(QModelIndex(set_current), QItemSelectionModel.SelectCurrent) + + def update_headers(self, headers, model=None): + if model is None: + model = self.model() + model.setHorizontalHeaderLabels(headers) self.header().setStretchLastSection(False) for col in range(len(headers)): sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents self.header().setSectionResizeMode(col, sm) - def editItem(self, item, column): - if column in self.editable_columns: - try: - self.editing_itemcol = (item, column, item.text(column)) - # Calling setFlags causes on_changed events for some reason - item.setFlags(item.flags() | Qt.ItemIsEditable) - QTreeWidget.editItem(self, item, column) - item.setFlags(item.flags() & ~Qt.ItemIsEditable) - except RuntimeError: - # (item) wrapped C/C++ object has been deleted - pass - def keyPressEvent(self, event): if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None: - self.on_activated(self.currentItem(), self.currentColumn()) - else: - QTreeWidget.keyPressEvent(self, event) + self.on_activated(self.selectionModel().currentIndex()) + return + super().keyPressEvent(event) - def permit_edit(self, item, column): - return (column in self.editable_columns - and self.on_permit_edit(item, column)) - - def on_permit_edit(self, item, column): - return True - - def on_doubleclick(self, item, column): - if self.permit_edit(item, column): - self.editItem(item, column) - - def on_activated(self, item, column): + def on_activated(self, idx): # on 'enter' we show the menu - pt = self.visualItemRect(item).bottomLeft() + pt = self.visualRect(idx).bottomLeft() pt.setX(50) self.customContextMenuRequested.emit(pt) def createEditor(self, parent, option, index): self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(), parent, option, index) - self.editor.editingFinished.connect(self.editing_finished) + persistent = QPersistentModelIndex(index) + user_role = index.data(Qt.UserRole) + assert user_role is not None + idx = QModelIndex(persistent) + index = self.proxy.mapToSource(idx) + item = self.std_model.itemFromIndex(index) + prior_text = item.text() + def editing_finished(): + # Long-time QT bug - pressing Enter to finish editing signals + # editingFinished twice. If the item changed the sequence is + # Enter key: editingFinished, on_change, editingFinished + # Mouse: on_change, editingFinished + # This mess is the cleanest way to ensure we make the + # on_edited callback with the updated item + if self.editor is None: + return + if self.editor.text() == prior_text: + self.editor = None # Unchanged - ignore any 2nd call + return + if item.text() == prior_text: + return # Buggy first call on Enter key, item not yet updated + if not idx.isValid(): + return + self.on_edited(idx, user_role, self.editor.text()) + self.editor = None + self.editor.editingFinished.connect(editing_finished) return self.editor - def editing_finished(self): - # Long-time QT bug - pressing Enter to finish editing signals - # editingFinished twice. If the item changed the sequence is - # Enter key: editingFinished, on_change, editingFinished - # Mouse: on_change, editingFinished - # This mess is the cleanest way to ensure we make the - # on_edited callback with the updated item - if self.editor: - (item, column, prior_text) = self.editing_itemcol - if self.editor.text() == prior_text: - self.editor = None # Unchanged - ignore any 2nd call - elif item.text(column) == prior_text: - pass # Buggy first call on Enter key, item not yet updated - else: - # What we want - the updated item - self.on_edited(*self.editing_itemcol) - self.editor = None + def edit(self, idx, trigger=QAbstractItemView.AllEditTriggers, event=None): + """ + this is to prevent: + edit: editing failed + from inside qt + """ + return super().edit(idx, trigger, event) - # Now do any pending updates - if self.editor is None and self.pending_update: - self.pending_update = False - self.on_update() - - def on_edited(self, item, column, prior): - '''Called only when the text actually changes''' - key = item.data(0, Qt.UserRole) - text = item.text(column) - self.parent.wallet.set_label(key, text) + def on_edited(self, idx: QModelIndex, user_role, text): + self.parent.wallet.set_label(user_role, text) self.parent.history_list.update_labels() self.parent.update_completions() - def update(self): - # Defer updates if editing - if self.editor: - self.pending_update = True - else: - self.setUpdatesEnabled(False) - scroll_pos = self.verticalScrollBar().value() - self.on_update() - self.setUpdatesEnabled(True) - # To paint the list before resetting the scroll position - self.parent.app.processEvents() - self.verticalScrollBar().setValue(scroll_pos) + def apply_filter(self): if self.current_filter: self.filter(self.current_filter) - def on_update(self): + @abstractmethod + def should_hide(self, row): + """ + row_num is for self.model(). So if there is a proxy, it is the row number + in that! + """ pass - def get_leaves(self, root): - child_count = root.childCount() - if child_count == 0: - yield root - for i in range(child_count): - item = root.child(i) - for x in self.get_leaves(item): - yield x + def hide_row(self, row_num): + """ + row_num is for self.model(). So if there is a proxy, it is the row number + in that! + """ + should_hide = self.should_hide(row_num) + if not self.current_filter and should_hide is None: + # no filters at all, neither date nor search + self.setRowHidden(row_num, QModelIndex(), False) + return + for column in self.filter_columns: + if isinstance(self.model(), QSortFilterProxyModel): + idx = self.model().mapToSource(self.model().index(row_num, column)) + item = self.model().sourceModel().itemFromIndex(idx) + else: + idx = self.model().index(row_num, column) + item = self.model().itemFromIndex(idx) + txt = item.text().lower() + if self.current_filter in txt: + # the filter matched, but the date filter might apply + self.setRowHidden(row_num, QModelIndex(), bool(should_hide)) + break + else: + # we did not find the filter in any columns, show the item + self.setRowHidden(row_num, QModelIndex(), True) def filter(self, p): - columns = self.__class__.filter_columns p = p.lower() self.current_filter = p - for item in self.get_leaves(self.invisibleRootItem()): - item.setHidden(all([item.text(column).lower().find(p) == -1 - for column in columns])) + self.hide_rows() + + def hide_rows(self): + for row in range(self.model().rowCount()): + self.hide_row(row) def create_toolbar(self, config=None): hbox = QHBoxLayout() @@ -790,22 +803,6 @@ def get_parent_main_window(widget): return widget return None -class SortableTreeWidgetItem(QTreeWidgetItem): - DataRole = Qt.UserRole + 100 - - def __lt__(self, other): - column = self.treeWidget().sortColumn() - if None not in [x.data(column, self.DataRole) for x in [self, other]]: - # We have set custom data to sort by - return self.data(column, self.DataRole) < other.data(column, self.DataRole) - try: - # Is the value something numeric? - return float(self.text(column)) < float(other.text(column)) - except ValueError: - # If not, we will just do string comparison - return self.text(column) < other.text(column) - - class IconCache: def __init__(self): @@ -821,6 +818,21 @@ def get_default_language(): name = QLocale.system().name() return name if name in languages else 'en_UK' +class FromList(QTreeWidget): + def __init__(self, parent, create_menu): + super().__init__(parent) + self.setHeaderHidden(True) + self.setMaximumHeight(300) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(create_menu) + self.setUniformRowHeights(True) + # remove left margin + self.setRootIsDecorated(False) + self.setColumnCount(2) + self.header().setStretchLastSection(False) + sm = QHeaderView.ResizeToContents + self.header().setSectionResizeMode(0, sm) + self.header().setSectionResizeMode(1, sm) if __name__ == "__main__": app = QApplication([]) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 0a6dc334..0b9d8550 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -23,49 +23,60 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from typing import Optional, List + from electrum.i18n import _ from .util import * - -class UTXOList(MyTreeWidget): - filter_columns = [0, 2] # Address, Label +class UTXOList(MyTreeView): + filter_columns = [0, 1] # Address, Label def __init__(self, parent=None): - MyTreeWidget.__init__(self, parent, self.create_menu, [ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')], 1) + super().__init__(parent, self.create_menu, 1) + self.setModel(QStandardItemModel(self)) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSortingEnabled(True) + self.update() - def get_name(self, x): - return x.get('prevout_hash') + ":%d"%x.get('prevout_n') - - def on_update(self): + def update(self): self.wallet = self.parent.wallet - item = self.currentItem() - self.clear() - self.utxos = self.wallet.get_utxos() - for x in self.utxos: + utxos = self.wallet.get_utxos() + self.utxo_dict = {} + self.model().clear() + self.update_headers([ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')]) + for idx, x in enumerate(utxos): address = x.get('address') height = x.get('height') - name = self.get_name(x) + name = x.get('prevout_hash') + ":%d"%x.get('prevout_n') + self.utxo_dict[name] = x label = self.wallet.get_label(x.get('prevout_hash')) amount = self.parent.format_amount(x['value'], whitespaces=True) - utxo_item = SortableTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]]) - utxo_item.setFont(0, QFont(MONOSPACE_FONT)) - utxo_item.setFont(2, QFont(MONOSPACE_FONT)) - utxo_item.setFont(4, QFont(MONOSPACE_FONT)) - utxo_item.setData(0, Qt.UserRole, name) + labels = [address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]] + utxo_item = [QStandardItem(x) for x in labels] + self.set_editability(utxo_item) + utxo_item[0].setFont(QFont(MONOSPACE_FONT)) + utxo_item[2].setFont(QFont(MONOSPACE_FONT)) + utxo_item[4].setFont(QFont(MONOSPACE_FONT)) + utxo_item[0].setData(name, Qt.UserRole) if self.wallet.is_frozen(address): - utxo_item.setBackground(0, ColorScheme.BLUE.as_color(True)) - self.addChild(utxo_item) + utxo_item[0].setBackground(ColorScheme.BLUE.as_color(True)) + self.model().insertRow(idx, utxo_item) + + def selected_column_0_user_roles(self) -> Optional[List[str]]: + if not self.model(): + return None + items = self.selected_in_column(0) + if not items: + return None + return [x.data(Qt.UserRole) for x in items] def create_menu(self, position): - selected = [x.data(0, Qt.UserRole) for x in self.selectedItems()] + selected = self.selected_column_0_user_roles() if not selected: return menu = QMenu() - coins = filter(lambda x: self.get_name(x) in selected, self.utxos) - + coins = (self.utxo_dict[name] for name in selected) menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins)) if len(selected) == 1: txid = selected[0].split(':')[0] @@ -75,7 +86,3 @@ class UTXOList(MyTreeWidget): menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label)) menu.exec_(self.viewport().mapToGlobal(position)) - - def on_permit_edit(self, item, column): - # disable editing fields in this tab (labels) - return False From 72957f4d51265f2d0e61d90f92ba42a6e2f92726 Mon Sep 17 00:00:00 2001 From: Janus Date: Mon, 3 Dec 2018 15:33:51 +0100 Subject: [PATCH 175/301] qt_standardmodel: only use proxymodel when appropriate --- electrum/gui/qt/history_list.py | 10 ++++------ electrum/gui/qt/util.py | 26 +++++++++++++------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 807484f9..f2feba9d 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -75,8 +75,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def should_hide(self, proxy_row): if self.start_timestamp and self.end_timestamp: - source_idx = self.proxy.mapToSource(self.proxy.index(proxy_row, 0)) - item = self.std_model.itemFromIndex(source_idx) + item = self.item_from_coordinate(proxy_row, 0) txid = item.data(self.TX_HASH_ROLE) date = self.transactions[txid]['date'] if date: @@ -418,9 +417,8 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): # TODO update unconfirmed tx'es def on_edited(self, index, user_role, text): - column = index.column() - index = self.proxy.mapToSource(index) - item = self.std_model.itemFromIndex(index) + row, column = index.row(), index.column() + item = self.item_from_coordinate(row, column) key = item.data(self.TX_HASH_ROLE) # fixme if column == 2: @@ -441,7 +439,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def mouseDoubleClickEvent(self, event: QMouseEvent): idx = self.indexAt(event.pos()) - item = self.std_model.itemFromIndex(self.proxy.mapToSource(idx)) + item = self.item_from_coordinate(idx.row(), idx.column()) if not item or item.isEditable(): super().mouseDoubleClickEvent(event) elif item: diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index e320715a..0d386bdf 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -468,15 +468,12 @@ class MyTreeView(QTreeView): pt.setX(50) self.customContextMenuRequested.emit(pt) - def createEditor(self, parent, option, index): + def createEditor(self, parent, option, idx): self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(), - parent, option, index) - persistent = QPersistentModelIndex(index) - user_role = index.data(Qt.UserRole) + parent, option, idx) + item = self.item_from_coordinate(idx.row(), idx.column()) + user_role = item.data(Qt.UserRole) assert user_role is not None - idx = QModelIndex(persistent) - index = self.proxy.mapToSource(idx) - item = self.std_model.itemFromIndex(index) prior_text = item.text() def editing_finished(): # Long-time QT bug - pressing Enter to finish editing signals @@ -524,6 +521,14 @@ class MyTreeView(QTreeView): """ pass + def item_from_coordinate(self, row_num, column): + if isinstance(self.model(), QSortFilterProxyModel): + idx = self.model().mapToSource(self.model().index(row_num, column)) + return self.model().sourceModel().itemFromIndex(idx) + else: + idx = self.model().index(row_num, column) + return self.model().itemFromIndex(idx) + def hide_row(self, row_num): """ row_num is for self.model(). So if there is a proxy, it is the row number @@ -535,12 +540,7 @@ class MyTreeView(QTreeView): self.setRowHidden(row_num, QModelIndex(), False) return for column in self.filter_columns: - if isinstance(self.model(), QSortFilterProxyModel): - idx = self.model().mapToSource(self.model().index(row_num, column)) - item = self.model().sourceModel().itemFromIndex(idx) - else: - idx = self.model().index(row_num, column) - item = self.model().itemFromIndex(idx) + item = self.item_from_coordinate(row_num, column) txt = item.text().lower() if self.current_filter in txt: # the filter matched, but the date filter might apply From 0677ce6d52b8951df04f31030e52b805027623d2 Mon Sep 17 00:00:00 2001 From: Janus Date: Mon, 3 Dec 2018 15:54:21 +0100 Subject: [PATCH 176/301] qt: avoid app.palette().text().color(), doesn't work on dark style --- electrum/gui/qt/history_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index f2feba9d..1ceb646b 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -97,7 +97,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.blue_brush = QBrush(QColor("#1E1EFF")) self.red_brush = QBrush(QColor("#BC1E1E")) self.monospace_font = QFont(MONOSPACE_FONT) - self.default_color = self.parent.app.palette().text().color() self.config = parent.config AcceptFileDragDrop.__init__(self, ".txn") self.setSortingEnabled(True) @@ -399,7 +398,8 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): cap_gains = self.parent.fx.get_history_capital_gains_config() items = self.txid_to_items[txid] self.ensure_fields_available(items, 7 if cap_gains else 5, txid) - items[5].setForeground(self.blue_brush if not row['fiat_default'] and row['fiat_value'] else self.default_color) + if not row['fiat_default'] and row['fiat_value']: + items[5].setForeground(self.blue_brush) value_str = self.parent.fx.format_fiat(row['fiat_value'].value) items[5].setText(value_str) items[5].setData(row['fiat_value'].value, self.SORT_ROLE) From d69ef890c0f7ab8c1f0e34c4f33853d57337804e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 3 Dec 2018 16:04:17 +0100 Subject: [PATCH 177/301] downgrade qdarkstyle for now see ColinDuquesnoy/QDarkStyleSheet#123 --- contrib/requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt index f4f458c3..7d42df3f 100644 --- a/contrib/requirements/requirements.txt +++ b/contrib/requirements/requirements.txt @@ -5,7 +5,7 @@ qrcode protobuf dnspython jsonrpclib-pelix -qdarkstyle<3.0 +qdarkstyle<2.6 aiorpcx>=0.9,<0.11 aiohttp aiohttp_socks From ea235a1468bacb609cf91c2639e2493df0a0306d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 3 Dec 2018 17:51:05 +0100 Subject: [PATCH 178/301] qt dark theme: use correct QR code icon (light/dark) --- electrum/gui/qt/address_dialog.py | 1 + electrum/gui/qt/qrtextedit.py | 3 ++- electrum/gui/qt/transaction_dialog.py | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index 9afd50ad..7b152b28 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -66,6 +66,7 @@ class AddressDialog(WindowModalDialog): for pubkey in pubkeys: pubkey_e = ButtonsLineEdit(pubkey) pubkey_e.addCopyButton(self.app) + pubkey_e.setReadOnly(True) vbox.addWidget(pubkey_e) try: diff --git a/electrum/gui/qt/qrtextedit.py b/electrum/gui/qt/qrtextedit.py index 09d5e2ce..5676d78a 100644 --- a/electrum/gui/qt/qrtextedit.py +++ b/electrum/gui/qt/qrtextedit.py @@ -13,7 +13,8 @@ class ShowQRTextEdit(ButtonsTextEdit): def __init__(self, text=None): ButtonsTextEdit.__init__(self, text) self.setReadOnly(1) - self.addButton(":icons/qrcode.png", self.qr_show, _("Show as QR code")) + icon = ":icons/qrcode_white.png" if ColorScheme.dark_scheme else ":icons/qrcode.png" + self.addButton(icon, self.qr_show, _("Show as QR code")) run_hook('show_text_edit', self) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 84a9e701..5e3836d3 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -98,7 +98,8 @@ class TxDialog(QDialog, MessageBoxMixin): vbox.addWidget(QLabel(_("Transaction ID:"))) self.tx_hash_e = ButtonsLineEdit() qr_show = lambda: parent.show_qrcode(str(self.tx_hash_e.text()), 'Transaction ID', parent=self) - self.tx_hash_e.addButton(":icons/qrcode.png", qr_show, _("Show as QR code")) + qr_icon = ":icons/qrcode_white.png" if ColorScheme.dark_scheme else ":icons/qrcode.png" + self.tx_hash_e.addButton(qr_icon, qr_show, _("Show as QR code")) self.tx_hash_e.setReadOnly(True) vbox.addWidget(self.tx_hash_e) self.tx_desc = QLabel() @@ -139,7 +140,7 @@ class TxDialog(QDialog, MessageBoxMixin): b.setDefault(True) self.qr_button = b = QPushButton() - b.setIcon(QIcon(":icons/qrcode.png")) + b.setIcon(QIcon(qr_icon)) b.clicked.connect(self.show_qr) self.copy_button = CopyButton(lambda: str(self.tx), parent.app) From 059beab700fcc4fe5cb979bae8c975ff6968554d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 3 Dec 2018 19:12:36 +0100 Subject: [PATCH 179/301] qt history list: small clean-up --- electrum/gui/qt/history_list.py | 22 ++++++---------------- electrum/gui/qt/main_window.py | 1 + electrum/gui/qt/util.py | 11 ++++++----- electrum/util.py | 4 ++-- 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 1ceb646b..8a381266 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -104,9 +104,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.end_timestamp = None self.years = [] self.create_toolbar_buttons() - self.wallet = None - - root = self.std_model.invisibleRootItem() self.wallet = self.parent.wallet # type: Abstract_Wallet fx = self.parent.fx @@ -144,7 +141,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.editable_columns -= {5} col_count = self.std_model.columnCount() diff = col_count-len(headers) - grew = False if col_count > len(headers): if diff == 2: self.std_model.removeColumns(6, diff) @@ -155,7 +151,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): while len(items) > col_count: items.pop() elif col_count < len(headers): - grew = True self.std_model.clear() self.txid_to_items.clear() self.transactions.clear() @@ -300,11 +295,9 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): tx_mined_status = TxMinedStatus(height, conf, timestamp, None) status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) has_invoice = self.wallet.invoices.paid.get(tx_hash) - icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) balance_str = self.parent.format_amount(balance, whitespaces=True) entry = ['', status_str, label, v_str, balance_str] - fiat_value = None item = [QStandardItem(e) for e in entry] item[3].setData(value, self.SORT_ROLE) item[4].setData(balance, self.SORT_ROLE) @@ -316,11 +309,11 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): item[2].setForeground(self.red_brush) item[3].setForeground(self.red_brush) self.txid_to_items[tx_hash] = item - self.update_item(tx_hash, self.parent.wallet.get_tx_height(tx_hash)) + self.update_item(tx_hash, self.wallet.get_tx_height(tx_hash)) source_row_idx = self.std_model.rowCount() self.std_model.insertRow(source_row_idx, item) new_idx = self.std_model.index(source_row_idx, 0) - history = self.parent.fx.show_history() + history = fx.show_history() if history: self.update_fiat(tx_hash, tx_item) self.hide_row(self.proxy.mapFromSource(new_idx).row()) @@ -344,7 +337,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): @profiler def update(self): - self.wallet = self.parent.wallet # type: Abstract_Wallet fx = self.parent.fx r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) seen = set() @@ -374,7 +366,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): old = self.transactions[txid] if old == row: continue - self.update_item(txid, self.parent.wallet.get_tx_height(txid)) + self.update_item(txid, self.wallet.get_tx_height(txid)) if history: self.update_fiat(txid, row) balance_str = self.parent.format_amount(row['balance'].value, whitespaces=True) @@ -422,16 +414,16 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): key = item.data(self.TX_HASH_ROLE) # fixme if column == 2: - self.parent.wallet.set_label(key, text) + self.wallet.set_label(key, text) self.update_labels() self.parent.update_completions() elif column == 5: tx_item = self.transactions[key] - self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, tx_item['value'].value) + self.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, tx_item['value'].value) value = tx_item['value'].value if value is not None: fee = tx_item['fee'] - fiat_fields = self.parent.wallet.get_tx_item_fiat(key, value, self.parent.fx, fee.value if fee else None) + fiat_fields = self.wallet.get_tx_item_fiat(key, value, self.parent.fx, fee.value if fee else None) tx_item.update(fiat_fields) self.update_fiat(key, tx_item) else: @@ -463,8 +455,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): item.setText(label) def update_item(self, tx_hash, tx_mined_status): - if self.wallet is None: - return conf = tx_mined_status.conf status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index db0e3065..468d97bc 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -117,6 +117,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.setup_exception_hook() self.network = gui_object.daemon.network # type: Network + assert wallet, "no wallet" self.wallet = wallet self.fx = gui_object.daemon.fx # type: FxThread self.invoices = wallet.invoices diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 0d386bdf..3329168b 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -4,8 +4,7 @@ import sys import platform import queue from functools import partial -from typing import NamedTuple, Callable, Optional -from abc import abstractmethod +from typing import NamedTuple, Callable, Optional, TYPE_CHECKING from PyQt5.QtGui import * from PyQt5.QtCore import * @@ -15,6 +14,9 @@ from electrum.i18n import _, languages from electrum.util import FileImportFailed, FileExportFailed from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED +if TYPE_CHECKING: + from .main_window import ElectrumWindow + if platform.system() == 'Windows': MONOSPACE_FONT = 'Lucida Console' @@ -401,7 +403,7 @@ class ElectrumItemDelegate(QStyledItemDelegate): class MyTreeView(QTreeView): - def __init__(self, parent, create_menu, stretch_column=None, editable_columns=None): + def __init__(self, parent: 'ElectrumWindow', create_menu, stretch_column=None, editable_columns=None): super().__init__(parent) self.parent = parent self.config = self.parent.config @@ -513,13 +515,12 @@ class MyTreeView(QTreeView): if self.current_filter: self.filter(self.current_filter) - @abstractmethod def should_hide(self, row): """ row_num is for self.model(). So if there is a proxy, it is the row number in that! """ - pass + return False def item_from_coordinate(self, row_num, column): if isinstance(self.model(), QSortFilterProxyModel): diff --git a/electrum/util.py b/electrum/util.py index ec28136e..a93b409e 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -860,8 +860,8 @@ def ignore_exceptions(func): class TxMinedStatus(NamedTuple): height: int conf: int - timestamp: int - header_hash: str + timestamp: Optional[int] + header_hash: Optional[str] class VerifiedTxInfo(NamedTuple): From 92a9cda4fc671323a461c9aba5e1fdc48a92c7ee Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 3 Dec 2018 22:11:36 +0100 Subject: [PATCH 180/301] plugins/digitalbitbox: compatibility with firmware v5.0.0 --- .../plugins/digitalbitbox/digitalbitbox.py | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index dd93f77f..314bf0da 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -4,7 +4,7 @@ # try: - from electrum.crypto import sha256d, EncodeAES_base64, DecodeAES_base64 + from electrum.crypto import sha256d, EncodeAES_base64, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, is_address) from electrum.bip32 import serialize_xpub, deserialize_xpub @@ -30,6 +30,8 @@ try: import base64 import os import sys + import re + import hmac DIGIBOX = True except ImportError as e: DIGIBOX = False @@ -43,6 +45,14 @@ except ImportError as e: def to_hexstr(s): return binascii.hexlify(s).decode('ascii') + +def derive_keys(x): + h = sha256d(x) + h = hashlib.sha512(h).digest() + return (h[:32],h[32:]) + +MIN_MAJOR_VERSION = 5 + class DigitalBitbox_Client(): def __init__(self, plugin, hidDevice): @@ -110,7 +120,6 @@ class DigitalBitbox_Client(): else: raise Exception('no reply') - def dbb_has_password(self): reply = self.hid_send_plain(b'{"ping":""}') if 'ping' not in reply: @@ -121,7 +130,6 @@ class DigitalBitbox_Client(): def stretch_key(self, key): - import hmac return to_hexstr(hashlib.pbkdf2_hmac('sha512', key.encode('utf-8'), b'Digital Bitbox', iterations = 20480)) @@ -158,6 +166,12 @@ class DigitalBitbox_Client(): def check_device_dialog(self): + match = re.search(r'v([0-9])+\.[0-9]+\.[0-9]+', self.dbb_hid.get_serial_number_string()) + if match is None: + raise Exception("error detecting firmware version") + major_version = int(match.group(1)) + if major_version < MIN_MAJOR_VERSION: + raise Exception("Please upgrade to the newest firmware using the BitBox Desktop app: https://shiftcrypto.ch/start") # Set password if fresh device if self.password is None and not self.dbb_has_password(): if not self.setupRunning: @@ -393,13 +407,21 @@ class DigitalBitbox_Client(): def hid_send_encrypt(self, msg): + sha256_byte_len = 32 reply = "" try: - secret = sha256d(self.password) - msg = EncodeAES_base64(secret, msg) - reply = self.hid_send_plain(msg) + encryption_key, authentication_key = derive_keys(self.password) + msg = EncodeAES_bytes(encryption_key, msg) + hmac_digest = hmac_oneshot(authentication_key, msg, hashlib.sha256) + authenticated_msg = base64.b64encode(msg + hmac_digest) + reply = self.hid_send_plain(authenticated_msg) if 'ciphertext' in reply: - reply = DecodeAES_base64(secret, ''.join(reply["ciphertext"])) + b64_unencoded = bytes(base64.b64decode(''.join(reply["ciphertext"]))) + reply_hmac = b64_unencoded[-sha256_byte_len:] + hmac_calculated = hmac_oneshot(authentication_key, b64_unencoded[:-sha256_byte_len], hashlib.sha256) + if not hmac.compare_digest(reply_hmac, hmac_calculated): + raise Exception("Failed to validate HMAC") + reply = DecodeAES_bytes(encryption_key, b64_unencoded[:-sha256_byte_len]) reply = to_string(reply, 'utf8') reply = json.loads(reply) if 'error' in reply: From bd5c82404d18370cb9a1c770d61dd05d57f9dcf1 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 4 Dec 2018 11:52:31 +0100 Subject: [PATCH 181/301] do not block load_wallet with watching_only warning --- electrum/gui/qt/__init__.py | 2 +- electrum/gui/qt/main_window.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 33ecd292..83f21cc2 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -191,13 +191,13 @@ class ElectrumGui(PrintError): self.network_updated_signal_obj) self.nd.show() - @profiler def create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() # FIXME: Remove in favour of the load_wallet hook run_hook('on_new_window', w) + w.warn_if_watching_only() return w def count_wizards_in_progress(func): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 468d97bc..4ee1cebb 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -413,7 +413,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.wallet.basename()) extra = [self.wallet.storage.get('wallet_type', '?')] if self.wallet.is_watching_only(): - self.warn_if_watching_only() extra.append(_('watching only')) title += ' [%s]'% ', '.join(extra) self.setWindowTitle(title) From ebea5b015997271abeb845ca01d58442e7454a7e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 4 Dec 2018 12:26:14 +0100 Subject: [PATCH 182/301] follow-up 5473320ce459b3076d60f71dab490ed3a07b86a5: do not call get_full_history in constructor --- electrum/gui/qt/history_list.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 8a381266..463213ca 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -104,27 +104,10 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.end_timestamp = None self.years = [] self.create_toolbar_buttons() - self.wallet = self.parent.wallet # type: Abstract_Wallet - fx = self.parent.fx - r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) - self.transactions.update([(x['txid'], x) for x in r['transactions']]) - self.summary = r['summary'] - if not self.years and self.transactions: - start_date = next(iter(self.transactions.values())).get('date') or date.today() - end_date = next(iter(reversed(self.transactions.values()))).get('date') or date.today() - self.years = [str(i) for i in range(start_date.year, end_date.year + 1)] - self.period_combo.insertItems(1, self.years) - if fx: fx.history_used_spot = False self.refresh_headers() - for tx_item in self.transactions.values(): - self.insert_tx(tx_item) self.sortByColumn(0, Qt.AscendingOrder) - #def on_activated(self, idx: QModelIndex): - # # TODO use siblingAtColumn when min Qt version is >=5.11 - # self.edit(idx.sibling(idx.row(), 2)) - def format_date(self, d): return str(datetime.date(d.year, d.month, d.day)) if d else _('None') @@ -338,6 +321,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): @profiler def update(self): fx = self.parent.fx + if fx: fx.history_used_spot = False r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) seen = set() history = fx.show_history() @@ -385,6 +369,13 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): assert removed_txid == txid, (idx, removed) removed += 1 self.apply_filter() + # update summary + self.summary = r['summary'] + if not self.years and self.transactions: + start_date = next(iter(self.transactions.values())).get('date') or date.today() + end_date = next(iter(reversed(self.transactions.values()))).get('date') or date.today() + self.years = [str(i) for i in range(start_date.year, end_date.year + 1)] + self.period_combo.insertItems(1, self.years) def update_fiat(self, txid, row): cap_gains = self.parent.fx.get_history_capital_gains_config() From 960855d0aace0417db1e0bedcaa6bcaad1f79e1d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Dec 2018 16:17:22 +0100 Subject: [PATCH 183/301] wallet history fees: only calculate fees when exporting history it's expensive, and it slows down startup of large wallets a lot --- electrum/address_synchronizer.py | 5 ++--- electrum/commands.py | 8 ++++++-- electrum/gui/qt/history_list.py | 11 ++++++++--- electrum/wallet.py | 9 ++++++--- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 909ed028..f59d350f 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -722,9 +722,8 @@ class AddressSynchronizer(PrintError): if fee is None: txid = tx.txid() fee = self.tx_fees.get(txid) - # cache fees. if wallet is synced, cache all; - # otherwise only cache non-None, as None can still change while syncing - if self.up_to_date or fee is not None: + # only cache non-None, as None can still change while syncing + if fee is not None: tx._cached_fee = fee return fee diff --git a/electrum/commands.py b/electrum/commands.py index 2192d992..fd478ded 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -520,9 +520,12 @@ class Commands: return tx.as_dict() @command('w') - def history(self, year=None, show_addresses=False, show_fiat=False): + def history(self, year=None, show_addresses=False, show_fiat=False, show_fees=False): """Wallet history. Returns the transaction history of your wallet.""" - kwargs = {'show_addresses': show_addresses} + kwargs = { + 'show_addresses': show_addresses, + 'show_fees': show_fees, + } if year: import time start_date = datetime.datetime(year, 1, 1) @@ -808,6 +811,7 @@ command_options = { 'paid': (None, "Show only paid requests."), 'show_addresses': (None, "Show input and output addresses"), 'show_fiat': (None, "Show fiat value of transactions"), + 'show_fees': (None, "Show miner fees paid by transactions"), 'year': (None, "Show history for a given year"), 'fee_method': (None, "Fee estimation method to use"), 'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position") diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 463213ca..49cdfe03 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -555,10 +555,15 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.parent.show_message(_("Your wallet history has been successfully exported.")) def do_export_history(self, file_name, is_csv): - history = self.transactions.values() + hist = self.wallet.get_full_history(domain=self.get_domain(), + from_timestamp=None, + to_timestamp=None, + fx=self.parent.fx, + show_fees=True) + txns = hist['transactions'] lines = [] if is_csv: - for item in history: + for item in txns: lines.append([item['txid'], item.get('label', ''), item['confirmations'], @@ -583,4 +588,4 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): transaction.writerow(line) else: from electrum.util import json_encode - f.write(json_encode(history)) + f.write(json_encode(txns)) diff --git a/electrum/wallet.py b/electrum/wallet.py index b38f441d..a32888cf 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -394,7 +394,8 @@ class Abstract_Wallet(AddressSynchronizer): return balance @profiler - def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False): + def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, + fx=None, show_addresses=False, show_fees=False): out = [] income = 0 expenditures = 0 @@ -420,8 +421,10 @@ class Abstract_Wallet(AddressSynchronizer): 'date': timestamp_to_datetime(timestamp), 'label': self.get_label(tx_hash), } - tx_fee = self.get_tx_fee(tx) - item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None + tx_fee = None + if show_fees: + tx_fee = self.get_tx_fee(tx) + item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None if show_addresses: item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs())) item['outputs'] = list(map(lambda x:{'address':x.address, 'value':Satoshis(x.value)}, From 923a9c36cbf4b47e5942e80ebb478dcc063e5611 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Dec 2018 16:44:50 +0100 Subject: [PATCH 184/301] util: Satoshis and Fiat should not be namedtuples undo part of 37b009a342b7a815bb31c63d3d496e019d9b1efa due to json encoding problems --- electrum/util.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index a93b409e..8de39592 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -130,15 +130,35 @@ class UserCancelled(Exception): '''An exception that is suppressed from the user''' pass -class Satoshis(NamedTuple): - value: int + +# note: this is not a NamedTuple as then its json encoding cannot be customized +class Satoshis(object): + __slots__ = ('value',) + + def __new__(cls, value): + self = super(Satoshis, cls).__new__(cls) + self.value = value + return self + + def __repr__(self): + return 'Satoshis(%d)'%self.value def __str__(self): return format_satoshis(self.value) + " BTC" -class Fiat(NamedTuple): - value: Optional[Decimal] - ccy: str + +# note: this is not a NamedTuple as then its json encoding cannot be customized +class Fiat(object): + __slots__ = ('value', 'ccy') + + def __new__(cls, value, ccy): + self = super(Fiat, cls).__new__(cls) + self.ccy = ccy + self.value = value + return self + + def __repr__(self): + return 'Fiat(%s)'% self.__str__() def __str__(self): if self.value is None or self.value.is_nan(): @@ -146,8 +166,10 @@ class Fiat(NamedTuple): else: return "{:.2f}".format(self.value) + ' ' + self.ccy + class MyEncoder(json.JSONEncoder): def default(self, obj): + # note: this does not get called for namedtuples :( https://bugs.python.org/issue30343 from .transaction import Transaction if isinstance(obj, Transaction): return obj.as_dict() From e35f2c5beded584fa2845af9f3a6f18211a75a23 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Dec 2018 17:27:02 +0100 Subject: [PATCH 185/301] qt history list: fix #4896 --- electrum/gui/qt/history_list.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 49cdfe03..a9ceb0cc 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -461,7 +461,9 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): org_idx: QModelIndex = self.indexAt(position) idx = self.proxy.mapToSource(org_idx) item: QStandardItem = self.std_model.itemFromIndex(idx) - assert item, 'create_menu: index not found in model' + if not item: + # can happen e.g. before list is populated for the first time + return tx_hash = idx.data(self.TX_HASH_ROLE) column = idx.column() assert tx_hash, "create_menu: no tx hash" From cc0db418797b469abe684beb1a8208c51c3e5e25 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Dec 2018 22:24:32 +0100 Subject: [PATCH 186/301] qt history: speed up ensure_fields_available (faster startup) --- electrum/gui/qt/history_list.py | 8 ++--- electrum/util.py | 54 ++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index a9ceb0cc..d5dbd107 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -31,7 +31,7 @@ from collections import OrderedDict from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ -from electrum.util import block_explorer_URL, profiler, print_error, TxMinedStatus, Fiat +from electrum.util import block_explorer_URL, profiler, print_error, TxMinedStatus, OrderedDictWithIndex from .util import * @@ -92,7 +92,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.setModel(self.proxy) self.txid_to_items = {} - self.transactions = OrderedDict() + self.transactions = OrderedDictWithIndex() self.summary = {} self.blue_brush = QBrush(QColor("#1E1EFF")) self.red_brush = QBrush(QColor("#BC1E1E")) @@ -311,7 +311,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def ensure_fields_available(self, items, idx, txid): while len(items) < idx + 1: - row = list(self.transactions.keys()).index(txid) + row = self.transactions.get_pos_of_key(txid) qidx = self.std_model.index(row, len(items)) assert qidx.isValid(), (self.std_model.columnCount(), idx) item = self.std_model.itemFromIndex(qidx) @@ -334,7 +334,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): txid = row['txid'] if txid not in self.transactions: self.transactions[txid] = row - self.transactions.move_to_end(txid, last=True) self.insert_tx(row) return else: @@ -344,7 +343,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): seen.add(txid) if txid not in self.transactions: self.transactions[txid] = row - self.transactions.move_to_end(txid, last=True) self.insert_tx(row) continue old = self.transactions[txid] diff --git a/electrum/util.py b/electrum/util.py index 8de39592..047206e6 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -22,7 +22,7 @@ # SOFTWARE. import binascii import os, sys, re, json -from collections import defaultdict +from collections import defaultdict, OrderedDict from typing import NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable from datetime import datetime import decimal @@ -993,3 +993,55 @@ def create_and_start_event_loop() -> Tuple[asyncio.AbstractEventLoop, name='EventLoop') loop_thread.start() return loop, stopping_fut, loop_thread + + +class OrderedDictWithIndex(OrderedDict): + """An OrderedDict that keeps track of the positions of keys. + + Note: very inefficient to modify contents, except to add new items. + """ + + _key_to_pos = {} + + def _recalc_key_to_pos(self): + self._key_to_pos = {key: pos for (pos, key) in enumerate(self.keys())} + + def get_pos_of_key(self, key): + return self._key_to_pos[key] + + def popitem(self, *args, **kwargs): + ret = super().popitem(*args, **kwargs) + self._recalc_key_to_pos() + return ret + + def move_to_end(self, *args, **kwargs): + ret = super().move_to_end(*args, **kwargs) + self._recalc_key_to_pos() + return ret + + def clear(self): + ret = super().clear() + self._recalc_key_to_pos() + return ret + + def pop(self, *args, **kwargs): + ret = super().pop(*args, **kwargs) + self._recalc_key_to_pos() + return ret + + def update(self, *args, **kwargs): + ret = super().update(*args, **kwargs) + self._recalc_key_to_pos() + return ret + + def __delitem__(self, *args, **kwargs): + ret = super().__delitem__(*args, **kwargs) + self._recalc_key_to_pos() + return ret + + def __setitem__(self, key, *args, **kwargs): + is_new_key = key not in self + ret = super().__setitem__(key, *args, **kwargs) + if is_new_key: + self._key_to_pos[key] = len(self) - 1 + return ret From c3deb16a7d546e4e234d66159629e528c2b7ccce Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Dec 2018 12:26:03 +0100 Subject: [PATCH 187/301] exchange rate: fix coinbase closes #4897 --- electrum/exchange_rate.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 4f3e9cb6..bfe99165 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -246,10 +246,9 @@ class BTCParalelo(ExchangeBase): class Coinbase(ExchangeBase): async def get_rates(self, ccy): - json = await self.get_json('coinbase.com', - '/api/v1/currencies/exchange_rates') - return dict([(r[7:].upper(), Decimal(json[r])) - for r in json if r.startswith('btc_to_')]) + json = await self.get_json('api.coinbase.com', + '/v2/exchange-rates?currency=BTC') + return {ccy: Decimal(rate) for (ccy, rate) in json["data"]["rates"].items()} class CoinDesk(ExchangeBase): From 8571cafcc8fcb6bb644836033724c5255cf21394 Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 5 Dec 2018 14:24:32 +0100 Subject: [PATCH 188/301] trezor: call get_xpub with correct argument `creating` indicates that this is a new wallet. Which is always the case in `setup_device` --- electrum/plugins/trezor/trezor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 1dc858ca..77954bf5 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -267,10 +267,9 @@ class TrezorPlugin(HW_PluginBase): _('Make sure it is in the correct state.')) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) - creating = not device_info.initialized - if creating: + if not device_info.initialized: self.initialize_device(device_id, wizard, client.handler) - client.get_xpub('m', 'standard', creating) + client.get_xpub('m', 'standard', creating=True) client.used() def get_xpub(self, device_id, derivation, xtype, wizard): From 43acd09df84db600be440b8f19df7a42e4c3f93f Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 5 Dec 2018 14:26:19 +0100 Subject: [PATCH 189/301] trezor: support outdated firmware notifications Outdated firmware error messages were originally raised from create_client, which would mean that a client for an outdated device would not be created. This had a number of undesirable outcomes due to "client does not exist" being conflated with "no device is connected". Instead, we raise in setup_client (which prevents creating new wallets with outdated devices, BUT shows them in device list), and python-trezor also raises on most calls (which gives us an error message when opening wallet and/or trying to do basically anything with it). This is still suboptimal - i.e., there's currently no way for Electrum to claim higher version requirement than the underlying python-trezor, and so minimum_firmware property is pretty much useless ATM. --- electrum/plugins/trezor/clientbase.py | 16 ++++++++-------- electrum/plugins/trezor/trezor.py | 22 ++++++++-------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index 037afeca..687f4c95 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -2,12 +2,12 @@ import time from struct import pack from electrum.i18n import _ -from electrum.util import PrintError, UserCancelled +from electrum.util import PrintError, UserCancelled, UserFacingException from electrum.keystore import bip39_normalize_passphrase from electrum.bip32 import serialize_xpub, convert_bip32_path_to_list_of_uint32 as parse_path from trezorlib.client import TrezorClient -from trezorlib.exceptions import TrezorFailure, Cancelled +from trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError from trezorlib.messages import WordRequestType, FailureType, RecoveryDeviceType import trezorlib.btc import trezorlib.device @@ -66,6 +66,8 @@ class TrezorClientBase(PrintError): raise UserCancelled from exc_value elif issubclass(exc_type, TrezorFailure): raise RuntimeError(exc_value.message) from exc_value + elif issubclass(exc_type, OutdatedFirmwareError): + raise UserFacingException(exc_value) from exc_value else: return False return True @@ -163,12 +165,10 @@ class TrezorClientBase(PrintError): self.print_error("closing client") self.clear_session() - def firmware_version(self): - f = self.features - return (f.major_version, f.minor_version, f.patch_version) - - def atleast_version(self, major, minor=0, patch=0): - return self.firmware_version() >= (major, minor, patch) + def is_uptodate(self): + if self.client.is_outdated(): + return False + return self.client.version >= self.plugin.minimum_firmware def get_trezor_model(self): """Returns '1' for Trezor One, 'T' for Trezor T.""" diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 77954bf5..92ea25cc 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -141,20 +141,7 @@ class TrezorPlugin(HW_PluginBase): self.print_error("connected to device at", device.path) # note that this call can still raise! - client = TrezorClientBase(transport, handler, self) - - if not client.atleast_version(*self.minimum_firmware): - msg = (_('Outdated {} firmware for device labelled {}. Please ' - 'download the updated firmware from {}') - .format(self.device, client.label(), self.firmware_URL)) - self.print_error(msg) - if handler: - handler.show_error(msg) - else: - raise UserFacingException(msg) - return None - - return client + return TrezorClientBase(transport, handler, self) def get_client(self, keystore, force_pair=True): devmgr = self.device_manager() @@ -265,6 +252,13 @@ class TrezorPlugin(HW_PluginBase): if client is None: raise UserFacingException(_('Failed to create a client for this device.') + '\n' + _('Make sure it is in the correct state.')) + + if not client.is_uptodate(): + msg = (_('Outdated {} firmware for device labelled {}. Please ' + 'download the updated firmware from {}') + .format(self.device, client.label(), self.firmware_URL)) + raise UserFacingException(msg) + # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) if not device_info.initialized: From 8e681c1723f934a3f68124557b51aface4e0cd9d Mon Sep 17 00:00:00 2001 From: matejcik Date: Wed, 5 Dec 2018 14:32:19 +0100 Subject: [PATCH 190/301] trezor: update name (TREZOR -> Trezor) --- electrum/plugins/trezor/__init__.py | 6 +++--- electrum/plugins/trezor/trezor.py | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/electrum/plugins/trezor/__init__.py b/electrum/plugins/trezor/__init__.py index e3b08ed6..2d4267d7 100644 --- a/electrum/plugins/trezor/__init__.py +++ b/electrum/plugins/trezor/__init__.py @@ -1,8 +1,8 @@ from electrum.i18n import _ -fullname = 'TREZOR Wallet' -description = _('Provides support for TREZOR hardware wallet') +fullname = 'Trezor Wallet' +description = _('Provides support for Trezor hardware wallet') requires = [('trezorlib','github.com/trezor/python-trezor')] -registers_keystore = ('hardware', 'trezor', _("TREZOR wallet")) +registers_keystore = ('hardware', 'trezor', _("Trezor wallet")) available_for = ['qt', 'cmdline'] diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 92ea25cc..47b4aa30 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -37,13 +37,15 @@ except Exception as e: RECOVERY_TYPE_SCRAMBLED_WORDS, RECOVERY_TYPE_MATRIX = range(2) -# TREZOR initialization methods +# Trezor initialization methods TIM_NEW, TIM_RECOVER = range(2) +TREZOR_PRODUCT_KEY = 'Trezor' + class TrezorKeyStore(Hardware_KeyStore): hw_type = 'trezor' - device = 'TREZOR' + device = TREZOR_PRODUCT_KEY def get_derivation(self): return self.derivation @@ -97,7 +99,7 @@ class TrezorPlugin(HW_PluginBase): minimum_library = (0, 11, 0) maximum_library = (0, 12) SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') - DEVICE_IDS = ('TREZOR',) + DEVICE_IDS = (TREZOR_PRODUCT_KEY,) MAX_LABEL_LEN = 32 @@ -122,7 +124,7 @@ class TrezorPlugin(HW_PluginBase): return [Device(path=d.get_path(), interface_number=-1, id_=d.get_path(), - product_key='TREZOR', + product_key=TREZOR_PRODUCT_KEY, usage_page=0, transport_ui_string=d.get_path()) for d in devices] From e8a8a172174ff49adff556be6b025fc6853b20ad Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Dec 2018 18:55:19 +0100 Subject: [PATCH 191/301] test_wallet_vertical: offline sign with old seed --- electrum/tests/test_wallet_vertical.py | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 2f4d336d..9bc1a6e8 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -1032,6 +1032,47 @@ class TestWalletOfflineSigning(TestCaseForTestnet): super().tearDownClass() shutil.rmtree(cls.electrum_path) + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_sending_offline_old_electrum_seed_online_mpk(self, mock_write): + wallet_offline = WalletIntegrityHelper.create_standard_wallet( + keystore.from_seed('alone body father children lead goodbye phone twist exist grass kick join', '', False), + gap_limit=4 + ) + wallet_online = WalletIntegrityHelper.create_standard_wallet( + keystore.from_master_key('cd805ed20aec61c7a8b409c121c6ba60a9221f46d20edbc2be83ebd91460e97937cd7d782e77c1cb08364c6bc1c98bc040fdad53f22f29f7d3a85c8e51f9c875'), + gap_limit=4 + ) + + # bootstrap wallet_online + funding_tx = Transaction('01000000000101161115f8d8110001aa0883989487f9c7a2faf4451038e4305c7594c5236cbb490100000000fdffffff0338117a0000000000160014c1d7b2ded7017cbde837aab36c1e7b2a3952a57800127a00000000001600143e2ab71fc9738ce16fbe6b3b1c210a68c12db84180969800000000001976a91424b64d981d621c227716b51479faf33019371f4688ac0247304402207a5efc6d970f6a5fdcd1933f68b353b4bf2904743f9f1dc3e9177d8754074baf02202eed707e661493bc450357f12cd7a8b8c610c7cb32ded10516c2933a2ba4346a01210287dce03f594fd889726b13a12970237992a0094a5c9f4eebcca6d50d454b39e9ff121600') + funding_txid = funding_tx.txid() + self.assertEqual('3b9e0581602f4656cb04633dac13662bc62d9f5191caa15cc901dcc76e430856', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qyw3c0rvn6kk2c688y3dygvckn57525y8qnxt3a', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, config=self.config, fee=5000) + tx.set_rbf(True) + tx.locktime = 1446655 + + self.assertFalse(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertFalse(tx.is_segwit()) + self.assertEqual('01000000015608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b020000008b483045022100e3489a26b47412c617a77024db8f51c68ed13950d147425bdbc7d64d95477ce2022009d09a6f011a47f827ae6655a828c79b1cf43f7c89ab4f8bb36f175ba8298d8a014104e79eb77f2f3f989f5e9d090bc0af50afeb0d5bd6ec916f2022c5629ed022e84a87584ef647d69f073ea314a0f0c110ebe24ad64bc1922a10819ea264fc3f35f5fdffffff02a02526000000000016001423a3878d93d5acac68e7245a4433169d3d455087585d7200000000001976a914b6a6bbbc4cf9da58786a8acc58291e218d52130688acff121600', + str(tx)) + self.assertEqual('5a88637fe51fc1780f61383d7d8cb44e6209dbc99e102a0efcfcbe877d203d7d', tx.txid()) + self.assertEqual('5a88637fe51fc1780f61383d7d8cb44e6209dbc99e102a0efcfcbe877d203d7d', tx.wtxid()) + @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_write): From 993374dce7bf5e0df9a54b6ebc8527d09a7bb515 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Dec 2018 19:03:46 +0100 Subject: [PATCH 192/301] travis: build android apk --- .travis.yml | 20 ++++++++++++++++++-- contrib/make_packages | 5 +---- electrum/gui/kivy/tools/Dockerfile | 8 ++++---- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index bfcef023..31c40c6b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ after_success: jobs: include: - stage: binary builds + name: "Windows build" sudo: true language: c python: false @@ -36,11 +37,26 @@ jobs: services: - docker install: - - sudo docker build --no-cache -t electrum-wine-builder-img ./contrib/build-wine/docker/ + - sudo docker build --no-cache -t electrum-wine-builder-img ./contrib/build-wine/docker/ script: - sudo docker run --name electrum-wine-builder-cont -v $PWD:/opt/wine64/drive_c/electrum --rm --workdir /opt/wine64/drive_c/electrum/contrib/build-wine electrum-wine-builder-img ./build.sh after_success: true - - os: osx + - name: "Android build" + language: python + python: 3.7 + services: + - docker + install: + - ./contrib/make_packages + - sudo docker build --no-cache -t electrum-android-builder-img electrum/gui/kivy/tools + script: + - sudo chown -R 1000:1000 . + - sudo docker run -it -u 1000:1000 --rm --name electrum-android-builder-cont -v $PWD:/home/user/wspace/electrum --workdir /home/user/wspace/electrum electrum-android-builder-img ./contrib/make_apk + - ls -la bin + - if [ $(ls bin | grep -c Electrum-*) -eq 0 ]; then exit 1; fi + after_success: true + - name: "MacOS build" + os: osx language: c env: - TARGET_OS=macOS diff --git a/contrib/make_packages b/contrib/make_packages index 9cfd32bb..0e4ac67b 100755 --- a/contrib/make_packages +++ b/contrib/make_packages @@ -3,11 +3,8 @@ contrib=$(dirname "$0") test -n "$contrib" -a -d "$contrib" || exit -whereis pip3 -if [ $? -ne 0 ] ; then echo "Install pip3" ; exit ; fi - rm "$contrib"/../packages/ -r #Install pure python modules in electrum directory -pip3 install -r $contrib/deterministic-build/requirements.txt -t $contrib/../packages +python3 -m pip install -r $contrib/deterministic-build/requirements.txt -t $contrib/../packages diff --git a/electrum/gui/kivy/tools/Dockerfile b/electrum/gui/kivy/tools/Dockerfile index 0300d862..71a04ee5 100644 --- a/electrum/gui/kivy/tools/Dockerfile +++ b/electrum/gui/kivy/tools/Dockerfile @@ -56,10 +56,10 @@ RUN apt -y update -qq \ RUN yes | "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" --licenses > /dev/null # download platforms, API, build tools -RUN "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-24" && \ - "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-28" && \ - "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;28.0.3" && \ - "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "extras;android;m2repository" && \ +RUN "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-24" > /dev/null && \ + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "platforms;android-28" > /dev/null && \ + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "build-tools;28.0.3" > /dev/null && \ + "${ANDROID_SDK_HOME}/tools/bin/sdkmanager" "extras;android;m2repository" > /dev/null && \ chmod +x "${ANDROID_SDK_HOME}/tools/bin/avdmanager" From a62e5d39ca14b5f6cd144828fdc390f987d19f2d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Dec 2018 05:10:24 +0100 Subject: [PATCH 193/301] android build: add "how to deploy apk on phone" to readme --- electrum/gui/kivy/Readme.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/electrum/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md index f394ddf1..f4c89ea1 100644 --- a/electrum/gui/kivy/Readme.md +++ b/electrum/gui/kivy/Readme.md @@ -55,6 +55,14 @@ folder. You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}` +### How do I deploy on connected phone for quick testing? +Assuming `adb` is installed: +``` +$ adb -d install -r bin/Electrum-*-debug.apk +$ adb shell monkey -p org.electrum.electrum 1 +``` + + ### How do I get an interactive shell inside docker? ``` $ sudo docker run -it --rm \ From 8999e92f76f39a965fc8d49a3efe56b1daa187fa Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Dec 2018 13:43:24 +0100 Subject: [PATCH 194/301] android build: fix warning re ndk_api "NDK API target was not set manually, using the default of 21 = min(android-api=28, default ndk-api=21)" --- electrum/gui/kivy/tools/buildozer.spec | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec index 2c4e7745..0b55acdf 100644 --- a/electrum/gui/kivy/tools/buildozer.spec +++ b/electrum/gui/kivy/tools/buildozer.spec @@ -57,7 +57,7 @@ android.permissions = INTERNET, CAMERA # (int) Android API to use android.api = 28 -# (int) Minimum API required (8 = Android 2.2 devices) +# (int) Minimum API required. You will need to set the android.ndk_api to be as low as this value. android.minapi = 21 # (int) Android SDK version to use @@ -66,6 +66,9 @@ android.sdk = 24 # (str) Android NDK version to use android.ndk = 14b +(int) Android NDK API to use (optional). This is the minimum API your app will support. +android.ndk_api = 21 + # (bool) Use --private data storage (True) or --dir public storage (False) android.private_storage = True From 2f7573850ef911333ad1d890558f87c9cb429721 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Dec 2018 16:05:35 +0100 Subject: [PATCH 195/301] fix prev --- electrum/gui/kivy/tools/buildozer.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec index 0b55acdf..670d24f4 100644 --- a/electrum/gui/kivy/tools/buildozer.spec +++ b/electrum/gui/kivy/tools/buildozer.spec @@ -66,7 +66,7 @@ android.sdk = 24 # (str) Android NDK version to use android.ndk = 14b -(int) Android NDK API to use (optional). This is the minimum API your app will support. +# (int) Android NDK API to use (optional). This is the minimum API your app will support. android.ndk_api = 21 # (bool) Use --private data storage (True) or --dir public storage (False) From 605982a2b73e85d314c2cfb2c1180b3b093aee98 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Dec 2018 16:23:24 +0100 Subject: [PATCH 196/301] android build: less verbose buildozer logs --- .travis.yml | 4 ++++ electrum/gui/kivy/Readme.md | 4 ++++ electrum/gui/kivy/tools/buildozer.spec | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 31c40c6b..5c87a03c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,7 +51,11 @@ jobs: - sudo docker build --no-cache -t electrum-android-builder-img electrum/gui/kivy/tools script: - sudo chown -R 1000:1000 . + # Output something every minute or Travis kills the job + - while sleep 60; do echo "=====[ $SECONDS seconds still running ]====="; done & - sudo docker run -it -u 1000:1000 --rm --name electrum-android-builder-cont -v $PWD:/home/user/wspace/electrum --workdir /home/user/wspace/electrum electrum-android-builder-img ./contrib/make_apk + # kill background sleep loop + - kill %1 - ls -la bin - if [ $(ls bin | grep -c Electrum-*) -eq 0 ]; then exit 1; fi after_success: true diff --git a/electrum/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md index f4c89ea1..b11ee9f0 100644 --- a/electrum/gui/kivy/Readme.md +++ b/electrum/gui/kivy/Readme.md @@ -70,3 +70,7 @@ $ sudo docker run -it --rm \ --workdir /home/user/wspace/electrum \ electrum-android-builder-img ``` + + +### How do I get more verbose logs? +See `log_level` in `buildozer.spec` diff --git a/electrum/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec index 670d24f4..302fae19 100644 --- a/electrum/gui/kivy/tools/buildozer.spec +++ b/electrum/gui/kivy/tools/buildozer.spec @@ -148,7 +148,7 @@ p4a.source_dir = /opt/python-for-android [buildozer] # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) -log_level = 2 +log_level = 1 # ----------------------------------------------------------------------------- From 9e86bc586ccaf753e845eb5b8cfe24336614efe9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Dec 2018 18:25:19 +0100 Subject: [PATCH 197/301] trezor: only confirm passphrase when creating wallet but not when decrypting --- electrum/plugins/trezor/trezor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 47b4aa30..ff9b2c8c 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -9,7 +9,7 @@ from electrum.i18n import _ from electrum.plugin import Device from electrum.transaction import deserialize, Transaction from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey -from electrum.base_wizard import ScriptTypeNotSupported +from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET from ..hw_wallet import HW_PluginBase from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data @@ -265,7 +265,8 @@ class TrezorPlugin(HW_PluginBase): client.handler = self.create_handler(wizard) if not device_info.initialized: self.initialize_device(device_id, wizard, client.handler) - client.get_xpub('m', 'standard', creating=True) + is_creating_wallet = purpose == HWD_SETUP_NEW_WALLET + client.get_xpub('m', 'standard', creating=is_creating_wallet) client.used() def get_xpub(self, device_id, derivation, xtype, wizard): From 20fa7fc2f7aa16551b188c5aec1103f01e8bd814 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Dec 2018 19:33:02 +0100 Subject: [PATCH 198/301] trezor: fix sign_transaction prev_tx --- electrum/plugins/trezor/trezor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index ff9b2c8c..36b89215 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -302,7 +302,7 @@ class TrezorPlugin(HW_PluginBase): raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) def sign_transaction(self, keystore, tx, prev_tx, xpub_path): - prev_tx = { txhash: self.electrum_tx_to_txtype(tx, xpub_path) for txhash, tx in prev_tx.items() } + prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx, xpub_path) for txhash, tx in prev_tx.items() } client = self.get_client(keystore) inputs = self.tx_inputs(tx, xpub_path, True) outputs = self.tx_outputs(keystore.get_derivation(), tx) From 8c3920a0db10d5519bc2435a5354c13aadb5ed1c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Dec 2018 18:16:34 +0100 Subject: [PATCH 199/301] hw: check_libraries_available now gets version of incompatible libs previously we would return early and the user would just see "missing libraries" --- electrum/plugins/coldcard/coldcard.py | 11 +++++++--- electrum/plugins/hw_wallet/plugin.py | 29 ++++++++++++++++----------- electrum/plugins/trezor/trezor.py | 14 ++++++++----- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index 96d80777..55191fbd 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -17,6 +17,7 @@ from electrum.util import print_error, bfh, bh2u, versiontuple, UserFacingExcept from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase +from ..hw_wallet.plugin import LibraryFoundButUnusable try: import hid @@ -610,7 +611,7 @@ class ColdcardPlugin(HW_PluginBase): def __init__(self, parent, config, name): HW_PluginBase.__init__(self, parent, config, name) - self.libraries_available = self.check_libraries_available() and requirements_ok + self.libraries_available = self.check_libraries_available() if not self.libraries_available: return @@ -620,9 +621,13 @@ class ColdcardPlugin(HW_PluginBase): def get_library_version(self): import ckcc try: - return ckcc.__version__ + version = ckcc.__version__ except AttributeError: - return 'unknown' + version = 'unknown' + if requirements_ok: + return version + else: + raise LibraryFoundButUnusable(library_version=version) def detect_simulator(self): # if there is a simulator running on this machine, diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index b1eadbd5..8abb210e 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -86,6 +86,7 @@ class HW_PluginBase(BasePlugin): Returns 'unknown' if library is found but cannot determine version. Raises 'ImportError' if library is not found. + Raises 'LibraryFoundButUnusable' if found but there was some problem (includes version num). """ raise NotImplementedError() @@ -94,23 +95,22 @@ class HW_PluginBase(BasePlugin): return ".".join(str(i) for i in t) try: + # this might raise ImportError or LibraryFoundButUnusable library_version = self.get_library_version() + # if no exception so far, we might still raise LibraryFoundButUnusable + if (library_version == 'unknown' + or versiontuple(library_version) < self.minimum_library + or hasattr(self, "maximum_library") and versiontuple(library_version) >= self.maximum_library): + raise LibraryFoundButUnusable(library_version=library_version) except ImportError: return False - if library_version == 'unknown' or \ - versiontuple(library_version) < self.minimum_library: - self.libraries_available_message = ( - _("Library version for '{}' is too old.").format(self.name) - + '\nInstalled: {}, Needed: {}' - .format(library_version, version_str(self.minimum_library))) - self.print_stderr(self.libraries_available_message) - return False - elif hasattr(self, "maximum_library") and \ - versiontuple(library_version) >= self.maximum_library: + except LibraryFoundButUnusable as e: + library_version = e.library_version + max_version_str = version_str(self.maximum_library) if hasattr(self, "maximum_library") else "inf" self.libraries_available_message = ( _("Library version for '{}' is incompatible.").format(self.name) - + '\nInstalled: {}, Needed: less than {}' - .format(library_version, version_str(self.maximum_library))) + + '\nInstalled: {}, Needed: {} <= x < {}' + .format(library_version, version_str(self.minimum_library), max_version_str)) self.print_stderr(self.libraries_available_message) return False @@ -155,3 +155,8 @@ def only_hook_if_libraries_available(func): if not self.libraries_available: return None return func(self, *args, **kwargs) return wrapper + + +class LibraryFoundButUnusable(Exception): + def __init__(self, library_version='unknown'): + self.library_version = library_version diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 36b89215..1df9d663 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -12,7 +12,8 @@ from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET from ..hw_wallet import HW_PluginBase -from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data +from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data, + LibraryFoundButUnusable) try: import trezorlib @@ -112,12 +113,15 @@ class TrezorPlugin(HW_PluginBase): self.device_manager().register_enumerate_func(self.enumerate) def get_library_version(self): - if not TREZORLIB: - raise ImportError + import trezorlib try: - return trezorlib.__version__ + version = trezorlib.__version__ except Exception: - return 'unknown' + version = 'unknown' + if TREZORLIB: + return version + else: + raise LibraryFoundButUnusable(library_version=version) def enumerate(self): devices = trezorlib.transport.enumerate_devices() From 503bd357f48d640612ceab501521125448daeba2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 7 Dec 2018 04:06:51 +0100 Subject: [PATCH 200/301] requirements: bump python-trezor to 0.11 --- 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 a6ae0a3a..5ba0271d 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -1,5 +1,5 @@ Cython>=0.27 -trezor[hidapi]>=0.9.0 +trezor[hidapi]>=0.11.0 safet[hidapi]>=0.1.0 keepkey btchip-python From 31a5d0c2f07ab3d60bfea26919b17a21adaf3ef7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 7 Dec 2018 04:32:17 +0100 Subject: [PATCH 201/301] tweak release notes for 3.3 --- RELEASE-NOTES | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 43486f91..bf831f91 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,13 +1,29 @@ -# Release 3.3 - (Hodler's Edition) +# Release 3.3.0 - Hodler's Edition (unreleased) - * The network layer has been rewritten using asyncio. - * Follow blockchain that has the most work, not length. + * The network layer has been rewritten using asyncio and aiorpcx. + In addition to easier maintenance, this makes the client + more robust against misbehaving servers. + * The minimum python version was increased to 3.6 + * The blockchain headers and fork handling logic has been generalized. + Clients by default now follow chain based on most work, not length. * New wallet creation defaults to native segwit (bech32). - * RBF batching (option): If the wallet has an unconfirmed RBF + * RBF batching (opt-in): If the wallet has an unconfirmed RBF transaction, new payments will be added to that transaction, instead of creating new transactions. - * OSX: support QR code scanner. - * Android APK: Use API 28, and do not use external storage. + * MacOS: support QR code scanner in binaries. + * Android APK: + - build using Google NDK instead of Crystax NDK + - target API 28 + - do not use external storage (previously for block headers) + * hardware wallets: + - Coldcard now supports spending from p2wpkh-p2sh, + fixed p2pkh signing for fw 1.1.0 + - Archos Safe-T mini: fix #4726 signing issue + - KeepKey: full segwit support + - Trezor: refactoring and compat with python-trezor 0.11 + - Digital BitBox: support firmware v5.0.0 + * fix bitcoin URI handling when app already running (#4796) + * Several other minor bugfixes and usability improvements. # Release 3.2.3 - (September 3, 2018) From 9a3f2e8fcca74595289b99a1f3b1b569ad568503 Mon Sep 17 00:00:00 2001 From: Janus Date: Fri, 7 Dec 2018 18:41:40 +0100 Subject: [PATCH 202/301] digitalbitbox: fix stretch_key bytes/str confusion --- electrum/plugins/digitalbitbox/digitalbitbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 314bf0da..61bae1a9 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -129,8 +129,8 @@ class DigitalBitbox_Client(): return False - def stretch_key(self, key): - return to_hexstr(hashlib.pbkdf2_hmac('sha512', key.encode('utf-8'), b'Digital Bitbox', iterations = 20480)) + def stretch_key(self, key: bytes): + return to_hexstr(hashlib.pbkdf2_hmac('sha512', key, b'Digital Bitbox', iterations = 20480)) def backup_password_dialog(self): From 0169ec880ca69efe78bc23b40dfcbef82edc0155 Mon Sep 17 00:00:00 2001 From: Janus Date: Fri, 7 Dec 2018 19:18:33 +0100 Subject: [PATCH 203/301] digitalbitbox: make constant strings --- .../plugins/digitalbitbox/digitalbitbox.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 61bae1a9..df931dec 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -53,6 +53,9 @@ def derive_keys(x): MIN_MAJOR_VERSION = 5 +ENCRYPTION_PRIVKEY_KEY = 'encryptionprivkey' +CHANNEL_ID_KEY = 'comserverchannelid' + class DigitalBitbox_Client(): def __init__(self, plugin, hidDevice): @@ -280,7 +283,7 @@ class DigitalBitbox_Client(): except (FileNotFoundError, jsonDecodeError): return - if 'encryptionprivkey' not in dbb_config or 'comserverchannelid' not in dbb_config: + if ENCRYPTION_PRIVKEY_KEY not in dbb_config or CHANNEL_ID_KEY not in dbb_config: return choices = [ @@ -294,12 +297,12 @@ class DigitalBitbox_Client(): if reply == 0: if self.plugin.is_mobile_paired(): - del self.plugin.digitalbitbox_config['encryptionprivkey'] - del self.plugin.digitalbitbox_config['comserverchannelid'] + del self.plugin.digitalbitbox_config[ENCRYPTION_PRIVKEY_KEY] + del self.plugin.digitalbitbox_config[CHANNEL_ID_KEY] elif reply == 1: # import pairing from dbb app - self.plugin.digitalbitbox_config['encryptionprivkey'] = dbb_config['encryptionprivkey'] - self.plugin.digitalbitbox_config['comserverchannelid'] = dbb_config['comserverchannelid'] + self.plugin.digitalbitbox_config[ENCRYPTION_PRIVKEY_KEY] = dbb_config[ENCRYPTION_PRIVKEY_KEY] + self.plugin.digitalbitbox_config[CHANNEL_ID_KEY] = dbb_config[CHANNEL_ID_KEY] self.plugin.config.set_key('digitalbitbox', self.plugin.digitalbitbox_config) def dbb_generate_wallet(self): @@ -729,15 +732,15 @@ class DigitalBitboxPlugin(HW_PluginBase): def is_mobile_paired(self): - return 'encryptionprivkey' in self.digitalbitbox_config + return ENCRYPTION_PRIVKEY_KEY in self.digitalbitbox_config def comserver_post_notification(self, payload): assert self.is_mobile_paired(), "unexpected mobile pairing error" url = 'https://digitalbitbox.com/smartverification/index.php' - key_s = base64.b64decode(self.digitalbitbox_config['encryptionprivkey']) + key_s = base64.b64decode(self.digitalbitbox_config[ENCRYPTION_PRIVKEY_KEY]) args = 'c=data&s=0&dt=0&uuid=%s&pl=%s' % ( - self.digitalbitbox_config['comserverchannelid'], + self.digitalbitbox_config[CHANNEL_ID_KEY], EncodeAES_base64(key_s, json.dumps(payload).encode('ascii')).decode('ascii'), ) try: From e1f4865844ce1bbdbd91a76b8ba245ae04b6300e Mon Sep 17 00:00:00 2001 From: Janus Date: Fri, 7 Dec 2018 19:19:40 +0100 Subject: [PATCH 204/301] digitalbitbox, trustedcoin: proxied http client use common cross-thread HTTP method, which is put in network.py, since that is where the proxy is. TrustedCoin tested successfully, but DigitalBitbox can't be tested completely due to #4903 before this commit, digitalbitbox would not use any proxying --- electrum/network.py | 36 ++++++++++- .../plugins/digitalbitbox/digitalbitbox.py | 60 ++++++++++--------- electrum/plugins/trustedcoin/trustedcoin.py | 35 +++++------ 3 files changed, 80 insertions(+), 51 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index 9fe78a5e..b55834e8 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -38,9 +38,12 @@ import traceback import dns import dns.resolver from aiorpcx import TaskGroup +from aiohttp import ClientResponse from . import util -from .util import PrintError, print_error, log_exceptions, ignore_exceptions, bfh, SilentTaskGroup +from .util import (PrintError, print_error, log_exceptions, ignore_exceptions, + bfh, SilentTaskGroup, make_aiohttp_session) + from .bitcoin import COIN from . import constants from . import blockchain @@ -903,3 +906,34 @@ class Network(PrintError): await self.interface.group.spawn(self._request_fee_estimates, self.interface) await asyncio.sleep(0.1) + + + async def _send_http_on_proxy(self, method: str, url: str, params: str = None, body: bytes = None, json: dict = None, headers=None, on_finish=None): + async def default_on_finish(resp: ClientResponse): + resp.raise_for_status() + return await resp.text() + if headers is None: + headers = {} + if on_finish is None: + on_finish = default_on_finish + async with make_aiohttp_session(self.proxy) as session: + if method == 'get': + async with session.get(url, params=params, headers=headers) as resp: + return await on_finish(resp) + elif method == 'post': + assert body is not None or json is not None, 'body or json must be supplied if method is post' + if body is not None: + async with session.post(url, data=body, headers=headers) as resp: + return await on_finish(resp) + elif json is not None: + async with session.post(url, json=json, headers=headers) as resp: + return await on_finish(resp) + else: + assert False + + @staticmethod + def send_http_on_proxy(method, url, **kwargs): + network = Network.get_instance() + assert network._loop_thread is not threading.currentThread() + coro = asyncio.run_coroutine_threadsafe(network._send_http_on_proxy(method, url, **kwargs), network.asyncio_loop) + return coro.result(5) diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index df931dec..32445e70 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -3,35 +3,36 @@ # digitalbitbox.com # -try: - from electrum.crypto import sha256d, EncodeAES_base64, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot - from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, - is_address) - from electrum.bip32 import serialize_xpub, deserialize_xpub - from electrum import ecc - from electrum.ecc import msg_magic - from electrum.wallet import Standard_Wallet - from electrum import constants - from electrum.transaction import Transaction - from electrum.i18n import _ - from electrum.keystore import Hardware_KeyStore - from ..hw_wallet import HW_PluginBase - from electrum.util import print_error, to_string, UserCancelled, UserFacingException - from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET +import base64 +import binascii +import hashlib +import hmac +import json +import math +import os +import re +import struct +import sys +import time - import time +from electrum.crypto import sha256d, EncodeAES_base64, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot +from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, + is_address) +from electrum.bip32 import serialize_xpub, deserialize_xpub +from electrum import ecc +from electrum.ecc import msg_magic +from electrum.wallet import Standard_Wallet +from electrum import constants +from electrum.transaction import Transaction +from electrum.i18n import _ +from electrum.keystore import Hardware_KeyStore +from ..hw_wallet import HW_PluginBase +from electrum.util import print_error, to_string, UserCancelled, UserFacingException +from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET +from electrum.network import Network + +try: import hid - import json - import math - import binascii - import struct - import hashlib - import requests - import base64 - import os - import sys - import re - import hmac DIGIBOX = True except ImportError as e: DIGIBOX = False @@ -744,9 +745,10 @@ class DigitalBitboxPlugin(HW_PluginBase): EncodeAES_base64(key_s, json.dumps(payload).encode('ascii')).decode('ascii'), ) try: - requests.post(url, args) + text = Network.send_http_on_proxy('post', url, body=args.encode('ascii'), headers={'content-type': 'application/x-www-form-urlencoded'}) + print_error('digitalbitbox reply from server', text) except Exception as e: - self.handler.show_error(str(e)) + self.handler.show_error(repr(e)) # repr because str(Exception()) == '' def get_xpub(self, device_id, derivation, xtype, wizard): diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 84c4313a..03a7ca83 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -31,6 +31,7 @@ import hashlib from urllib.parse import urljoin from urllib.parse import quote +from aiohttp import ClientResponse from electrum import ecc, constants, keystore, version, bip32 from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, public_key_to_p2pkh @@ -42,7 +43,7 @@ from electrum.mnemonic import Mnemonic from electrum.wallet import Multisig_Wallet, Deterministic_Wallet from electrum.i18n import _ from electrum.plugin import BasePlugin, hook -from electrum.util import NotEnoughFunds, make_aiohttp_session +from electrum.util import NotEnoughFunds from electrum.storage import STO_EV_USER_PW from electrum.network import Network @@ -108,14 +109,7 @@ class TrustedCoinCosignerClient(object): self.debug = False self.user_agent = user_agent - def send_request(self, method, relative_url, data=None): - network = Network.get_instance() - if network: - return asyncio.run_coroutine_threadsafe(self._send_request(method, relative_url, data), network.asyncio_loop).result() - else: - raise ErrorConnectingServer('You are offline.') - - async def handle_response(self, resp): + async def handle_response(self, resp: ClientResponse): if resp.status != 200: try: r = await resp.json() @@ -128,7 +122,10 @@ class TrustedCoinCosignerClient(object): except: return await resp.text() - async def _send_request(self, method, relative_url, data): + def send_request(self, method, relative_url, data=None): + network = Network.get_instance() + if not network: + raise ErrorConnectingServer('You are offline.') url = urljoin(self.base_url, relative_url) if self.debug: print('%s %s %s' % (method, url, data)) @@ -136,16 +133,12 @@ class TrustedCoinCosignerClient(object): if self.user_agent: headers['user-agent'] = self.user_agent try: - proxy = Network.get_instance().proxy - async with make_aiohttp_session(proxy) as session: - if method == 'get': - async with session.get(url, params=data, headers=headers) as resp: - return await self.handle_response(resp) - elif method == 'post': - async with session.post(url, json=data, headers=headers) as resp: - return await self.handle_response(resp) - else: - assert False + if method == 'get': + return Network.send_http_on_proxy(method, url, params=data, headers=headers, on_finish=self.handle_response) + elif method == 'post': + return Network.send_http_on_proxy(method, url, json=data, headers=headers, on_finish=self.handle_response) + else: + assert False except TrustedCoinException: raise except Exception as e: @@ -434,7 +427,7 @@ class TrustedCoinPlugin(BasePlugin): try: billing_info = server.get(wallet.get_user_id()[1]) except ErrorConnectingServer as e: - self.print_error('cannot connect to TrustedCoin server: {}'.format(e)) + self.print_error('cannot connect to TrustedCoin server: {}'.format(repr(e))) return billing_index = billing_info['billing_index'] billing_address = make_billing_address(wallet, billing_index) From c017f788ac2ba42a48c9533b4be12d8bd137cb2c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 7 Dec 2018 20:47:28 +0100 Subject: [PATCH 205/301] wallet: TxMinedInfo (merged TxMinedStatus and VerifiedTxInfo) --- electrum/address_synchronizer.py | 23 +++++++++++++---------- electrum/gui/qt/history_list.py | 4 ++-- electrum/tests/test_wallet.py | 4 ++-- electrum/util.py | 18 ++++++------------ electrum/verifier.py | 9 ++++++--- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index f59d350f..1b08b7c2 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -29,7 +29,7 @@ from typing import TYPE_CHECKING, Dict, Optional from . import bitcoin from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY -from .util import PrintError, profiler, bfh, VerifiedTxInfo, TxMinedStatus +from .util import PrintError, profiler, bfh, TxMinedInfo from .transaction import Transaction, TxOutput from .synchronizer import Synchronizer from .verifier import SPV @@ -70,11 +70,15 @@ class AddressSynchronizer(PrintError): self.transaction_lock = threading.RLock() # address -> list(txid, height) self.history = storage.get('addr_history',{}) - # Verified transactions. txid -> VerifiedTxInfo. Access with self.lock. + # Verified transactions. txid -> TxMinedInfo. Access with self.lock. verified_tx = storage.get('verified_tx3', {}) - self.verified_tx = {} + self.verified_tx = {} # type: Dict[str, TxMinedInfo] for txid, (height, timestamp, txpos, header_hash) in verified_tx.items(): - self.verified_tx[txid] = VerifiedTxInfo(height, timestamp, txpos, header_hash) + self.verified_tx[txid] = TxMinedInfo(height=height, + conf=None, + timestamp=timestamp, + txpos=txpos, + header_hash=header_hash) # Transactions pending verification. txid -> tx_height. Access with self.lock. self.unverified_tx = defaultdict(int) # true when synchronized @@ -562,7 +566,7 @@ class AddressSynchronizer(PrintError): if new_height == tx_height: self.unverified_tx.pop(tx_hash, None) - def add_verified_tx(self, tx_hash: str, info: VerifiedTxInfo): + def add_verified_tx(self, tx_hash: str, info: TxMinedInfo): # Remove from the unverified map and add to the verified map with self.lock: self.unverified_tx.pop(tx_hash, None) @@ -605,19 +609,18 @@ class AddressSynchronizer(PrintError): return cached_local_height return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) - def get_tx_height(self, tx_hash: str) -> TxMinedStatus: - """ Given a transaction, returns (height, conf, timestamp, header_hash) """ + def get_tx_height(self, tx_hash: str) -> TxMinedInfo: with self.lock: if tx_hash in self.verified_tx: info = self.verified_tx[tx_hash] conf = max(self.get_local_height() - info.height + 1, 0) - return TxMinedStatus(info.height, conf, info.timestamp, info.header_hash) + return info._replace(conf=conf) elif tx_hash in self.unverified_tx: height = self.unverified_tx[tx_hash] - return TxMinedStatus(height, 0, None, None) + return TxMinedInfo(height=height, conf=0) else: # local transaction - return TxMinedStatus(TX_HEIGHT_LOCAL, 0, None, None) + return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0) def set_up_to_date(self, up_to_date): with self.lock: diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index d5dbd107..b60f29d5 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -31,7 +31,7 @@ from collections import OrderedDict from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ -from electrum.util import block_explorer_URL, profiler, print_error, TxMinedStatus, OrderedDictWithIndex +from electrum.util import block_explorer_URL, profiler, print_error, TxMinedInfo, OrderedDictWithIndex from .util import * @@ -275,7 +275,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): value = tx_item['value'].value balance = tx_item['balance'].value label = tx_item['label'] - tx_mined_status = TxMinedStatus(height, conf, timestamp, None) + tx_mined_status = TxMinedInfo(height=height, conf=conf, timestamp=timestamp) status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) has_invoice = self.wallet.invoices.paid.get(tx_hash) v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py index 275f20bb..aaa7ad10 100644 --- a/electrum/tests/test_wallet.py +++ b/electrum/tests/test_wallet.py @@ -11,7 +11,7 @@ from io import StringIO from electrum.storage import WalletStorage, FINAL_SEED_VERSION from electrum.wallet import Abstract_Wallet from electrum.exchange_rate import ExchangeBase, FxThread -from electrum.util import TxMinedStatus +from electrum.util import TxMinedInfo from electrum.bitcoin import COIN from . import SequentialTestCase @@ -99,7 +99,7 @@ class FakeWallet: def get_tx_height(self, txid): # because we use a current timestamp, and history is empty, # FxThread.history_rate will use spot prices - return TxMinedStatus(height=10, conf=10, timestamp=time.time(), header_hash='def') + return TxMinedInfo(height=10, conf=10, timestamp=int(time.time()), header_hash='def') default_fiat_value = Abstract_Wallet.default_fiat_value price_at_timestamp = Abstract_Wallet.price_at_timestamp diff --git a/electrum/util.py b/electrum/util.py index 047206e6..f770c67c 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -879,18 +879,12 @@ def ignore_exceptions(func): return wrapper -class TxMinedStatus(NamedTuple): - height: int - conf: int - timestamp: Optional[int] - header_hash: Optional[str] - - -class VerifiedTxInfo(NamedTuple): - height: int - timestamp: int - txpos: int - header_hash: str +class TxMinedInfo(NamedTuple): + height: int # height of block that mined tx + conf: Optional[int] = None # number of confirmations (None means unknown) + timestamp: Optional[int] = None # timestamp of block that mined tx + txpos: Optional[int] = None # position of tx in serialized block + header_hash: Optional[str] = None # hash of block that mined tx def make_aiohttp_session(proxy: dict, headers=None, timeout=None): diff --git a/electrum/verifier.py b/electrum/verifier.py index 27913ce1..247d124d 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -26,7 +26,7 @@ from typing import Sequence, Optional, TYPE_CHECKING import aiorpcx -from .util import bh2u, VerifiedTxInfo, NetworkJobOnDefaultServer +from .util import bh2u, TxMinedInfo, NetworkJobOnDefaultServer from .crypto import sha256d from .bitcoin import hash_decode, hash_encode from .transaction import Transaction @@ -124,8 +124,11 @@ class SPV(NetworkJobOnDefaultServer): except KeyError: pass self.print_error("verified %s" % tx_hash) header_hash = hash_header(header) - vtx_info = VerifiedTxInfo(tx_height, header.get('timestamp'), pos, header_hash) - self.wallet.add_verified_tx(tx_hash, vtx_info) + tx_info = TxMinedInfo(height=tx_height, + timestamp=header.get('timestamp'), + txpos=pos, + header_hash=header_hash) + self.wallet.add_verified_tx(tx_hash, tx_info) if self.is_up_to_date() and self.wallet.is_up_to_date(): self.wallet.save_verified_tx(write=True) From c9482b5ea2169464450240bf9a595997069d06f3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 7 Dec 2018 20:59:19 +0100 Subject: [PATCH 206/301] fix prev --- electrum/address_synchronizer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 1b08b7c2..ab1f64d1 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -448,7 +448,11 @@ class AddressSynchronizer(PrintError): def save_verified_tx(self, write=False): with self.lock: - self.storage.put('verified_tx3', self.verified_tx) + verified_tx_to_save = {} + for txid, tx_info in self.verified_tx.items(): + verified_tx_to_save[txid] = (tx_info.height, tx_info.timestamp, + tx_info.txpos, tx_info.header_hash) + self.storage.put('verified_tx3', verified_tx_to_save) if write: self.storage.write() From 258b5040003c62b57cad8c46843fb31195ddf4b8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 06:31:28 +0100 Subject: [PATCH 207/301] qt main window: unregister network callbacks --- electrum/gui/qt/main_window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 4ee1cebb..6e021051 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -3102,6 +3102,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.wallet.thread.stop() if self.network: self.network.unregister_callback(self.on_network) + self.network.unregister_callback(self.on_quotes) + self.network.unregister_callback(self.on_history) self.config.set_key("is_maximized", self.isMaximized()) if not self.isMaximized(): g = self.geometry() From 0294844c11a4da2b30faba26d4649c2aae24860f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 06:56:18 +0100 Subject: [PATCH 208/301] labels plugin qt: only update corresponding window; disconnect signal --- electrum/gui/qt/main_window.py | 6 +++++- electrum/plugins/labels/qt.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 6e021051..d34f6f95 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -794,7 +794,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if self.wallet.up_to_date or not self.network or not self.network.is_connected(): self.update_tabs() - def update_tabs(self): + def update_tabs(self, wallet=None): + if wallet is None: + wallet = self.wallet + if wallet != self.wallet: + return self.history_list.update() self.request_list.update() self.address_list.update() diff --git a/electrum/plugins/labels/qt.py b/electrum/plugins/labels/qt.py index 2a66d98e..d834b7a3 100644 --- a/electrum/plugins/labels/qt.py +++ b/electrum/plugins/labels/qt.py @@ -75,4 +75,8 @@ class Plugin(LabelsPlugin): @hook def on_close_window(self, window): + try: + self.obj.labels_changed_signal.disconnect(window.update_tabs) + except TypeError: + pass # 'method' object is not connected self.stop_wallet(window.wallet) From 6c203403384724ada56be494fc1c7922e11a3166 Mon Sep 17 00:00:00 2001 From: benma Date: Sat, 8 Dec 2018 17:02:24 +0100 Subject: [PATCH 209/301] bitbox: fix seed command (#4906) Entropy required to be 64 bytes. --- electrum/plugins/digitalbitbox/digitalbitbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 32445e70..653d67d3 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -309,7 +309,7 @@ class DigitalBitbox_Client(): def dbb_generate_wallet(self): key = self.stretch_key(self.password) filename = ("Electrum-" + time.strftime("%Y-%m-%d-%H-%M-%S") + ".pdf") - msg = ('{"seed":{"source": "create", "key": "%s", "filename": "%s", "entropy": "%s"}}' % (key, filename, 'Digital Bitbox Electrum Plugin')).encode('utf8') + msg = ('{"seed":{"source": "create", "key": "%s", "filename": "%s", "entropy": "%s"}}' % (key, filename, to_hexstr(os.urandom(32)))).encode('utf8') reply = self.hid_send_encrypt(msg) if 'error' in reply: raise UserFacingException(reply['error']['message']) From f59a4f85dbbe820e4dfe12437941dd5c6880f2db Mon Sep 17 00:00:00 2001 From: ghost43 Date: Sun, 9 Dec 2018 05:09:28 +0100 Subject: [PATCH 210/301] win/mac build: strip parts of pyqt5 from binaries to reduce size (#4901) When bumping pyinstaller to 3.4, binary sizes had increased drastically. The main reason seems to be that pyinstaller is pulling in "all" of qt. based on Electron-Cash/Electron-Cash@4b0996959420dfca3d53f178d86205616d8c568b --- contrib/build-wine/deterministic.spec | 18 ++++++++++++++++++ contrib/osx/osx.spec | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index dafda1c9..b208ee16 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -78,6 +78,24 @@ for d in a.datas: a.datas.remove(d) break +# Strip out parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815 +qt_bins2remove=('qt5web', 'qt53d', 'qt5game', 'qt5designer', 'qt5quick', + 'qt5location', 'qt5test', 'qt5xml', r'pyqt5\qt\qml\qtquick') +print("Removing Qt binaries:", *qt_bins2remove) +for x in a.binaries.copy(): + for r in qt_bins2remove: + if x[0].lower().startswith(r): + a.binaries.remove(x) + print('----> Removed x =', x) + +qt_data2remove=(r'pyqt5\qt\translations\qtwebengine_locales', ) +print("Removing Qt datas:", *qt_data2remove) +for x in a.datas.copy(): + for r in qt_data2remove: + if x[0].lower().startswith(r): + a.datas.remove(x) + print('----> Removed x =', x) + # hotfix for #3171 (pre-Win10 binaries) a.binaries = [x for x in a.binaries if not x[1].lower().startswith(r'c:\windows')] diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec index ac9c07a0..7b56d6f4 100644 --- a/contrib/osx/osx.spec +++ b/contrib/osx/osx.spec @@ -81,6 +81,15 @@ for d in a.datas: a.datas.remove(d) break +# Strip out parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815 +qt_bins2remove=('qtweb', 'qt3d', 'qtgame', 'qtdesigner', 'qtquick', 'qtlocation', 'qttest', 'qtxml') +print("Removing Qt binaries:", *qt_bins2remove) +for x in a.binaries.copy(): + for r in qt_bins2remove: + if x[0].lower().startswith(r): + a.binaries.remove(x) + print('----> Removed x =', x) + pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, From 762082e13d7e9db7c1ca603a04d9dc1aebe8b0af Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 9 Dec 2018 07:17:37 +0100 Subject: [PATCH 211/301] wine build: dedupe PYTHON_VERSION --- contrib/build-wine/build-electrum-git.sh | 5 ++--- contrib/build-wine/deterministic.spec | 3 +-- contrib/build-wine/docker/README.md | 2 +- contrib/build-wine/prepare-wine.sh | 7 ++++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index 19a42f41..863ca733 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -1,14 +1,13 @@ #!/bin/bash NAME_ROOT=electrum -PYTHON_VERSION=3.6.6 # These settings probably don't need any change export WINEPREFIX=/opt/wine64 export PYTHONDONTWRITEBYTECODE=1 export PYTHONHASHSEED=22 -PYHOME=c:/python$PYTHON_VERSION +PYHOME=c:/python3 PYTHON="wine $PYHOME/python.exe -OO -B" @@ -60,7 +59,7 @@ cd .. rm -rf dist/ # build standalone and portable versions -wine "C:/python$PYTHON_VERSION/scripts/pyinstaller.exe" --noconfirm --ascii --clean --name $NAME_ROOT-$VERSION -w deterministic.spec +wine "$PYHOME/scripts/pyinstaller.exe" --noconfirm --ascii --clean --name $NAME_ROOT-$VERSION -w deterministic.spec # set timestamps in dist, in order to make the installer reproducible pushd dist diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index b208ee16..3429bc7b 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -10,8 +10,7 @@ for i, x in enumerate(sys.argv): else: raise Exception('no name') -PYTHON_VERSION = '3.6.6' -PYHOME = 'c:/python' + PYTHON_VERSION +PYHOME = 'c:/python3' home = 'C:\\electrum\\' diff --git a/contrib/build-wine/docker/README.md b/contrib/build-wine/docker/README.md index aba87018..9caf53f2 100644 --- a/contrib/build-wine/docker/README.md +++ b/contrib/build-wine/docker/README.md @@ -42,7 +42,7 @@ folder. And then build from this directory: ``` $ git checkout $REV - $ sudo docker run \ + $ sudo docker run -it \ --name electrum-wine-builder-cont \ -v $PWD:/opt/wine64/drive_c/electrum \ --rm \ diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 3b5c49be..481bbe44 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -19,7 +19,8 @@ PYTHON_VERSION=3.6.6 export WINEPREFIX=/opt/wine64 #export WINEARCH='win32' -PYHOME=c:/python$PYTHON_VERSION +PYTHON_FOLDER="python3" +PYHOME="c:/$PYTHON_FOLDER" PYTHON="wine $PYHOME/python.exe -OO -B" @@ -105,7 +106,7 @@ for msifile in core dev exe lib pip tools; do wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi" wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc" verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV - wine msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/python$PYTHON_VERSION + wine msiexec /i "${msifile}.msi" /qb TARGETDIR=$PYHOME done # upgrade pip @@ -136,7 +137,7 @@ download_if_not_exist $LIBUSB_FILENAME "$LIBUSB_URL" verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256" 7z x -olibusb $LIBUSB_FILENAME -aoa -cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/python$PYTHON_VERSION/ +cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/$PYTHON_FOLDER/ mkdir -p $WINEPREFIX/drive_c/tmp cp secp256k1/libsecp256k1.dll $WINEPREFIX/drive_c/tmp/ From b3ff173b4507e8325c65d607a4547320ba2c8798 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 9 Dec 2018 20:02:00 +0100 Subject: [PATCH 212/301] interface: change close() implementation was getting on lightning branch in some circumstances RecursionError: maximum recursion depth exceeded while calling a Python object --- electrum/interface.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index 99e2349e..0d05ae79 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -247,7 +247,7 @@ class Interface(PrintError): return sslc def handle_disconnect(func): - async def wrapper_func(self, *args, **kwargs): + async def wrapper_func(self: 'Interface', *args, **kwargs): try: return await func(self, *args, **kwargs) except GracefulDisconnect as e: @@ -380,7 +380,9 @@ class Interface(PrintError): await self.session.send_request('server.ping') async def close(self): - await self.group.cancel_remaining() + if self.session: + await self.session.close() + # monitor_connection will cancel tasks async def run_fetch_blocks(self): header_queue = asyncio.Queue() From 62e352a2a8e61ae930b6a09b1457d5419afea35c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 9 Dec 2018 20:04:42 +0100 Subject: [PATCH 213/301] network: don't let _maintain_sessions die from CancelledError as then the network would get paralysed and no one can fix it --- electrum/network.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index b55834e8..2230fe92 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -197,7 +197,7 @@ class Network(PrintError): if not self.default_server: self.default_server = pick_random_server() - self.main_taskgroup = None + self.main_taskgroup = None # type: TaskGroup # locks self.restart_lock = asyncio.Lock() @@ -817,7 +817,7 @@ class Network(PrintError): async def _start(self): assert not self.main_taskgroup - self.main_taskgroup = SilentTaskGroup() + self.main_taskgroup = main_taskgroup = SilentTaskGroup() assert not self.interface and not self.interfaces assert not self.connecting and not self.server_queue self.print_error('starting network') @@ -831,7 +831,9 @@ class Network(PrintError): async def main(): try: await self._init_headers_file() - async with self.main_taskgroup as group: + # note: if a task finishes with CancelledError, that + # will NOT raise, and the group will keep the other tasks running + async with main_taskgroup as group: await group.spawn(self._maintain_sessions()) [await group.spawn(job) for job in self._jobs] except Exception as e: @@ -852,7 +854,7 @@ class Network(PrintError): await asyncio.wait_for(self.main_taskgroup.cancel_remaining(), timeout=2) except (asyncio.TimeoutError, asyncio.CancelledError) as e: self.print_error(f"exc during main_taskgroup cancellation: {repr(e)}") - self.main_taskgroup = None + self.main_taskgroup = None # type: TaskGroup self.interface = None # type: Interface self.interfaces = {} # type: Dict[str, Interface] self.connecting.clear() @@ -884,13 +886,11 @@ class Network(PrintError): await self.switch_to_interface(self.default_server) async def _maintain_sessions(self): - while True: - # launch already queued up new interfaces + async def launch_already_queued_up_new_interfaces(): while self.server_queue.qsize() > 0: server = self.server_queue.get() await self.main_taskgroup.spawn(self._run_new_interface(server)) - - # maybe queue new interfaces to be launched later + async def maybe_queue_new_interfaces_to_be_launched_later(): now = time.time() for i in range(self.num_server - len(self.interfaces) - len(self.connecting)): self._start_random_interface() @@ -898,13 +898,22 @@ class Network(PrintError): self.print_error('network: retrying connections') self.disconnected_servers = set([]) self.nodes_retry_time = now - - # main interface + async def maintain_main_interface(): await self._ensure_there_is_a_main_interface() if self.is_connected(): if self.config.is_fee_estimates_update_required(): await self.interface.group.spawn(self._request_fee_estimates, self.interface) + while True: + try: + await launch_already_queued_up_new_interfaces() + await maybe_queue_new_interfaces_to_be_launched_later() + await maintain_main_interface() + except asyncio.CancelledError: + # suppress spurious cancellations + group = self.main_taskgroup + if not group or group._closed: + raise await asyncio.sleep(0.1) From 9607854b67d3a8a59b569c904c85466fbb34a8bc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Dec 2018 08:03:42 +0100 Subject: [PATCH 214/301] network: fix switching interface (restart old one) follow-up b3ff173b4507e8325c65d607a4547320ba2c8798 connection_down was killing the already restarted old interface --- electrum/interface.py | 2 +- electrum/network.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index 0d05ae79..596ecfca 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -253,7 +253,7 @@ class Interface(PrintError): except GracefulDisconnect as e: self.print_error("disconnecting gracefully. {}".format(e)) finally: - await self.network.connection_down(self.server) + await self.network.connection_down(self) self.got_disconnected.set_result(1) return wrapper_func diff --git a/electrum/network.py b/electrum/network.py index 2230fe92..e089c8e6 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -638,16 +638,16 @@ class Network(PrintError): self.recent_servers = self.recent_servers[0:20] self._save_recent_servers() - async def connection_down(self, server): + async def connection_down(self, interface: Interface): '''A connection to server either went down, or was never made. We distinguish by whether it is in self.interfaces.''' + if not interface: return + server = interface.server self.disconnected_servers.add(server) if server == self.default_server: self._set_status('disconnected') - interface = self.interfaces.get(server, None) - if interface: - await self._close_interface(interface) - self.trigger_callback('network_updated') + await self._close_interface(interface) + self.trigger_callback('network_updated') @ignore_exceptions # do not kill main_taskgroup @log_exceptions From 5b9b6a931d0a05629511dd743a5ff326e80261ca Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Dec 2018 08:04:54 +0100 Subject: [PATCH 215/301] qt network dialog: fix NodesListWidget if there is fork undo part of 5473320ce459b3076d60f71dab490ed3a07b86a5 --- electrum/gui/qt/network_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index a1f2dace..94ae7773 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -100,6 +100,7 @@ class NodesListWidget(QTreeWidget): def update(self, network: Network): self.clear() + self.addChild = self.addTopLevelItem chains = network.get_blockchains() n_chains = len(chains) for chain_id, interfaces in chains.items(): @@ -117,7 +118,7 @@ class NodesListWidget(QTreeWidget): item = QTreeWidgetItem([i.host + star, '%d'%i.tip]) item.setData(0, Qt.UserRole, 0) item.setData(1, Qt.UserRole, i.server) - x.addTopLevelItem(item) + x.addChild(item) if n_chains > 1: self.addTopLevelItem(x) x.setExpanded(True) From 4eb4b341db58f127d4ba1eb1930b7d067c60fe85 Mon Sep 17 00:00:00 2001 From: Janus Date: Wed, 5 Dec 2018 20:57:21 +0100 Subject: [PATCH 216/301] QAbstractItemModel: initial version, filter not done --- electrum/gui/qt/address_dialog.py | 18 +- electrum/gui/qt/history_list.py | 471 +++++++++++++++--------------- electrum/gui/qt/main_window.py | 20 +- electrum/gui/qt/util.py | 39 ++- 4 files changed, 277 insertions(+), 271 deletions(-) diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index 7b152b28..a54774a7 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -30,9 +30,16 @@ from PyQt5.QtGui import * from PyQt5.QtWidgets import * from .util import * -from .history_list import HistoryList +from .history_list import HistoryList, HistoryModel from .qrtextedit import ShowQRTextEdit +class AddressHistoryModel(HistoryModel): + def __init__(self, parent, address): + super().__init__(parent) + self.address = address + + def get_domain(self): + return [self.address] class AddressDialog(WindowModalDialog): @@ -80,16 +87,13 @@ class AddressDialog(WindowModalDialog): vbox.addWidget(redeem_e) vbox.addWidget(QLabel(_("History"))) - self.hw = HistoryList(self.parent) - self.hw.get_domain = self.get_domain + addr_hist_model = AddressHistoryModel(self.parent, self.address) + self.hw = HistoryList(self.parent, addr_hist_model) vbox.addWidget(self.hw) vbox.addLayout(Buttons(CloseButton(self))) self.format_amount = self.parent.format_amount - self.hw.update() - - def get_domain(self): - return [self.address] + addr_hist_model.refresh('address dialog constructor') def show_qr(self): text = self.address diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index b60f29d5..1a2fb30a 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -27,11 +27,10 @@ import webbrowser import datetime from datetime import date from typing import TYPE_CHECKING -from collections import OrderedDict from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ -from electrum.util import block_explorer_URL, profiler, print_error, TxMinedInfo, OrderedDictWithIndex +from electrum.util import block_explorer_URL, profiler, print_error, TxMinedInfo from .util import * @@ -60,43 +59,227 @@ TX_ICONS = [ class HistorySortModel(QSortFilterProxyModel): def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): - item1 = self.sourceModel().itemFromIndex(source_left) - item2 = self.sourceModel().itemFromIndex(source_right) - data1 = item1.data(HistoryList.SORT_ROLE) - data2 = item2.data(HistoryList.SORT_ROLE) - if data1 is not None and data2 is not None: - return data1 < data2 - return item1.text() < item2.text() + item1 = self.sourceModel().data(source_left, Qt.UserRole) + 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: + return False + return item1.value() < item2.value() + +# requires PyQt5 5.11 +indexIsValid = QAbstractItemModel.CheckIndexOptions(QAbstractItemModel.CheckIndexOption.IndexIsValid.value) + +class HistoryModel(QAbstractItemModel): + def __init__(self, parent): + super().__init__(parent) + self.parent = parent + self.transactions = [] + + def columnCount(self, parent: QModelIndex): + return 8 + + def rowCount(self, parent: QModelIndex): + l = len(self.transactions) + return l + + def index(self, row: int, column: int, parent : QModelIndex): + return self.createIndex(row,column) + + def data(self, index: QModelIndex, role: Qt.ItemDataRole): + assert self.checkIndex(index, indexIsValid) + assert index.isValid() + tx_item = self.transactions[index.row()] + tx_hash = tx_item['txid'] + height = tx_item['height'] + conf = tx_item['confirmations'] + timestamp = tx_item['timestamp'] + tx_mined_info = TxMinedInfo(height=height, conf=conf, timestamp=timestamp) + status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) + if role == Qt.UserRole: + # for sorting + d = { + 0: (status, conf), + 1: status_str, + 2: tx_item['label'], + 3: tx_item['value'].value, + 4: tx_item['balance'].value, + 5: tx_item['fiat_value'].value if 'fiat_value' in tx_item else None, + 6: tx_item['acquisition_price'].value if 'acquisition_price' in tx_item else None, + 7: tx_item['capital_gain'].value if 'capital_gain' in tx_item else None, + } + return QVariant(d[index.column()]) + if role not in (Qt.DisplayRole, Qt.EditRole): + if index.column() == 0 and role == Qt.DecorationRole: + return QVariant(self.parent.history_list.icon_cache.get(":icons/" + TX_ICONS[status])) + elif index.column() == 0 and role == Qt.ToolTipRole: + return QVariant(str(conf) + _(" confirmation" + ("s" if conf != 1 else ""))) + elif index.column() > 2 and role == Qt.TextAlignmentRole: + return QVariant(Qt.AlignRight | Qt.AlignVCenter) + elif index.column() != 1 and role == Qt.FontRole: + monospace_font = QFont(MONOSPACE_FONT) + return QVariant(monospace_font) + elif index.column() == 2 and role == Qt.DecorationRole and self.parent.wallet.invoices.paid.get(tx_hash): + return QVariant(self.parent.history_list.icon_cache.get(":icons/seal")) + elif index.column() in (2, 3) and role == Qt.ForegroundRole and tx_item['value'].value < 0: + red_brush = QBrush(QColor("#BC1E1E")) + return QVariant(red_brush) + elif index.column() == 5 and role == Qt.ForegroundRole and not tx_item.get('fiat_default') and tx_item.get('fiat_value') is not None: + blue_brush = QBrush(QColor("#1E1EFF")) + return QVariant(blue_brush) + return None + if index.column() == 1: + return QVariant(status_str) + elif index.column() == 2: + return QVariant(tx_item['label']) + elif index.column() == 3: + value = tx_item['value'].value + v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) + return QVariant(v_str) + elif index.column() == 4: + balance = tx_item['balance'].value + balance_str = self.parent.format_amount(balance, whitespaces=True) + return QVariant(balance_str) + elif index.column() == 5 and 'fiat_value' in tx_item: + value_str = self.parent.fx.format_fiat(tx_item['fiat_value'].value) + return QVariant(value_str) + elif index.column() == 6 and tx_item['value'].value < 0 and 'acquisition_price' in tx_item: + # fixme: should use is_mine + acq = tx_item['acquisition_price'].value + return QVariant(self.parent.fx.format_fiat(acq)) + elif index.column() == 7 and 'capital_gain' in tx_item: + cg = tx_item['capital_gain'].value + return QVariant(self.parent.fx.format_fiat(cg)) + return None + + def parent(self, index: QModelIndex): + return QModelIndex() + + def hasChildren(self, index: QModelIndex): + return not index.isValid() + + def update_label(self, row): + tx_item = self.transactions[row] + tx_item['label'] = self.parent.wallet.get_label(tx_item['txid']) + topLeft = bottomRight = self.createIndex(row, 2) + self.dataChanged.emit(topLeft, bottomRight, [Qt.DisplayRole]) + + def get_domain(self): + '''Overridden in address_dialog.py''' + return self.parent.wallet.get_addresses() + + def refresh(self, reason: str): + fx = self.parent.fx + if fx: fx.history_used_spot = False + r = self.parent.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) + if r['transactions'] == self.transactions: + return + old_length = len(self.transactions) + if old_length != 0: + self.beginRemoveRows(QModelIndex(), 0, old_length) + self.transactions.clear() + self.endRemoveRows() + self.beginInsertRows(QModelIndex(), 0, len(r['transactions'])-1) + self.transactions = r['transactions'] + self.endInsertRows() + f = self.parent.history_list.current_filter + if f: + self.parent.history_list.filter(f) + # update summary + self.summary = r['summary'] + if not self.parent.history_list.years and self.transactions: + start_date = date.today() + end_date = date.today() + if len(self.transactions) > 0: + start_date = self.transactions[0].get('date') or start_date + end_date = self.transactions[-1].get('date') or end_date + self.parent.history_list.years = [str(i) for i in range(start_date.year, end_date.year + 1)] + self.parent.history_list.period_combo.insertItems(1, self.parent.history_list.years) + + history = self.parent.fx.show_history() + cap_gains = self.parent.fx.get_history_capital_gains_config() + hide = self.parent.history_list.hideColumn + show = self.parent.history_list.showColumn + if history and cap_gains: + show(5) + show(6) + show(7) + elif history: + show(5) + hide(6) + hide(7) + else: + hide(5) + hide(6) + hide(7) + + def update_fiat(self, row, idx): + tx_item = self.transactions[row] + key = tx_item['txid'] + fee = tx_item.get('fee') + value = tx_item['value'].value + fiat_fields = self.parent.wallet.get_tx_item_fiat(key, value, self.parent.fx, fee.value if fee else None) + tx_item.update(fiat_fields) + self.dataChanged.emit(idx, idx, [Qt.DisplayRole, Qt.ForegroundRole]) + + def update_item(self, *args): + self.refresh('update_item') + + def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole): + assert orientation == Qt.Horizontal + if role != Qt.DisplayRole: + return None + fx = self.parent.fx + fiat_title = 'n/a fiat value' + fiat_acq_title = 'n/a fiat acquisition price' + fiat_cg_title = 'n/a fiat capital gains' + if fx and fx.show_history(): + fiat_title = '%s '%fx.ccy + _('Value') + if fx.get_history_capital_gains_config(): + fiat_acq_title = '%s '%fx.ccy + _('Acquisition price') + fiat_cg_title = '%s '%fx.ccy + _('Capital Gains') + return { + 0: '', + 1: _('Date'), + 2: _('Description'), + 3: _('Amount'), + 4: _('Balance'), + 5: fiat_title, + 6: fiat_acq_title, + 7: fiat_cg_title, + }[section] + + def flags(self, idx): + extra_flags = Qt.NoItemFlags # type: Qt.ItemFlag + if idx.column() in self.parent.history_list.editable_columns: + extra_flags |= Qt.ItemIsEditable + return super().flags(idx) | extra_flags class HistoryList(MyTreeView, AcceptFileDragDrop): filter_columns = [1, 2, 3] # Date, Description, Amount - TX_HASH_ROLE = Qt.UserRole - SORT_ROLE = Qt.UserRole + 1 + + def tx_item_from_proxy_row(self, proxy_row): + hm_idx = self.model().mapToSource(self.model().index(proxy_row, 0)) + return self.hm.transactions[hm_idx.row()] def should_hide(self, proxy_row): if self.start_timestamp and self.end_timestamp: - item = self.item_from_coordinate(proxy_row, 0) - txid = item.data(self.TX_HASH_ROLE) - date = self.transactions[txid]['date'] + tx_item = self.tx_item_from_proxy_row(proxy_row) + date = tx_item['date'] if date: in_interval = self.start_timestamp <= date <= self.end_timestamp if not in_interval: return True return False - def __init__(self, parent=None): + def __init__(self, parent, model): super().__init__(parent, self.create_menu, 2) - self.std_model = QStandardItemModel(self) + self.hm = model self.proxy = HistorySortModel(self) - self.proxy.setSourceModel(self.std_model) + self.proxy.setSourceModel(model) self.setModel(self.proxy) - self.txid_to_items = {} - self.transactions = OrderedDictWithIndex() self.summary = {} - self.blue_brush = QBrush(QColor("#1E1EFF")) - self.red_brush = QBrush(QColor("#BC1E1E")) - self.monospace_font = QFont(MONOSPACE_FONT) self.config = parent.config AcceptFileDragDrop.__init__(self, ".txn") self.setSortingEnabled(True) @@ -105,44 +288,19 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.years = [] self.create_toolbar_buttons() self.wallet = self.parent.wallet # type: Abstract_Wallet - self.refresh_headers() self.sortByColumn(0, Qt.AscendingOrder) + self.editable_columns |= {5} + + self.header().setStretchLastSection(False) + for col in range(8): + sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents + self.header().setSectionResizeMode(col, sm) def format_date(self, d): return str(datetime.date(d.year, d.month, d.day)) if d else _('None') - def refresh_headers(self): - headers = ['', _('Date'), _('Description'), _('Amount'), _('Balance')] - fx = self.parent.fx - if fx and fx.show_history(): - headers.extend(['%s '%fx.ccy + _('Value')]) - self.editable_columns |= {5} - if fx.get_history_capital_gains_config(): - headers.extend(['%s '%fx.ccy + _('Acquisition price')]) - headers.extend(['%s '%fx.ccy + _('Capital Gains')]) - else: - self.editable_columns -= {5} - col_count = self.std_model.columnCount() - diff = col_count-len(headers) - if col_count > len(headers): - if diff == 2: - self.std_model.removeColumns(6, diff) - else: - assert diff in [1, 3] - self.std_model.removeColumns(5, diff) - for items in self.txid_to_items.values(): - while len(items) > col_count: - items.pop() - elif col_count < len(headers): - self.std_model.clear() - self.txid_to_items.clear() - self.transactions.clear() - self.summary.clear() - self.update_headers(headers, self.std_model) - - def get_domain(self): - '''Replaced in address_dialog.py''' - return self.wallet.get_addresses() + def update_headers(self, headers): + raise NotImplementedError def on_combo(self, x): s = self.period_combo.itemText(x) @@ -266,166 +424,33 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): except NothingToPlotException as e: self.parent.show_message(str(e)) - def insert_tx(self, tx_item): - fx = self.parent.fx - tx_hash = tx_item['txid'] - height = tx_item['height'] - conf = tx_item['confirmations'] - timestamp = tx_item['timestamp'] - value = tx_item['value'].value - balance = tx_item['balance'].value - label = tx_item['label'] - tx_mined_status = TxMinedInfo(height=height, conf=conf, timestamp=timestamp) - status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) - has_invoice = self.wallet.invoices.paid.get(tx_hash) - v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) - balance_str = self.parent.format_amount(balance, whitespaces=True) - entry = ['', status_str, label, v_str, balance_str] - item = [QStandardItem(e) for e in entry] - item[3].setData(value, self.SORT_ROLE) - item[4].setData(balance, self.SORT_ROLE) - if has_invoice: - item[2].setIcon(self.icon_cache.get(":icons/seal")) - for i in range(len(entry)): - self.set_item_properties(item[i], i, tx_hash) - if value and value < 0: - item[2].setForeground(self.red_brush) - item[3].setForeground(self.red_brush) - self.txid_to_items[tx_hash] = item - self.update_item(tx_hash, self.wallet.get_tx_height(tx_hash)) - source_row_idx = self.std_model.rowCount() - self.std_model.insertRow(source_row_idx, item) - new_idx = self.std_model.index(source_row_idx, 0) - history = fx.show_history() - if history: - self.update_fiat(tx_hash, tx_item) - self.hide_row(self.proxy.mapFromSource(new_idx).row()) - - def set_item_properties(self, item, i, tx_hash): - if i>2: - item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) - if i!=1: - item.setFont(self.monospace_font) - item.setEditable(i in self.editable_columns) - item.setData(tx_hash, self.TX_HASH_ROLE) - - def ensure_fields_available(self, items, idx, txid): - while len(items) < idx + 1: - row = self.transactions.get_pos_of_key(txid) - qidx = self.std_model.index(row, len(items)) - assert qidx.isValid(), (self.std_model.columnCount(), idx) - item = self.std_model.itemFromIndex(qidx) - self.set_item_properties(item, len(items), txid) - items.append(item) - - @profiler - def update(self): - fx = self.parent.fx - if fx: fx.history_used_spot = False - r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) - seen = set() - history = fx.show_history() - tx_list = list(self.transactions.values()) - if r['transactions'] == tx_list: - return - if r['transactions'][:-1] == tx_list: - print_error('history_list: one new transaction') - row = r['transactions'][-1] - txid = row['txid'] - if txid not in self.transactions: - self.transactions[txid] = row - self.insert_tx(row) - return - else: - print_error('history_list: tx added but txid is already in list (weird), txid: ', txid) - for idx, row in enumerate(r['transactions']): - txid = row['txid'] - seen.add(txid) - if txid not in self.transactions: - self.transactions[txid] = row - self.insert_tx(row) - continue - old = self.transactions[txid] - if old == row: - continue - self.update_item(txid, self.wallet.get_tx_height(txid)) - if history: - self.update_fiat(txid, row) - balance_str = self.parent.format_amount(row['balance'].value, whitespaces=True) - self.txid_to_items[txid][4].setText(balance_str) - self.txid_to_items[txid][4].setData(row['balance'].value, self.SORT_ROLE) - old.clear() - old.update(**row) - removed = 0 - l = list(enumerate(self.transactions.keys())) - for idx, txid in l: - if txid not in seen: - del self.transactions[txid] - del self.txid_to_items[txid] - items = self.std_model.takeRow(idx - removed) - removed_txid = items[0].data(self.TX_HASH_ROLE) - assert removed_txid == txid, (idx, removed) - removed += 1 - self.apply_filter() - # update summary - self.summary = r['summary'] - if not self.years and self.transactions: - start_date = next(iter(self.transactions.values())).get('date') or date.today() - end_date = next(iter(reversed(self.transactions.values()))).get('date') or date.today() - self.years = [str(i) for i in range(start_date.year, end_date.year + 1)] - self.period_combo.insertItems(1, self.years) - - def update_fiat(self, txid, row): - cap_gains = self.parent.fx.get_history_capital_gains_config() - items = self.txid_to_items[txid] - self.ensure_fields_available(items, 7 if cap_gains else 5, txid) - if not row['fiat_default'] and row['fiat_value']: - items[5].setForeground(self.blue_brush) - value_str = self.parent.fx.format_fiat(row['fiat_value'].value) - items[5].setText(value_str) - items[5].setData(row['fiat_value'].value, self.SORT_ROLE) - # fixme: should use is_mine - if row['value'].value < 0 and cap_gains: - acq = row['acquisition_price'].value - items[6].setText(self.parent.fx.format_fiat(acq)) - items[6].setData(acq, self.SORT_ROLE) - cg = row['capital_gain'].value - items[7].setText(self.parent.fx.format_fiat(cg)) - items[7].setData(cg, self.SORT_ROLE) - - def update_on_new_fee_histogram(self): - pass - # TODO update unconfirmed tx'es - def on_edited(self, index, user_role, text): + print("on_edited") row, column = index.row(), index.column() - item = self.item_from_coordinate(row, column) - key = item.data(self.TX_HASH_ROLE) + tx_item = self.hm.transactions[row] + key = tx_item['txid'] # fixme if column == 2: - self.wallet.set_label(key, text) - self.update_labels() - self.parent.update_completions() + if self.wallet.set_label(key, text): #changed + self.hm.update_label(row) + self.parent.update_completions() elif column == 5: - tx_item = self.transactions[key] self.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, tx_item['value'].value) value = tx_item['value'].value if value is not None: - fee = tx_item['fee'] - fiat_fields = self.wallet.get_tx_item_fiat(key, value, self.parent.fx, fee.value if fee else None) - tx_item.update(fiat_fields) - self.update_fiat(key, tx_item) + self.hm.update_fiat(row, self.model().mapToSource(index)) else: assert False def mouseDoubleClickEvent(self, event: QMouseEvent): idx = self.indexAt(event.pos()) - item = self.item_from_coordinate(idx.row(), idx.column()) - if not item or item.isEditable(): + if not idx.isValid(): + return + tx_item = self.tx_item_from_proxy_row(idx.row()) + if self.hm.flags(self.model().mapToSource(idx)) & Qt.ItemIsEditable: super().mouseDoubleClickEvent(event) - elif item: - tx_hash = item.data(self.TX_HASH_ROLE) - self.show_transaction(tx_hash) + else: + self.show_transaction(tx_item['txid']) def show_transaction(self, tx_hash): tx = self.wallet.transactions.get(tx_hash) @@ -434,45 +459,22 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): label = self.wallet.get_label(tx_hash) or None # prefer 'None' if not defined (force tx dialog to hide Description field if missing) self.parent.show_transaction(tx, label) - def update_labels(self): - root = self.std_model.invisibleRootItem() - child_count = root.rowCount() - for i in range(child_count): - item = root.child(i, 2) - txid = item.data(self.TX_HASH_ROLE) - label = self.wallet.get_label(txid) - item.setText(label) - - def update_item(self, tx_hash, tx_mined_status): - conf = tx_mined_status.conf - status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) - icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) - if tx_hash not in self.txid_to_items: - return - items = self.txid_to_items[tx_hash] - items[0].setIcon(icon) - items[0].setToolTip(str(conf) + _(" confirmation" + ("s" if conf != 1 else ""))) - items[0].setData((status, conf), self.SORT_ROLE) - items[1].setText(status_str) - def create_menu(self, position: QPoint): org_idx: QModelIndex = self.indexAt(position) idx = self.proxy.mapToSource(org_idx) - item: QStandardItem = self.std_model.itemFromIndex(idx) - if not item: + if not idx.isValid(): # can happen e.g. before list is populated for the first time return - tx_hash = idx.data(self.TX_HASH_ROLE) + tx_item = self.hm.transactions[idx.row()] column = idx.column() - assert tx_hash, "create_menu: no tx hash" - tx = self.wallet.transactions.get(tx_hash) - assert tx, "create_menu: no tx" if column == 0: column_title = _('Transaction ID') - column_data = tx_hash + column_data = tx_item['txid'] else: - column_title = self.std_model.horizontalHeaderItem(column).text() - column_data = item.text() + column_title = self.hm.headerData(column, Qt.Horizontal, Qt.DisplayRole) + column_data = str(self.hm.data(idx, Qt.DisplayRole)) + tx_hash = tx_item['txid'] + tx = self.wallet.transactions[tx_hash] tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) height = self.wallet.get_tx_height(tx_hash).height is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) @@ -483,7 +485,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) for c in self.editable_columns: - label = self.std_model.horizontalHeaderItem(c).text() + label = self.hm.headerData(c, Qt.Horizontal, Qt.DisplayRole) # TODO use siblingAtColumn when min Qt version is >=5.11 persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c)) menu.addAction(_("Edit {}").format(label), lambda p=persistent: self.edit(QModelIndex(p))) @@ -589,3 +591,8 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): else: from electrum.util import json_encode f.write(json_encode(txns)) + + def text_txid_from_coordinate(self, row, col): + idx = self.model().mapToSource(self.model().index(row, col)) + tx_item = self.hm.transactions[idx.row()] + return str(self.hm.data(idx, Qt.DisplayRole)), tx_item['txid'] diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index d34f6f95..2037ddc6 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -73,6 +73,7 @@ from .transaction_dialog import show_transaction from .fee_slider import FeeSlider from .util import * from .installwizard import WIF_HELP_TEXT +from .history_list import HistoryList, HistoryModel class StatusBarButton(QPushButton): @@ -230,8 +231,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): Exception_Hook(self) def on_fx_history(self): - self.history_list.refresh_headers() - self.history_list.update() + self.history_model.refresh('fx_history') self.address_list.update() def on_quotes(self, b): @@ -246,7 +246,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): edit.textEdited.emit(edit.text()) # History tab needs updating if it used spot if self.fx.history_used_spot: - self.history_list.update() + self.history_model.refresh('fx_quotes') def toggle_tab(self, tab): show = not self.config.get('show_{}_tab'.format(tab.tab_name), False) @@ -345,7 +345,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): elif event == 'verified': wallet, tx_hash, tx_mined_status = args if wallet == self.wallet: - self.history_list.update_item(tx_hash, tx_mined_status) + self.history_model.update_item(tx_hash, tx_mined_status) elif event == 'fee': if self.config.is_dynfee(): self.fee_slider.update() @@ -354,7 +354,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if self.config.is_dynfee(): self.fee_slider.update() self.do_update_fee() - self.history_list.update_on_new_fee_histogram() + self.history_model.refresh('fee_histogram') else: self.print_error("unexpected network_qt signal:", event, args) @@ -799,7 +799,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): wallet = self.wallet if wallet != self.wallet: return - self.history_list.update() + self.history_model.refresh('update_tabs') self.request_list.update() self.address_list.update() self.utxo_list.update() @@ -808,8 +808,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.update_completions() def create_history_tab(self): - from .history_list import HistoryList - self.history_list = l = HistoryList(self) + self.history_model = HistoryModel(self) + self.history_list = l = HistoryList(self, self.history_model) l.searchable_list = l toolbar = l.create_toolbar(self.config) toolbar_shown = self.config.get('show_toolbar_history', False) @@ -3022,7 +3022,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if not self.fx: return self.fx.set_history_config(checked) update_exchanges() - self.history_list.refresh_headers() + self.history_model.refresh('on_history') if self.fx.is_enabled() and checked: self.fx.trigger_update() update_history_capgains_cb() @@ -3030,7 +3030,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def on_history_capgains(checked): if not self.fx: return self.fx.set_history_capital_gains_config(checked) - self.history_list.refresh_headers() + self.history_model.refresh('on_history_capgains') def on_fiat_address(checked): if not self.fx: return diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 3329168b..7fba7ce6 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -449,9 +449,8 @@ class MyTreeView(QTreeView): assert set_current.isValid() self.selectionModel().select(QModelIndex(set_current), QItemSelectionModel.SelectCurrent) - def update_headers(self, headers, model=None): - if model is None: - model = self.model() + def update_headers(self, headers): + model = self.model() model.setHorizontalHeaderLabels(headers) self.header().setStretchLastSection(False) for col in range(len(headers)): @@ -473,11 +472,9 @@ class MyTreeView(QTreeView): def createEditor(self, parent, option, idx): self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(), parent, option, idx) - item = self.item_from_coordinate(idx.row(), idx.column()) - user_role = item.data(Qt.UserRole) - assert user_role is not None - prior_text = item.text() + prior_text, user_role = self.text_txid_from_coordinate(idx.row(), idx.column()) def editing_finished(): + print("editing finished") # Long-time QT bug - pressing Enter to finish editing signals # editingFinished twice. If the item changed the sequence is # Enter key: editingFinished, on_change, editingFinished @@ -487,12 +484,16 @@ class MyTreeView(QTreeView): if self.editor is None: return if self.editor.text() == prior_text: + print("unchanged ignore any 2nd call") self.editor = None # Unchanged - ignore any 2nd call return - if item.text() == prior_text: - return # Buggy first call on Enter key, item not yet updated if not idx.isValid(): + print("idx not valid") return + new_text, _ = self.text_txid_from_coordinate(idx.row(), idx.column()) + if new_text == prior_text: + print("buggy first call", new_text, prior_text) + return # Buggy first call on Enter key, item not yet updated self.on_edited(idx, user_role, self.editor.text()) self.editor = None self.editor.editingFinished.connect(editing_finished) @@ -511,10 +512,6 @@ class MyTreeView(QTreeView): self.parent.history_list.update_labels() self.parent.update_completions() - def apply_filter(self): - if self.current_filter: - self.filter(self.current_filter) - def should_hide(self, row): """ row_num is for self.model(). So if there is a proxy, it is the row number @@ -522,13 +519,11 @@ class MyTreeView(QTreeView): """ return False - def item_from_coordinate(self, row_num, column): - if isinstance(self.model(), QSortFilterProxyModel): - idx = self.model().mapToSource(self.model().index(row_num, column)) - return self.model().sourceModel().itemFromIndex(idx) - else: - idx = self.model().index(row_num, column) - return self.model().itemFromIndex(idx) + def text_txid_from_coordinate(self, row_num, column): + assert not isinstance(self.model(), QSortFilterProxyModel) + idx = self.model().index(row_num, column) + item = self.model().itemFromIndex(idx) + return item.text(), item.data(Qt.UserRole) def hide_row(self, row_num): """ @@ -541,8 +536,8 @@ class MyTreeView(QTreeView): self.setRowHidden(row_num, QModelIndex(), False) return for column in self.filter_columns: - item = self.item_from_coordinate(row_num, column) - txt = item.text().lower() + txt, _ = self.text_txid_from_coordinate(row_num, column) + txt = txt.lower() if self.current_filter in txt: # the filter matched, but the date filter might apply self.setRowHidden(row_num, QModelIndex(), bool(should_hide)) From 3960070a509fad40ddf3f76b1995efe2a0d8eca0 Mon Sep 17 00:00:00 2001 From: Janus Date: Thu, 6 Dec 2018 20:22:38 +0100 Subject: [PATCH 217/301] QAbstractItemModel: fix sorting, QAbstractItemDelegate usage, QVariant usage --- electrum/gui/qt/contact_list.py | 8 ++--- electrum/gui/qt/history_list.py | 27 +++++++------- electrum/gui/qt/util.py | 62 +++++++++++++-------------------- electrum/gui/qt/utxo_list.py | 2 +- electrum/wallet.py | 2 +- 5 files changed, 44 insertions(+), 57 deletions(-) diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index e1915b1b..5c167f3e 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -49,12 +49,8 @@ class ContactList(MyTreeView): def on_edited(self, idx, user_role, text): _type, prior_name = self.parent.contacts.pop(user_role) - - # TODO when min Qt >= 5.11, use siblingAtColumn - col_1_sibling = idx.sibling(idx.row(), 1) - col_1_item = self.model().itemFromIndex(col_1_sibling) - - self.parent.set_contact(text, col_1_item.text()) + self.parent.set_contact(text, user_role) + self.update() def import_contacts(self): import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.update) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 1a2fb30a..b50aaa7d 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -67,9 +67,6 @@ class HistorySortModel(QSortFilterProxyModel): return False return item1.value() < item2.value() -# requires PyQt5 5.11 -indexIsValid = QAbstractItemModel.CheckIndexOptions(QAbstractItemModel.CheckIndexOption.IndexIsValid.value) - class HistoryModel(QAbstractItemModel): def __init__(self, parent): super().__init__(parent) @@ -80,14 +77,15 @@ class HistoryModel(QAbstractItemModel): return 8 def rowCount(self, parent: QModelIndex): - l = len(self.transactions) - return l + return len(self.transactions) def index(self, row: int, column: int, parent : QModelIndex): return self.createIndex(row,column) def data(self, index: QModelIndex, role: Qt.ItemDataRole): - assert self.checkIndex(index, indexIsValid) + # requires PyQt5 5.11 + # indexIsValid = QAbstractItemModel.CheckIndexOptions(QAbstractItemModel.CheckIndexOption.IndexIsValid.value) + # assert self.checkIndex(index, indexIsValid) assert index.isValid() tx_item = self.transactions[index.row()] tx_hash = tx_item['txid'] @@ -169,6 +167,10 @@ class HistoryModel(QAbstractItemModel): return self.parent.wallet.get_addresses() def refresh(self, reason: str): + selected = self.parent.history_list.selectionModel().currentIndex() + selected_row = None + if selected: + selected_row = selected.row() fx = self.parent.fx if fx: fx.history_used_spot = False r = self.parent.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) @@ -182,6 +184,8 @@ class HistoryModel(QAbstractItemModel): self.beginInsertRows(QModelIndex(), 0, len(r['transactions'])-1) self.transactions = r['transactions'] self.endInsertRows() + if selected_row: + self.parent.history_list.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent) f = self.parent.history_list.current_filter if f: self.parent.history_list.filter(f) @@ -279,7 +283,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.proxy.setSourceModel(model) self.setModel(self.proxy) - self.summary = {} self.config = parent.config AcceptFileDragDrop.__init__(self, ".txn") self.setSortingEnabled(True) @@ -374,7 +377,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): return datetime.datetime(date.year, date.month, date.day) def show_summary(self): - h = self.summary + h = self.model().sourceModel().summary if not h: self.parent.show_message(_("Nothing to summarize.")) return @@ -425,7 +428,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.parent.show_message(str(e)) def on_edited(self, index, user_role, text): - print("on_edited") + index = self.model().mapToSource(index) row, column = index.row(), index.column() tx_item = self.hm.transactions[row] key = tx_item['txid'] @@ -438,7 +441,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, tx_item['value'].value) value = tx_item['value'].value if value is not None: - self.hm.update_fiat(row, self.model().mapToSource(index)) + self.hm.update_fiat(row, index) else: assert False @@ -472,7 +475,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): column_data = tx_item['txid'] else: column_title = self.hm.headerData(column, Qt.Horizontal, Qt.DisplayRole) - column_data = str(self.hm.data(idx, Qt.DisplayRole)) + column_data = self.hm.data(idx, Qt.DisplayRole).value() tx_hash = tx_item['txid'] tx = self.wallet.transactions[tx_hash] tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) @@ -595,4 +598,4 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def text_txid_from_coordinate(self, row, col): idx = self.model().mapToSource(self.model().index(row, col)) tx_item = self.hm.transactions[idx.row()] - return str(self.hm.data(idx, Qt.DisplayRole)), tx_item['txid'] + return self.hm.data(idx, Qt.DisplayRole).value(), tx_item['txid'] diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 7fba7ce6..c88b9e33 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -398,8 +398,23 @@ def filename_field(parent, config, defaultname, select_msg): return vbox, filename_e, b1 class ElectrumItemDelegate(QStyledItemDelegate): - def createEditor(self, parent, option, index): - return self.parent().createEditor(parent, option, index) + def __init__(self, tv): + super().__init__(tv) + self.tv = tv + self.opened = None + def on_closeEditor(editor: QLineEdit, hint): + self.opened = None + def on_commitData(editor: QLineEdit): + new_text = editor.text() + idx = QModelIndex(self.opened) + _prior_text, user_role = self.tv.text_txid_from_coordinate(idx.row(), idx.column()) + self.tv.on_edited(idx, user_role, new_text) + self.closeEditor.connect(on_closeEditor) + self.commitData.connect(on_commitData) + + def createEditor(self, parent, option, idx): + self.opened = QPersistentModelIndex(idx) + return super().createEditor(parent, option, idx) class MyTreeView(QTreeView): @@ -415,8 +430,6 @@ class MyTreeView(QTreeView): self.icon_cache = IconCache() # Control which columns are editable - self.editor = None - self.pending_update = False if editable_columns is None: editable_columns = {stretch_column} else: @@ -458,7 +471,9 @@ class MyTreeView(QTreeView): self.header().setSectionResizeMode(col, sm) def keyPressEvent(self, event): - if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None: + if self.itemDelegate().opened: + return + if event.key() in [ Qt.Key_F2, Qt.Key_Return ]: self.on_activated(self.selectionModel().currentIndex()) return super().keyPressEvent(event) @@ -469,36 +484,6 @@ class MyTreeView(QTreeView): pt.setX(50) self.customContextMenuRequested.emit(pt) - def createEditor(self, parent, option, idx): - self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(), - parent, option, idx) - prior_text, user_role = self.text_txid_from_coordinate(idx.row(), idx.column()) - def editing_finished(): - print("editing finished") - # Long-time QT bug - pressing Enter to finish editing signals - # editingFinished twice. If the item changed the sequence is - # Enter key: editingFinished, on_change, editingFinished - # Mouse: on_change, editingFinished - # This mess is the cleanest way to ensure we make the - # on_edited callback with the updated item - if self.editor is None: - return - if self.editor.text() == prior_text: - print("unchanged ignore any 2nd call") - self.editor = None # Unchanged - ignore any 2nd call - return - if not idx.isValid(): - print("idx not valid") - return - new_text, _ = self.text_txid_from_coordinate(idx.row(), idx.column()) - if new_text == prior_text: - print("buggy first call", new_text, prior_text) - return # Buggy first call on Enter key, item not yet updated - self.on_edited(idx, user_role, self.editor.text()) - self.editor = None - self.editor.editingFinished.connect(editing_finished) - return self.editor - def edit(self, idx, trigger=QAbstractItemView.AllEditTriggers, event=None): """ this is to prevent: @@ -509,7 +494,7 @@ class MyTreeView(QTreeView): def on_edited(self, idx: QModelIndex, user_role, text): self.parent.wallet.set_label(user_role, text) - self.parent.history_list.update_labels() + self.parent.history_model.refresh('on_edited in MyTreeView') self.parent.update_completions() def should_hide(self, row): @@ -523,7 +508,10 @@ class MyTreeView(QTreeView): assert not isinstance(self.model(), QSortFilterProxyModel) idx = self.model().index(row_num, column) item = self.model().itemFromIndex(idx) - return item.text(), item.data(Qt.UserRole) + user_role = item.data(Qt.UserRole) + # check that we didn't forget to set UserRole on an editable field + assert user_role is not None, (row_num, column) + return item.text(), user_role def hide_row(self, row_num): """ diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 0b9d8550..046f30fc 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -33,7 +33,7 @@ class UTXOList(MyTreeView): filter_columns = [0, 1] # Address, Label def __init__(self, parent=None): - super().__init__(parent, self.create_menu, 1) + super().__init__(parent, self.create_menu, 1, editable_columns=[]) self.setModel(QStandardItemModel(self)) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSortingEnabled(True) diff --git a/electrum/wallet.py b/electrum/wallet.py index a32888cf..746f25ca 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -239,7 +239,7 @@ class Abstract_Wallet(AddressSynchronizer): self.labels[name] = text changed = True else: - if old_text: + if old_text is not None: self.labels.pop(name) changed = True if changed: From d2ddb255eff8590e3e1d504612666327bb2fa3f7 Mon Sep 17 00:00:00 2001 From: Janus Date: Fri, 7 Dec 2018 15:12:04 +0100 Subject: [PATCH 218/301] QAbstractItemModel: Release Notes and Address List fiat bug fix --- RELEASE-NOTES | 11 +++++++++++ electrum/gui/qt/main_window.py | 1 + 2 files changed, 12 insertions(+) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index bf831f91..932711f9 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -23,6 +23,17 @@ - Trezor: refactoring and compat with python-trezor 0.11 - Digital BitBox: support firmware v5.0.0 * fix bitcoin URI handling when app already running (#4796) + * Qt listing fixes: + - Selection by arrow keys disabled while editing e.g. label + - Enter key on unedited value does not pop up context menu + - Contacts: + - Prevent editing of OpenAlias names + - Receive: + - Icon for status of payment requests + - Disable editing of 'Description' in list, interaction + between labels and memo of invoice confusing + - Addresses: + - Fiat prices would show "No Data" incorrectly upon start * Several other minor bugfixes and usability improvements. diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2037ddc6..afb6c67d 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -247,6 +247,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): # History tab needs updating if it used spot if self.fx.history_used_spot: self.history_model.refresh('fx_quotes') + self.address_list.update() def toggle_tab(self, tab): show = not self.config.get('show_{}_tab'.format(tab.tab_name), False) From 1c0c21159b33cf4ed3276e61a21e3e7ed3246cf5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 7 Dec 2018 20:23:35 +0100 Subject: [PATCH 219/301] qt history list: performance optimisations --- electrum/gui/qt/history_list.py | 58 ++++++++++++++++++++------------- electrum/gui/qt/main_window.py | 2 +- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index b50aaa7d..6ea8054c 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -26,7 +26,7 @@ import webbrowser import datetime from datetime import date -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Tuple, Dict from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ @@ -72,6 +72,7 @@ class HistoryModel(QAbstractItemModel): super().__init__(parent) self.parent = parent self.transactions = [] + self.tx_status_cache = {} # type: Dict[str, Tuple[int, str]] def columnCount(self, parent: QModelIndex): return 8 @@ -79,25 +80,27 @@ class HistoryModel(QAbstractItemModel): def rowCount(self, parent: QModelIndex): return len(self.transactions) - def index(self, row: int, column: int, parent : QModelIndex): - return self.createIndex(row,column) + def index(self, row: int, column: int, parent: QModelIndex): + return self.createIndex(row, column) def data(self, index: QModelIndex, role: Qt.ItemDataRole): # requires PyQt5 5.11 # indexIsValid = QAbstractItemModel.CheckIndexOptions(QAbstractItemModel.CheckIndexOption.IndexIsValid.value) # assert self.checkIndex(index, indexIsValid) assert index.isValid() + col = index.column() tx_item = self.transactions[index.row()] tx_hash = tx_item['txid'] - height = tx_item['height'] conf = tx_item['confirmations'] - timestamp = tx_item['timestamp'] - tx_mined_info = TxMinedInfo(height=height, conf=conf, timestamp=timestamp) - status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) + try: + status, status_str = self.tx_status_cache[tx_hash] + except KeyError: + tx_mined_status = TxMinedInfo(height=tx_item['height'], conf=conf, timestamp=tx_item['timestamp']) + status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_status) if role == Qt.UserRole: # for sorting d = { - 0: (status, conf), + 0: (status, conf), # FIXME tx_pos needed as tie-breaker 1: status_str, 2: tx_item['label'], 3: tx_item['value'].value, @@ -106,46 +109,46 @@ class HistoryModel(QAbstractItemModel): 6: tx_item['acquisition_price'].value if 'acquisition_price' in tx_item else None, 7: tx_item['capital_gain'].value if 'capital_gain' in tx_item else None, } - return QVariant(d[index.column()]) + return QVariant(d[col]) if role not in (Qt.DisplayRole, Qt.EditRole): - if index.column() == 0 and role == Qt.DecorationRole: + if col == 0 and role == Qt.DecorationRole: return QVariant(self.parent.history_list.icon_cache.get(":icons/" + TX_ICONS[status])) - elif index.column() == 0 and role == Qt.ToolTipRole: + elif col == 0 and role == Qt.ToolTipRole: return QVariant(str(conf) + _(" confirmation" + ("s" if conf != 1 else ""))) - elif index.column() > 2 and role == Qt.TextAlignmentRole: + elif col > 2 and role == Qt.TextAlignmentRole: return QVariant(Qt.AlignRight | Qt.AlignVCenter) - elif index.column() != 1 and role == Qt.FontRole: + elif col != 1 and role == Qt.FontRole: monospace_font = QFont(MONOSPACE_FONT) return QVariant(monospace_font) - elif index.column() == 2 and role == Qt.DecorationRole and self.parent.wallet.invoices.paid.get(tx_hash): + elif col == 2 and role == Qt.DecorationRole and self.parent.wallet.invoices.paid.get(tx_hash): return QVariant(self.parent.history_list.icon_cache.get(":icons/seal")) - elif index.column() in (2, 3) and role == Qt.ForegroundRole and tx_item['value'].value < 0: + elif col in (2, 3) and role == Qt.ForegroundRole and tx_item['value'].value < 0: red_brush = QBrush(QColor("#BC1E1E")) return QVariant(red_brush) - elif index.column() == 5 and role == Qt.ForegroundRole and not tx_item.get('fiat_default') and tx_item.get('fiat_value') is not None: + elif col == 5 and role == Qt.ForegroundRole and not tx_item.get('fiat_default') and tx_item.get('fiat_value') is not None: blue_brush = QBrush(QColor("#1E1EFF")) return QVariant(blue_brush) return None - if index.column() == 1: + if col == 1: return QVariant(status_str) - elif index.column() == 2: + elif col == 2: return QVariant(tx_item['label']) - elif index.column() == 3: + elif col == 3: value = tx_item['value'].value v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) return QVariant(v_str) - elif index.column() == 4: + elif col == 4: balance = tx_item['balance'].value balance_str = self.parent.format_amount(balance, whitespaces=True) return QVariant(balance_str) - elif index.column() == 5 and 'fiat_value' in tx_item: + elif col == 5 and 'fiat_value' in tx_item: value_str = self.parent.fx.format_fiat(tx_item['fiat_value'].value) return QVariant(value_str) - elif index.column() == 6 and tx_item['value'].value < 0 and 'acquisition_price' in tx_item: + elif col == 6 and tx_item['value'].value < 0 and 'acquisition_price' in tx_item: # fixme: should use is_mine acq = tx_item['acquisition_price'].value return QVariant(self.parent.fx.format_fiat(acq)) - elif index.column() == 7 and 'capital_gain' in tx_item: + elif col == 7 and 'capital_gain' in tx_item: cg = tx_item['capital_gain'].value return QVariant(self.parent.fx.format_fiat(cg)) return None @@ -199,6 +202,15 @@ class HistoryModel(QAbstractItemModel): end_date = self.transactions[-1].get('date') or end_date self.parent.history_list.years = [str(i) for i in range(start_date.year, end_date.year + 1)] self.parent.history_list.period_combo.insertItems(1, self.parent.history_list.years) + # update tx_status_cache + self.tx_status_cache.clear() + for tx_item in self.transactions: + txid = tx_item['txid'] + height = tx_item['height'] + conf = tx_item['confirmations'] + timestamp = tx_item['timestamp'] + tx_mined_status = TxMinedInfo(height=height, conf=conf, timestamp=timestamp) + self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_status) history = self.parent.fx.show_history() cap_gains = self.parent.fx.get_history_capital_gains_config() diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index afb6c67d..087e14e8 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -345,7 +345,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.console.showMessage(args[0]) elif event == 'verified': wallet, tx_hash, tx_mined_status = args - if wallet == self.wallet: + if wallet == self.wallet and wallet.up_to_date: self.history_model.update_item(tx_hash, tx_mined_status) elif event == 'fee': if self.config.is_dynfee(): From e023d8abddb36e843b240562bb1cdf8bfb43ef6c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 7 Dec 2018 22:11:18 +0100 Subject: [PATCH 220/301] qt history list: sorting of first column now considers txpos same block txns were in unnatural order, maybe sort is not stable? --- electrum/gui/qt/history_list.py | 10 +++++----- electrum/wallet.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 6ea8054c..f22b7f6a 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -92,6 +92,7 @@ class HistoryModel(QAbstractItemModel): tx_item = self.transactions[index.row()] tx_hash = tx_item['txid'] conf = tx_item['confirmations'] + txpos = tx_item['txpos_in_block'] or 0 try: status, status_str = self.tx_status_cache[tx_hash] except KeyError: @@ -100,7 +101,7 @@ class HistoryModel(QAbstractItemModel): if role == Qt.UserRole: # for sorting d = { - 0: (status, conf), # FIXME tx_pos needed as tie-breaker + 0: (status, conf, -txpos), 1: status_str, 2: tx_item['label'], 3: tx_item['value'].value, @@ -206,10 +207,9 @@ class HistoryModel(QAbstractItemModel): self.tx_status_cache.clear() for tx_item in self.transactions: txid = tx_item['txid'] - height = tx_item['height'] - conf = tx_item['confirmations'] - timestamp = tx_item['timestamp'] - tx_mined_status = TxMinedInfo(height=height, conf=conf, timestamp=timestamp) + tx_mined_status = TxMinedInfo(height=tx_item['height'], + conf=tx_item['confirmations'], + timestamp=tx_item['timestamp']) self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_status) history = self.parent.fx.show_history() diff --git a/electrum/wallet.py b/electrum/wallet.py index 746f25ca..64cbb80b 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -420,6 +420,7 @@ class Abstract_Wallet(AddressSynchronizer): 'balance': Satoshis(balance), 'date': timestamp_to_datetime(timestamp), 'label': self.get_label(tx_hash), + 'txpos_in_block': tx_mined_status.txpos, } tx_fee = None if show_fees: From 48e119b59e05be918b88267222b820214ddba846 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:07:46 +0100 Subject: [PATCH 221/301] qt history: minor clean-up and sanity checking --- electrum/daemon.py | 2 +- electrum/gui/qt/__init__.py | 1 + electrum/gui/qt/history_list.py | 11 ++++++++--- electrum/gui/qt/main_window.py | 1 + electrum/wallet.py | 12 ++++++------ 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index ad1c2a85..0db918c4 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -319,12 +319,12 @@ class Daemon(DaemonThread): DaemonThread.stop(self) def init_gui(self, config, plugins): + threading.current_thread().setName('GUI') gui_name = config.get('gui', 'qt') if gui_name in ['lite', 'classic']: gui_name = 'qt' gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum']) self.gui = gui.ElectrumGui(config, self, plugins) - threading.current_thread().setName('GUI') try: self.gui.main() except BaseException as e: diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 83f21cc2..885684ea 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -97,6 +97,7 @@ class ElectrumGui(PrintError): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum.desktop') + self.gui_thread = threading.current_thread() self.config = config self.daemon = daemon self.plugins = plugins diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index f22b7f6a..aefd4e2c 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -27,10 +27,12 @@ import webbrowser import datetime from datetime import date from typing import TYPE_CHECKING, Tuple, Dict +import threading from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ -from electrum.util import block_explorer_URL, profiler, print_error, TxMinedInfo +from electrum.util import (block_explorer_URL, profiler, print_error, TxMinedInfo, + PrintError) from .util import * @@ -67,7 +69,8 @@ class HistorySortModel(QSortFilterProxyModel): return False return item1.value() < item2.value() -class HistoryModel(QAbstractItemModel): +class HistoryModel(QAbstractItemModel, PrintError): + def __init__(self, parent): super().__init__(parent) self.parent = parent @@ -171,6 +174,8 @@ class HistoryModel(QAbstractItemModel): return self.parent.wallet.get_addresses() def refresh(self, reason: str): + self.print_error(f"refreshing... reason: {reason}") + assert self.parent.gui_thread == threading.current_thread(), 'must be called from GUI thread' selected = self.parent.history_list.selectionModel().currentIndex() selected_row = None if selected: @@ -288,7 +293,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): return True return False - def __init__(self, parent, model): + def __init__(self, parent, model: HistoryModel): super().__init__(parent, self.create_menu, 2) self.hm = model self.proxy = HistorySortModel(self) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 087e14e8..f5a7b420 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -114,6 +114,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.gui_object = gui_object self.config = config = gui_object.config # type: SimpleConfig + self.gui_thread = gui_object.gui_thread self.setup_exception_hook() diff --git a/electrum/wallet.py b/electrum/wallet.py index 64cbb80b..0d58f44d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -45,7 +45,7 @@ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, - Fiat, bfh, bh2u) + Fiat, bfh, bh2u, TxMinedInfo) from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script, is_minikey, relayfee, dust_threshold) from .version import * @@ -523,11 +523,11 @@ class Abstract_Wallet(AddressSynchronizer): return ', '.join(labels) return '' - def get_tx_status(self, tx_hash, tx_mined_status): + def get_tx_status(self, tx_hash, tx_mined_info: TxMinedInfo): extra = [] - height = tx_mined_status.height - conf = tx_mined_status.conf - timestamp = tx_mined_status.timestamp + height = tx_mined_info.height + conf = tx_mined_info.conf + timestamp = tx_mined_info.timestamp if conf == 0: tx = self.transactions.get(tx_hash) if not tx: @@ -554,7 +554,7 @@ class Abstract_Wallet(AddressSynchronizer): elif height == TX_HEIGHT_UNCONFIRMED: status = 0 else: - status = 2 + status = 2 # not SPV verified else: status = 3 + min(conf, 6) time_str = format_time(timestamp) if timestamp else _("unknown") From 5e61ad09c195980212e01ead716a3e8609c3d938 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:09:38 +0100 Subject: [PATCH 222/301] qt addresses list: fix filtering --- electrum/gui/qt/util.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index c88b9e33..0b1043c9 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -407,7 +407,10 @@ class ElectrumItemDelegate(QStyledItemDelegate): def on_commitData(editor: QLineEdit): new_text = editor.text() idx = QModelIndex(self.opened) - _prior_text, user_role = self.tv.text_txid_from_coordinate(idx.row(), idx.column()) + row, col = idx.row(), idx.column() + _prior_text, user_role = self.tv.text_txid_from_coordinate(row, col) + # check that we didn't forget to set UserRole on an editable field + assert user_role is not None, (row, col) self.tv.on_edited(idx, user_role, new_text) self.closeEditor.connect(on_closeEditor) self.commitData.connect(on_commitData) @@ -509,8 +512,6 @@ class MyTreeView(QTreeView): idx = self.model().index(row_num, column) item = self.model().itemFromIndex(idx) user_role = item.data(Qt.UserRole) - # check that we didn't forget to set UserRole on an editable field - assert user_role is not None, (row_num, column) return item.text(), user_role def hide_row(self, row_num): From 3c3fac7ca41396d25f6fe73e317f1af5158ea3d3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:12:07 +0100 Subject: [PATCH 223/301] qt history list: fix shortcut in refresh() --- electrum/util.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/electrum/util.py b/electrum/util.py index f770c67c..fd2bb472 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -146,6 +146,12 @@ class Satoshis(object): def __str__(self): return format_satoshis(self.value) + " BTC" + def __eq__(self, other): + return self.value == other.value + + def __ne__(self, other): + return not (self == other) + # note: this is not a NamedTuple as then its json encoding cannot be customized class Fiat(object): @@ -166,6 +172,12 @@ class Fiat(object): else: return "{:.2f}".format(self.value) + ' ' + self.ccy + def __eq__(self, other): + return self.ccy == other.ccy and self.value == other.value + + def __ne__(self, other): + return not (self == other) + class MyEncoder(json.JSONEncoder): def default(self, obj): From 8bb930dd041a18073f7274665adb9582e93399b9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:17:05 +0100 Subject: [PATCH 224/301] fix OrderedDictWithIndex setitem() would modify the dict of the class. oops. --- electrum/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/util.py b/electrum/util.py index fd2bb472..d2ca64a0 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1007,7 +1007,9 @@ class OrderedDictWithIndex(OrderedDict): Note: very inefficient to modify contents, except to add new items. """ - _key_to_pos = {} + def __init__(self): + super().__init__() + self._key_to_pos = {} def _recalc_key_to_pos(self): self._key_to_pos = {key: pos for (pos, key) in enumerate(self.keys())} From 65e8eef87fdc0ae47fadca8b1def4607e73c0e73 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:22:53 +0100 Subject: [PATCH 225/301] qt history list: use OrderedDictWithIndex for txns --- electrum/gui/qt/history_list.py | 54 +++++++++++++++++++-------------- electrum/util.py | 26 ++++++++++------ 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index aefd4e2c..a05e9f31 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -32,7 +32,7 @@ import threading from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ from electrum.util import (block_explorer_URL, profiler, print_error, TxMinedInfo, - PrintError) + OrderedDictWithIndex, PrintError) from .util import * @@ -71,14 +71,16 @@ class HistorySortModel(QSortFilterProxyModel): class HistoryModel(QAbstractItemModel, PrintError): + NUM_COLUMNS = 8 + def __init__(self, parent): super().__init__(parent) self.parent = parent - self.transactions = [] + self.transactions = OrderedDictWithIndex() self.tx_status_cache = {} # type: Dict[str, Tuple[int, str]] def columnCount(self, parent: QModelIndex): - return 8 + return self.NUM_COLUMNS def rowCount(self, parent: QModelIndex): return len(self.transactions) @@ -92,15 +94,15 @@ class HistoryModel(QAbstractItemModel, PrintError): # assert self.checkIndex(index, indexIsValid) assert index.isValid() col = index.column() - tx_item = self.transactions[index.row()] + tx_item = self.transactions.value_from_pos(index.row()) tx_hash = tx_item['txid'] conf = tx_item['confirmations'] txpos = tx_item['txpos_in_block'] or 0 try: status, status_str = self.tx_status_cache[tx_hash] except KeyError: - tx_mined_status = TxMinedInfo(height=tx_item['height'], conf=conf, timestamp=tx_item['timestamp']) - status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_status) + tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) + status, status_str = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) if role == Qt.UserRole: # for sorting d = { @@ -164,7 +166,7 @@ class HistoryModel(QAbstractItemModel, PrintError): return not index.isValid() def update_label(self, row): - tx_item = self.transactions[row] + tx_item = self.transactions.value_from_pos(row) tx_item['label'] = self.parent.wallet.get_label(tx_item['txid']) topLeft = bottomRight = self.createIndex(row, 2) self.dataChanged.emit(topLeft, bottomRight, [Qt.DisplayRole]) @@ -183,7 +185,7 @@ class HistoryModel(QAbstractItemModel, PrintError): fx = self.parent.fx if fx: fx.history_used_spot = False r = self.parent.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) - if r['transactions'] == self.transactions: + if r['transactions'] == list(self.transactions.values()): return old_length = len(self.transactions) if old_length != 0: @@ -191,7 +193,9 @@ class HistoryModel(QAbstractItemModel, PrintError): self.transactions.clear() self.endRemoveRows() self.beginInsertRows(QModelIndex(), 0, len(r['transactions'])-1) - self.transactions = r['transactions'] + for tx_item in r['transactions']: + txid = tx_item['txid'] + self.transactions[txid] = tx_item self.endInsertRows() if selected_row: self.parent.history_list.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent) @@ -204,18 +208,15 @@ class HistoryModel(QAbstractItemModel, PrintError): start_date = date.today() end_date = date.today() if len(self.transactions) > 0: - start_date = self.transactions[0].get('date') or start_date - end_date = self.transactions[-1].get('date') or end_date + start_date = self.transactions.value_from_pos(0).get('date') or start_date + end_date = self.transactions.value_from_pos(len(self.transactions) - 1).get('date') or end_date self.parent.history_list.years = [str(i) for i in range(start_date.year, end_date.year + 1)] self.parent.history_list.period_combo.insertItems(1, self.parent.history_list.years) # update tx_status_cache self.tx_status_cache.clear() - for tx_item in self.transactions: - txid = tx_item['txid'] - tx_mined_status = TxMinedInfo(height=tx_item['height'], - conf=tx_item['confirmations'], - timestamp=tx_item['timestamp']) - self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_status) + for txid, tx_item in self.transactions.items(): + tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) + self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_info) history = self.parent.fx.show_history() cap_gains = self.parent.fx.get_history_capital_gains_config() @@ -235,7 +236,7 @@ class HistoryModel(QAbstractItemModel, PrintError): hide(7) def update_fiat(self, row, idx): - tx_item = self.transactions[row] + tx_item = self.transactions.value_from_pos(row) key = tx_item['txid'] fee = tx_item.get('fee') value = tx_item['value'].value @@ -276,12 +277,19 @@ class HistoryModel(QAbstractItemModel, PrintError): extra_flags |= Qt.ItemIsEditable return super().flags(idx) | extra_flags + @staticmethod + def tx_mined_info_from_tx_item(tx_item): + tx_mined_info = TxMinedInfo(height=tx_item['height'], + conf=tx_item['confirmations'], + timestamp=tx_item['timestamp']) + return tx_mined_info + class HistoryList(MyTreeView, AcceptFileDragDrop): filter_columns = [1, 2, 3] # Date, Description, Amount def tx_item_from_proxy_row(self, proxy_row): hm_idx = self.model().mapToSource(self.model().index(proxy_row, 0)) - return self.hm.transactions[hm_idx.row()] + return self.hm.transactions.value_from_pos(hm_idx.row()) def should_hide(self, proxy_row): if self.start_timestamp and self.end_timestamp: @@ -439,7 +447,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): _("Perhaps some dependencies are missing...") + " (matplotlib?)") return try: - plt = plot_history(list(self.transactions.values())) + plt = plot_history(list(self.hm.transactions.values())) plt.show() except NothingToPlotException as e: self.parent.show_message(str(e)) @@ -447,7 +455,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def on_edited(self, index, user_role, text): index = self.model().mapToSource(index) row, column = index.row(), index.column() - tx_item = self.hm.transactions[row] + tx_item = self.hm.transactions.value_from_pos(row) key = tx_item['txid'] # fixme if column == 2: @@ -485,7 +493,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): if not idx.isValid(): # can happen e.g. before list is populated for the first time return - tx_item = self.hm.transactions[idx.row()] + tx_item = self.hm.transactions.value_from_pos(idx.row()) column = idx.column() if column == 0: column_title = _('Transaction ID') @@ -614,5 +622,5 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def text_txid_from_coordinate(self, row, col): idx = self.model().mapToSource(self.model().index(row, col)) - tx_item = self.hm.transactions[idx.row()] + tx_item = self.hm.transactions.value_from_pos(idx.row()) return self.hm.data(idx, Qt.DisplayRole).value(), tx_item['txid'] diff --git a/electrum/util.py b/electrum/util.py index d2ca64a0..f158d9d0 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1010,46 +1010,54 @@ class OrderedDictWithIndex(OrderedDict): def __init__(self): super().__init__() self._key_to_pos = {} + self._pos_to_key = {} - def _recalc_key_to_pos(self): + def _recalc_index(self): self._key_to_pos = {key: pos for (pos, key) in enumerate(self.keys())} + self._pos_to_key = {pos: key for (pos, key) in enumerate(self.keys())} - def get_pos_of_key(self, key): + def pos_from_key(self, key): return self._key_to_pos[key] + def value_from_pos(self, pos): + key = self._pos_to_key[pos] + return self[key] + def popitem(self, *args, **kwargs): ret = super().popitem(*args, **kwargs) - self._recalc_key_to_pos() + self._recalc_index() return ret def move_to_end(self, *args, **kwargs): ret = super().move_to_end(*args, **kwargs) - self._recalc_key_to_pos() + self._recalc_index() return ret def clear(self): ret = super().clear() - self._recalc_key_to_pos() + self._recalc_index() return ret def pop(self, *args, **kwargs): ret = super().pop(*args, **kwargs) - self._recalc_key_to_pos() + self._recalc_index() return ret def update(self, *args, **kwargs): ret = super().update(*args, **kwargs) - self._recalc_key_to_pos() + self._recalc_index() return ret def __delitem__(self, *args, **kwargs): ret = super().__delitem__(*args, **kwargs) - self._recalc_key_to_pos() + self._recalc_index() return ret def __setitem__(self, key, *args, **kwargs): is_new_key = key not in self ret = super().__setitem__(key, *args, **kwargs) if is_new_key: - self._key_to_pos[key] = len(self) - 1 + pos = len(self) - 1 + self._key_to_pos[key] = pos + self._pos_to_key[pos] = key return ret From b1e15751d6c0a70759c273ee1c28d2b41fa88bad Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:24:49 +0100 Subject: [PATCH 226/301] qt history list: "status"-based sort should also tie-break on height --- electrum/gui/qt/history_list.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index a05e9f31..4522bc09 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -98,6 +98,7 @@ class HistoryModel(QAbstractItemModel, PrintError): tx_hash = tx_item['txid'] conf = tx_item['confirmations'] txpos = tx_item['txpos_in_block'] or 0 + height = tx_item['height'] try: status, status_str = self.tx_status_cache[tx_hash] except KeyError: @@ -106,7 +107,9 @@ class HistoryModel(QAbstractItemModel, PrintError): if role == Qt.UserRole: # for sorting d = { - 0: (status, conf, -txpos), + # height breaks ties for unverified txns + # txpos breaks ties for verified same block txns + 0: (status, conf, -height, -txpos), 1: status_str, 2: tx_item['label'], 3: tx_item['value'].value, From 696db310a573de9f1512ba0af44e2a5e571eee47 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:27:44 +0100 Subject: [PATCH 227/301] qt history list: optimise update_item (tx mined status) --- electrum/gui/qt/history_list.py | 11 +++++++++-- electrum/gui/qt/main_window.py | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 4522bc09..87a89e06 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -247,8 +247,15 @@ class HistoryModel(QAbstractItemModel, PrintError): tx_item.update(fiat_fields) self.dataChanged.emit(idx, idx, [Qt.DisplayRole, Qt.ForegroundRole]) - def update_item(self, *args): - self.refresh('update_item') + def update_tx_mined_status(self, tx_hash: str, tx_mined_info: TxMinedInfo): + self.tx_status_cache[tx_hash] = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) + try: + row = self.transactions.pos_from_key(tx_hash) + except KeyError: + return + topLeft = self.createIndex(row, 0) + bottomRight = self.createIndex(row, self.NUM_COLUMNS-1) + self.dataChanged.emit(topLeft, bottomRight) def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole): assert orientation == Qt.Horizontal diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index f5a7b420..71cde538 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -346,8 +346,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.console.showMessage(args[0]) elif event == 'verified': wallet, tx_hash, tx_mined_status = args - if wallet == self.wallet and wallet.up_to_date: - self.history_model.update_item(tx_hash, tx_mined_status) + if wallet == self.wallet: + self.history_model.update_tx_mined_status(tx_hash, tx_mined_status) elif event == 'fee': if self.config.is_dynfee(): self.fee_slider.update() From a99b92f613b40a1e65c6ea461039a8089911121f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 04:29:09 +0100 Subject: [PATCH 228/301] qt history list: optimise fee histogram induced refresh --- electrum/gui/qt/history_list.py | 9 +++++++++ electrum/gui/qt/main_window.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 87a89e06..6be2f8cc 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -257,6 +257,15 @@ class HistoryModel(QAbstractItemModel, PrintError): bottomRight = self.createIndex(row, self.NUM_COLUMNS-1) self.dataChanged.emit(topLeft, bottomRight) + def on_fee_histogram(self): + for tx_hash, tx_item in self.transactions.items(): + tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) + if tx_mined_info.conf > 0: + # note: we could actually break here if we wanted to rely on the order of txns in self.transactions + continue + self.tx_status_cache[tx_hash] = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) + self.update_tx_mined_status(tx_hash, tx_mined_info) + def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole): assert orientation == Qt.Horizontal if role != Qt.DisplayRole: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 71cde538..1a6d5be2 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -356,7 +356,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if self.config.is_dynfee(): self.fee_slider.update() self.do_update_fee() - self.history_model.refresh('fee_histogram') + self.history_model.on_fee_histogram() else: self.print_error("unexpected network_qt signal:", event, args) From 0d755b86ab0141f753296294174a5c9cc794deab Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 05:21:19 +0100 Subject: [PATCH 229/301] qt address dialog: HistoryModel needs reference to correct HistoryList refresh() was hiding/showing the headers of the main HistoryList --- electrum/gui/qt/address_dialog.py | 1 + electrum/gui/qt/history_list.py | 31 ++++++++++++++++--------------- electrum/gui/qt/main_window.py | 1 + 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index a54774a7..d039ec42 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -89,6 +89,7 @@ class AddressDialog(WindowModalDialog): vbox.addWidget(QLabel(_("History"))) addr_hist_model = AddressHistoryModel(self.parent, self.address) self.hw = HistoryList(self.parent, addr_hist_model) + addr_hist_model.view = self.hw vbox.addWidget(self.hw) vbox.addLayout(Buttons(CloseButton(self))) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 6be2f8cc..d303caa9 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -76,6 +76,7 @@ class HistoryModel(QAbstractItemModel, PrintError): def __init__(self, parent): super().__init__(parent) self.parent = parent + self.view = None # type: HistoryList # set by caller! FIXME... self.transactions = OrderedDictWithIndex() self.tx_status_cache = {} # type: Dict[str, Tuple[int, str]] @@ -121,7 +122,7 @@ class HistoryModel(QAbstractItemModel, PrintError): return QVariant(d[col]) if role not in (Qt.DisplayRole, Qt.EditRole): if col == 0 and role == Qt.DecorationRole: - return QVariant(self.parent.history_list.icon_cache.get(":icons/" + TX_ICONS[status])) + return QVariant(self.view.icon_cache.get(":icons/" + TX_ICONS[status])) elif col == 0 and role == Qt.ToolTipRole: return QVariant(str(conf) + _(" confirmation" + ("s" if conf != 1 else ""))) elif col > 2 and role == Qt.TextAlignmentRole: @@ -130,7 +131,7 @@ class HistoryModel(QAbstractItemModel, PrintError): monospace_font = QFont(MONOSPACE_FONT) return QVariant(monospace_font) elif col == 2 and role == Qt.DecorationRole and self.parent.wallet.invoices.paid.get(tx_hash): - return QVariant(self.parent.history_list.icon_cache.get(":icons/seal")) + return QVariant(self.view.icon_cache.get(":icons/seal")) elif col in (2, 3) and role == Qt.ForegroundRole and tx_item['value'].value < 0: red_brush = QBrush(QColor("#BC1E1E")) return QVariant(red_brush) @@ -181,7 +182,8 @@ class HistoryModel(QAbstractItemModel, PrintError): def refresh(self, reason: str): self.print_error(f"refreshing... reason: {reason}") assert self.parent.gui_thread == threading.current_thread(), 'must be called from GUI thread' - selected = self.parent.history_list.selectionModel().currentIndex() + assert self.view, 'view not set' + selected = self.view.selectionModel().currentIndex() selected_row = None if selected: selected_row = selected.row() @@ -201,20 +203,20 @@ class HistoryModel(QAbstractItemModel, PrintError): self.transactions[txid] = tx_item self.endInsertRows() if selected_row: - self.parent.history_list.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent) - f = self.parent.history_list.current_filter + self.view.selectionModel().select(self.createIndex(selected_row, 0), QItemSelectionModel.Rows | QItemSelectionModel.SelectCurrent) + f = self.view.current_filter if f: - self.parent.history_list.filter(f) + self.view.filter(f) # update summary self.summary = r['summary'] - if not self.parent.history_list.years and self.transactions: + if not self.view.years and self.transactions: start_date = date.today() end_date = date.today() if len(self.transactions) > 0: start_date = self.transactions.value_from_pos(0).get('date') or start_date end_date = self.transactions.value_from_pos(len(self.transactions) - 1).get('date') or end_date - self.parent.history_list.years = [str(i) for i in range(start_date.year, end_date.year + 1)] - self.parent.history_list.period_combo.insertItems(1, self.parent.history_list.years) + self.view.years = [str(i) for i in range(start_date.year, end_date.year + 1)] + self.view.period_combo.insertItems(1, self.view.years) # update tx_status_cache self.tx_status_cache.clear() for txid, tx_item in self.transactions.items(): @@ -223,8 +225,8 @@ class HistoryModel(QAbstractItemModel, PrintError): history = self.parent.fx.show_history() cap_gains = self.parent.fx.get_history_capital_gains_config() - hide = self.parent.history_list.hideColumn - show = self.parent.history_list.showColumn + hide = self.view.hideColumn + show = self.view.showColumn if history and cap_gains: show(5) show(6) @@ -276,9 +278,8 @@ class HistoryModel(QAbstractItemModel, PrintError): fiat_cg_title = 'n/a fiat capital gains' if fx and fx.show_history(): fiat_title = '%s '%fx.ccy + _('Value') - if fx.get_history_capital_gains_config(): - fiat_acq_title = '%s '%fx.ccy + _('Acquisition price') - fiat_cg_title = '%s '%fx.ccy + _('Capital Gains') + fiat_acq_title = '%s '%fx.ccy + _('Acquisition price') + fiat_cg_title = '%s '%fx.ccy + _('Capital Gains') return { 0: '', 1: _('Date'), @@ -292,7 +293,7 @@ class HistoryModel(QAbstractItemModel, PrintError): def flags(self, idx): extra_flags = Qt.NoItemFlags # type: Qt.ItemFlag - if idx.column() in self.parent.history_list.editable_columns: + if idx.column() in self.view.editable_columns: extra_flags |= Qt.ItemIsEditable return super().flags(idx) | extra_flags diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 1a6d5be2..c33429b6 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -812,6 +812,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def create_history_tab(self): self.history_model = HistoryModel(self) self.history_list = l = HistoryList(self, self.history_model) + self.history_model.view = self.history_list l.searchable_list = l toolbar = l.create_toolbar(self.config) toolbar_shown = self.config.get('show_toolbar_history', False) From 5be696646279d544ceceb2d773149f402331e90e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 05:50:19 +0100 Subject: [PATCH 230/301] qt history list: allow filtering by (partial) txid --- electrum/gui/qt/history_list.py | 19 ++++++++++++++----- electrum/gui/qt/util.py | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index d303caa9..5b34ce44 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -71,7 +71,7 @@ class HistorySortModel(QSortFilterProxyModel): class HistoryModel(QAbstractItemModel, PrintError): - NUM_COLUMNS = 8 + NUM_COLUMNS = 9 def __init__(self, parent): super().__init__(parent) @@ -118,6 +118,7 @@ class HistoryModel(QAbstractItemModel, PrintError): 5: tx_item['fiat_value'].value if 'fiat_value' in tx_item else None, 6: tx_item['acquisition_price'].value if 'acquisition_price' in tx_item else None, 7: tx_item['capital_gain'].value if 'capital_gain' in tx_item else None, + 8: tx_hash, } return QVariant(d[col]) if role not in (Qt.DisplayRole, Qt.EditRole): @@ -161,6 +162,8 @@ class HistoryModel(QAbstractItemModel, PrintError): elif col == 7 and 'capital_gain' in tx_item: cg = tx_item['capital_gain'].value return QVariant(self.parent.fx.format_fiat(cg)) + elif col == 8: + return QVariant(tx_hash) return None def parent(self, index: QModelIndex): @@ -222,11 +225,16 @@ class HistoryModel(QAbstractItemModel, PrintError): for txid, tx_item in self.transactions.items(): tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_info) + self.set_visibility_of_columns() - history = self.parent.fx.show_history() - cap_gains = self.parent.fx.get_history_capital_gains_config() + def set_visibility_of_columns(self): hide = self.view.hideColumn show = self.view.showColumn + # txid + hide(8) + # fiat + history = self.parent.fx.show_history() + cap_gains = self.parent.fx.get_history_capital_gains_config() if history and cap_gains: show(5) show(6) @@ -289,6 +297,7 @@ class HistoryModel(QAbstractItemModel, PrintError): 5: fiat_title, 6: fiat_acq_title, 7: fiat_cg_title, + 8: 'TXID', }[section] def flags(self, idx): @@ -305,7 +314,7 @@ class HistoryModel(QAbstractItemModel, PrintError): return tx_mined_info class HistoryList(MyTreeView, AcceptFileDragDrop): - filter_columns = [1, 2, 3] # Date, Description, Amount + filter_columns = [1, 2, 3, 8] # Date, Description, Amount, TXID def tx_item_from_proxy_row(self, proxy_row): hm_idx = self.model().mapToSource(self.model().index(proxy_row, 0)) @@ -340,7 +349,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.editable_columns |= {5} self.header().setStretchLastSection(False) - for col in range(8): + for col in range(HistoryModel.NUM_COLUMNS): sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents self.header().setSectionResizeMode(col, sm) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 0b1043c9..f71243ed 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -532,7 +532,7 @@ class MyTreeView(QTreeView): self.setRowHidden(row_num, QModelIndex(), bool(should_hide)) break else: - # we did not find the filter in any columns, show the item + # we did not find the filter in any columns, hide the item self.setRowHidden(row_num, QModelIndex(), True) def filter(self, p): From ca1043ffdab24a13dcec7521b09f0b73e597bb02 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Dec 2018 05:52:09 +0100 Subject: [PATCH 231/301] qt history list: hide columns sooner while wallet was starting up "hidden columns" were visible --- electrum/gui/qt/address_dialog.py | 2 +- electrum/gui/qt/history_list.py | 8 +++++++- electrum/gui/qt/main_window.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index d039ec42..25d9dbf8 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -89,7 +89,7 @@ class AddressDialog(WindowModalDialog): vbox.addWidget(QLabel(_("History"))) addr_hist_model = AddressHistoryModel(self.parent, self.address) self.hw = HistoryList(self.parent, addr_hist_model) - addr_hist_model.view = self.hw + addr_hist_model.set_view(self.hw) vbox.addWidget(self.hw) vbox.addLayout(Buttons(CloseButton(self))) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 5b34ce44..d11c4514 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -76,10 +76,16 @@ class HistoryModel(QAbstractItemModel, PrintError): def __init__(self, parent): super().__init__(parent) self.parent = parent - self.view = None # type: HistoryList # set by caller! FIXME... + self.view = None # type: HistoryList self.transactions = OrderedDictWithIndex() self.tx_status_cache = {} # type: Dict[str, Tuple[int, str]] + def set_view(self, history_list: 'HistoryList'): + # FIXME HistoryModel and HistoryList mutually depend on each other. + # After constructing both, this method needs to be called. + self.view = history_list # type: HistoryList + self.set_visibility_of_columns() + def columnCount(self, parent: QModelIndex): return self.NUM_COLUMNS diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index c33429b6..037ff1bd 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -812,7 +812,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def create_history_tab(self): self.history_model = HistoryModel(self) self.history_list = l = HistoryList(self, self.history_model) - self.history_model.view = self.history_list + self.history_model.set_view(self.history_list) l.searchable_list = l toolbar = l.create_toolbar(self.config) toolbar_shown = self.config.get('show_toolbar_history', False) From 059fb51893e68522fc6061f4bbda4b2c442dcb93 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 10 Dec 2018 10:18:24 +0100 Subject: [PATCH 232/301] reintroduce profiler --- electrum/gui/qt/history_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index d11c4514..8275d551 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -188,6 +188,7 @@ class HistoryModel(QAbstractItemModel, PrintError): '''Overridden in address_dialog.py''' return self.parent.wallet.get_addresses() + @profiler def refresh(self, reason: str): self.print_error(f"refreshing... reason: {reason}") assert self.parent.gui_thread == threading.current_thread(), 'must be called from GUI thread' From e35ed172003ac9bcd586fbd3947b804bf2193bc8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 10 Dec 2018 13:07:03 +0100 Subject: [PATCH 233/301] remove call to undefined method refresh_headers --- electrum/gui/qt/history_list.py | 3 --- electrum/gui/qt/main_window.py | 1 - 2 files changed, 4 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 8275d551..5c5b9223 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -363,9 +363,6 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): def format_date(self, d): return str(datetime.date(d.year, d.month, d.day)) if d else _('None') - def update_headers(self, headers): - raise NotImplementedError - def on_combo(self, x): s = self.period_combo.itemText(x) x = s == _('Custom') diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 037ff1bd..eecf390a 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2667,7 +2667,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): b = self.fx and self.fx.is_enabled() self.fiat_send_e.setVisible(b) self.fiat_receive_e.setVisible(b) - self.history_list.refresh_headers() self.history_list.update() self.address_list.refresh_headers() self.address_list.update() From b0631f90f807fd37d2d2e507157aedf6c826ba20 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Dec 2018 16:40:32 +0100 Subject: [PATCH 234/301] qt history: fix slowness arghhhhh finalllllllllllly figured it out... --- electrum/gui/qt/history_list.py | 5 ++--- electrum/gui/qt/util.py | 7 +++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 5c5b9223..3b3a6fa2 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -96,9 +96,8 @@ class HistoryModel(QAbstractItemModel, PrintError): return self.createIndex(row, column) def data(self, index: QModelIndex, role: Qt.ItemDataRole): - # requires PyQt5 5.11 - # indexIsValid = QAbstractItemModel.CheckIndexOptions(QAbstractItemModel.CheckIndexOption.IndexIsValid.value) - # assert self.checkIndex(index, indexIsValid) + # note: this method is performance-critical. + # it is called a lot, and so must run extremely fast. assert index.isValid() col = index.column() tx_item = self.transactions.value_from_pos(index.row()) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index f71243ed..409cbc8a 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -444,6 +444,13 @@ class MyTreeView(QTreeView): self.setRootIsDecorated(False) # remove left margin self.toolbar_shown = False + # When figuring out the size of columns, Qt by default looks at + # the first 1000 rows (at least if resize mode is QHeaderView.ResizeToContents). + # This would be REALLY SLOW, and it's not perfect anyway. + # So to speed the UI up considerably, set it to + # only look at as many rows as currently visible. + self.header().setResizeContentsPrecision(0) + def set_editability(self, items): for idx, i in enumerate(items): i.setEditable(idx in self.editable_columns) From 4791c7f4240eb05812053a95b41d096bf960986a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Dec 2018 16:53:46 +0100 Subject: [PATCH 235/301] qt history: fix toggling fiat capital gains --- electrum/gui/qt/history_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 3b3a6fa2..d3e257b1 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -199,6 +199,7 @@ class HistoryModel(QAbstractItemModel, PrintError): fx = self.parent.fx if fx: fx.history_used_spot = False r = self.parent.wallet.get_full_history(domain=self.get_domain(), from_timestamp=None, to_timestamp=None, fx=fx) + self.set_visibility_of_columns() if r['transactions'] == list(self.transactions.values()): return old_length = len(self.transactions) @@ -231,7 +232,6 @@ class HistoryModel(QAbstractItemModel, PrintError): for txid, tx_item in self.transactions.items(): tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_info) - self.set_visibility_of_columns() def set_visibility_of_columns(self): hide = self.view.hideColumn From 0ddccd56c70a5629618065c30526dcc38b9cdc90 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Dec 2018 17:46:37 +0100 Subject: [PATCH 236/301] interface: fix only-genesis regtest case --- electrum/interface.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index 596ecfca..87001b75 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -438,14 +438,17 @@ class Interface(PrintError): return last, height async def step(self, height, header=None): - assert height != 0 - assert height <= self.tip, (height, self.tip) + assert 0 <= height <= self.tip, (height, self.tip) if header is None: header = await self.get_block_header(height, 'catchup') chain = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header) if chain: self.blockchain = chain if isinstance(chain, Blockchain) else self.blockchain + # note: there is an edge case here that is not handled. + # we might know the blockhash (enough for check_header) but + # not have the header itself. e.g. regtest chain with only genesis. + # this situation resolves itself on the next block return 'catchup', height+1 can_connect = blockchain.can_connect(header) if 'mock' not in header else header['mock']['connect'](height) From 4e7b2f3ea3a4b3df8f4f725f3bd87ae75c25cebe Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Dec 2018 19:25:38 +0100 Subject: [PATCH 237/301] qt history: use IntEnum for column indices --- electrum/gui/qt/history_list.py | 137 +++++++++++++++++--------------- 1 file changed, 74 insertions(+), 63 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index d3e257b1..e56dc92b 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -28,6 +28,7 @@ import datetime from datetime import date from typing import TYPE_CHECKING, Tuple, Dict import threading +from enum import IntEnum from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.i18n import _ @@ -59,6 +60,17 @@ TX_ICONS = [ "confirmed.png", ] +class HistoryColumns(IntEnum): + STATUS_ICON = 0 + STATUS_TEXT = 1 + DESCRIPTION = 2 + COIN_VALUE = 3 + RUNNING_COIN_BALANCE = 4 + FIAT_VALUE = 5 + FIAT_ACQ_PRICE = 6 + FIAT_CAP_GAINS = 7 + TXID = 8 + class HistorySortModel(QSortFilterProxyModel): def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): item1 = self.sourceModel().data(source_left, Qt.UserRole) @@ -71,8 +83,6 @@ class HistorySortModel(QSortFilterProxyModel): class HistoryModel(QAbstractItemModel, PrintError): - NUM_COLUMNS = 9 - def __init__(self, parent): super().__init__(parent) self.parent = parent @@ -87,7 +97,7 @@ class HistoryModel(QAbstractItemModel, PrintError): self.set_visibility_of_columns() def columnCount(self, parent: QModelIndex): - return self.NUM_COLUMNS + return len(HistoryColumns) def rowCount(self, parent: QModelIndex): return len(self.transactions) @@ -113,61 +123,69 @@ class HistoryModel(QAbstractItemModel, PrintError): if role == Qt.UserRole: # for sorting d = { - # height breaks ties for unverified txns - # txpos breaks ties for verified same block txns - 0: (status, conf, -height, -txpos), - 1: status_str, - 2: tx_item['label'], - 3: tx_item['value'].value, - 4: tx_item['balance'].value, - 5: tx_item['fiat_value'].value if 'fiat_value' in tx_item else None, - 6: tx_item['acquisition_price'].value if 'acquisition_price' in tx_item else None, - 7: tx_item['capital_gain'].value if 'capital_gain' in tx_item else None, - 8: tx_hash, + HistoryColumns.STATUS_ICON: + # height breaks ties for unverified txns + # txpos breaks ties for verified same block txns + (status, conf, -height, -txpos), + HistoryColumns.STATUS_TEXT: status_str, + HistoryColumns.DESCRIPTION: tx_item['label'], + HistoryColumns.COIN_VALUE: tx_item['value'].value, + HistoryColumns.RUNNING_COIN_BALANCE: tx_item['balance'].value, + HistoryColumns.FIAT_VALUE: + tx_item['fiat_value'].value if 'fiat_value' in tx_item else None, + HistoryColumns.FIAT_ACQ_PRICE: + tx_item['acquisition_price'].value if 'acquisition_price' in tx_item else None, + HistoryColumns.FIAT_CAP_GAINS: + tx_item['capital_gain'].value if 'capital_gain' in tx_item else None, + HistoryColumns.TXID: tx_hash, } return QVariant(d[col]) if role not in (Qt.DisplayRole, Qt.EditRole): - if col == 0 and role == Qt.DecorationRole: + if col == HistoryColumns.STATUS_ICON and role == Qt.DecorationRole: return QVariant(self.view.icon_cache.get(":icons/" + TX_ICONS[status])) - elif col == 0 and role == Qt.ToolTipRole: + elif col == HistoryColumns.STATUS_ICON and role == Qt.ToolTipRole: return QVariant(str(conf) + _(" confirmation" + ("s" if conf != 1 else ""))) - elif col > 2 and role == Qt.TextAlignmentRole: + elif col > HistoryColumns.DESCRIPTION and role == Qt.TextAlignmentRole: return QVariant(Qt.AlignRight | Qt.AlignVCenter) - elif col != 1 and role == Qt.FontRole: + elif col != HistoryColumns.STATUS_TEXT and role == Qt.FontRole: monospace_font = QFont(MONOSPACE_FONT) return QVariant(monospace_font) - elif col == 2 and role == Qt.DecorationRole and self.parent.wallet.invoices.paid.get(tx_hash): + elif col == HistoryColumns.DESCRIPTION and role == Qt.DecorationRole \ + and self.parent.wallet.invoices.paid.get(tx_hash): return QVariant(self.view.icon_cache.get(":icons/seal")) - elif col in (2, 3) and role == Qt.ForegroundRole and tx_item['value'].value < 0: + elif col in (HistoryColumns.DESCRIPTION, HistoryColumns.COIN_VALUE) \ + and role == Qt.ForegroundRole and tx_item['value'].value < 0: red_brush = QBrush(QColor("#BC1E1E")) return QVariant(red_brush) - elif col == 5 and role == Qt.ForegroundRole and not tx_item.get('fiat_default') and tx_item.get('fiat_value') is not None: + elif col == HistoryColumns.FIAT_VALUE and role == Qt.ForegroundRole \ + and not tx_item.get('fiat_default') and tx_item.get('fiat_value') is not None: blue_brush = QBrush(QColor("#1E1EFF")) return QVariant(blue_brush) return None - if col == 1: + if col == HistoryColumns.STATUS_TEXT: return QVariant(status_str) - elif col == 2: + elif col == HistoryColumns.DESCRIPTION: return QVariant(tx_item['label']) - elif col == 3: + elif col == HistoryColumns.COIN_VALUE: value = tx_item['value'].value v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) return QVariant(v_str) - elif col == 4: + elif col == HistoryColumns.RUNNING_COIN_BALANCE: balance = tx_item['balance'].value balance_str = self.parent.format_amount(balance, whitespaces=True) return QVariant(balance_str) - elif col == 5 and 'fiat_value' in tx_item: + elif col == HistoryColumns.FIAT_VALUE and 'fiat_value' in tx_item: value_str = self.parent.fx.format_fiat(tx_item['fiat_value'].value) return QVariant(value_str) - elif col == 6 and tx_item['value'].value < 0 and 'acquisition_price' in tx_item: + elif col == HistoryColumns.FIAT_ACQ_PRICE and \ + tx_item['value'].value < 0 and 'acquisition_price' in tx_item: # fixme: should use is_mine acq = tx_item['acquisition_price'].value return QVariant(self.parent.fx.format_fiat(acq)) - elif col == 7 and 'capital_gain' in tx_item: + elif col == HistoryColumns.FIAT_CAP_GAINS and 'capital_gain' in tx_item: cg = tx_item['capital_gain'].value return QVariant(self.parent.fx.format_fiat(cg)) - elif col == 8: + elif col == HistoryColumns.TXID: return QVariant(tx_hash) return None @@ -234,25 +252,16 @@ class HistoryModel(QAbstractItemModel, PrintError): self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_info) def set_visibility_of_columns(self): - hide = self.view.hideColumn - show = self.view.showColumn + def set_visible(col: int, b: bool): + self.view.showColumn(col) if b else self.view.hideColumn(col) # txid - hide(8) + set_visible(HistoryColumns.TXID, False) # fiat history = self.parent.fx.show_history() cap_gains = self.parent.fx.get_history_capital_gains_config() - if history and cap_gains: - show(5) - show(6) - show(7) - elif history: - show(5) - hide(6) - hide(7) - else: - hide(5) - hide(6) - hide(7) + set_visible(HistoryColumns.FIAT_VALUE, history) + set_visible(HistoryColumns.FIAT_ACQ_PRICE, history and cap_gains) + set_visible(HistoryColumns.FIAT_CAP_GAINS, history and cap_gains) def update_fiat(self, row, idx): tx_item = self.transactions.value_from_pos(row) @@ -270,7 +279,7 @@ class HistoryModel(QAbstractItemModel, PrintError): except KeyError: return topLeft = self.createIndex(row, 0) - bottomRight = self.createIndex(row, self.NUM_COLUMNS-1) + bottomRight = self.createIndex(row, len(HistoryColumns) - 1) self.dataChanged.emit(topLeft, bottomRight) def on_fee_histogram(self): @@ -295,15 +304,15 @@ class HistoryModel(QAbstractItemModel, PrintError): fiat_acq_title = '%s '%fx.ccy + _('Acquisition price') fiat_cg_title = '%s '%fx.ccy + _('Capital Gains') return { - 0: '', - 1: _('Date'), - 2: _('Description'), - 3: _('Amount'), - 4: _('Balance'), - 5: fiat_title, - 6: fiat_acq_title, - 7: fiat_cg_title, - 8: 'TXID', + HistoryColumns.STATUS_ICON: '', + HistoryColumns.STATUS_TEXT: _('Date'), + HistoryColumns.DESCRIPTION: _('Description'), + HistoryColumns.COIN_VALUE: _('Amount'), + HistoryColumns.RUNNING_COIN_BALANCE: _('Balance'), + HistoryColumns.FIAT_VALUE: fiat_title, + HistoryColumns.FIAT_ACQ_PRICE: fiat_acq_title, + HistoryColumns.FIAT_CAP_GAINS: fiat_cg_title, + HistoryColumns.TXID: 'TXID', }[section] def flags(self, idx): @@ -320,7 +329,10 @@ class HistoryModel(QAbstractItemModel, PrintError): return tx_mined_info class HistoryList(MyTreeView, AcceptFileDragDrop): - filter_columns = [1, 2, 3, 8] # Date, Description, Amount, TXID + filter_columns = [HistoryColumns.STATUS_TEXT, + HistoryColumns.DESCRIPTION, + HistoryColumns.COIN_VALUE, + HistoryColumns.TXID] def tx_item_from_proxy_row(self, proxy_row): hm_idx = self.model().mapToSource(self.model().index(proxy_row, 0)) @@ -337,7 +349,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): return False def __init__(self, parent, model: HistoryModel): - super().__init__(parent, self.create_menu, 2) + super().__init__(parent, self.create_menu, stretch_column=HistoryColumns.DESCRIPTION) self.hm = model self.proxy = HistorySortModel(self) self.proxy.setSourceModel(model) @@ -351,11 +363,11 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.years = [] self.create_toolbar_buttons() self.wallet = self.parent.wallet # type: Abstract_Wallet - self.sortByColumn(0, Qt.AscendingOrder) - self.editable_columns |= {5} + self.sortByColumn(HistoryColumns.STATUS_ICON, Qt.AscendingOrder) + self.editable_columns |= {HistoryColumns.FIAT_VALUE} self.header().setStretchLastSection(False) - for col in range(HistoryModel.NUM_COLUMNS): + for col in HistoryColumns: sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents self.header().setSectionResizeMode(col, sm) @@ -489,12 +501,11 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): row, column = index.row(), index.column() tx_item = self.hm.transactions.value_from_pos(row) key = tx_item['txid'] - # fixme - if column == 2: + if column == HistoryColumns.DESCRIPTION: if self.wallet.set_label(key, text): #changed self.hm.update_label(row) self.parent.update_completions() - elif column == 5: + elif column == HistoryColumns.FIAT_VALUE: self.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, tx_item['value'].value) value = tx_item['value'].value if value is not None: @@ -527,7 +538,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): return tx_item = self.hm.transactions.value_from_pos(idx.row()) column = idx.column() - if column == 0: + if column == HistoryColumns.STATUS_ICON: column_title = _('Transaction ID') column_data = tx_item['txid'] else: From 0ec7005f90d986d801a94a2038b5fab94d36cf70 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Dec 2018 19:42:31 +0100 Subject: [PATCH 238/301] qt history: data() should return QVariant the docs says so, and also HistoryList.create_menu() was crashing sometimes re "Copy {}" --- electrum/gui/qt/history_list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index e56dc92b..3f0c7ee4 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -105,7 +105,7 @@ class HistoryModel(QAbstractItemModel, PrintError): def index(self, row: int, column: int, parent: QModelIndex): return self.createIndex(row, column) - def data(self, index: QModelIndex, role: Qt.ItemDataRole): + def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVariant: # note: this method is performance-critical. # it is called a lot, and so must run extremely fast. assert index.isValid() @@ -161,7 +161,7 @@ class HistoryModel(QAbstractItemModel, PrintError): and not tx_item.get('fiat_default') and tx_item.get('fiat_value') is not None: blue_brush = QBrush(QColor("#1E1EFF")) return QVariant(blue_brush) - return None + return QVariant() if col == HistoryColumns.STATUS_TEXT: return QVariant(status_str) elif col == HistoryColumns.DESCRIPTION: @@ -187,7 +187,7 @@ class HistoryModel(QAbstractItemModel, PrintError): return QVariant(self.parent.fx.format_fiat(cg)) elif col == HistoryColumns.TXID: return QVariant(tx_hash) - return None + return QVariant() def parent(self, index: QModelIndex): return QModelIndex() From 5a93bf054ea14f9c8dee8aad1f61eb37bd0317b4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Nov 2018 16:24:18 +0100 Subject: [PATCH 239/301] 2fa segwit (from ghost43's PR) --- electrum/base_wizard.py | 26 +++++---- electrum/bitcoin.py | 6 ++ electrum/plugins/trustedcoin/trustedcoin.py | 64 +++++++++++++++------ electrum/tests/test_wallet_vertical.py | 3 +- electrum/version.py | 9 ++- 5 files changed, 74 insertions(+), 34 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 7efd8229..838e6c4a 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -417,7 +417,7 @@ class BaseWizard(object): self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('') elif self.seed_type == 'old': self.run('create_keystore', seed, '') - elif self.seed_type == '2fa': + elif bitcoin.is_any_2fa_seed_type(self.seed_type): self.load_2fa() self.run('on_restore_seed', seed, is_ext) else: @@ -540,18 +540,20 @@ class BaseWizard(object): def show_xpub_and_add_cosigners(self, xpub): self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore')) - def choose_seed_type(self): + def choose_seed_type(self, message=None, choices=None): title = _('Choose Seed type') - message = ' '.join([ - _("The type of addresses used by your wallet will depend on your seed."), - _("Segwit wallets use bech32 addresses, defined in BIP173."), - _("Please note that websites and other wallets may not support these addresses yet."), - _("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.") - ]) - choices = [ - ('create_segwit_seed', _('Segwit')), - ('create_standard_seed', _('Legacy')), - ] + if message is None: + message = ' '.join([ + _("The type of addresses used by your wallet will depend on your seed."), + _("Segwit wallets use bech32 addresses, defined in BIP173."), + _("Please note that websites and other wallets may not support these addresses yet."), + _("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.") + ]) + if choices is None: + choices = [ + ('create_segwit_seed', _('Segwit')), + ('create_standard_seed', _('Legacy')), + ] self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) def create_segwit_seed(self): self.create_seed('segwit') diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 194ccfa7..ee1b6c0f 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -207,6 +207,8 @@ def seed_type(x: str) -> str: return 'segwit' elif is_new_seed(x, version.SEED_PREFIX_2FA): return '2fa' + elif is_new_seed(x, version.SEED_PREFIX_2FA_SW): + return '2fa_segwit' return '' @@ -214,6 +216,10 @@ def is_seed(x: str) -> bool: return bool(seed_type(x)) +def is_any_2fa_seed_type(seed_type): + return seed_type in ['2fa', '2fa_segwit'] + + ############ functions from pywallet ##################### def hash160_to_b58_address(h160: bytes, addrtype: int) -> str: diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 03a7ca83..b3b2d0a9 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -34,9 +34,9 @@ from urllib.parse import quote from aiohttp import ClientResponse from electrum import ecc, constants, keystore, version, bip32 -from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, public_key_to_p2pkh +from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, public_key_to_p2pkh, seed_type, is_any_2fa_seed_type from electrum.bip32 import (deserialize_xpub, deserialize_xprv, bip32_private_key, CKD_pub, - serialize_xpub, bip32_root, bip32_private_derivation) + serialize_xpub, bip32_root, bip32_private_derivation, xpub_type) from electrum.crypto import sha256 from electrum.transaction import TxOutput from electrum.mnemonic import Mnemonic @@ -47,12 +47,20 @@ from electrum.util import NotEnoughFunds from electrum.storage import STO_EV_USER_PW from electrum.network import Network -# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server -def get_signing_xpub(): - if constants.net.TESTNET: - return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY" +def get_signing_xpub(xtype): + if xtype == 'standard': + if constants.net.TESTNET: + return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY" + else: + return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" + elif xtype == 'p2wsh': + # TODO these are temp xpubs + if constants.net.TESTNET: + return "Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf" + else: + return "Zpub6xwgqLvc42wXB1wEELTdALD9iXwStMUkGqBgxkJFYumaL2dWgNvUkjEDWyDFZD3fZuDWDzd1KQJ4NwVHS7hs6H6QkpNYSShfNiUZsgMdtNg" else: - return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" + raise NotImplementedError('xtype: {}'.format(xtype)) def get_billing_xpub(): if constants.net.TESTNET: @@ -60,7 +68,6 @@ def get_billing_xpub(): else: return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU" -SEED_PREFIX = version.SEED_PREFIX_2FA DISCLAIMER = [ _("Two-factor authentication is a service provided by TrustedCoin. " @@ -377,7 +384,8 @@ class TrustedCoinPlugin(BasePlugin): @staticmethod def is_valid_seed(seed): - return is_new_seed(seed, SEED_PREFIX) + t = seed_type(seed) + return is_any_2fa_seed_type(t) def is_available(self): return True @@ -449,8 +457,10 @@ class TrustedCoinPlugin(BasePlugin): t.start() return t - def make_seed(self): - return Mnemonic('english').make_seed(seed_type='2fa', num_bits=128) + def make_seed(self, seed_type): + if not is_any_2fa_seed_type(seed_type): + raise BaseException('unexpected seed type: {}'.format(seed_type)) + return Mnemonic('english').make_seed(seed_type=seed_type, num_bits=128) @hook def do_clear(self, window): @@ -465,25 +475,41 @@ class TrustedCoinPlugin(BasePlugin): title = _('Create or restore') message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') choices = [ - ('create_seed', _('Create a new seed')), + ('choose_seed_type', _('Create a new seed')), ('restore_wallet', _('I already have a seed')), ] wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run) - def create_seed(self, wizard): - seed = self.make_seed() + def choose_seed_type(self, wizard): + choices = [ + ('create_2fa_seed', _('Standard 2FA')), + ('create_2fa_segwit_seed', _('Segwit 2FA')), + ] + wizard.choose_seed_type(choices=choices) + + def create_2fa_seed(self, wizard): self.create_seed(wizard, '2fa') + def create_2fa_segwit_seed(self, wizard): self.create_seed(wizard, '2fa_segwit') + + def create_seed(self, wizard, seed_type): + seed = self.make_seed(seed_type) f = lambda x: wizard.request_passphrase(seed, x) wizard.show_seed_dialog(run_next=f, seed_text=seed) @classmethod def get_xkeys(self, seed, passphrase, derivation): + t = seed_type(seed) + assert is_any_2fa_seed_type(t) + xtype = 'standard' if t == '2fa' else 'p2wsh' bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase) - xprv, xpub = bip32_root(bip32_seed, 'standard') + xprv, xpub = bip32_root(bip32_seed, xtype) xprv, xpub = bip32_private_derivation(xprv, "m/", derivation) return xprv, xpub @classmethod def xkeys_from_seed(self, seed, passphrase): + t = seed_type(seed) + if not is_any_2fa_seed_type(t): + raise BaseException('unexpected seed type: {}'.format(t)) words = seed.split() n = len(words) # old version use long seed phrases @@ -495,7 +521,7 @@ class TrustedCoinPlugin(BasePlugin): raise Exception('old 2fa seed cannot have passphrase') xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), '', "m/") xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), '', "m/") - elif n==12: + elif not t == '2fa' or n == 12: xprv1, xpub1 = self.get_xkeys(seed, passphrase, "m/0'/") xprv2, xpub2 = self.get_xkeys(seed, passphrase, "m/1'/") else: @@ -561,7 +587,8 @@ class TrustedCoinPlugin(BasePlugin): storage.put('x1/', k1.dump()) storage.put('x2/', k2.dump()) long_user_id, short_id = get_user_id(storage) - xpub3 = make_xpub(get_signing_xpub(), long_user_id) + xtype = xpub_type(xpub1) + xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id) k3 = keystore.from_xpub(xpub3) storage.put('x3/', k3.dump()) @@ -578,7 +605,8 @@ class TrustedCoinPlugin(BasePlugin): xpub2 = wizard.storage.get('x2/')['xpub'] # Generate third key deterministically. long_user_id, short_id = get_user_id(wizard.storage) - xpub3 = make_xpub(get_signing_xpub(), long_user_id) + xtype = xpub_type(xpub1) + xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id) # secret must be sent by the server try: r = server.create(xpub1, xpub2, email) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 9bc1a6e8..0acc2776 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -178,7 +178,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(SequentialTestCase): long_user_id, short_id = trustedcoin.get_user_id( {'x1/': {'xpub': xpub1}, 'x2/': {'xpub': xpub2}}) - xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(), long_user_id) + xtype = bitcoin.xpub_type(xpub1) + xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id) ks3 = keystore.from_xpub(xpub3) WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3) self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore)) diff --git a/electrum/version.py b/electrum/version.py index 5866941f..31ecebe5 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -4,9 +4,10 @@ APK_VERSION = '3.3.0.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested # The hash of the mnemonic seed must begin with this -SEED_PREFIX = '01' # Standard wallet -SEED_PREFIX_2FA = '101' # Two-factor authentication -SEED_PREFIX_SW = '100' # Segwit wallet +SEED_PREFIX = '01' # Standard wallet +SEED_PREFIX_SW = '100' # Segwit wallet +SEED_PREFIX_2FA = '101' # Two-factor authentication +SEED_PREFIX_2FA_SW = '102' # Two-factor auth, using segwit def seed_prefix(seed_type): @@ -16,3 +17,5 @@ def seed_prefix(seed_type): return SEED_PREFIX_SW elif seed_type == '2fa': return SEED_PREFIX_2FA + elif seed_type == '2fa_segwit': + return SEED_PREFIX_2FA_SW From df59a43300add969692f2c0f62c3398f159a0519 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Nov 2018 17:01:04 +0100 Subject: [PATCH 240/301] fix test --- electrum/tests/test_wallet_vertical.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 0acc2776..87213318 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -4,7 +4,7 @@ import tempfile from typing import Sequence import asyncio -from electrum import storage, bitcoin, keystore +from electrum import storage, bitcoin, keystore, bip32 from electrum import Transaction from electrum import SimpleConfig from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT @@ -178,7 +178,7 @@ class TestWalletKeystoreAddressIntegrityForMainnet(SequentialTestCase): long_user_id, short_id = trustedcoin.get_user_id( {'x1/': {'xpub': xpub1}, 'x2/': {'xpub': xpub2}}) - xtype = bitcoin.xpub_type(xpub1) + xtype = bip32.xpub_type(xpub1) xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id) ks3 = keystore.from_xpub(xpub3) WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3) From eeea4fcb319acd35708d7cc395796708dfd8f9f0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 28 Nov 2018 17:24:32 +0100 Subject: [PATCH 241/301] rename 2fa non-segwit type to "legacy 2fa" and make segwit the default --- electrum/plugins/trustedcoin/trustedcoin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index b3b2d0a9..adf3f413 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -482,8 +482,8 @@ class TrustedCoinPlugin(BasePlugin): def choose_seed_type(self, wizard): choices = [ - ('create_2fa_seed', _('Standard 2FA')), ('create_2fa_segwit_seed', _('Segwit 2FA')), + ('create_2fa_seed', _('Legacy 2FA')), ] wizard.choose_seed_type(choices=choices) From 7b90d6944354847762b1668af24ad50850eb59e9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 28 Nov 2018 19:00:41 +0100 Subject: [PATCH 242/301] trustedcoin: p2wpkh billing addresses --- electrum/plugins/trustedcoin/qt.py | 12 ---- electrum/plugins/trustedcoin/trustedcoin.py | 62 +++++++++++++-------- electrum/storage.py | 12 +++- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py index da4ce84c..c84a2bbb 100644 --- a/electrum/plugins/trustedcoin/qt.py +++ b/electrum/plugins/trustedcoin/qt.py @@ -195,18 +195,6 @@ class Plugin(TrustedCoinPlugin): vbox.addLayout(Buttons(CloseButton(d))) d.exec_() - def on_buy(self, window, k, v, d): - d.close() - if window.pluginsdialog: - window.pluginsdialog.close() - wallet = window.wallet - uri = "bitcoin:" + wallet.billing_info['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000) - wallet.is_billing = True - window.pay_to_URI(uri) - window.payto_e.setFrozen(True) - window.message_e.setFrozen(True) - window.amount_e.setFrozen(True) - def go_online_dialog(self, wizard): msg = [ _("Your wallet file is: {}.").format(os.path.abspath(wizard.storage.path)), diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index adf3f413..d93bf354 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -28,13 +28,15 @@ import json import base64 import time import hashlib +from collections import defaultdict +from typing import Dict from urllib.parse import urljoin from urllib.parse import quote from aiohttp import ClientResponse -from electrum import ecc, constants, keystore, version, bip32 -from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, public_key_to_p2pkh, seed_type, is_any_2fa_seed_type +from electrum import ecc, constants, keystore, version, bip32, bitcoin +from electrum.bitcoin import TYPE_ADDRESS, is_new_seed, seed_type, is_any_2fa_seed_type from electrum.bip32 import (deserialize_xpub, deserialize_xprv, bip32_private_key, CKD_pub, serialize_xpub, bip32_root, bip32_private_derivation, xpub_type) from electrum.crypto import sha256 @@ -244,14 +246,18 @@ class Wallet_2fa(Multisig_Wallet): self.is_billing = False self.billing_info = None self._load_billing_addresses() + self.plugin = None # type: TrustedCoinPlugin def _load_billing_addresses(self): billing_addresses = self.storage.get('trustedcoin_billing_addresses', {}) - self._billing_addresses = {} # index -> addr - # convert keys from str to int - for index, addr in list(billing_addresses.items()): - self._billing_addresses[int(index)] = addr - self._billing_addresses_set = set(self._billing_addresses.values()) # set of addrs + self._billing_addresses = defaultdict(dict) # type: Dict[str, Dict[int, str]] # addr_type -> index -> addr + self._billing_addresses_set = set() # set of addrs + for addr_type, d in list(billing_addresses.items()): + self._billing_addresses[addr_type] = {} + # convert keys from str to int + for index, addr in d.items(): + self._billing_addresses[addr_type][int(index)] = addr + self._billing_addresses_set.add(addr) def can_sign_without_server(self): return not self.keystores['x2/'].is_watching_only() @@ -291,7 +297,7 @@ class Wallet_2fa(Multisig_Wallet): self, coins, o, config, fixed_fee, change_addr) fee = self.extra_fee(config) if not is_sweep else 0 if fee: - address = self.billing_info['billing_address'] + address = self.billing_info['billing_address_segwit'] fee_output = TxOutput(TYPE_ADDRESS, address, fee) try: tx = mk_tx(outputs + [fee_output]) @@ -322,8 +328,9 @@ class Wallet_2fa(Multisig_Wallet): self.billing_info = None self.plugin.start_request_thread(self) - def add_new_billing_address(self, billing_index: int, address: str): - saved_addr = self._billing_addresses.get(billing_index) + def add_new_billing_address(self, billing_index: int, address: str, addr_type: str): + billing_addresses_of_this_type = self._billing_addresses[addr_type] + saved_addr = billing_addresses_of_this_type.get(billing_index) if saved_addr is not None: if saved_addr == address: return # already saved this address @@ -332,15 +339,16 @@ class Wallet_2fa(Multisig_Wallet): 'for index {}, already saved {}, now got {}' .format(billing_index, saved_addr, address)) # do we have all prior indices? (are we synced?) - largest_index_we_have = max(self._billing_addresses) if self._billing_addresses else -1 + largest_index_we_have = max(billing_addresses_of_this_type) if billing_addresses_of_this_type else -1 if largest_index_we_have + 1 < billing_index: # need to sync for i in range(largest_index_we_have + 1, billing_index): - addr = make_billing_address(self, i) - self._billing_addresses[i] = addr + addr = make_billing_address(self, i, addr_type=addr_type) + billing_addresses_of_this_type[i] = addr self._billing_addresses_set.add(addr) # save this address; and persist to disk - self._billing_addresses[billing_index] = address + billing_addresses_of_this_type[billing_index] = address self._billing_addresses_set.add(address) + self._billing_addresses[addr_type] = billing_addresses_of_this_type self.storage.put('trustedcoin_billing_addresses', self._billing_addresses) # FIXME this often runs in a daemon thread, where storage.write will fail self.storage.write() @@ -365,12 +373,17 @@ def make_xpub(xpub, s): cK2, c2 = bip32._CKD_pub(cK, c, s) return serialize_xpub(version, c2, cK2) -def make_billing_address(wallet, num): +def make_billing_address(wallet, num, addr_type): long_id, short_id = wallet.get_user_id() xpub = make_xpub(get_billing_xpub(), long_id) version, _, _, _, c, cK = deserialize_xpub(xpub) cK, c = CKD_pub(cK, c, num) - return public_key_to_p2pkh(cK) + if addr_type == 'legacy': + return bitcoin.public_key_to_p2pkh(cK) + elif addr_type == 'segwit': + return bitcoin.public_key_to_p2wpkh(cK) + else: + raise ValueError(f'unexpected billing type: {addr_type}') class TrustedCoinPlugin(BasePlugin): @@ -428,7 +441,7 @@ class TrustedCoinPlugin(BasePlugin): return f @finish_requesting - def request_billing_info(self, wallet): + def request_billing_info(self, wallet: 'Wallet_2fa'): if wallet.can_sign_without_server(): return self.print_error("request billing info") @@ -438,11 +451,16 @@ class TrustedCoinPlugin(BasePlugin): self.print_error('cannot connect to TrustedCoin server: {}'.format(repr(e))) return billing_index = billing_info['billing_index'] - billing_address = make_billing_address(wallet, billing_index) - if billing_address != billing_info['billing_address']: - raise Exception('unexpected trustedcoin billing address: expected {}, received {}' - .format(billing_address, billing_info['billing_address'])) - wallet.add_new_billing_address(billing_index, billing_address) + # add segwit billing address; this will be used for actual billing + billing_address = make_billing_address(wallet, billing_index, addr_type='segwit') + if billing_address != billing_info['billing_address_segwit']: + raise Exception(f'unexpected trustedcoin billing address: ' + f'calculated {billing_address}, received {billing_info["billing_address_segwit"]}') + wallet.add_new_billing_address(billing_index, billing_address, addr_type='segwit') + # also add legacy billing address; only used for detecting past payments in GUI + billing_address = make_billing_address(wallet, billing_index, addr_type='legacy') + wallet.add_new_billing_address(billing_index, billing_address, addr_type='legacy') + wallet.billing_info = billing_info wallet.price_per_tx = dict(billing_info['price_per_tx']) wallet.price_per_tx.pop(1, None) diff --git a/electrum/storage.py b/electrum/storage.py index d526000e..b88e8313 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -44,7 +44,7 @@ from .keystore import bip44_derivation OLD_SEED_VERSION = 4 # electrum versions < 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 18 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 19 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format @@ -354,6 +354,7 @@ class WalletStorage(JsonDB): self.convert_version_16() self.convert_version_17() self.convert_version_18() + self.convert_version_19() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.write() @@ -572,11 +573,16 @@ class WalletStorage(JsonDB): # delete verified_tx3 as its structure changed if not self._is_upgrade_method_needed(17, 17): return - self.put('verified_tx3', None) - self.put('seed_version', 18) + def convert_version_19(self): + # delete trustedcoin_billing_addresses + if not self._is_upgrade_method_needed(18, 18): + return + self.put('trustedcoin_billing_addresses', None) + self.put('seed_version', 19) + def convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return From 852f2a0d65de2b5d34083fa0cdd031fec257714e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 9 Dec 2018 06:17:33 +0100 Subject: [PATCH 243/301] trustedcoin: do not require wallet file upgrade --- electrum/plugins/trustedcoin/trustedcoin.py | 10 +++++++--- electrum/storage.py | 10 +--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index d93bf354..1a989ca0 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -249,8 +249,11 @@ class Wallet_2fa(Multisig_Wallet): self.plugin = None # type: TrustedCoinPlugin def _load_billing_addresses(self): - billing_addresses = self.storage.get('trustedcoin_billing_addresses', {}) - self._billing_addresses = defaultdict(dict) # type: Dict[str, Dict[int, str]] # addr_type -> index -> addr + # type: Dict[str, Dict[int, str]] # addr_type -> index -> addr + self._billing_addresses = { + 'legacy': self.storage.get('trustedcoin_billing_addresses', {}), + 'segwit': self.storage.get('trustedcoin_billing_addresses_segwit', {}) + } self._billing_addresses_set = set() # set of addrs for addr_type, d in list(billing_addresses.items()): self._billing_addresses[addr_type] = {} @@ -349,7 +352,8 @@ class Wallet_2fa(Multisig_Wallet): billing_addresses_of_this_type[billing_index] = address self._billing_addresses_set.add(address) self._billing_addresses[addr_type] = billing_addresses_of_this_type - self.storage.put('trustedcoin_billing_addresses', self._billing_addresses) + self.storage.put('trustedcoin_billing_addresses', self._billing_addresses['legacy']) + self.storage.put('trustedcoin_billing_addresses_segwit', self._billing_addresses['segwit']) # FIXME this often runs in a daemon thread, where storage.write will fail self.storage.write() diff --git a/electrum/storage.py b/electrum/storage.py index b88e8313..7dcce567 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -44,7 +44,7 @@ from .keystore import bip44_derivation OLD_SEED_VERSION = 4 # electrum versions < 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 19 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 18 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format @@ -354,7 +354,6 @@ class WalletStorage(JsonDB): self.convert_version_16() self.convert_version_17() self.convert_version_18() - self.convert_version_19() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.write() @@ -576,13 +575,6 @@ class WalletStorage(JsonDB): self.put('verified_tx3', None) self.put('seed_version', 18) - def convert_version_19(self): - # delete trustedcoin_billing_addresses - if not self._is_upgrade_method_needed(18, 18): - return - self.put('trustedcoin_billing_addresses', None) - self.put('seed_version', 19) - def convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return From 84519752c381a688b612cc3bfbd7987908eba7a4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 9 Dec 2018 06:33:32 +0100 Subject: [PATCH 244/301] trustedcoin: fix prev. remove temp xpubs. --- electrum/plugins/trustedcoin/trustedcoin.py | 24 ++++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 1a989ca0..5c309b93 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -50,19 +50,17 @@ from electrum.storage import STO_EV_USER_PW from electrum.network import Network def get_signing_xpub(xtype): - if xtype == 'standard': - if constants.net.TESTNET: - return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY" - else: - return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" - elif xtype == 'p2wsh': - # TODO these are temp xpubs - if constants.net.TESTNET: - return "Vpub5fcdcgEwTJmbmqAktuK8Kyq92fMf7sWkcP6oqAii2tG47dNbfkGEGUbfS9NuZaRywLkHE6EmUksrqo32ZL3ouLN1HTar6oRiHpDzKMAF1tf" - else: - return "Zpub6xwgqLvc42wXB1wEELTdALD9iXwStMUkGqBgxkJFYumaL2dWgNvUkjEDWyDFZD3fZuDWDzd1KQJ4NwVHS7hs6H6QkpNYSShfNiUZsgMdtNg" + if not constants.net.TESTNET: + xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" else: + xpub = "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY" + if xtype not in ('standard', 'p2wsh'): raise NotImplementedError('xtype: {}'.format(xtype)) + if xtype == 'standard': + return xpub + _, depth, fingerprint, child_number, c, cK = bip32.deserialize_xpub(xpub) + xpub = bip32.serialize_xpub(xtype, c, cK, depth, fingerprint, child_number) + return xpub def get_billing_xpub(): if constants.net.TESTNET: @@ -249,11 +247,11 @@ class Wallet_2fa(Multisig_Wallet): self.plugin = None # type: TrustedCoinPlugin def _load_billing_addresses(self): - # type: Dict[str, Dict[int, str]] # addr_type -> index -> addr - self._billing_addresses = { + billing_addresses = { 'legacy': self.storage.get('trustedcoin_billing_addresses', {}), 'segwit': self.storage.get('trustedcoin_billing_addresses_segwit', {}) } + self._billing_addresses = {} # type: Dict[str, Dict[int, str]] # addr_type -> index -> addr self._billing_addresses_set = set() # set of addrs for addr_type, d in list(billing_addresses.items()): self._billing_addresses[addr_type] = {} From 040b5b3f88aacaee033b96dbee584aa59d4572fa Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 11 Dec 2018 09:59:39 +0100 Subject: [PATCH 245/301] trustedcoin: fix get_xkeys --- electrum/plugins/trustedcoin/trustedcoin.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 5c309b93..92c14cb0 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -516,8 +516,7 @@ class TrustedCoinPlugin(BasePlugin): wizard.show_seed_dialog(run_next=f, seed_text=seed) @classmethod - def get_xkeys(self, seed, passphrase, derivation): - t = seed_type(seed) + def get_xkeys(self, seed, t, passphrase, derivation): assert is_any_2fa_seed_type(t) xtype = 'standard' if t == '2fa' else 'p2wsh' bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase) @@ -539,11 +538,11 @@ class TrustedCoinPlugin(BasePlugin): # the probability of it being < 20 words is about 2^(-(256+12-19*11)) = 2^(-59) if passphrase != '': raise Exception('old 2fa seed cannot have passphrase') - xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), '', "m/") - xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), '', "m/") + xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), t, '', "m/") + xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), t, '', "m/") elif not t == '2fa' or n == 12: - xprv1, xpub1 = self.get_xkeys(seed, passphrase, "m/0'/") - xprv2, xpub2 = self.get_xkeys(seed, passphrase, "m/1'/") + xprv1, xpub1 = self.get_xkeys(seed, t, passphrase, "m/0'/") + xprv2, xpub2 = self.get_xkeys(seed, t, passphrase, "m/1'/") else: raise Exception('unrecognized seed length: {} words'.format(n)) return xprv1, xpub1, xprv2, xpub2 From 467e40b555bad9f5bc01952c9fcadc3df4aa0fe4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 11 Dec 2018 11:46:31 +0100 Subject: [PATCH 246/301] trustedcoin: serialize using PARTIAL_TXN_HEADER_MAGIC --- electrum/plugins/trustedcoin/trustedcoin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 92c14cb0..9bb9491f 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -319,7 +319,7 @@ class Wallet_2fa(Multisig_Wallet): return otp = int(otp) long_user_id, short_id = self.get_user_id() - raw_tx = tx.serialize_to_network() + raw_tx = tx.serialize() r = server.sign(short_id, raw_tx, otp) if r: raw_tx = r.get('transaction') From 502a4819b6a831152357fd017921b4a857dcfa50 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 11 Dec 2018 13:08:10 +0100 Subject: [PATCH 247/301] trustedcoin: do not set wallet.plugin in constructor --- electrum/plugins/trustedcoin/trustedcoin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 9bb9491f..86887c96 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -244,7 +244,6 @@ class Wallet_2fa(Multisig_Wallet): self.is_billing = False self.billing_info = None self._load_billing_addresses() - self.plugin = None # type: TrustedCoinPlugin def _load_billing_addresses(self): billing_addresses = { From 4681ac8c23cc587f7bd0842e6aa922a9feff01de Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 11 Dec 2018 13:58:05 +0100 Subject: [PATCH 248/301] CLI deserialize: always force full parse --- electrum/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/commands.py b/electrum/commands.py index fd478ded..41ec9e3d 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -323,7 +323,7 @@ class Commands: def deserialize(self, tx): """Deserialize a serialized transaction""" tx = Transaction(tx) - return tx.deserialize() + return tx.deserialize(force_full_parse=True) @command('n') def broadcast(self, tx): From 363dd12a2a6d52addf4bb099c2ef20fd36abb6ed Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 11 Dec 2018 21:29:23 +0100 Subject: [PATCH 249/301] qt: try even harder not to crash whole app on first start --- electrum/gui/qt/__init__.py | 6 +++++- electrum/gui/qt/main_window.py | 11 +++-------- electrum/util.py | 11 +++++++++++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 885684ea..07e7e503 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -44,7 +44,7 @@ from electrum.plugin import run_hook from electrum.storage import WalletStorage from electrum.base_wizard import GoBack from electrum.util import (UserCancelled, PrintError, profiler, - WalletFileException, BitcoinException) + WalletFileException, BitcoinException, get_new_wallet_name) from .installwizard import InstallWizard @@ -263,6 +263,10 @@ class ElectrumGui(PrintError): d = QMessageBox(QMessageBox.Warning, _('Error'), _('Cannot create window for wallet') + ':\n' + str(e)) d.exec_() + if app_is_starting: + wallet_dir = os.path.dirname(path) + path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir)) + self.start_new_window(path, uri) return if uri: w.pay_to_URI(uri) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index eecf390a..a93b81f8 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -55,7 +55,8 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, export_meta, import_meta, bh2u, bfh, InvalidPassword, base_units, base_units_list, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, quantize_feerate, - UnknownBaseUnit, DECIMAL_POINT_DEFAULT, UserFacingException) + UnknownBaseUnit, DECIMAL_POINT_DEFAULT, UserFacingException, + get_new_wallet_name) from electrum.transaction import Transaction, TxOutput from electrum.address_synchronizer import AddTransactionException from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, @@ -487,13 +488,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): except FileNotFoundError as e: self.show_error(str(e)) return - i = 1 - while True: - filename = "wallet_%d" % i - if filename in os.listdir(wallet_folder): - i += 1 - else: - break + filename = get_new_wallet_name(wallet_folder) full_path = os.path.join(wallet_folder, filename) self.gui_object.start_new_window(full_path, None) diff --git a/electrum/util.py b/electrum/util.py index f158d9d0..0cc60a29 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -413,6 +413,17 @@ def assert_file_in_datadir_available(path, config_path): 'Should be at {}'.format(path)) +def get_new_wallet_name(wallet_folder: str) -> str: + i = 1 + while True: + filename = "wallet_%d" % i + if filename in os.listdir(wallet_folder): + i += 1 + else: + break + return filename + + def assert_bytes(*args): """ porting helper, assert args type From 9bbfd610be8457fb2911f963df3d0e8bca56a2c1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 12 Dec 2018 19:58:13 +0100 Subject: [PATCH 250/301] qt: don't flash QWidgets on startup before main window is visible Consider wallet without password set. Using Qt GUI. When starting the app, before the main window appears, small artefacts ("minimised" windows?) would appear very briefly and then disappear. --- electrum/gui/qt/invoice_list.py | 6 ++++-- electrum/gui/qt/request_list.py | 13 +++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 4789aa06..111bf142 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -58,8 +58,10 @@ class InvoiceList(MyTreeView): item[3].setFont(QFont(MONOSPACE_FONT)) self.addTopLevelItem(item) self.selectionModel().select(self.model().index(0,0), QItemSelectionModel.SelectCurrent) - self.setVisible(len(inv_list)) - self.parent.invoices_label.setVisible(len(inv_list)) + if self.parent.isVisible(): + b = len(inv_list) > 0 + self.setVisible(b) + self.parent.invoices_label.setVisible(b) def import_invoices(self): import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.update) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 8c6567fc..79faca43 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -67,12 +67,13 @@ class RequestList(MyTreeView): def update(self): self.wallet = self.parent.wallet # hide receive tab if no receive requests available - b = len(self.wallet.receive_requests) > 0 - self.setVisible(b) - self.parent.receive_requests_label.setVisible(b) - if not b: - self.parent.expires_label.hide() - self.parent.expires_combo.show() + if self.parent.isVisible(): + b = len(self.wallet.receive_requests) > 0 + self.setVisible(b) + self.parent.receive_requests_label.setVisible(b) + if not b: + self.parent.expires_label.hide() + self.parent.expires_combo.show() # update the receive address if necessary current_address = self.parent.receive_address_e.text() From ef94af950c410abb9df724a00b93471584852007 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 12 Dec 2018 20:50:53 +0100 Subject: [PATCH 251/301] wallet: try detecting internal address corruption --- electrum/gui/kivy/main_window.py | 13 ++++++-- electrum/gui/kivy/uix/screens.py | 29 ++++++++++------ electrum/gui/qt/address_list.py | 12 ++++++- electrum/gui/qt/main_window.py | 33 +++++++++++++++--- electrum/gui/qt/request_list.py | 7 +++- electrum/util.py | 4 +++ electrum/wallet.py | 57 ++++++++++++++++++++++++++++++-- 7 files changed, 134 insertions(+), 21 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 811dda6f..b6b5cd48 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -9,9 +9,9 @@ import threading from electrum.bitcoin import TYPE_ADDRESS from electrum.storage import WalletStorage -from electrum.wallet import Wallet +from electrum.wallet import Wallet, InternalAddressCorruption from electrum.paymentrequest import InvoiceStore -from electrum.util import profiler, InvalidPassword +from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter 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 @@ -712,6 +712,11 @@ class ElectrumWindow(App): self.receive_screen.clear() self.update_tabs() run_hook('load_wallet', wallet, self) + try: + wallet.try_detecting_internal_addresses_corruption() + except InternalAddressCorruption as e: + self.show_error(str(e)) + send_exception_to_crash_reporter(e) def update_status(self, *dt): self.num_blocks = self.network.get_local_height() @@ -754,6 +759,10 @@ class ElectrumWindow(App): return '' except NotEnoughFunds: return '' + except InternalAddressCorruption as e: + self.show_error(str(e)) + send_exception_to_crash_reporter(e) + return '' amount = tx.output_value() __, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0) amount_after_all_fees = amount - x_fee_amount diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index 73930aef..49b0d0b3 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -21,9 +21,10 @@ from kivy.utils import platform from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat from electrum import bitcoin from electrum.transaction import TxOutput -from electrum.util import timestamp_to_datetime +from electrum.util import send_exception_to_crash_reporter from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from electrum.plugin import run_hook +from electrum.wallet import InternalAddressCorruption from .context_menu import ContextMenu @@ -331,18 +332,24 @@ class ReceiveScreen(CScreen): self.screen.amount = '' self.screen.message = '' - def get_new_address(self): + def get_new_address(self) -> bool: + """Sets the address field, and returns whether the set address + is unused.""" if not self.app.wallet: return False self.clear() - addr = self.app.wallet.get_unused_address() - if addr is None: - addr = self.app.wallet.get_receiving_address() or '' - b = False - else: - b = True + unused = True + try: + addr = self.app.wallet.get_unused_address() + if addr is None: + addr = self.app.wallet.get_receiving_address() or '' + unused = False + except InternalAddressCorruption as e: + addr = '' + self.app.show_error(str(e)) + send_exception_to_crash_reporter(e) self.screen.address = addr - return b + return unused def on_address(self, addr): req = self.app.wallet.get_payment_request(addr, self.app.electrum_config) @@ -401,8 +408,8 @@ class ReceiveScreen(CScreen): Clock.schedule_once(lambda dt: self.update_qr()) def do_new(self): - addr = self.get_new_address() - if not addr: + is_unused = self.get_new_address() + if not is_unused: self.app.show_info(_('Please use the existing requests first.')) def do_save(self): diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 01958810..e9b6b143 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -28,6 +28,7 @@ from electrum.i18n import _ from electrum.util import block_explorer_URL from electrum.plugin import run_hook from electrum.bitcoin import is_address +from electrum.wallet import InternalAddressCorruption from .util import * @@ -168,7 +169,7 @@ class AddressList(MyTreeView): column_title = self.model().horizontalHeaderItem(col).text() copy_text = self.model().itemFromIndex(idx).text() - menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(copy_text)) + menu.addAction(_("Copy {}").format(column_title), lambda: self.place_text_on_clipboard(copy_text)) menu.addAction(_('Details'), lambda: self.parent.show_address(addr)) persistent = QPersistentModelIndex(addr_idx) menu.addAction(_("Edit {}").format(addr_column_title), lambda p=persistent: self.edit(QModelIndex(p))) @@ -195,3 +196,12 @@ class AddressList(MyTreeView): run_hook('receive_menu', menu, addrs, self.wallet) menu.exec_(self.viewport().mapToGlobal(position)) + + def place_text_on_clipboard(self, text): + if is_address(text): + try: + self.wallet.raise_if_cannot_rederive_address(text) + except InternalAddressCorruption as e: + self.parent.show_error(str(e)) + raise + self.parent.app.clipboard().setText(text) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index a93b81f8..cfc18036 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -56,11 +56,11 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, base_units, base_units_list, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, quantize_feerate, UnknownBaseUnit, DECIMAL_POINT_DEFAULT, UserFacingException, - get_new_wallet_name) + get_new_wallet_name, send_exception_to_crash_reporter) from electrum.transaction import Transaction, TxOutput from electrum.address_synchronizer import AddTransactionException from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, - sweep_preparations) + sweep_preparations, InternalAddressCorruption) from electrum.version import ELECTRUM_VERSION from electrum.network import Network from electrum.exchange_rate import FxThread @@ -399,6 +399,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.show() self.watching_only_changed() run_hook('load_wallet', wallet, self) + try: + wallet.try_detecting_internal_addresses_corruption() + except InternalAddressCorruption as e: + self.show_error(str(e)) + send_exception_to_crash_reporter(e) def init_geometry(self): winpos = self.wallet.storage.get("winpos-qt") @@ -1030,7 +1035,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.receive_amount_e.setAmount(None) def clear_receive_tab(self): - addr = self.wallet.get_receiving_address() or '' + try: + addr = self.wallet.get_receiving_address() or '' + except InternalAddressCorruption as e: + self.show_error(str(e)) + addr = '' self.receive_address_e.setText(addr) self.receive_message_e.setText('') self.receive_amount_e.setAmount(None) @@ -1557,6 +1566,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): except (NotEnoughFunds, NoDynamicFeeEstimates) as e: self.show_message(str(e)) return + except InternalAddressCorruption as e: + self.show_error(str(e)) + raise except BaseException as e: traceback.print_exc(file=sys.stdout) self.show_message(str(e)) @@ -2600,11 +2612,24 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): text = str(keys_e.toPlainText()) return keystore.get_private_keys(text) + def on_address(text): + # set text color + addr = get_address() + ss = (ColorScheme.DEFAULT if addr else ColorScheme.RED).as_stylesheet() + address_e.setStyleSheet(ss) + # if addr looks to be ours, make sure we can re-derive it + if addr and self.wallet.is_mine(addr): + try: + self.wallet.raise_if_cannot_rederive_address(addr) + except InternalAddressCorruption as e: + self.show_error(str(e)) + raise + f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None) - on_address = lambda text: address_e.setStyleSheet((ColorScheme.DEFAULT if get_address() else ColorScheme.RED).as_stylesheet()) keys_e.textChanged.connect(f) address_e.textChanged.connect(f) address_e.textChanged.connect(on_address) + on_address(str(address_e.text())) if not d.exec_(): return # user pressed "sweep" diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 79faca43..a21acd97 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -31,6 +31,7 @@ from electrum.i18n import _ from electrum.util import format_time, age from electrum.plugin import run_hook from electrum.paymentrequest import PR_UNKNOWN +from electrum.wallet import InternalAddressCorruption from .util import MyTreeView, pr_tooltips, pr_icons @@ -78,7 +79,11 @@ class RequestList(MyTreeView): # update the receive address if necessary current_address = self.parent.receive_address_e.text() domain = self.wallet.get_receiving_addresses() - addr = self.wallet.get_unused_address() + try: + addr = self.wallet.get_unused_address() + except InternalAddressCorruption as e: + self.parent.show_error(str(e)) + addr = '' if not current_address in domain and addr: self.parent.set_receive_address(addr) self.parent.new_request_button.setEnabled(addr != current_address) diff --git a/electrum/util.py b/electrum/util.py index 0cc60a29..0dffc0de 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -835,6 +835,10 @@ def setup_thread_excepthook(): threading.Thread.__init__ = init +def send_exception_to_crash_reporter(e: BaseException): + sys.excepthook(type(e), e, e.__traceback__) + + def versiontuple(v): return tuple(map(int, (v.split(".")))) diff --git a/electrum/wallet.py b/electrum/wallet.py index 0d58f44d..10252f63 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -61,6 +61,7 @@ from .paymentrequest import (PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, InvoiceStore) from .contacts import Contacts from .interface import RequestTimedOut +from .ecc_fast import is_using_fast_ecc if TYPE_CHECKING: from .network import Network @@ -149,6 +150,11 @@ def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=N class CannotBumpFee(Exception): pass +class InternalAddressCorruption(Exception): + def __str__(self): + return _("Internal address database inconsistency detected. " + "You should restore from seed.") + class Abstract_Wallet(AddressSynchronizer): @@ -632,6 +638,10 @@ class Abstract_Wallet(AddressSynchronizer): # if there are none, take one randomly from the last few addrs = self.get_change_addresses()[-self.gap_limit_for_change:] change_addrs = [random.choice(addrs)] if addrs else [] + for addr in change_addrs: + # note that change addresses are not necessarily ismine + # in which case this is a no-op + self.raise_if_cannot_rederive_address(addr) # Fee estimator if fixed_fee is None: @@ -887,17 +897,33 @@ class Abstract_Wallet(AddressSynchronizer): continue return tx + @profiler + def try_detecting_internal_addresses_corruption(self): + pass + + def raise_if_cannot_rederive_address(self, addr): + pass + + def try_rederiving_returned_address(func): + def wrapper(self, *args, **kwargs): + addr = func(self, *args, **kwargs) + self.raise_if_cannot_rederive_address(addr) + return addr + return wrapper + def get_unused_addresses(self): # fixme: use slots from expired requests domain = self.get_receiving_addresses() return [addr for addr in domain if not self.history.get(addr) and addr not in self.receive_requests.keys()] + @try_rederiving_returned_address def get_unused_address(self): addrs = self.get_unused_addresses() if addrs: return addrs[0] + @try_rederiving_returned_address def get_receiving_address(self): # always return an address domain = self.get_receiving_addresses() @@ -1462,6 +1488,29 @@ class Deterministic_Wallet(Abstract_Wallet): def get_change_addresses(self): return self.change_addresses + @profiler + def try_detecting_internal_addresses_corruption(self): + if not is_using_fast_ecc(): + self.print_error("internal address corruption test skipped due to missing libsecp256k1") + return + addresses_all = self.get_addresses() + # sample 1: first few + addresses_sample1 = addresses_all[:10] + # sample2: a few more randomly selected + addresses_rand = addresses_all[10:] + addresses_sample2 = random.sample(addresses_rand, min(len(addresses_rand), 10)) + for addr_found in addresses_sample1 + addresses_sample2: + self.raise_if_cannot_rederive_address(addr_found) + + def raise_if_cannot_rederive_address(self, addr): + if not addr: + return + if not self.is_mine(addr): + return + addr_derived = self.derive_address(*self.get_address_index(addr)) + if addr != addr_derived: + raise InternalAddressCorruption() + def get_seed(self, password): return self.keystore.get_seed(password) @@ -1515,13 +1564,17 @@ class Deterministic_Wallet(Abstract_Wallet): for i, addr in enumerate(self.change_addresses): self._addr_to_addr_index[addr] = (True, i) + def derive_address(self, for_change, n): + x = self.derive_pubkeys(for_change, n) + address = self.pubkeys_to_address(x) + return address + def create_new_address(self, for_change=False): assert type(for_change) is bool with self.lock: addr_list = self.change_addresses if for_change else self.receiving_addresses n = len(addr_list) - x = self.derive_pubkeys(for_change, n) - address = self.pubkeys_to_address(x) + address = self.derive_address(for_change, n) addr_list.append(address) self._addr_to_addr_index[address] = (for_change, n) self.save_addresses() From 3184d6f369cb113432ebbc55ed3ab22ace7233e4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 13 Dec 2018 12:10:01 +0100 Subject: [PATCH 252/301] simplify previous commit --- electrum/gui/qt/address_list.py | 2 +- electrum/gui/qt/main_window.py | 22 ++++++++------------- electrum/wallet.py | 34 ++++++++++++++------------------- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index e9b6b143..fd1f5e11 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -200,7 +200,7 @@ class AddressList(MyTreeView): def place_text_on_clipboard(self, text): if is_address(text): try: - self.wallet.raise_if_cannot_rederive_address(text) + self.wallet.check_address(text) except InternalAddressCorruption as e: self.parent.show_error(str(e)) raise diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index cfc18036..e8dad31e 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2612,20 +2612,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): text = str(keys_e.toPlainText()) return keystore.get_private_keys(text) - def on_address(text): - # set text color - addr = get_address() - ss = (ColorScheme.DEFAULT if addr else ColorScheme.RED).as_stylesheet() - address_e.setStyleSheet(ss) - # if addr looks to be ours, make sure we can re-derive it - if addr and self.wallet.is_mine(addr): - try: - self.wallet.raise_if_cannot_rederive_address(addr) - except InternalAddressCorruption as e: - self.show_error(str(e)) - raise - f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None) + on_address = lambda text: address_e.setStyleSheet((ColorScheme.DEFAULT if get_address() else ColorScheme.RED).as_stylesheet()) keys_e.textChanged.connect(f) address_e.textChanged.connect(f) address_e.textChanged.connect(on_address) @@ -2633,6 +2621,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if not d.exec_(): return # user pressed "sweep" + addr = get_address() + try: + self.wallet.check_address(addr) + except InternalAddressCorruption as e: + self.show_error(str(e)) + raise try: coins, keypairs = sweep_preparations(get_pk(), self.network) except Exception as e: # FIXME too broad... @@ -2642,7 +2636,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.do_clear() self.tx_external_keypairs = keypairs self.spend_coins(coins) - self.payto_e.setText(get_address()) + self.payto_e.setText(addr) self.spend_max() self.payto_e.setFrozen(True) self.amount_e.setFrozen(True) diff --git a/electrum/wallet.py b/electrum/wallet.py index 10252f63..42e0ae9e 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -152,8 +152,8 @@ class CannotBumpFee(Exception): pass class InternalAddressCorruption(Exception): def __str__(self): - return _("Internal address database inconsistency detected. " - "You should restore from seed.") + return _("Wallet file corruption detected. " + "Please restore your wallet from seed, and compare the addresses in both files") @@ -641,7 +641,7 @@ class Abstract_Wallet(AddressSynchronizer): for addr in change_addrs: # note that change addresses are not necessarily ismine # in which case this is a no-op - self.raise_if_cannot_rederive_address(addr) + self.check_address(addr) # Fee estimator if fixed_fee is None: @@ -897,17 +897,16 @@ class Abstract_Wallet(AddressSynchronizer): continue return tx - @profiler def try_detecting_internal_addresses_corruption(self): pass - def raise_if_cannot_rederive_address(self, addr): + def check_address(self, addr): pass - def try_rederiving_returned_address(func): + def check_returned_address(func): def wrapper(self, *args, **kwargs): addr = func(self, *args, **kwargs) - self.raise_if_cannot_rederive_address(addr) + self.check_address(addr) return addr return wrapper @@ -917,13 +916,13 @@ class Abstract_Wallet(AddressSynchronizer): return [addr for addr in domain if not self.history.get(addr) and addr not in self.receive_requests.keys()] - @try_rederiving_returned_address + @check_returned_address def get_unused_address(self): addrs = self.get_unused_addresses() if addrs: return addrs[0] - @try_rederiving_returned_address + @check_returned_address def get_receiving_address(self): # always return an address domain = self.get_receiving_addresses() @@ -1500,16 +1499,12 @@ class Deterministic_Wallet(Abstract_Wallet): addresses_rand = addresses_all[10:] addresses_sample2 = random.sample(addresses_rand, min(len(addresses_rand), 10)) for addr_found in addresses_sample1 + addresses_sample2: - self.raise_if_cannot_rederive_address(addr_found) + self.check_address(addr_found) - def raise_if_cannot_rederive_address(self, addr): - if not addr: - return - if not self.is_mine(addr): - return - addr_derived = self.derive_address(*self.get_address_index(addr)) - if addr != addr_derived: - raise InternalAddressCorruption() + def check_address(self, addr): + if addr and self.is_mine(addr): + if addr != self.derive_address(*self.get_address_index(addr)): + raise InternalAddressCorruption() def get_seed(self, password): return self.keystore.get_seed(password) @@ -1566,8 +1561,7 @@ class Deterministic_Wallet(Abstract_Wallet): def derive_address(self, for_change, n): x = self.derive_pubkeys(for_change, n) - address = self.pubkeys_to_address(x) - return address + return self.pubkeys_to_address(x) def create_new_address(self, for_change=False): assert type(for_change) is bool From 14363f8f2fe75382fd3b7c291fc1b14c678a51a8 Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Wed, 12 Dec 2018 00:53:55 +0200 Subject: [PATCH 253/301] [Qt] Got rid of qt.util.Timer class and instead replaced the functionality with the more efficient QTimer. Also added disconnection from the timer on window close. (cherry picked from https://github.com/Electron-Cash/Electron-Cash/commit/19a21eb08d4c3bb665d4a3b50daf38d51b6589b3) --- electrum/gui/qt/__init__.py | 6 +++++- electrum/gui/qt/main_window.py | 7 +++---- electrum/gui/qt/util.py | 13 ------------- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 07e7e503..4379c894 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -105,7 +105,11 @@ class ElectrumGui(PrintError): self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) - self.timer = Timer() + # timer + self.timer = QTimer(self.app) + self.timer.setSingleShot(False) + self.timer.setInterval(500) # msec + self.nd = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() self._num_wizards_in_progress = 0 diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index e8dad31e..0db7fa05 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -222,7 +222,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): # update fee slider in case we missed the callback self.fee_slider.update() self.load_wallet(wallet) - self.connect_slots(gui_object.timer) + gui_object.timer.timeout.connect(self.timer_actions) self.fetch_alias() def on_history(self, b): @@ -670,9 +670,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.config.set_key('io_dir', os.path.dirname(fileName), True) return fileName - def connect_slots(self, sender): - sender.timer_signal.connect(self.timer_actions) - def timer_actions(self): # Note this runs in the GUI thread if self.need_update.is_set(): @@ -3134,6 +3131,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if self.qr_window: self.qr_window.close() self.close_wallet() + + self.gui_object.timer.timeout.disconnect(self.timer_actions) self.gui_object.close_window(self) def plugins_dialog(self): diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 409cbc8a..ff93b181 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -48,19 +48,6 @@ expiration_values = [ ] -class Timer(QThread): - stopped = False - timer_signal = pyqtSignal() - - def run(self): - while not self.stopped: - self.timer_signal.emit() - time.sleep(0.5) - - def stop(self): - self.stopped = True - self.wait() - class EnterButton(QPushButton): def __init__(self, text, func): QPushButton.__init__(self, text) From 67b2aebed67988c733b3258fdc020e4ca9d2a409 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 13 Dec 2018 15:27:48 +0100 Subject: [PATCH 254/301] android build: use rebased p4a fork https://github.com/SomberNight/python-for-android/commit/86eeec7c19679a5886d5e095ce0a43f1da138f87 --- electrum/gui/kivy/tools/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/kivy/tools/Dockerfile b/electrum/gui/kivy/tools/Dockerfile index 71a04ee5..4e8f2b3b 100644 --- a/electrum/gui/kivy/tools/Dockerfile +++ b/electrum/gui/kivy/tools/Dockerfile @@ -134,7 +134,7 @@ RUN cd /opt \ && cd python-for-android \ && git remote add sombernight https://github.com/SomberNight/python-for-android \ && git fetch --all \ - && git checkout f74226666af69f9915afaee9ef9292db85a6c617 \ + && git checkout 86eeec7c19679a5886d5e095ce0a43f1da138f87 \ && python3 -m pip install -e . # build env vars From 7a4270f5a4441c708556432a45f2b553129ae84e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 13 Dec 2018 17:21:05 +0100 Subject: [PATCH 255/301] Qt: camera icon --- electrum/gui/qt/qrtextedit.py | 2 +- electrum/gui/qt/util.py | 1 + icons.qrc | 2 ++ icons/camera_dark.png | Bin 0 -> 687 bytes icons/camera_white.png | Bin 0 -> 1304 bytes 5 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 icons/camera_dark.png create mode 100644 icons/camera_white.png diff --git a/electrum/gui/qt/qrtextedit.py b/electrum/gui/qt/qrtextedit.py index 5676d78a..53cb4ff4 100644 --- a/electrum/gui/qt/qrtextedit.py +++ b/electrum/gui/qt/qrtextedit.py @@ -39,7 +39,7 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin): self.allow_multi = allow_multi self.setReadOnly(0) self.addButton(":icons/file.png", self.file_input, _("Read file")) - icon = ":icons/qrcode_white.png" if ColorScheme.dark_scheme else ":icons/qrcode.png" + icon = ":icons/camera_white.png" if ColorScheme.dark_scheme else ":icons/camera_dark.png" self.addButton(icon, self.qr_input, _("Read QR code")) run_hook('scan_text_edit', self) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index ff93b181..29cc20aa 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -588,6 +588,7 @@ class ButtonsWidget(QWidget): def addButton(self, icon_name, on_click, tooltip): button = QToolButton(self) button.setIcon(QIcon(icon_name)) + button.setIconSize(QSize(25,25)) button.setStyleSheet("QToolButton { border: none; hover {border: 1px} pressed {border: 1px} padding: 0px; }") button.setVisible(True) button.setToolTip(tooltip) diff --git a/icons.qrc b/icons.qrc index 19c298ad..cd77e912 100644 --- a/icons.qrc +++ b/icons.qrc @@ -30,6 +30,8 @@ icons/revealer_c.png icons/qrcode.png icons/qrcode_white.png + icons/camera_dark.png + icons/camera_white.png icons/preferences.png icons/safe-t_unpaired.png icons/safe-t.png diff --git a/icons/camera_dark.png b/icons/camera_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b73f70c729746bf776b40e33bfc000cbc4684e GIT binary patch literal 687 zcmV;g0#N;lP)s~<(Kd6qO|?Q_@-?mIXQZy)P3AiG>k%Mw^eXS~b(KASUylGK zM=u|;wAOFF3&cr|opZN5J9ob^)yV%lqw8o&RV#wu)EFj*TPuQBXo#yY*CKy+i|rx> zy!W`P4&+MfDn+nlVk?<3nw!P_p&;Ht=&5EJDg~*YeKq`DnR%6w=K>7n?_&)bS_|r;UFGP;+1*^6}{X=#C-bw}0 z{pmiiw3s&W*O)9oOV1f?Em6DB0(!TXLxP$AbdOR;&eRZc2}zhZ=T5g!`*(PBIXwIU ziHVuf^Fhaxq72iksPbbZBSi8KBI4nX$<1mE-JI2Xhjwg_hq?_0gTY`h7z~Ds@dHtE V$t<7t@g4vG002ovPDHLkV1k!{ISv2- literal 0 HcmV?d00001 diff --git a/icons/camera_white.png b/icons/camera_white.png new file mode 100644 index 0000000000000000000000000000000000000000..1a07a6f5095badd052778a700480d5c1ef017daa GIT binary patch literal 1304 zcmV+z1?T#SP)Is6iwUU!a0++=!X2OGS6?1e3K{1$1dZ zFj)xh6jTy3i;3Vy22?az3{Tj-frLfq{5=kEe&w0=@Ag<^J$qZO~3bcVw z%CT^1Xx1SnxwQ3x!II-TGE1~c+lbIv?eLh=*Y;{c}e^Iow28`*i&=l=j|Z1 z9hbD?&!$6;nMzt~0Sg6RE(MY)RdJ?$gZqpf}3|NS2 z!1hj=+5JG*>xr~<=4RQk<$o>Pb|G?Y$dqhR34vHQvu;JQF$!Kbv(uTi<%_9|l}c*+ z(Hc;GcRigS0hT>3RMUWpi}_wj0L+BHZ3Iht!_3a)1kTuK+st|%4^9Ag0lxu%n%TvS z+6ERtSdB{h0XXadwUi^51&XA?F>Jv5qR%|Y{9n?dq?@^UgjkkzU-)eB4yz#74$$sk zXQIKx+kXC|-&3U6p8vi(KnZh_UZ|r2dY)R_xd~?}E=#||yzg>>b^lamp;Na2pP2QT z?C%wuFL@-A-p&U{Nga2kLX-u;Ng@HRnAu}Nu)`VXn|`#$c>jJ6l4&lWYMhSHr17Do zPMGeT@SN&H#fL!`IBRBUA9=qn;s(=HKn#4En@lX}*aX^d9bhp}mOQY2Uc?KgY5`_; z)#-JH7VuOBu2Hq^b+C_2D@B#=v|Ru*Q*gfN)!n3UAZJMe{;g9Cn7Ow2t?15%1Zd7p zbW->wSC^R6^m-^j(!IGcx4d|1PsqKl=VA=NQDCzU3P^xwbLlQIvnyj(lvx_Q(5JBK z0Z$k4i)p*Si)Pl%F}B!wz&K5p4)D69sL%T3gGC%;+C1PynDf13Y-D{ z$eDjf(&D(~aNUccuB4@zDuyOp4!=KZ<#6p9BGaq}$7zWjV0$;p&y#M6?WO{mp=poJ+v*%B8h-A8!R6;86)y$A7<(v^C7&CMm7K+Q4_=!bsWL zK%l6WnQe`@Fan^6?Trn)z0d6)U|rIA;EuATv20-kctp~Q3$I~?2iA)gM|T5LUmP`G z9=}ktDm8V%It7YqO~7>*vz)mK+)cgq2;53+0yoU;aI;0mGhu9I^Bznx6HE5kkri5( zT=*DJ**3X-%>HJ{!fkT>a%@zzLwshnFkqu(g9Z&6G-%MEL4yYWQ~U?y-1%< Date: Thu, 13 Dec 2018 22:54:53 +0100 Subject: [PATCH 256/301] ssl: use certifi explicitly for aiohttp and electrum-server connections fixes ssl issues on Android --- electrum/interface.py | 6 +++++- electrum/util.py | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index 87001b75..ac44864e 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -33,6 +33,7 @@ from collections import defaultdict import aiorpcx from aiorpcx import RPCSession, Notification +import requests from .util import PrintError, ignore_exceptions, log_exceptions, bfh, SilentTaskGroup from . import util @@ -48,6 +49,9 @@ if TYPE_CHECKING: from .network import Network +ca_path = requests.certs.where() + + class NotificationSession(RPCSession): def __init__(self, *args, **kwargs): @@ -232,7 +236,7 @@ class Interface(PrintError): return None # see if we already have cert for this server; or get it for the first time - ca_sslc = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + ca_sslc = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path) if not self._is_saved_ssl_cert_available(): await self._try_saving_ssl_cert_for_first_time(ca_sslc) # now we have a file saved in our certificate store diff --git a/electrum/util.py b/electrum/util.py index 0dffc0de..c0529277 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -40,10 +40,12 @@ import builtins import json import time from typing import NamedTuple, Optional +import ssl import aiohttp from aiohttp_socks import SocksConnector, SocksVer from aiorpcx import TaskGroup +import requests from .i18n import _ @@ -57,6 +59,9 @@ def inv_dict(d): return {v: k for k, v in d.items()} +ca_path = requests.certs.where() + + base_units = {'BTC':8, 'mBTC':5, 'bits':2, 'sat':0} base_units_inverse = inv_dict(base_units) base_units_list = ['BTC', 'mBTC', 'bits', 'sat'] # list(dict) does not guarantee order @@ -919,6 +924,8 @@ def make_aiohttp_session(proxy: dict, headers=None, timeout=None): headers = {'User-Agent': 'Electrum'} if timeout is None: timeout = aiohttp.ClientTimeout(total=10) + ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path) + if proxy: connector = SocksConnector( socks_ver=SocksVer.SOCKS5 if proxy['mode'] == 'socks5' else SocksVer.SOCKS4, @@ -926,11 +933,13 @@ def make_aiohttp_session(proxy: dict, headers=None, timeout=None): port=int(proxy['port']), username=proxy.get('user', None), password=proxy.get('password', None), - rdns=True + rdns=True, + ssl_context=ssl_context, ) - return aiohttp.ClientSession(headers=headers, timeout=timeout, connector=connector) else: - return aiohttp.ClientSession(headers=headers, timeout=timeout) + connector = aiohttp.TCPConnector(ssl_context=ssl_context) + + return aiohttp.ClientSession(headers=headers, timeout=timeout, connector=connector) class SilentTaskGroup(TaskGroup): From 78f5afff7461830951245c971cec02e19d401edf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 13 Dec 2018 23:11:59 +0100 Subject: [PATCH 257/301] use certifi directly instead of requests --- contrib/requirements/requirements.txt | 1 + electrum/interface.py | 4 ++-- electrum/paymentrequest.py | 4 ++-- electrum/util.py | 4 ++-- electrum/x509.py | 4 ++-- run_electrum | 4 ++-- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt index 7d42df3f..be1a7135 100644 --- a/contrib/requirements/requirements.txt +++ b/contrib/requirements/requirements.txt @@ -9,3 +9,4 @@ qdarkstyle<2.6 aiorpcx>=0.9,<0.11 aiohttp aiohttp_socks +certifi diff --git a/electrum/interface.py b/electrum/interface.py index ac44864e..fc6ddfc0 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -33,7 +33,7 @@ from collections import defaultdict import aiorpcx from aiorpcx import RPCSession, Notification -import requests +import certifi from .util import PrintError, ignore_exceptions, log_exceptions, bfh, SilentTaskGroup from . import util @@ -49,7 +49,7 @@ if TYPE_CHECKING: from .network import Network -ca_path = requests.certs.where() +ca_path = certifi.where() class NotificationSession(RPCSession): diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 8b0d4bee..f94920a4 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -28,7 +28,7 @@ import time import traceback import json -import requests +import certifi import urllib.parse import aiohttp @@ -49,7 +49,7 @@ from .network import Network REQUEST_HEADERS = {'Accept': 'application/bitcoin-paymentrequest', 'User-Agent': 'Electrum'} ACK_HEADERS = {'Content-Type':'application/bitcoin-payment','Accept':'application/bitcoin-paymentack','User-Agent':'Electrum'} -ca_path = requests.certs.where() # FIXME do we need to depend on requests here? +ca_path = certifi.where() ca_list = None ca_keyID = None diff --git a/electrum/util.py b/electrum/util.py index c0529277..69a8926c 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -45,7 +45,7 @@ import ssl import aiohttp from aiohttp_socks import SocksConnector, SocksVer from aiorpcx import TaskGroup -import requests +import certifi from .i18n import _ @@ -59,7 +59,7 @@ def inv_dict(d): return {v: k for k, v in d.items()} -ca_path = requests.certs.where() +ca_path = certifi.where() base_units = {'BTC':8, 'mBTC':5, 'bits':2, 'sat':0} diff --git a/electrum/x509.py b/electrum/x509.py index f4b8ffca..6e519cad 100644 --- a/electrum/x509.py +++ b/electrum/x509.py @@ -337,8 +337,8 @@ def load_certificates(ca_path): if __name__ == "__main__": - import requests + import certifi util.set_verbosity(True) - ca_path = requests.certs.where() + ca_path = certifi.where() ca_list, ca_keyID = load_certificates(ca_path) diff --git a/run_electrum b/run_electrum index dbb88d9c..2dd730d0 100755 --- a/run_electrum +++ b/run_electrum @@ -44,7 +44,7 @@ def check_imports(): import dns import pyaes import ecdsa - import requests + import certifi import qrcode import google.protobuf import jsonrpclib @@ -58,7 +58,7 @@ def check_imports(): from google.protobuf import descriptor_pb2 from jsonrpclib import SimpleJSONRPCServer # make sure that certificates are here - assert os.path.exists(requests.utils.DEFAULT_CA_BUNDLE_PATH) + assert os.path.exists(certifi.where()) if not is_android: From 8b775fd24a0af1a03d8bab1ac5683fe988ab7808 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 13 Dec 2018 23:25:52 +0100 Subject: [PATCH 258/301] contrib: import 'requests' in try-except --- .../deterministic-build/find_restricted_dependencies.py | 5 ++++- contrib/make_locale | 7 ++++++- electrum/gui/kivy/main_window.py | 2 +- electrum/tests/test_bitcoin.py | 2 +- run_electrum | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/contrib/deterministic-build/find_restricted_dependencies.py b/contrib/deterministic-build/find_restricted_dependencies.py index 1734d575..e77e04c0 100755 --- a/contrib/deterministic-build/find_restricted_dependencies.py +++ b/contrib/deterministic-build/find_restricted_dependencies.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 import sys -import requests +try: + import requests +except ImportError as e: + sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install '") def check_restriction(p, r): diff --git a/contrib/make_locale b/contrib/make_locale index 3c28d570..ba95eb5c 100755 --- a/contrib/make_locale +++ b/contrib/make_locale @@ -3,7 +3,12 @@ import os import subprocess import io import zipfile -import requests +import sys + +try: + import requests +except ImportError as e: + sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install '") os.chdir(os.path.dirname(os.path.realpath(__file__))) os.chdir('..') diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index b6b5cd48..07c3ee67 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -790,7 +790,7 @@ class ElectrumWindow(App): notification.notify('Electrum', message, app_icon=icon, app_name='Electrum') except ImportError: - Logger.Error('Notification: needs plyer; `sudo pip install plyer`') + Logger.Error('Notification: needs plyer; `sudo python3 -m pip install plyer`') def on_pause(self): self.pause_time = time.time() diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index c19473f1..752e5017 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -29,7 +29,7 @@ from . import FAST_TESTS try: import ecdsa except ImportError: - sys.exit("Error: python-ecdsa does not seem to be installed. Try 'sudo pip install ecdsa'") + sys.exit("Error: python-ecdsa does not seem to be installed. Try 'sudo python3 -m pip install ecdsa'") def needs_test_with_all_ecc_implementations(func): diff --git a/run_electrum b/run_electrum index 2dd730d0..68040693 100755 --- a/run_electrum +++ b/run_electrum @@ -50,7 +50,7 @@ def check_imports(): import jsonrpclib import aiorpcx except ImportError as e: - sys.exit("Error: %s. Try 'sudo pip install '"%str(e)) + sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install '") # the following imports are for pyinstaller from google.protobuf import descriptor from google.protobuf import message From 95cb2fbebfe5ad7345a7ced5a9a9f5e375649623 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Dec 2018 07:19:26 +0100 Subject: [PATCH 259/301] remove requests from requirements --- contrib/requirements/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt index be1a7135..7249a40d 100644 --- a/contrib/requirements/requirements.txt +++ b/contrib/requirements/requirements.txt @@ -1,6 +1,5 @@ pyaes>=0.1a1 ecdsa>=0.9 -requests qrcode protobuf dnspython From 75f6ab913316998e1e3c4b1d256de17bc367ea8e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Dec 2018 07:41:26 +0100 Subject: [PATCH 260/301] rm requests from greenaddress plugin --- electrum/plugins/greenaddress_instant/qt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/plugins/greenaddress_instant/qt.py b/electrum/plugins/greenaddress_instant/qt.py index 85977b86..fefc834f 100644 --- a/electrum/plugins/greenaddress_instant/qt.py +++ b/electrum/plugins/greenaddress_instant/qt.py @@ -26,12 +26,12 @@ import base64 import urllib.parse import sys -import requests from PyQt5.QtWidgets import QApplication, QPushButton from electrum.plugin import BasePlugin, hook from electrum.i18n import _ +from electrum.network import Network class Plugin(BasePlugin): @@ -89,8 +89,8 @@ class Plugin(BasePlugin): sig = base64.b64encode(sig).decode('ascii') # 2. send the request - response = requests.request("GET", ("https://greenaddress.it/verify/?signature=%s&txhash=%s" % (urllib.parse.quote(sig), tx.txid())), - headers = {'User-Agent': 'Electrum'}) + url = "https://greenaddress.it/verify/?signature=%s&txhash=%s" % (urllib.parse.quote(sig), tx.txid()) + response = Network.send_http_on_proxy('get', url, headers = {'User-Agent': 'Electrum'}) response = response.json() # 3. display the result From 27caa683fe8447f20831a46395e6bd653edd150d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Dec 2018 08:27:03 +0100 Subject: [PATCH 261/301] kivy: show synchronization status in the balance field --- electrum/gui/kivy/main.kv | 2 +- electrum/gui/kivy/main_window.py | 26 +++++++++------------- electrum/gui/kivy/uix/ui_screens/status.kv | 2 +- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/electrum/gui/kivy/main.kv b/electrum/gui/kivy/main.kv index b4f05188..8e14f70d 100644 --- a/electrum/gui/kivy/main.kv +++ b/electrum/gui/kivy/main.kv @@ -435,7 +435,7 @@ BoxLayout: size_hint: 1, 1 bold: True color: 0.7, 0.7, 0.7, 1 - text: app.status + text: app.wallet_name font_size: '22dp' #minimum_width: '1dp' on_release: app.popup_dialog('status') diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 07c3ee67..04937ca5 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -193,8 +193,8 @@ class ElectrumWindow(App): self._trigger_update_status() self._trigger_update_history() + wallet_name = StringProperty(_('No Wallet')) base_unit = AliasProperty(_get_bu, _set_bu) - status = StringProperty('') fiat_unit = StringProperty('') def on_fiat_unit(self, a, b): @@ -308,9 +308,6 @@ class ElectrumWindow(App): self._password_dialog = None self.fee_status = self.electrum_config.get_fee_status() - def wallet_name(self): - return os.path.basename(self.wallet.storage.path) if self.wallet else ' ' - def on_pr(self, pr): if not self.wallet: self.show_error(_('No wallet loaded.')) @@ -547,8 +544,6 @@ class ElectrumWindow(App): else: self.load_wallet(wallet) else: - Logger.debug('Electrum: Wallet not found or action needed. Launching install wizard') - def launch_wizard(): storage = WalletStorage(path, manual_upgrades=True) wizard = Factory.InstallWizard(self.electrum_config, self.plugins, storage) @@ -559,7 +554,6 @@ class ElectrumWindow(App): launch_wizard() else: from .uix.dialogs.question import Question - def handle_answer(b: bool): if b: launch_wizard() @@ -705,6 +699,7 @@ class ElectrumWindow(App): if self.wallet: self.stop_wallet() self.wallet = wallet + self.wallet_name = wallet.basename() self.update_wallet() # Once GUI has been initialized check if we want to announce something # since the callback has been called before the GUI was initialized @@ -719,13 +714,12 @@ class ElectrumWindow(App): send_exception_to_crash_reporter(e) def update_status(self, *dt): - self.num_blocks = self.network.get_local_height() if not self.wallet: - self.status = _("No Wallet") return if self.network is None or not self.network.is_connected(): status = _("Offline") elif self.network.is_connected(): + self.num_blocks = self.network.get_local_height() server_height = self.network.get_server_height() server_lag = self.num_blocks - server_height if not self.wallet.up_to_date or server_height == 0: @@ -736,12 +730,14 @@ class ElectrumWindow(App): status = '' else: status = _("Disconnected") - self.status = self.wallet.basename() + (' [size=15dp](%s)[/size]'%status if status else '') - # balance - c, u, x = self.wallet.get_balance() - text = self.format_amount(c+x+u) - self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit - self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy + if status: + self.balance = status + self.fiat_balance = status + else: + c, u, x = self.wallet.get_balance() + text = self.format_amount(c+x+u) + self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit + self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy def get_max_amount(self): from electrum.transaction import TxOutput diff --git a/electrum/gui/kivy/uix/ui_screens/status.kv b/electrum/gui/kivy/uix/ui_screens/status.kv index 67b21bf6..77bfd761 100644 --- a/electrum/gui/kivy/uix/ui_screens/status.kv +++ b/electrum/gui/kivy/uix/ui_screens/status.kv @@ -25,7 +25,7 @@ Popup: spacing: '10dp' BoxLabel: text: _('Wallet Name') - value: app.wallet_name() + value: app.wallet_name BoxLabel: text: _("Wallet type:") value: app.wallet.wallet_type From 1e8b34e63e4d2b830237570207b847b161970924 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Dec 2018 08:57:31 +0100 Subject: [PATCH 262/301] rerun freeze_packages --- .../requirements-binaries.txt | 63 ++++---- .../deterministic-build/requirements-hw.txt | 104 +++++++------- contrib/deterministic-build/requirements.txt | 134 +++++++++--------- 3 files changed, 151 insertions(+), 150 deletions(-) diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt index ea3a1d3f..1c4ff76f 100644 --- a/contrib/deterministic-build/requirements-binaries.txt +++ b/contrib/deterministic-build/requirements-binaries.txt @@ -1,43 +1,42 @@ pip==18.1 \ --hash=sha256:7909d0a0932e88ea53a7014dfd14522ffef91a464daaaf5c573343852ef98550 \ --hash=sha256:c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1 -pycryptodomex==3.7.0 \ - --hash=sha256:02c358fa2445821d110857266e4e400f110054694636efe678dc60ba22a1aaef \ - --hash=sha256:09989c8a1b83e576d02ad77b9b019648648c569febca41f58fa04b9d9fdd1e8f \ - --hash=sha256:0f8fe28aec591d1b86af596c9fc5f75fc0204fb1026188a44e5e1b199780f1e5 \ - --hash=sha256:0fb58c2065030a5381f3c466aaa7c4de707901badad0d6a0575952bb10e6c35f \ - --hash=sha256:0fb9f3e6b28a280436afe9192a9957c7f56e20ceecb73f2d7db807368fdf3aaf \ - --hash=sha256:12ff38a68bbd743407018f9dd87d4cc21f9cb28fe2d8ba93beca004ada9a09ff \ - --hash=sha256:1650143106383bae79cbbda3701fd9979d0a624dba2ec2fa63f88cae29dd7174 \ - --hash=sha256:20a646cd0e690b07b7da619bc5b3ee1467243b2e32dfff579825c3ad5d7637ab \ - --hash=sha256:284779f0908682657adb8c60d8484174baa0d2620fb1df49183be6e2e06e73ce \ - --hash=sha256:2f3ce5bfe81d975c45e4a3cbe2bef15b809acc24f952f5f6aa67c2ae3c1a6808 \ - --hash=sha256:30ac12f0c9ac8332cc76832fea88a547b49ef60c31f74697ee2584f215723d4f \ - --hash=sha256:4f038b815d66dea0b1d4286515d96474204e137eb5d883229616781865902789 \ - --hash=sha256:57199a867b9991b1950f438b788e818338cee8ed8698e2eebdc5664521ad92a9 \ - --hash=sha256:5c5349385e9863e3bba6804899f4125c8335f66d090e892d6a5bb915f5c89d4c \ - --hash=sha256:5d546fac597b5007d5ff38c50c9031945057a6a6fa1ab7585058165d370ea202 \ - --hash=sha256:614eddfa0cf325e49b5b803fcb41c9334de79c4b18bf8de07e7737e1efc1d2b9 \ - --hash=sha256:82ae66244824d50b2b657c32e5912fde70a6e36f41e61f2869151f226204430d \ - --hash=sha256:96a733f3be325fb17c2ba79648e85ab446767af3dc3b396f1404b9397aa28fe5 \ - --hash=sha256:9c3834d27c1cff84e2a5c218e373d80ebbb3edca81912656f16137f7f97e58e0 \ - --hash=sha256:9f11823636128acbe4e17c35ff668f4d0a9f3133450753a0675525b6413aa1b0 \ - --hash=sha256:a3f9ad4e3f90f14707776f13b886fbac491ebe65d96a64f3ce0b378e167c3bbf \ - --hash=sha256:a89dee72a0f5024cc1cbaf85535eee8d14e891384513145d2f368b5c481dcd54 \ - --hash=sha256:ccadde651e712093052286ad9ee27f5aa5f657ca688a1bf6d5c41ade709467f3 \ - --hash=sha256:ced9ea10977dd52cb1b936a92119fc38fcdc5eaa4148f925ef22bbf0f0d4a5bd \ - --hash=sha256:eb0c6d3b91d55e3481158ecf77f3963c1725454fdcf5b79302c27c1c9c0d2c2a \ - --hash=sha256:f6714569a4039287972c672a8bd4b8d7dc78a601def8b31ffa39cd2fec00cb4b \ - --hash=sha256:fa4036582c8755259d4b8f4fe203ae534b7b187dcea143ab53a24e0f3931d547 \ - --hash=sha256:fb31bb0c8301e5a43d8d7aad22acabef65f28f7ab057eaeb2c21433309cc41e8 +pycryptodomex==3.7.2 \ + --hash=sha256:09f433a6ca4b96a4c89096af9eaa20c5d5e9029a03266f6a80062163d2042030 \ + --hash=sha256:0bb7686dd46d40cace053941638d2db4f9d86a50a62d6100f14157162e56e82f \ + --hash=sha256:13ef8e8a26a9ac7ae84616e9c500ca0cf721d3725d740124f68dda1b58e7d408 \ + --hash=sha256:195b6540f31cbe3dc7bf3877177bb4fe1a145ce02191efdd0cb6399bff275d4b \ + --hash=sha256:329ca5986c5e5f6a60edc3ff8b70c28d6cb259491e35e44870db6a8d92430e50 \ + --hash=sha256:42b2eccccb9da547e0ff140784037ea31c1a37012488706b51cbcadb885d2053 \ + --hash=sha256:4b2817bd02dd7dd36f223917f9bcc90d658bb66124bc596d7e3a0c250509acf7 \ + --hash=sha256:5b765870e17bf82a992f2ebb312207da76dbf8694ce865fee847005cce9244e4 \ + --hash=sha256:5d4e10ad9ff7940da534119ef92a500dcf7c28351d15e12d74ef0ce025e37d5b \ + --hash=sha256:75186284b593df724451869035bfab4a03b6b388ed6eea284ad7fe84c9b64ea0 \ + --hash=sha256:77928b02c28adb4d1d637c609aba380d890fb40ad63c58c826bef871c84cf488 \ + --hash=sha256:7cd21c1a4dd0879dc3b289afeb0033a9382da7d0a50cb9de8d0e59c1ab0977a7 \ + --hash=sha256:83b7fe71fb9d27ff12df9dd3f57ea84153a86f45a674ee2b4763aaaf8ca84988 \ + --hash=sha256:8a67f26365ddad6ae53c0b3800edce16d164e675ee773de31c110624e63736d8 \ + --hash=sha256:8e08238f8d146caabf9d80cca09dd31323a333f74f62323aabd075e7effbc45c \ + --hash=sha256:938af93204c93d1f576acdef970e95dfd0bbe40133f4bbf73c4b12b67f7bd4ed \ + --hash=sha256:b2f33943f91c764f2b27a1504cd3ec2c075deb7d1b8fcc3ab4d575bede7037a8 \ + --hash=sha256:b7efd0ac8b38dfdf8b2d387e84b007c843a66de9da437a950a86caa2d1437e1e \ + --hash=sha256:b95b14e5baaa4ca77ecb7a3d0f85fd13a8e31ada83c5a2bae5652d446414d63d \ + --hash=sha256:bad0c66c42d16e81eef2199b3c4fb78c873f54c7f9d156e69664e2138155747c \ + --hash=sha256:c384a85bede5396eed50f6c0374d8c51490fc10fdc65dffc8e613b57e5ab0ee0 \ + --hash=sha256:c58a324a8eb48f767abe8c906295d76e04330aa0a03d99dd58d9a42938ee0596 \ + --hash=sha256:d498731d66f9b9ad978c9a8018b0c6cfc4081e202a79c6e283ade7dcf2a72c18 \ + --hash=sha256:d516746fc25daff1c5e84d3f816d134d75a10e3551ffac92aa957a75b6b0208f \ + --hash=sha256:ec51ea9f11b11df2719a01ea1cacdcf80858542a93f530a25a3bc742d0fe2f4b \ + --hash=sha256:ef2792281138a29e54bdd7302fdab72be140192485dc622bb9e7e9a6f9cee4f9 \ + --hash=sha256:f79650ec8812b01f20aca503763b93b0b1b347423ecf9fd3a9ebb611bee84079 PyQt5==5.10.1 \ --hash=sha256:1e652910bd1ffd23a3a48c510ecad23a57a853ed26b782cd54b16658e6f271ac \ --hash=sha256:4db7113f464c733a99fcb66c4c093a47cf7204ad3f8b3bda502efcc0839ac14b \ --hash=sha256:9c17ab3974c1fc7bbb04cc1c9dae780522c0ebc158613f3025fccae82227b5f7 \ --hash=sha256:f6035baa009acf45e5f460cf88f73580ad5dc0e72330029acd99e477f20a5d61 -setuptools==40.6.2 \ - --hash=sha256:86bb4d8e1b0fabad1f4642b64c335b673e53e7a381de03c9a89fe678152c4c64 \ - --hash=sha256:88ee6bcd5decec9bd902252e02e641851d785c6e5e75677d2744a9d13fed0b0a +setuptools==40.6.3 \ + --hash=sha256:3b474dad69c49f0d2d86696b68105f3a6f195f7ab655af12ef9a9c326d2b08f8 \ + --hash=sha256:e2c1ce9a832f34cf7a31ed010aabcab5008eb65ce8f2aadc04622232c14bdd0b SIP==4.19.8 \ --hash=sha256:09f9a4e6c28afd0bafedb26ffba43375b97fe7207bd1a0d3513f79b7d168b331 \ --hash=sha256:105edaaa1c8aa486662226360bd3999b4b89dd56de3e314d82b83ed0587d8783 \ diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index e61cb344..44125520 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -1,8 +1,8 @@ btchip-python==0.1.28 \ --hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83 -certifi==2018.10.15 \ - --hash=sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c \ - --hash=sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a +certifi==2018.11.29 \ + --hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \ + --hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033 chardet==3.0.4 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 @@ -12,35 +12,37 @@ ckcc-protocol==0.7.2 \ click==7.0 \ --hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \ --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 -Cython==0.29 \ - --hash=sha256:019008a69e6b7c102f2ed3d733a288d1784363802b437dd2b91e6256b12746da \ - --hash=sha256:1441fe19c56c90b8c2159d7b861c31a134d543ef7886fd82a5d267f9f11f35ac \ - --hash=sha256:1d1a5e9d6ed415e75a676b72200ad67082242ec4d2d76eb7446da255ae72d3f7 \ - --hash=sha256:339f5b985de3662b1d6c69991ab46fdbdc736feb4ac903ef6b8c00e14d87f4d8 \ - --hash=sha256:35bdf3f48535891fee2eaade70e91d5b2cc1ee9fc2a551847c7ec18bce55a92c \ - --hash=sha256:3d0afba0aec878639608f013045697fb0969ff60b3aea2daec771ea8d01ad112 \ - --hash=sha256:42c53786806e24569571a7a24ebe78ec6b364fe53e79a3f27eddd573cacd398f \ - --hash=sha256:48b919da89614d201e72fbd8247b5ae8881e296cf968feb5595a015a14c67f1f \ - --hash=sha256:49906e008eeb91912654a36c200566392bd448b87a529086694053a280f8af2d \ - --hash=sha256:49fc01a7c9c4e3c1784e9a15d162c2cac3990fcc28728227a6f8f0837aabda7c \ - --hash=sha256:501b671b639b9ca17ad303f8807deb1d0ff754d1dab106f2607d14b53cb0ff0b \ - --hash=sha256:5574574142364804423ab4428bd331a05c65f7ecfd31ac97c936f0c720fe6a53 \ - --hash=sha256:6092239a772b3c6604be9e94b9ab4f0dacb7452e8ad299fd97eae0611355b679 \ - --hash=sha256:71ff5c7632501c4f60edb8a24fd0a772e04c5bdca2856d978d04271b63666ef7 \ - --hash=sha256:7dcf2ad14e25b05eda8bdd104f8c03a642a384aeefd25a5b51deac0826e646fa \ - --hash=sha256:8ca3a99f5a7443a6a8f83a5d8fcc11854b44e6907e92ba8640d8a8f7b9085e21 \ - --hash=sha256:927da3b5710fb705aab173ad630b45a4a04c78e63dcd89411a065b2fe60e4770 \ - --hash=sha256:94916d1ede67682638d3cc0feb10648ff14dc51fb7a7f147f4fedce78eaaea97 \ - --hash=sha256:a3e5e5ca325527d312cdb12a4dab8b0459c458cad1c738c6f019d0d8d147081c \ - --hash=sha256:a7716a98f0b9b8f61ddb2bae7997daf546ac8fc594be6ba397f4bde7d76bfc62 \ - --hash=sha256:acf10d1054de92af8d5bfc6620bb79b85f04c98214b4da7db77525bfa9fc2a89 \ - --hash=sha256:de46ffb67e723975f5acab101c5235747af1e84fbbc89bf3533e2ea93fb26947 \ - --hash=sha256:df428969154a9a4cd9748c7e6efd18432111fbea3d700f7376046c38c5e27081 \ - --hash=sha256:f5ebf24b599caf466f9da8c4115398d663b2567b89e92f58a835e9da4f74669f \ - --hash=sha256:f79e45d5c122c4fb1fd54029bf1d475cecc05f4ed5b68136b0d6ec268bae68b6 \ - --hash=sha256:f7a43097d143bd7846ffba6d2d8cd1cc97f233318dbd0f50a235ea01297a096b \ - --hash=sha256:fceb8271bc2fd3477094ca157c824e8ea840a7b393e89e766eea9a3b9ce7e0c6 \ - --hash=sha256:ff919ceb40259f5332db43803aa6c22ff487e86036ce3921ae04b9185efc99a4 +construct==2.9.45 \ + --hash=sha256:2271a0efd0798679dea825ff47e22a4c550456a5db0ba8baa82f7eae0af0118c +Cython==0.29.1 \ + --hash=sha256:0202f753b0a69dd87095b698df00010daf452ab61279747248a042a24892a2a9 \ + --hash=sha256:0fbe9514ffe35aad337db27b11f7ee1bf27d01059b2e27f112315b185d69de79 \ + --hash=sha256:18ab7646985a97e02cee72e1ddba2e732d4931d4e1732494ff30c5aa084bfb97 \ + --hash=sha256:18bb95daa41fd2ff0102844172bc068150bf031186249fc70c6f57fc75c9c0a9 \ + --hash=sha256:222c65c7022ff52faf3ac6c706e4e8a726ddaa29dabf2173b2a0fdfc1a2f1586 \ + --hash=sha256:2387c5a2a436669de9157d117fd426dfc2b46ffdc49e43f0a2267380896c04ea \ + --hash=sha256:31bad130b701587ab7e74c3c304bb3d63d9f0d365e3f81880203e8e476d914b1 \ + --hash=sha256:3895014b1a653726a9da5aca852d9e6d0e2c2667bf315d6a2cd632bf7463130b \ + --hash=sha256:3d38967ef9c1c0ffabe80827f56817609153e2da83e3dce84476d0928c72972c \ + --hash=sha256:5478efd92291084adc9b679666aeaeaafca69d6bf3e95fe3efce82814e3ab782 \ + --hash=sha256:5c2a6121e4e1e65690b60c270012218e38201bcf700314b1926d5dbeae78a499 \ + --hash=sha256:5f66f7f76fc870500fe6db0c02d5fc4187062d29e582431f5a986881c5aef4e3 \ + --hash=sha256:6572d74990b16480608441b941c1cefd60bf742416bc3668cf311980f740768d \ + --hash=sha256:6990b9965f31762ac71340869c064f39fb6776beca396d0558d3b5b1ebb7f027 \ + --hash=sha256:87c82803f9c51c275b16c729aade952ca93c74a8aec963b9b8871df9bbb3120a \ + --hash=sha256:8fd32974024052b2260d08b94f970c4c1d92c327ed3570a2b4708070fa53a879 \ + --hash=sha256:9a81bba33c7fbdb76e6fe8d15b6e793a1916afd4d2463f07d762c69efaaea466 \ + --hash=sha256:9c31cb9bfaa1004a2a50115a37e1fcb79d664917968399dae3e04610356afe8c \ + --hash=sha256:a0b28235c28a088e052f90a0b5fefaa503e5378046a29d0af045e2ec9d5d6555 \ + --hash=sha256:a3f5022d818b6c91a8bbc466211e6fd708f234909cbb10bc4dbccb2a04884ef6 \ + --hash=sha256:a7252ca498f510404185e3c1bdda3224e80b1be1a5fbc2b174aab83a477ea0cb \ + --hash=sha256:aa8d7136cad8b2a7bf3596e1bc053476edeee567271f197449b2d30ea0c37175 \ + --hash=sha256:b50a8de6f2820286129fe7d71d76c9e0c0f53a8c83cf39bbe6375b827994e4f1 \ + --hash=sha256:b528a9c152c569062375d5c2260b59f8243bb4136fc38420854ac1bd4aa0d02f \ + --hash=sha256:b72db7201a4aa0445f27af9954d48ed7d2c119ce3b8b253e4dcd514fc72e5dc6 \ + --hash=sha256:d3444e10ccb5b16e4c1bed3cb3c565ec676b20a21eb43430e70ec4168c631dcc \ + --hash=sha256:e16d6f06f4d2161347e51c4bc1f7a8feedeee444d26efa92243f18441a6fa742 \ + --hash=sha256:f5774bef92d33a62a584f6e7552a9a8653241ecc036e259bfb03d33091599537 ecdsa==0.13 \ --hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \ --hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa @@ -56,13 +58,13 @@ hidapi==0.7.99.post21 \ --hash=sha256:d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b \ --hash=sha256:e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97 \ --hash=sha256:edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922 -idna==2.7 \ - --hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \ - --hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16 +idna==2.8 \ + --hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \ + --hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c keepkey==4.0.2 \ --hash=sha256:cddee60ae405841cdff789cbc54168ceaeb2282633420f2be155554c25c69138 -libusb1==1.6.6 \ - --hash=sha256:a49917a2262cf7134396f6720c8be011f14aabfc5cdc53f880cc672c0f39d271 +libusb1==1.7 \ + --hash=sha256:9d4f66d2ed699986b06bc3082cd262101cb26af7a76a34bd15b7eb56cba37e0f mnemonic==0.18 \ --hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d pbkdf2==1.3 \ @@ -100,21 +102,25 @@ pyblake2==1.1.2 \ --hash=sha256:baa2190bfe549e36163aa44664d4ee3a9080b236fc5d42f50dc6fd36bbdc749e \ --hash=sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4 \ --hash=sha256:fbc9fcde75713930bc2a91b149e97be2401f7c9c56d735b46a109210f58d7358 -requests==2.20.1 \ - --hash=sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54 \ - --hash=sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263 +requests==2.21.0 \ + --hash=sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e \ + --hash=sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b safet==0.1.4 \ --hash=sha256:522c257910f9472e9c77c487425ed286f6721c314653e232bc41c6cedece1bb1 \ --hash=sha256:b152874acdc89ff0c8b2d680bfbf020b3e53527c2ad3404489dd61a548aa56a1 -setuptools==40.6.2 \ - --hash=sha256:86bb4d8e1b0fabad1f4642b64c335b673e53e7a381de03c9a89fe678152c4c64 \ - --hash=sha256:88ee6bcd5decec9bd902252e02e641851d785c6e5e75677d2744a9d13fed0b0a -six==1.11.0 \ - --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \ - --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb -trezor==0.10.2 \ - --hash=sha256:4dba4d5c53d3ca22884d79fb4aa68905fb8353a5da5f96c734645d8cf537138d \ - --hash=sha256:d2b32f25982ab403758d870df1d0de86d0751c106ef1cd1289f452880ce68b84 +setuptools==40.6.3 \ + --hash=sha256:3b474dad69c49f0d2d86696b68105f3a6f195f7ab655af12ef9a9c326d2b08f8 \ + --hash=sha256:e2c1ce9a832f34cf7a31ed010aabcab5008eb65ce8f2aadc04622232c14bdd0b +six==1.12.0 \ + --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ + --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 +trezor==0.11.0 \ + --hash=sha256:1132f6a97afb0979c5018b067494bc8917b881c02d965f991270a70543b5050c \ + --hash=sha256:ce8f6ff2502b530d0cd3c5aa4b59330a56abbc6046a34f22c7eb0b2713b4f09d +typing-extensions==3.6.6 \ + --hash=sha256:2a6c6e78e291a4b6cbd0bbfd30edc0baaa366de962129506ec8fe06bdec66457 \ + --hash=sha256:51e7b7f3dcabf9ad22eed61490f3b8d23d9922af400fe6656cb08e66656b701f \ + --hash=sha256:55401f6ed58ade5638eb566615c150ba13624e2f0c1eedd080fc3c1b6cb76f1d urllib3==1.24.1 \ --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \ --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 diff --git a/contrib/deterministic-build/requirements.txt b/contrib/deterministic-build/requirements.txt index 99fb37e9..e23c357a 100644 --- a/contrib/deterministic-build/requirements.txt +++ b/contrib/deterministic-build/requirements.txt @@ -21,8 +21,8 @@ aiohttp==3.4.4 \ --hash=sha256:e0c9c8d4150ae904f308ff27b35446990d2b1dfc944702a21925937e937394c6 \ --hash=sha256:f1839db4c2b08a9c8f9788112644f8a8557e8e0ecc77b07091afabb941dc55d0 \ --hash=sha256:f3df52362be39908f9c028a65490fae0475e4898b43a03d8aa29d1e765b45e07 -aiohttp_socks==0.2 \ - --hash=sha256:eba0a6e198d9a69d254bf956d68cec7615c2a4cadd861b8da46464bd13c5641d +aiohttp-socks==0.2.1 \ + --hash=sha256:8bcfde1bb0d394b0ff0d8c284de7459c38507bff1f7c144ac734a6de49f36a29 aiorpcX==0.10.1 \ --hash=sha256:0c0a3342a43d939f00af84684fd08c0c5e7de4fa3eb21740063bea98f6070798 \ --hash=sha256:58fe42b3695bc4e761b61b9a61416b0c6d69b220630be222b9b129b96ac9c331 @@ -32,56 +32,56 @@ async_timeout==3.0.1 \ attrs==18.2.0 \ --hash=sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69 \ --hash=sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb -certifi==2018.10.15 \ - --hash=sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c \ - --hash=sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a +certifi==2018.11.29 \ + --hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \ + --hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033 chardet==3.0.4 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 -dnspython==1.15.0 \ - --hash=sha256:40f563e1f7a7b80dc5a4e76ad75c23da53d62f1e15e6e517293b04e1f84ead7c \ - --hash=sha256:861e6e58faa730f9845aaaa9c6c832851fbf89382ac52915a51f89c71accdd31 +dnspython==1.16.0 \ + --hash=sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01 \ + --hash=sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d ecdsa==0.13 \ --hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \ --hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa -idna==2.7 \ - --hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \ - --hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16 +idna==2.8 \ + --hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \ + --hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c idna_ssl==1.1.0 \ --hash=sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c jsonrpclib-pelix==0.3.2 \ --hash=sha256:14d288d1b3d3273cf96a729dd21a2470851c4962be8509f3dd62f0137ff90339 \ --hash=sha256:27fcd919d3dbf6179bcce587f73e1bad006922ae23c83c308e01227b8533178c -multidict==4.5.1 \ - --hash=sha256:013eb6591ab95173fd3deb7667d80951abac80100335b3e97b5fa778c1bb4b91 \ - --hash=sha256:0bffbbbb48db35f57dfb4733e943ac8178efb31aab5601cb7b303ee228ce96af \ - --hash=sha256:1a34aab1dfba492407c757532f665ba3282ec4a40b0d2f678bda828ef422ebb7 \ - --hash=sha256:1b4b46a33f459a2951b0fd26c2d80639810631eb99b3d846d298b02d28a3e31d \ - --hash=sha256:1d616d80c37a388891bf760d64bc50cac7c61dbb7d7013f2373aa4b44936e9f0 \ - --hash=sha256:225aefa7befbe05bd0116ef87e8cd76cbf4ac39457a66faf7fb5f3c2d7bea19a \ - --hash=sha256:2c9b28985ef7c830d5c7ea344d068bcdee22f8b6c251369dea98c3a814713d44 \ - --hash=sha256:39e0600f8dd72acb011d09960da560ba3451b1eca8de5557c15705afc9d35f0e \ - --hash=sha256:3c642c40ea1ca074397698446893a45cd6059d5d071fc3ba3915c430c125320f \ - --hash=sha256:42357c90b488fac38852bcd7b31dcd36b1e2325413960304c28b8d98e6ff5fd4 \ - --hash=sha256:6ac668f27dbdf8a69c31252f501e128a69a60b43a44e43d712fb58ce3e5dfcca \ - --hash=sha256:713683da2e3f1dd81a920c995df5dda51f1fff2b3995f5864c3ee782fcdcb96c \ - --hash=sha256:73b6e7853b6d3bc0eac795044e700467631dff37a5a33d3230122b03076ac2f9 \ - --hash=sha256:77534c1b9f4a5d0962392cad3f668d1a04036b807618e3357eb2c50d8b05f7f7 \ - --hash=sha256:77b579ef57e27457064bb6bb4c8e5ede866af071af60fe3576226136048c6dfa \ - --hash=sha256:82cf28f18c935d66c15a6f82fda766a4138d21e78532a1946b8ec603019ba0b8 \ - --hash=sha256:937e8f12f9edc0d2e351c09fc3e7335a65eefb75406339d488ee46ef241f75d8 \ - --hash=sha256:985dbf59e92f475573a04598f9a00f92b4fdb64fc41f1df2ea6f33b689319537 \ - --hash=sha256:9c4fab7599ba8c0dbf829272c48c519625c2b7f5630b49925802f1af3a77f1f4 \ - --hash=sha256:9e8772be8455b49a85ad6dbf6ce433da7856ba481d6db36f53507ae540823b15 \ - --hash=sha256:a06d6d88ce3be4b54deabd078810e3c077a8b2e20f0ce541c979b5dd49337031 \ - --hash=sha256:a1da0cdc3bc45315d313af976dab900888dbb477d812997ee0e6e4ea43d325e5 \ - --hash=sha256:a6652466a4800e9fde04bf0252e914fff5f05e2a40ee1453db898149624dfe04 \ - --hash=sha256:a7f23523ea6a01f77e0c6da8aae37ab7943e35630a8d2eda7e49502f36b51b46 \ - --hash=sha256:a87429da49f4c9fb37a6a171fa38b59a99efdeabffb34b4255a7a849ffd74a20 \ - --hash=sha256:c26bb81d0d19619367a96593a097baec2d5a7b3a0cfd1e3a9470277505a465c2 \ - --hash=sha256:d4f4545edb4987f00fde44241cef436bf6471aaac7d21c6bbd497cca6049f613 \ - --hash=sha256:daabc2766a2b76b3bec2086954c48d5f215f75a335eaee1e89c8357922a3c4d5 \ - --hash=sha256:f08c1dcac70b558183b3b755b92f1135a76fd1caa04009b89ddea57a815599aa +multidict==4.5.2 \ + --hash=sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f \ + --hash=sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3 \ + --hash=sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef \ + --hash=sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b \ + --hash=sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73 \ + --hash=sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc \ + --hash=sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3 \ + --hash=sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd \ + --hash=sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351 \ + --hash=sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941 \ + --hash=sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d \ + --hash=sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1 \ + --hash=sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b \ + --hash=sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a \ + --hash=sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3 \ + --hash=sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7 \ + --hash=sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0 \ + --hash=sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0 \ + --hash=sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014 \ + --hash=sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5 \ + --hash=sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036 \ + --hash=sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d \ + --hash=sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a \ + --hash=sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce \ + --hash=sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1 \ + --hash=sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a \ + --hash=sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9 \ + --hash=sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7 \ + --hash=sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b pip==18.1 \ --hash=sha256:7909d0a0932e88ea53a7014dfd14522ffef91a464daaaf5c573343852ef98550 \ --hash=sha256:c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1 @@ -105,37 +105,33 @@ protobuf==3.6.1 \ --hash=sha256:fcfc907746ec22716f05ea96b7f41597dfe1a1c088f861efb8a0d4f4196a6f10 pyaes==1.6.1 \ --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f -QDarkStyle==2.6.4 \ - --hash=sha256:99a21e27405850b4e49610bb7f1720e7f756a9e7b461a4ee54cb6b35cfed3b15 \ - --hash=sha256:e16eae2c3d448b7e0dd13e24b26183bbaae9b1e8fcb2c819c858a3d4bd4caf44 +QDarkStyle==2.5.4 \ + --hash=sha256:3eb60922b8c4d9cedecb6897ca4c9f8a259d81bdefe5791976ccdf12432de1f0 \ + --hash=sha256:51331fc6490b38c376e6ba8d8c814320c8d2d1c2663055bc396321a7c28fa8be qrcode==6.0 \ --hash=sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf \ --hash=sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3 -requests==2.20.1 \ - --hash=sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54 \ - --hash=sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263 -setuptools==40.6.2 \ - --hash=sha256:86bb4d8e1b0fabad1f4642b64c335b673e53e7a381de03c9a89fe678152c4c64 \ - --hash=sha256:88ee6bcd5decec9bd902252e02e641851d785c6e5e75677d2744a9d13fed0b0a -six==1.11.0 \ - --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \ - --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb -urllib3==1.24.1 \ - --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \ - --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 +setuptools==40.6.3 \ + --hash=sha256:3b474dad69c49f0d2d86696b68105f3a6f195f7ab655af12ef9a9c326d2b08f8 \ + --hash=sha256:e2c1ce9a832f34cf7a31ed010aabcab5008eb65ce8f2aadc04622232c14bdd0b +six==1.12.0 \ + --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ + --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 wheel==0.32.3 \ --hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \ --hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44 -yarl==1.2.6 \ - --hash=sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9 \ - --hash=sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee \ - --hash=sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308 \ - --hash=sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357 \ - --hash=sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78 \ - --hash=sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8 \ - --hash=sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1 \ - --hash=sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4 \ - --hash=sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7 -colorama==0.4.0 \ - --hash=sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3 \ - --hash=sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c +yarl==1.3.0 \ + --hash=sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9 \ + --hash=sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f \ + --hash=sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb \ + --hash=sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320 \ + --hash=sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842 \ + --hash=sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0 \ + --hash=sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829 \ + --hash=sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310 \ + --hash=sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4 \ + --hash=sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8 \ + --hash=sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1 +colorama==0.4.1 \ + --hash=sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d \ + --hash=sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48 From 664b0c234eca5afec9a7de9c7533cbc41227af14 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Dec 2018 22:50:25 +0100 Subject: [PATCH 263/301] wizard: fix imported address wallets assertion added in 9350709f13bc7e3d79b8e0f1515a3fdba4f2cbff was failing --- electrum/base_wizard.py | 2 +- electrum/commands.py | 2 +- electrum/wallet.py | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 838e6c4a..da9cf9e3 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -194,7 +194,7 @@ class BaseWizard(object): if keystore.is_address_list(text): w = Imported_Wallet(self.storage) addresses = text.split() - good_inputs, bad_inputs = w.import_addresses(addresses) + good_inputs, bad_inputs = w.import_addresses(addresses, write_to_disk=False) elif keystore.is_private_key_list(text): k = keystore.Imported_KeyStore({}) self.storage.put('keystore', k.dump()) diff --git a/electrum/commands.py b/electrum/commands.py index 41ec9e3d..1ac6b306 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -167,7 +167,7 @@ class Commands: if keystore.is_address_list(text): wallet = Imported_Wallet(storage) addresses = text.split() - good_inputs, bad_inputs = wallet.import_addresses(addresses) + good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False) # FIXME tell user about bad_inputs if not good_inputs: raise Exception("None of the given addresses can be imported") diff --git a/electrum/wallet.py b/electrum/wallet.py index 42e0ae9e..6ac10929 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1329,7 +1329,8 @@ class Imported_Wallet(Simple_Wallet): def get_change_addresses(self): return [] - def import_addresses(self, addresses: List[str]) -> Tuple[List[str], List[Tuple[str, str]]]: + def import_addresses(self, addresses: List[str], *, + write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]: good_addr = [] # type: List[str] bad_addr = [] # type: List[Tuple[str, str]] for address in addresses: @@ -1343,7 +1344,7 @@ class Imported_Wallet(Simple_Wallet): self.addresses[address] = {} self.add_address(address) self.save_addresses() - self.save_transactions(write=True) + self.save_transactions(write=write_to_disk) return good_addr, bad_addr def import_address(self, address: str) -> str: @@ -1408,7 +1409,7 @@ class Imported_Wallet(Simple_Wallet): def get_public_key(self, address): return self.addresses[address].get('pubkey') - def import_private_keys(self, keys: List[str], password: Optional[str], + def import_private_keys(self, keys: List[str], password: Optional[str], *, write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]: good_addr = [] # type: List[str] bad_keys = [] # type: List[Tuple[str, str]] From 0657bb1b36f894a789762904035ad58a8110b6e9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Dec 2018 23:01:52 +0100 Subject: [PATCH 264/301] test_wallet_vertical: add segwit 2fa test --- electrum/tests/test_wallet_vertical.py | 37 +++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 87213318..2e5b7556 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -157,7 +157,7 @@ class TestWalletKeystoreAddressIntegrityForMainnet(SequentialTestCase): @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_2fa(self, mock_write): + def test_electrum_seed_2fa_legacy(self, mock_write): seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove' self.assertEqual(bitcoin.seed_type(seed_words), '2fa') @@ -190,6 +190,41 @@ class TestWalletKeystoreAddressIntegrityForMainnet(SequentialTestCase): self.assertEqual(w.get_receiving_addresses()[0], '35L8XmCDoEBKeaWRjvmZvoZvhp8BXMMMPV') self.assertEqual(w.get_change_addresses()[0], '3PeZEcumRqHSPNN43hd4yskGEBdzXgY8Cy') + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_electrum_seed_2fa_segwit(self, mock_write): + seed_words = 'universe topic remind silver february ranch shine worth innocent cattle enhance wise' + self.assertEqual(bitcoin.seed_type(seed_words), '2fa_segwit') + + xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '') + + ks1 = keystore.from_xprv(xprv1) + self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore)) + self.assertEqual(ks1.xprv, 'ZprvAm1R3RZMrkSLYKZer8QECGoc8oA1RQuKfsztHkBTmi2yF8RhmN1JRb7Ag69mMrL88sP67WiaegaSSDnKndorWEpFr7a5B2QgrD7TkERSYX6') + self.assertEqual(ks1.xpub, 'Zpub6yzmSw6Fh7zdkoe7x9wEZQkLgpzVpsdB36vV68b5L3Zx7vkrJuKYyPReXMSjBegmtUjFBxP2uZEdL87cYvtTtGaVuwtRRCTSFUsoAdKZMge') + self.assertEqual(ks1.xpub, xpub1) + + ks2 = keystore.from_xprv(xprv2) + self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore)) + self.assertEqual(ks2.xprv, 'ZprvAm1R3RZMrkSLab4jVKTwuroBgKEfnsmK9CQa1ErkuRzpsPauYuv9z2UzhDNn9YgbLHcmXpmxbNq4MdDRAUM5B2N9Wr3Uq9yp2c4AtTJDFdi') + self.assertEqual(ks2.xpub, 'Zpub6yzmSw6Fh7zdo59CbLzxGzjvEM5ACLVAWRLAodGNTmXokBv46TEQXpoUYUaoxPCeynysxg7APfScikCQ2jhCfM3NcNEk46BCVfSSrdrSkbR') + self.assertEqual(ks2.xpub, xpub2) + + long_user_id, short_id = trustedcoin.get_user_id( + {'x1/': {'xpub': xpub1}, + 'x2/': {'xpub': xpub2}}) + xtype = bip32.xpub_type(xpub1) + xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id) + ks3 = keystore.from_xpub(xpub3) + WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks3) + self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore)) + + w = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3') + self.assertEqual(w.txin_type, 'p2wsh') + + self.assertEqual(w.get_receiving_addresses()[0], 'bc1qpmufh0zjp5prfsrk2yskcy82sa26srqkd97j0457andc6m0gh5asw7kqd2') + self.assertEqual(w.get_change_addresses()[0], 'bc1qd4q50nft7kxm9yglfnpup9ed2ukj3tkxp793y0zya8dc9m39jcwq308dxz') + @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') def test_bip39_seed_bip44_standard(self, mock_write): From b41a83cedac03ba0d7a4c8fa0e824b5288298957 Mon Sep 17 00:00:00 2001 From: tiagotrs Date: Mon, 10 Dec 2018 22:05:39 +0100 Subject: [PATCH 265/301] new hook/interface ref #4540 --- electrum/gui/qt/main_window.py | 1 + electrum/gui/qt/seed_dialog.py | 4 - electrum/plugins/revealer/qt.py | 156 ++++++++++++++++---------------- 3 files changed, 81 insertions(+), 80 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 0db7fa05..31d67a80 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2014,6 +2014,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.status_button = StatusBarButton(QIcon(":icons/status_disconnected.png"), _("Network"), lambda: self.gui_object.show_network_dialog(self)) sb.addPermanentWidget(self.status_button) run_hook('create_status_bar', sb) + run_hook('revealer_hook', sb) self.setStatusBar(sb) def update_lock_icon(self): diff --git a/electrum/gui/qt/seed_dialog.py b/electrum/gui/qt/seed_dialog.py index 86fc15ec..bca282b0 100644 --- a/electrum/gui/qt/seed_dialog.py +++ b/electrum/gui/qt/seed_dialog.py @@ -26,8 +26,6 @@ from electrum.i18n import _ from electrum.mnemonic import Mnemonic import electrum.old_mnemonic -from electrum.plugin import run_hook - from .util import * from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit @@ -207,6 +205,4 @@ class SeedDialog(WindowModalDialog): title = _("Your wallet generation seed is:") slayout = SeedLayout(title=title, seed=seed, msg=True, passphrase=passphrase) vbox.addLayout(slayout) - has_extension = True if passphrase else False - run_hook('set_seed', seed, has_extension, slayout.seed_e) vbox.addLayout(Buttons(CloseButton(self))) diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index d64dc585..06f817dd 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -1,19 +1,12 @@ ''' Revealer -So you have something to hide? - -plug-in for the electrum wallet. - -Features: - - Deep Cold multi-factor backup solution - - Safety - One time pad security - - Redundancy - Trustless printing & distribution - - Encrypt your seedphrase or any secret you want for your revealer - - Based on crypto by legendary cryptographers Naor and Shamir +Do you have something to hide? +Secret backup plug-in for the electrum wallet. Tiago Romagnani Silveira, 2017 + ''' import os @@ -31,6 +24,7 @@ from electrum.i18n import _ from electrum.util import to_bytes, make_dir from electrum.gui.qt.util import * from electrum.gui.qt.qrtextedit import ScanQRTextEdit +from electrum.gui.qt.main_window import StatusBarButton from .hmac_drbg import DRBG @@ -58,10 +52,8 @@ class Plugin(BasePlugin): make_dir(self.base_dir) @hook - def set_seed(self, seed, has_extension, parent): - self.cseed = seed.upper() - self.has_extension = has_extension - parent.addButton(':icons/revealer.png', partial(self.setup_dialog, parent), "Revealer"+_(" secret backup utility")) + def revealer_hook(self, parent): + parent.addPermanentWidget(StatusBarButton(QIcon(':icons/revealer.png'), "Revealer"+_(" secret backup utility"), partial(self.setup_dialog, parent))) def requires_settings(self): return True @@ -69,39 +61,70 @@ class Plugin(BasePlugin): def settings_widget(self, window): return EnterButton(_('Printer Calibration'), partial(self.calibration_dialog, window)) + def password_dialog(self, msg=None, parent=None): + from electrum.gui.qt.password_dialog import PasswordDialog + parent = parent or self + d = PasswordDialog(parent, msg) + return d.run() + + def get_seed(self): + password = None + if self.wallet.has_keystore_encryption(): + password = self.password_dialog(parent=self.d.parent()) + if not password: + return + + keystore = self.wallet.get_keystore() + try: + self.cseed = keystore.get_seed(password) + if keystore.get_passphrase(password): + self.extension = True + except Exception: + traceback.print_exc(file=sys.stdout) + return + def setup_dialog(self, window): - self.update_wallet_name(window.parent().parent().wallet) + self.wallet = window.parent().wallet + self.update_wallet_name(self.wallet) self.user_input = False self.noise_seed = False - self.d = WindowModalDialog(window, "Revealer") - self.d.setMinimumWidth(420) - vbox = QVBoxLayout(self.d) - vbox.addSpacing(21) - logo = QLabel() - vbox.addWidget(logo) - logo.setPixmap(QPixmap(':icons/revealer.png')) - logo.setAlignment(Qt.AlignCenter) - vbox.addSpacing(42) + self.d = WindowModalDialog(window, "Setup Dialog") + self.d.setMinimumWidth(500) + self.d.setMinimumHeight(210) + self.d.setMaximumHeight(320) + self.d.setContentsMargins(11,11,1,1) + + self.hbox = QHBoxLayout(self.d) + vbox = QVBoxLayout() + logo = QLabel() + self.hbox.addWidget(logo) + logo.setPixmap(QPixmap(':icons/revealer.png')) + logo.setAlignment(Qt.AlignLeft) + self.hbox.addSpacing(16) + vbox.addWidget(WWLabel(""+_("Revealer Secret Backup Plugin")+"
" + +_("To encrypt your backup, first we need to load some noise.")+"
")) + vbox.addSpacing(7) + bcreate = QPushButton(_("Create a new Revealer")) + bcreate.setMaximumWidth(181) + bcreate.setDefault(True) + vbox.addWidget(bcreate, Qt.AlignCenter) self.load_noise = ScanQRTextEdit() self.load_noise.setTabChangesFocus(True) self.load_noise.textChanged.connect(self.on_edit) self.load_noise.setMaximumHeight(33) - - vbox.addWidget(WWLabel(""+_("Enter your physical revealer code:")+"")) + self.hbox.addLayout(vbox) + vbox.addWidget(WWLabel(_("or type a existing revealer code below and click 'next':"))) vbox.addWidget(self.load_noise) - vbox.addSpacing(11) - + vbox.addSpacing(3) self.next_button = QPushButton(_("Next"), self.d) - self.next_button.setDefault(True) self.next_button.setEnabled(False) vbox.addLayout(Buttons(self.next_button)) self.next_button.clicked.connect(self.d.close) self.next_button.clicked.connect(partial(self.cypherseed_dialog, window)) - vbox.addSpacing(21) - - vbox.addWidget(WWLabel(_("or, alternatively: "))) - bcreate = QPushButton(_("Create a digital Revealer")) + vbox.addWidget( + QLabel("" + _("Warning") + ": " + _("Each revealer should be used only once.") + +"
"+_("more info at https://revealer.cc/faq"))) def mk_digital(): try: @@ -112,15 +135,6 @@ class Plugin(BasePlugin): self.cypherseed_dialog(window) bcreate.clicked.connect(mk_digital) - - vbox.addWidget(bcreate) - vbox.addSpacing(11) - vbox.addWidget(QLabel(''.join([ ""+_("WARNING")+ ":" + _("Printing a revealer and encrypted seed"), '
', - _("on the same printer is not trustless towards the printer."), '
', - ]))) - vbox.addSpacing(11) - vbox.addLayout(Buttons(CloseButton(self.d))) - return bool(self.d.exec_()) def get_noise(self): @@ -174,7 +188,7 @@ class Plugin(BasePlugin): dialog.close() def ext_warning(self, dialog): - dialog.show_message(''.join(["",_("Warning: "), "", _("your seed extension will not be included in the encrypted backup.")])) + dialog.show_message(''.join(["",_("Warning: "), "", _("your seed extension will ")+""+_("not")+""+_(" be included in the encrypted backup.")])) dialog.close() def bdone(self, dialog): @@ -198,57 +212,49 @@ class Plugin(BasePlugin): def cypherseed_dialog(self, window): - d = WindowModalDialog(window, "Revealer") - d.setMinimumWidth(420) - + d = WindowModalDialog(window, "Encryption Dialog") + d.setMinimumWidth(500) + d.setMinimumHeight(210) + d.setMaximumHeight(450) + d.setContentsMargins(11, 11, 1, 1) self.c_dialog = d - self.vbox = QVBoxLayout(d) - self.vbox.addSpacing(21) - + hbox = QHBoxLayout(d) + self.vbox = QVBoxLayout() logo = QLabel() - self.vbox.addWidget(logo) + hbox.addWidget(logo) logo.setPixmap(QPixmap(':icons/revealer.png')) - logo.setAlignment(Qt.AlignCenter) - self.vbox.addSpacing(42) - + logo.setAlignment(Qt.AlignLeft) + hbox.addSpacing(16) + self.vbox.addWidget(WWLabel("" + _("Revealer Secret Backup Plugin") + "
" + + _("Ready to encrypt for revealer ")+self.version+'_'+self.code_id )) + self.vbox.addSpacing(11) + hbox.addLayout(self.vbox) grid = QGridLayout() self.vbox.addLayout(grid) - cprint = QPushButton(_("Generate encrypted seed backup")) + cprint = QPushButton(_("Encrypt ")+self.wallet_name+_("'s seed")) + cprint.setMaximumWidth(250) cprint.clicked.connect(partial(self.seed_img, True)) self.vbox.addWidget(cprint) - self.vbox.addSpacing(14) - - self.vbox.addWidget(WWLabel(_("OR type any secret below:"))) + self.vbox.addSpacing(1) + self.vbox.addWidget(WWLabel(""+_("OR ")+""+_("type a custom alphanumerical secret below:"))) self.text = ScanQRTextEdit() self.text.setTabChangesFocus(True) self.text.setMaximumHeight(70) self.text.textChanged.connect(self.customtxt_limits) self.vbox.addWidget(self.text) - self.char_count = WWLabel("") self.char_count.setAlignment(Qt.AlignRight) self.vbox.addWidget(self.char_count) - self.max_chars = WWLabel("" + _("This version supports a maximum of 216 characters.")+"") self.vbox.addWidget(self.max_chars) self.max_chars.setVisible(False) - - self.ctext = QPushButton(_("Generate custom secret encrypted backup")) + self.ctext = QPushButton(_("Encrypt custom secret")) self.ctext.clicked.connect(self.t) - self.vbox.addWidget(self.ctext) self.ctext.setEnabled(False) - self.vbox.addSpacing(11) - self.vbox.addWidget( - QLabel(''.join(["" + _("WARNING") + ": " + _("Revealer is a one-time-pad and should be used only once."), '
', - _("Multiple secrets encrypted for the same Revealer can be attacked."), '
', - ]))) - self.vbox.addSpacing(11) - - self.vbox.addSpacing(21) self.vbox.addLayout(Buttons(CloseButton(d))) return bool(d.exec_()) @@ -259,11 +265,9 @@ class Plugin(BasePlugin): def seed_img(self, is_seed = True): - if not self.cseed and self.txt == False: - return - if is_seed: - txt = self.cseed + self.get_seed() + txt = self.cseed.upper() else: txt = self.txt.upper() @@ -328,7 +332,7 @@ class Plugin(BasePlugin): entropy = binascii.unhexlify(str(format(self.noise_seed, '032x'))) code_id = binascii.unhexlify(self.version + self.code_id) - + print (self.hex_noise) drbg = DRBG(entropy + code_id) noise_array=bin(int.from_bytes(drbg.generate(1929), 'big'))[2:] @@ -384,7 +388,7 @@ class Plugin(BasePlugin): self.filename = self.wallet_name+'_'+ _('seed')+'_' self.was = self.wallet_name +' ' + _('seed') - if self.has_extension: + if self.extension: self.ext_warning(self.c_dialog) if not calibration: From ff2cdf9f16440c3583e9857e16178b29dd65b66d Mon Sep 17 00:00:00 2001 From: tiagotrs Date: Mon, 10 Dec 2018 23:00:50 +0100 Subject: [PATCH 266/301] small fixes, simplification/improvement of texts --- electrum/plugins/revealer/__init__.py | 8 ++------ electrum/plugins/revealer/qt.py | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/electrum/plugins/revealer/__init__.py b/electrum/plugins/revealer/__init__.py index dcc8c31f..beb58d30 100644 --- a/electrum/plugins/revealer/__init__.py +++ b/electrum/plugins/revealer/__init__.py @@ -1,13 +1,9 @@ from electrum.i18n import _ -fullname = _('Revealer') +fullname = _('Revealer Backup Utility') description = ''.join(["
", ""+_("Do you have something to hide ?")+"", '
', '
', - _("Revealer is a seed phrase back-up solution. It allows you to create a cold, analog, multi-factor backup of your wallet seeds, or of any arbitrary secret."), '
', '
', - _("Using a Revealer is better than writing your seed phrases on paper: a revealer is invulnerable to physical access and allows creation of trustless redundancy."), '
', '
', - _("This plug-in allows you to generate a pdf file of your secret phrase encrypted visually for your physical Revealer. You can print it trustlessly - it can only be decrypted optically with your Revealer."), '
', '
', - _("The plug-in also allows you to generate a digital Revealer file and print it yourself on a transparent overhead foil."), '
', '
', - _("Once activated you can access the plug-in through the icon at the seed dialog."), '
', '
', + _("This plug-in allows you to create a visually encrypted backup of your wallet seeds, or of custom alphanumeric secrets."), '
', '
', _("For more information, visit"), "
https://revealer.cc", '
', '
', ]) diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index 06f817dd..b846a199 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -79,6 +79,8 @@ class Plugin(BasePlugin): self.cseed = keystore.get_seed(password) if keystore.get_passphrase(password): self.extension = True + else: + self.extension = False except Exception: traceback.print_exc(file=sys.stdout) return @@ -103,7 +105,7 @@ class Plugin(BasePlugin): logo.setAlignment(Qt.AlignLeft) self.hbox.addSpacing(16) vbox.addWidget(WWLabel(""+_("Revealer Secret Backup Plugin")+"
" - +_("To encrypt your backup, first we need to load some noise.")+"
")) + +_("To encrypt your backup, first we need to load some noise.")+"
")) vbox.addSpacing(7) bcreate = QPushButton(_("Create a new Revealer")) bcreate.setMaximumWidth(181) @@ -124,7 +126,7 @@ class Plugin(BasePlugin): self.next_button.clicked.connect(partial(self.cypherseed_dialog, window)) vbox.addWidget( QLabel("" + _("Warning") + ": " + _("Each revealer should be used only once.") - +"
"+_("more info at https://revealer.cc/faq"))) + +"
"+_("more information at https://revealer.cc/faq"))) def mk_digital(): try: @@ -183,8 +185,9 @@ class Plugin(BasePlugin): def bcrypt(self, dialog): self.rawnoise = False - dialog.show_message(''.join([_("{} encrypted for Revealer {}_{} saved as PNG and PDF at:").format(self.was, self.version, self.code_id), - "
","", self.base_dir+ self.filename+self.version+"_"+self.code_id,""])) + dialog.show_message(''.join([_("{} encrypted for Revealer {}_{} saved as PNG and PDF at: ").format(self.was, self.version, self.code_id), + "", self.base_dir+ self.filename+self.version+"_"+self.code_id,"", "
", + "
", "", _("Always check you backups.") ])) dialog.close() def ext_warning(self, dialog): @@ -199,11 +202,11 @@ class Plugin(BasePlugin): def customtxt_limits(self): txt = self.text.text() self.max_chars.setVisible(False) - self.char_count.setText("("+str(len(txt))+"/216)") + self.char_count.setText("("+str(len(txt))+"/189)") if len(txt)>0: self.ctext.setEnabled(True) - if len(txt) > 216: - self.text.setPlainText(self.text.toPlainText()[:216]) + if len(txt) > 189: + self.text.setPlainText(self.text.toPlainText()[:189]) self.max_chars.setVisible(True) def t(self): @@ -227,7 +230,7 @@ class Plugin(BasePlugin): logo.setAlignment(Qt.AlignLeft) hbox.addSpacing(16) self.vbox.addWidget(WWLabel("" + _("Revealer Secret Backup Plugin") + "
" - + _("Ready to encrypt for revealer ")+self.version+'_'+self.code_id )) + + _("Ready to encrypt for revealer {}").format(self.version+'_'+self.code_id ))) self.vbox.addSpacing(11) hbox.addLayout(self.vbox) grid = QGridLayout() @@ -247,7 +250,7 @@ class Plugin(BasePlugin): self.char_count = WWLabel("") self.char_count.setAlignment(Qt.AlignRight) self.vbox.addWidget(self.char_count) - self.max_chars = WWLabel("" + _("This version supports a maximum of 216 characters.")+"") + self.max_chars = WWLabel("" + _("This version supports a maximum of 189 characters.")+"") self.vbox.addWidget(self.max_chars) self.max_chars.setVisible(False) self.ctext = QPushButton(_("Encrypt custom secret")) @@ -286,7 +289,7 @@ class Plugin(BasePlugin): else: fontsize = 12 linespace = 10 - max_letters = 23 + max_letters = 21 max_lines = 9 max_words = int(max_letters/4) @@ -387,9 +390,9 @@ class Plugin(BasePlugin): else: self.filename = self.wallet_name+'_'+ _('seed')+'_' self.was = self.wallet_name +' ' + _('seed') + if self.extension: + self.ext_warning(self.c_dialog) - if self.extension: - self.ext_warning(self.c_dialog) if not calibration: self.toPdf(QImage(cypherseed)) From e1ba962fe184fbbef2c06ebadfff063e932120fb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Dec 2018 23:59:26 +0100 Subject: [PATCH 267/301] revealer: clean-up prev and fixes --- electrum/gui/qt/main_window.py | 1 - electrum/plugins/revealer/qt.py | 57 ++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 31d67a80..0db7fa05 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2014,7 +2014,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.status_button = StatusBarButton(QIcon(":icons/status_disconnected.png"), _("Network"), lambda: self.gui_object.show_network_dialog(self)) sb.addPermanentWidget(self.status_button) run_hook('create_status_bar', sb) - run_hook('revealer_hook', sb) self.setStatusBar(sb) def update_lock_icon(self): diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index b846a199..5ffca429 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -21,7 +21,7 @@ from PyQt5.QtPrintSupport import QPrinter from electrum.plugin import BasePlugin, hook from electrum.i18n import _ -from electrum.util import to_bytes, make_dir +from electrum.util import to_bytes, make_dir, InvalidPassword, UserCancelled from electrum.gui.qt.util import * from electrum.gui.qt.qrtextedit import ScanQRTextEdit from electrum.gui.qt.main_window import StatusBarButton @@ -30,6 +30,8 @@ from .hmac_drbg import DRBG class Plugin(BasePlugin): + MAX_PLAINTEXT_LEN = 189 # chars + def __init__(self, parent, config, name): BasePlugin.__init__(self, parent, config, name) self.base_dir = config.electrum_path()+'/revealer/' @@ -51,9 +53,13 @@ class Plugin(BasePlugin): self.rawnoise = False make_dir(self.base_dir) + self.extension = False + @hook - def revealer_hook(self, parent): - parent.addPermanentWidget(StatusBarButton(QIcon(':icons/revealer.png'), "Revealer"+_(" secret backup utility"), partial(self.setup_dialog, parent))) + def create_status_bar(self, parent): + b = StatusBarButton(QIcon(':icons/revealer.png'), "Revealer "+_("secret backup utility"), + partial(self.setup_dialog, parent)) + parent.addPermanentWidget(b) def requires_settings(self): return True @@ -72,18 +78,13 @@ class Plugin(BasePlugin): if self.wallet.has_keystore_encryption(): password = self.password_dialog(parent=self.d.parent()) if not password: - return + raise UserCancelled() keystore = self.wallet.get_keystore() - try: - self.cseed = keystore.get_seed(password) - if keystore.get_passphrase(password): - self.extension = True - else: - self.extension = False - except Exception: - traceback.print_exc(file=sys.stdout) + if not keystore or not keystore.has_seed(): return + self.extension = bool(keystore.get_passphrase(password)) + return keystore.get_seed(password) def setup_dialog(self, window): self.wallet = window.parent().wallet @@ -191,7 +192,8 @@ class Plugin(BasePlugin): dialog.close() def ext_warning(self, dialog): - dialog.show_message(''.join(["",_("Warning: "), "", _("your seed extension will ")+""+_("not")+""+_(" be included in the encrypted backup.")])) + dialog.show_message(''.join(["",_("Warning"), ": ", + _("your seed extension will not be included in the encrypted backup.")])) dialog.close() def bdone(self, dialog): @@ -202,11 +204,11 @@ class Plugin(BasePlugin): def customtxt_limits(self): txt = self.text.text() self.max_chars.setVisible(False) - self.char_count.setText("("+str(len(txt))+"/189)") + self.char_count.setText(f"({len(txt)}/{self.MAX_PLAINTEXT_LEN})") if len(txt)>0: self.ctext.setEnabled(True) - if len(txt) > 189: - self.text.setPlainText(self.text.toPlainText()[:189]) + if len(txt) > self.MAX_PLAINTEXT_LEN: + self.text.setPlainText(txt[:self.MAX_PLAINTEXT_LEN]) self.max_chars.setVisible(True) def t(self): @@ -236,12 +238,12 @@ class Plugin(BasePlugin): grid = QGridLayout() self.vbox.addLayout(grid) - cprint = QPushButton(_("Encrypt ")+self.wallet_name+_("'s seed")) + cprint = QPushButton(_("Encrypt {}'s seed").format(self.wallet_name)) cprint.setMaximumWidth(250) cprint.clicked.connect(partial(self.seed_img, True)) self.vbox.addWidget(cprint) self.vbox.addSpacing(1) - self.vbox.addWidget(WWLabel(""+_("OR ")+""+_("type a custom alphanumerical secret below:"))) + self.vbox.addWidget(WWLabel(""+_("OR")+" "+_("type a custom alphanumerical secret below:"))) self.text = ScanQRTextEdit() self.text.setTabChangesFocus(True) self.text.setMaximumHeight(70) @@ -250,7 +252,9 @@ class Plugin(BasePlugin): self.char_count = WWLabel("") self.char_count.setAlignment(Qt.AlignRight) self.vbox.addWidget(self.char_count) - self.max_chars = WWLabel("" + _("This version supports a maximum of 189 characters.")+"") + self.max_chars = WWLabel("" + + _("This version supports a maximum of {} characters.").format(self.MAX_PLAINTEXT_LEN) + +"") self.vbox.addWidget(self.max_chars) self.max_chars.setVisible(False) self.ctext = QPushButton(_("Encrypt custom secret")) @@ -269,8 +273,17 @@ class Plugin(BasePlugin): def seed_img(self, is_seed = True): if is_seed: - self.get_seed() - txt = self.cseed.upper() + try: + cseed = self.get_seed() + except UserCancelled: + return + except InvalidPassword as e: + self.d.show_error(str(e)) + return + if not cseed: + self.d.show_message(_("This wallet has no seed")) + return + txt = cseed.upper() else: txt = self.txt.upper() @@ -335,7 +348,7 @@ class Plugin(BasePlugin): entropy = binascii.unhexlify(str(format(self.noise_seed, '032x'))) code_id = binascii.unhexlify(self.version + self.code_id) - print (self.hex_noise) + drbg = DRBG(entropy + code_id) noise_array=bin(int.from_bytes(drbg.generate(1929), 'big'))[2:] From 91ef367176eeb516e6baee2923222078d938f702 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 15 Dec 2018 01:12:59 +0100 Subject: [PATCH 268/301] revealer: fix path madness don't use translated strings in file system paths! --- electrum/plugins/revealer/qt.py | 43 +++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index 5ffca429..14a94cdc 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -34,7 +34,7 @@ class Plugin(BasePlugin): def __init__(self, parent, config, name): BasePlugin.__init__(self, parent, config, name) - self.base_dir = config.electrum_path()+'/revealer/' + self.base_dir = os.path.join(config.electrum_path(), 'revealer') if self.config.get('calibration_h') is None: self.config.set_key('calibration_h', 0) @@ -184,11 +184,20 @@ class Plugin(BasePlugin): self.bdone(dialog) self.d.close() + def get_path_to_revealer_file(self, ext: str= '') -> str: + filename = self.filename_prefix + self.version + "_" + self.code_id + ext + path = os.path.join(self.base_dir, filename) + return os.path.normcase(os.path.abspath(path)) + + def get_path_to_calibration_file(self): + path = os.path.join(self.base_dir, 'calibration.pdf') + return os.path.normcase(os.path.abspath(path)) + def bcrypt(self, dialog): self.rawnoise = False dialog.show_message(''.join([_("{} encrypted for Revealer {}_{} saved as PNG and PDF at: ").format(self.was, self.version, self.code_id), - "", self.base_dir+ self.filename+self.version+"_"+self.code_id,"", "
", - "
", "", _("Always check you backups.") ])) + "", self.get_path_to_revealer_file(), "", "
", + "
", "", _("Always check you backups.")])) dialog.close() def ext_warning(self, dialog): @@ -198,7 +207,7 @@ class Plugin(BasePlugin): def bdone(self, dialog): dialog.show_message(''.join([_("Digital Revealer ({}_{}) saved as PNG and PDF at:").format(self.version, self.code_id), - "
","", self.base_dir + 'revealer_' +self.version + '_'+ self.code_id, ''])) + "
","", self.get_path_to_revealer_file(), ''])) def customtxt_limits(self): @@ -265,10 +274,8 @@ class Plugin(BasePlugin): self.vbox.addLayout(Buttons(CloseButton(d))) return bool(d.exec_()) - - def update_wallet_name (self, name): + def update_wallet_name(self, name): self.wallet_name = str(name) - self.base_name = self.base_dir + self.wallet_name def seed_img(self, is_seed = True): @@ -380,10 +387,10 @@ class Plugin(BasePlugin): revealer = revealer.scaled(self.f_size, Qt.KeepAspectRatio) revealer = self.overlay_marks(revealer) - self.filename = 'Revealer - ' - revealer.save(self.base_dir + self.filename + self.version+'_'+self.code_id + '.png') + self.filename_prefix = 'revealer_' + revealer.save(self.get_path_to_revealer_file('.png')) self.toPdf(QImage(revealer)) - QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.abspath(self.base_dir + self.filename + self.version+'_'+ self.code_id + '.pdf'))) + QDesktopServices.openUrl(QUrl.fromLocalFile(self.get_path_to_revealer_file('.pdf'))) def make_cypherseed(self, img, rawnoise, calibration=False, is_seed = True): img = img.convertToFormat(QImage.Format_Mono) @@ -398,19 +405,19 @@ class Plugin(BasePlugin): cypherseed = self.overlay_marks(cypherseed, True, calibration) if not is_seed: - self.filename = _('custom_secret')+'_' + self.filename_prefix = 'custom_secret_' self.was = _('Custom secret') else: - self.filename = self.wallet_name+'_'+ _('seed')+'_' - self.was = self.wallet_name +' ' + _('seed') + self.filename_prefix = self.wallet_name + '_seed_' + self.was = self.wallet_name + ' ' + _('seed') if self.extension: self.ext_warning(self.c_dialog) if not calibration: self.toPdf(QImage(cypherseed)) - QDesktopServices.openUrl (QUrl.fromLocalFile(os.path.abspath(self.base_dir+self.filename+self.version+'_'+self.code_id+'.pdf'))) - cypherseed.save(self.base_dir + self.filename +self.version + '_'+ self.code_id + '.png') + QDesktopServices.openUrl(QUrl.fromLocalFile(self.get_path_to_revealer_file('.pdf'))) + cypherseed.save(self.get_path_to_revealer_file('.png')) self.bcrypt(self.c_dialog) return cypherseed @@ -421,7 +428,7 @@ class Plugin(BasePlugin): self.make_calnoise() img = self.overlay_marks(self.calnoise.scaledToHeight(self.f_size.height()), False, True) self.calibration_pdf(img) - QDesktopServices.openUrl (QUrl.fromLocalFile(os.path.abspath(self.base_dir+_('calibration')+'.pdf'))) + QDesktopServices.openUrl(QUrl.fromLocalFile(self.get_path_to_calibration_file())) return img def toPdf(self, image): @@ -429,7 +436,7 @@ class Plugin(BasePlugin): printer.setPaperSize(QSizeF(210, 297), QPrinter.Millimeter) printer.setResolution(600) printer.setOutputFormat(QPrinter.PdfFormat) - printer.setOutputFileName(self.base_dir+self.filename+self.version + '_'+self.code_id+'.pdf') + printer.setOutputFileName(self.get_path_to_revealer_file('.pdf')) printer.setPageMargins(0,0,0,0,6) painter = QPainter() painter.begin(printer) @@ -454,7 +461,7 @@ class Plugin(BasePlugin): printer.setPaperSize(QSizeF(210, 297), QPrinter.Millimeter) printer.setResolution(600) printer.setOutputFormat(QPrinter.PdfFormat) - printer.setOutputFileName(self.base_dir+_('calibration')+'.pdf') + printer.setOutputFileName(self.get_path_to_calibration_file()) printer.setPageMargins(0,0,0,0,6) painter = QPainter() From 94afd7a9eaa03097b81c566ecc2fe02b2e03c988 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 15 Dec 2018 08:13:30 +0100 Subject: [PATCH 269/301] revealer: clean-up noise-generation. support regeneration of v0 again --- electrum/plugins/revealer/qt.py | 181 +++++++++++++++++++------------- 1 file changed, 108 insertions(+), 73 deletions(-) diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index 14a94cdc..8a07bca1 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -15,22 +15,42 @@ import qrcode import traceback from hashlib import sha256 from decimal import Decimal -import binascii +from typing import NamedTuple, Optional, Dict, Tuple from PyQt5.QtPrintSupport import QPrinter from electrum.plugin import BasePlugin, hook from electrum.i18n import _ -from electrum.util import to_bytes, make_dir, InvalidPassword, UserCancelled +from electrum.util import to_bytes, make_dir, InvalidPassword, UserCancelled, bh2u, bfh from electrum.gui.qt.util import * from electrum.gui.qt.qrtextedit import ScanQRTextEdit from electrum.gui.qt.main_window import StatusBarButton from .hmac_drbg import DRBG + +class VersionedSeed(NamedTuple): + version: str + seed: str + checksum: str + + def get_ui_string_version_plus_seed(self): + version, seed = self.version, self.seed + assert isinstance(version, str) and len(version) == 1, version + assert isinstance(seed, str) and len(seed) >= 32 + ret = version + seed + ret = ret.upper() + return ' '.join(ret[i : i+4] for i in range(0, len(ret), 4)) + + class Plugin(BasePlugin): + LATEST_VERSION = '1' + KNOWN_VERSIONS = ('0', '1') + assert LATEST_VERSION in KNOWN_VERSIONS + MAX_PLAINTEXT_LEN = 189 # chars + SIZE = (159, 97) def __init__(self, parent, config, name): BasePlugin.__init__(self, parent, config, name) @@ -44,8 +64,6 @@ class Plugin(BasePlugin): self.calibration_h = self.config.get('calibration_h') self.calibration_v = self.config.get('calibration_v') - self.version = '1' - self.size = (159, 97) self.f_size = QSize(1014*2, 642*2) self.abstand_h = 21 self.abstand_v = 34 @@ -90,7 +108,6 @@ class Plugin(BasePlugin): self.wallet = window.parent().wallet self.update_wallet_name(self.wallet) self.user_input = False - self.noise_seed = False self.d = WindowModalDialog(window, "Setup Dialog") self.d.setMinimumWidth(500) @@ -145,39 +162,36 @@ class Plugin(BasePlugin): return ''.join(text.split()).lower() def on_edit(self): - s = self.get_noise() - b = self.is_noise(s) - if b: - self.noise_seed = s[1:-3] - self.user_input = True - self.next_button.setEnabled(b) + txt = self.get_noise() + versioned_seed = self.get_versioned_seed_from_user_input(txt) + if versioned_seed: + self.versioned_seed = versioned_seed + self.user_input = bool(versioned_seed) + self.next_button.setEnabled(bool(versioned_seed)) - def code_hashid(self, txt): + @classmethod + def code_hashid(cls, txt: str) -> str: x = to_bytes(txt, 'utf8') hash = sha256(x).hexdigest() return hash[-3:].upper() - def is_noise(self, txt): - if (len(txt) >= 34): - try: - int(txt, 16) - except: - self.user_input = False - return False - else: - id = self.code_hashid(txt[:-3]) - if (txt[-3:].upper() == id.upper()): - self.code_id = id - self.user_input = True - return True - else: - return False - else: - - if (len(txt)>0 and txt[0]=='0'): - self.d.show_message(''.join(["",_("Warning: "), "", _("Revealers starting with 0 had a vulnerability and are not supported.")])) - self.user_input = False - return False + @classmethod + def get_versioned_seed_from_user_input(cls, txt: str) -> Optional[VersionedSeed]: + if len(txt) < 34: + return None + try: + int(txt, 16) + except: + return None + version = txt[0] + if version not in cls.KNOWN_VERSIONS: + return None + checksum = cls.code_hashid(txt[:-3]) + if txt[-3:].upper() != checksum.upper(): + return None + return VersionedSeed(version=version.upper(), + seed=txt[1:-3].upper(), + checksum=checksum.upper()) def make_digital(self, dialog): self.make_rawnoise(True) @@ -185,7 +199,9 @@ class Plugin(BasePlugin): self.d.close() def get_path_to_revealer_file(self, ext: str= '') -> str: - filename = self.filename_prefix + self.version + "_" + self.code_id + ext + version = self.versioned_seed.version + code_id = self.versioned_seed.checksum + filename = self.filename_prefix + version + "_" + code_id + ext path = os.path.join(self.base_dir, filename) return os.path.normcase(os.path.abspath(path)) @@ -195,7 +211,9 @@ class Plugin(BasePlugin): def bcrypt(self, dialog): self.rawnoise = False - dialog.show_message(''.join([_("{} encrypted for Revealer {}_{} saved as PNG and PDF at: ").format(self.was, self.version, self.code_id), + version = self.versioned_seed.version + 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.")])) dialog.close() @@ -206,7 +224,9 @@ class Plugin(BasePlugin): dialog.close() def bdone(self, dialog): - dialog.show_message(''.join([_("Digital Revealer ({}_{}) saved as PNG and PDF at:").format(self.version, self.code_id), + version = self.versioned_seed.version + code_id = self.versioned_seed.checksum + dialog.show_message(''.join([_("Digital Revealer ({}_{}) saved as PNG and PDF at:").format(version, code_id), "
","", self.get_path_to_revealer_file(), ''])) @@ -241,7 +261,8 @@ class Plugin(BasePlugin): logo.setAlignment(Qt.AlignLeft) hbox.addSpacing(16) self.vbox.addWidget(WWLabel("" + _("Revealer Secret Backup Plugin") + "
" - + _("Ready to encrypt for revealer {}").format(self.version+'_'+self.code_id ))) + + _("Ready to encrypt for revealer {}") + .format(self.versioned_seed.version+'_'+self.versioned_seed.checksum))) self.vbox.addSpacing(11) hbox.addLayout(self.vbox) grid = QGridLayout() @@ -294,7 +315,7 @@ class Plugin(BasePlugin): else: txt = self.txt.upper() - img = QImage(self.size[0],self.size[1], QImage.Format_Mono) + img = QImage(self.SIZE[0], self.SIZE[1], QImage.Format_Mono) bitmap = QBitmap.fromImage(img, Qt.MonoOnly) bitmap.fill(Qt.white) painter = QPainter() @@ -325,7 +346,7 @@ class Plugin(BasePlugin): while len(' '.join(map(str, temp_seed))) > max_letters: nwords = nwords - 1 temp_seed = seed_array[:nwords] - painter.drawText(QRect(0, linespace*n , self.size[0], self.size[1]), Qt.AlignHCenter, ' '.join(map(str, temp_seed))) + painter.drawText(QRect(0, linespace*n , self.SIZE[0], self.SIZE[1]), Qt.AlignHCenter, ' '.join(map(str, temp_seed))) del seed_array[:nwords] painter.end() @@ -337,43 +358,55 @@ class Plugin(BasePlugin): return img def make_rawnoise(self, create_revealer=False): - w = self.size[0] - h = self.size[1] + if not self.user_input: + version = self.LATEST_VERSION + hex_seed = bh2u(os.urandom(16)) + checksum = self.code_hashid(version + hex_seed) + self.versioned_seed = VersionedSeed(version=version.upper(), + seed=hex_seed.upper(), + checksum=checksum.upper()) + assert self.versioned_seed + w, h = self.SIZE rawnoise = QImage(w, h, QImage.Format_Mono) - if(self.noise_seed == False): - self.noise_seed = random.SystemRandom().getrandbits(128) - self.hex_noise = format(self.noise_seed, '032x') - self.hex_noise = self.version + str(self.hex_noise) - - if (self.user_input == True): - self.noise_seed = int(self.noise_seed, 16) - self.hex_noise = self.version + str(format(self.noise_seed, '032x')) - - self.code_id = self.code_hashid(self.hex_noise) - self.hex_noise = ' '.join(self.hex_noise[i:i+4] for i in range(0,len(self.hex_noise),4)) - - entropy = binascii.unhexlify(str(format(self.noise_seed, '032x'))) - code_id = binascii.unhexlify(self.version + self.code_id) - - drbg = DRBG(entropy + code_id) - noise_array=bin(int.from_bytes(drbg.generate(1929), 'big'))[2:] - - i=0 - for x in range(w): - for y in range(h): - rawnoise.setPixel(x,y,int(noise_array[i])) - i+=1 + noise_map = self.get_noise_map(self.versioned_seed) + for (x,y), pixel in noise_map.items(): + rawnoise.setPixel(x, y, pixel) self.rawnoise = rawnoise - if create_revealer==True: + if create_revealer: self.make_revealer() - self.noise_seed = False + + @classmethod + def get_noise_map(self, versioned_seed: VersionedSeed) -> Dict[Tuple[int, int], int]: + """Returns a map from (x,y) coordinate to pixel value 0/1, to be used as noise.""" + w, h = self.SIZE + version = versioned_seed.version + hex_seed = versioned_seed.seed + checksum = versioned_seed.checksum + noise_map = {} + if version == '0': + random.seed(int(hex_seed, 16)) + for x in range(w): + for y in range(h): + noise_map[(x, y)] = random.randint(0, 1) + elif version == '1': + prng_seed = bfh(hex_seed + version + checksum) + drbg = DRBG(prng_seed) + num_noise_bytes = 1929 # ~ w*h + noise_array = bin(int.from_bytes(drbg.generate(num_noise_bytes), 'big'))[2:] + i = 0 + for x in range(w): + for y in range(h): + noise_map[(x, y)] = int(noise_array[i]) + i += 1 + else: + raise Exception(f"unexpected revealer version: {version}") + return noise_map def make_calnoise(self): random.seed(self.calibration_noise) - w = self.size[0] - h = self.size[1] + w, h = self.SIZE rawnoise = QImage(w, h, QImage.Format_Mono) for x in range(w): for y in range(h): @@ -422,7 +455,7 @@ class Plugin(BasePlugin): return cypherseed def calibration(self): - img = QImage(self.size[0],self.size[1], QImage.Format_Mono) + img = QImage(self.SIZE[0], self.SIZE[1], QImage.Format_Mono) bitmap = QBitmap.fromImage(img, Qt.MonoOnly) bitmap.fill(Qt.black) self.make_calnoise() @@ -586,7 +619,8 @@ class Plugin(BasePlugin): (base_img.height()-((total_distance_h)))-((border_thick*8)/2)-(border_thick/2)-2) painter.setPen(QColor(0,0,0,255)) painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick - 11, - base_img.height()-total_distance_h - border_thick), Qt.AlignRight, self.version + '_'+self.code_id) + base_img.height()-total_distance_h - border_thick), Qt.AlignRight, + self.versioned_seed.version + '_'+self.versioned_seed.checksum) painter.end() else: # revealer @@ -635,12 +669,13 @@ class Plugin(BasePlugin): painter.setPen(QColor(0,0,0,255)) painter.drawText(QRect(((base_img.width()/2) +21)-qr_size, base_img.height()-107, base_img.width()-total_distance_h - border_thick -93, - base_img.height()-total_distance_h - border_thick), Qt.AlignLeft, self.hex_noise.upper()) + base_img.height()-total_distance_h - border_thick), Qt.AlignLeft, self.versioned_seed.get_ui_string_version_plus_seed()) painter.drawText(QRect(0, base_img.height()-107, base_img.width()-total_distance_h - border_thick -3 -qr_size, - base_img.height()-total_distance_h - border_thick), Qt.AlignRight, self.code_id) + base_img.height()-total_distance_h - border_thick), Qt.AlignRight, self.versioned_seed.checksum) # draw qr code - qr_qt = self.paintQR(self.hex_noise.upper() +self.code_id) + qr_qt = self.paintQR(self.versioned_seed.get_ui_string_version_plus_seed() + + self.versioned_seed.checksum) target = QRectF(base_img.width()-65-qr_size, base_img.height()-65-qr_size, qr_size, qr_size ) From f969edcf50cb7e5e8208815730a86b0b6216f4d0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 15 Dec 2018 08:52:00 +0100 Subject: [PATCH 270/301] revealer: split some core parts out into separate file for easier testing --- electrum/plugins/revealer/qt.py | 82 ++----------------------- electrum/plugins/revealer/revealer.py | 86 +++++++++++++++++++++++++++ electrum/tests/test_revealer.py | 26 ++++++++ 3 files changed, 117 insertions(+), 77 deletions(-) create mode 100644 electrum/plugins/revealer/revealer.py create mode 100644 electrum/tests/test_revealer.py diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index 8a07bca1..e38d4126 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -13,47 +13,26 @@ import os import random import qrcode import traceback -from hashlib import sha256 from decimal import Decimal -from typing import NamedTuple, Optional, Dict, Tuple from PyQt5.QtPrintSupport import QPrinter -from electrum.plugin import BasePlugin, hook +from electrum.plugin import hook from electrum.i18n import _ -from electrum.util import to_bytes, make_dir, InvalidPassword, UserCancelled, bh2u, bfh +from electrum.util import make_dir, InvalidPassword, UserCancelled, bh2u, bfh from electrum.gui.qt.util import * from electrum.gui.qt.qrtextedit import ScanQRTextEdit from electrum.gui.qt.main_window import StatusBarButton -from .hmac_drbg import DRBG +from .revealer import RevealerPlugin, VersionedSeed -class VersionedSeed(NamedTuple): - version: str - seed: str - checksum: str - - def get_ui_string_version_plus_seed(self): - version, seed = self.version, self.seed - assert isinstance(version, str) and len(version) == 1, version - assert isinstance(seed, str) and len(seed) >= 32 - ret = version + seed - ret = ret.upper() - return ' '.join(ret[i : i+4] for i in range(0, len(ret), 4)) - - -class Plugin(BasePlugin): - - LATEST_VERSION = '1' - KNOWN_VERSIONS = ('0', '1') - assert LATEST_VERSION in KNOWN_VERSIONS +class Plugin(RevealerPlugin): MAX_PLAINTEXT_LEN = 189 # chars - SIZE = (159, 97) def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) + RevealerPlugin.__init__(self, parent, config, name) self.base_dir = os.path.join(config.electrum_path(), 'revealer') if self.config.get('calibration_h') is None: @@ -169,30 +148,6 @@ class Plugin(BasePlugin): self.user_input = bool(versioned_seed) self.next_button.setEnabled(bool(versioned_seed)) - @classmethod - def code_hashid(cls, txt: str) -> str: - x = to_bytes(txt, 'utf8') - hash = sha256(x).hexdigest() - return hash[-3:].upper() - - @classmethod - def get_versioned_seed_from_user_input(cls, txt: str) -> Optional[VersionedSeed]: - if len(txt) < 34: - return None - try: - int(txt, 16) - except: - return None - version = txt[0] - if version not in cls.KNOWN_VERSIONS: - return None - checksum = cls.code_hashid(txt[:-3]) - if txt[-3:].upper() != checksum.upper(): - return None - return VersionedSeed(version=version.upper(), - seed=txt[1:-3].upper(), - checksum=checksum.upper()) - def make_digital(self, dialog): self.make_rawnoise(True) self.bdone(dialog) @@ -377,33 +332,6 @@ class Plugin(BasePlugin): if create_revealer: self.make_revealer() - @classmethod - def get_noise_map(self, versioned_seed: VersionedSeed) -> Dict[Tuple[int, int], int]: - """Returns a map from (x,y) coordinate to pixel value 0/1, to be used as noise.""" - w, h = self.SIZE - version = versioned_seed.version - hex_seed = versioned_seed.seed - checksum = versioned_seed.checksum - noise_map = {} - if version == '0': - random.seed(int(hex_seed, 16)) - for x in range(w): - for y in range(h): - noise_map[(x, y)] = random.randint(0, 1) - elif version == '1': - prng_seed = bfh(hex_seed + version + checksum) - drbg = DRBG(prng_seed) - num_noise_bytes = 1929 # ~ w*h - noise_array = bin(int.from_bytes(drbg.generate(num_noise_bytes), 'big'))[2:] - i = 0 - for x in range(w): - for y in range(h): - noise_map[(x, y)] = int(noise_array[i]) - i += 1 - else: - raise Exception(f"unexpected revealer version: {version}") - return noise_map - def make_calnoise(self): random.seed(self.calibration_noise) w, h = self.SIZE diff --git a/electrum/plugins/revealer/revealer.py b/electrum/plugins/revealer/revealer.py new file mode 100644 index 00000000..cda25d3b --- /dev/null +++ b/electrum/plugins/revealer/revealer.py @@ -0,0 +1,86 @@ +import random +from hashlib import sha256 +from typing import NamedTuple, Optional, Dict, Tuple + +from electrum.plugin import BasePlugin +from electrum.util import to_bytes, bh2u, bfh + +from .hmac_drbg import DRBG + + +class VersionedSeed(NamedTuple): + version: str + seed: str + checksum: str + + def get_ui_string_version_plus_seed(self): + version, seed = self.version, self.seed + assert isinstance(version, str) and len(version) == 1, version + assert isinstance(seed, str) and len(seed) >= 32 + ret = version + seed + ret = ret.upper() + return ' '.join(ret[i : i+4] for i in range(0, len(ret), 4)) + + +class RevealerPlugin(BasePlugin): + + LATEST_VERSION = '1' + KNOWN_VERSIONS = ('0', '1') + assert LATEST_VERSION in KNOWN_VERSIONS + + SIZE = (159, 97) + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + + @classmethod + def code_hashid(cls, txt: str) -> str: + txt = txt.lower() + x = to_bytes(txt, 'utf8') + hash = sha256(x).hexdigest() + return hash[-3:].upper() + + @classmethod + def get_versioned_seed_from_user_input(cls, txt: str) -> Optional[VersionedSeed]: + if len(txt) < 34: + return None + try: + int(txt, 16) + except: + return None + version = txt[0] + if version not in cls.KNOWN_VERSIONS: + return None + checksum = cls.code_hashid(txt[:-3]) + if txt[-3:].upper() != checksum.upper(): + return None + return VersionedSeed(version=version.upper(), + seed=txt[1:-3].upper(), + checksum=checksum.upper()) + + @classmethod + def get_noise_map(self, versioned_seed: VersionedSeed) -> Dict[Tuple[int, int], int]: + """Returns a map from (x,y) coordinate to pixel value 0/1, to be used as rawnoise.""" + w, h = self.SIZE + version = versioned_seed.version + hex_seed = versioned_seed.seed + checksum = versioned_seed.checksum + noise_map = {} + if version == '0': + random.seed(int(hex_seed, 16)) + for x in range(w): + for y in range(h): + noise_map[(x, y)] = random.randint(0, 1) + elif version == '1': + prng_seed = bfh(hex_seed + version + checksum) + drbg = DRBG(prng_seed) + num_noise_bytes = 1929 # ~ w*h + noise_array = bin(int.from_bytes(drbg.generate(num_noise_bytes), 'big'))[2:] + i = 0 + for x in range(w): + for y in range(h): + noise_map[(x, y)] = int(noise_array[i]) + i += 1 + else: + raise Exception(f"unexpected revealer version: {version}") + return noise_map diff --git a/electrum/tests/test_revealer.py b/electrum/tests/test_revealer.py new file mode 100644 index 00000000..5bf9a359 --- /dev/null +++ b/electrum/tests/test_revealer.py @@ -0,0 +1,26 @@ +from electrum.plugins.revealer.revealer import RevealerPlugin + +from . import SequentialTestCase + + +class TestRevealer(SequentialTestCase): + + def test_version_0_noisemap(self): + versioned_seed = RevealerPlugin.get_versioned_seed_from_user_input('03b0c557d6d0d4308a3393851d78bd8c7861') + noise_map = RevealerPlugin.get_noise_map(versioned_seed) + bigint = 0 + for (x, y), pixel in noise_map.items(): + if pixel: + bigint |= 1 << (y*RevealerPlugin.SIZE[1]+x) + self.assertEqual(0x541dde00b20ac7d320510e943d7ed9ffff5ff6b431c915353fbeffbc1beb737ff3a59c032a39ff8cbd532dffe42655bccbbef4f777ffeff8ec90e64aacbff5f4ff37ef4f32ac1d7240ed2bbb37dfeff459c7c2e2e0bfddffff7fffc7fd27eeb84a5ceafcf6bf9ffaff632367f97fffbf9fbfecff2b3a11a1c5befdfbfe7f125fba2c3e5d4ded591f9fbbbadbeed2220fb4337df9e4c7bfbe6ce4ad7b18ad57f3d75dfe7b6deb7350478bdbf7b7bfdf776eb301217d1f5c7f7ffffeefffe2070f52dbedfff2fef7f27f7d27f80b6a7bfb7f67bcbf7faf11f6b577dfbefebd44ffffe7bf5ee17ba4fb3377e1fcffeded781eca37a5bff3ebefdccbe1538c16129aed7fadfd7eb3bab55bcbdaee7e5d5b9fff57bd662333923b27af4f4da5ffd8bb15b58effed8bbeeff9ab7ecb75b62b977fdd88f3fbaeef6997a999b4dfffbfa375bf9e9c12b6011e2fde9fef7f66efc1155cc4fedfeefffeeff6ded645712b12bfe2b35df796f7ca05e0f12afbff6fefd1dd7f736bb9a567dff5797eafc1bfa0cf6cd090ddddfbfb79fd9f7f17bba2197e5dd3fb7fd9ff7579f0b6e28f7df3bdfe6fa8efd5a0a2e48f4d6efff79bf5efebc2638ff7eefffbbdfdb5bfac80426052df6fe6fd33eff5336a3c87c9fcff797b6bddbf91fea62e635333ffd7bfdd35f5c365432f5dfe7fd8bfb6c6e7cc90e6b5796d1dfeef567fdf390124a4bfeefd7efd1eee7f88ca45658fbf5cabffbfebf9fefe2c9f73dbffd36d7df77d73665c5f1dfa7b5b7fffafb6ea18bf9396e37b77fffffffb6aefdb0635ff7e43cfeef77fd8a527741dd3fffef1eddedf7f4259cc4253ffd9dffffb3bfbb0632d3d7fbf7bfbbfedfff2a7589be7624faffffbdbd7dfb5b5189b66fde8abf8bfbba7f440b80c2ba86de5fdfdf7ffba25625877fb9fdbf6f39333fe20e7710cdffadef8e7fb727d059237ef3dfceb9bd7ffef7b041565f2bb7ffdfefffff1ba3c7abb9a0fcf3fdf78fee7efb5da83dd1ffadebde675ff36d725426027dfffd9f76df3f7605b7f6fa4f75ff6e3df57ffd1a56c6239feffebffeecffdbd1ecc69f99ffea7f5ec759e2b2a99977b467edbffafae5faef9e719b7bff73fe9fed753ddc20ba23d8e7fdf4adfdbefadbb6a6775f7ef7eddffdfffeead7be0b38dcefeeb6afffef3d272d1b0492e733fff15dc3bfa2bfb83b9fefbfdf853fbddfbdbdf354868fd6dedf93edffca29130013bfcfbe27f4feffc86bbaa925bffdeed3fede76b321dc0abb57fe367df5adaaf30cc615c1efef7fbffe3993e583ff3721bdfbe66edef8faef24697f311ff6ff57ffefbebff9b90325bda76f77daeabfbcb9abd45c0bffe576bc3fffffc96911d477ddbbc3feef7f63a4510ec1265e6e1fe765eaafbca10400876bff7bffdfdffcc9920f60119beedfffd57e6ff383e6c3637def9fdffb7bfffb3339f94eb3fdf5bd7bbdfdf621d8f008dad195dffd6ffbff57a1ce166e5ef9f85febdde28a4a013987bf7ffebffffb56cf7aa522589bdafdf51ff4f39ed386097667fafdbfffff7ef9379dbc136bedfb9ff7aefffc3f081be97bf4e7ecfbde35cea3018d1bf1bffbfaeebefb9fac072f05bac77f7fdffeffe2eb1bd4d90a6fddafd7c2ff7bf9ba80d6f6df77ce727ff9a97fb41f03dcfbc557b3fbcc80b, + bigint) + + def test_version_1_noisemap(self): + versioned_seed = RevealerPlugin.get_versioned_seed_from_user_input('125Df05b7ccf079ce2978Ae18e99219868cd') + noise_map = RevealerPlugin.get_noise_map(versioned_seed) + bigint = 0 + for (x, y), pixel in noise_map.items(): + if pixel: + bigint |= 1 << (y*RevealerPlugin.SIZE[1]+x) + self.assertEqual(0x36fde1eece10b3f674227ea76f7ababbcbf87dfba1eddf2edebfeefec3dffff719ee1cbd477b9be7cf6fcdaff924ff05a26ff2fb7bfdbbdef1a2f90c097d7cdadfbb9d1ef592c27c85efffffff7ffc8dff60d6de87f71c9fe77f7f5372cfdb1dc0eb9e959dbed42197c7ee4288f7fbf73b65fdfbd5e153ede49bb957edaefe6f7dee4a72502eef77babfa7fff7d0a3fc6f5ffefb3b7b67aab66118a56eb1fffe1ddafbeefefa96b26d715bb8e5fafbbb2ffcf64e8df2bdffeffed39ffdfdef986491a7fbf97bffb7fafee072640b7af6edf8da2f7cdff268ccd52b75f53f9afef77b4be4db9c5f9debffeff5f7f1f7b1882cb4eed67f757b37ebf0b2c7f849bd73f4737f3ffb5a3f75ac537ff5fff8edcdfeb6be63d3147ffeefa9caf7ebf740989520c1eddedabdfd73f7f821fc3977fdfbe9fbee7d6e8ca9f16b8b8f1decbffef17f806ade988d77fef5775cff3f7bd9759675f4773fff6fefaff385fe807fdbfcbbffefa6d7c4ed54a0d1959cefeecffdffe8cd539451dfdfbff71ff7e97cf37aec8069efffcf7536ebbc515e991cf293ff97feffd72cebe110d44bf787f1efecda7306ac88cd49dfff257cbd9ff7ff8fd1686eedf2dfeb373fbe2a10ed81f7d9c979316efceeea745926377ffcffba7edf67fc79cace0eeef5ff5ceffeeeff94d20c4dadd53efee6f97bfed2f8ae059ff7ffbfedfbf17f2d45bc8afdebf1dff7856ebd02c39ae22ef7befc9e97cbb7f31af5bd57e3feffafc4d7fef9ae222e9fdcf5a2dfbffefd50399d7d095d22fd7bf9fdef6d5d5044bafffdfd57fff7cbe6af91096b3f5fffe3fff7fb97fa930d316db6dfbf236ddbc8abb3bbea6edf9deafd39b8efac7ae014e7ffbdd97cfebe7ec84a72a7b323fe77afffd7f1c8eaec48e6ff3fdf7da9fffaf2d57e961d7f5debc9afe7ff32d72ff374d3ff57fff2fbffdbe9833405fcbabff8beb8ff1e55f53b2d6e96bdf7e7fbfd0fb6b130071c13cf5de5be5ef8ade46a2dd53d77ff69eff7ff946ad4e32febddff87b73fceeaf2ce94adafbb9fddeff8fbf11f4161ad6cb29dbe5f2ffe6ee2023ceeb79c76d7fff7bbffaf4485b6f6f3b7f97f9f75ce372c173177fedd65e5fb76fdc5bbf7a737f9bdddefefff1f7a5533dd1efde7ffafabcd96e1193e3cadb93e76fcdfdb4fa533bc3d3a7eff5eebc9f3ffce91aa51bbf5e7f6bcfd7dfbfd1928c0726f3f26ef8f3ffe2eb8843cbb1dfd2eabfe4f7ffec47a95263e5c65737affe73e3e3735d61e8cbffdbf75e37fc04a991ad7ff7feffa6fafdfed988b50fdf379ffdfff2f7eeb6738c0ceffff77f32fe7f2b22c866514db75f7c3df6e5fd210a70bb4bbf31bcfb6d325f4a00b06ed7d34c9dbbdffff3fd6fd8ed570d7def1dcf789ff5ae040339ff35deb5e37bedf889d83bcf5feffb77e57ffcfdd8edfd91bb8bffd3b7dbd8fea3083734c3d7dedd9ffefcb78c5d87e1919f5655bf5bf1bfb3dd65fb64ee2fffd777afef18965d03872d73bbfb6fa7df7250f3e8d6ef7ff9ffffeadff5e39abf8727fde93febddfffee3096ca1779ceabbf7ff7bda2f756353be9dfabf2bcff6feec1cad233fffbf9ecefffffa21b7b8b17ffded7ff7fef56ee44b02d9bdf3d3cf42aa777fd90ba9b08af4cfd5b797dadb3694bf3282abcb39fb2d760f9, + bigint) From e7e9f8e7f2378b515c80b67792b53671b6b90a6d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 15 Dec 2018 09:05:12 +0100 Subject: [PATCH 271/301] revealer: fix unlucky hex seed causing crash --- electrum/plugins/revealer/qt.py | 7 +------ electrum/plugins/revealer/revealer.py | 23 +++++++++++++++++++++-- electrum/tests/test_revealer.py | 10 ++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index e38d4126..e9ed73ba 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -314,12 +314,7 @@ class Plugin(RevealerPlugin): def make_rawnoise(self, create_revealer=False): if not self.user_input: - version = self.LATEST_VERSION - hex_seed = bh2u(os.urandom(16)) - checksum = self.code_hashid(version + hex_seed) - self.versioned_seed = VersionedSeed(version=version.upper(), - seed=hex_seed.upper(), - checksum=checksum.upper()) + self.versioned_seed = self.gen_random_versioned_seed() assert self.versioned_seed w, h = self.SIZE rawnoise = QImage(w, h, QImage.Format_Mono) diff --git a/electrum/plugins/revealer/revealer.py b/electrum/plugins/revealer/revealer.py index cda25d3b..33df769b 100644 --- a/electrum/plugins/revealer/revealer.py +++ b/electrum/plugins/revealer/revealer.py @@ -1,4 +1,5 @@ import random +import os from hashlib import sha256 from typing import NamedTuple, Optional, Dict, Tuple @@ -59,9 +60,9 @@ class RevealerPlugin(BasePlugin): checksum=checksum.upper()) @classmethod - def get_noise_map(self, versioned_seed: VersionedSeed) -> Dict[Tuple[int, int], int]: + def get_noise_map(cls, versioned_seed: VersionedSeed) -> Dict[Tuple[int, int], int]: """Returns a map from (x,y) coordinate to pixel value 0/1, to be used as rawnoise.""" - w, h = self.SIZE + w, h = cls.SIZE version = versioned_seed.version hex_seed = versioned_seed.seed checksum = versioned_seed.checksum @@ -76,6 +77,9 @@ class RevealerPlugin(BasePlugin): drbg = DRBG(prng_seed) num_noise_bytes = 1929 # ~ w*h noise_array = bin(int.from_bytes(drbg.generate(num_noise_bytes), 'big'))[2:] + # there's an approx 1/1024 chance that the generated number is 'too small' + # and we would get IndexError below. easiest backwards compat fix: + noise_array += '0' * (w * h - len(noise_array)) i = 0 for x in range(w): for y in range(h): @@ -84,3 +88,18 @@ class RevealerPlugin(BasePlugin): else: raise Exception(f"unexpected revealer version: {version}") return noise_map + + @classmethod + def gen_random_versioned_seed(cls): + version = cls.LATEST_VERSION + hex_seed = bh2u(os.urandom(16)) + checksum = cls.code_hashid(version + hex_seed) + return VersionedSeed(version=version.upper(), + seed=hex_seed.upper(), + checksum=checksum.upper()) + + +if __name__ == '__main__': + for i in range(10**4): + vs = RevealerPlugin.gen_random_versioned_seed() + nm = RevealerPlugin.get_noise_map(vs) diff --git a/electrum/tests/test_revealer.py b/electrum/tests/test_revealer.py index 5bf9a359..0667c95c 100644 --- a/electrum/tests/test_revealer.py +++ b/electrum/tests/test_revealer.py @@ -24,3 +24,13 @@ class TestRevealer(SequentialTestCase): bigint |= 1 << (y*RevealerPlugin.SIZE[1]+x) self.assertEqual(0x36fde1eece10b3f674227ea76f7ababbcbf87dfba1eddf2edebfeefec3dffff719ee1cbd477b9be7cf6fcdaff924ff05a26ff2fb7bfdbbdef1a2f90c097d7cdadfbb9d1ef592c27c85efffffff7ffc8dff60d6de87f71c9fe77f7f5372cfdb1dc0eb9e959dbed42197c7ee4288f7fbf73b65fdfbd5e153ede49bb957edaefe6f7dee4a72502eef77babfa7fff7d0a3fc6f5ffefb3b7b67aab66118a56eb1fffe1ddafbeefefa96b26d715bb8e5fafbbb2ffcf64e8df2bdffeffed39ffdfdef986491a7fbf97bffb7fafee072640b7af6edf8da2f7cdff268ccd52b75f53f9afef77b4be4db9c5f9debffeff5f7f1f7b1882cb4eed67f757b37ebf0b2c7f849bd73f4737f3ffb5a3f75ac537ff5fff8edcdfeb6be63d3147ffeefa9caf7ebf740989520c1eddedabdfd73f7f821fc3977fdfbe9fbee7d6e8ca9f16b8b8f1decbffef17f806ade988d77fef5775cff3f7bd9759675f4773fff6fefaff385fe807fdbfcbbffefa6d7c4ed54a0d1959cefeecffdffe8cd539451dfdfbff71ff7e97cf37aec8069efffcf7536ebbc515e991cf293ff97feffd72cebe110d44bf787f1efecda7306ac88cd49dfff257cbd9ff7ff8fd1686eedf2dfeb373fbe2a10ed81f7d9c979316efceeea745926377ffcffba7edf67fc79cace0eeef5ff5ceffeeeff94d20c4dadd53efee6f97bfed2f8ae059ff7ffbfedfbf17f2d45bc8afdebf1dff7856ebd02c39ae22ef7befc9e97cbb7f31af5bd57e3feffafc4d7fef9ae222e9fdcf5a2dfbffefd50399d7d095d22fd7bf9fdef6d5d5044bafffdfd57fff7cbe6af91096b3f5fffe3fff7fb97fa930d316db6dfbf236ddbc8abb3bbea6edf9deafd39b8efac7ae014e7ffbdd97cfebe7ec84a72a7b323fe77afffd7f1c8eaec48e6ff3fdf7da9fffaf2d57e961d7f5debc9afe7ff32d72ff374d3ff57fff2fbffdbe9833405fcbabff8beb8ff1e55f53b2d6e96bdf7e7fbfd0fb6b130071c13cf5de5be5ef8ade46a2dd53d77ff69eff7ff946ad4e32febddff87b73fceeaf2ce94adafbb9fddeff8fbf11f4161ad6cb29dbe5f2ffe6ee2023ceeb79c76d7fff7bbffaf4485b6f6f3b7f97f9f75ce372c173177fedd65e5fb76fdc5bbf7a737f9bdddefefff1f7a5533dd1efde7ffafabcd96e1193e3cadb93e76fcdfdb4fa533bc3d3a7eff5eebc9f3ffce91aa51bbf5e7f6bcfd7dfbfd1928c0726f3f26ef8f3ffe2eb8843cbb1dfd2eabfe4f7ffec47a95263e5c65737affe73e3e3735d61e8cbffdbf75e37fc04a991ad7ff7feffa6fafdfed988b50fdf379ffdfff2f7eeb6738c0ceffff77f32fe7f2b22c866514db75f7c3df6e5fd210a70bb4bbf31bcfb6d325f4a00b06ed7d34c9dbbdffff3fd6fd8ed570d7def1dcf789ff5ae040339ff35deb5e37bedf889d83bcf5feffb77e57ffcfdd8edfd91bb8bffd3b7dbd8fea3083734c3d7dedd9ffefcb78c5d87e1919f5655bf5bf1bfb3dd65fb64ee2fffd777afef18965d03872d73bbfb6fa7df7250f3e8d6ef7ff9ffffeadff5e39abf8727fde93febddfffee3096ca1779ceabbf7ff7bda2f756353be9dfabf2bcff6feec1cad233fffbf9ecefffffa21b7b8b17ffded7ff7fef56ee44b02d9bdf3d3cf42aa777fd90ba9b08af4cfd5b797dadb3694bf3282abcb39fb2d760f9, bigint) + + def test_version_1_noisemap_indexerror(self): + versioned_seed = RevealerPlugin.get_versioned_seed_from_user_input('1A082CBDC627FFA37ABD154A64AD2565D725') + noise_map = RevealerPlugin.get_noise_map(versioned_seed) + bigint = 0 + for (x, y), pixel in noise_map.items(): + if pixel: + bigint |= 1 << (y*RevealerPlugin.SIZE[1]+x) + self.assertEqual(0x20bdba94d80b604107d92e4bb77fbdff66cbd769d14d31cc26fdbb77fffff237db49c1cb6bf5cfd4fbb7e169dfc0213b57ffbf7e7fb3dffbdce1f92bbb595efffbefeeb06e00cd6fbfcf5572e6d4f376843fed920475dfdad7dbfffedf1f5fb9de7f67ff77aefefb753daebce8eeb77ff7e35eaeefaf48fe37c7fd71ecfa96faf6d49bd1b60e29cbff7ff2f5fe7daa83d231efdfdfc2ffd17887d9aa79b1713f16dfcf75f7fa1e64c574b7abbea7abfff9c0e27b9f55cbdedffbff3eff95508796eb8bbf9dfffc367f9b6b1b59337ffebf3ff9fecdffd61a6febbc3aafff379dbafcfc814c56bfaffdd1fb7ffeb35a1c293e9f393e35b5d70fbeca50020427db9ecabaf3fff9a2211ee279fdffb53973fbbefa0d2d9270fe2fef5ffbaffefd753cc251ebff7ff352e3f77fdd50bd3975ee3ffcdff6f5fe199284436d7adfbffbbfaf9efabde086fe79bfdf21fb7efba8079b7eae57ffbabff97b3e7463300e53ff5fd5f295ffe7340062f84f689dadfd3ff13feaef33d83ef7fa9aea65fff1f53021f5baef9ffddfbfbf5dff0ad38328f03ebfadfcffdffdc45e2a9a1ef64fbe4f1b7f7fb567039baafffdffed87f7bb8784f28e7dffd7d7fbfff2e7c0d13cf0d7ff77baffefbf73f6fe851607ef9fb5bf7fd5e8fe0d07d6e9bf7de7effd3b7cbd8e55336e7fef8fcbc67fb9a74ba6481d7dfdc6f3fedd3ffe87e7184216dcffd7dfef7b7a6024b2c4d5ffc72d99d7cd5ff6da6717ecf2ffffceffb6e686a4fb4d57fbcebf7ef9b3ae4c5a553340bafff7f7377bffbe3dae1fb43b738ac7ebd5dfed0c8537fe4b0ddc277b7ffbeff63c618bebcdffe9bdff6f9e52be359a5feac691ffbcdf6abb62102b09fa66f9ffafd4b7f73d88a81e3ff2be4b7f7feba2a2566203f7af53ef7253ebf64ac799ff9e7dcff7fadadbffd23fc9b2fd9efecaeaffcff04efa1e0866fe1f9379ffaed4b9792af1b63fffceed58fcff21bac450bb7ffd9aebeffef5d6525510a55b5ffffef78f7bfa1c94ff7f3d7fcbe3df7d3fbc5a0f710157befdeff762edf2bcfed7fb7ef96ff17dd6bddb9dcf01126dfee1f5b33bf1ef3c4c21c13ffbfbfd985fbfd3804539d95ba5def9dfcdceb7b17c6bf29df6dbefbe7fb9fd9f4fd39a8fb4727b87ee579d88721c9bb3f7ffffb7ffd9df369daca81d57d5ffd0bfdeffaf1434ffcb5b6af7f7eddebdbe403b7d4abfec6f719f6b9fee205da43d79fdef7c7dbfdef2f0f61b98fb39efb43cf7eeee082d36101abb73d9effbbf77dc474f6f47fbbebb1f36b76df0dffa846efedbb7affdfbf77222153dff7fdd7dffff5e81f3b5a8ee07ff3bdfe9b77ef1fecabd51c4ef272feffebeefea7334e38d7fc5effce7ffaccffdd18506bee13bffddefbd4f092fbf6e57dddfbfd7fefdf78303790e7b1befc77f6bbcbf9348c44b37fbee7feebffb57217373b0febef97effeb29ff79daad6df17f7fedfde7eeff727c6b54217fcfbb56ffeeffbc492064ad04cff6ff9b7c2efe308364110873875bff4bb73f5c2666500afbdbbc75ff9bebc5be4465eafbbffb7ff7b7fdc8bb7dee0747df7fcfd76a4bfe5e698aa3f37fe2bffbffb65b780150337efb7a21fdfb747d14fd95ceefd7f7cfe0f6db3c0ca5eb52fb7fe82ff7f7acdea3f9ddabf48ceec4ce41bb13, + bigint) From f0868f5a51614178d92f531ec99a041e90034e9b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 15 Dec 2018 09:26:54 +0100 Subject: [PATCH 272/301] revealer: warning re version 0 vulnerability --- electrum/plugins/revealer/qt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index e9ed73ba..80907e4b 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -199,7 +199,15 @@ class Plugin(RevealerPlugin): self.txt = self.text.text() self.seed_img(is_seed=False) + def warn_old_revealer(self): + if self.versioned_seed.version == '0': + self.d.show_warning(''.join( + ["", _("Warning"), ": ", + _("Revealers starting with 0 are not secure due to a vulnerability."), ' ', + _("Proceed at your own risk.")])) + def cypherseed_dialog(self, window): + self.warn_old_revealer() d = WindowModalDialog(window, "Encryption Dialog") d.setMinimumWidth(500) From c59ac49feadeb6905aef17990b0f93414fd93574 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 17 Dec 2018 13:41:00 +0100 Subject: [PATCH 273/301] fix greenaddress plugin: follow-up 75f6ab913316998e1e3c4b1d256de17bc367ea8e --- electrum/plugins/greenaddress_instant/qt.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/plugins/greenaddress_instant/qt.py b/electrum/plugins/greenaddress_instant/qt.py index fefc834f..73352aae 100644 --- a/electrum/plugins/greenaddress_instant/qt.py +++ b/electrum/plugins/greenaddress_instant/qt.py @@ -26,6 +26,7 @@ import base64 import urllib.parse import sys +from typing import TYPE_CHECKING from PyQt5.QtWidgets import QApplication, QPushButton @@ -33,6 +34,9 @@ from electrum.plugin import BasePlugin, hook from electrum.i18n import _ from electrum.network import Network +if TYPE_CHECKING: + from aiohttp import ClientResponse + class Plugin(BasePlugin): @@ -89,15 +93,17 @@ class Plugin(BasePlugin): sig = base64.b64encode(sig).decode('ascii') # 2. send the request + async def handle_request(resp: 'ClientResponse'): + resp.raise_for_status() + return await resp.json() url = "https://greenaddress.it/verify/?signature=%s&txhash=%s" % (urllib.parse.quote(sig), tx.txid()) - response = Network.send_http_on_proxy('get', url, headers = {'User-Agent': 'Electrum'}) - response = response.json() + response = Network.send_http_on_proxy('get', url, headers = {'User-Agent': 'Electrum'}, on_finish=handle_request) # 3. display the result if response.get('verified'): d.show_message(_('{} is covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification successful!')) else: - d.show_critical(_('{} is not covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification failed!')) + d.show_warning(_('{} is not covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification failed!')) except BaseException as e: import traceback traceback.print_exc(file=sys.stdout) From 0c9a03ac5481dfdf8e7b3e86d8a2e51e2c2a7463 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Dec 2018 15:37:29 +0100 Subject: [PATCH 274/301] keystore: revert KDF change from #4838 making the KDF expensive is blocked on #4909 --- electrum/crypto.py | 53 +++++++++++++++------------------- electrum/tests/test_bitcoin.py | 10 +++---- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/electrum/crypto.py b/electrum/crypto.py index 7df21156..3046f0c1 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -116,9 +116,11 @@ def DecodeAES_bytes(secret: bytes, ciphertext: bytes) -> bytes: return s -PW_HASH_VERSION_LATEST = 2 -KNOWN_PW_HASH_VERSIONS = (1, 2) +PW_HASH_VERSION_LATEST = 1 +KNOWN_PW_HASH_VERSIONS = (1, 2, ) +SUPPORTED_PW_HASH_VERSIONS = (1, ) assert PW_HASH_VERSION_LATEST in KNOWN_PW_HASH_VERSIONS +assert PW_HASH_VERSION_LATEST in SUPPORTED_PW_HASH_VERSIONS class UnexpectedPasswordHashVersion(InvalidPassword): @@ -126,23 +128,30 @@ class UnexpectedPasswordHashVersion(InvalidPassword): self.version = version def __str__(self): - return "{unexpected}: {version}\n{please_update}".format( + return "{unexpected}: {version}\n{instruction}".format( unexpected=_("Unexpected password hash version"), version=self.version, - please_update=_('You are most likely using an outdated version of Electrum. Please update.')) + instruction=_('You are most likely using an outdated version of Electrum. Please update.')) -def _hash_password(password: Union[bytes, str], *, version: int, salt: bytes) -> bytes: +class UnsupportedPasswordHashVersion(InvalidPassword): + def __init__(self, version): + self.version = version + + def __str__(self): + return "{unsupported}: {version}\n{instruction}".format( + unsupported=_("Unsupported password hash version"), + version=self.version, + instruction=f"To open this wallet, try 'git checkout password_v{self.version}'.\n" + "Alternatively, restore from seed.") + + +def _hash_password(password: Union[bytes, str], *, version: int) -> bytes: pw = to_bytes(password, 'utf8') + if version not in SUPPORTED_PW_HASH_VERSIONS: + raise UnsupportedPasswordHashVersion(version) if version == 1: return sha256d(pw) - elif version == 2: - if not isinstance(salt, bytes) or len(salt) < 16: - raise Exception('too weak salt', salt) - return hashlib.pbkdf2_hmac(hash_name='sha256', - password=pw, - salt=b'ELECTRUM_PW_HASH_V2'+salt, - iterations=50_000) else: assert version not in KNOWN_PW_HASH_VERSIONS raise UnexpectedPasswordHashVersion(version) @@ -154,17 +163,9 @@ def pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> if version not in KNOWN_PW_HASH_VERSIONS: raise UnexpectedPasswordHashVersion(version) # derive key from password - if version == 1: - salt = b'' - elif version == 2: - salt = bytes(os.urandom(16)) - else: - assert False, version - secret = _hash_password(password, version=version, salt=salt) + secret = _hash_password(password, version=version) # encrypt given data - e = EncodeAES_bytes(secret, to_bytes(data, "utf8")) - # return base64(salt + encrypted data) - ciphertext = salt + e + ciphertext = EncodeAES_bytes(secret, to_bytes(data, "utf8")) ciphertext_b64 = base64.b64encode(ciphertext) return ciphertext_b64.decode('utf8') @@ -176,13 +177,7 @@ def pw_decode(data: str, password: Union[bytes, str, None], *, version: int) -> raise UnexpectedPasswordHashVersion(version) data_bytes = bytes(base64.b64decode(data)) # derive key from password - if version == 1: - salt = b'' - elif version == 2: - salt, data_bytes = data_bytes[:16], data_bytes[16:] - else: - assert False, version - secret = _hash_password(password, version=version, salt=salt) + secret = _hash_password(password, version=version) # decrypt given data try: d = to_string(DecodeAES_bytes(secret, data_bytes), "utf8") diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index 752e5017..2b74907f 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -11,7 +11,7 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key, from electrum.bip32 import (bip32_root, bip32_public_derivation, bip32_private_derivation, xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation, is_xpub, convert_bip32_path_to_list_of_uint32) -from electrum.crypto import sha256d, KNOWN_PW_HASH_VERSIONS +from electrum.crypto import sha256d, SUPPORTED_PW_HASH_VERSIONS from electrum import ecc, crypto, constants from electrum.ecc import number_to_string, string_to_number from electrum.transaction import opcodes @@ -219,7 +219,7 @@ class Test_bitcoin(SequentialTestCase): """Make sure AES is homomorphic.""" payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' password = u'secret' - for version in KNOWN_PW_HASH_VERSIONS: + for version in SUPPORTED_PW_HASH_VERSIONS: enc = crypto.pw_encode(payload, password, version=version) dec = crypto.pw_decode(enc, password, version=version) self.assertEqual(dec, payload) @@ -228,7 +228,7 @@ class Test_bitcoin(SequentialTestCase): def test_aes_encode_without_password(self): """When not passed a password, pw_encode is noop on the payload.""" payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' - for version in KNOWN_PW_HASH_VERSIONS: + for version in SUPPORTED_PW_HASH_VERSIONS: enc = crypto.pw_encode(payload, None, version=version) self.assertEqual(payload, enc) @@ -236,7 +236,7 @@ class Test_bitcoin(SequentialTestCase): def test_aes_deencode_without_password(self): """When not passed a password, pw_decode is noop on the payload.""" payload = u'\u66f4\u7a33\u5b9a\u7684\u4ea4\u6613\u5e73\u53f0' - for version in KNOWN_PW_HASH_VERSIONS: + for version in SUPPORTED_PW_HASH_VERSIONS: enc = crypto.pw_decode(payload, None, version=version) self.assertEqual(payload, enc) @@ -246,7 +246,7 @@ class Test_bitcoin(SequentialTestCase): payload = u"blah" password = u"uber secret" wrong_password = u"not the password" - for version in KNOWN_PW_HASH_VERSIONS: + for version in SUPPORTED_PW_HASH_VERSIONS: enc = crypto.pw_encode(payload, password, version=version) with self.assertRaises(InvalidPassword): crypto.pw_decode(enc, wrong_password, version=version) From 8f5f0e46aac97c923c5ca826177781389d6a5486 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Dec 2018 19:57:58 +0100 Subject: [PATCH 275/301] keystore: fail sooner if unsupported version follow-up #4937 --- electrum/crypto.py | 6 +++--- electrum/keystore.py | 5 ++++- electrum/storage.py | 5 +++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/electrum/crypto.py b/electrum/crypto.py index 3046f0c1..a206abad 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -31,7 +31,7 @@ from typing import Union import pyaes -from .util import assert_bytes, InvalidPassword, to_bytes, to_string +from .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFileException from .i18n import _ @@ -123,7 +123,7 @@ assert PW_HASH_VERSION_LATEST in KNOWN_PW_HASH_VERSIONS assert PW_HASH_VERSION_LATEST in SUPPORTED_PW_HASH_VERSIONS -class UnexpectedPasswordHashVersion(InvalidPassword): +class UnexpectedPasswordHashVersion(InvalidPassword, WalletFileException): def __init__(self, version): self.version = version @@ -134,7 +134,7 @@ class UnexpectedPasswordHashVersion(InvalidPassword): instruction=_('You are most likely using an outdated version of Electrum. Please update.')) -class UnsupportedPasswordHashVersion(InvalidPassword): +class UnsupportedPasswordHashVersion(InvalidPassword, WalletFileException): def __init__(self, version): self.version = version diff --git a/electrum/keystore.py b/electrum/keystore.py index e0e21fa6..8c7e80de 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -35,7 +35,8 @@ from .bip32 import (bip32_public_derivation, deserialize_xpub, CKD_pub, bip32_private_key, bip32_derivation, BIP32_PRIME, is_xpub, is_xprv) from .ecc import string_to_number, number_to_string -from .crypto import (pw_decode, pw_encode, sha256d, PW_HASH_VERSION_LATEST) +from .crypto import (pw_decode, pw_encode, sha256d, PW_HASH_VERSION_LATEST, + SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion) from .util import (PrintError, InvalidPassword, hfu, WalletFileException, BitcoinException, bh2u, bfh, print_error, inv_dict) from .mnemonic import Mnemonic, load_wordlist @@ -95,6 +96,8 @@ class Software_KeyStore(KeyStore): def __init__(self, d): KeyStore.__init__(self) self.pw_hash_version = d.get('pw_hash_version', 1) + if self.pw_hash_version not in SUPPORTED_PW_HASH_VERSIONS: + raise UnsupportedPasswordHashVersion(self.pw_hash_version) def may_have_password(self): return not self.is_watching_only() diff --git a/electrum/storage.py b/electrum/storage.py index 7dcce567..2ac3f46b 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -575,6 +575,11 @@ class WalletStorage(JsonDB): self.put('verified_tx3', None) self.put('seed_version', 18) + # def convert_version_19(self): + # TODO for "next" upgrade: + # - move "pw_hash_version" from keystore to storage + # pass + def convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return From 8d1cb3c36a7134ef79e1443da7b6fa8c4d642449 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Dec 2018 20:52:01 +0100 Subject: [PATCH 276/301] bump pyqt version in binaries closes #4777 --- contrib/requirements/requirements-binaries.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/requirements/requirements-binaries.txt b/contrib/requirements/requirements-binaries.txt index 9faf682e..10a1c8d9 100644 --- a/contrib/requirements/requirements-binaries.txt +++ b/contrib/requirements/requirements-binaries.txt @@ -1,2 +1,2 @@ -PyQt5<5.11 +PyQt5 pycryptodomex From ba4af29bf82f7addb68c3ed996002665313ab223 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Dec 2018 21:01:16 +0100 Subject: [PATCH 277/301] rerun freeze_packages --- .../requirements-binaries.txt | 36 ++++++------ .../deterministic-build/requirements-hw.txt | 58 +++++++++---------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt index 1c4ff76f..e615ae3a 100644 --- a/contrib/deterministic-build/requirements-binaries.txt +++ b/contrib/deterministic-build/requirements-binaries.txt @@ -29,27 +29,27 @@ pycryptodomex==3.7.2 \ --hash=sha256:ec51ea9f11b11df2719a01ea1cacdcf80858542a93f530a25a3bc742d0fe2f4b \ --hash=sha256:ef2792281138a29e54bdd7302fdab72be140192485dc622bb9e7e9a6f9cee4f9 \ --hash=sha256:f79650ec8812b01f20aca503763b93b0b1b347423ecf9fd3a9ebb611bee84079 -PyQt5==5.10.1 \ - --hash=sha256:1e652910bd1ffd23a3a48c510ecad23a57a853ed26b782cd54b16658e6f271ac \ - --hash=sha256:4db7113f464c733a99fcb66c4c093a47cf7204ad3f8b3bda502efcc0839ac14b \ - --hash=sha256:9c17ab3974c1fc7bbb04cc1c9dae780522c0ebc158613f3025fccae82227b5f7 \ - --hash=sha256:f6035baa009acf45e5f460cf88f73580ad5dc0e72330029acd99e477f20a5d61 +PyQt5==5.11.3 \ + --hash=sha256:517e4339135c4874b799af0d484bc2e8c27b54850113a68eec40a0b56534f450 \ + --hash=sha256:ac1eb5a114b6e7788e8be378be41c5e54b17d5158994504e85e43b5fca006a39 \ + --hash=sha256:d2309296a5a79d0a1c0e6c387c30f0398b65523a6dcc8a19cc172e46b949e00d \ + --hash=sha256:e85936bae1581bcb908847d2038e5b34237a5e6acc03130099a78930770e7ead +PyQt5-sip==4.19.13 \ + --hash=sha256:125f77c087572c9272219cda030a63c2f996b8507592b2a54d7ef9b75f9f054d \ + --hash=sha256:14c37b06e3fb7c2234cb208fa461ec4e62b4ba6d8b32ca3753c0b2cfd61b00e3 \ + --hash=sha256:1cb2cf52979f9085fc0eab7e0b2438eb4430d4aea8edec89762527e17317175b \ + --hash=sha256:4babef08bccbf223ec34464e1ed0a23caeaeea390ca9a3529227d9a57f0d6ee4 \ + --hash=sha256:53cb9c1208511cda0b9ed11cffee992a5a2f5d96eb88722569b2ce65ecf6b960 \ + --hash=sha256:549449d9461d6c665cbe8af4a3808805c5e6e037cd2ce4fd93308d44a049bfac \ + --hash=sha256:5f5b3089b200ff33de3f636b398e7199b57a6b5c1bb724bdb884580a072a14b5 \ + --hash=sha256:a4d9bf6e1fa2dd6e73f1873f1a47cee11a6ba0cf9ba8cf7002b28c76823600d0 \ + --hash=sha256:a4ee6026216f1fbe25c8847f9e0fbce907df5b908f84816e21af16ec7666e6fe \ + --hash=sha256:a91a308a5e0cc99de1e97afd8f09f46dd7ca20cfaa5890ef254113eebaa1adff \ + --hash=sha256:b0342540da479d2713edc68fb21f307473f68da896ad5c04215dae97630e0069 \ + --hash=sha256:f997e21b4e26a3397cb7b255b8d1db5b9772c8e0c94b6d870a5a0ab5c27eacaa setuptools==40.6.3 \ --hash=sha256:3b474dad69c49f0d2d86696b68105f3a6f195f7ab655af12ef9a9c326d2b08f8 \ --hash=sha256:e2c1ce9a832f34cf7a31ed010aabcab5008eb65ce8f2aadc04622232c14bdd0b -SIP==4.19.8 \ - --hash=sha256:09f9a4e6c28afd0bafedb26ffba43375b97fe7207bd1a0d3513f79b7d168b331 \ - --hash=sha256:105edaaa1c8aa486662226360bd3999b4b89dd56de3e314d82b83ed0587d8783 \ - --hash=sha256:1bb10aac55bd5ab0e2ee74b3047aa2016cfa7932077c73f602a6f6541af8cd51 \ - --hash=sha256:265ddf69235dd70571b7d4da20849303b436192e875ce7226be7144ca702a45c \ - --hash=sha256:52074f7cb5488e8b75b52f34ec2230bc75d22986c7fe5cd3f2d266c23f3349a7 \ - --hash=sha256:5ff887a33839de8fc77d7f69aed0259b67a384dc91a1dc7588e328b0b980bde2 \ - --hash=sha256:74da4ddd20c5b35c19cda753ce1e8e1f71616931391caeac2de7a1715945c679 \ - --hash=sha256:7d69e9cf4f8253a3c0dfc5ba6bb9ac8087b8239851f22998e98cb35cfe497b68 \ - --hash=sha256:97bb93ee0ef01ba90f57be2b606e08002660affd5bc380776dd8b0fcaa9e093a \ - --hash=sha256:cf98150a99e43fda7ae22abe655b6f202e491d6291486548daa56cb15a2fcf85 \ - --hash=sha256:d9023422127b94d11c1a84bfa94933e959c484f2c79553c1ef23c69fe00d25f8 \ - --hash=sha256:e72955e12f4fccf27aa421be383453d697b8a44bde2cc26b08d876fd492d0174 wheel==0.32.3 \ --hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \ --hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44 diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 44125520..60ff68b6 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -14,35 +14,35 @@ click==7.0 \ --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 construct==2.9.45 \ --hash=sha256:2271a0efd0798679dea825ff47e22a4c550456a5db0ba8baa82f7eae0af0118c -Cython==0.29.1 \ - --hash=sha256:0202f753b0a69dd87095b698df00010daf452ab61279747248a042a24892a2a9 \ - --hash=sha256:0fbe9514ffe35aad337db27b11f7ee1bf27d01059b2e27f112315b185d69de79 \ - --hash=sha256:18ab7646985a97e02cee72e1ddba2e732d4931d4e1732494ff30c5aa084bfb97 \ - --hash=sha256:18bb95daa41fd2ff0102844172bc068150bf031186249fc70c6f57fc75c9c0a9 \ - --hash=sha256:222c65c7022ff52faf3ac6c706e4e8a726ddaa29dabf2173b2a0fdfc1a2f1586 \ - --hash=sha256:2387c5a2a436669de9157d117fd426dfc2b46ffdc49e43f0a2267380896c04ea \ - --hash=sha256:31bad130b701587ab7e74c3c304bb3d63d9f0d365e3f81880203e8e476d914b1 \ - --hash=sha256:3895014b1a653726a9da5aca852d9e6d0e2c2667bf315d6a2cd632bf7463130b \ - --hash=sha256:3d38967ef9c1c0ffabe80827f56817609153e2da83e3dce84476d0928c72972c \ - --hash=sha256:5478efd92291084adc9b679666aeaeaafca69d6bf3e95fe3efce82814e3ab782 \ - --hash=sha256:5c2a6121e4e1e65690b60c270012218e38201bcf700314b1926d5dbeae78a499 \ - --hash=sha256:5f66f7f76fc870500fe6db0c02d5fc4187062d29e582431f5a986881c5aef4e3 \ - --hash=sha256:6572d74990b16480608441b941c1cefd60bf742416bc3668cf311980f740768d \ - --hash=sha256:6990b9965f31762ac71340869c064f39fb6776beca396d0558d3b5b1ebb7f027 \ - --hash=sha256:87c82803f9c51c275b16c729aade952ca93c74a8aec963b9b8871df9bbb3120a \ - --hash=sha256:8fd32974024052b2260d08b94f970c4c1d92c327ed3570a2b4708070fa53a879 \ - --hash=sha256:9a81bba33c7fbdb76e6fe8d15b6e793a1916afd4d2463f07d762c69efaaea466 \ - --hash=sha256:9c31cb9bfaa1004a2a50115a37e1fcb79d664917968399dae3e04610356afe8c \ - --hash=sha256:a0b28235c28a088e052f90a0b5fefaa503e5378046a29d0af045e2ec9d5d6555 \ - --hash=sha256:a3f5022d818b6c91a8bbc466211e6fd708f234909cbb10bc4dbccb2a04884ef6 \ - --hash=sha256:a7252ca498f510404185e3c1bdda3224e80b1be1a5fbc2b174aab83a477ea0cb \ - --hash=sha256:aa8d7136cad8b2a7bf3596e1bc053476edeee567271f197449b2d30ea0c37175 \ - --hash=sha256:b50a8de6f2820286129fe7d71d76c9e0c0f53a8c83cf39bbe6375b827994e4f1 \ - --hash=sha256:b528a9c152c569062375d5c2260b59f8243bb4136fc38420854ac1bd4aa0d02f \ - --hash=sha256:b72db7201a4aa0445f27af9954d48ed7d2c119ce3b8b253e4dcd514fc72e5dc6 \ - --hash=sha256:d3444e10ccb5b16e4c1bed3cb3c565ec676b20a21eb43430e70ec4168c631dcc \ - --hash=sha256:e16d6f06f4d2161347e51c4bc1f7a8feedeee444d26efa92243f18441a6fa742 \ - --hash=sha256:f5774bef92d33a62a584f6e7552a9a8653241ecc036e259bfb03d33091599537 +Cython==0.29.2 \ + --hash=sha256:004c181b75f926f48dc0570372ca2cfb06a1b3210cb647185977ce9fde98b66e \ + --hash=sha256:085d596c016130f5b1e2fe72446e3e63bfcf67535e7ff6772eaa05c5d2ad6fd5 \ + --hash=sha256:1014758344717844a05882c92ebd76d8fab15b0a8e9b42b378a99a6c5299ab3b \ + --hash=sha256:12c007d3704ca9840734748fd6c052960be67562ff15609c3b85d1ca638289d2 \ + --hash=sha256:1a20f575197e814453f2814829715fcb21436075e298d883a34c7ffe4d567a1d \ + --hash=sha256:1b6f201228368ec9b307261b46512f3605f84d4994bb6eb78cdab71455810424 \ + --hash=sha256:2ac187ff998a95abb7fae452b5178f91e1a713698c9ced89836c94e6b1d3f41e \ + --hash=sha256:3585fbe18d8666d91ecb3b3366ca6e9ea49001cd0a7c38a226cececb7852aa0d \ + --hash=sha256:3669dfe4117ee8825a48cf527cb4ac15a39a0142fcb72ecedfd75fe6545b2cda \ + --hash=sha256:382c1e0f8f8be36e9057264677fd60b669a41c5810113694cbbb4060ee0cefc0 \ + --hash=sha256:44bb606d8c60d8acaa7f72b3bbc2ebd9816785124c58d33c057ca326f1185dae \ + --hash=sha256:6f1a5344ff1f0f44023c41d4b0e52215b490658b42e017520cb89a56250ecbca \ + --hash=sha256:7a29f9d780ac497dcd76ce814a9d170575bedddeb89ecc25fe738abef4c87172 \ + --hash=sha256:8022a5b83ef442584f2efd941fe8678de1c67e28bf81e6671b20627ec8a79387 \ + --hash=sha256:998af90092cd1231990eb878e2c71ed92716d6a758aa03a2e6673e077a7dd072 \ + --hash=sha256:9e60b83afe8914ab6607f7150fd282d1cb0531a45cf98d2a40159f976ae4cd7a \ + --hash=sha256:a6581d3dda15adea19ac70b89211aadbf21f45c7f3ee3bc8e1536e5437c9faf9 \ + --hash=sha256:af515924b8aebb729f631590eb43300ce948fa67d3885fdae9238717c0a68821 \ + --hash=sha256:b49ea3bb357bc34eaa7b461633ce7c2d79186fe681297115ff9e2b8f5ceba2fd \ + --hash=sha256:bc524cc603f0aa23af00111ddd1aa0aad12d629f5a9a5207f425a1af66393094 \ + --hash=sha256:ca7daccffb14896767b20d69bfc8de9e41e9589b9377110292c3af8460ef9c2b \ + --hash=sha256:cdfb68eb11c6c4e90e34cf54ffd678a7813782fae980d648db6185e6b0c8a0ba \ + --hash=sha256:d21fb6e7a3831f1f8346275839d46ed1eb2abd350fc81bad2fdf208cc9e4f998 \ + --hash=sha256:e17104c6871e7c0eee4de12717892a1083bd3b8b1da0ec103fa464b1c6c80964 \ + --hash=sha256:e7f71b489959d478cff72d8dcab1531efd43299341e3f8b85ab980beec452ded \ + --hash=sha256:e8420326e4b40bcbb06f070efb218ca2ca21827891b7c69d4cc4802b3ce1afc9 \ + --hash=sha256:eec1b890cf5c16cb656a7062769ac276c0fccf898ce215ff8ef75eac740063f7 \ + --hash=sha256:f04e21ba7c117b20f57b0af2d4c8ed760495e5bb3f21b0352dbcfe5d2221678b ecdsa==0.13 \ --hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \ --hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa From fa33d1880c572820d69c02536242f2b0475e502c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Dec 2018 21:41:29 +0100 Subject: [PATCH 278/301] win build: bump python version --- contrib/build-wine/prepare-wine.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 481bbe44..24196d22 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -13,7 +13,7 @@ LIBUSB_FILENAME=libusb-1.0.22.7z LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.22/$LIBUSB_FILENAME?download LIBUSB_SHA256=671f1a420757b4480e7fadc8313d6fb3cbb75ca00934c417c1efa6e77fb8779b -PYTHON_VERSION=3.6.6 +PYTHON_VERSION=3.6.7 ## These settings probably don't need change export WINEPREFIX=/opt/wine64 From f160f4bf6791ff70edff0730ead9fe5c476c7087 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Dec 2018 20:33:13 +0100 Subject: [PATCH 279/301] mac build: use old xcode to build qr scanner on El Capitan --- contrib/osx/README.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/contrib/osx/README.md b/contrib/osx/README.md index 056d9fb8..f65a32e2 100644 --- a/contrib/osx/README.md +++ b/contrib/osx/README.md @@ -3,22 +3,40 @@ Building Mac OS binaries This guide explains how to build Electrum binaries for macOS systems. -The build process consists of two steps: ## 1. Building the binary -This needs to be done on a system running macOS or OS X. We use El Capitan (10.11.6) as building it on High Sierra +This needs to be done on a system running macOS or OS X. We use El Capitan (10.11.6) as building it +on High Sierra (or later) makes the binaries incompatible with older versions. -Before starting, make sure that the Xcode command line tools are installed (e.g. you have `git`). +#### 1.1 Get Xcode + +Building the QR code reader (CalinsQRReader) requires full Xcode (not just command line tools). + +The last Xcode version compatible with El Capitan is Xcode 8.2.1 + +Get it from [here](https://developer.apple.com/download/more/). + +Unfortunately, you need an "Apple ID" account. + +After downloading, uncompress it. + +Make sure it is the "selected" xcode (e.g.): + + sudo xcode-select -s $HOME/Downloads/Xcode.app/Contents/Developer/ + + +#### 1.2 Build Electrum cd electrum ./contrib/osx/make_osx -This creates a folder named Electrum.app. +This creates both a folder named Electrum.app and the .dmg file. -## 2. Building the image + +## 2. Building the image deterministically (WIP) The usual way to distribute macOS applications is to use image files containing the application. Although these images can be created on a Mac with the built-in `hdiutil`, they are not deterministic. @@ -33,4 +51,4 @@ Copy the Electrum.app directory over and install the dependencies, e.g.: Then you can just invoke `package.sh` with the path to the app: cd electrum - ./contrib/osx/package.sh ~/Electrum.app/ \ No newline at end of file + ./contrib/osx/package.sh ~/Electrum.app/ From f54c3871721a3f524e8c8df6991bb312dc683460 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Dec 2018 20:33:47 +0100 Subject: [PATCH 280/301] mac build: bump python version --- contrib/osx/make_osx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/osx/make_osx b/contrib/osx/make_osx index b1cf4b72..fd4a09f4 100755 --- a/contrib/osx/make_osx +++ b/contrib/osx/make_osx @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Parameterize -PYTHON_VERSION=3.6.4 +PYTHON_VERSION=3.6.7 BUILDDIR=/tmp/electrum-build PACKAGE=Electrum GIT_REPO=https://github.com/spesmilo/electrum @@ -73,6 +73,8 @@ info "Downloading libusb..." curl https://homebrew.bintray.com/bottles/libusb-1.0.22.el_capitan.bottle.tar.gz | \ 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" From bec18601977b9daf57309f9e6e1bd41fcc32dc53 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Dec 2018 21:33:26 +0100 Subject: [PATCH 281/301] mac build: build qr scanner on separate machine --- contrib/osx/README.md | 18 +++++++++++++++--- contrib/osx/make_osx | 3 ++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/contrib/osx/README.md b/contrib/osx/README.md index f65a32e2..ca404ff1 100644 --- a/contrib/osx/README.md +++ b/contrib/osx/README.md @@ -8,12 +8,13 @@ This guide explains how to build Electrum binaries for macOS systems. This needs to be done on a system running macOS or OS X. We use El Capitan (10.11.6) as building it on High Sierra (or later) -makes the binaries incompatible with older versions. +makes the binaries [incompatible with older versions](https://github.com/pyinstaller/pyinstaller/issues/1191). +Before starting, make sure that the Xcode command line tools are installed (e.g. you have `git`). -#### 1.1 Get Xcode +#### 1.1a Get Xcode -Building the QR code reader (CalinsQRReader) requires full Xcode (not just command line tools). +Building the QR scanner (CalinsQRReader) requires full Xcode (not just command line tools). The last Xcode version compatible with El Capitan is Xcode 8.2.1 @@ -27,6 +28,17 @@ Make sure it is the "selected" xcode (e.g.): sudo xcode-select -s $HOME/Downloads/Xcode.app/Contents/Developer/ +#### 1.1b Build QR scanner separately on newer Mac + +Alternatively, you can try building just the QR scanner on newer macOS. + +On newer Mac, run: + + pushd contrib/osx/CalinsQRReader; xcodebuild; popd + cp -r contrib/osx/CalinsQRReader/build prebuilt_qr + +Move `prebuilt_qr` to El Capitan: `contrib/osx/CalinsQRReader/prebuilt_qr`. + #### 1.2 Build Electrum diff --git a/contrib/osx/make_osx b/contrib/osx/make_osx index fd4a09f4..17eb3fb9 100755 --- a/contrib/osx/make_osx +++ b/contrib/osx/make_osx @@ -94,7 +94,8 @@ info "Building CalinsQRReader..." d=contrib/osx/CalinsQRReader pushd $d rm -fr build -xcodebuild || fail "Could not build CalinsQRReader" +# prefer building using xcode ourselves. otherwise fallback to prebuilt binary +xcodebuild || cp -r prebuilt_qr build || fail "Could not build CalinsQRReader" popd DoCodeSignMaybe "CalinsQRReader.app" "${d}/build/Release/CalinsQRReader.app" "$APP_SIGN" # If APP_SIGN is empty will be a noop From ba33bc4ad85e759d67093fcc1edae51e6ab5a692 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Dec 2018 02:10:47 +0100 Subject: [PATCH 282/301] plugins: fix hook/attr name collision in close() Revealer plugin has method "password_dialog" "password_dialog" is also a hook name, but revealer.password_dialog is not a hook --- electrum/plugin.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/electrum/plugin.py b/electrum/plugin.py index f24206c6..f9072601 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -251,11 +251,16 @@ class BasePlugin(PrintError): def close(self): # remove self from hooks - for k in dir(self): - if k in hook_names: - l = hooks.get(k, []) - l.remove((self, getattr(self, k))) - hooks[k] = l + for attr_name in dir(self): + if attr_name in hook_names: + # found attribute in self that is also the name of a hook + l = hooks.get(attr_name, []) + try: + l.remove((self, getattr(self, attr_name))) + except ValueError: + # maybe attr name just collided with hook name and was not hook + continue + hooks[attr_name] = l self.parent.close_plugin(self) self.on_close() From 02478024792aa642bab2ba108ddf0d9fff8cb396 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Dec 2018 13:29:50 +0100 Subject: [PATCH 283/301] update submodules --- contrib/deterministic-build/electrum-icons | 2 +- contrib/deterministic-build/electrum-locale | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/deterministic-build/electrum-icons b/contrib/deterministic-build/electrum-icons index 0b8cbcca..bce0d7a4 160000 --- a/contrib/deterministic-build/electrum-icons +++ b/contrib/deterministic-build/electrum-icons @@ -1 +1 @@ -Subproject commit 0b8cbcca428ceb791527bcbb2ef2b36b4ab29c73 +Subproject commit bce0d7a427ecf2106bf4d1ec56feb4067a50b234 diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale index 27e36687..caba5f30 160000 --- a/contrib/deterministic-build/electrum-locale +++ b/contrib/deterministic-build/electrum-locale @@ -1 +1 @@ -Subproject commit 27e36687f4b0fbd126628bdde80758b63ade7347 +Subproject commit caba5f30b134ec08d3e29150b33c87200597181a From f0f73380a2e79422609788baf563ab078411642b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Dec 2018 16:47:26 +0100 Subject: [PATCH 284/301] qt history: fix refresh bug ("verified"/fee histogram interplay) --- electrum/gui/qt/history_list.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 3f0c7ee4..0fac1ea1 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -273,22 +273,27 @@ class HistoryModel(QAbstractItemModel, PrintError): self.dataChanged.emit(idx, idx, [Qt.DisplayRole, Qt.ForegroundRole]) def update_tx_mined_status(self, tx_hash: str, tx_mined_info: TxMinedInfo): - self.tx_status_cache[tx_hash] = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) try: row = self.transactions.pos_from_key(tx_hash) + tx_item = self.transactions[tx_hash] except KeyError: return + self.tx_status_cache[tx_hash] = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) + tx_item.update({ + 'confirmations': tx_mined_info.conf, + 'timestamp': tx_mined_info.timestamp, + 'txpos_in_block': tx_mined_info.txpos, + }) topLeft = self.createIndex(row, 0) bottomRight = self.createIndex(row, len(HistoryColumns) - 1) self.dataChanged.emit(topLeft, bottomRight) def on_fee_histogram(self): - for tx_hash, tx_item in self.transactions.items(): + for tx_hash, tx_item in list(self.transactions.items()): tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) if tx_mined_info.conf > 0: # note: we could actually break here if we wanted to rely on the order of txns in self.transactions continue - self.tx_status_cache[tx_hash] = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) self.update_tx_mined_status(tx_hash, tx_mined_info) def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole): From d5591da6827d5283a646435a5bd591c9934364f7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Dec 2018 17:01:20 +0100 Subject: [PATCH 285/301] qt history: consider column is hidden in context menu --- electrum/gui/qt/history_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 0fac1ea1..3734b1d6 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -561,6 +561,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) for c in self.editable_columns: + if self.isColumnHidden(c): continue label = self.hm.headerData(c, Qt.Horizontal, Qt.DisplayRole) # TODO use siblingAtColumn when min Qt version is >=5.11 persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c)) From b2d635060ca39c5458a01639f718ab71a01590a1 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Dec 2018 18:28:34 +0100 Subject: [PATCH 286/301] update submodule --- contrib/osx/CalinsQRReader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/osx/CalinsQRReader b/contrib/osx/CalinsQRReader index 20189155..48ea51e5 160000 --- a/contrib/osx/CalinsQRReader +++ b/contrib/osx/CalinsQRReader @@ -1 +1 @@ -Subproject commit 20189155a461cf7fbad14357e58fbc8e7c964608 +Subproject commit 48ea51e54a7210c090569464bf30bffbc218cf5b From 383b517405f745454d042d31d702fd491e480f7d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Dec 2018 18:35:15 +0100 Subject: [PATCH 287/301] update submodule (follow-up prev commit) --- contrib/osx/CalinsQRReader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/osx/CalinsQRReader b/contrib/osx/CalinsQRReader index 48ea51e5..59dfc032 160000 --- a/contrib/osx/CalinsQRReader +++ b/contrib/osx/CalinsQRReader @@ -1 +1 @@ -Subproject commit 48ea51e54a7210c090569464bf30bffbc218cf5b +Subproject commit 59dfc03272751cd29ee311456fa34c40f7ebb7c0 From df15571b82727ebbb9aa45fc0a5fb23ea07e22c5 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Dec 2018 18:37:44 +0100 Subject: [PATCH 288/301] osx build: revert to python 3.6.4 --- contrib/osx/make_osx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/osx/make_osx b/contrib/osx/make_osx index 17eb3fb9..76091401 100755 --- a/contrib/osx/make_osx +++ b/contrib/osx/make_osx @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Parameterize -PYTHON_VERSION=3.6.7 +PYTHON_VERSION=3.6.4 BUILDDIR=/tmp/electrum-build PACKAGE=Electrum GIT_REPO=https://github.com/spesmilo/electrum From 4225e794569e8f02f143f4dab0479373ab19f3a1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Dec 2018 21:22:41 +0100 Subject: [PATCH 289/301] win build: wine upstream gpg key weirdness --- contrib/build-wine/docker/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contrib/build-wine/docker/Dockerfile b/contrib/build-wine/docker/Dockerfile index 8ed7a546..20ca548f 100644 --- a/contrib/build-wine/docker/Dockerfile +++ b/contrib/build-wine/docker/Dockerfile @@ -12,7 +12,9 @@ RUN dpkg --add-architecture i386 && \ software-properties-common=0.96.24.32.1 \ && \ wget -nc https://dl.winehq.org/wine-builds/Release.key && \ + wget -nc https://dl.winehq.org/wine-builds/winehq.key && \ apt-key add Release.key && \ + apt-key add winehq.key && \ apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/ && \ apt-get update -q && \ apt-get install -qy \ From fc18912ecdd8538f0c5f51f146fbe038db41e3c9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Dec 2018 21:24:38 +0100 Subject: [PATCH 290/301] release notes: mention 2fa, shorten qt --- RELEASE-NOTES | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 932711f9..3e470f4f 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,4 +1,4 @@ -# Release 3.3.0 - Hodler's Edition (unreleased) +# Release 3.3.0 - Hodler's Edition (December 19, 2018) * The network layer has been rewritten using asyncio and aiorpcx. In addition to easier maintenance, this makes the client @@ -7,6 +7,8 @@ * The blockchain headers and fork handling logic has been generalized. Clients by default now follow chain based on most work, not length. * New wallet creation defaults to native segwit (bech32). + * Segwit 2FA: TrustedCoin now supports native segwit p2wsh + two-factor wallets. * RBF batching (opt-in): If the wallet has an unconfirmed RBF transaction, new payments will be added to that transaction, instead of creating new transactions. @@ -23,17 +25,9 @@ - Trezor: refactoring and compat with python-trezor 0.11 - Digital BitBox: support firmware v5.0.0 * fix bitcoin URI handling when app already running (#4796) - * Qt listing fixes: - - Selection by arrow keys disabled while editing e.g. label - - Enter key on unedited value does not pop up context menu - - Contacts: - - Prevent editing of OpenAlias names - - Receive: - - Icon for status of payment requests - - Disable editing of 'Description' in list, interaction - between labels and memo of invoice confusing - - Addresses: - - Fiat prices would show "No Data" incorrectly upon start + * Qt listings rewritten: + the History tab now uses QAbstractItemModel, the other tabs use + QStandardItemModel. Performance should be better for large wallets. * Several other minor bugfixes and usability improvements. From 1b7672f70e88a7bb396fd8577a2d4a637f601cc8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Dec 2018 01:09:16 +0100 Subject: [PATCH 291/301] qt: fix invoices tab closes #4941 --- electrum/gui/qt/invoice_list.py | 34 +++++++++++++++++---------------- electrum/gui/qt/main_window.py | 3 ++- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 111bf142..6f863bf0 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -33,7 +33,7 @@ class InvoiceList(MyTreeView): filter_columns = [0, 1, 2, 3] # Date, Requestor, Description, Amount def __init__(self, parent): - super().__init__(parent, self.create_menu, 2) + super().__init__(parent, self.create_menu, stretch_column=2, editable_columns=[]) self.setSortingEnabled(True) self.setColumnWidth(1, 200) self.setModel(QStandardItemModel(self)) @@ -44,19 +44,20 @@ class InvoiceList(MyTreeView): self.model().clear() self.update_headers([_('Expires'), _('Requestor'), _('Description'), _('Amount'), _('Status')]) self.header().setSectionResizeMode(1, QHeaderView.Interactive) - for pr in inv_list: + for idx, pr in enumerate(inv_list): key = pr.get_id() status = self.parent.invoices.get_status(key) requestor = pr.get_requestor() exp = pr.get_expiration_date() date_str = format_time(exp) if exp else _('Never') labels = [date_str, requestor, pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')] - item = [QStandardItem(e) for e in labels] - item[4].setIcon(self.icon_cache.get(pr_icons.get(status))) - item[0].setData(Qt.UserRole, key) - item[1].setFont(QFont(MONOSPACE_FONT)) - item[3].setFont(QFont(MONOSPACE_FONT)) - self.addTopLevelItem(item) + items = [QStandardItem(e) for e in labels] + self.set_editability(items) + items[4].setIcon(self.icon_cache.get(pr_icons.get(status))) + items[0].setData(key, role=Qt.UserRole) + items[1].setFont(QFont(MONOSPACE_FONT)) + items[3].setFont(QFont(MONOSPACE_FONT)) + self.model().insertRow(idx, items) self.selectionModel().select(self.model().index(0,0), QItemSelectionModel.SelectCurrent) if self.parent.isVisible(): b = len(inv_list) > 0 @@ -70,16 +71,17 @@ class InvoiceList(MyTreeView): export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file) def create_menu(self, position): - menu = QMenu() - item = self.itemAt(position) - if not item: + idx = self.indexAt(position) + item = self.model().itemFromIndex(idx) + item_col0 = self.model().itemFromIndex(idx.sibling(idx.row(), 0)) + if not item or not item_col0: return - key = item.data(0, Qt.UserRole) - column = self.currentColumn() - column_title = self.headerItem().text(column) - column_data = item.text(column) - pr = self.parent.invoices.get(key) + key = item_col0.data(Qt.UserRole) + column = idx.column() + column_title = self.model().horizontalHeaderItem(column).text() + column_data = item.text() status = self.parent.invoices.get_status(key) + menu = QMenu(self) if column_data: menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Details"), lambda: self.parent.show_invoice(key)) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 0db7fa05..0a8ecb28 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1929,7 +1929,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): grid.addWidget(QLabel(format_time(expires)), 4, 1) vbox.addLayout(grid) def do_export(): - fn = self.getSaveFileName(_("Save invoice to file"), "*.bip70") + name = str(key) + '.bip70' + fn = self.getSaveFileName(_("Save invoice to file"), name, filter="*.bip70") if not fn: return with open(fn, 'wb') as f: From caae9f8a6a99fdb163b42f74b41580bdc0f5467a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Dec 2018 04:21:40 +0100 Subject: [PATCH 292/301] revealer: warning re version 0 now includes URL --- electrum/gui/qt/util.py | 2 +- electrum/plugins/revealer/qt.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 29cc20aa..bfdf751a 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -216,7 +216,7 @@ class MessageBoxMixin(object): d = QMessageBox(icon, title, str(text), buttons, parent) d.setWindowModality(Qt.WindowModal) d.setDefaultButton(defaultButton) - d.setTextInteractionFlags(Qt.TextSelectableByMouse) + d.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse) return d.exec_() class WindowModalDialog(QDialog, MessageBoxMixin): diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index 80907e4b..cfcf85f8 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -201,10 +201,14 @@ class Plugin(RevealerPlugin): def warn_old_revealer(self): if self.versioned_seed.version == '0': - self.d.show_warning(''.join( - ["", _("Warning"), ": ", - _("Revealers starting with 0 are not secure due to a vulnerability."), ' ', - _("Proceed at your own risk.")])) + link = "https://revealer.cc/revealer-warning-and-upgrade/" + self.d.show_warning(("{warning}: {ver0}
" + "{url}
" + "{risk}") + .format(warning=_("Warning"), + ver0=_("Revealers starting with 0 are not secure due to a vulnerability."), + url=_("More info at: {}").format(f'{link}'), + risk=_("Proceed at your own risk."))) def cypherseed_dialog(self, window): self.warn_old_revealer() From 58a9fa0ad5faee8becc8617f2220acc6de56bc52 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 20 Dec 2018 11:32:01 +0100 Subject: [PATCH 293/301] kivy: use default scroll_distance and scroll_timeout --- electrum/gui/kivy/main.kv | 2 -- 1 file changed, 2 deletions(-) diff --git a/electrum/gui/kivy/main.kv b/electrum/gui/kivy/main.kv index 8e14f70d..0dc4095e 100644 --- a/electrum/gui/kivy/main.kv +++ b/electrum/gui/kivy/main.kv @@ -307,8 +307,6 @@ carousel: carousel do_default_tab: False Carousel: - scroll_timeout: 250 - scroll_distance: '100dp' anim_type: 'out_quart' min_move: .05 anim_move_duration: .1 From 96b66b7e4f1a6cf56a3d779a22cb046a3bd43b29 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 20 Dec 2018 12:19:54 +0100 Subject: [PATCH 294/301] kivy: use on_state instead of on_release --- electrum/gui/kivy/main.kv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/kivy/main.kv b/electrum/gui/kivy/main.kv index 0dc4095e..68758449 100644 --- a/electrum/gui/kivy/main.kv +++ b/electrum/gui/kivy/main.kv @@ -238,7 +238,7 @@ padding: dp(12) spacing: dp(5) screen: None - on_release: + on_state: self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu() canvas.before: Color: From 2e078493a770aa2f485b3fc9969302b5639f2dcb Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 20 Dec 2018 12:43:31 +0100 Subject: [PATCH 295/301] kivy: improve context menu --- electrum/gui/kivy/uix/context_menu.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/electrum/gui/kivy/uix/context_menu.py b/electrum/gui/kivy/uix/context_menu.py index 84d5ba64..af63108b 100644 --- a/electrum/gui/kivy/uix/context_menu.py +++ b/electrum/gui/kivy/uix/context_menu.py @@ -19,16 +19,18 @@ Builder.load_string(''' size_hint: 1, None - height: '48dp' + height: '60dp' pos: (0, 0) show_arrow: False arrow_pos: 'top_mid' padding: 0 orientation: 'horizontal' + background_color: (0.1, 0.1, 0.1, 1) + background_image: '' BoxLayout: size_hint: 1, 1 - height: '48dp' - padding: '12dp', '0dp' + height: '54dp' + padding: '0dp', '0dp' spacing: '3dp' orientation: 'horizontal' id: buttons From b1b6b250d15b19fb1ffd8eb4d7f6d42316b894e5 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 20 Dec 2018 13:23:46 +0100 Subject: [PATCH 296/301] kivy: do not request PIN for watching-only wallets --- electrum/gui/kivy/uix/dialogs/installwizard.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/gui/kivy/uix/dialogs/installwizard.py b/electrum/gui/kivy/uix/dialogs/installwizard.py index 04af3734..eb8c459d 100644 --- a/electrum/gui/kivy/uix/dialogs/installwizard.py +++ b/electrum/gui/kivy/uix/dialogs/installwizard.py @@ -1027,6 +1027,10 @@ class InstallWizard(BaseWizard, Widget): Clock.schedule_once(lambda dt: app.show_error(msg)) def request_password(self, run_next, force_disable_encrypt_cb=False): + if force_disable_encrypt_cb: + # do not request PIN for watching-only wallets + run_next(None, False) + return def on_success(old_pin, pin): assert old_pin is None run_next(pin, False) From 85b712967f9f66a6e2a1c4f1b8c33897da01eb51 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 20 Dec 2018 13:24:35 +0100 Subject: [PATCH 297/301] prepare release 3.3.1 --- RELEASE-NOTES | 6 ++++++ electrum/version.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 3e470f4f..630ef402 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,9 @@ +# Release 3.3.1 - (December 20, 2018) + + * Qt: Fix invoices tab crash (#4941) + * Android: Minor GUI improvements + + # Release 3.3.0 - Hodler's Edition (December 19, 2018) * The network layer has been rewritten using asyncio and aiorpcx. diff --git a/electrum/version.py b/electrum/version.py index 31ecebe5..eae7acd1 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '3.3.0' # version of the client package -APK_VERSION = '3.3.0.0' # read by buildozer.spec +ELECTRUM_VERSION = '3.3.1' # version of the client package +APK_VERSION = '3.3.1.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From 43461a1866e270a16f12d9a973c34f21113f0497 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Dec 2018 16:46:58 +0100 Subject: [PATCH 298/301] qt history: fix exporting history closes #4948 --- electrum/gui/qt/history_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 3734b1d6..9e1104f3 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -634,7 +634,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): self.parent.show_message(_("Your wallet history has been successfully exported.")) def do_export_history(self, file_name, is_csv): - hist = self.wallet.get_full_history(domain=self.get_domain(), + hist = self.wallet.get_full_history(domain=self.hm.get_domain(), from_timestamp=None, to_timestamp=None, fx=self.parent.fx, From 7773443c17e642dcda1d9f76e4feed29256dfe25 Mon Sep 17 00:00:00 2001 From: ghost43 Date: Thu, 20 Dec 2018 16:49:17 +0100 Subject: [PATCH 299/301] network: put NetworkTimeout constants together (#4945) * network: put NetworkTimeout constants together * fix prev --- electrum/interface.py | 19 ++++++++++++++++--- electrum/network.py | 16 +++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index fc6ddfc0..53ce4fde 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -52,6 +52,17 @@ if TYPE_CHECKING: ca_path = certifi.where() +class NetworkTimeout: + # seconds + class Generic: + NORMAL = 30 + RELAXED = 45 + MOST_RELAXED = 180 + class Urgent(Generic): + NORMAL = 10 + RELAXED = 20 + MOST_RELAXED = 60 + class NotificationSession(RPCSession): def __init__(self, *args, **kwargs): @@ -59,6 +70,7 @@ class NotificationSession(RPCSession): self.subscriptions = defaultdict(list) self.cache = {} self.in_flight_requests_semaphore = asyncio.Semaphore(100) + self.default_timeout = NetworkTimeout.Generic.NORMAL async def handle_request(self, request): # note: if server sends malformed request and we raise, the superclass @@ -76,7 +88,7 @@ class NotificationSession(RPCSession): async def send_request(self, *args, timeout=None, **kwargs): # note: the timeout starts after the request touches the wire! if timeout is None: - timeout = 30 + timeout = self.default_timeout # note: the semaphore implementation guarantees no starvation async with self.in_flight_requests_semaphore: try: @@ -327,9 +339,9 @@ class Interface(PrintError): return None async def get_block_header(self, height, assert_mode): - # use lower timeout as we usually have network.bhi_lock here self.print_error('requesting block header {} in mode {}'.format(height, assert_mode)) - timeout = 5 if not self.proxy else 10 + # use lower timeout as we usually have network.bhi_lock here + timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Urgent) res = await self.session.send_request('blockchain.block.header', [height], timeout=timeout) return blockchain.deserialize_header(bytes.fromhex(res), height) @@ -358,6 +370,7 @@ class Interface(PrintError): host=self.host, port=self.port, ssl=sslc, proxy=self.proxy) as session: self.session = session # type: NotificationSession + self.session.default_timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Generic) try: ver = await session.send_request('server.version', [ELECTRUM_VERSION, PROTOCOL_VERSION]) except aiorpcx.jsonrpc.RPCError as e: diff --git a/electrum/network.py b/electrum/network.py index e089c8e6..1704e038 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -49,7 +49,8 @@ from . import constants from . import blockchain from . import bitcoin from .blockchain import Blockchain, HEADER_SIZE -from .interface import Interface, serialize_server, deserialize_server, RequestTimedOut +from .interface import (Interface, serialize_server, deserialize_server, + RequestTimedOut, NetworkTimeout) from .version import PROTOCOL_VERSION from .simple_config import SimpleConfig @@ -649,11 +650,18 @@ class Network(PrintError): await self._close_interface(interface) self.trigger_callback('network_updated') + def get_network_timeout_seconds(self, request_type=NetworkTimeout.Generic) -> int: + if self.oneserver and not self.auto_connect: + return request_type.MOST_RELAXED + if self.proxy: + return request_type.RELAXED + return request_type.NORMAL + @ignore_exceptions # do not kill main_taskgroup @log_exceptions async def _run_new_interface(self, server): interface = Interface(self, server, self.proxy) - timeout = 10 if not self.proxy else 20 + timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent) try: await asyncio.wait_for(interface.ready, timeout) except BaseException as e: @@ -724,7 +732,9 @@ 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=10): + async def broadcast_transaction(self, tx, *, timeout=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) if out != tx.txid(): raise Exception(out) From 744bfc1eeb6f67b5bb98c54b4eb3e07dc758b00a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Dec 2018 17:09:58 +0100 Subject: [PATCH 300/301] util.profiler: simplify follow-up 6192bfce463fbd05e3ccdc851aab24a994a7258c closes #4904 --- electrum/util.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index 69a8926c..ac41d9e3 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -32,7 +32,6 @@ import urllib import threading import hmac import stat -import inspect from locale import localeconv import asyncio import urllib.request, urllib.parse, urllib.error @@ -358,18 +357,8 @@ def constant_time_compare(val1, val2): # decorator that prints execution time def profiler(func): - def get_func_name(args): - arg_names_from_sig = inspect.getfullargspec(func).args - # prepend class name if there is one (and if we can find it) - if len(arg_names_from_sig) > 0 and len(args) > 0 \ - and arg_names_from_sig[0] in ('self', 'cls', 'klass'): - classname = args[0].__class__.__name__ - else: - classname = '' - name = '{}.{}'.format(classname, func.__name__) if classname else func.__name__ - return name def do_profile(args, kw_args): - name = get_func_name(args) + name = func.__qualname__ t0 = time.time() o = func(*args, **kw_args) t = time.time() - t0 From 1d303fa9d223299b1dabbaa9f289316d01ea1c65 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Dec 2018 18:06:16 +0100 Subject: [PATCH 301/301] win build: rm win_inet_pton was needed by PySocks; and we no longer use PySocks also, it seems the functionality it provided is now part of Python stdlib since 3.4 https://docs.python.org/3/library/socket.html#socket.inet_pton related: #2358 --- contrib/build-wine/prepare-wine.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 24196d22..e80259f5 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -112,8 +112,6 @@ done # upgrade pip $PYTHON -m pip install pip --upgrade -# install PySocks -$PYTHON -m pip install win_inet_pton==1.0.1 $PYTHON -m pip install -r $here/../deterministic-build/requirements-binaries.txt