From eaf203dbb566a555253e22dbd42affefad218869 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 27 May 2019 19:09:54 +0200 Subject: [PATCH 001/115] interface: fix connecting to new servers using self-signed certs got broken in 6ec1578a90916436b3bfabe90cafd9bfc804a332 --- electrum/interface.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index 1dfb5c670..b6ae54625 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -177,7 +177,8 @@ class _Connector(aiorpcx.Connector): try: return await super().create_connection() except OSError as e: - raise ConnectError(e) + # note: using "from e" here will set __cause__ of ConnectError + raise ConnectError(e) from e def deserialize_server(server_str: str) -> Tuple[str, str, str]: @@ -254,11 +255,11 @@ class Interface(Logger): """ try: await self.open_session(ca_ssl_context, exit_early=True) - except ssl.SSLError as e: - if e.reason == 'CERTIFICATE_VERIFY_FAILED': + except ConnectError as e: + cause = e.__cause__ + if isinstance(cause, ssl.SSLError) and cause.reason == 'CERTIFICATE_VERIFY_FAILED': # failures due to self-signed certs are normal return False - # e.g. too weak crypto raise return True From 41f160dd74dc60434acac882393374cb15a381f0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 27 May 2019 19:35:30 +0200 Subject: [PATCH 002/115] update to aiorpcx 0.18 --- contrib/requirements/requirements.txt | 2 +- electrum/interface.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt index 4ca242273..d5ec8e32f 100644 --- a/contrib/requirements/requirements.txt +++ b/contrib/requirements/requirements.txt @@ -5,7 +5,7 @@ protobuf dnspython jsonrpclib-pelix qdarkstyle<3.0 -aiorpcx>=0.17,<0.18 +aiorpcx>=0.18,<0.19 aiohttp>=3.3.0 aiohttp_socks certifi diff --git a/electrum/interface.py b/electrum/interface.py index b6ae54625..524b3d572 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -39,6 +39,7 @@ import aiorpcx from aiorpcx import RPCSession, Notification, NetAddress from aiorpcx.curio import timeout_after, TaskTimeout from aiorpcx.jsonrpc import JSONRPC +from aiorpcx.rawsocket import RSClient import certifi from .util import ignore_exceptions, log_exceptions, bfh, SilentTaskGroup @@ -172,7 +173,7 @@ class ErrorGettingSSLCertFromServer(Exception): pass class ConnectError(Exception): pass -class _Connector(aiorpcx.Connector): +class _RSClient(RSClient): async def create_connection(self): try: return await super().create_connection() @@ -392,9 +393,9 @@ class Interface(Logger): async def get_certificate(self): sslc = ssl.SSLContext() try: - async with _Connector(RPCSession, - host=self.host, port=self.port, - ssl=sslc, proxy=self.proxy) as session: + async with _RSClient(session_factory=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 @@ -430,9 +431,9 @@ class Interface(Logger): return self.network.default_server == self.server async def open_session(self, sslc, exit_early=False): - async with _Connector(NotificationSession, - host=self.host, port=self.port, - ssl=sslc, proxy=self.proxy) as session: + async with _RSClient(session_factory=NotificationSession, + host=self.host, port=self.port, + ssl=sslc, proxy=self.proxy) as session: self.session = session # type: NotificationSession self.session.interface = self self.session.set_default_timeout(self.network.get_network_timeout_seconds(NetworkTimeout.Generic)) From 41802d8094f07398254ed49093ce312a56f29cce Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 27 May 2019 20:24:09 +0200 Subject: [PATCH 003/115] qt receive tab: "receive address" is now coloured red if already used closes #3812 closes #5374 --- electrum/gui/qt/main_window.py | 11 +++++++++++ electrum/gui/qt/request_list.py | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index d1a7751a5..87e3dc266 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -907,6 +907,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): msg = _('Bitcoin address where the payment should be received. Note that each payment request uses a different Bitcoin address.') self.receive_address_label = HelpLabel(_('Receiving address'), msg) self.receive_address_e.textChanged.connect(self.update_receive_qr) + self.receive_address_e.textChanged.connect(self.update_receive_address_styling) self.receive_address_e.setFocusPolicy(Qt.ClickFocus) grid.addWidget(self.receive_address_label, 0, 0) grid.addWidget(self.receive_address_e, 0, 1, 1, -1) @@ -1152,6 +1153,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): if self.qr_window and self.qr_window.isVisible(): self.qr_window.qrw.setData(uri) + def update_receive_address_styling(self): + addr = str(self.receive_address_e.text()) + if self.wallet.is_used(addr): + self.receive_address_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + self.receive_address_e.setToolTip(_("This address has already been used. " + "For better privacy, do not reuse it for new payments.")) + else: + self.receive_address_e.setStyleSheet("") + self.receive_address_e.setToolTip("") + def set_feerounding_text(self, num_satoshis_added): self.feerounding_text = (_('Additional {} satoshis are going to be added.') .format(num_satoshis_added)) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 602f2091f..2c0699be6 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -105,9 +105,10 @@ class RequestList(MyTreeView): except InternalAddressCorruption as e: self.parent.show_error(str(e)) addr = '' - if not current_address in domain and addr: + if current_address not in domain and addr: self.parent.set_receive_address(addr) self.parent.new_request_button.setEnabled(addr != current_address) + self.parent.update_receive_address_styling() self.model().clear() self.update_headers(self.__class__.headers) From d17e6a1b87aa59dc0156106a0d2bae61efb0db17 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 May 2019 06:14:53 +0200 Subject: [PATCH 004/115] interface: fix for aiorpcx 0.18 --- electrum/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/interface.py b/electrum/interface.py index 524b3d572..e9691335e 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -396,7 +396,7 @@ class Interface(Logger): async with _RSClient(session_factory=RPCSession, host=self.host, port=self.port, ssl=sslc, proxy=self.proxy) as session: - return session.transport._ssl_protocol._sslpipe._sslobj.getpeercert(True) + return session.transport._asyncio_transport._ssl_protocol._sslpipe._sslobj.getpeercert(True) except ValueError: return None From ab81a09de2e16501315ca25007c0e079c100ac59 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 May 2019 21:23:06 +0200 Subject: [PATCH 005/115] interface: hide some server-induced errors from log --- electrum/interface.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index e9691335e..23cc61f48 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -328,6 +328,9 @@ class Interface(Logger): return await func(self, *args, **kwargs) except GracefulDisconnect as e: self.logger.log(e.log_level, f"disconnecting due to {repr(e)}") + except aiorpcx.jsonrpc.RPCError as e: + self.logger.warning(f"disconnecting due to {repr(e)}") + self.logger.debug(f"(disconnect) trace for {repr(e)}", exc_info=True) finally: await self.network.connection_down(self) self.got_disconnected.set_result(1) @@ -454,8 +457,10 @@ class Interface(Logger): await group.spawn(self.run_fetch_blocks) await group.spawn(self.monitor_connection) except aiorpcx.jsonrpc.RPCError as e: - if e.code in (JSONRPC.EXCESSIVE_RESOURCE_USAGE, JSONRPC.SERVER_BUSY): - raise GracefulDisconnect(e, log_level=logging.ERROR) from e + if e.code in (JSONRPC.EXCESSIVE_RESOURCE_USAGE, + JSONRPC.SERVER_BUSY, + JSONRPC.METHOD_NOT_FOUND): + raise GracefulDisconnect(e, log_level=logging.WARNING) from e raise async def monitor_connection(self): From 7cba46c317caa95b77337592ffa10e3de0fd2107 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 May 2019 21:38:27 +0200 Subject: [PATCH 006/115] deprecation warnings: only show when running from source --- run_electrum | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/run_electrum b/run_electrum index 654cf15fb..ef21b8d2a 100755 --- a/run_electrum +++ b/run_electrum @@ -35,13 +35,16 @@ _min_python_version_tuple = tuple(map(int, (MIN_PYTHON_VERSION.split(".")))) if sys.version_info[:3] < _min_python_version_tuple: sys.exit("Error: Electrum requires Python version >= %s..." % MIN_PYTHON_VERSION) -warnings.simplefilter('default', DeprecationWarning) script_dir = os.path.dirname(os.path.realpath(__file__)) is_bundle = getattr(sys, 'frozen', False) is_local = not is_bundle and os.path.exists(os.path.join(script_dir, "electrum.desktop")) is_android = 'ANDROID_DATA' in os.environ +if is_local: # running from source + # developers should probably see all deprecation warnings. + warnings.simplefilter('default', DeprecationWarning) + # move this back to gui/kivy/__init.py once plugins are moved os.environ['KIVY_DATA_DIR'] = os.path.abspath(os.path.dirname(__file__)) + '/electrum/gui/kivy/data/' From 371e1a6ebff4cf7660edf7d586a47dfc940e4263 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 31 May 2019 04:09:03 +0200 Subject: [PATCH 007/115] hw: allow bypassing "too old firmware" error when using hw wallets The framework here is generic enough that it can be used for any hw plugin, however atm only Trezor is implemented. closes #5391 --- electrum/base_wizard.py | 11 ++++++++++- electrum/gui/qt/util.py | 12 ++++++++---- electrum/plugin.py | 2 +- electrum/plugins/hw_wallet/plugin.py | 20 ++++++++++++++++++++ electrum/plugins/hw_wallet/qt.py | 20 +++++++++++++++++++- electrum/plugins/trezor/clientbase.py | 19 +++++++++++-------- electrum/plugins/trezor/trezor.py | 4 ++-- 7 files changed, 71 insertions(+), 17 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index ac8d7874b..72bc4c95e 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -44,6 +44,7 @@ from .util import UserCancelled, InvalidPassword, WalletFileException from .simple_config import SimpleConfig from .plugin import Plugins, HardwarePluginLibraryUnavailable from .logging import Logger +from .plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HW_PluginBase if TYPE_CHECKING: from .plugin import DeviceInfo @@ -323,7 +324,7 @@ class BaseWizard(Logger): run_next=lambda *args: self.on_device(*args, purpose=purpose, storage=storage)) def on_device(self, name, device_info, *, purpose, storage=None): - self.plugin = self.plugins.get_plugin(name) + self.plugin = self.plugins.get_plugin(name) # type: HW_PluginBase try: self.plugin.setup_device(device_info, self, purpose) except OSError as e: @@ -335,6 +336,14 @@ class BaseWizard(Logger): devmgr.unpair_id(device_info.device.id_) self.choose_hw_device(purpose, storage=storage) return + except OutdatedHwFirmwareException as e: + if self.question(e.text_ignore_old_fw_and_continue(), title=_("Outdated device firmware")): + self.plugin.set_ignore_outdated_fw() + # will need to re-pair + devmgr = self.plugins.device_manager + devmgr.unpair_id(device_info.device.id_) + self.choose_hw_device(purpose, storage=storage) + return except (UserCancelled, GoBack): self.choose_hw_device(purpose, storage=storage) return diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 9c48f7ff4..9ac6649ca 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -206,11 +206,15 @@ class MessageBoxMixin(object): def top_level_window(self, test_func=None): return self.top_level_window_recurse(test_func) - def question(self, msg, parent=None, title=None, icon=None): + def question(self, msg, parent=None, title=None, icon=None, **kwargs) -> bool: Yes, No = QMessageBox.Yes, QMessageBox.No - return self.msg_box(icon or QMessageBox.Question, - parent, title or '', - msg, buttons=Yes|No, defaultButton=No) == Yes + return Yes == self.msg_box(icon=icon or QMessageBox.Question, + parent=parent, + title=title or '', + text=msg, + buttons=Yes|No, + defaultButton=No, + **kwargs) def show_warning(self, msg, parent=None, title=None, **kwargs): return self.msg_box(QMessageBox.Warning, parent, diff --git a/electrum/plugin.py b/electrum/plugin.py index cf72ef221..173ecdc57 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -403,7 +403,7 @@ class DeviceMgr(ThreadJob): def unpair_xpub(self, xpub): with self.lock: - if not xpub in self.xpub_ids: + if xpub not in self.xpub_ids: return _id = self.xpub_ids.pop(xpub) self._close_client(_id) diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index 9b2270284..fd3ed6979 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -44,6 +44,7 @@ class HW_PluginBase(BasePlugin): BasePlugin.__init__(self, parent, config, name) self.device = self.keystore_class.device self.keystore_class.plugin = self + self._ignore_outdated_fw = False def is_enabled(self): return True @@ -124,6 +125,12 @@ class HW_PluginBase(BasePlugin): message += '\n' + _("Make sure you install it with python3") return message + def set_ignore_outdated_fw(self): + self._ignore_outdated_fw = True + + def is_outdated_fw_ignored(self) -> bool: + return self._ignore_outdated_fw + def is_any_tx_output_on_change_branch(tx: Transaction): if not tx.output_info: @@ -160,3 +167,16 @@ def only_hook_if_libraries_available(func): class LibraryFoundButUnusable(Exception): def __init__(self, library_version='unknown'): self.library_version = library_version + + +class OutdatedHwFirmwareException(UserFacingException): + + def text_ignore_old_fw_and_continue(self) -> str: + suffix = (_("The firmware of your hardware device is too old. " + "If possible, you should upgrade it. " + "You can ignore this error and try to continue, however things are likely to break.") + "\n\n" + + _("Ignore and continue?")) + if str(self): + return str(self) + "\n\n" + suffix + else: + return suffix diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index 86f9a4099..a93a9d30c 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -37,6 +37,8 @@ from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDial from electrum.i18n import _ from electrum.logging import Logger +from .plugin import OutdatedHwFirmwareException + # The trickiest thing about this handler was getting windows properly # parented on macOS. @@ -212,11 +214,27 @@ class QtPluginBase(object): handler = self.create_handler(window) handler.button = button keystore.handler = handler - keystore.thread = TaskThread(window, window.on_error) + keystore.thread = TaskThread(window, on_error=partial(self.on_task_thread_error, window, keystore)) self.add_show_address_on_hw_device_button_for_receive_addr(wallet, keystore, window) # Trigger a pairing keystore.thread.add(partial(self.get_client, keystore)) + def on_task_thread_error(self, window, keystore, exc_info): + e = exc_info[1] + if isinstance(e, OutdatedHwFirmwareException): + if window.question(e.text_ignore_old_fw_and_continue(), title=_("Outdated device firmware")): + self.set_ignore_outdated_fw() + # will need to re-pair + devmgr = self.device_manager() + def re_pair_device(): + device_id = self.choose_device(window, keystore) + devmgr.unpair_id(device_id) + self.get_client(keystore) + keystore.thread.add(re_pair_device) + return + else: + window.on_error(exc_info) + def choose_device(self, window, keystore): '''This dialog box should be usable even if the user has forgotten their PIN or it is in bootloader mode.''' diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index 9ce2b3699..7188c3798 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -7,6 +7,7 @@ from electrum.util import UserCancelled, UserFacingException from electrum.keystore import bip39_normalize_passphrase from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path from electrum.logging import Logger +from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException from trezorlib.client import TrezorClient from trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError @@ -29,6 +30,8 @@ MESSAGES = { class TrezorClientBase(Logger): def __init__(self, transport, handler, plugin): + if plugin.is_outdated_fw_ignored(): + TrezorClient.is_outdated = lambda *args, **kwargs: False self.client = TrezorClient(transport, ui=self) self.plugin = plugin self.device = plugin.device @@ -62,15 +65,15 @@ class TrezorClientBase(Logger): def __enter__(self): return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type, e, 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(str(exc_value)) from exc_value - elif issubclass(exc_type, OutdatedFirmwareError): - raise UserFacingException(exc_value) from exc_value + if e is not None: + if isinstance(e, Cancelled): + raise UserCancelled from e + elif isinstance(e, TrezorFailure): + raise RuntimeError(str(e)) from e + elif isinstance(e, OutdatedFirmwareError): + raise OutdatedHwFirmwareException(e) from e else: return False return True diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 83a05668e..26c240f9c 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -15,7 +15,7 @@ from electrum.logging import get_logger 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, - LibraryFoundButUnusable) + LibraryFoundButUnusable, OutdatedHwFirmwareException) _logger = get_logger(__name__) @@ -275,7 +275,7 @@ class TrezorPlugin(HW_PluginBase): msg = (_('Outdated {} firmware for device labelled {}. Please ' 'download the updated firmware from {}') .format(self.device, client.label(), self.firmware_URL)) - raise UserFacingException(msg) + raise OutdatedHwFirmwareException(msg) # fixme: we should use: client.handler = wizard client.handler = self.create_handler(wizard) From 0ef853c046bdb5749db608e70107d96d192016a6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 3 Jun 2019 20:35:37 +0200 Subject: [PATCH 008/115] rm dead code --- electrum/gui/qt/installwizard.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index 909aa7522..309bf03ee 100644 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -121,8 +121,6 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): self.setWindowTitle('Electrum - ' + _('Install Wizard')) self.app = app self.config = config - # Set for base base class - self.language_for_seed = config.get('language') self.setMinimumSize(600, 400) self.accept_signal.connect(self.accept) self.title = QLabel() From 21ab65e5f76c10c46dc067cf4427522fee22fd64 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 3 Jun 2019 22:21:53 +0200 Subject: [PATCH 009/115] qt lists right click: fix #5365 --- electrum/gui/qt/address_list.py | 2 ++ electrum/gui/qt/utxo_list.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 4656b2778..0977cf9e7 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -187,6 +187,8 @@ class AddressList(MyTreeView): menu = QMenu() if not multi_select: idx = self.indexAt(position) + if not idx.isValid(): + return col = idx.column() item = self.model().itemFromIndex(idx) if not item: diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index c18d4426f..f549cc9cf 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -124,6 +124,8 @@ class UTXOList(MyTreeView): menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label)) # "Copy ..." idx = self.indexAt(position) + if not idx.isValid(): + return col = idx.column() column_title = self.model().horizontalHeaderItem(col).text() copy_text = self.model().itemFromIndex(idx).text() if col != self.Columns.OUTPOINT else selected[0] From 6cf7aefe28bc2d248782623378e8eb624f744be7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Jun 2019 19:20:31 +0200 Subject: [PATCH 010/115] kivy: offer to copy raw hex tx to clipboard related: #5405 --- electrum/gui/kivy/main_window.py | 3 ++- electrum/gui/kivy/uix/dialogs/qr_dialog.py | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 0aef417af..277b17f7d 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -435,7 +435,8 @@ class ElectrumWindow(App): msg += '\n' + _('Text copied to clipboard.') self._clipboard.copy(text_for_clipboard) Clock.schedule_once(lambda dt: self.show_info(msg)) - popup = QRDialog(title, data, show_text, on_qr_failure) + popup = QRDialog(title, data, show_text, failure_cb=on_qr_failure, + text_for_clipboard=text_for_clipboard) popup.open() def scan_qr(self, on_complete): diff --git a/electrum/gui/kivy/uix/dialogs/qr_dialog.py b/electrum/gui/kivy/uix/dialogs/qr_dialog.py index b12eb6ce6..0685dfa9d 100644 --- a/electrum/gui/kivy/uix/dialogs/qr_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/qr_dialog.py @@ -1,5 +1,11 @@ from kivy.factory import Factory from kivy.lang import Builder +from kivy.core.clipboard import Clipboard +from kivy.app import App +from kivy.clock import Clock + +from electrum.gui.kivy.i18n import _ + Builder.load_string(''' @@ -24,9 +30,12 @@ Builder.load_string(''' BoxLayout: size_hint: 1, None height: '48dp' - Widget: + Button: size_hint: 1, None height: '48dp' + text: _('Copy to clipboard') + on_release: + root.copy_to_clipboard() Button: size_hint: 1, None height: '48dp' @@ -36,12 +45,20 @@ Builder.load_string(''' ''') class QRDialog(Factory.Popup): - def __init__(self, title, data, show_text, failure_cb=None): + def __init__(self, title, data, show_text, *, + failure_cb=None, text_for_clipboard=None): Factory.Popup.__init__(self) + self.app = App.get_running_app() self.title = title self.data = data self.show_text = show_text self.failure_cb = failure_cb + self.text_for_clipboard = text_for_clipboard if text_for_clipboard else data def on_open(self): self.ids.qr.set_data(self.data, self.failure_cb) + + def copy_to_clipboard(self): + Clipboard.copy(self.text_for_clipboard) + msg = _('Text copied to clipboard.') + Clock.schedule_once(lambda dt: self.app.show_info(msg)) From 046518d7f73fdee30fb974b283780fc4691a614e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Jun 2019 20:35:46 +0200 Subject: [PATCH 011/115] requirements: restrict qdarkstyle to <2.7 qdarkstyle 2.7 pulls in new dependencies see ColinDuquesnoy/QDarkStyleSheet#182 --- 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 d5ec8e32f..1159a0469 100644 --- a/contrib/requirements/requirements.txt +++ b/contrib/requirements/requirements.txt @@ -4,7 +4,7 @@ qrcode protobuf dnspython jsonrpclib-pelix -qdarkstyle<3.0 +qdarkstyle<2.7 aiorpcx>=0.18,<0.19 aiohttp>=3.3.0 aiohttp_socks From fbcf6f48b91ec90d7997c13553daeecaaa17b72c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Jun 2019 20:36:52 +0200 Subject: [PATCH 012/115] rerun freeze_packages --- .../requirements-binaries.txt | 12 +- .../deterministic-build/requirements-hw.txt | 138 +++++++++--------- contrib/deterministic-build/requirements.txt | 58 ++++---- 3 files changed, 104 insertions(+), 104 deletions(-) diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt index 7a6a41bdc..5b6be40a9 100644 --- a/contrib/deterministic-build/requirements-binaries.txt +++ b/contrib/deterministic-build/requirements-binaries.txt @@ -1,6 +1,6 @@ -pip==19.1 \ - --hash=sha256:8f59b6cf84584d7962d79fd1be7a8ec0eb198aa52ea864896551736b3614eee9 \ - --hash=sha256:d9137cb543d8a4d73140a3282f6d777b2e786bb6abb8add3ac5b6539c82cd624 +pip==19.1.1 \ + --hash=sha256:44d3d7d3d30a1eb65c7e5ff1173cdf8f7467850605ac7cc3707b6064bddd0958 \ + --hash=sha256:993134f0475471b91452ca029d4390dc8f298ac63a712814f101cd1b6db46676 pycryptodomex==3.7.3 \ --hash=sha256:0bda549e20db1eb8e29fb365d10acf84b224d813b1131c828fc830b2ce313dcd \ --hash=sha256:1210c0818e5334237b16d99b5785aa0cee815d9997ee258bd5e2936af8e8aa50 \ @@ -51,6 +51,6 @@ PyQt5-sip==4.19.13 \ setuptools==41.0.1 \ --hash=sha256:a222d126f5471598053c9a77f4b5d4f26eaa1f150ad6e01dcf1a42e185d05613 \ --hash=sha256:c7769ce668c7a333d84e17fe8b524b1c45e7ee9f7908ad0a73e1eda7e6a5aebf -wheel==0.33.1 \ - --hash=sha256:66a8fd76f28977bb664b098372daef2b27f60dc4d1688cfab7b37a09448f0e9d \ - --hash=sha256:8eb4a788b3aec8abf5ff68d4165441bc57420c9f64ca5f471f58c3969fe08668 +wheel==0.33.4 \ + --hash=sha256:5e79117472686ac0c4aef5bad5172ea73a1c2d1646b808c35926bd26bdfb0c08 \ + --hash=sha256:62fcfa03d45b5b722539ccbc07b190e4bfff4bb9e3a4d470dd9f6a0981002565 diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 0ea85b86e..039dc81ad 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -6,43 +6,43 @@ certifi==2019.3.9 \ chardet==3.0.4 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 -ckcc-protocol==0.7.4 \ - --hash=sha256:5af1d268a62e03997832b6300453c8f005630591df30a7156b450c80dd74a881 \ - --hash=sha256:fb41a4c2fb22c0bd04356d14b0c6dbf3e708bc3ad080dddbc088bb48cda03699 +ckcc-protocol==0.7.6 \ + --hash=sha256:b2a782aa37b22dd21b5859618b84a69bc19271c279eb89fe63aba378916d07b0 \ + --hash=sha256:f2e8181f9814959e4a6dfa3d1175c11b4e622a32a2ce2b311f64e5bcb3e7b271 click==7.0 \ --hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \ --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 construct==2.9.45 \ --hash=sha256:2271a0efd0798679dea825ff47e22a4c550456a5db0ba8baa82f7eae0af0118c -Cython==0.29.7 \ - --hash=sha256:0ce8f6c789c907472c9084a44b625eba76a85d0189513de1497ab102a9d39ef8 \ - --hash=sha256:0d67964b747ac09758ba31fe25da2f66f575437df5f121ff481889a7a4485f56 \ - --hash=sha256:1630823619a87a814e5c1fa9f96544272ce4f94a037a34093fbec74989342328 \ - --hash=sha256:1a4c634bb049c8482b7a4f3121330de1f1c1f66eac3570e1e885b0c392b6a451 \ - --hash=sha256:1ec91cc09e9f9a2c3173606232adccc68f3d14be1a15a8c5dc6ab97b47b31528 \ - --hash=sha256:237a8fdd8333f7248718875d930d1e963ffa519fefeb0756d01d91cbfadab0bc \ - --hash=sha256:28a308cbfdf9b7bb44def918ad4a26b2d25a0095fa2f123addda33a32f308d00 \ - --hash=sha256:2fe3dde34fa125abf29996580d0182c18b8a240d7fa46d10984cc28d27808731 \ - --hash=sha256:30bda294346afa78c49a343e26f3ab2ad701e09f6a6373f579593f0cfcb1235a \ - --hash=sha256:33d27ea23e12bf0d420e40c20308c03ef192d312e187c1f72f385edd9bd6d570 \ - --hash=sha256:34d24d9370a6089cdd5afe56aa3c4af456e6400f8b4abb030491710ee765bafc \ - --hash=sha256:4e4877c2b96fae90f26ee528a87b9347872472b71c6913715ca15c8fe86a68c9 \ - --hash=sha256:50d6f1f26702e5f2a19890c7bc3de00f9b8a0ec131b52edccd56a60d02519649 \ - --hash=sha256:55d081162191b7c11c7bfcb7c68e913827dfd5de6ecdbab1b99dab190586c1e8 \ - --hash=sha256:59d339c7f99920ff7e1d9d162ea309b35775172e4bab9553f1b968cd43b21d6d \ - --hash=sha256:6cf4d10df9edc040c955fca708bbd65234920e44c30fccd057ecf3128efb31ad \ - --hash=sha256:6ec362539e2a6cf2329cd9820dec64868d8f0babe0d8dc5deff6c87a84d13f68 \ - --hash=sha256:7edc61a17c14b6e54d5317b0300d2da23d94a719c466f93cafa3b666b058c43b \ - --hash=sha256:8e37fc4db3f2c4e7e1ed98fe4fe313f1b7202df985de4ee1451d2e331332afae \ - --hash=sha256:b8c996bde5852545507bff45af44328fa48a7b22b5bec2f43083f0b8d1024fd9 \ - --hash=sha256:bf9c16f3d46af82f89fdefc0d64b2fb02f899c20da64548a8ea336beefcf8d23 \ - --hash=sha256:c1038aba898bed34ab1b5ddb0d3f9c9ae33b0649387ab9ffe6d0af677f66bfc1 \ - --hash=sha256:d405649c1bfc42e20d86178257658a859a3217b6e6d950ee8cb76353fcea9c39 \ - --hash=sha256:db6eeb20a3bd60e1cdcf6ce9a784bc82aec6ab891c800dc5d7824d5cfbfe77f2 \ - --hash=sha256:e382f8cb40dca45c3b439359028a4b60e74e22d391dc2deb360c0b8239d6ddc0 \ - --hash=sha256:f3f6c09e2c76f2537d61f907702dd921b04d1c3972f01d5530ef1f748f22bd89 \ - --hash=sha256:f749287087f67957c020e1de26906e88b8b0c4ea588facb7349c115a63346f67 \ - --hash=sha256:f86b96e014732c0d1ded2c1f51444c80176a98c21856d0da533db4e4aef54070 +Cython==0.29.10 \ + --hash=sha256:0afa0b121b89de619e71587e25702e2b7068d7da2164c47e6eee80c17823a62f \ + --hash=sha256:1c608ba76f7a20cc9f0c021b7fe5cb04bc1a70327ae93a9298b1bc3e0edddebe \ + --hash=sha256:26229570d6787ff3caa932fe9d802960f51a89239b990d275ae845405ce43857 \ + --hash=sha256:2a9deafa437b6154cac2f25bb88e0bfd075a897c8dc847669d6f478d7e3ee6b1 \ + --hash=sha256:2f28396fbce6d9d68a40edbf49a6729cf9d92a4d39ff0f501947a89188e9099f \ + --hash=sha256:3983dd7b67297db299b403b29b328d9e03e14c4c590ea90aa1ad1d7b35fb178b \ + --hash=sha256:4100a3f8e8bbe47d499cdac00e56d5fe750f739701ea52dc049b6c56f5421d97 \ + --hash=sha256:51abfaa7b6c66f3f18028876713c8804e73d4c2b6ceddbcbcfa8ec62429377f0 \ + --hash=sha256:61c24f4554efdb8fb1ac6c8e75dab301bcdf2b7b739ed0c2b267493bb43163c5 \ + --hash=sha256:700ccf921b2fdc9b23910e95b5caae4b35767685e0812343fa7172409f1b5830 \ + --hash=sha256:7b41eb2e792822a790cb2a171df49d1a9e0baaa8e81f58077b7380a273b93d5f \ + --hash=sha256:803987d3b16d55faa997bfc12e8b97f1091f145930dee229b020487aed8a1f44 \ + --hash=sha256:99af5cfcd208c81998dcf44b3ca466dee7e17453cfb50e98b87947c3a86f8753 \ + --hash=sha256:9faea1cca34501c7e139bc7ef8e504d532b77865c58592493e2c154a003b450f \ + --hash=sha256:a7ba4c9a174db841cfee9a0b92563862a0301d7ca543334666c7266b541f141a \ + --hash=sha256:b26071c2313d1880599c69fd831a07b32a8c961ba69d7ccbe5db1cd8d319a4ca \ + --hash=sha256:b49dc8e1116abde13a3e6a9eb8da6ab292c5a3325155fb872e39011b110b37e6 \ + --hash=sha256:bd40def0fd013569887008baa6da9ca428e3d7247adeeaeada153006227bb2e7 \ + --hash=sha256:bfd0db770e8bd4e044e20298dcae6dfc42561f85d17ee546dcd978c8b23066ae \ + --hash=sha256:c2fad1efae5889925c8fd7867fdd61f59480e4e0b510f9db096c912e884704f1 \ + --hash=sha256:c81aea93d526ccf6bc0b842c91216ee9867cd8792f6725a00f19c8b5837e1715 \ + --hash=sha256:da786e039b4ad2bce3d53d4799438cf1f5e01a0108f1b8d78ac08e6627281b1a \ + --hash=sha256:deab85a069397540987082d251e9c89e0e5b2e3e044014344ff81f60e211fc4b \ + --hash=sha256:e3f1e6224c3407beb1849bdc5ae3150929e593e4cffff6ca41c6ec2b10942c80 \ + --hash=sha256:e74eb224e53aae3943d66e2d29fe42322d5753fd4c0641329bccb7efb3a46552 \ + --hash=sha256:ee697c7ea65cb14915a64f36874da8ffc2123df43cf8bc952172e04a26656cd6 \ + --hash=sha256:f37792b16d11606c28e428460bd6a3d14b8917b109e77cdbe4ca78b0b9a52c87 \ + --hash=sha256:fd2906b54cbf879c09d875ad4e4687c58d87f5ed03496063fec1c9065569fd5d ecdsa==0.13.2 \ --hash=sha256:20c17e527e75acad8f402290e158a6ac178b91b881f941fc6ea305bfdfb9657c \ --hash=sha256:5c034ffa23413ac923541ceb3ac14ec15a0d2530690413bff58c12b80e56d884 @@ -65,35 +65,35 @@ keepkey==6.1.0 \ --hash=sha256:058548e733e1df8d1879ea747eef167c84cb04cdd685240e50d599f48d08e5c6 \ --hash=sha256:2e1623409307c86f709054ad191bc7707c4feeacae2e497bd933f2f0054c6eb0 \ --hash=sha256:54ef1b134657d3d14ef24c0c98e29d0276ad8f0e053d5e50d836ba8a520230e7 -libusb1==1.7 \ - --hash=sha256:9d4f66d2ed699986b06bc3082cd262101cb26af7a76a34bd15b7eb56cba37e0f +libusb1==1.7.1 \ + --hash=sha256:adf64a4f3f5c94643a1286f8153bcf4bc787c348b38934aacd7fe17fbeebc571 mnemonic==0.18 \ --hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d pbkdf2==1.3 \ --hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979 -pip==19.1 \ - --hash=sha256:8f59b6cf84584d7962d79fd1be7a8ec0eb198aa52ea864896551736b3614eee9 \ - --hash=sha256:d9137cb543d8a4d73140a3282f6d777b2e786bb6abb8add3ac5b6539c82cd624 -protobuf==3.7.1 \ - --hash=sha256:21e395d7959551e759d604940a115c51c6347d90a475c9baf471a1a86b5604a9 \ - --hash=sha256:57e05e16955aee9e6a0389fcbd58d8289dd2420e47df1a1096b3a232c26eb2dd \ - --hash=sha256:67819e8e48a74c68d87f25cad9f40edfe2faf278cdba5ca73173211b9213b8c9 \ - --hash=sha256:75da7d43a2c8a13b0bc7238ab3c8ae217cbfd5979d33b01e98e1f78defb2d060 \ - --hash=sha256:78e08371e236f193ce947712c072542ff19d0043ab5318c2ea46bbc2aaebdca6 \ - --hash=sha256:7ee5b595db5abb0096e8c4755e69c20dfad38b2d0bcc9bc7bafc652d2496b471 \ - --hash=sha256:86260ecfe7a66c0e9d82d2c61f86a14aa974d340d159b829b26f35f710f615db \ - --hash=sha256:92c77db4bd33ea4ee5f15152a835273f2338a5246b2cbb84bab5d0d7f6e9ba94 \ - --hash=sha256:9c7b90943e0e188394b4f068926a759e3b4f63738190d1ab3d500d53b9ce7614 \ - --hash=sha256:a77f217ea50b2542bae5b318f7acee50d9fc8c95dd6d3656eaeff646f7cab5ee \ - --hash=sha256:ad589ed1d1f83db22df867b10e01fe445516a5a4d7cfa37fe3590a5f6cfc508b \ - --hash=sha256:b06a794901bf573f4b2af87e6139e5cd36ac7c91ac85d7ae3fe5b5f6fc317513 \ - --hash=sha256:bd8592cc5f8b4371d0bad92543370d4658dc41a5ccaaf105597eb5524c616291 \ - --hash=sha256:be48e5a6248a928ec43adf2bea037073e5da692c0b3c10b34f9904793bd63138 \ - --hash=sha256:cc5eb13f5ccc4b1b642cc147c2cdd121a34278b341c7a4d79e91182fff425836 \ - --hash=sha256:cd3b0e0ad69b74ee55e7c321f52a98effed2b4f4cc9a10f3683d869de00590d5 \ - --hash=sha256:d6e88c4920660aa75c0c2c4b53407aef5efd9a6e0ca7d2fc84d79aba2ccbda3a \ - --hash=sha256:ec3c49b6d247152e19110c3a53d9bb4cf917747882017f70796460728b02722e \ - --hash=sha256:f1f5d8b8e0bc9651d81b40ad3d9fb7cdd858ea31fc116dd230393465849dbecd +pip==19.1.1 \ + --hash=sha256:44d3d7d3d30a1eb65c7e5ff1173cdf8f7467850605ac7cc3707b6064bddd0958 \ + --hash=sha256:993134f0475471b91452ca029d4390dc8f298ac63a712814f101cd1b6db46676 +protobuf==3.8.0 \ + --hash=sha256:03f43eac9d5b651f976e91cf46a25b75e5779d98f0f4114b0abfed83376d75f8 \ + --hash=sha256:0c94b21e6de01362f91a86b372555d22a60b59708599ca9d5032ae9fdf8e3538 \ + --hash=sha256:2d2a9f30f61f4063fadd7fb68a2510a6939b43c0d6ceeec5c4704f22225da28e \ + --hash=sha256:34a0b05fca061e4abb77dd180209f68d8637115ff319f51e28a6a9382d69853a \ + --hash=sha256:358710fd0db25372edcf1150fa691f48376a134a6c69ce29f38f185eea7699e6 \ + --hash=sha256:3761ab21883f1d3add8643413b326a0026776879b13ecf904e1e05fe18532c03 \ + --hash=sha256:41e47198b94c27ba05a08b4a95160656105745c462af574e4bcb0807164065c0 \ + --hash=sha256:8c61cc8a76e9d381c665aecc5105fa0f1878cf7db8b5cd17202603bcb386d0fc \ + --hash=sha256:a6eebc4db759e58fdac02efcd3028b811effac881d8a5bad1996e4e8ee6acb47 \ + --hash=sha256:a9c12f7c98093da0a46ba76ec40ace725daa1ac4038c41e4b1466afb5c45bb01 \ + --hash=sha256:cb95068492ba0859b8c9e61fa8ba206a83c64e5d0916fb4543700b2e2b214115 \ + --hash=sha256:cd98476ce7bb4dcd6a7b101f5eecdc073dafea19f311e36eb8fba1a349346277 \ + --hash=sha256:ce64cfbea18c535176bdaa10ba740c0fc4c6d998a3f511c17bedb0ae4b3b167c \ + --hash=sha256:dcbb59eac73fd454e8f2c5fba9e3d3320fd4707ed6a9d3ea3717924a6f0903ea \ + --hash=sha256:dd67f34458ae716029e2a71ede998e9092493b62a519236ca52e3c5202096c87 \ + --hash=sha256:e3c96056eb5b7284a20e256cb0bf783c8f36ad82a4ae5434a7b7cd02384144a7 \ + --hash=sha256:f612d584d7a27e2f39e7b17878430a959c1bc09a74ba09db096b468558e5e126 \ + --hash=sha256:f6de8a7d6122297b81566e5bd4df37fd5d62bec14f8f90ebff8ede1c9726cd0a \ + --hash=sha256:fa529d9261682b24c2aaa683667253175c9acebe0a31105394b221090da75832 pyaes==1.6.1 \ --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f pyblake2==1.1.2 \ @@ -106,9 +106,9 @@ pyblake2==1.1.2 \ --hash=sha256:baa2190bfe549e36163aa44664d4ee3a9080b236fc5d42f50dc6fd36bbdc749e \ --hash=sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4 \ --hash=sha256:fbc9fcde75713930bc2a91b149e97be2401f7c9c56d735b46a109210f58d7358 -requests==2.21.0 \ - --hash=sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e \ - --hash=sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b +requests==2.22.0 \ + --hash=sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4 \ + --hash=sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31 safet==0.1.4 \ --hash=sha256:522c257910f9472e9c77c487425ed286f6721c314653e232bc41c6cedece1bb1 \ --hash=sha256:b152874acdc89ff0c8b2d680bfbf020b3e53527c2ad3404489dd61a548aa56a1 @@ -118,19 +118,19 @@ setuptools==41.0.1 \ six==1.12.0 \ --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \ --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 -trezor==0.11.2 \ - --hash=sha256:7bdec3d6e35e41666580547674f2652c0c466172964da42b325cab2c30b4eb46 \ - --hash=sha256:a6f4b47b37a21247535fc43411cb70a8c61ef0a5a2dfee668bd05611e2741fb8 +trezor==0.11.3 \ + --hash=sha256:c79a500e90d003073c8060d319dceb042caaba9472f13990c77ed37d04a82108 \ + --hash=sha256:f3a99ec0fe7b28f83f936f87bf6ad89c77fef9f576934efc3a70dd569009ded1 typing-extensions==3.7.2 \ --hash=sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64 \ --hash=sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c \ --hash=sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71 -urllib3==1.24.3 \ - --hash=sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4 \ - --hash=sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb +urllib3==1.25.3 \ + --hash=sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1 \ + --hash=sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232 websocket_client==0.56.0 \ --hash=sha256:1151d5fb3a62dc129164292e1227655e4bbc5dd5340a5165dfae61128ec50aa9 \ --hash=sha256:1fd5520878b68b84b5748bb30e592b10d0a91529d5383f74f4964e72b297fd3a -wheel==0.33.1 \ - --hash=sha256:66a8fd76f28977bb664b098372daef2b27f60dc4d1688cfab7b37a09448f0e9d \ - --hash=sha256:8eb4a788b3aec8abf5ff68d4165441bc57420c9f64ca5f471f58c3969fe08668 +wheel==0.33.4 \ + --hash=sha256:5e79117472686ac0c4aef5bad5172ea73a1c2d1646b808c35926bd26bdfb0c08 \ + --hash=sha256:62fcfa03d45b5b722539ccbc07b190e4bfff4bb9e3a4d470dd9f6a0981002565 diff --git a/contrib/deterministic-build/requirements.txt b/contrib/deterministic-build/requirements.txt index d8a74c008..a804caeb1 100644 --- a/contrib/deterministic-build/requirements.txt +++ b/contrib/deterministic-build/requirements.txt @@ -24,9 +24,9 @@ aiohttp==3.5.4 \ aiohttp-socks==0.2.2 \ --hash=sha256:e473ee222b001fe33798957b9ce3352b32c187cf41684f8e2259427925914993 \ --hash=sha256:eebd8939a7c3c1e3e7e1b2552c60039b4c65ef6b8b2351efcbdd98290538e310 -aiorpcX==0.17.0 \ - --hash=sha256:13ccc8361bc3049d649094b69aead6118f6deb5f1b88ad77211be85c4e2ed792 \ - --hash=sha256:b08e7c350c78701ec698c851b405a07d20ac64380c394440c1740b48bb3c5502 +aiorpcX==0.18.3 \ + --hash=sha256:42e354c3e0088cb99a4a46e6f7ca777a08d989519ca1bc46323fef836e25579b \ + --hash=sha256:b7a7ced5df95c79c74f7834e7cc58bb7747dbad9eb37bf7580da507e182ca44c async_timeout==3.0.1 \ --hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \ --hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3 @@ -83,29 +83,29 @@ multidict==4.5.2 \ --hash=sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9 \ --hash=sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7 \ --hash=sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b -pip==19.1 \ - --hash=sha256:8f59b6cf84584d7962d79fd1be7a8ec0eb198aa52ea864896551736b3614eee9 \ - --hash=sha256:d9137cb543d8a4d73140a3282f6d777b2e786bb6abb8add3ac5b6539c82cd624 -protobuf==3.7.1 \ - --hash=sha256:21e395d7959551e759d604940a115c51c6347d90a475c9baf471a1a86b5604a9 \ - --hash=sha256:57e05e16955aee9e6a0389fcbd58d8289dd2420e47df1a1096b3a232c26eb2dd \ - --hash=sha256:67819e8e48a74c68d87f25cad9f40edfe2faf278cdba5ca73173211b9213b8c9 \ - --hash=sha256:75da7d43a2c8a13b0bc7238ab3c8ae217cbfd5979d33b01e98e1f78defb2d060 \ - --hash=sha256:78e08371e236f193ce947712c072542ff19d0043ab5318c2ea46bbc2aaebdca6 \ - --hash=sha256:7ee5b595db5abb0096e8c4755e69c20dfad38b2d0bcc9bc7bafc652d2496b471 \ - --hash=sha256:86260ecfe7a66c0e9d82d2c61f86a14aa974d340d159b829b26f35f710f615db \ - --hash=sha256:92c77db4bd33ea4ee5f15152a835273f2338a5246b2cbb84bab5d0d7f6e9ba94 \ - --hash=sha256:9c7b90943e0e188394b4f068926a759e3b4f63738190d1ab3d500d53b9ce7614 \ - --hash=sha256:a77f217ea50b2542bae5b318f7acee50d9fc8c95dd6d3656eaeff646f7cab5ee \ - --hash=sha256:ad589ed1d1f83db22df867b10e01fe445516a5a4d7cfa37fe3590a5f6cfc508b \ - --hash=sha256:b06a794901bf573f4b2af87e6139e5cd36ac7c91ac85d7ae3fe5b5f6fc317513 \ - --hash=sha256:bd8592cc5f8b4371d0bad92543370d4658dc41a5ccaaf105597eb5524c616291 \ - --hash=sha256:be48e5a6248a928ec43adf2bea037073e5da692c0b3c10b34f9904793bd63138 \ - --hash=sha256:cc5eb13f5ccc4b1b642cc147c2cdd121a34278b341c7a4d79e91182fff425836 \ - --hash=sha256:cd3b0e0ad69b74ee55e7c321f52a98effed2b4f4cc9a10f3683d869de00590d5 \ - --hash=sha256:d6e88c4920660aa75c0c2c4b53407aef5efd9a6e0ca7d2fc84d79aba2ccbda3a \ - --hash=sha256:ec3c49b6d247152e19110c3a53d9bb4cf917747882017f70796460728b02722e \ - --hash=sha256:f1f5d8b8e0bc9651d81b40ad3d9fb7cdd858ea31fc116dd230393465849dbecd +pip==19.1.1 \ + --hash=sha256:44d3d7d3d30a1eb65c7e5ff1173cdf8f7467850605ac7cc3707b6064bddd0958 \ + --hash=sha256:993134f0475471b91452ca029d4390dc8f298ac63a712814f101cd1b6db46676 +protobuf==3.8.0 \ + --hash=sha256:03f43eac9d5b651f976e91cf46a25b75e5779d98f0f4114b0abfed83376d75f8 \ + --hash=sha256:0c94b21e6de01362f91a86b372555d22a60b59708599ca9d5032ae9fdf8e3538 \ + --hash=sha256:2d2a9f30f61f4063fadd7fb68a2510a6939b43c0d6ceeec5c4704f22225da28e \ + --hash=sha256:34a0b05fca061e4abb77dd180209f68d8637115ff319f51e28a6a9382d69853a \ + --hash=sha256:358710fd0db25372edcf1150fa691f48376a134a6c69ce29f38f185eea7699e6 \ + --hash=sha256:3761ab21883f1d3add8643413b326a0026776879b13ecf904e1e05fe18532c03 \ + --hash=sha256:41e47198b94c27ba05a08b4a95160656105745c462af574e4bcb0807164065c0 \ + --hash=sha256:8c61cc8a76e9d381c665aecc5105fa0f1878cf7db8b5cd17202603bcb386d0fc \ + --hash=sha256:a6eebc4db759e58fdac02efcd3028b811effac881d8a5bad1996e4e8ee6acb47 \ + --hash=sha256:a9c12f7c98093da0a46ba76ec40ace725daa1ac4038c41e4b1466afb5c45bb01 \ + --hash=sha256:cb95068492ba0859b8c9e61fa8ba206a83c64e5d0916fb4543700b2e2b214115 \ + --hash=sha256:cd98476ce7bb4dcd6a7b101f5eecdc073dafea19f311e36eb8fba1a349346277 \ + --hash=sha256:ce64cfbea18c535176bdaa10ba740c0fc4c6d998a3f511c17bedb0ae4b3b167c \ + --hash=sha256:dcbb59eac73fd454e8f2c5fba9e3d3320fd4707ed6a9d3ea3717924a6f0903ea \ + --hash=sha256:dd67f34458ae716029e2a71ede998e9092493b62a519236ca52e3c5202096c87 \ + --hash=sha256:e3c96056eb5b7284a20e256cb0bf783c8f36ad82a4ae5434a7b7cd02384144a7 \ + --hash=sha256:f612d584d7a27e2f39e7b17878430a959c1bc09a74ba09db096b468558e5e126 \ + --hash=sha256:f6de8a7d6122297b81566e5bd4df37fd5d62bec14f8f90ebff8ede1c9726cd0a \ + --hash=sha256:fa529d9261682b24c2aaa683667253175c9acebe0a31105394b221090da75832 pyaes==1.6.1 \ --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f QDarkStyle==2.6.8 \ @@ -124,9 +124,9 @@ typing-extensions==3.7.2 \ --hash=sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64 \ --hash=sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c \ --hash=sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71 -wheel==0.33.1 \ - --hash=sha256:66a8fd76f28977bb664b098372daef2b27f60dc4d1688cfab7b37a09448f0e9d \ - --hash=sha256:8eb4a788b3aec8abf5ff68d4165441bc57420c9f64ca5f471f58c3969fe08668 +wheel==0.33.4 \ + --hash=sha256:5e79117472686ac0c4aef5bad5172ea73a1c2d1646b808c35926bd26bdfb0c08 \ + --hash=sha256:62fcfa03d45b5b722539ccbc07b190e4bfff4bb9e3a4d470dd9f6a0981002565 yarl==1.3.0 \ --hash=sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9 \ --hash=sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f \ From 0ec574bcf8b9d58462efee700df96a744a753435 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Jun 2019 21:00:48 +0200 Subject: [PATCH 013/115] kivy tx_dialog: fix size of buttons in "Options" dropdown --- electrum/gui/kivy/uix/dialogs/tx_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py index d4c0e67b2..d018e5a77 100644 --- a/electrum/gui/kivy/uix/dialogs/tx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/tx_dialog.py @@ -184,7 +184,7 @@ class TxDialog(Factory.Popup): self._action_button_fn = dropdown.open for option in options: if option.enabled: - btn = Button(text=option.text, size_hint_y=None, height=48) + btn = Button(text=option.text, size_hint_y=None, height='48dp') btn.bind(on_release=option.func) dropdown.add_widget(btn) From d2de8de356213585eccb5d4b963429f9a674ed65 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Jun 2019 16:29:33 +0200 Subject: [PATCH 014/115] qt payment requests: fix some races closes #5283, #5407, #5121 --- electrum/gui/qt/main_window.py | 19 ++++++++++++++----- electrum/paymentrequest.py | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 87e3dc266..7c252afb1 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1602,11 +1602,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): """Returns whether there are errors with outputs. Also shows error dialog to user if so. """ - if self.payment_request and self.payment_request.has_expired(): - self.show_error(_('Payment request has expired')) - return True + pr = self.payment_request + if pr: + if pr.error: + return True + if pr.has_expired(): + self.show_error(_('Payment request has expired')) + return True - if not self.payment_request: + if not pr: errors = self.payto_e.get_errors() if errors: self.show_warning(_("Invalid Lines found:") + "\n\n" + '\n'.join([ _("Line #") + str(x[0]+1) + ": " + x[1] for x in errors])) @@ -1820,6 +1824,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def payment_request_ok(self): pr = self.payment_request + if not pr: + return key = self.invoices.add(pr) status = self.invoices.get_status(key) self.invoice_list.update() @@ -1840,7 +1846,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.amount_e.textEdited.emit("") def payment_request_error(self): - self.show_message(self.payment_request.error) + pr = self.payment_request + if not pr: + return + self.show_message(pr.error) self.payment_request = None self.do_clear() diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 881a3eea2..05967918d 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -123,6 +123,7 @@ class PaymentRequest: return str(self.raw) def parse(self, r): + self.outputs = [] if self.error: return self.id = bh2u(sha256(r)[0:16]) @@ -134,7 +135,6 @@ class PaymentRequest: return self.details = pb2.PaymentDetails() self.details.ParseFromString(self.data.serialized_payment_details) - self.outputs = [] for o in self.details.outputs: type_, addr = transaction.get_address_from_output_script(o.script) if type_ != TYPE_ADDRESS: From 0553ab7f3ff31326571a197aa9b2218f4bc97097 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Jun 2019 19:05:58 +0200 Subject: [PATCH 015/115] follow-up prev PaymentRequest.error is really not intuitive......... --- electrum/gui/qt/main_window.py | 9 +++------ electrum/paymentrequest.py | 11 +++++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 7c252afb1..7ebe5e907 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -37,6 +37,7 @@ import base64 from functools import partial import queue import asyncio +from typing import Optional from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal @@ -71,6 +72,7 @@ from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed from electrum.exchange_rate import FxThread from electrum.simple_config import SimpleConfig from electrum.logging import Logger +from electrum.paymentrequest import PR_PAID from .exception_window import Exception_Hook from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit @@ -109,9 +111,6 @@ class StatusBarButton(QPushButton): self.func() -from electrum.paymentrequest import PR_PAID - - class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): payment_request_ok_signal = pyqtSignal() @@ -141,7 +140,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.tray = gui_object.tray self.app = gui_object.app self.cleaned_up = False - self.payment_request = None + self.payment_request = None # type: Optional[paymentrequest.PaymentRequest] self.checking_accounts = False self.qr_window = None self.not_enough_funds = False @@ -1604,8 +1603,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): """ pr = self.payment_request if pr: - if pr.error: - return True if pr.has_expired(): self.show_error(_('Payment request has expired')) return True diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 05967918d..f12fd4026 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -27,6 +27,7 @@ import sys import time import traceback import json +from typing import Optional import certifi import urllib.parse @@ -106,15 +107,15 @@ async def get_payment_request(url: str) -> 'PaymentRequest': else: data = None error = f"Unknown scheme for payment request. URL: {url}" - pr = PaymentRequest(data, error) + pr = PaymentRequest(data, error=error) return pr class PaymentRequest: - def __init__(self, data, error=None): + def __init__(self, data, *, error=None): self.raw = data - self.error = error + self.error = error # FIXME overloaded and also used when 'verify' succeeds self.parse(data) self.requestor = None # known after verify self.tx = None @@ -235,7 +236,9 @@ class PaymentRequest: self.error = "unknown algo" return False - def has_expired(self): + def has_expired(self) -> Optional[bool]: + if not hasattr(self, 'details'): + return None return self.details.expires and self.details.expires < int(time.time()) def get_expiration_date(self): From 33308307a47bf9ffc6e553a273c3ab0dfea57aea Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Jun 2019 19:40:33 +0200 Subject: [PATCH 016/115] bip70 payreq: do not show error messages in gui closes #5393 --- electrum/paymentrequest.py | 32 ++++++++++++++++++++++++++------ electrum/util.py | 1 + 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index f12fd4026..fedcd1b3a 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -93,9 +93,19 @@ async def get_payment_request(url: str) -> 'PaymentRequest': data_len = len(data) if data is not None else None _logger.info(f'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") + error = f"Error while contacting payment URL: {url}.\nerror type: {type(e)}" + if isinstance(e, aiohttp.ClientResponseError): + error += f"\nGot HTTP status code {e.status}." + if resp_content: + try: + error_text_received = resp_content.decode("utf8") + except UnicodeDecodeError: + error_text_received = "(failed to decode error)" + else: + error_text_received = error_text_received[:400] + error_oneline = ' -- '.join(error.split('\n')) + _logger.info(f"{error_oneline} -- [DO NOT TRUST THIS MESSAGE] " + f"{repr(e)} text: {error_text_received}") data = None elif u.scheme == 'file': try: @@ -305,9 +315,19 @@ class PaymentRequest: 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") + error = f"Payment Message/PaymentACK Failed:\nerror type: {type(e)}" + if isinstance(e, aiohttp.ClientResponseError): + error += f"\nGot HTTP status code {e.status}." + if resp_content: + try: + error_text_received = resp_content.decode("utf8") + except UnicodeDecodeError: + error_text_received = "(failed to decode error)" + else: + error_text_received = error_text_received[:400] + error_oneline = ' -- '.join(error.split('\n')) + _logger.info(f"{error_oneline} -- [DO NOT TRUST THIS MESSAGE] " + f"{repr(e)} text: {error_text_received}") return False, error diff --git a/electrum/util.py b/electrum/util.py index 44ab5ec52..85aa46325 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -800,6 +800,7 @@ def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict: sig = out.get('sig') name = out.get('name') if on_pr and (r or (name and sig)): + @log_exceptions async def get_payment_request(): from . import paymentrequest as pr if name and sig: From 53d189fc7ae5994e360849a123e687bd257badc0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Jun 2019 19:49:06 +0200 Subject: [PATCH 017/115] storage: fix some madness about get_data_ref() and put() interacting badly previously load_transactions() had to be called before upgrade(); now we reverse this order. to reproduce/illustrate issue, before this commit: try running convert_version_17 and convert_version_18 (e.g. see testcase test_upgrade_from_client_2_9_3_old_seeded_with_realistic_history) and then in qt console: >> wallet.storage.db.get_data_ref('spent_outpoints') == wallet.storage.db.spent_outpoints False >> wallet.storage.db.get_data_ref('verified_tx3') == wallet.storage.db.verified_tx False --- electrum/address_synchronizer.py | 3 ++ electrum/json_db.py | 41 +++++++++++++------------- electrum/storage.py | 4 +++ electrum/tests/test_storage_upgrade.py | 6 ++++ electrum/wallet.py | 4 +-- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 9da7b8852..698fcfac1 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -61,6 +61,9 @@ class AddressSynchronizer(Logger): """ def __init__(self, storage: 'WalletStorage'): + if not storage.is_ready_to_be_used_by_wallet(): + raise Exception("storage not ready to be used by AddressSynchronizer") + self.storage = storage self.db = self.storage.db self.network = None # type: Network diff --git a/electrum/json_db.py b/electrum/json_db.py index c83c53870..e990bbf9d 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -59,12 +59,12 @@ class JsonDB(Logger): self.data = {} self._modified = False self.manual_upgrades = manual_upgrades - self._called_load_transactions = False + self._called_after_upgrade_tasks = False if raw: # loading existing db self.load_data(raw) else: # creating new db self.put('seed_version', FINAL_SEED_VERSION) - self.load_transactions() + self._after_upgrade_tasks() def set_modified(self, b): with self.lock: @@ -108,12 +108,6 @@ class JsonDB(Logger): self.data[key] = copy.deepcopy(value) return True elif key in self.data: - # clear current contents in case of references - cur_val = self.data[key] - clear_method = getattr(cur_val, "clear", None) - if callable(clear_method): - clear_method() - # pop from dict to delete key self.data.pop(key) return True return False @@ -149,9 +143,9 @@ class JsonDB(Logger): if not self.manual_upgrades and self.requires_split(): raise WalletFileException("This wallet has multiple accounts and must be split") - self.load_transactions() - - if not self.manual_upgrades and self.requires_upgrade(): + if not self.requires_upgrade(): + self._after_upgrade_tasks() + elif not self.manual_upgrades: self.upgrade() def requires_split(self): @@ -204,11 +198,9 @@ class JsonDB(Logger): @profiler def upgrade(self): self.logger.info('upgrading wallet format') - if not self._called_load_transactions: - # note: not sure if this is how we should go about this... - # alternatively, we could make sure load_transactions is always called after upgrade - # still, we need strict ordering between the two. - raise Exception("'load_transactions' must be called before 'upgrade'") + if self._called_after_upgrade_tasks: + # we need strict ordering between upgrade() and after_upgrade_tasks() + raise Exception("'after_upgrade_tasks' must NOT be called before 'upgrade'") self._convert_imported() self._convert_wallet_type() self._convert_account() @@ -220,6 +212,12 @@ class JsonDB(Logger): self._convert_version_18() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure + self._after_upgrade_tasks() + + def _after_upgrade_tasks(self): + self._called_after_upgrade_tasks = True + self._load_transactions() + def _convert_wallet_type(self): if not self._is_upgrade_method_needed(0, 13): return @@ -415,9 +413,10 @@ class JsonDB(Logger): self.put('pruned_txo', None) - transactions = self.get('transactions', {}) # txid -> Transaction + transactions = self.get('transactions', {}) # txid -> raw_tx spent_outpoints = defaultdict(dict) - for txid, tx in transactions.items(): + for txid, raw_tx in transactions.items(): + tx = Transaction(raw_tx) for txin in tx.inputs(): if txin['type'] == 'coinbase': continue @@ -475,6 +474,7 @@ class JsonDB(Logger): self.put('accounts', None) def _is_upgrade_method_needed(self, min_version, max_version): + assert min_version <= max_version cur_version = self.get_seed_version() if cur_version > max_version: return False @@ -673,6 +673,8 @@ class JsonDB(Logger): @locked def get_data_ref(self, name): + # Warning: interacts un-intuitively with 'put': certain parts + # of 'data' will have pointers saved as separate variables. if name not in self.data: self.data[name] = {} return self.data[name] @@ -745,8 +747,7 @@ class JsonDB(Logger): self._addr_to_addr_index[addr] = (True, i) @profiler - def load_transactions(self): - self._called_load_transactions = True + def _load_transactions(self): # references in self.data self.txi = self.get_data_ref('txi') # txid -> address -> list of (prev_outpoint, value) self.txo = self.get_data_ref('txo') # txid -> address -> list of (output_index, value, is_coinbase) diff --git a/electrum/storage.py b/electrum/storage.py index e00e373f9..7a711bd88 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -226,6 +226,9 @@ class WalletStorage(Logger): raise Exception("storage not yet decrypted!") return self.db.requires_upgrade() + def is_ready_to_be_used_by_wallet(self): + return not self.requires_upgrade() and self.db._called_after_upgrade_tasks + def upgrade(self): self.db.upgrade() self.write() @@ -240,6 +243,7 @@ class WalletStorage(Logger): path = self.path + '.' + data['suffix'] storage = WalletStorage(path) storage.db.data = data + storage.db._called_after_upgrade_tasks = False storage.db.upgrade() storage.write() out.append(path) diff --git a/electrum/tests/test_storage_upgrade.py b/electrum/tests/test_storage_upgrade.py index 96a601922..bb74fc026 100644 --- a/electrum/tests/test_storage_upgrade.py +++ b/electrum/tests/test_storage_upgrade.py @@ -305,7 +305,13 @@ class TestStorageUpgrade(WalletTestCase): storage2 = self._load_storage_from_json_string(wallet_json=wallet_json, path=path2, manual_upgrades=False) + storage2.write() self._sanity_check_upgraded_storage(storage2) + # test opening upgraded storages again + s1 = WalletStorage(path2, manual_upgrades=False) + self._sanity_check_upgraded_storage(s1) + s2 = WalletStorage(path2, manual_upgrades=True) + self._sanity_check_upgraded_storage(s2) else: storage = self._load_storage_from_json_string(wallet_json=wallet_json, path=self.wallet_path, diff --git a/electrum/wallet.py b/electrum/wallet.py index a3837952a..ce35ca0d2 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -206,8 +206,8 @@ class Abstract_Wallet(AddressSynchronizer): gap_limit_for_change = 6 def __init__(self, storage: WalletStorage): - if storage.requires_upgrade(): - raise Exception("storage must be upgraded before constructing wallet") + if not storage.is_ready_to_be_used_by_wallet(): + raise Exception("storage not ready to be used by Abstract_Wallet") # load addresses needs to be called before constructor for sanity checks storage.db.load_addresses(self.wallet_type) From 6bdc6f559c468a8d24b9581badd1efc3caa78815 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Jun 2019 19:51:37 +0200 Subject: [PATCH 018/115] storage: fix bug in convert_version_17 closes #5400 --- electrum/json_db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/json_db.py b/electrum/json_db.py index e990bbf9d..e88c582f3 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -422,7 +422,7 @@ class JsonDB(Logger): continue prevout_hash = txin['prevout_hash'] prevout_n = txin['prevout_n'] - spent_outpoints[prevout_hash][prevout_n] = txid + spent_outpoints[prevout_hash][str(prevout_n)] = txid self.put('spent_outpoints', spent_outpoints) self.put('seed_version', 17) From 5c83df7709505ddde9584cd9de1ce2c614292a3a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 7 Jun 2019 20:06:15 +0200 Subject: [PATCH 019/115] android: update kivy, p4a, buildozer --- electrum/gui/kivy/tools/Dockerfile | 14 ++++++++------ electrum/gui/kivy/tools/buildozer.spec | 14 +++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/electrum/gui/kivy/tools/Dockerfile b/electrum/gui/kivy/tools/Dockerfile index 319385759..5e1f55e83 100644 --- a/electrum/gui/kivy/tools/Dockerfile +++ b/electrum/gui/kivy/tools/Dockerfile @@ -127,6 +127,8 @@ USER ${USER} RUN python3 -m pip install --upgrade cython==0.28.6 +RUN python3 -m pip install --upgrade pip +RUN python3 -m pip install --user wheel # prepare git RUN git config --global user.name "John Doe" \ @@ -136,8 +138,8 @@ RUN git config --global user.name "John Doe" \ RUN cd /opt \ && git clone https://github.com/kivy/buildozer \ && cd buildozer \ - && git checkout 88e4a4b0c7733eec1d14c00579ec412fb59ad7f2 \ - && python3 -m pip install -e . + && git checkout 678b1bf52cf63daa51b06e86a43ea4e2ea8a0b24 \ + && python3 -m pip install --user -e . # install python-for-android RUN cd /opt \ @@ -145,12 +147,12 @@ RUN cd /opt \ && cd python-for-android \ && git remote add sombernight https://github.com/SomberNight/python-for-android \ && git fetch --all \ - && git checkout dec1badc3bd134a9a1c69275339423a95d63413e \ + && git checkout ccb0f8e1bab36f1b7d1508216b4b4afb076e614f \ # allowBackup="false": && git cherry-pick d7f722e4e5d4b3e6f5b1733c95e6a433f78ee570 \ - # enable IPv6: - && git cherry-pick a607f4a446773ac0b0a5150171092b0617fbe670 \ - && python3 -m pip install -e . + # fix gradle "versionCode" overflow: + && git cherry-pick ed20e196fbcdce718a180f88f23bb2d165c4c5d8 \ + && python3 -m pip install --user -e . # build env vars ENV USE_SDK_WRAPPER=1 diff --git a/electrum/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec index 9748ba36c..77fb0291a 100644 --- a/electrum/gui/kivy/tools/buildozer.spec +++ b/electrum/gui/kivy/tools/buildozer.spec @@ -35,7 +35,14 @@ version.filename = %(source.dir)s/electrum/version.py #version = 1.9.8 # (list) Application requirements -requirements = python3, android, openssl, plyer, kivy==b47f669f44dbda4f463bcb7d2cada639f7fed3bc, libffi, libsecp256k1 +requirements = + python3, + android, + openssl, + plyer, + kivy==82d561d62577757d478df52173610f925c05ecab, + libffi, + libsecp256k1 # (str) Presplash of the application #presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png @@ -64,11 +71,8 @@ android.api = 28 # (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 -android.sdk = 24 - # (str) Android NDK version to use -android.ndk = 14b +android.ndk = 17c # (int) Android NDK API to use (optional). This is the minimum API your app will support. android.ndk_api = 21 From 7120c344b2d12b60be8c5fe82e7a9964425075c9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 8 Jun 2019 15:37:49 +0200 Subject: [PATCH 020/115] qt seed completer: colour words yellow if only in old electrum list Some people complained that due to merging the two word lists, it is difficult to restore from a metal backup, as they planned to rely on the "4 letter prefixes are unique in bip39 word list" property. So we colour words that are only in old list. --- electrum/gui/qt/seed_dialog.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/seed_dialog.py b/electrum/gui/qt/seed_dialog.py index f2b559fa8..41372228d 100644 --- a/electrum/gui/qt/seed_dialog.py +++ b/electrum/gui/qt/seed_dialog.py @@ -24,16 +24,16 @@ # SOFTWARE. from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPixmap +from PyQt5.QtGui import QPixmap, QPalette from PyQt5.QtWidgets import (QVBoxLayout, QCheckBox, QHBoxLayout, QLineEdit, - QLabel, QCompleter, QDialog) + QLabel, QCompleter, QDialog, QStyledItemDelegate) from electrum.i18n import _ from electrum.mnemonic import Mnemonic, seed_type import electrum.old_mnemonic from .util import (Buttons, OkButton, WWLabel, ButtonsTextEdit, icon_path, - EnterButton, CloseButton, WindowModalDialog) + EnterButton, CloseButton, WindowModalDialog, ColorScheme) from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit from .completion_text_edit import CompletionTextEdit @@ -149,11 +149,26 @@ class SeedLayout(QVBoxLayout): self.addWidget(self.seed_warning) def initialize_completer(self): - english_list = Mnemonic('en').wordlist + bip39_english_list = Mnemonic('en').wordlist old_list = electrum.old_mnemonic.words - self.wordlist = english_list + list(set(old_list) - set(english_list)) #concat both lists + only_old_list = set(old_list) - set(bip39_english_list) + self.wordlist = bip39_english_list + list(only_old_list) # concat both lists self.wordlist.sort() + + class CompleterDelegate(QStyledItemDelegate): + def initStyleOption(self, option, index): + super().initStyleOption(option, index) + # Some people complained that due to merging the two word lists, + # it is difficult to restore from a metal backup, as they planned + # to rely on the "4 letter prefixes are unique in bip39 word list" property. + # So we color words that are only in old list. + if option.text in only_old_list: + # yellow bg looks ~ok on both light/dark theme, regardless if (un)selected + option.backgroundBrush = ColorScheme.YELLOW.as_color(background=True) + self.completer = QCompleter(self.wordlist) + delegate = CompleterDelegate(self.seed_e) + self.completer.popup().setItemDelegate(delegate) self.seed_e.set_completer(self.completer) def get_seed(self): @@ -174,7 +189,7 @@ class SeedLayout(QVBoxLayout): self.seed_type_label.setText(label) self.parent.next_button.setEnabled(b) - # to account for bip39 seeds + # disable suggestions if user already typed an unknown word for word in self.get_seed().split(" ")[:-1]: if word not in self.wordlist: self.seed_e.disable_suggestions() From 9d2b601cc7998afe10c1a2e239df4a051adf85a0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 11 Jun 2019 19:19:43 +0200 Subject: [PATCH 021/115] update block explorer URL for blockchain.info closes #5408 --- electrum/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index 85aa46325..8fee2414a 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -652,7 +652,7 @@ mainnet_block_explorers = { {'tx': 'transactions/', 'addr': 'addresses/'}), 'Bitflyer.jp': ('https://chainflyer.bitflyer.jp/', {'tx': 'Transaction/', 'addr': 'Address/'}), - 'Blockchain.info': ('https://blockchain.info/', + 'Blockchain.info': ('https://blockchain.com/btc/', {'tx': 'tx/', 'addr': 'address/'}), 'blockchainbdgpzk.onion': ('https://blockchainbdgpzk.onion/', {'tx': 'tx/', 'addr': 'address/'}), @@ -687,7 +687,7 @@ testnet_block_explorers = { {'tx': '', 'addr': ''}), 'BlockCypher.com': ('https://live.blockcypher.com/btc-testnet/', {'tx': 'tx/', 'addr': 'address/'}), - 'Blockchain.info': ('https://testnet.blockchain.info/', + 'Blockchain.info': ('https://www.blockchain.com/btctest/', {'tx': 'tx/', 'addr': 'address/'}), 'Blockstream.info': ('https://blockstream.info/testnet/', {'tx': 'tx/', 'addr': 'address/'}), From 63e5119ceb83e7f005c6c4983c49c6d1506c87b2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 11 Jun 2019 20:02:28 +0200 Subject: [PATCH 022/115] builds: parallelise "make" by setting "-j4" --- contrib/build-linux/appimage/build.sh | 11 ++++++----- contrib/build-wine/build-secp256k1.sh | 2 +- contrib/osx/make_osx | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 959ef4c9b..78c6afecc 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -4,10 +4,11 @@ set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.." CONTRIB="$PROJECT_ROOT/contrib" +CONTRIB_APPIMAGE="$CONTRIB/build-linux/appimage" DISTDIR="$PROJECT_ROOT/dist" -BUILDDIR="$CONTRIB/build-linux/appimage/build/appimage" +BUILDDIR="$CONTRIB_APPIMAGE/build/appimage" APPDIR="$BUILDDIR/electrum.AppDir" -CACHEDIR="$CONTRIB/build-linux/appimage/.cache/appimage" +CACHEDIR="$CONTRIB_APPIMAGE/.cache/appimage" # pinned versions PYTHON_VERSION=3.6.8 @@ -49,7 +50,7 @@ tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$BUILDDIR" --enable-shared \ --with-threads \ -q - TZ=UTC faketime -f '2019-01-01 01:01:01' make -s + TZ=UTC faketime -f '2019-01-01 01:01:01' make -j4 -s make -s install > /dev/null ) @@ -71,7 +72,7 @@ info "building libsecp256k1." --enable-module-ecdh \ --disable-jni \ -q - make -s + make -j4 -s make -s install > /dev/null ) @@ -127,7 +128,7 @@ cp "$PROJECT_ROOT/electrum/gui/icons/electrum.png" "$APPDIR/electrum.png" # add launcher -cp "$CONTRIB/build-linux/appimage/apprun.sh" "$APPDIR/AppRun" +cp "$CONTRIB_APPIMAGE/apprun.sh" "$APPDIR/AppRun" info "finalizing AppDir." ( diff --git a/contrib/build-wine/build-secp256k1.sh b/contrib/build-wine/build-secp256k1.sh index 4d1375646..9879a75d3 100755 --- a/contrib/build-wine/build-secp256k1.sh +++ b/contrib/build-wine/build-secp256k1.sh @@ -14,7 +14,7 @@ build_dll() { --enable-experimental \ --enable-module-ecdh \ --disable-jni - make + make -j4 ${1}-strip .libs/libsecp256k1-0.dll } diff --git a/contrib/osx/make_osx b/contrib/osx/make_osx index 58cdb70c6..4217191e7 100755 --- a/contrib/osx/make_osx +++ b/contrib/osx/make_osx @@ -89,7 +89,7 @@ git reset --hard $LIBSECP_VERSION git clean -f -x -q ./autogen.sh ./configure --enable-module-recovery --enable-experimental --enable-module-ecdh --disable-jni -make +make -j4 popd cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/osx From c7b64f4794bbd9dd8a2b44046039c26d36d1db67 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 11 Jun 2019 20:24:51 +0200 Subject: [PATCH 023/115] AppImage: update appimagetool version --- contrib/build-linux/appimage/build.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 78c6afecc..a8f5631e2 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -12,7 +12,7 @@ CACHEDIR="$CONTRIB_APPIMAGE/.cache/appimage" # pinned versions PYTHON_VERSION=3.6.8 -PKG2APPIMAGE_COMMIT="83483c2971fcaa1cb0c1253acd6c731ef8404381" +PKG2APPIMAGE_COMMIT="eb8f3acdd9f11ab19b78f5cb15daa772367daf15" LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7" @@ -28,10 +28,10 @@ mkdir -p "$APPDIR" "$CACHEDIR" "$DISTDIR" info "downloading some dependencies." download_if_not_exist "$CACHEDIR/functions.sh" "https://raw.githubusercontent.com/AppImage/pkg2appimage/$PKG2APPIMAGE_COMMIT/functions.sh" -verify_hash "$CACHEDIR/functions.sh" "a73a21a6c1d1e15c0a9f47f017ae833873d1dc6aa74a4c840c0b901bf1dcf09c" +verify_hash "$CACHEDIR/functions.sh" "78b7ee5a04ffb84ee1c93f0cb2900123773bc6709e5d1e43c37519f590f86918" -download_if_not_exist "$CACHEDIR/appimagetool" "https://github.com/probonopd/AppImageKit/releases/download/11/appimagetool-x86_64.AppImage" -verify_hash "$CACHEDIR/appimagetool" "c13026b9ebaa20a17e7e0a4c818a901f0faba759801d8ceab3bb6007dde00372" +download_if_not_exist "$CACHEDIR/appimagetool" "https://github.com/AppImage/AppImageKit/releases/download/12/appimagetool-x86_64.AppImage" +verify_hash "$CACHEDIR/appimagetool" "d918b4df547b388ef253f3c9e7f6529ca81a885395c31f619d9aaf7030499a13" download_if_not_exist "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz" verify_hash "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "35446241e995773b1bed7d196f4b624dadcadc8429f26282e756b2fb8a351193" From 9e21b76c916c139be5c1d10316c737f183494cd3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 12 Jun 2019 18:09:38 +0200 Subject: [PATCH 024/115] wallet: stricter validation in export_private_key fixes #5422 --- electrum/bip32.py | 4 ++++ electrum/tests/test_commands.py | 37 +++++++++++++++++++++++++++++++++ electrum/tests/test_wallet.py | 10 +++++---- electrum/wallet.py | 19 +++++++++++++---- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/electrum/bip32.py b/electrum/bip32.py index ba7b4a821..102fb0a98 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -200,6 +200,8 @@ class BIP32Node(NamedTuple): return isinstance(self.eckey, ecc.ECPrivkey) def subkey_at_private_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32Node': + if path is None: + raise Exception("derivation path must not be None") if isinstance(path, str): path = convert_bip32_path_to_list_of_uint32(path) if not self.is_private(): @@ -224,6 +226,8 @@ class BIP32Node(NamedTuple): child_number=child_number) def subkey_at_public_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32Node': + if path is None: + raise Exception("derivation path must not be None") if isinstance(path, str): path = convert_bip32_path_to_list_of_uint32(path) if not path: diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py index 4dc8642bb..5e08661ab 100644 --- a/electrum/tests/test_commands.py +++ b/electrum/tests/test_commands.py @@ -75,6 +75,43 @@ class TestCommands(unittest.TestCase): ciphertext = cmds.encrypt(pubkey, cleartext) self.assertEqual(cleartext, cmds.decrypt(pubkey, ciphertext)) + @mock.patch.object(storage.WalletStorage, '_write') + def test_export_private_key_imported(self, mock_write): + wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', + path='if_this_exists_mocking_failed_648151893')['wallet'] + cmds = Commands(config=None, wallet=wallet, network=None) + # single address tests + with self.assertRaises(Exception): + cmds.getprivatekeys("asdasd") # invalid addr, though might raise "not in wallet" + with self.assertRaises(Exception): + cmds.getprivatekeys("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23") # not in wallet + self.assertEqual("p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL", + cmds.getprivatekeys("bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw")) + # list of addresses tests + with self.assertRaises(Exception): + cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'asd']) + self.assertEqual(['p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'], + cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'])) + + @mock.patch.object(storage.WalletStorage, '_write') + def test_export_private_key_deterministic(self, mock_write): + wallet = restore_wallet_from_text('bitter grass shiver impose acquire brush forget axis eager alone wine silver', + gap_limit=2, + path='if_this_exists_mocking_failed_648151893')['wallet'] + cmds = Commands(config=None, wallet=wallet, network=None) + # single address tests + with self.assertRaises(Exception): + cmds.getprivatekeys("asdasd") # invalid addr, though might raise "not in wallet" + with self.assertRaises(Exception): + cmds.getprivatekeys("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23") # not in wallet + self.assertEqual("p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2", + cmds.getprivatekeys("bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af")) + # list of addresses tests + with self.assertRaises(Exception): + cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'asd']) + self.assertEqual(['p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'], + cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'])) + class TestCommandsTestnet(TestCaseForTestnet): diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py index ce98fb156..f7ed78494 100644 --- a/electrum/tests/test_wallet.py +++ b/electrum/tests/test_wallet.py @@ -156,7 +156,8 @@ class TestCreateRestoreWallet(WalletTestCase): passphrase=passphrase, password=password, encrypt_file=encrypt_file, - segwit=True) + segwit=True, + gap_limit=1) wallet = d['wallet'] # type: Standard_Wallet wallet.check_password(password) self.assertEqual(passphrase, wallet.keystore.get_passphrase(password)) @@ -173,7 +174,8 @@ class TestCreateRestoreWallet(WalletTestCase): network=None, passphrase=passphrase, password=password, - encrypt_file=encrypt_file) + encrypt_file=encrypt_file, + gap_limit=1) wallet = d['wallet'] # type: Standard_Wallet self.assertEqual(passphrase, wallet.keystore.get_passphrase(password)) self.assertEqual(text, wallet.keystore.get_seed(password)) @@ -182,14 +184,14 @@ class TestCreateRestoreWallet(WalletTestCase): def test_restore_wallet_from_text_xpub(self): text = 'zpub6nydoME6CFdJtMpzHW5BNoPz6i6XbeT9qfz72wsRqGdgGEYeivso6xjfw8cGcCyHwF7BNW4LDuHF35XrZsovBLWMF4qXSjmhTXYiHbWqGLt' - d = restore_wallet_from_text(text, path=self.wallet_path, network=None) + d = restore_wallet_from_text(text, path=self.wallet_path, network=None, gap_limit=1) wallet = d['wallet'] # type: Standard_Wallet self.assertEqual(text, wallet.keystore.get_master_public_key()) self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0]) def test_restore_wallet_from_text_xprv(self): text = 'zprvAZzHPqhCMt51fskXBUYB1fTFYgG3CBjJUT4WEZTpGw6hPSDWBPZYZARC5sE9xAcX8NeWvvucFws8vZxEa65RosKAhy7r5MsmKTxr3hmNmea' - d = restore_wallet_from_text(text, path=self.wallet_path, network=None) + d = restore_wallet_from_text(text, path=self.wallet_path, network=None, gap_limit=1) wallet = d['wallet'] # type: Standard_Wallet self.assertEqual(text, wallet.keystore.get_master_private_key(password=None)) self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0]) diff --git a/electrum/wallet.py b/electrum/wallet.py index ce35ca0d2..2ceaac3f9 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -346,7 +346,9 @@ class Abstract_Wallet(AddressSynchronizer): def export_private_key(self, address, password): if self.is_watching_only(): - return [] + raise Exception(_("This is a watching-only wallet")) + if not self.is_mine(address): + raise Exception(_('Address not in wallet.') + f' {address}') index = self.get_address_index(address) pk, compressed = self.keystore.get_private_key(index, password) txin_type = self.get_txin_type(address) @@ -1485,7 +1487,9 @@ class Imported_Wallet(Simple_Wallet): return self.db.has_imported_address(address) def get_address_index(self, address): - # returns None is address is not mine + # returns None if address is not mine + if not is_address(address): + raise Exception(f"Invalid bitcoin address: {address}") return self.get_public_key(address) def get_public_key(self, address): @@ -1677,6 +1681,8 @@ class Deterministic_Wallet(Abstract_Wallet): return True def get_address_index(self, address): + if not is_address(address): + raise Exception(f"Invalid bitcoin address: {address}") return self.db.get_address_index(address) def get_master_public_keys(self): @@ -1875,7 +1881,7 @@ class Wallet(object): raise WalletFileException("Unknown wallet type: " + str(wallet_type)) -def create_new_wallet(*, path, passphrase=None, password=None, encrypt_file=True, segwit=True): +def create_new_wallet(*, path, passphrase=None, password=None, encrypt_file=True, segwit=True, gap_limit=None): """Create a new wallet""" storage = WalletStorage(path) if storage.file_exists(): @@ -1886,6 +1892,8 @@ def create_new_wallet(*, path, passphrase=None, password=None, encrypt_file=True k = keystore.from_seed(seed, passphrase) storage.put('keystore', k.dump()) storage.put('wallet_type', 'standard') + if gap_limit is not None: + storage.put('gap_limit', gap_limit) wallet = Wallet(storage) wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file) wallet.synchronize() @@ -1896,7 +1904,8 @@ def create_new_wallet(*, path, passphrase=None, password=None, encrypt_file=True def restore_wallet_from_text(text, *, path, network=None, - passphrase=None, password=None, encrypt_file=True): + passphrase=None, password=None, encrypt_file=True, + gap_limit=None): """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.""" @@ -1930,6 +1939,8 @@ def restore_wallet_from_text(text, *, path, network=None, raise Exception("Seed or key not recognized") storage.put('keystore', k.dump()) storage.put('wallet_type', 'standard') + if gap_limit is not None: + storage.put('gap_limit', gap_limit) wallet = Wallet(storage) assert not storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk" From 29ce50a30566d1c69c99896c355df9084a3b3cbd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 12 Jun 2019 18:27:13 +0200 Subject: [PATCH 025/115] follow-up prev wallet.is_mine needs to tolerate None as input --- electrum/wallet.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 2ceaac3f9..d1fa0b88c 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -347,6 +347,8 @@ class Abstract_Wallet(AddressSynchronizer): def export_private_key(self, address, password): if self.is_watching_only(): raise Exception(_("This is a watching-only wallet")) + if not is_address(address): + raise Exception(f"Invalid bitcoin address: {address}") if not self.is_mine(address): raise Exception(_('Address not in wallet.') + f' {address}') index = self.get_address_index(address) @@ -1488,8 +1490,6 @@ class Imported_Wallet(Simple_Wallet): def get_address_index(self, address): # returns None if address is not mine - if not is_address(address): - raise Exception(f"Invalid bitcoin address: {address}") return self.get_public_key(address) def get_public_key(self, address): @@ -1681,8 +1681,6 @@ class Deterministic_Wallet(Abstract_Wallet): return True def get_address_index(self, address): - if not is_address(address): - raise Exception(f"Invalid bitcoin address: {address}") return self.db.get_address_index(address) def get_master_public_keys(self): From 811169da4b9a31388cc1d9e8e2cac738f14eded9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 12 Jun 2019 20:07:36 +0200 Subject: [PATCH 026/115] plugins: on some systems plugins with relative imports failed to load this caused electrum to fail to start potentially only older python 3.6.x are affected fixes #5421 --- electrum/plugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/plugin.py b/electrum/plugin.py index 173ecdc57..a7313862e 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -27,6 +27,7 @@ import pkgutil import importlib.util import time import threading +import sys from typing import NamedTuple, Any, Union, TYPE_CHECKING, Optional from .i18n import _ @@ -73,6 +74,9 @@ class Plugins(DaemonThread): raise Exception(f"Error pre-loading {full_name}: no spec") try: module = importlib.util.module_from_spec(spec) + # sys.modules needs to be modified for relative imports to work + # see https://stackoverflow.com/a/50395128 + sys.modules[spec.name] = module spec.loader.exec_module(module) except Exception as e: raise Exception(f"Error pre-loading {full_name}: {repr(e)}") from e From 23ec426b4f419c3004d043cf67b7d89f3f2f7651 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 13 Jun 2019 01:03:56 +0200 Subject: [PATCH 027/115] qt history list: tweak sort order of items --- 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 333487a1a..5edee2a54 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -149,7 +149,7 @@ class HistoryModel(QAbstractItemModel, Logger): HistoryColumns.STATUS_ICON: # height breaks ties for unverified txns # txpos breaks ties for verified same block txns - (status, conf, -height, -txpos), + (conf, -status, -height, -txpos), HistoryColumns.STATUS_TEXT: status_str, HistoryColumns.DESCRIPTION: tx_item['label'], HistoryColumns.COIN_VALUE: tx_item['value'].value, From d07caaf601c7f6ac6bc2534d2dadbb90a23ea00f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 13 Jun 2019 17:03:12 +0200 Subject: [PATCH 028/115] qt msgbox: when using rich text, set text format to "AutoText" instead "\n" newlines were ignored for WIF_HELP_TEXT InfoButtons --- electrum/gui/qt/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 9ac6649ca..b64223a1d 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -258,7 +258,11 @@ def custom_message_box(*, icon, parent, title, text, buttons=QMessageBox.Ok, d.setDefaultButton(defaultButton) if rich_text: d.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse) - d.setTextFormat(Qt.RichText) + # set AutoText instead of RichText + # AutoText lets Qt figure out whether to render as rich text. + # e.g. if text is actually plain text and uses "\n" newlines; + # and we set RichText here, newlines would be swallowed + d.setTextFormat(Qt.AutoText) else: d.setTextInteractionFlags(Qt.TextSelectableByMouse) d.setTextFormat(Qt.PlainText) From e3c26d7c7a7e3b0e179b0274c12017f5bb04368d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 15 Jun 2019 03:51:11 +0200 Subject: [PATCH 029/115] json_db: fix remove_spent_outpoint method should make sure prevout_n is str... also wrote failing test --- electrum/address_synchronizer.py | 3 +- electrum/json_db.py | 9 +++-- electrum/tests/test_wallet_vertical.py | 46 +++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 698fcfac1..094e83db4 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -195,7 +195,8 @@ class AddressSynchronizer(Logger): if spending_tx_hash is None: continue # this outpoint has already been spent, by spending_tx - assert self.db.get_transaction(spending_tx_hash) + # annoying assert that has revealed several bugs over time: + assert self.db.get_transaction(spending_tx_hash), "spending tx not in wallet db" conflicting_txns |= {spending_tx_hash} if tx_hash in conflicting_txns: # this tx is already in history, so it conflicts with itself diff --git a/electrum/json_db.py b/electrum/json_db.py index e88c582f3..6c2be0891 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -582,19 +582,22 @@ class JsonDB(Logger): @locked def get_spent_outpoint(self, prevout_hash, prevout_n): - return self.spent_outpoints.get(prevout_hash, {}).get(str(prevout_n)) + prevout_n = str(prevout_n) + return self.spent_outpoints.get(prevout_hash, {}).get(prevout_n) @modifier def remove_spent_outpoint(self, prevout_hash, prevout_n): - self.spent_outpoints[prevout_hash].pop(prevout_n, None) # FIXME + prevout_n = str(prevout_n) + self.spent_outpoints[prevout_hash].pop(prevout_n, None) if not self.spent_outpoints[prevout_hash]: self.spent_outpoints.pop(prevout_hash) @modifier def set_spent_outpoint(self, prevout_hash, prevout_n, tx_hash): + prevout_n = str(prevout_n) if prevout_hash not in self.spent_outpoints: self.spent_outpoints[prevout_hash] = {} - self.spent_outpoints[prevout_hash][str(prevout_n)] = tx_hash + self.spent_outpoints[prevout_hash][prevout_n] = tx_hash @modifier def add_transaction(self, tx_hash: str, tx: Transaction) -> None: diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 8889f72e6..a2b1de971 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -8,7 +8,7 @@ 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 -from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet +from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, restore_wallet_from_text from electrum.util import bfh, bh2u from electrum.transaction import TxOutput from electrum.mnemonic import seed_type @@ -1745,3 +1745,47 @@ class TestWalletHistory_EvilGapLimit(TestCaseForTestnet): {}) w.synchronize() self.assertEqual(9999788, sum(w.get_balance())) + + +class TestWalletHistory_DoubleSpend(TestCaseForTestnet): + transactions = { + # txn A: + "a3849040f82705151ba12a4389310b58a17b78025d81116a3338595bdefa1625": "020000000001011b7eb29921187b40209c234344f57a3365669c8883a3d511fbde5155f11f64d10000000000fdffffff024c400f0000000000160014b50d21483fb5e088db90bf766ea79219fb377fef40420f0000000000160014aaf5fc4a6297375c32403a9c2768e7029c8dbd750247304402206efd510954b289829f8f778163b98a2a4039deb93c3b0beb834b00cd0add14fd02201c848315ddc52ced0350a981fe1a7f3cbba145c7a43805db2f126ed549eaa500012103083a50d63264743456a3e812bfc91c11bd2a673ba4628c09f02d78f62157e56d788d1700", + # txn B: + "0e2182ead6660790290371516cb0b80afa8baebd30dad42b5e58a24ceea17f1c": "020000000001012516fade5b5938336a11815d02787ba1580b3189432aa11b150527f8409084a30100000000fdffffff02a086010000000000160014cb893c9fbb565363556fb18a3bcdda6f20af0bf8d8ba0d0000000000160014478902f02c2b6cd405bb6bd1f90e9860bec173e20247304402206940671b5bdb230a9721aa57396af73d399fb210d795e7dbb8ec1977e101a5470220625505de035d4006b72bd6dfcf09468d1e8da53071080b37b16b0dbbf776db78012102254b5b20ed21c3bba75ec2a9ff230257d13a2493f6b7da066d8195dcdd484310788d1700", + # txn C: + "2c9aa33d9c8ec649f9bfb84af027a5414b760be5231fe9eca4a95b9eb3f8a017": "020000000001012516fade5b5938336a11815d02787ba1580b3189432aa11b150527f8409084a30100000000fdffffff01d2410f00000000001600147880a7c79744b908a5f6d6235f2eb46c174c84f002483045022100974d27c872f09115e57c6acb674cd4da6d0b26656ad967ddb2678ff409714b9502206d91b49cf778ced6ca9e40b4094fb57b86c86fac09ce46ce53aea4afa68ff311012102254b5b20ed21c3bba75ec2a9ff230257d13a2493f6b7da066d8195dcdd484310788d1700", + } + + @mock.patch.object(storage.WalletStorage, '_write') + def test_restoring_wallet_without_manual_delete(self, mock_write): + w = restore_wallet_from_text("small rapid pattern language comic denial donate extend tide fever burden barrel", + path='if_this_exists_mocking_failed_648151893', + gap_limit=5)['wallet'] + for txid in self.transactions: + tx = Transaction(self.transactions[txid]) + w.add_transaction(tx.txid(), tx) + # txn A is an external incoming txn funding the wallet + # txn B is an outgoing payment to an external address + # txn C is double-spending txn B, to a wallet address + self.assertEqual(999890, sum(w.get_balance())) + + @mock.patch.object(storage.WalletStorage, '_write') + def test_restoring_wallet_with_manual_delete(self, mock_write): + w = restore_wallet_from_text("small rapid pattern language comic denial donate extend tide fever burden barrel", + path='if_this_exists_mocking_failed_648151893', + gap_limit=5)['wallet'] + # txn A is an external incoming txn funding the wallet + txA = Transaction(self.transactions["a3849040f82705151ba12a4389310b58a17b78025d81116a3338595bdefa1625"]) + w.add_transaction(txA.txid(), txA) + # txn B is an outgoing payment to an external address + txB = Transaction(self.transactions["0e2182ead6660790290371516cb0b80afa8baebd30dad42b5e58a24ceea17f1c"]) + w.add_transaction(txB.txid(), txB) + # now the user manually deletes txn B to attempt the double spend + # txn C is double-spending txn B, to a wallet address + # rationale1: user might do this with opt-in RBF transactions + # rationale2: this might be a local transaction, in which case the GUI even allows it + w.remove_transaction(txB) + txC = Transaction(self.transactions["2c9aa33d9c8ec649f9bfb84af027a5414b760be5231fe9eca4a95b9eb3f8a017"]) + w.add_transaction(txC.txid(), txC) + self.assertEqual(999890, sum(w.get_balance())) From cb204dd969955e6ac6b23821500c406678a3ce97 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 17 Jun 2019 19:55:39 +0200 Subject: [PATCH 030/115] coinchooser: better account for fees in penalty_func --- electrum/coinchooser.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index 5ab633dbf..e911a56df 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -121,7 +121,7 @@ class CoinChooserBase(Logger): return list(map(make_Bucket, buckets.keys(), buckets.values())) - def penalty_func(self, tx): + def penalty_func(self, tx, *, fee_for_buckets): def penalty(candidate): return 0 return penalty @@ -251,10 +251,19 @@ class CoinChooserBase(Logger): total_weight = get_tx_weight(buckets) return total_input >= spent_amount + fee_estimator_w(total_weight) + def fee_for_buckets(buckets) -> int: + """Given a list of buckets, return the total fee paid by the + transaction, in satoshis. + Note that the change output(s) are not yet known here, + so fees for those are excluded and hence this is a lower bound. + """ + total_weight = get_tx_weight(buckets) + return fee_estimator_w(total_weight) + # Collect the coins into buckets, choose a subset of the buckets buckets = self.bucketize_coins(coins) buckets = self.choose_buckets(buckets, sufficient_funds, - self.penalty_func(tx)) + self.penalty_func(tx, fee_for_buckets=fee_for_buckets)) tx.add_inputs([coin for b in buckets for coin in b.coins]) tx_weight = get_tx_weight(buckets) @@ -379,7 +388,7 @@ class CoinChooserPrivacy(CoinChooserRandom): def keys(self, coins): return [coin['address'] for coin in coins] - def penalty_func(self, tx): + def penalty_func(self, tx, *, fee_for_buckets): min_change = min(o.value for o in tx.outputs()) * 0.75 max_change = max(o.value for o in tx.outputs()) * 1.33 spent_amount = sum(o.value for o in tx.outputs()) @@ -387,8 +396,10 @@ class CoinChooserPrivacy(CoinChooserRandom): def penalty(buckets): badness = len(buckets) - 1 total_input = sum(bucket.value for bucket in buckets) - # FIXME "change" here also includes fees - change = float(total_input - spent_amount) + # FIXME fee_for_buckets does not include fees needed to cover the change output(s) + # so fee here is a lower bound + fee = fee_for_buckets(buckets) + change = float(total_input - spent_amount - fee) # Penalize change not roughly in output range if change < min_change: badness += (min_change - change) / (min_change + 10000) From c7a8540d063a6bf058c1151d2b0c8ea6d0b2ff0e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Jun 2019 21:56:52 +0200 Subject: [PATCH 031/115] kivy: show tx fee rate in tx dialog --- electrum/gui/kivy/main_window.py | 6 +++++- electrum/gui/kivy/uix/dialogs/tx_dialog.py | 12 +++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 277b17f7d..2ec4de174 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -14,7 +14,7 @@ from electrum.wallet import Wallet, InternalAddressCorruption from electrum.paymentrequest import InvoiceStore 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.util import format_satoshis, format_satoshis_plain, format_fee_satoshis from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from electrum import blockchain from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed @@ -808,6 +808,10 @@ class ElectrumWindow(App): def format_amount_and_units(self, x): return format_satoshis_plain(x, self.decimal_point()) + ' ' + self.base_unit + def format_fee_rate(self, fee_rate): + # fee_rate is in sat/kB + return format_fee_satoshis(fee_rate/1000) + ' sat/byte' + #@profiler def update_wallet(self, *dt): self._trigger_update_status() diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py index d018e5a77..f62f926dd 100644 --- a/electrum/gui/kivy/uix/dialogs/tx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/tx_dialog.py @@ -27,6 +27,7 @@ Builder.load_string(''' can_broadcast: False can_rbf: False fee_str: '' + feerate_str: '' date_str: '' date_label:'' amount_str: '' @@ -65,6 +66,9 @@ Builder.load_string(''' BoxLabel: text: _('Transaction fee') if root.fee_str else '' value: root.fee_str + BoxLabel: + text: _('Transaction fee rate') if root.feerate_str else '' + value: root.feerate_str TopLabel: text: _('Transaction ID') + ':' if root.tx_hash else '' TxHashLabel: @@ -148,7 +152,13 @@ class TxDialog(Factory.Popup): else: self.is_mine = True self.amount_str = format_amount(-amount) - self.fee_str = format_amount(fee) if fee is not None else _('unknown') + if fee is not None: + self.fee_str = format_amount(fee) + fee_per_kb = fee / self.tx.estimated_size() * 1000 + self.feerate_str = self.app.format_fee_rate(fee_per_kb) + else: + self.fee_str = _('unknown') + self.feerate_str = _('unknown') self.can_sign = self.wallet.can_sign(self.tx) self.ids.output_list.update(self.tx.get_outputs_for_UI()) self.is_local_tx = tx_mined_status.height == TX_HEIGHT_LOCAL From 5effaaf4281f0161ee59471c6eaf576bf2dbb683 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Jun 2019 21:59:49 +0200 Subject: [PATCH 032/115] TxOutput usage: trivial clean-up --- electrum/transaction.py | 2 +- electrum/wallet.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index abbd247ca..57753ca25 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1043,7 +1043,7 @@ class Transaction: return sum(x['value'] for x in self.inputs()) def output_value(self): - return sum(val for tp, addr, val in self.outputs()) + return sum(o.value for o in self.outputs()) def get_fee(self): return self.input_value() - self.output_value() diff --git a/electrum/wallet.py b/electrum/wallet.py index d1fa0b88c..7dac7e19d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -869,17 +869,17 @@ class Abstract_Wallet(AddressSynchronizer): inputs = tx.inputs() outputs = tx.outputs() # use own outputs - s = list(filter(lambda x: self.is_mine(x[1]), outputs)) + s = list(filter(lambda o: self.is_mine(o.address), outputs)) # ... unless there is none if not s: s = outputs x_fee = run_hook('get_tx_extra_fee', self, tx) if x_fee: x_fee_address, x_fee_amount = x_fee - s = filter(lambda x: x[1]!=x_fee_address, s) + s = filter(lambda o: o.address != x_fee_address, s) # prioritize low value outputs, to get rid of dust - s = sorted(s, key=lambda x: x[2]) + s = sorted(s, key=lambda o: o.value) for o in s: i = outputs.index(o) if o.value - delta >= self.dust_threshold(): From 5f71163449cc1a484dad2ae3ac5fd3841ea32789 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 17:32:21 +0200 Subject: [PATCH 033/115] qt crash reporter: add warning that report contents are public --- electrum/gui/qt/exception_window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/gui/qt/exception_window.py b/electrum/gui/qt/exception_window.py index da05044ab..982312c3c 100644 --- a/electrum/gui/qt/exception_window.py +++ b/electrum/gui/qt/exception_window.py @@ -69,6 +69,8 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger): self.description_textfield = QTextEdit() self.description_textfield.setFixedHeight(50) + self.description_textfield.setPlaceholderText(_("Do not enter sensitive/private information here. " + "The report will be visible on the public issue tracker.")) main_box.addWidget(self.description_textfield) main_box.addWidget(QLabel(BaseCrashReporter.ASK_CONFIRM_SEND)) From 6424163d4bad3de72733849db797d10f11b47479 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 21:53:24 +0200 Subject: [PATCH 034/115] wallet: fix rbf_batching edge case The old change output was given to coinchooser as part of possible UTXOs to use. (Though the coinchooser was really unlikely to select it, as by definition that UTXO is unconfirmed) --- electrum/wallet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/wallet.py b/electrum/wallet.py index 7dac7e19d..f5709ab60 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -732,6 +732,8 @@ 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: + # make sure we don't try to spend change from the tx-to-be-replaced: + coins = [c for c in coins if c['prevout_hash'] != base_tx.txid()] 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) From f409b5da40e607819f94093f4ca6a91f8a2b71f0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 17:45:56 +0200 Subject: [PATCH 035/115] coinchooser: refactor so that penalty_func has access to change outputs --- electrum/coinchooser.py | 164 +++++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 76 deletions(-) diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index e911a56df..c8604afb2 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -24,7 +24,7 @@ # SOFTWARE. from collections import defaultdict from math import floor, log10 -from typing import NamedTuple, List +from typing import NamedTuple, List, Callable from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address from .transaction import Transaction, TxOutput @@ -79,6 +79,12 @@ class Bucket(NamedTuple): witness: bool # whether any coin uses segwit +class ScoredCandidate(NamedTuple): + penalty: float + tx: Transaction + buckets: List[Bucket] + + def strip_unneeded(bkts, sufficient_funds): '''Remove buckets that are unnecessary in achieving the spend amount''' if sufficient_funds([], bucket_value_sum=0): @@ -121,12 +127,10 @@ class CoinChooserBase(Logger): return list(map(make_Bucket, buckets.keys(), buckets.values())) - def penalty_func(self, tx, *, fee_for_buckets): - def penalty(candidate): - return 0 - return penalty + def penalty_func(self, base_tx, *, tx_from_buckets) -> Callable[[List[Bucket]], ScoredCandidate]: + raise NotImplementedError - def change_amounts(self, tx, count, fee_estimator, dust_threshold): + def _change_amounts(self, tx, count, fee_estimator): # Break change up if bigger than max_change output_amounts = [o.value for o in tx.outputs()] # Don't split change of less than 0.02 BTC @@ -180,22 +184,60 @@ class CoinChooserBase(Logger): return amounts - def change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold): - amounts = self.change_amounts(tx, len(change_addrs), fee_estimator, - dust_threshold) + def _change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold): + amounts = self._change_amounts(tx, len(change_addrs), fee_estimator) assert min(amounts) >= 0 assert len(change_addrs) >= len(amounts) # If change is above dust threshold after accounting for the # size of the change output, add it to the transaction. - dust = sum(amount for amount in amounts if amount < dust_threshold) amounts = [amount for amount in amounts if amount >= dust_threshold] change = [TxOutput(TYPE_ADDRESS, addr, amount) for addr, amount in zip(change_addrs, amounts)] - self.logger.info(f'change: {change}') - if dust: - self.logger.info(f'not keeping dust {dust}') return change + def _construct_tx_from_selected_buckets(self, *, buckets, base_tx, change_addrs, + fee_estimator_w, dust_threshold, base_weight): + # make a copy of base_tx so it won't get mutated + tx = Transaction.from_io(base_tx.inputs()[:], base_tx.outputs()[:]) + + tx.add_inputs([coin for b in buckets for coin in b.coins]) + tx_weight = self._get_tx_weight(buckets, base_weight=base_weight) + + # change is sent back to sending address unless specified + if not change_addrs: + change_addrs = [tx.inputs()[0]['address']] + # note: this is not necessarily the final "first input address" + # because the inputs had not been sorted at this point + assert is_address(change_addrs[0]) + + # This takes a count of change outputs and returns a tx fee + output_weight = 4 * Transaction.estimated_output_size(change_addrs[0]) + fee = lambda count: fee_estimator_w(tx_weight + count * output_weight) + change = self._change_outputs(tx, change_addrs, fee, dust_threshold) + tx.add_outputs(change) + + return tx, change + + def _get_tx_weight(self, buckets, *, base_weight) -> int: + """Given a collection of buckets, return the total weight of the + resulting transaction. + base_weight is the weight of the tx that includes the fixed (non-change) + outputs and potentially some fixed inputs. Note that the change outputs + at this point are not yet known so they are NOT accounted for. + """ + total_weight = base_weight + sum(bucket.weight for bucket in buckets) + is_segwit_tx = any(bucket.witness for bucket in buckets) + if is_segwit_tx: + total_weight += 2 # marker and flag + # non-segwit inputs were previously assumed to have + # a witness of '' instead of '00' (hex) + # note that mixed legacy/segwit buckets are already ok + num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins) + for bucket in buckets) + total_weight += num_legacy_inputs + + return total_weight + 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 @@ -211,34 +253,20 @@ class CoinChooserBase(Logger): self.p = PRNG(''.join(sorted(utxos))) # Copy the outputs so when adding change we don't modify "outputs" - tx = Transaction.from_io(inputs[:], outputs[:]) - input_value = tx.input_value() + base_tx = Transaction.from_io(inputs[:], outputs[:]) + input_value = base_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() + base_weight = base_tx.estimated_weight() + spent_amount = base_tx.output_value() def fee_estimator_w(weight): return fee_estimator(Transaction.virtual_size_from_weight(weight)) - def get_tx_weight(buckets): - total_weight = base_weight + sum(bucket.weight for bucket in buckets) - is_segwit_tx = any(bucket.witness for bucket in buckets) - if is_segwit_tx: - total_weight += 2 # marker and flag - # non-segwit inputs were previously assumed to have - # a witness of '' instead of '00' (hex) - # note that mixed legacy/segwit buckets are already ok - num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins) - for bucket in buckets) - total_weight += num_legacy_inputs - - return total_weight - def sufficient_funds(buckets, *, bucket_value_sum): '''Given a list of buckets, return True if it has enough value to pay for the transaction''' @@ -248,45 +276,30 @@ class CoinChooserBase(Logger): return False # note re performance: so far this was constant time # what follows is linear in len(buckets) - total_weight = get_tx_weight(buckets) + total_weight = self._get_tx_weight(buckets, base_weight=base_weight) return total_input >= spent_amount + fee_estimator_w(total_weight) - def fee_for_buckets(buckets) -> int: - """Given a list of buckets, return the total fee paid by the - transaction, in satoshis. - Note that the change output(s) are not yet known here, - so fees for those are excluded and hence this is a lower bound. - """ - total_weight = get_tx_weight(buckets) - return fee_estimator_w(total_weight) + def tx_from_buckets(buckets): + return self._construct_tx_from_selected_buckets(buckets=buckets, + base_tx=base_tx, + change_addrs=change_addrs, + fee_estimator_w=fee_estimator_w, + dust_threshold=dust_threshold, + base_weight=base_weight) # Collect the coins into buckets, choose a subset of the buckets - buckets = self.bucketize_coins(coins) - buckets = self.choose_buckets(buckets, sufficient_funds, - self.penalty_func(tx, fee_for_buckets=fee_for_buckets)) - - tx.add_inputs([coin for b in buckets for coin in b.coins]) - tx_weight = get_tx_weight(buckets) - - # change is sent back to sending address unless specified - if not change_addrs: - change_addrs = [tx.inputs()[0]['address']] - # note: this is not necessarily the final "first input address" - # because the inputs had not been sorted at this point - assert is_address(change_addrs[0]) - - # This takes a count of change outputs and returns a tx fee - output_weight = 4 * Transaction.estimated_output_size(change_addrs[0]) - fee = lambda count: fee_estimator_w(tx_weight + count * output_weight) - change = self.change_outputs(tx, change_addrs, fee, dust_threshold) - tx.add_outputs(change) + all_buckets = self.bucketize_coins(coins) + scored_candidate = self.choose_buckets(all_buckets, sufficient_funds, + self.penalty_func(base_tx, tx_from_buckets=tx_from_buckets)) + tx = scored_candidate.tx self.logger.info(f"using {len(tx.inputs())} inputs") - self.logger.info(f"using buckets: {[bucket.desc for bucket in buckets]}") + self.logger.info(f"using buckets: {[bucket.desc for bucket in scored_candidate.buckets]}") return tx - def choose_buckets(self, buckets, sufficient_funds, penalty_func): + def choose_buckets(self, buckets, sufficient_funds, + penalty_func: Callable[[List[Bucket]], ScoredCandidate]) -> ScoredCandidate: raise NotImplemented('To be subclassed') @@ -368,12 +381,14 @@ class CoinChooserRandom(CoinChooserBase): def choose_buckets(self, buckets, sufficient_funds, penalty_func): candidates = self.bucket_candidates_prefer_confirmed(buckets, sufficient_funds) - penalties = [penalty_func(cand) for cand in candidates] - winner = candidates[penalties.index(min(penalties))] - self.logger.info(f"Bucket sets: {len(buckets)}") - self.logger.info(f"Winning penalty: {min(penalties)}") + scored_candidates = [penalty_func(cand) for cand in candidates] + winner = min(scored_candidates, key=lambda x: x.penalty) + self.logger.info(f"Total number of buckets: {len(buckets)}") + self.logger.info(f"Num candidates considered: {len(candidates)}. " + f"Winning penalty: {winner.penalty}") return winner + class CoinChooserPrivacy(CoinChooserRandom): """Attempts to better preserve user privacy. First, if any coin is spent from a user address, all coins are. @@ -388,18 +403,15 @@ class CoinChooserPrivacy(CoinChooserRandom): def keys(self, coins): return [coin['address'] for coin in coins] - def penalty_func(self, tx, *, fee_for_buckets): - min_change = min(o.value for o in tx.outputs()) * 0.75 - max_change = max(o.value for o in tx.outputs()) * 1.33 - spent_amount = sum(o.value for o in tx.outputs()) + def penalty_func(self, base_tx, *, tx_from_buckets): + min_change = min(o.value for o in base_tx.outputs()) * 0.75 + max_change = max(o.value for o in base_tx.outputs()) * 1.33 - def penalty(buckets): + def penalty(buckets) -> ScoredCandidate: + # Penalize using many buckets (~inputs) badness = len(buckets) - 1 - total_input = sum(bucket.value for bucket in buckets) - # FIXME fee_for_buckets does not include fees needed to cover the change output(s) - # so fee here is a lower bound - fee = fee_for_buckets(buckets) - change = float(total_input - spent_amount - fee) + tx, change_outputs = tx_from_buckets(buckets) + change = sum(o.value for o in change_outputs) # Penalize change not roughly in output range if change < min_change: badness += (min_change - change) / (min_change + 10000) @@ -407,7 +419,7 @@ class CoinChooserPrivacy(CoinChooserRandom): badness += (change - max_change) / (max_change + 10000) # Penalize large change; 5 BTC excess ~= using 1 more input badness += change / (COIN * 5) - return badness + return ScoredCandidate(badness, tx, buckets) return penalty From e864fa50882db90df50e6e0240f55197b28097ed Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 17:47:43 +0200 Subject: [PATCH 036/115] coinchooser: tweak heuristic scoring. transactions without any change now get better scores. transactions with really small change get worse scores. --- electrum/coinchooser.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index c8604afb2..9c6a7b88f 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -413,8 +413,13 @@ class CoinChooserPrivacy(CoinChooserRandom): tx, change_outputs = tx_from_buckets(buckets) change = sum(o.value for o in change_outputs) # Penalize change not roughly in output range - if change < min_change: + if change == 0: + pass # no change is great! + elif change < min_change: badness += (min_change - change) / (min_change + 10000) + # Penalize really small change; under 1 mBTC ~= using 1 more input + if change < COIN / 1000: + badness += 1 elif change > max_change: badness += (change - max_change) / (max_change + 10000) # Penalize large change; 5 BTC excess ~= using 1 more input From 8bfe12e047a4be2f534b5c066f0698d9273ff391 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 17:54:37 +0200 Subject: [PATCH 037/115] wallet: split "change address logic" from make_unsigned_transaction --- electrum/wallet.py | 52 ++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index f5709ab60..66380d87e 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -674,6 +674,33 @@ class Abstract_Wallet(AddressSynchronizer): return tx return candidate + def get_change_addresses_for_new_transaction(self, preferred_change_addr=None) -> List[str]: + change_addrs = [] + if preferred_change_addr: + if isinstance(preferred_change_addr, (list, tuple)): + change_addrs = list(preferred_change_addr) + else: + change_addrs = [preferred_change_addr] + elif self.use_change: + # Recalc and get unused change addresses + addrs = self.calc_unused_change_addresses() + # New change addresses are created only after a few + # confirmations. + if addrs: + # if there are any unused, select all + change_addrs = addrs + else: + # 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: + assert is_address(addr), f"not valid bitcoin address: {addr}" + # note that change addresses are not necessarily ismine + # in which case this is a no-op + self.check_address(addr) + max_change = self.max_change_outputs if self.multiple_change else 1 + return change_addrs[:max_change] + def make_unsigned_transaction(self, coins, outputs, config, fixed_fee=None, change_addr=None, is_sweep=False): # check outputs @@ -694,26 +721,8 @@ class Abstract_Wallet(AddressSynchronizer): self.add_input_info(item) # change address - # if we leave it empty, coin_chooser will set it - change_addrs = [] - if change_addr: - change_addrs = [change_addr] - elif self.use_change: - # Recalc and get unused change addresses - addrs = self.calc_unused_change_addresses() - # New change addresses are created only after a few - # confirmations. - if addrs: - # if there are any unused, select all - change_addrs = addrs - else: - # 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.check_address(addr) + # if empty, coin_chooser will set it + change_addrs = self.get_change_addresses_for_new_transaction(change_addr) # Fee estimator if fixed_fee is None: @@ -727,7 +736,6 @@ class Abstract_Wallet(AddressSynchronizer): if i_max is None: # 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) # If there is an unconfirmed RBF tx, merge with it base_tx = self.get_unconfirmed_base_tx_for_batching() @@ -751,7 +759,7 @@ class Abstract_Wallet(AddressSynchronizer): else: txi = [] txo = [] - tx = coin_chooser.make_tx(coins, txi, outputs[:] + txo, change_addrs[:max_change], + tx = coin_chooser.make_tx(coins, txi, outputs[:] + txo, change_addrs, fee_estimator, self.dust_threshold()) else: # FIXME?? this might spend inputs with negative effective value... From d0a43662bd91c3f0c04301ca8574e4840c7122ad Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 18:37:22 +0200 Subject: [PATCH 038/115] wallet: make "increase fee" RBF logic smarter There are now two internal strategies to bump the fee of a txn. bump fee method 1: keep all inputs, keep all not is_mine outputs, allow adding new inputs bump fee method 2: keep all inputs, no new inputs are added, allow decreasing and removing outputs (change is decreased first) Method 2 is less "safe" as it might end up decreasing e.g. a payment to a merchant; but e.g. if the user has sent "Max" previously, this is the only way to RBF. We try method 1 first, and fail-over to method 2. Previous versions always used method 2. fixes #3652 --- .../gui/kivy/uix/dialogs/bump_fee_dialog.py | 21 ++--- electrum/gui/kivy/uix/dialogs/tx_dialog.py | 15 ++-- electrum/gui/qt/main_window.py | 34 +++---- electrum/tests/test_wallet_vertical.py | 7 +- electrum/wallet.py | 89 +++++++++++++++++-- 5 files changed, 122 insertions(+), 44 deletions(-) diff --git a/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py b/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py index 854be26bd..21f3ca2b7 100644 --- a/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py @@ -24,8 +24,8 @@ Builder.load_string(''' text: _('Current Fee') value: '' BoxLabel: - id: new_fee - text: _('New Fee') + id: old_feerate + text: _('Current Fee rate') value: '' Label: id: tooltip1 @@ -78,15 +78,14 @@ class BumpFeeDialog(Factory.Popup): self.mempool = self.config.use_mempool_fees() self.dynfees = self.config.is_dynfee() and bool(self.app.network) and self.config.has_dynamic_fees_ready() self.ids.old_fee.value = self.app.format_amount_and_units(self.init_fee) + self.ids.old_feerate.value = self.app.format_fee_rate(fee / self.tx_size * 1000) self.update_slider() self.update_text() def update_text(self): - fee = self.get_fee() - self.ids.new_fee.value = self.app.format_amount_and_units(fee) pos = int(self.ids.slider.value) - fee_rate = self.get_fee_rate() - text, tooltip = self.config.get_fee_text(pos, self.dynfees, self.mempool, fee_rate) + new_fee_rate = self.get_fee_rate() + text, tooltip = self.config.get_fee_text(pos, self.dynfees, self.mempool, new_fee_rate) self.ids.tooltip1.text = text self.ids.tooltip2.text = tooltip @@ -103,16 +102,12 @@ class BumpFeeDialog(Factory.Popup): fee_rate = self.config.depth_to_fee(pos) if self.mempool else self.config.eta_to_fee(pos) else: fee_rate = self.config.static_fee(pos) - return fee_rate - - def get_fee(self): - fee_rate = self.get_fee_rate() - return int(fee_rate * self.tx_size // 1000) + return fee_rate # sat/kbyte def on_ok(self): - new_fee = self.get_fee() + new_fee_rate = self.get_fee_rate() / 1000 is_final = self.ids.final_cb.active - self.callback(self.init_fee, new_fee, is_final) + self.callback(new_fee_rate, is_final) def on_slider(self, value): self.update_text() diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py index f62f926dd..d65571f8e 100644 --- a/electrum/gui/kivy/uix/dialogs/tx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/tx_dialog.py @@ -15,6 +15,7 @@ from electrum.gui.kivy.i18n import _ from electrum.util import InvalidPassword from electrum.address_synchronizer import TX_HEIGHT_LOCAL +from electrum.wallet import CannotBumpFee Builder.load_string(''' @@ -212,16 +213,14 @@ class TxDialog(Factory.Popup): d = BumpFeeDialog(self.app, fee, size, self._do_rbf) d.open() - def _do_rbf(self, old_fee, new_fee, is_final): - if new_fee is None: - return - delta = new_fee - old_fee - if delta < 0: - self.app.show_error("fee too low") + def _do_rbf(self, new_fee_rate, is_final): + if new_fee_rate is None: return try: - new_tx = self.wallet.bump_fee(self.tx, delta) - except BaseException as e: + new_tx = self.wallet.bump_fee(tx=self.tx, + new_fee_rate=new_fee_rate, + config=self.app.electrum_config) + except CannotBumpFee as e: self.app.show_error(str(e)) return if is_final: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 7ebe5e907..797355ed5 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -3425,19 +3425,27 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): return tx_label = self.wallet.get_label(tx.txid()) tx_size = tx.estimated_size() + old_fee_rate = fee / tx_size # sat/vbyte d = WindowModalDialog(self, _('Bump Fee')) vbox = QVBoxLayout(d) vbox.addWidget(WWLabel(_("Increase your transaction's fee to improve its position in mempool."))) - vbox.addWidget(QLabel(_('Current fee') + ': %s'% self.format_amount(fee) + ' ' + self.base_unit())) - vbox.addWidget(QLabel(_('New fee' + ':'))) - fee_e = BTCAmountEdit(self.get_decimal_point) - fee_e.setAmount(fee * 1.5) - vbox.addWidget(fee_e) + vbox.addWidget(QLabel(_('Current Fee') + ': %s'% self.format_amount(fee) + ' ' + self.base_unit())) + vbox.addWidget(QLabel(_('Current Fee rate') + ': %s' % self.format_fee_rate(1000 * old_fee_rate))) + vbox.addWidget(QLabel(_('New Fee rate') + ':')) - def on_rate(dyn, pos, fee_rate): - fee = fee_rate * tx_size / 1000 - fee_e.setAmount(fee) - fee_slider = FeeSlider(self, self.config, on_rate) + def on_textedit_rate(): + fee_slider.deactivate() + feerate_e = FeerateEdit(lambda: 0) + feerate_e.setAmount(max(old_fee_rate * 1.5, old_fee_rate + 1)) + feerate_e.textEdited.connect(on_textedit_rate) + vbox.addWidget(feerate_e) + + def on_slider_rate(dyn, pos, fee_rate): + fee_slider.activate() + if fee_rate is not None: + feerate_e.setAmount(fee_rate / 1000) + fee_slider = FeeSlider(self, self.config, on_slider_rate) + fee_slider.deactivate() vbox.addWidget(fee_slider) cb = QCheckBox(_('Final')) vbox.addWidget(cb) @@ -3445,13 +3453,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): if not d.exec_(): return is_final = cb.isChecked() - new_fee = fee_e.get_amount() - delta = new_fee - fee - if delta < 0: - self.show_error("fee too low") - return + new_fee_rate = feerate_e.get_amount() try: - new_tx = self.wallet.bump_fee(tx, delta) + new_tx = self.wallet.bump_fee(tx=tx, new_fee_rate=new_fee_rate, config=self.config) except CannotBumpFee as e: self.show_error(str(e)) return diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index a2b1de971..1fb597c42 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -1,3 +1,4 @@ +import unittest from unittest import mock import shutil import tempfile @@ -857,6 +858,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 1000000 - 5000 + 300000, 0), wallet1a.get_balance()) self.assertEqual((0, 1000000 - 5000 - 300000, 0), wallet2.get_balance()) + @unittest.skip("broken as wallet.bump_fee interface changed") @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') def test_bump_fee_p2pkh(self, mock_write): @@ -895,7 +897,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000) + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000) # FIXME tx.locktime = 1325501 tx.version = 1 self.assertFalse(tx.is_complete()) @@ -946,6 +948,7 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) + @unittest.skip("broken as wallet.bump_fee interface changed") @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') def test_bump_fee_p2wpkh(self, mock_write): @@ -984,7 +987,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000) + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000) # FIXME tx.locktime = 1325500 tx.version = 1 self.assertFalse(tx.is_complete()) diff --git a/electrum/wallet.py b/electrum/wallet.py index 66380d87e..03b3c3f09 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -45,7 +45,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler, format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, - Fiat, bfh, bh2u, TxMinedInfo) + Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate) from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script, is_minikey, relayfee, dust_threshold) from .crypto import sha256d @@ -393,6 +393,7 @@ class Abstract_Wallet(AddressSynchronizer): else: status = _('Local') can_broadcast = self.network is not None + can_bump = is_mine and not tx.is_final() else: status = _("Signed") can_broadcast = self.network is not None @@ -869,15 +870,88 @@ class Abstract_Wallet(AddressSynchronizer): age = tx_age return age > age_limit - def bump_fee(self, tx, delta): + def bump_fee(self, *, tx, new_fee_rate, config) -> Transaction: + """Increase the miner fee of 'tx'. + 'new_fee_rate' is the target min rate in sat/vbyte + """ if tx.is_final(): raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final')) + old_tx_size = tx.estimated_size() + old_fee = self.get_tx_fee(tx) + if old_fee is None: + raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('current fee unknown')) + old_fee_rate = old_fee / old_tx_size # sat/vbyte + if new_fee_rate <= old_fee_rate: + raise CannotBumpFee(_('Cannot bump fee') + ': ' + _("The new fee rate needs to be higher than the old fee rate.")) + + try: + # method 1: keep all inputs, keep all not is_mine outputs, + # allow adding new inputs + tx_new = self._bump_fee_through_coinchooser( + tx=tx, new_fee_rate=new_fee_rate, config=config) + method_used = 1 + except CannotBumpFee: + # method 2: keep all inputs, no new inputs are added, + # allow decreasing and removing outputs (change is decreased first) + # This is less "safe" as it might end up decreasing e.g. a payment to a merchant; + # but e.g. if the user has sent "Max" previously, this is the only way to RBF. + tx_new = self._bump_fee_through_decreasing_outputs( + tx=tx, new_fee_rate=new_fee_rate) + method_used = 2 + + actual_new_fee_rate = tx_new.get_fee() / tx_new.estimated_size() + if actual_new_fee_rate < quantize_feerate(new_fee_rate): + raise Exception(f"bump_fee feerate target was not met (method: {method_used}). " + f"got {actual_new_fee_rate}, expected >={new_fee_rate}") + + tx_new.locktime = get_locktime_for_new_transaction(self.network) + return tx_new + + def _bump_fee_through_coinchooser(self, *, tx, new_fee_rate, config): + tx = Transaction(tx.serialize()) + tx.deserialize(force_full_parse=True) # need to parse inputs + tx.remove_signatures() + tx.add_inputs_info(self) + old_inputs = tx.inputs()[:] + old_outputs = tx.outputs()[:] + # change address + old_change_addrs = [o.address for o in old_outputs if self.is_change(o.address)] + change_addrs = self.get_change_addresses_for_new_transaction(old_change_addrs) + # which outputs to keep? + if old_change_addrs: + fixed_outputs = list(filter(lambda o: not self.is_change(o.address), old_outputs)) + else: + if all(self.is_mine(o.address) for o in old_outputs): + # all outputs are is_mine and none of them are change. + # we bail out as it's unclear what the user would want! + # the coinchooser bump fee method is probably not a good idea in this case + raise CannotBumpFee(_('Cannot bump fee') + ': all outputs are non-change is_mine') + old_not_is_mine = list(filter(lambda o: not self.is_mine(o.address), old_outputs)) + if old_not_is_mine: + fixed_outputs = old_not_is_mine + else: + fixed_outputs = old_outputs + + coins = self.get_spendable_coins(None, config) + for item in coins: + self.add_input_info(item) + def fee_estimator(size): + return config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size) + coin_chooser = coinchooser.get_coin_chooser(config) + try: + return coin_chooser.make_tx(coins, old_inputs, fixed_outputs, change_addrs, + fee_estimator, self.dust_threshold()) + except NotEnoughFunds as e: + raise CannotBumpFee(e) + + def _bump_fee_through_decreasing_outputs(self, *, tx, new_fee_rate): tx = Transaction(tx.serialize()) tx.deserialize(force_full_parse=True) # need to parse inputs tx.remove_signatures() tx.add_inputs_info(self) inputs = tx.inputs() outputs = tx.outputs() + # use own outputs s = list(filter(lambda o: self.is_mine(o.address), outputs)) # ... unless there is none @@ -887,13 +961,17 @@ class Abstract_Wallet(AddressSynchronizer): if x_fee: x_fee_address, x_fee_amount = x_fee s = filter(lambda o: o.address != x_fee_address, s) + if not s: + raise CannotBumpFee(_('Cannot bump fee') + ': no outputs at all??') # prioritize low value outputs, to get rid of dust s = sorted(s, key=lambda o: o.value) for o in s: + target_fee = tx.estimated_size() * new_fee_rate + delta = target_fee - tx.get_fee() i = outputs.index(o) if o.value - delta >= self.dust_threshold(): - outputs[i] = o._replace(value=o.value-delta) + outputs[i] = o._replace(value=o.value - delta) delta = 0 break else: @@ -903,9 +981,8 @@ class Abstract_Wallet(AddressSynchronizer): continue if delta > 0: raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs')) - locktime = get_locktime_for_new_transaction(self.network) - tx_new = Transaction.from_io(inputs, outputs, locktime=locktime) - return tx_new + + return Transaction.from_io(inputs, outputs) def cpfp(self, tx, fee): txid = tx.txid() From 8491a2d329dcf8cae9cc08cec4ff21a22bc7e944 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 18:43:00 +0200 Subject: [PATCH 039/115] wallet: RBF batching will now reuse the change address --- electrum/wallet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 03b3c3f09..6f0cfa4fc 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -721,10 +721,6 @@ class Abstract_Wallet(AddressSynchronizer): for item in coins: self.add_input_info(item) - # change address - # if empty, coin_chooser will set it - change_addrs = self.get_change_addresses_for_new_transaction(change_addr) - # Fee estimator if fixed_fee is None: fee_estimator = config.estimate_fee @@ -757,9 +753,13 @@ class Abstract_Wallet(AddressSynchronizer): 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())) + old_change_addrs = [o.address for o in base_tx.outputs() if self.is_change(o.address)] else: txi = [] txo = [] + old_change_addrs = [] + # change address. if empty, coin_chooser will set it + change_addrs = self.get_change_addresses_for_new_transaction(change_addr or old_change_addrs) tx = coin_chooser.make_tx(coins, txi, outputs[:] + txo, change_addrs, fee_estimator, self.dust_threshold()) else: From 0c20fcb6b31947497a2c64cc2965800538c716b9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 19:52:45 +0200 Subject: [PATCH 040/115] tests: fix existing bump_fee tests --- electrum/tests/test_wallet_vertical.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 1fb597c42..1f27dd742 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -858,7 +858,6 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 1000000 - 5000 + 300000, 0), wallet1a.get_balance()) self.assertEqual((0, 1000000 - 5000 - 300000, 0), wallet2.get_balance()) - @unittest.skip("broken as wallet.bump_fee interface changed") @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') def test_bump_fee_p2pkh(self, mock_write): @@ -897,7 +896,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000) # FIXME + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70, config=self.config) tx.locktime = 1325501 tx.version = 1 self.assertFalse(tx.is_complete()) @@ -906,13 +905,13 @@ class TestWalletSending(TestCaseForTestnet): self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) tx_copy = Transaction(tx.serialize()) - self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006a473044022055b7e6b7e89a55740f7aa2ad1ffcd4b5c913f0de63cf512438921534bc9c3a8d022043b3b27bdc2da4cc6265e4cc9673a3780ccd5cd6f0ee2eaedb51720c15b7a00a012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987d0497200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400', + self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006b483045022100a30c21d1ba8cf751b1b78b5a41684cbab6e39687fa188a4295881c7b06f10a6202204ba4f56cbfdeb8ed948d8a18e34112c256c48e921db048f134819b2ca7ed85fd012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987a0337200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400', str(tx_copy)) - self.assertEqual('f26edcf20991dccedf16058adbee923db7057c9b102db660156b8142b6a59bc7', tx_copy.txid()) - self.assertEqual('f26edcf20991dccedf16058adbee923db7057c9b102db660156b8142b6a59bc7', tx_copy.wtxid()) + self.assertEqual('40768e1e418f8e851d496448c9627ee29f04c33f67a59ac49d2bbc66288d2077', tx_copy.txid()) + self.assertEqual('40768e1e418f8e851d496448c9627ee29f04c33f67a59ac49d2bbc66288d2077', tx_copy.wtxid()) wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual((0, funding_output_value - 2500000 - 10000, 0), wallet.get_balance()) + self.assertEqual((0, 7484320, 0), wallet.get_balance()) @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') @@ -948,7 +947,6 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) - @unittest.skip("broken as wallet.bump_fee interface changed") @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') def test_bump_fee_p2wpkh(self, mock_write): @@ -987,7 +985,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), delta=5000) # FIXME + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70, config=self.config) tx.locktime = 1325500 tx.version = 1 self.assertFalse(tx.is_complete()) @@ -996,13 +994,13 @@ class TestWalletSending(TestCaseForTestnet): self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) tx_copy = Transaction(tx.serialize()) - self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987d049720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5024730440220517fed3a902b5b41fa718ffd5f229b835b8ed26f23433c4ea437d24eff66d15b0220526854a6ebcd351ab2373d0e7c4e20f17c420520b5d570c2df7ca1d773d6a55d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400', + self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f9870c4a720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402202a7e412d37f7a54f7ede0f85e58c7f9dc0f7244d222a4f50a90f87b05badeed40220788d4a4a13f660de7d5464dce5e79419361fdd5d1853c7da65469cd32f7981a90121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400', str(tx_copy)) - self.assertEqual('9a1c0ef7e871798b86074c7f8dd1e81b6d9a758ff07e0059eee31dc6fbf4f438', tx_copy.txid()) - self.assertEqual('59144d30c911ac33359b0a32d5a3fdd2ca806982c85838e193eb95f5d315e813', tx_copy.wtxid()) + self.assertEqual('dad75ab7078b9ce9698a83e7a954c1c38b235d3a4ab79bcb340245e3d9b62b93', tx_copy.txid()) + self.assertEqual('05a484c64a094724b1c58a15463c8c772a98f084cc23ee636204ad9c4d9e5b51', tx_copy.wtxid()) wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) - self.assertEqual((0, funding_output_value - 2500000 - 10000, 0), wallet.get_balance()) + self.assertEqual((0, 7490060, 0), wallet.get_balance()) @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') From e0b1bbfc4603556e646c41dec39d31a61a656c1a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 21:46:20 +0200 Subject: [PATCH 041/115] tests: new tests for bump_fee and rbf_batching --- electrum/tests/test_wallet_vertical.py | 219 ++++++++++++++++++++++++- 1 file changed, 217 insertions(+), 2 deletions(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 1f27dd742..6a80e1a60 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -860,7 +860,7 @@ class TestWalletSending(TestCaseForTestnet): @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') - def test_bump_fee_p2pkh(self, mock_write): + def test_bump_fee_p2pkh_when_there_is_a_change_address(self, mock_write): wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean') # bootstrap wallet @@ -949,7 +949,7 @@ class TestWalletSending(TestCaseForTestnet): @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') - def test_bump_fee_p2wpkh(self, mock_write): + def test_bump_fee_p2wpkh_when_there_is_a_change_address(self, mock_write): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') # bootstrap wallet @@ -1002,6 +1002,221 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 7490060, 0), wallet.get_balance()) + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bump_fee_when_user_sends_max(self, mock_write): + wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') + + # bootstrap wallet + funding_tx = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400') + funding_txid = funding_tx.txid() + self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid) + wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create tx + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')] + coins = wallet.get_spendable_coins(domain=None, config=self.config) + tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000) + tx.set_rbf(True) + tx.locktime = 1325499 + tx.version = 1 + wallet.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220520ab41536d5d0fac8ad44e6aa4a8258a266121bab1eb6599f1ee86bbc65719d02205944c2fb765fca4753a850beadac49f5305c6722410c347c08cec4d90e3eb4430121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400', + str(tx_copy)) + self.assertEqual('dc4b622f3225f00edb886011fa02b74630cdbc24cebdd3210d5ea3b68bef5cc9', tx_copy.txid()) + self.assertEqual('a00340ee8c90673e05f2cf368601b6bba6a7f0513bd974feb218a326e39b1874', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, 0, 0), wallet.get_balance()) + + # bump tx + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70, config=self.config) + tx.locktime = 1325500 + tx.version = 1 + self.assertFalse(tx.is_complete()) + + wallet.sign_transaction(tx, password=None) + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + tx_copy = Transaction(tx.serialize()) + self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01267898000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98702473044022069412007c3a6509fdfcfbe90679395c202c973740b0530b8ff366bc86ebff99d02206a02e3c0beb0921fa7d30379db4999d685d4b97239a2b8c7dd839531c72863110121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400', + str(tx_copy)) + self.assertEqual('53824cc67e8fe973b0dfa1b8cc10f4e2441b9b4b2b1eb92576fbba7000c2908a', tx_copy.txid()) + self.assertEqual('bb137a5a810bb44d3b1cc77fb4f840e7c8c0f84771f7ce4671c3b1a9f5f93724', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, 0, 0), wallet.get_balance()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_bump_fee_when_new_inputs_need_to_be_added(self, mock_write): + wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') + + # bootstrap wallet (incoming funding_tx1) + funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400') + funding_txid1 = funding_tx1.txid() + #funding_output_value = 10_000_000 + self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid1) + wallet.receive_tx_callback(funding_txid1, funding_tx1, TX_HEIGHT_UNCONFIRMED) + + # create tx + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')] + coins = wallet.get_spendable_coins(domain=None, config=self.config) + tx = wallet.make_unsigned_transaction(coins, outputs, config=self.config, fixed_fee=5000) + tx.set_rbf(True) + tx.locktime = 1325499 + tx.version = 1 + wallet.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220520ab41536d5d0fac8ad44e6aa4a8258a266121bab1eb6599f1ee86bbc65719d02205944c2fb765fca4753a850beadac49f5305c6722410c347c08cec4d90e3eb4430121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400', + str(tx_copy)) + self.assertEqual('dc4b622f3225f00edb886011fa02b74630cdbc24cebdd3210d5ea3b68bef5cc9', tx_copy.txid()) + self.assertEqual('a00340ee8c90673e05f2cf368601b6bba6a7f0513bd974feb218a326e39b1874', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, 0, 0), wallet.get_balance()) + + # another incoming transaction (funding_tx2) + funding_tx2 = Transaction('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400') + funding_txid2 = funding_tx2.txid() + #funding_output_value = 5_000_000 + self.assertEqual('c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a', funding_txid2) + wallet.receive_tx_callback(funding_txid2, funding_tx2, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, 5_000_000, 0), wallet.get_balance()) + + # bump tx + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70, config=self.config) + tx.locktime = 1325500 + tx.version = 1 + self.assertFalse(tx.is_complete()) + + wallet.sign_transaction(tx, password=None) + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + tx_copy = Transaction(tx.serialize()) + self.assertEqual('01000000000102c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000feffffff025c254c0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220075992f2696076ca14265372c797fa5c6116ef9b8023f36fa7500442fe3e21430220252677cce7b009d8a65681e8f50b78c9a31c6461f67c995b8804041a290893660121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c502473044022018379b52ea52436eaeef1593e08aba78db1fd624b804ab747722f748203d553702204cbe4c87a010c8b67be9034014b503354e72f9c8205172269c00de20883fac61012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbc391400', + str(tx_copy)) + self.assertEqual('056aaf5ec628a492742b083ad7790836e2d12e89061f32d5b517679764fdaff1', tx_copy.txid()) + self.assertEqual('0c26d17386408d0111ebc94a5d05f6afd681add632dfbcd986658f9d9fe25ff7', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, 4_990_300, 0), wallet.get_balance()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_rbf_batching(self, mock_write): + wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') + config = SimpleConfig({'electrum_path': self.electrum_path, 'batch_rbf': True}) + + # bootstrap wallet (incoming funding_tx1) + funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400') + funding_txid1 = funding_tx1.txid() + #funding_output_value = 10_000_000 + self.assertEqual('52e669a20a26c8b3df5b41e5e6309b18bcde8e1ad7ea17a18f63b6dc6c8becc0', funding_txid1) + wallet.receive_tx_callback(funding_txid1, funding_tx1, TX_HEIGHT_UNCONFIRMED) + + # create tx + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2_500_000)] + coins = wallet.get_spendable_coins(domain=None, config=config) + tx = wallet.make_unsigned_transaction(coins, outputs, config=config, fixed_fee=5000) + tx.set_rbf(True) + tx.locktime = 1325499 + tx.version = 1 + wallet.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402205442705e988abe74bf391b293bb1b886674284a92ed0788c33024f9336d60aef022013a93049d3bed693254cd31a704d70bb988a36750f0b74d0a5b4d9e29c54ca9d0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400', + str(tx_copy)) + self.assertEqual('b019bbad45a46ed25365e46e4cae6428fb12ae425977eb93011ffb294cb4977e', tx_copy.txid()) + self.assertEqual('ba87313e2b3b42f1cc478843d4d53c72d6e06f6c66ac8cfbe2a59cdac2fd532d', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, 7_495_000, 0), wallet.get_balance()) + + # another incoming transaction (funding_tx2) + funding_tx2 = Transaction('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520000000017160014ba9ca815474a674ff1efb3fc82cf0f3460de8c57fdffffff0230390f000000000017a9148b59abaca8215c0d4b18cbbf715550aa2b50c85b87404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab9002473044022038a05f7d38bcf810dfebb39f1feda5cc187da4cf5d6e56986957ddcccedc75d302203ab67ccf15431b4e2aeeab1582b9a5a7821e7ac4be8ebf512505dbfdc7e094fd0121032168234e0ba465b8cedc10173ea9391725c0f6d9fa517641af87926626a5144abd391400') + funding_txid2 = funding_tx2.txid() + #funding_output_value = 5_000_000 + self.assertEqual('c36a6e1cd54df108e69574f70bc9b88dc13beddc70cfad9feb7f8f6593255d4a', funding_txid2) + wallet.receive_tx_callback(funding_txid2, funding_tx2, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, 12_495_000, 0), wallet.get_balance()) + + # create new tx (output should be batched with existing!) + # no new input will be needed. just a new output, and change decreased. + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)] + coins = wallet.get_spendable_coins(domain=None, config=config) + tx = wallet.make_unsigned_transaction(coins, outputs, config=config, fixed_fee=20000) + tx.set_rbf(True) + tx.locktime = 1325499 + tx.version = 1 + wallet.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff03a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98720fd4b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402206add1d6fc8b5fc6fd1bbf50d06fe432e65b16a9d715dbfe7f2d26473f48a128302207983d8db3508e3b953e6e26581d2bbba5a7ca0ff0dd07361de60977dc61ed1580121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bb391400', + str(tx_copy)) + self.assertEqual('21112d35fa08b9577bfe46405ad17720d0fa85bcefab0b0a1cffe79b9d6167c4', tx_copy.txid()) + self.assertEqual('d49ffdaa832a35d88f3f43bcfb08306347c2342200098f450e41ccb289b26db3', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, 9_980_000, 0), wallet.get_balance()) + + # create new tx (output should be batched with existing!) + # new input will be needed! + outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)] + coins = wallet.get_spendable_coins(domain=None, config=config) + tx = wallet.make_unsigned_transaction(coins, outputs, config=config, fixed_fee=100_000) + tx.set_rbf(True) + tx.locktime = 1325499 + tx.version = 1 + wallet.sign_transaction(tx, password=None) + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(2, len(tx.inputs())) + tx_copy = Transaction(tx.serialize()) + self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual(tx.txid(), tx_copy.txid()) + self.assertEqual(tx.wtxid(), tx_copy.wtxid()) + self.assertEqual('01000000000102c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff04a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98760823b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5808d5b000000000017a914d332f2f63019da6f2d23ee77bbe30eed7739790587024730440220730ac17af4ac14f008ee5d0a7be524d8ca344afc19b548faa9ac8c21a216df81022010d9cc878402103c1dd6b06e97e7910a23b7ec88251627f47ed1d5a8d741beba0121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50247304402201005fc1e9091ac36d98b60c1c8b65aada0d4fe4da438d69b3262028644005cfc02207353c987be9e33d1e8702689960df76ac28adacc2f9093d731bc56c9578c5458012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbb391400', + str(tx_copy)) + self.assertEqual('88791bcd352b50592a5521c15595972b14b5d6be165be2df0e57ea19e588c025', tx_copy.txid()) + self.assertEqual('7c5e5bff601e5467036b574b41090681a86de403867dd2b14097920b95e392ed', tx_copy.wtxid()) + + wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + self.assertEqual((0, 3_900_000, 0), wallet.get_balance()) + @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') def test_cpfp_p2wpkh(self, mock_write): From cb69aa80f7f9751ee5ed4699f93e4b00f8c59b10 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 22:40:57 +0200 Subject: [PATCH 042/115] coinchooser: don't spend buckets with negative effective value Calculate the effective value of buckets, and filter <0 out. Note that the filtering is done on the buckets, not per-coin. This should better preserve the user's privacy in certain cases. When the user "sends Max", as before, all UTXOs are selected, even if they are not economical to spend. see #5433 --- electrum/coinchooser.py | 38 ++++++++++++++++++++++++++++++++------ electrum/simple_config.py | 6 ++++-- electrum/wallet.py | 15 +++++++++++---- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index 9c6a7b88f..87a8ea655 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -25,6 +25,7 @@ from collections import defaultdict from math import floor, log10 from typing import NamedTuple, List, Callable +from decimal import Decimal from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address from .transaction import Transaction, TxOutput @@ -74,6 +75,7 @@ class Bucket(NamedTuple): desc: str weight: int # as in BIP-141 value: int # in satoshis + effective_value: int # estimate of value left after subtracting fees. in satoshis coins: List[dict] # UTXOs min_height: int # min block height where a coin was confirmed witness: bool # whether any coin uses segwit @@ -109,11 +111,14 @@ class CoinChooserBase(Logger): def keys(self, coins): raise NotImplementedError - def bucketize_coins(self, coins): + def bucketize_coins(self, coins, *, fee_estimator): keys = self.keys(coins) buckets = defaultdict(list) for key, coin in zip(keys, coins): buckets[key].append(coin) + # fee_estimator returns fee to be paid, for given vbytes. + # guess whether it is just returning a constant as follows. + constant_fee = fee_estimator(2000) == fee_estimator(200) def make_Bucket(desc, coins): witness = any(Transaction.is_segwit_input(coin, guess_for_address=True) for coin in coins) @@ -123,7 +128,23 @@ class CoinChooserBase(Logger): for coin in coins) value = sum(coin['value'] for coin in coins) min_height = min(coin['height'] for coin in coins) - return Bucket(desc, weight, value, coins, min_height, witness) + # the fee estimator is typically either a constant or a linear function, + # so the "function:" effective_value(bucket) will be homomorphic for addition + # i.e. effective_value(b1) + effective_value(b2) = effective_value(b1 + b2) + if constant_fee: + effective_value = value + else: + # when converting from weight to vBytes, instead of rounding up, + # keep fractional part, to avoid overestimating fee + fee = fee_estimator(Decimal(weight) / 4) + effective_value = value - fee + return Bucket(desc=desc, + weight=weight, + value=value, + effective_value=effective_value, + coins=coins, + min_height=min_height, + witness=witness) return list(map(make_Bucket, buckets.keys(), buckets.values())) @@ -287,8 +308,14 @@ class CoinChooserBase(Logger): dust_threshold=dust_threshold, base_weight=base_weight) - # Collect the coins into buckets, choose a subset of the buckets - all_buckets = self.bucketize_coins(coins) + # Collect the coins into buckets + all_buckets = self.bucketize_coins(coins, fee_estimator=fee_estimator) + # Filter some buckets out. Only keep those that have positive effective value. + # Note that this filtering is intentionally done on the bucket level + # instead of per-coin, as each bucket should be either fully spent or not at all. + # (e.g. CoinChooserPrivacy ensures that same-address coins go into one bucket) + all_buckets = list(filter(lambda b: b.effective_value > 0, all_buckets)) + # Choose a subset of the buckets scored_candidate = self.choose_buckets(all_buckets, sufficient_funds, self.penalty_func(base_tx, tx_from_buckets=tx_from_buckets)) tx = scored_candidate.tx @@ -334,8 +361,7 @@ class CoinChooserRandom(CoinChooserBase): candidates.add(tuple(sorted(permutation[:count + 1]))) break else: - # FIXME this assumes that the effective value of any bkt is >= 0 - # we should make sure not to choose buckets with <= 0 eff. val. + # note: this assumes that the effective value of any bkt is >= 0 raise NotEnoughFunds() candidates = [[buckets[n] for n in c] for c in candidates] diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 853b5c7c6..8a970802b 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -533,14 +533,16 @@ class SimpleConfig(Logger): fee_per_kb = self.fee_per_kb() return fee_per_kb / 1000 if fee_per_kb is not None else None - def estimate_fee(self, size): + def estimate_fee(self, size: Union[int, float, Decimal]) -> int: fee_per_kb = self.fee_per_kb() if fee_per_kb is None: raise NoDynamicFeeEstimates() return self.estimate_fee_for_feerate(fee_per_kb, size) @classmethod - def estimate_fee_for_feerate(cls, fee_per_kb, size): + def estimate_fee_for_feerate(cls, fee_per_kb: Union[int, float, Decimal], + size: Union[int, float, Decimal]) -> int: + size = Decimal(size) fee_per_kb = Decimal(fee_per_kb) fee_per_byte = fee_per_kb / 1000 # to be consistent with what is displayed in the GUI, diff --git a/electrum/wallet.py b/electrum/wallet.py index 6f0cfa4fc..de7e0525f 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -745,12 +745,13 @@ class Abstract_Wallet(AddressSynchronizer): base_tx.remove_signatures() base_tx.add_inputs_info(self) base_tx_fee = base_tx.get_fee() - relayfeerate = self.relayfee() / 1000 + relayfeerate = Decimal(self.relayfee()) / 1000 original_fee_estimator = fee_estimator - def fee_estimator(size: int) -> int: + def fee_estimator(size: Union[int, float, Decimal]) -> int: + size = Decimal(size) 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)) + return int(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())) old_change_addrs = [o.address for o in base_tx.outputs() if self.is_change(o.address)] @@ -763,7 +764,13 @@ class Abstract_Wallet(AddressSynchronizer): tx = coin_chooser.make_tx(coins, txi, outputs[:] + txo, change_addrs, fee_estimator, self.dust_threshold()) else: - # FIXME?? this might spend inputs with negative effective value... + # "spend max" branch + # note: This *will* spend inputs with negative effective value (if there are any). + # Given as the user is spending "max", and so might be abandoning the wallet, + # try to include all UTXOs, otherwise leftover might remain in the UTXO set + # forever. see #5433 + # note: Actually it might be the case that not all UTXOs from the wallet are + # being spent if the user manually selected UTXOs. sendable = sum(map(lambda x:x['value'], coins)) outputs[i_max] = outputs[i_max]._replace(value=0) tx = Transaction.from_io(coins, outputs[:]) From fd5d1dab4f4aed84b6cfacc3a2ee845f7bb9a91b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Jun 2019 22:46:22 +0200 Subject: [PATCH 043/115] coinchooser: clear up what "fee_estimator" expects --- electrum/coinchooser.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index 87a8ea655..98036f99f 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -111,14 +111,14 @@ class CoinChooserBase(Logger): def keys(self, coins): raise NotImplementedError - def bucketize_coins(self, coins, *, fee_estimator): + def bucketize_coins(self, coins, *, fee_estimator_vb): keys = self.keys(coins) buckets = defaultdict(list) for key, coin in zip(keys, coins): buckets[key].append(coin) # fee_estimator returns fee to be paid, for given vbytes. # guess whether it is just returning a constant as follows. - constant_fee = fee_estimator(2000) == fee_estimator(200) + constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200) def make_Bucket(desc, coins): witness = any(Transaction.is_segwit_input(coin, guess_for_address=True) for coin in coins) @@ -136,7 +136,7 @@ class CoinChooserBase(Logger): else: # when converting from weight to vBytes, instead of rounding up, # keep fractional part, to avoid overestimating fee - fee = fee_estimator(Decimal(weight) / 4) + fee = fee_estimator_vb(Decimal(weight) / 4) effective_value = value - fee return Bucket(desc=desc, weight=weight, @@ -151,7 +151,7 @@ class CoinChooserBase(Logger): def penalty_func(self, base_tx, *, tx_from_buckets) -> Callable[[List[Bucket]], ScoredCandidate]: raise NotImplementedError - def _change_amounts(self, tx, count, fee_estimator): + def _change_amounts(self, tx, count, fee_estimator_numchange): # Break change up if bigger than max_change output_amounts = [o.value for o in tx.outputs()] # Don't split change of less than 0.02 BTC @@ -160,7 +160,7 @@ class CoinChooserBase(Logger): # Use N change outputs for n in range(1, count + 1): # How much is left if we add this many change outputs? - change_amount = max(0, tx.get_fee() - fee_estimator(n)) + change_amount = max(0, tx.get_fee() - fee_estimator_numchange(n)) if change_amount // n <= max_change: break @@ -205,8 +205,8 @@ class CoinChooserBase(Logger): return amounts - def _change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold): - amounts = self._change_amounts(tx, len(change_addrs), fee_estimator) + def _change_outputs(self, tx, change_addrs, fee_estimator_numchange, dust_threshold): + amounts = self._change_amounts(tx, len(change_addrs), fee_estimator_numchange) assert min(amounts) >= 0 assert len(change_addrs) >= len(amounts) # If change is above dust threshold after accounting for the @@ -233,8 +233,8 @@ class CoinChooserBase(Logger): # This takes a count of change outputs and returns a tx fee output_weight = 4 * Transaction.estimated_output_size(change_addrs[0]) - fee = lambda count: fee_estimator_w(tx_weight + count * output_weight) - change = self._change_outputs(tx, change_addrs, fee, dust_threshold) + fee_estimator_numchange = lambda count: fee_estimator_w(tx_weight + count * output_weight) + change = self._change_outputs(tx, change_addrs, fee_estimator_numchange, dust_threshold) tx.add_outputs(change) return tx, change @@ -259,14 +259,14 @@ class CoinChooserBase(Logger): return total_weight - def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator, + def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator_vb, dust_threshold): """Select unspent coins to spend to pay outputs. If the change is greater than dust_threshold (after adding the change output to the transaction) it is kept, otherwise none is sent and it is added to the transaction fee. - Note: fee_estimator expects virtual bytes + Note: fee_estimator_vb expects virtual bytes """ # Deterministic randomness from coins @@ -286,7 +286,7 @@ class CoinChooserBase(Logger): spent_amount = base_tx.output_value() def fee_estimator_w(weight): - return fee_estimator(Transaction.virtual_size_from_weight(weight)) + return fee_estimator_vb(Transaction.virtual_size_from_weight(weight)) def sufficient_funds(buckets, *, bucket_value_sum): '''Given a list of buckets, return True if it has enough @@ -309,7 +309,7 @@ class CoinChooserBase(Logger): base_weight=base_weight) # Collect the coins into buckets - all_buckets = self.bucketize_coins(coins, fee_estimator=fee_estimator) + all_buckets = self.bucketize_coins(coins, fee_estimator_vb=fee_estimator_vb) # Filter some buckets out. Only keep those that have positive effective value. # Note that this filtering is intentionally done on the bucket level # instead of per-coin, as each bucket should be either fully spent or not at all. From ae714772c38410a0169f2c76a14a64a62c0daff0 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Sun, 23 Jun 2019 02:40:29 +0200 Subject: [PATCH 044/115] AppImage: Make build reproducible We build our own mksquashfs from squashfskit which supports generating reproducible squashfs images. We use a small wrapper script to remove the -mkfs-fixed-time which appimagekit passes but squashfskits mksquashfs does not support. ----- taken from Electron-Cash/Electron-Cash@dd1f106f4f500fbf993094cf73da89a5745a0e2c see AppImage/AppImageKit#929 --- contrib/build-linux/appimage/build.sh | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index a8f5631e2..1147b9218 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -14,6 +14,7 @@ CACHEDIR="$CONTRIB_APPIMAGE/.cache/appimage" PYTHON_VERSION=3.6.8 PKG2APPIMAGE_COMMIT="eb8f3acdd9f11ab19b78f5cb15daa772367daf15" LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7" +SQUASHFSKIT_COMMIT="ae0d656efa2d0df2fcac795b6823b44462f19386" VERSION=`git describe --tags --dirty --always` @@ -55,6 +56,16 @@ tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$BUILDDIR" ) +info "Building squashfskit" +git clone "https://github.com/squashfskit/squashfskit.git" "$BUILDDIR/squashfskit" +( + cd "$BUILDDIR/squashfskit" + git checkout "$SQUASHFSKIT_COMMIT" + make -C squashfs-tools mksquashfs +) +MKSQUASHFS="$BUILDDIR/squashfskit/squashfs-tools/mksquashfs" + + info "building libsecp256k1." ( git clone https://github.com/bitcoin-core/secp256k1 "$CACHEDIR"/secp256k1 \ @@ -203,7 +214,14 @@ info "creating the AppImage." cd "$BUILDDIR" chmod +x "$CACHEDIR/appimagetool" "$CACHEDIR/appimagetool" --appimage-extract - env VERSION="$VERSION" ARCH=x86_64 ./squashfs-root/AppRun --no-appstream --verbose "$APPDIR" "$APPIMAGE" + # We build a small wrapper for mksquashfs that removes the -mkfs-fixed-time option + # that mksquashfs from squashfskit does not support. It is not needed for squashfskit. + cat > ./squashfs-root/usr/lib/appimagekit/mksquashfs << EOF +#!/bin/sh +args=\$(echo "\$@" | sed -e 's/-mkfs-fixed-time 0//') +"$MKSQUASHFS" \$args +EOF + env VERSION="$VERSION" ARCH=x86_64 SOURCE_DATE_EPOCH=1530212462 ./squashfs-root/AppRun --no-appstream --verbose "$APPDIR" "$APPIMAGE" ) From 501fd8f9e5c419a9aa482b149c39fad163ddd4de Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Sun, 23 Jun 2019 02:47:16 +0200 Subject: [PATCH 045/115] AppImage: Improve reproducible Python build reliability on Linux There was a problem where Python would not properly include the faketime timestamp sometimes. This patch replaces faketime with a patch that is used by Ubuntu for reproducible builds by exporting BUILD_DATE and BUILD_TIME with the desired values. ----- taken from Electron-Cash/Electron-Cash@9532508a3f466aab794fae4f8e314617d5a873f9 --- contrib/build-linux/appimage/build.sh | 8 ++++++-- .../python-3.6.8-reproducible-buildinfo.diff | 13 +++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 contrib/build-linux/appimage/patches/python-3.6.8-reproducible-buildinfo.diff diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 1147b9218..1b4917ce4 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -44,14 +44,18 @@ tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$BUILDDIR" ( cd "$BUILDDIR/Python-$PYTHON_VERSION" export SOURCE_DATE_EPOCH=1530212462 - TZ=UTC faketime -f '2019-01-01 01:01:01' ./configure \ + LC_ALL=C export BUILD_DATE=$(date -u -d "@$SOURCE_DATE_EPOCH" "+%b %d %Y") + LC_ALL=C export BUILD_TIME=$(date -u -d "@$SOURCE_DATE_EPOCH" "+%H:%M:%S") + # Patch taken from Ubuntu python3.6_3.6.8-1~18.04.1.debian.tar.xz + patch -p1 < "$CONTRIB_APPIMAGE/patches/python-3.6.8-reproducible-buildinfo.diff" + ./configure \ --cache-file="$CACHEDIR/python.config.cache" \ --prefix="$APPDIR/usr" \ --enable-ipv6 \ --enable-shared \ --with-threads \ -q - TZ=UTC faketime -f '2019-01-01 01:01:01' make -j4 -s + make -j4 -s make -s install > /dev/null ) diff --git a/contrib/build-linux/appimage/patches/python-3.6.8-reproducible-buildinfo.diff b/contrib/build-linux/appimage/patches/python-3.6.8-reproducible-buildinfo.diff new file mode 100644 index 000000000..38d6fbdc4 --- /dev/null +++ b/contrib/build-linux/appimage/patches/python-3.6.8-reproducible-buildinfo.diff @@ -0,0 +1,13 @@ +# DP: Build getbuildinfo.o with DATE/TIME values when defined + +--- a/Makefile.pre.in ++++ b/Makefile.pre.in +@@ -741,6 +741,8 @@ Modules/getbuildinfo.o: $(PARSER_OBJS) \ + -DGITVERSION="\"`LC_ALL=C $(GITVERSION)`\"" \ + -DGITTAG="\"`LC_ALL=C $(GITTAG)`\"" \ + -DGITBRANCH="\"`LC_ALL=C $(GITBRANCH)`\"" \ ++ $(if $(BUILD_DATE),-DDATE='"$(BUILD_DATE)"') \ ++ $(if $(BUILD_TIME),-DTIME='"$(BUILD_TIME)"') \ + -o $@ $(srcdir)/Modules/getbuildinfo.c + + Modules/getpath.o: $(srcdir)/Modules/getpath.c Makefile From b69249f6c333c56aa62761974bcf58a4cf3e8db1 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Sun, 23 Jun 2019 02:56:33 +0200 Subject: [PATCH 046/115] AppImage: Remove unused binaries There are a lot of dupliacted files, testing files and unused libraries present in the AppImage. Removing these reduces the AppImage size significantly. ----- taken from Electron-Cash/Electron-Cash@cff5fb128954853c2c672e2afaa48a40050e7183 --- contrib/build-linux/appimage/build.sh | 37 +++++++++++++++++---------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 1b4917ce4..dabf47d29 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -191,23 +191,34 @@ remove_emptydirs info "removing some unneeded stuff to decrease binary size." -rm -rf "$APPDIR"/usr/lib/python3.6/test -rm -rf "$APPDIR"/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu -rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/translations/qtwebengine_locales -rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/resources/qtwebengine_* -rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/qml -for component in Web Designer Qml Quick Location Test Xml ; do - rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5${component}* - rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt${component}* +rm -rf "$APPDIR"/usr/{share,include} +PYDIR="$APPDIR"/usr/lib/python3.6 +rm -rf "$PYDIR"/{test,ensurepip,lib2to3,idlelib,turtledemo} +rm -rf "$PYDIR"/{ctypes,sqlite3,tkinter,unittest}/test +rm -rf "$PYDIR"/distutils/{command,tests} +rm -rf "$PYDIR"/config-3.6m-x86_64-linux-gnu +rm -rf "$PYDIR"/site-packages/{opt,pip,setuptools,wheel} +rm -rf "$PYDIR"/site-packages/Cython/Tests +rm -rf "$PYDIR"/site-packages/Cython/*/Tests +rm -rf "$PYDIR"/site-packages/Cryptodome/SelfTest +rm -rf "$PYDIR"/site-packages/{psutil,qrcode,websocket}/tests +for component in connectivity declarative help location multimedia quickcontrols2 serialport webengine websockets xmlpatterns ; do + rm -rf "$PYDIR"/site-packages/PyQt5/Qt/translations/qt${component}_* + rm -rf "$PYDIR"/site-packages/PyQt5/Qt/resources/qt${component}_* done -rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt.so - +rm -rf "$PYDIR"/site-packages/PyQt5/Qt/{qml,libexec} +rm -rf "$PYDIR"/site-packages/PyQt5/{pyrcc.so,pylupdate.so,uic} +rm -rf "$PYDIR"/site-packages/PyQt5/Qt/plugins/{bearer,gamepads,geometryloaders,geoservices,playlistformats,position,renderplugins,sceneparsers,sensors,sqldrivers,texttospeech,webview} +for component in Bluetooth Concurrent Designer Help Location NetworkAuth Nfc Positioning PositioningQuick Qml Quick Sensors SerialPort Sql Test Web Xml ; do + rm -rf "$PYDIR"/site-packages/PyQt5/Qt/lib/libQt5${component}* + rm -rf "$PYDIR"/site-packages/PyQt5/Qt${component}* +done +rm -rf "$PYDIR"/site-packages/PyQt5/Qt.so # these are deleted as they were not deterministic; and are not needed anyway find "$APPDIR" -path '*/__pycache__*' -delete -rm "$APPDIR"/usr/lib/libsecp256k1.a -rm "$APPDIR"/usr/lib/python3.6/site-packages/pyblake2-*.dist-info/RECORD -rm "$APPDIR"/usr/lib/python3.6/site-packages/hidapi-*.dist-info/RECORD +rm -rf "$PYDIR"/site-packages/*.dist-info/ +rm -rf "$PYDIR"/site-packages/*.egg-info/ find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + From ec496a8222938a070fd864571b9d97b4af6f42fc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Jun 2019 03:06:36 +0200 Subject: [PATCH 047/115] requirements-hw: rm Cython not actually needed based on Electron-Cash/Electron-Cash@70de1a2b531257b1f2f70174327b863f9c6a4efb --- contrib/build-linux/appimage/build.sh | 2 -- .../deterministic-build/requirements-hw.txt | 29 ------------------- contrib/requirements/requirements-hw.txt | 1 - 3 files changed, 32 deletions(-) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index dabf47d29..05c52e6b2 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -198,8 +198,6 @@ rm -rf "$PYDIR"/{ctypes,sqlite3,tkinter,unittest}/test rm -rf "$PYDIR"/distutils/{command,tests} rm -rf "$PYDIR"/config-3.6m-x86_64-linux-gnu rm -rf "$PYDIR"/site-packages/{opt,pip,setuptools,wheel} -rm -rf "$PYDIR"/site-packages/Cython/Tests -rm -rf "$PYDIR"/site-packages/Cython/*/Tests rm -rf "$PYDIR"/site-packages/Cryptodome/SelfTest rm -rf "$PYDIR"/site-packages/{psutil,qrcode,websocket}/tests for component in connectivity declarative help location multimedia quickcontrols2 serialport webengine websockets xmlpatterns ; do diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 039dc81ad..bc7ba0a26 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -14,35 +14,6 @@ click==7.0 \ --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 construct==2.9.45 \ --hash=sha256:2271a0efd0798679dea825ff47e22a4c550456a5db0ba8baa82f7eae0af0118c -Cython==0.29.10 \ - --hash=sha256:0afa0b121b89de619e71587e25702e2b7068d7da2164c47e6eee80c17823a62f \ - --hash=sha256:1c608ba76f7a20cc9f0c021b7fe5cb04bc1a70327ae93a9298b1bc3e0edddebe \ - --hash=sha256:26229570d6787ff3caa932fe9d802960f51a89239b990d275ae845405ce43857 \ - --hash=sha256:2a9deafa437b6154cac2f25bb88e0bfd075a897c8dc847669d6f478d7e3ee6b1 \ - --hash=sha256:2f28396fbce6d9d68a40edbf49a6729cf9d92a4d39ff0f501947a89188e9099f \ - --hash=sha256:3983dd7b67297db299b403b29b328d9e03e14c4c590ea90aa1ad1d7b35fb178b \ - --hash=sha256:4100a3f8e8bbe47d499cdac00e56d5fe750f739701ea52dc049b6c56f5421d97 \ - --hash=sha256:51abfaa7b6c66f3f18028876713c8804e73d4c2b6ceddbcbcfa8ec62429377f0 \ - --hash=sha256:61c24f4554efdb8fb1ac6c8e75dab301bcdf2b7b739ed0c2b267493bb43163c5 \ - --hash=sha256:700ccf921b2fdc9b23910e95b5caae4b35767685e0812343fa7172409f1b5830 \ - --hash=sha256:7b41eb2e792822a790cb2a171df49d1a9e0baaa8e81f58077b7380a273b93d5f \ - --hash=sha256:803987d3b16d55faa997bfc12e8b97f1091f145930dee229b020487aed8a1f44 \ - --hash=sha256:99af5cfcd208c81998dcf44b3ca466dee7e17453cfb50e98b87947c3a86f8753 \ - --hash=sha256:9faea1cca34501c7e139bc7ef8e504d532b77865c58592493e2c154a003b450f \ - --hash=sha256:a7ba4c9a174db841cfee9a0b92563862a0301d7ca543334666c7266b541f141a \ - --hash=sha256:b26071c2313d1880599c69fd831a07b32a8c961ba69d7ccbe5db1cd8d319a4ca \ - --hash=sha256:b49dc8e1116abde13a3e6a9eb8da6ab292c5a3325155fb872e39011b110b37e6 \ - --hash=sha256:bd40def0fd013569887008baa6da9ca428e3d7247adeeaeada153006227bb2e7 \ - --hash=sha256:bfd0db770e8bd4e044e20298dcae6dfc42561f85d17ee546dcd978c8b23066ae \ - --hash=sha256:c2fad1efae5889925c8fd7867fdd61f59480e4e0b510f9db096c912e884704f1 \ - --hash=sha256:c81aea93d526ccf6bc0b842c91216ee9867cd8792f6725a00f19c8b5837e1715 \ - --hash=sha256:da786e039b4ad2bce3d53d4799438cf1f5e01a0108f1b8d78ac08e6627281b1a \ - --hash=sha256:deab85a069397540987082d251e9c89e0e5b2e3e044014344ff81f60e211fc4b \ - --hash=sha256:e3f1e6224c3407beb1849bdc5ae3150929e593e4cffff6ca41c6ec2b10942c80 \ - --hash=sha256:e74eb224e53aae3943d66e2d29fe42322d5753fd4c0641329bccb7efb3a46552 \ - --hash=sha256:ee697c7ea65cb14915a64f36874da8ffc2123df43cf8bc952172e04a26656cd6 \ - --hash=sha256:f37792b16d11606c28e428460bd6a3d14b8917b109e77cdbe4ca78b0b9a52c87 \ - --hash=sha256:fd2906b54cbf879c09d875ad4e4687c58d87f5ed03496063fec1c9065569fd5d ecdsa==0.13.2 \ --hash=sha256:20c17e527e75acad8f402290e158a6ac178b91b881f941fc6ea305bfdfb9657c \ --hash=sha256:5c034ffa23413ac923541ceb3ac14ec15a0d2530690413bff58c12b80e56d884 diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 49cebd423..f38092f83 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -1,4 +1,3 @@ -Cython>=0.27 trezor[hidapi]>=0.11.0 safet[hidapi]>=0.1.0 keepkey>=6.0.3 From 31ba440d1c3945cb4d7838c07d1993faadffbac6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Jun 2019 03:09:05 +0200 Subject: [PATCH 048/115] build-wine: print some text before "pip install" --- contrib/build-wine/build-electrum-git.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index 1dcf6f464..be3530697 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -50,6 +50,8 @@ $PYTHON -m pip install -r ../../deterministic-build/requirements.txt $PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt pushd $WINEPREFIX/drive_c/electrum +# see https://github.com/pypa/pip/issues/2195 -- pip makes a copy of the entire directory +echo "Pip installing Electrum. This might take a long time if the project folder is large." $PYTHON -m pip install . popd From 212ed8b18bb658683f92762baa034f6f61e5234b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Jun 2019 03:10:09 +0200 Subject: [PATCH 049/115] qt: set WWLabel text to be mouse-selectable by default this lets user to copy-paste text in e.g. many wizard dialogs --- electrum/gui/qt/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index b64223a1d..066e7bd15 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -92,6 +92,7 @@ class WWLabel(QLabel): def __init__ (self, text="", parent=None): QLabel.__init__(self, text, parent) self.setWordWrap(True) + self.setTextInteractionFlags(Qt.TextSelectableByMouse) class HelpLabel(QLabel): From bb59a1298a04713dc4a4e4c8d8a22e48810ae6bd Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Sun, 23 Jun 2019 03:15:33 +0200 Subject: [PATCH 050/115] AppImage: Patch Python sysconfigdata When building in docker on macOS, python builds with .exe extension because the case insensitive file system of macOS leaks into docker. This causes the build to result in a different output on macOS compared to Linux. We simply patch sysconfigdata to remove the extension. Some more info: https://bugs.python.org/issue27631 --- contrib/build-linux/appimage/build.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 05c52e6b2..7f88f05f8 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -57,6 +57,12 @@ tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$BUILDDIR" -q make -j4 -s make -s install > /dev/null + # When building in docker on macOS, python builds with .exe extension because the + # case insensitive file system of macOS leaks into docker. This causes the build + # to result in a different output on macOS compared to Linux. We simply patch + # sysconfigdata to remove the extension. + # Some more info: https://bugs.python.org/issue27631 + sed -i -e 's/\.exe//g' "$APPDIR"/usr/lib/python3.6/_sysconfigdata* ) From 266484e0fdbc011269a19a4599733aa0608ce720 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Jun 2019 03:21:59 +0200 Subject: [PATCH 051/115] Appimage: nits. use "fail" somewhat based on same script in Electron-Cash/Electron-Cash --- contrib/build-linux/appimage/build.sh | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 7f88f05f8..4afc39536 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -55,8 +55,8 @@ tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$BUILDDIR" --enable-shared \ --with-threads \ -q - make -j4 -s - make -s install > /dev/null + make -j4 -s || fail "Could not build Python" + make -s install > /dev/null || fail "Could not install Python" # When building in docker on macOS, python builds with .exe extension because the # case insensitive file system of macOS leaks into docker. This causes the build # to result in a different output on macOS compared to Linux. We simply patch @@ -71,7 +71,7 @@ git clone "https://github.com/squashfskit/squashfskit.git" "$BUILDDIR/squashfski ( cd "$BUILDDIR/squashfskit" git checkout "$SQUASHFSKIT_COMMIT" - make -C squashfs-tools mksquashfs + make -C squashfs-tools mksquashfs || fail "Could not build squashfskit" ) MKSQUASHFS="$BUILDDIR/squashfskit/squashfs-tools/mksquashfs" @@ -93,8 +93,8 @@ info "building libsecp256k1." --enable-module-ecdh \ --disable-jni \ -q - make -j4 -s - make -s install > /dev/null + make -j4 -s || fail "Could not build libsecp" + make -s install > /dev/null || fail "Could not install libsecp" ) @@ -119,8 +119,7 @@ info "preparing electrum-locale." pushd "$CONTRIB"/deterministic-build/electrum-locale if ! which msgfmt > /dev/null 2>&1; then - echo "Please install gettext" - exit 1 + fail "Please install gettext" fi for i in ./locale/*; do dir="$PROJECT_ROOT/electrum/$i/LC_MESSAGES" @@ -170,7 +169,7 @@ info "finalizing AppDir." mv usr/include usr/include.tmp delete_blacklisted mv usr/include.tmp usr/include -) +) || fail "Could not finalize AppDir" # copy libusb here because it is on the AppImage excludelist and it can cause problems if we use system libusb info "Copying libusb" From 9f28f8bcc64d3b921f8bb94d38b584b862520c8c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Jun 2019 04:17:46 +0200 Subject: [PATCH 052/115] Appimage: follow-up b69249f6c333c56aa62761974bcf58a4cf3e8db1 libsecp256k1.a needs to be deleted as it's not reproducible... --- contrib/build-linux/appimage/build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 4afc39536..64ff0938b 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -220,6 +220,7 @@ rm -rf "$PYDIR"/site-packages/PyQt5/Qt.so # these are deleted as they were not deterministic; and are not needed anyway find "$APPDIR" -path '*/__pycache__*' -delete +rm "$APPDIR"/usr/lib/libsecp256k1.a rm -rf "$PYDIR"/site-packages/*.dist-info/ rm -rf "$PYDIR"/site-packages/*.egg-info/ From 570c0aeca39e56c742b77380ec274d178d660c29 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 24 Jun 2019 21:51:47 +0200 Subject: [PATCH 053/115] build: make NSIS windows binary deterministic by changing the .ico file see bitcoin/bitcoin@217208a36d210e7d51e405d0e531ac2b75a3a087 ----- A lot of time was wasted on this... over the years actually... Some notes and rant here, for future reference. During the initial effort to try to make binaries reproducible, out of the three windows binaries being distributed (standalone, portable, setup), only the first two were successfully made deterministic. Later, we started to use Docker-based builds. At that point ThomasV and I could reproducibly build the same setup/nsis exe but Travis kept building a different one. Recently I have noticed that if I do two subsequent builds of the setup exe on the same machine, adding a new file in contrib/build-wine/ between the builds, then I get different binaries. Playing around with this a bit, it seems: - other files that are in the same folder as contrib/build-wine/electrum.nsi affect the binary - only files that are in exactly the same folder matter (not recursively) - only filenames matter (not permission, owner, timestamps, or file contents) To see the difference in the binaries, use vbindiff, and disable the compression done by nsis (SetCompress off). There is a ~48 byte diff near the very beginning of the "Uninstaller" section. I am only guessing it is the uninstaller section based on the sizes of the sections printed by nsis during the build. I have downloaded the binary built by Travis, and the diff is consistent with this (i.e. it's the same kind of diff that manifests if I change the filename of one of the supposedly unrelated files). Commenting out the "WriteUninstaller" line in .nsi fixes the issue. i.e. if no uninstaller is created then the binary becomes deterministic. Commenting out the "!define MUI_ICON" line in .nsi also fixes the issue. At this point I remembered the above referenced commit by bluematt; which I had thought we had already followed up on... Replacing the .ico file fixes the issue. Note that it's not actually clear what the exact requirements for the .ico file are. Removing any of the layers in the image seems to introduce non-determinicity. The new .ico file has layers with resolutions and properties the bitcoin.ico file has. I guess NSIS must have strict requirements for the icon size, and if a given size icon is missing it might be creating it itself?? And during the downscaling it uses a non-deterministic algorithm that initialises some RNG from the directory listing (bauerj's guess somewhat adapted :D). Just crazy. --- electrum/gui/icons/electrum.ico | Bin 51631 -> 63932 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/electrum/gui/icons/electrum.ico b/electrum/gui/icons/electrum.ico index 618bc99e2aaeb553c3ae8da1ef1ab1847766e2a6..3baebbce3c648931ce186c731e54a63690981385 100644 GIT binary patch literal 63932 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 From 62e6ca50e1aa810b501d85b26bdc4f6fa6208f54 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 25 Jun 2019 15:26:24 +0200 Subject: [PATCH 054/115] do not log client-side RPC executions --- run_electrum | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/run_electrum b/run_electrum index ef21b8d2a..1f5ee2f03 100755 --- a/run_electrum +++ b/run_electrum @@ -343,7 +343,6 @@ if __name__ == '__main__': # todo: defer this to gui config = SimpleConfig(config_options) - configure_logging(config) cmdname = config.get('cmd') @@ -355,6 +354,7 @@ if __name__ == '__main__': constants.set_simnet() if cmdname == 'gui': + configure_logging(config) fd, server = daemon.get_fd_or_server(config) if fd is not None: plugins = init_plugins(config, config.get('gui', 'qt')) @@ -370,6 +370,7 @@ if __name__ == '__main__': init_daemon(config_options) if subcommand in [None, 'start']: + configure_logging(config) fd, server = daemon.get_fd_or_server(config) if fd is not None: if subcommand == 'start': From 4fc43da3444ce2ec3e09e333a9056576a1cf58dd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 26 Jun 2019 01:16:34 +0200 Subject: [PATCH 055/115] interface.debug will now also print errors --- electrum/interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/interface.py b/electrum/interface.py index 23cc61f48..1d54be3d2 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -38,7 +38,7 @@ import logging import aiorpcx from aiorpcx import RPCSession, Notification, NetAddress from aiorpcx.curio import timeout_after, TaskTimeout -from aiorpcx.jsonrpc import JSONRPC +from aiorpcx.jsonrpc import JSONRPC, CodeMessageError from aiorpcx.rawsocket import RSClient import certifi @@ -115,6 +115,9 @@ class NotificationSession(RPCSession): timeout) except (TaskTimeout, asyncio.TimeoutError) as e: raise RequestTimedOut(f'request timed out: {args} (id: {msg_id})') from e + except CodeMessageError as e: + self.maybe_log(f"--> {repr(e)} (id: {msg_id})") + raise else: self.maybe_log(f"--> {response} (id: {msg_id})") return response From 7bf6786bf5f4b9427e1750398a9894234e482c9a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 26 Jun 2019 04:18:24 +0200 Subject: [PATCH 056/115] build: note whether binary is reproducible in each case --- contrib/build-linux/README.md | 2 ++ contrib/build-linux/appimage/README.md | 12 ++++++++++-- contrib/build-wine/README.md | 13 +++++-------- contrib/osx/README.md | 3 +++ electrum/gui/kivy/Readme.md | 5 ++++- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/contrib/build-linux/README.md b/contrib/build-linux/README.md index 8d45038c4..22705a435 100644 --- a/contrib/build-linux/README.md +++ b/contrib/build-linux/README.md @@ -1,6 +1,8 @@ Source tarballs =============== +✗ _This script does not produce reproducible output (yet!)._ + 1. Build locale files ``` diff --git a/contrib/build-linux/appimage/README.md b/contrib/build-linux/appimage/README.md index 747c84c55..6f6be280a 100644 --- a/contrib/build-linux/appimage/README.md +++ b/contrib/build-linux/appimage/README.md @@ -1,9 +1,17 @@ AppImage binary for Electrum ============================ +✓ _This binary should be reproducible, meaning you should be able to generate + binaries that match the official releases._ + 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. +similar system. The host architecture should be x86_64 (amd64). +The docker commands should be executed in the project's root folder. + +We currently only build a single AppImage, for x86_64 architecture. +Help to adapt these scripts to build for (some flavor of) ARM would be welcome, +see [issue #5159](https://github.com/spesmilo/electrum/issues/5159). + 1. Install Docker diff --git a/contrib/build-wine/README.md b/contrib/build-wine/README.md index 4c4f3b43c..9c6cb1d7b 100644 --- a/contrib/build-wine/README.md +++ b/contrib/build-wine/README.md @@ -1,10 +1,10 @@ -Deterministic Windows binaries with Docker -========================================== +Windows binaries +================ -Produced binaries are deterministic, so you should be able to generate -binaries that match the official releases. +✓ _These binaries should be reproducible, meaning you should be able to generate + binaries that match the official releases._ -This assumes an Ubuntu host, but it should not be too hard to adapt to another +This assumes an Ubuntu (x86_64) 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. @@ -54,9 +54,6 @@ folder. -Note: the `setup` binary (NSIS installer) is not deterministic yet. - - Code Signing ============ diff --git a/contrib/osx/README.md b/contrib/osx/README.md index ca404ff11..fdba1913f 100644 --- a/contrib/osx/README.md +++ b/contrib/osx/README.md @@ -1,6 +1,9 @@ Building Mac OS binaries ======================== +✗ _This script does not produce reproducible output (yet!). + Please help us remedy this._ + This guide explains how to build Electrum binaries for macOS systems. diff --git a/electrum/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md index 85e54494e..887d7899f 100644 --- a/electrum/gui/kivy/Readme.md +++ b/electrum/gui/kivy/Readme.md @@ -5,7 +5,10 @@ To generate an APK file, follow these instructions. ## Android binary with Docker -This assumes an Ubuntu host, but it should not be too hard to adapt to another +✗ _This script does not produce reproducible output (yet!). + Please help us remedy this._ + +This assumes an Ubuntu (x86_64) 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. From 0fafd8c0a776336d59db1f1effbdbf143e036fc5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 27 Jun 2019 05:00:16 +0200 Subject: [PATCH 057/115] fix #4777 again... --- 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 797355ed5..474ff563e 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2241,6 +2241,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): mpk_text.addCopyButton(self.app) def show_mpk(index): mpk_text.setText(mpk_list[index]) + mpk_text.repaint() # macOS hack for #4777 # only show the combobox in case multiple accounts are available if len(mpk_list) > 1: def label(key): From baa02936207e5fdfbad446bf635d5d032f7cb398 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 27 Jun 2019 07:08:03 +0200 Subject: [PATCH 058/115] android build: persist debug keystore so that we can upgrade debug installations on the phone and keep the datadir --- .gitignore | 1 + contrib/make_apk | 11 +++++++++++ electrum/gui/kivy/Readme.md | 23 ++++++++++++++++++++++- electrum/gui/kivy/tools/Dockerfile | 2 ++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 84d5565c9..f5985cac9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ contrib/build-wine/tmp/ contrib/build-wine/fresh_clone/ contrib/build-linux/appimage/build/ contrib/build-linux/appimage/.cache/ +contrib/android_debug.keystore diff --git a/contrib/make_apk b/contrib/make_apk index f6d691876..115b28ee9 100755 --- a/contrib/make_apk +++ b/contrib/make_apk @@ -30,6 +30,17 @@ if [[ -n "$1" && "$1" == "release" ]] ; then export P4A_RELEASE_KEYALIAS=electrum make release else + export P4A_DEBUG_KEYSTORE="$CONTRIB"/android_debug.keystore + export P4A_DEBUG_KEYSTORE_PASSWD=unsafepassword + export P4A_DEBUG_KEYALIAS_PASSWD=unsafepassword + export P4A_DEBUG_KEYALIAS=electrum + if [ ! -f "$P4A_DEBUG_KEYSTORE" ]; then + keytool -genkey -v -keystore "$CONTRIB"/android_debug.keystore \ + -alias "$P4A_DEBUG_KEYALIAS" -keyalg RSA -keysize 2048 -validity 10000 \ + -dname "CN=mqttserver.ibm.com, OU=ID, O=IBM, L=Hursley, S=Hants, C=GB" \ + -storepass "$P4A_DEBUG_KEYSTORE_PASSWD" \ + -keypass "$P4A_DEBUG_KEYALIAS_PASSWD" + fi make apk fi diff --git a/electrum/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md index 887d7899f..f0b4b52cd 100644 --- a/electrum/gui/kivy/Readme.md +++ b/electrum/gui/kivy/Readme.md @@ -81,13 +81,34 @@ $ sudo docker run -it --rm \ ``` -### How do I get more verbose logs? +### How do I get more verbose logs for the build? See `log_level` in `buildozer.spec` +### How can I see logs at runtime? +This should work OK for most scenarios: +``` +adb logcat | grep python +``` +Better `grep` but fragile because of `cut`: +``` +adb logcat | grep -F "`adb shell ps | grep org.electrum.electrum | cut -c14-19`" +``` + + ### Kivy can be run directly on Linux Desktop. How? Install Kivy. Build atlas: `(cd electrum/gui/kivy/; make theming)` Run electrum with the `-g` switch: `electrum -g kivy` + +### debug vs release build +If you just follow the instructions above, you will build the apk +in debug mode. The most notable difference is that the apk will be +signed using a debug keystore. If you are planning to upload +what you build to e.g. the Play Store, you should create your own +keystore, back it up safely, and run `./contrib/make_apk release`. + +See e.g. [kivy wiki](https://github.com/kivy/kivy/wiki/Creating-a-Release-APK) +and [android dev docs](https://developer.android.com/studio/build/building-cmdline#sign_cmdline). diff --git a/electrum/gui/kivy/tools/Dockerfile b/electrum/gui/kivy/tools/Dockerfile index 5e1f55e83..13ec31d41 100644 --- a/electrum/gui/kivy/tools/Dockerfile +++ b/electrum/gui/kivy/tools/Dockerfile @@ -152,6 +152,8 @@ RUN cd /opt \ && git cherry-pick d7f722e4e5d4b3e6f5b1733c95e6a433f78ee570 \ # fix gradle "versionCode" overflow: && git cherry-pick ed20e196fbcdce718a180f88f23bb2d165c4c5d8 \ + # gradle: persist debug keystore: + && git cherry-pick aaa0d5d0e7a334631df71e0a9bf127817e0ab9ab \ && python3 -m pip install --user -e . # build env vars From a2bffb9137ae1aaa0edf628cf767ca6f53332def Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 27 Jun 2019 19:10:25 +0200 Subject: [PATCH 059/115] network: harden against eclipse attacks --- electrum/network.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index e936a4f3b..02c01bf9c 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -476,20 +476,26 @@ class Network(Logger): @with_recent_servers_lock def get_servers(self): - # start with hardcoded servers - out = dict(constants.net.DEFAULT_SERVERS) # copy + # note: order of sources when adding servers here is crucial! + # don't let "server_peers" overwrite anything, + # otherwise main server can eclipse the client + out = dict() + # add servers received from main interface + server_peers = self.server_peers + if server_peers: + out.update(filter_version(server_peers.copy())) + # hardcoded servers + out.update(constants.net.DEFAULT_SERVERS) # add recent servers for s in self.recent_servers: try: host, port, protocol = deserialize_server(s) except: continue - if host not in out: + if host in out: + out[host].update({protocol: port}) + else: out[host] = {protocol: port} - # add servers received from main interface - server_peers = self.server_peers - if server_peers: - out.update(filter_version(server_peers.copy())) # potentially filter out some if self.config.get('noonion'): out = filter_noonion(out) From c6a54f05f5ba431e069cba0eef14a2df1e596c14 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 28 Jun 2019 20:20:24 +0200 Subject: [PATCH 060/115] wallet: some performance optimisations for get_receiving_addresses jsondb takes a copy of the whole self.receiving_addresses good for avoiding race conditions but horrible for performance... this significantly speeds up at least - synchronize_sequence, and - is_beyond_limit (used by Qt AddressList) --- electrum/gui/qt/address_list.py | 3 ++- electrum/json_db.py | 10 ++++--- electrum/wallet.py | 46 +++++++++++++++++++++------------ 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 0977cf9e7..9d3a3808f 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -31,7 +31,7 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont from PyQt5.QtWidgets import QAbstractItemView, QComboBox, QLabel, QMenu from electrum.i18n import _ -from electrum.util import block_explorer_URL +from electrum.util import block_explorer_URL, profiler from electrum.plugin import run_hook from electrum.bitcoin import is_address from electrum.wallet import InternalAddressCorruption @@ -107,6 +107,7 @@ class AddressList(MyTreeView): self.show_used = state self.update() + @profiler def update(self): self.wallet = self.parent.wallet current_address = self.current_item_user_role(col=self.Columns.LABEL) diff --git a/electrum/json_db.py b/electrum/json_db.py index 6c2be0891..f2e679181 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -691,12 +691,14 @@ class JsonDB(Logger): return len(self.receiving_addresses) @locked - def get_change_addresses(self): - return list(self.change_addresses) + def get_change_addresses(self, *, slice_start=None, slice_stop=None): + # note: slicing makes a shallow copy + return self.change_addresses[slice_start:slice_stop] @locked - def get_receiving_addresses(self): - return list(self.receiving_addresses) + def get_receiving_addresses(self, *, slice_start=None, slice_stop=None): + # note: slicing makes a shallow copy + return self.receiving_addresses[slice_start:slice_stop] @modifier def add_change_address(self, addr): diff --git a/electrum/wallet.py b/electrum/wallet.py index de7e0525f..0e087d678 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, List, Optional, Tuple, Union, NamedTuple +from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence from .i18n import _ from .util import (NotEnoughFunds, UserCancelled, profiler, @@ -434,8 +434,15 @@ class Abstract_Wallet(AddressSynchronizer): utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)] return utxos + def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence: + raise NotImplementedError() # implemented by subclasses + + def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence: + raise NotImplementedError() # implemented by subclasses + def dummy_address(self): - return self.get_receiving_addresses()[0] + # first receiving address + return self.get_receiving_addresses(slice_start=0, slice_stop=1)[0] def get_frozen_balance(self): if not self.frozen_coins: # shortcut @@ -692,7 +699,7 @@ class Abstract_Wallet(AddressSynchronizer): change_addrs = addrs else: # if there are none, take one randomly from the last few - addrs = self.get_change_addresses()[-self.gap_limit_for_change:] + addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change) change_addrs = [random.choice(addrs)] if addrs else [] for addr in change_addrs: assert is_address(addr), f"not valid bitcoin address: {addr}" @@ -1506,10 +1513,10 @@ class Imported_Wallet(Simple_Wallet): # note: overridden so that the history can be cleared return self.db.get_imported_addresses() - def get_receiving_addresses(self): + def get_receiving_addresses(self, **kwargs): return self.get_addresses() - def get_change_addresses(self): + def get_change_addresses(self, **kwargs): return [] def import_addresses(self, addresses: List[str], *, @@ -1661,16 +1668,15 @@ class Deterministic_Wallet(Abstract_Wallet): def get_addresses(self): # note: overridden so that the history can be cleared. # addresses are ordered based on derivation - out = [] - out += self.get_receiving_addresses() + out = self.get_receiving_addresses() out += self.get_change_addresses() return out - def get_receiving_addresses(self): - return self.db.get_receiving_addresses() + def get_receiving_addresses(self, *, slice_start=None, slice_stop=None): + return self.db.get_receiving_addresses(slice_start=slice_start, slice_stop=slice_stop) - def get_change_addresses(self): - return self.db.get_change_addresses() + def get_change_addresses(self, *, slice_start=None, slice_stop=None): + return self.db.get_change_addresses(slice_start=slice_start, slice_stop=slice_stop) @profiler def try_detecting_internal_addresses_corruption(self): @@ -1748,11 +1754,15 @@ class Deterministic_Wallet(Abstract_Wallet): def synchronize_sequence(self, for_change): limit = self.gap_limit_for_change if for_change else self.gap_limit while True: - addresses = self.get_change_addresses() if for_change else self.get_receiving_addresses() - if len(addresses) < limit: + num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() + if num_addr < limit: self.create_new_address(for_change) continue - if any(map(self.address_is_old, addresses[-limit:])): + if for_change: + last_few_addresses = self.get_change_addresses(slice_start=-limit) + else: + last_few_addresses = self.get_receiving_addresses(slice_start=-limit) + if any(map(self.address_is_old, last_few_addresses)): self.create_new_address(for_change) else: break @@ -1764,11 +1774,15 @@ class Deterministic_Wallet(Abstract_Wallet): def is_beyond_limit(self, address): is_change, i = self.get_address_index(address) - addr_list = self.get_change_addresses() if is_change else self.get_receiving_addresses() limit = self.gap_limit_for_change if is_change else self.gap_limit if i < limit: return False - prev_addresses = addr_list[max(0, i - limit):max(0, i)] + slice_start = max(0, i - limit) + slice_stop = max(0, i) + if is_change: + prev_addresses = self.get_change_addresses(slice_start=slice_start, slice_stop=slice_stop) + else: + prev_addresses = self.get_receiving_addresses(slice_start=slice_start, slice_stop=slice_stop) for addr in prev_addresses: if self.db.get_addr_history(addr): return False From 935ab9a12f4cd6cf8e06c7207fafda72bb6ea1c7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 28 Jun 2019 21:13:33 +0200 Subject: [PATCH 061/115] interface: check if future already done in handle_disconnect future could get cancelled in network.py in which case set_result raised --- electrum/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/interface.py b/electrum/interface.py index 1d54be3d2..b5eac439c 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -336,7 +336,8 @@ class Interface(Logger): self.logger.debug(f"(disconnect) trace for {repr(e)}", exc_info=True) finally: await self.network.connection_down(self) - self.got_disconnected.set_result(1) + if not self.got_disconnected.done(): + self.got_disconnected.set_result(1) # if was not 'ready' yet, schedule waiting coroutines: self.ready.cancel() return wrapper_func From f405c3fbddc813837403f7cfae3ccb6c1145f838 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 29 Jun 2019 02:28:00 +0200 Subject: [PATCH 062/115] ledger: (trivial) rm some remnants of hw1 setup --- electrum/plugins/ledger/qt.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/electrum/plugins/ledger/qt.py b/electrum/plugins/ledger/qt.py index b2e5bc005..225c5ef4f 100644 --- a/electrum/plugins/ledger/qt.py +++ b/electrum/plugins/ledger/qt.py @@ -1,7 +1,5 @@ from functools import partial -#from btchip.btchipPersoWizard import StartBTChipPersoDialog - from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QInputDialog, QLabel, QVBoxLayout, QLineEdit @@ -83,6 +81,3 @@ class Ledger_Handler(QtHandlerBase): def setup_dialog(self): self.show_error(_('Initialization of Ledger HW devices is currently disabled.')) - return - dialog = StartBTChipPersoDialog() - dialog.exec_() From 8a4e307b7813e65a8182ed2420f46438e11d907d Mon Sep 17 00:00:00 2001 From: nachunjae <51984395+nachunjae@users.noreply.github.com> Date: Sat, 29 Jun 2019 10:54:53 +0900 Subject: [PATCH 063/115] Update block explorer URL for btc.com (#5438) * update block explorer URL for btc.com --- electrum/util.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index 8fee2414a..62df080a9 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -660,8 +660,8 @@ mainnet_block_explorers = { {'tx': 'tx/', 'addr': 'address/'}), 'Bitaps.com': ('https://btc.bitaps.com/', {'tx': '', 'addr': ''}), - 'BTC.com': ('https://chain.btc.com/', - {'tx': 'tx/', 'addr': 'address/'}), + 'BTC.com': ('https://btc.com/', + {'tx': '', 'addr': ''}), 'Chain.so': ('https://www.chain.so/', {'tx': 'tx/BTC/', 'addr': 'address/BTC/'}), 'Insight.is': ('https://insight.bitpay.com/', @@ -691,8 +691,6 @@ testnet_block_explorers = { {'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/', {'tx': 'tx/', 'addr': 'address/'}), 'system default': ('blockchain://000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943/', From e7304ce23ec90340dc8d12905267fdbd92c2492d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 29 Jun 2019 04:03:29 +0200 Subject: [PATCH 064/115] TorDetector: minor clean-up --- electrum/gui/qt/network_dialog.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 00a97de66..31c21f73f 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -26,6 +26,7 @@ import socket import time from enum import IntEnum +from typing import Tuple from PyQt5.QtCore import Qt, pyqtSignal, QThread from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox, @@ -521,19 +522,20 @@ class TorDetector(QThread): ports = [9050, 9150] while True: for p in ports: - if TorDetector.is_tor_port(p): - self.found_proxy.emit(("127.0.0.1", p)) + net_addr = ("127.0.0.1", p) + if TorDetector.is_tor_port(net_addr): + self.found_proxy.emit(net_addr) break else: self.found_proxy.emit(None) time.sleep(10) @staticmethod - def is_tor_port(port): + def is_tor_port(net_addr: Tuple[str, int]) -> bool: try: - s = (socket._socketobject if hasattr(socket, "_socketobject") else socket.socket)(socket.AF_INET, socket.SOCK_STREAM) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.1) - s.connect(("127.0.0.1", port)) + s.connect(net_addr) # Tor responds uniquely to HTTP-like requests s.send(b"GET\n") if b"Tor is not an HTTP Proxy" in s.recv(1024): From 37809bed7459eb7cffff0b1f03954a6b3dc3a616 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 29 Jun 2019 05:27:28 +0200 Subject: [PATCH 065/115] qt high dpi: fix some text fields There are probably other DPI related issues though. closes #5471 closes #4597 closes #1927 --- electrum/gui/qt/amountedit.py | 6 ++++-- electrum/gui/qt/installwizard.py | 4 ++-- electrum/gui/qt/main_window.py | 14 +++++++------- electrum/gui/qt/network_dialog.py | 16 ++++++++++------ electrum/gui/qt/util.py | 12 +++++++++--- electrum/plugins/hw_wallet/qt.py | 4 ++-- 6 files changed, 34 insertions(+), 22 deletions(-) diff --git a/electrum/gui/qt/amountedit.py b/electrum/gui/qt/amountedit.py index c3920b9d6..8fc63fa1a 100644 --- a/electrum/gui/qt/amountedit.py +++ b/electrum/gui/qt/amountedit.py @@ -3,9 +3,11 @@ from decimal import Decimal from PyQt5.QtCore import pyqtSignal, Qt -from PyQt5.QtGui import QPalette, QPainter +from PyQt5.QtGui import QPalette, QPainter, QFontMetrics from PyQt5.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame) +from .util import char_width_in_lineedit + from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name, FEERATE_PRECISION, quantize_feerate) @@ -24,7 +26,7 @@ class AmountEdit(MyLineEdit): def __init__(self, base_unit, is_int=False, parent=None): QLineEdit.__init__(self, parent) # This seems sufficient for hundred-BTC amounts with 8 decimals - self.setFixedWidth(140) + self.setFixedWidth(16 * char_width_in_lineedit()) self.base_unit = base_unit self.textChanged.connect(self.numbify) self.is_int = is_int diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index 309bf03ee..722940042 100644 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -23,7 +23,7 @@ from electrum.i18n import _ from .seed_dialog import SeedLayout, KeysLayout from .network_dialog import NetworkChoiceLayout from .util import (MessageBoxMixin, Buttons, icon_path, ChoicesLayout, WWLabel, - InfoButton) + InfoButton, char_width_in_lineedit) from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW from electrum.plugin import run_hook @@ -180,7 +180,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): vbox.addWidget(self.msg_label) hbox2 = QHBoxLayout() self.pw_e = QLineEdit('', self) - self.pw_e.setFixedWidth(150) + self.pw_e.setFixedWidth(17 * char_width_in_lineedit()) self.pw_e.setEchoMode(2) self.pw_label = QLabel(_('Password') + ':') hbox2.addWidget(self.pw_label) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 474ff563e..72add77a1 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -85,7 +85,7 @@ from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialo OkButton, InfoButton, WWLabel, TaskThread, CancelButton, CloseButton, HelpButton, MessageBoxMixin, EnterButton, expiration_values, ButtonsLineEdit, CopyCloseButton, import_meta_gui, export_meta_gui, - filename_field, address_field) + filename_field, address_field, char_width_in_lineedit) from .installwizard import WIF_HELP_TEXT from .history_list import HistoryList, HistoryModel from .update_checker import UpdateCheck, UpdateCheckThread @@ -1216,7 +1216,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly())) self.max_button = EnterButton(_("Max"), self.spend_max) - self.max_button.setFixedWidth(140) + self.max_button.setFixedWidth(self.amount_e.width()) self.max_button.setCheckable(True) grid.addWidget(self.max_button, 4, 3) hbox = QHBoxLayout() @@ -1248,7 +1248,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.spend_max() if self.max_button.isChecked() else self.update_fee() self.fee_slider = FeeSlider(self, self.config, fee_cb) - self.fee_slider.setFixedWidth(140) + self.fee_slider.setFixedWidth(self.amount_e.width()) def on_fee_or_feerate(edit_changed, editing_finished): edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e @@ -1271,7 +1271,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.size_e = TxSizeLabel() self.size_e.setAlignment(Qt.AlignCenter) self.size_e.setAmount(0) - self.size_e.setFixedWidth(140) + self.size_e.setFixedWidth(self.amount_e.width()) self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) self.feerate_e = FeerateEdit(lambda: 0) @@ -1293,7 +1293,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.show_message(title=_('Fee rounding'), msg=text) self.feerounding_icon = QPushButton(read_QIcon('info.png'), '') - self.feerounding_icon.setFixedWidth(20) + self.feerounding_icon.setFixedWidth(round(2.2 * char_width_in_lineedit())) self.feerounding_icon.setFlat(True) self.feerounding_icon.clicked.connect(feerounding_onclick) self.feerounding_icon.setVisible(False) @@ -2198,9 +2198,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): vbox.addWidget(QLabel(_('New Contact') + ':')) grid = QGridLayout() line1 = QLineEdit() - line1.setFixedWidth(280) + line1.setFixedWidth(32 * char_width_in_lineedit()) line2 = QLineEdit() - line2.setFixedWidth(280) + line2.setFixedWidth(32 * char_width_in_lineedit()) grid.addWidget(QLabel(_("Address")), 1, 0) grid.addWidget(line1, 1, 1) grid.addWidget(QLabel(_("Name")), 2, 0) diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 31c21f73f..511700c35 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -32,6 +32,7 @@ from PyQt5.QtCore import Qt, pyqtSignal, QThread from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox, QLineEdit, QDialog, QVBoxLayout, QHeaderView, QCheckBox, QTabWidget, QWidget, QLabel) +from PyQt5.QtGui import QFontMetrics from electrum.i18n import _ from electrum import constants, blockchain @@ -39,7 +40,7 @@ from electrum.interface import serialize_server, deserialize_server from electrum.network import Network from electrum.logging import get_logger -from .util import Buttons, CloseButton, HelpButton, read_QIcon +from .util import Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit _logger = get_logger(__name__) @@ -214,14 +215,17 @@ class NetworkChoiceLayout(object): tabs.addTab(server_tab, _('Server')) tabs.addTab(proxy_tab, _('Proxy')) + fixed_width_hostname = 24 * char_width_in_lineedit() + fixed_width_port = 6 * char_width_in_lineedit() + # server tab grid = QGridLayout(server_tab) grid.setSpacing(8) self.server_host = QLineEdit() - self.server_host.setFixedWidth(200) + self.server_host.setFixedWidth(fixed_width_hostname) self.server_port = QLineEdit() - self.server_port.setFixedWidth(60) + self.server_port.setFixedWidth(fixed_width_port) self.autoconnect_cb = QCheckBox(_('Select server automatically')) self.autoconnect_cb.setEnabled(self.config.is_modifiable('auto_connect')) @@ -258,15 +262,15 @@ class NetworkChoiceLayout(object): self.proxy_mode = QComboBox() self.proxy_mode.addItems(['SOCKS4', 'SOCKS5']) self.proxy_host = QLineEdit() - self.proxy_host.setFixedWidth(200) + self.proxy_host.setFixedWidth(fixed_width_hostname) self.proxy_port = QLineEdit() - self.proxy_port.setFixedWidth(60) + self.proxy_port.setFixedWidth(fixed_width_port) self.proxy_user = QLineEdit() self.proxy_user.setPlaceholderText(_("Proxy user")) self.proxy_password = QLineEdit() self.proxy_password.setPlaceholderText(_("Password")) self.proxy_password.setEchoMode(QLineEdit.Password) - self.proxy_password.setFixedWidth(60) + self.proxy_password.setFixedWidth(fixed_width_port) self.proxy_mode.currentIndexChanged.connect(self.set_proxy) self.proxy_host.editingFinished.connect(self.set_proxy) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 066e7bd15..ba5411b5c 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -10,7 +10,7 @@ from functools import partial, lru_cache from typing import NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List, Dict from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem, - QPalette, QIcon) + QPalette, QIcon, QFontMetrics) from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal, QCoreApplication, QItemSelectionModel, QThread, QSortFilterProxyModel, QSize, QLocale) @@ -127,7 +127,7 @@ class HelpButton(QPushButton): QPushButton.__init__(self, '?') self.help_text = text self.setFocusPolicy(Qt.NoFocus) - self.setFixedWidth(20) + self.setFixedWidth(round(2.2 * char_width_in_lineedit())) self.clicked.connect(self.onclick) def onclick(self): @@ -143,7 +143,7 @@ class InfoButton(QPushButton): QPushButton.__init__(self, 'Info') self.help_text = text self.setFocusPolicy(Qt.NoFocus) - self.setFixedWidth(60) + self.setFixedWidth(6 * char_width_in_lineedit()) self.clicked.connect(self.onclick) def onclick(self): @@ -872,6 +872,12 @@ class FromList(QTreeWidget): self.header().setSectionResizeMode(1, sm) +def char_width_in_lineedit() -> int: + char_width = QFontMetrics(QLineEdit().font()).averageCharWidth() + # 'averageCharWidth' seems to underestimate on Windows, hence 'max()' + return max(9, char_width) + + if __name__ == "__main__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done")) diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index a93a9d30c..5a8c9dfe7 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -32,7 +32,7 @@ from PyQt5.QtWidgets import QVBoxLayout, QLineEdit, QHBoxLayout, QLabel from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog, - Buttons, CancelButton, TaskThread) + Buttons, CancelButton, TaskThread, char_width_in_lineedit) from electrum.i18n import _ from electrum.logging import Logger @@ -149,7 +149,7 @@ class QtHandlerBase(QObject, Logger): hbox = QHBoxLayout(dialog) hbox.addWidget(QLabel(msg)) text = QLineEdit() - text.setMaximumWidth(100) + text.setMaximumWidth(12 * char_width_in_lineedit()) text.returnPressed.connect(dialog.accept) hbox.addWidget(text) hbox.addStretch(1) From 72d06038a7e0668359f7d3d036c2c3f11193a213 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 29 Jun 2019 06:03:14 +0200 Subject: [PATCH 066/115] synchronizer: fix race in _on_address_status Triggering needs two consecutive scripthash status changes in very quick succession. Client gets notification from server, but then response to "blockchain.scripthash.get_history" will already contain the changed-again history that has a different status. 20190627T101547.902638Z | INFO | synchronizer.[default_wallet] | receiving history mwXtx49BCGAiy4tU1r7MBX5VVLWSdtasCL 1 20190627T101547.903262Z | INFO | synchronizer.[default_wallet] | error: status mismatch: mwXtx49BCGAiy4tU1r7MBX5VVLWSdtasCL --- electrum/synchronizer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py index cb4d6cbeb..313467a8d 100644 --- a/electrum/synchronizer.py +++ b/electrum/synchronizer.py @@ -147,7 +147,7 @@ class Synchronizer(SynchronizerBase): def _reset(self): super()._reset() self.requested_tx = {} - self.requested_histories = {} + self.requested_histories = set() def diagnostic_name(self): return self.wallet.diagnostic_name() @@ -161,10 +161,10 @@ class Synchronizer(SynchronizerBase): history = self.wallet.db.get_addr_history(addr) if history_status(history) == status: return - if addr in self.requested_histories: + if (addr, status) in self.requested_histories: return # request address history - self.requested_histories[addr] = status + self.requested_histories.add((addr, status)) h = address_to_scripthash(addr) self._requests_sent += 1 result = await self.network.get_history_for_scripthash(h) @@ -188,7 +188,7 @@ class Synchronizer(SynchronizerBase): await self._request_missing_txs(hist) # Remove request; this allows up_to_date to be True - self.requested_histories.pop(addr) + self.requested_histories.discard((addr, status)) async def _request_missing_txs(self, hist, *, allow_server_not_finding_tx=False): # "hist" is a list of [tx_hash, tx_height] lists From 4f51308eab65c82b90536110b60aeea588a3d657 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 29 Jun 2019 16:21:07 +0200 Subject: [PATCH 067/115] coinchooser: clarify docs for make_tx --- electrum/coinchooser.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index 98036f99f..f7ecffbe2 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -266,6 +266,10 @@ class CoinChooserBase(Logger): the transaction) it is kept, otherwise none is sent and it is added to the transaction fee. + `inputs` and `outputs` are guaranteed to be a subset of the + inputs and outputs of the resulting transaction. + `coins` are further UTXOs we can choose from. + Note: fee_estimator_vb expects virtual bytes """ From 4c63eca896019a720f052a91a34a47142d5be43c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 29 Jun 2019 16:22:37 +0200 Subject: [PATCH 068/115] wallet.bump_fee: loosen sanity check a tiny bit --- electrum/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 0e087d678..92ae99f5e 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -914,7 +914,7 @@ class Abstract_Wallet(AddressSynchronizer): method_used = 2 actual_new_fee_rate = tx_new.get_fee() / tx_new.estimated_size() - if actual_new_fee_rate < quantize_feerate(new_fee_rate): + if quantize_feerate(actual_new_fee_rate) < quantize_feerate(new_fee_rate): raise Exception(f"bump_fee feerate target was not met (method: {method_used}). " f"got {actual_new_fee_rate}, expected >={new_fee_rate}") From 7c5247081b014d089a3a81c99a756edbc39af07e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 1 Jul 2019 15:00:21 +0200 Subject: [PATCH 069/115] change electrum.png to square (by padding) ran "appimagelint" and apparently icon file needs to be a square (could have just created another copy, but I guess a square icon might make sense in other cases too) --- electrum/gui/icons/electrum.png | Bin 9322 -> 24089 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/electrum/gui/icons/electrum.png b/electrum/gui/icons/electrum.png index e07a7dc07fc1e4af61e1aed5f2256fbbc4906258..2ab3260e042b6b9891a9148589c33f4c0e87a628 100644 GIT binary patch literal 24089 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 VNPDbT9$gm%c&G3lTrOu3_8*h*D|!F` From 6455f515f0556886a40f606c23e487882cd13bbe Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 1 Jul 2019 18:01:14 +0200 Subject: [PATCH 070/115] build-wine: don't use gpg keyservers based on Electron-Cash/Electron-Cash@a582be04d3baa263a9fd2179a87bbb27a4dd6d87 --- ...D10B6531D7C8E1BC296021FC624643487034E5.asc | 108 ++++++++++++++++++ contrib/build-wine/prepare-wine.sh | 11 +- 2 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 contrib/build-wine/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc diff --git a/contrib/build-wine/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc b/contrib/build-wine/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc new file mode 100644 index 000000000..a87dbe180 --- /dev/null +++ b/contrib/build-wine/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc @@ -0,0 +1,108 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: User-ID: Steve Dower (Python Release Signing) +Comment: Created: 2015-04-06 02:32 +Comment: Type: 4096-bit RSA +Comment: Usage: Signing, Encryption, Certifying User-IDs +Comment: Fingerprint: 7ED10B6531D7C8E1BC296021FC624643487034E5 + + +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 +=fjOq +-----END PGP PUBLIC KEY BLOCK----- diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 84ba25618..42e60c9d2 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -28,7 +28,7 @@ PYTHON="wine $PYHOME/python.exe -OO -B" here="$(dirname "$(readlink -e "$0")")" set -e -. $here/../build_tools_util.sh +. "$here"/../build_tools_util.sh wine 'wineboot' @@ -38,15 +38,8 @@ cd /tmp/electrum-build # Install Python # note: you might need "sudo apt-get install dirmngr" for the following # keys from https://www.python.org/downloads/#pubkeys -KEYLIST_PYTHON_DEV="531F072D39700991925FED0C0EDDC5F26A45C816 26DEA9D4613391EF3E25C9FF0A5B101836580288 CBC547978A3964D14B9AB36A6AF053F07D9DC8D2 C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF 12EF3DC38047DA382D18A5B999CDEA9DA4135B38 8417157EDBE73D9EAC1E539B126EB563A74B06BF DBBF2EEBF925FAADCF1F3FFFD9866941EA5BBD71 2BA0DB82515BBB9EFFAC71C5C9BE28DEE6DF025C 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D C9B104B3DD3AA72D7CCB1066FB9921286F5E1540 97FC712E4C024BBEA48A61ED3A5CA953F73C700D 7ED10B6531D7C8E1BC296021FC624643487034E5" KEYRING_PYTHON_DEV="keyring-electrum-build-python-dev.gpg" -for server in $(shuf -e ha.pool.sks-keyservers.net \ - hkp://p80.pool.sks-keyservers.net:80 \ - keyserver.ubuntu.com \ - hkp://keyserver.ubuntu.com:80) ; do - retry gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --keyserver "$server" --recv-keys $KEYLIST_PYTHON_DEV \ - && break || : ; -done +gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --import "$here"/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc for msifile in core dev exe lib pip tools; do echo "Installing $msifile..." wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi" From 423d44bcaf533e93b0ac505f0b81f496f6130606 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 1 Jul 2019 20:18:30 +0200 Subject: [PATCH 071/115] build-wine: some clean-up. cache downloads. better status messages --- contrib/build-wine/build-electrum-git.sh | 25 +++++------ contrib/build-wine/build-secp256k1.sh | 27 ++++++++---- contrib/build-wine/build.sh | 28 +++++++----- contrib/build-wine/prepare-wine.sh | 54 +++++++++++++----------- 4 files changed, 77 insertions(+), 57 deletions(-) diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index be3530697..d0c680603 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -12,11 +12,11 @@ PYTHON="wine $PYHOME/python.exe -OO -B" # Let's begin! -cd `dirname $0` set -e -mkdir -p tmp -cd tmp +here="$(dirname "$(readlink -e "$0")")" + +. "$CONTRIB"/build_tools_util.sh pushd $WINEPREFIX/drive_c/electrum @@ -25,12 +25,11 @@ git submodule init git submodule update VERSION=`git describe --tags --dirty --always` -echo "Last commit: $VERSION" +info "Last commit: $VERSION" pushd ./contrib/deterministic-build/electrum-locale if ! which msgfmt > /dev/null 2>&1; then - echo "Please install gettext" - exit 1 + fail "Please install gettext" fi for i in ./locale/*; do dir=$WINEPREFIX/drive_c/electrum/electrum/$i/LC_MESSAGES @@ -42,24 +41,23 @@ popd find -exec touch -d '2000-11-11T11:11:11+00:00' {} + popd -cp $WINEPREFIX/drive_c/electrum/LICENCE . # Install frozen dependencies -$PYTHON -m pip install -r ../../deterministic-build/requirements.txt +$PYTHON -m pip install -r "$CONTRIB"/deterministic-build/requirements.txt -$PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt +$PYTHON -m pip install -r "$CONTRIB"/deterministic-build/requirements-hw.txt pushd $WINEPREFIX/drive_c/electrum # see https://github.com/pypa/pip/issues/2195 -- pip makes a copy of the entire directory -echo "Pip installing Electrum. This might take a long time if the project folder is large." +info "Pip installing Electrum. This might take a long time if the project folder is large." $PYTHON -m pip install . popd -cd .. rm -rf dist/ # build standalone and portable versions +info "Running pyinstaller..." 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 @@ -67,7 +65,7 @@ pushd dist find -exec touch -d '2000-11-11T11:11:11+00:00' {} + popd -# build NSIS installer +info "building NSIS installer" # $VERSION could be passed to the electrum.nsi script, but this would require some rewriting in the script itself. wine "$WINEPREFIX/drive_c/Program Files (x86)/NSIS/makensis.exe" /DPRODUCT_VERSION=$VERSION electrum.nsi @@ -75,5 +73,4 @@ cd dist mv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe cd .. -echo "Done." -sha256sum dist/electrum*exe +sha256sum dist/electrum*.exe diff --git a/contrib/build-wine/build-secp256k1.sh b/contrib/build-wine/build-secp256k1.sh index 9879a75d3..2ae60b905 100755 --- a/contrib/build-wine/build-secp256k1.sh +++ b/contrib/build-wine/build-secp256k1.sh @@ -3,6 +3,14 @@ set -e +here="$(dirname "$(readlink -e "$0")")" +LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7" + +. "$CONTRIB"/build_tools_util.sh + +info "building libsecp256k1..." + + build_dll() { #sudo apt-get install -y mingw-w64 export SOURCE_DATE_EPOCH=1530212462 @@ -19,23 +27,26 @@ build_dll() { } -cd /tmp/electrum-build +cd "$CACHEDIR" + +if [ -f "secp256k1/libsecp256k1.dll" ]; then + info "libsecp256k1.dll already built, skipping" + exit 0 +fi + if [ ! -d secp256k1 ]; then git clone https://github.com/bitcoin-core/secp256k1.git - cd secp256k1; -else - cd secp256k1 - git pull fi -LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7" -git reset --hard "$LIBSECP_VERSION" +cd secp256k1 +git reset --hard git clean -f -x -q +git checkout $LIBSECP_VERSION build_dll i686-w64-mingw32 # 64-bit would be: x86_64-w64-mingw32 mv .libs/libsecp256k1-0.dll libsecp256k1.dll find -exec touch -d '2000-11-11T11:11:11+00:00' {} + -echo "building libsecp256k1 finished" +info "building libsecp256k1 finished" diff --git a/contrib/build-wine/build.sh b/contrib/build-wine/build.sh index 01ca071fe..8b79402fd 100755 --- a/contrib/build-wine/build.sh +++ b/contrib/build-wine/build.sh @@ -1,28 +1,36 @@ #!/bin/bash + +set -e + # Lucky number export PYTHONHASHSEED=22 -here=$(dirname "$0") +here="$(dirname "$(readlink -e "$0")")" test -n "$here" -a -d "$here" || exit -echo "Clearing $here/build and $here/dist..." +export CONTRIB="$here/.." +export CACHEDIR="$here/.cache" +export PIP_CACHE_DIR="$CACHEDIR/pip_cache" + +. "$CONTRIB"/build_tools_util.sh + +info "Clearing $here/build and $here/dist..." rm "$here"/build/* -rf rm "$here"/dist/* -rf -mkdir -p /tmp/electrum-build -mkdir -p /tmp/electrum-build/pip-cache -export PIP_CACHE_DIR="/tmp/electrum-build/pip-cache" +mkdir -p "$CACHEDIR" "$PIP_CACHE_DIR" -$here/build-secp256k1.sh || exit 1 +$here/build-secp256k1.sh || fail "build-secp256k1 failed" -$here/prepare-wine.sh || exit 1 +$here/prepare-wine.sh || fail "prepare-wine failed" -echo "Resetting modification time in C:\Python..." +info "Resetting modification time in C:\Python..." # (Because of some bugs in pyinstaller) pushd /opt/wine64/drive_c/python* find -exec touch -d '2000-11-11T11:11:11+00:00' {} + popd ls -l /opt/wine64/drive_c/python* -$here/build-electrum-git.sh && \ -echo "Done." +$here/build-electrum-git.sh || fail "build-electrum-git failed" + +info "Done." diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 42e60c9d2..5b086688e 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -25,53 +25,57 @@ PYTHON="wine $PYHOME/python.exe -OO -B" # Let's begin! -here="$(dirname "$(readlink -e "$0")")" set -e -. "$here"/../build_tools_util.sh +here="$(dirname "$(readlink -e "$0")")" +. "$CONTRIB"/build_tools_util.sh + +info "Booting wine." wine 'wineboot' -cd /tmp/electrum-build +cd "$CACHEDIR" -# Install Python +info "Installing Python." # note: you might need "sudo apt-get install dirmngr" for the following # keys from https://www.python.org/downloads/#pubkeys KEYRING_PYTHON_DEV="keyring-electrum-build-python-dev.gpg" gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --import "$here"/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc +PYTHON_DOWNLOADS="$CACHEDIR/python$PYTHON_VERSION" +mkdir -p "$PYTHON_DOWNLOADS" for msifile in core dev exe lib pip tools; do echo "Installing $msifile..." - 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=$PYHOME + download_if_not_exist "$PYTHON_DOWNLOADS/${msifile}.msi" "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi" + download_if_not_exist "$PYTHON_DOWNLOADS/${msifile}.msi.asc" "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc" + verify_signature "$PYTHON_DOWNLOADS/${msifile}.msi.asc" $KEYRING_PYTHON_DEV + wine msiexec /i "$PYTHON_DOWNLOADS/${msifile}.msi" /qb TARGETDIR=$PYHOME done -# Install dependencies specific to binaries +info "Installing dependencies specific to binaries." # note that this also installs pinned versions of both pip and setuptools -$PYTHON -m pip install -r "$here"/../deterministic-build/requirements-binaries.txt +$PYTHON -m pip install -r "$CONTRIB"/deterministic-build/requirements-binaries.txt -# Install PyInstaller +info "Installing PyInstaller." $PYTHON -m pip install pyinstaller==3.4 --no-use-pep517 -# Install ZBar -download_if_not_exist $ZBAR_FILENAME "$ZBAR_URL" -verify_hash $ZBAR_FILENAME "$ZBAR_SHA256" -wine "$PWD/$ZBAR_FILENAME" /S +info "Installing ZBar." +download_if_not_exist "$CACHEDIR/$ZBAR_FILENAME" "$ZBAR_URL" +verify_hash "$CACHEDIR/$ZBAR_FILENAME" "$ZBAR_SHA256" +wine "$CACHEDIR/$ZBAR_FILENAME" /S -# Install NSIS installer -download_if_not_exist $NSIS_FILENAME "$NSIS_URL" -verify_hash $NSIS_FILENAME "$NSIS_SHA256" -wine "$PWD/$NSIS_FILENAME" /S - -download_if_not_exist $LIBUSB_FILENAME "$LIBUSB_URL" -verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256" -7z x -olibusb $LIBUSB_FILENAME -aoa +info "Installing NSIS." +download_if_not_exist "$CACHEDIR/$NSIS_FILENAME" "$NSIS_URL" +verify_hash "$CACHEDIR/$NSIS_FILENAME" "$NSIS_SHA256" +wine "$CACHEDIR/$NSIS_FILENAME" /S +info "Installing libusb." +download_if_not_exist "$CACHEDIR/$LIBUSB_FILENAME" "$LIBUSB_URL" +verify_hash "$CACHEDIR/$LIBUSB_FILENAME" "$LIBUSB_SHA256" +7z x -olibusb "$CACHEDIR/$LIBUSB_FILENAME" -aoa 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/ +cp "$CACHEDIR/secp256k1/libsecp256k1.dll" $WINEPREFIX/drive_c/tmp/ -echo "Wine is configured." +info "Wine is configured." From 1d0f67996e7875091fec253ed68d240403a11a0d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 1 Jul 2019 22:05:28 +0200 Subject: [PATCH 072/115] build-wine: build our own pyinstaller bootloader This seems to reduce anti-virus false positives. based on: Electron-Cash/Electron-Cash@1ac12e41114b509be90c75213829a73621f1610e Electron-Cash/Electron-Cash@9726498e95166801ac1e6326ae5833b965df72e3 Electron-Cash/Electron-Cash@40b1139d67013b90b983dc3f9185a771d38e57ff --- contrib/build-wine/prepare-wine.sh | 37 +++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 5b086688e..4bc147841 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -13,6 +13,10 @@ 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 +PYINSTALLER_REPO="https://github.com/SomberNight/pyinstaller.git" +PYINSTALLER_COMMIT=d1cdd726d6a9edc70150d5302453fb90fdd09bf2 +# ^ tag 3.4, plus a custom commit that fixes cross-compilation with MinGW + PYTHON_VERSION=3.6.8 ## These settings probably don't need change @@ -56,9 +60,6 @@ info "Installing dependencies specific to binaries." # note that this also installs pinned versions of both pip and setuptools $PYTHON -m pip install -r "$CONTRIB"/deterministic-build/requirements-binaries.txt -info "Installing PyInstaller." -$PYTHON -m pip install pyinstaller==3.4 --no-use-pep517 - info "Installing ZBar." download_if_not_exist "$CACHEDIR/$ZBAR_FILENAME" "$ZBAR_URL" verify_hash "$CACHEDIR/$ZBAR_FILENAME" "$ZBAR_SHA256" @@ -78,4 +79,34 @@ cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/$PYTHON_FOLDER/ mkdir -p $WINEPREFIX/drive_c/tmp cp "$CACHEDIR/secp256k1/libsecp256k1.dll" $WINEPREFIX/drive_c/tmp/ + +info "Building PyInstaller." +# we build our own PyInstaller boot loader as the default one has high +# anti-virus false positives +( + cd "$WINEPREFIX/drive_c/electrum" + ELECTRUM_COMMIT_HASH=$(git rev-parse HEAD) + cd "$CACHEDIR" + rm -rf pyinstaller + mkdir pyinstaller + cd pyinstaller + # Shallow clone + git init + git remote add origin $PYINSTALLER_REPO + git fetch --depth 1 origin $PYINSTALLER_COMMIT + git checkout FETCH_HEAD + rm -fv PyInstaller/bootloader/Windows-*/run*.exe || true + # add reproducible randomness. this ensures we build a different bootloader for each commit. + # if we built the same one for all releases, that might also get anti-virus false positives + echo "const char *electrum_tag = \"tagged by Electrum@$ELECTRUM_COMMIT_HASH\";" >> ./bootloader/src/pyi_main.c + pushd bootloader + # cross-compile to Windows using host python + python3 ./waf all CC=i686-w64-mingw32-gcc CFLAGS="-Wno-stringop-overflow -static" + popd + # sanity check bootloader is there: + [[ -e PyInstaller/bootloader/Windows-32bit/runw.exe ]] || fail "Could not find runw.exe in target dir!" +) || fail "PyInstaller build failed" +info "Installing PyInstaller." +$PYTHON -m pip install ./pyinstaller + info "Wine is configured." From 53893be4c9224b6914df40248c94d065c2512643 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 2 Jul 2019 19:27:36 +0200 Subject: [PATCH 073/115] crash reporter: in Qt subclass, do network request using WaitingDialog so it does not block the GUI --- electrum/base_crash_reporter.py | 9 +++--- .../gui/kivy/uix/dialogs/crash_reporter.py | 5 +++- electrum/gui/qt/exception_window.py | 30 +++++++++++-------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py index e432436aa..b6d02d881 100644 --- a/electrum/base_crash_reporter.py +++ b/electrum/base_crash_reporter.py @@ -31,10 +31,10 @@ from .version import ELECTRUM_VERSION from . import constants from .i18n import _ from .util import make_aiohttp_session -from .logging import describe_os_version +from .logging import describe_os_version, Logger -class BaseCrashReporter: +class BaseCrashReporter(Logger): report_server = "https://crashhub.electrum.org" config_key = "show_crash_reporter" issue_template = """

Traceback

@@ -59,9 +59,10 @@ class BaseCrashReporter: ASK_CONFIRM_SEND = _("Do you want to send this report?") def __init__(self, exctype, value, tb): + Logger.__init__(self) self.exc_args = (exctype, value, tb) - def send_report(self, asyncio_loop, proxy, endpoint="/crash"): + def send_report(self, asyncio_loop, proxy, endpoint="/crash", *, timeout=None): if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server: # Gah! Some kind of altcoin wants to send us crash reports. raise Exception(_("Missing report URL.")) @@ -69,7 +70,7 @@ class BaseCrashReporter: report.update(self.get_additional_info()) report = json.dumps(report) coro = self.do_post(proxy, BaseCrashReporter.report_server + endpoint, data=report) - response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(5) + response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(timeout) return response async def do_post(self, proxy, url, data): diff --git a/electrum/gui/kivy/uix/dialogs/crash_reporter.py b/electrum/gui/kivy/uix/dialogs/crash_reporter.py index dcbd5cccf..f8c087d93 100644 --- a/electrum/gui/kivy/uix/dialogs/crash_reporter.py +++ b/electrum/gui/kivy/uix/dialogs/crash_reporter.py @@ -119,8 +119,11 @@ class CrashReporter(BaseCrashReporter, Factory.Popup): try: loop = self.main_window.network.asyncio_loop proxy = self.main_window.network.proxy - response = json.loads(BaseCrashReporter.send_report(self, loop, proxy, "/crash.json")) + # FIXME network request in GUI thread... + response = json.loads(BaseCrashReporter.send_report(self, loop, proxy, + "/crash.json", timeout=10)) except (ValueError, ClientError): + #self.logger.debug("", exc_info=True) self.show_popup(_('Unable to send report'), _("Please check your network connection.")) else: self.show_popup(_('Report sent'), response["text"]) diff --git a/electrum/gui/qt/exception_window.py b/electrum/gui/qt/exception_window.py index 982312c3c..4ea065cd0 100644 --- a/electrum/gui/qt/exception_window.py +++ b/electrum/gui/qt/exception_window.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import (QWidget, QLabel, QPushButton, QTextEdit, from electrum.i18n import _ from electrum.base_crash_reporter import BaseCrashReporter from electrum.logging import Logger -from .util import MessageBoxMixin, read_QIcon +from .util import MessageBoxMixin, read_QIcon, WaitingDialog class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger): @@ -96,17 +96,23 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger): self.show() def send_report(self): - try: - proxy = self.main_window.network.proxy - response = BaseCrashReporter.send_report(self, self.main_window.network.asyncio_loop, proxy) - except BaseException as e: - self.logger.exception('There was a problem with the automatic reporting') - self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' + - str(e) + '\n' + - _("Please report this issue manually.")) - return - QMessageBox.about(self, _("Crash report"), response) - self.close() + def on_success(response): + self.show_message(parent=self, + title=_("Crash report"), + msg=response) + self.close() + def on_failure(exc_info): + e = exc_info[1] + self.logger.error('There was a problem with the automatic reporting', exc_info=exc_info) + self.show_critical(parent=self, + msg=(_('There was a problem with the automatic reporting:') + '\n' + + str(e) + '\n' + + _("Please report this issue manually."))) + + proxy = self.main_window.network.proxy + task = lambda: BaseCrashReporter.send_report(self, self.main_window.network.asyncio_loop, proxy) + msg = _('Sending crash report...') + WaitingDialog(self, msg, task, on_success, on_failure) def on_close(self): Exception_Window._active_window = None From fb76fcc886b7c999387a6676f479678df742fdaa Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 2 Jul 2019 21:21:39 +0200 Subject: [PATCH 074/115] trezor: use only Bridge when available fixes #5420 --- electrum/base_wizard.py | 18 ++++++++++-------- electrum/plugins/trezor/trezor.py | 12 +++++++++++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 72bc4c95e..f907dab40 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -298,14 +298,16 @@ class BaseWizard(Logger): if not debug_msg: debug_msg = ' {}'.format(_('No exceptions encountered.')) if not devices: - msg = ''.join([ - _('No hardware device detected.') + '\n', - _('To trigger a rescan, press \'Next\'.') + '\n\n', - _('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ', - _('On Linux, you might have to add a new permission to your udev rules.') + '\n\n', - _('Debug message') + '\n', - debug_msg - ]) + msg = (_('No hardware device detected.') + '\n' + + _('To trigger a rescan, press \'Next\'.') + '\n\n') + if sys.platform == 'win32': + msg += _('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", ' + 'and do "Remove device". Then, plug your device again.') + '\n' + msg += _('While this is less than ideal, it might help if you run Electrum as Administrator.') + '\n' + else: + msg += _('On Linux, you might have to add a new permission to your udev rules.') + '\n' + msg += '\n\n' + msg += _('Debug message') + '\n' + debug_msg self.confirm_dialog(title=title, message=msg, run_next=lambda x: self.choose_hw_device(purpose, storage=storage)) return diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 26c240f9c..75b168f26 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -23,6 +23,7 @@ _logger = get_logger(__name__) try: import trezorlib import trezorlib.transport + from trezorlib.transport.bridge import BridgeTransport, call_bridge from .clientbase import TrezorClientBase @@ -137,7 +138,16 @@ class TrezorPlugin(HW_PluginBase): raise LibraryFoundButUnusable(library_version=version) def enumerate(self): - devices = trezorlib.transport.enumerate_devices() + # If there is a bridge, prefer that. + # On Windows, the bridge runs as Admin (and Electrum usually does not), + # so the bridge has better chances of finding devices. see #5420 + # This also avoids duplicate entries. + try: + call_bridge("enumerate") + except Exception: + devices = trezorlib.transport.enumerate_devices() + else: + devices = BridgeTransport.enumerate() return [Device(path=d.get_path(), interface_number=-1, id_=d.get_path(), From 37e7add7765dc4ec48dbb5fdeb05e79e9f97eac0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 3 Jul 2019 08:46:00 +0200 Subject: [PATCH 075/115] Do not pass storage to address_synchronizer --- electrum/address_synchronizer.py | 25 +++++-------------------- electrum/wallet.py | 14 +++++++++++--- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 094e83db4..f21617911 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -38,7 +38,6 @@ from .i18n import _ from .logging import Logger if TYPE_CHECKING: - from .storage import WalletStorage from .network import Network @@ -60,12 +59,8 @@ class AddressSynchronizer(Logger): inherited by wallet """ - def __init__(self, storage: 'WalletStorage'): - if not storage.is_ready_to_be_used_by_wallet(): - raise Exception("storage not ready to be used by AddressSynchronizer") - - self.storage = storage - self.db = self.storage.db + def __init__(self, db): + self.db = db self.network = None # type: Network Logger.__init__(self) # verifier (SPV) and synchronizer are started in start_network @@ -158,7 +153,7 @@ class AddressSynchronizer(Logger): def on_blockchain_updated(self, event, *args): self._get_addr_balance_cache = {} # invalidate cache - def stop_threads(self, write_to_disk=True): + def stop_threads(self): if self.network: if self.synchronizer: asyncio.run_coroutine_threadsafe(self.synchronizer.stop(), self.network.asyncio_loop) @@ -167,9 +162,7 @@ class AddressSynchronizer(Logger): asyncio.run_coroutine_threadsafe(self.verifier.stop(), self.network.asyncio_loop) self.verifier = None self.network.unregister_callback(self.on_blockchain_updated) - self.storage.put('stored_height', self.get_local_height()) - if write_to_disk: - self.storage.write() + self.db.put('stored_height', self.get_local_height()) def add_address(self, address): if not self.db.get_addr_history(address): @@ -370,12 +363,10 @@ class AddressSynchronizer(Logger): @profiler def check_history(self): - save = False hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.db.get_history())) hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.db.get_history())) for addr in hist_addrs_not_mine: self.db.remove_addr_history(addr) - save = True for addr in hist_addrs_mine: hist = self.db.get_addr_history(addr) for tx_hash, tx_height in hist: @@ -384,9 +375,6 @@ class AddressSynchronizer(Logger): tx = self.db.get_transaction(tx_hash) if tx is not None: self.add_transaction(tx_hash, tx, allow_unrelated=True) - save = True - if save: - self.storage.write() def remove_local_transactions_we_dont_have(self): for txid in itertools.chain(self.db.list_txi(), self.db.list_txo()): @@ -398,7 +386,6 @@ class AddressSynchronizer(Logger): with self.lock: with self.transaction_lock: self.db.clear_history() - self.storage.write() def get_txpos(self, tx_hash): """Returns (height, txpos) tuple, even if the tx is unverified.""" @@ -560,7 +547,7 @@ class AddressSynchronizer(Logger): cached_local_height = getattr(self.threadlocal_cache, 'local_height', None) if cached_local_height is not None: return cached_local_height - return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) + return self.network.get_local_height() if self.network else self.db.get('stored_height', 0) def get_tx_height(self, tx_hash: str) -> TxMinedInfo: with self.lock: @@ -580,8 +567,6 @@ class AddressSynchronizer(Logger): self.up_to_date = up_to_date if self.network: self.network.notify('status') - if up_to_date: - self.storage.write() def is_up_to_date(self): with self.lock: return self.up_to_date diff --git a/electrum/wallet.py b/electrum/wallet.py index 92ae99f5e..188f418a5 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -209,10 +209,10 @@ class Abstract_Wallet(AddressSynchronizer): if not storage.is_ready_to_be_used_by_wallet(): raise Exception("storage not ready to be used by Abstract_Wallet") + self.storage = storage # load addresses needs to be called before constructor for sanity checks - storage.db.load_addresses(self.wallet_type) - - AddressSynchronizer.__init__(self, storage) + self.storage.db.load_addresses(self.wallet_type) + AddressSynchronizer.__init__(self, storage.db) # saved fields self.use_change = storage.get('use_change', True) @@ -235,6 +235,14 @@ class Abstract_Wallet(AddressSynchronizer): self._coin_price_cache = {} + def stop_threads(self): + super().stop_threads() + self.storage.write() + + def set_up_to_date(self, b): + super().set_up_to_date(b) + if b: self.storage.write() + def load_and_cleanup(self): self.load_keystore() self.test_addresses_sanity() From d293b2e0386765b6c2f6fbedb440a57dd17233bc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Jul 2019 13:40:42 +0200 Subject: [PATCH 076/115] wallet: follow-up prev --- electrum/address_synchronizer.py | 3 ++- electrum/wallet.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index f21617911..40a8a5e58 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -39,6 +39,7 @@ from .logging import Logger if TYPE_CHECKING: from .network import Network + from .json_db import JsonDB TX_HEIGHT_LOCAL = -2 @@ -59,7 +60,7 @@ class AddressSynchronizer(Logger): inherited by wallet """ - def __init__(self, db): + def __init__(self, db: JsonDB): self.db = db self.network = None # type: Network Logger.__init__(self) diff --git a/electrum/wallet.py b/electrum/wallet.py index 188f418a5..2ef89efc3 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -243,6 +243,10 @@ class Abstract_Wallet(AddressSynchronizer): super().set_up_to_date(b) if b: self.storage.write() + def clear_history(self): + super().clear_history() + self.storage.write() + def load_and_cleanup(self): self.load_keystore() self.test_addresses_sanity() From e431a07258534b679055c1d349fced254986cf01 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Jul 2019 13:56:11 +0200 Subject: [PATCH 077/115] fix prev: conditional import / type hint failure --- electrum/address_synchronizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 40a8a5e58..843f3679f 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -60,7 +60,7 @@ class AddressSynchronizer(Logger): inherited by wallet """ - def __init__(self, db: JsonDB): + def __init__(self, db: 'JsonDB'): self.db = db self.network = None # type: Network Logger.__init__(self) From 034c1e0828f412db7a24a0ff76b0c3b7394d32e0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Jul 2019 15:47:05 +0200 Subject: [PATCH 078/115] prepare release 3.3.7 --- RELEASE-NOTES | 32 ++++++++++++++++++++++++++++++++ electrum/version.py | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index f5a4ee328..6bbdefd5b 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,35 @@ +# Release 3.3.7 - (July 3, 2019) + + * The AppImage Linux x86_64 binary and the Windows setup.exe + (so now all Windows binaries) are now built reproducibly. + * Bump fee (RBF) improvements: + Implemented a new fee-bump strategy that can add new inputs, + so now any tx can be fee-bumped (d0a4366). The old strategy + was to decrease the value of outputs (starting with change). + We will now try the new strategy first, and only use the old + as a fallback (needed e.g. when spending "Max"). + * CoinChooser improvements: + - more likely to construct txs without change (when possible) + - less likely to construct txs with really small change (e864fa5) + - will now only spend negative effective value coins when + beneficial for privacy (cb69aa8) + * fix long-standing bug that broke wallets with >65k addresses (#5366) + * Windows binaries: we now build the PyInstaller boot loader ourselves, + as this seems to reduce anti-virus false positives (1d0f679) + * Android: (fix) BIP70 payment requests could not be paid (#5376) + * Android: allow copy-pasting partial transactions from/to clipboard + * Fix a performance regression for large wallets (c6a54f0) + * Qt: fix some high DPI issues related to text fields (37809be) + * Trezor: + - allow bypassing "too old firmware" error (#5391) + - use only the Bridge to scan devices if it is available (#5420) + * hw wallets: (known issue) on Win10-1903, some hw devices + (that also have U2F functionality) can only be detected with + Administrator privileges. (see #5420 and #5437) + A workaround is to run as Admin, or for Trezor to install the Bridge. + * Several other minor bugfixes and usability improvements. + + # Release 3.3.6 - (May 16, 2019) * qt: fix crash during 2FA wallet creation (#5334) diff --git a/electrum/version.py b/electrum/version.py index 95d751533..19248c702 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '3.3.6' # version of the client package -APK_VERSION = '3.3.6.0' # read by buildozer.spec +ELECTRUM_VERSION = '3.3.7' # version of the client package +APK_VERSION = '3.3.7.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From aa00fa2a5cf3d050a4a8968b3f20e2c20db512d1 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 3 Jul 2019 16:01:10 +0200 Subject: [PATCH 079/115] update submodule --- contrib/deterministic-build/electrum-locale | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale index 4a960a5ea..aafd932d3 160000 --- a/contrib/deterministic-build/electrum-locale +++ b/contrib/deterministic-build/electrum-locale @@ -1 +1 @@ -Subproject commit 4a960a5ea9157e4112b49860c6e35267c79ce91f +Subproject commit aafd932d37f35a1f276909b6ec27d2f7a60e606a From 5db21134aa86019745cd455d13c6c75ef343cd28 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 3 Jul 2019 16:19:06 +0200 Subject: [PATCH 080/115] separate push and pull locale --- .travis.yml | 7 +--- contrib/make_tgz | 2 +- contrib/{make_locale => pull_locale} | 22 ----------- contrib/push_locale | 59 ++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 28 deletions(-) rename contrib/{make_locale => pull_locale} (66%) create mode 100644 contrib/push_locale diff --git a/.travis.yml b/.travis.yml index 42e316ae6..8d45ee9b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ cache: script: - tox after_success: - - if [ "$TRAVIS_BRANCH" = "master" ]; then pip install requests && contrib/make_locale; fi + - if [ "$TRAVIS_BRANCH" = "master" ]; then pip install requests && contrib/push_locale; fi - coveralls jobs: include: @@ -44,13 +44,10 @@ jobs: - name: "Android build" language: python python: 3.7 - env: - # reset API key to not have make_locale upload stuff here - - crowdin_api_key= services: - docker install: - - pip install requests && ./contrib/make_locale + - pip install requests && ./contrib/pull_locale - ./contrib/make_packages - sudo docker build --no-cache -t electrum-android-builder-img electrum/gui/kivy/tools script: diff --git a/contrib/make_tgz b/contrib/make_tgz index 09c0cea7c..699fe3c58 100755 --- a/contrib/make_tgz +++ b/contrib/make_tgz @@ -8,7 +8,7 @@ PACKAGES="$ROOT_FOLDER"/packages/ LOCALE="$ROOT_FOLDER"/electrum/locale/ if [ ! -d "$LOCALE" ]; then - echo "Run make_locale first!" + echo "Run pull_locale first!" exit 1 fi diff --git a/contrib/make_locale b/contrib/pull_locale similarity index 66% rename from contrib/make_locale rename to contrib/pull_locale index ba95eb5ca..4b187504a 100755 --- a/contrib/make_locale +++ b/contrib/pull_locale @@ -34,28 +34,6 @@ os.chdir('electrum') crowdin_identifier = 'electrum' crowdin_file_name = 'files[electrum-client/messages.pot]' locale_file_name = 'locale/messages.pot' -crowdin_api_key = None - -filename = os.path.expanduser('~/.crowdin_api_key') -if os.path.exists(filename): - with open(filename) as f: - crowdin_api_key = f.read().strip() - -if "crowdin_api_key" in os.environ: - crowdin_api_key = os.environ["crowdin_api_key"] - -if crowdin_api_key: - # Push to Crowdin - print('Push to Crowdin') - url = ('https://api.crowdin.com/api/project/' + crowdin_identifier + '/update-file?key=' + crowdin_api_key) - with open(locale_file_name, 'rb') as f: - files = {crowdin_file_name: f} - response = requests.request('POST', url, files=files) - print("", "update-file:", "-"*20, response.text, "-"*20, sep="\n") - # Build translations - print('Build translations') - response = requests.request('GET', 'https://api.crowdin.com/api/project/' + crowdin_identifier + '/export?key=' + crowdin_api_key) - print("", "export:", "-" * 20, response.text, "-" * 20, sep="\n") # Download & unzip print('Download translations') diff --git a/contrib/push_locale b/contrib/push_locale new file mode 100644 index 000000000..01106cf7f --- /dev/null +++ b/contrib/push_locale @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import os +import subprocess +import io +import zipfile +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('..') + +cmd = "find electrum -type f -name '*.py' -o -name '*.kv'" + +files = subprocess.check_output(cmd, shell=True) + +with open("app.fil", "wb") as f: + f.write(files) + +print("Found {} files to translate".format(len(files.splitlines()))) + +# Generate fresh translation template +if not os.path.exists('electrum/locale'): + os.mkdir('electrum/locale') +cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=electrum/locale/messages.pot' +print('Generate template') +os.system(cmd) + +os.chdir('electrum') + +crowdin_identifier = 'electrum' +crowdin_file_name = 'files[electrum-client/messages.pot]' +locale_file_name = 'locale/messages.pot' +crowdin_api_key = None + +filename = os.path.expanduser('~/.crowdin_api_key') +if os.path.exists(filename): + with open(filename) as f: + crowdin_api_key = f.read().strip() + +if "crowdin_api_key" in os.environ: + crowdin_api_key = os.environ["crowdin_api_key"] + +if crowdin_api_key: + # Push to Crowdin + print('Push to Crowdin') + url = ('https://api.crowdin.com/api/project/' + crowdin_identifier + '/update-file?key=' + crowdin_api_key) + with open(locale_file_name, 'rb') as f: + files = {crowdin_file_name: f} + response = requests.request('POST', url, files=files) + print("", "update-file:", "-"*20, response.text, "-"*20, sep="\n") + # Build translations + print('Build translations') + response = requests.request('GET', 'https://api.crowdin.com/api/project/' + crowdin_identifier + '/export?key=' + crowdin_api_key) + print("", "export:", "-" * 20, response.text, "-" * 20, sep="\n") + From 7b7397a8c7300a1ba1331a3c8fb5383bb0020eb9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 3 Jul 2019 16:20:40 +0200 Subject: [PATCH 081/115] chmod push_locale --- contrib/push_locale | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 contrib/push_locale diff --git a/contrib/push_locale b/contrib/push_locale old mode 100644 new mode 100755 From ec56a4612c0168ed2e01927be7ec2786d024bc19 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Jul 2019 17:36:29 +0200 Subject: [PATCH 082/115] make_tgz: build locale from deterministic submodule --- contrib/make_tgz | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/contrib/make_tgz b/contrib/make_tgz index 699fe3c58..9fcafbd59 100755 --- a/contrib/make_tgz +++ b/contrib/make_tgz @@ -7,16 +7,27 @@ ROOT_FOLDER="$CONTRIB"/.. PACKAGES="$ROOT_FOLDER"/packages/ LOCALE="$ROOT_FOLDER"/electrum/locale/ -if [ ! -d "$LOCALE" ]; then - echo "Run pull_locale first!" - exit 1 -fi - if [ ! -d "$PACKAGES" ]; then echo "Run make_packages first!" exit 1 fi +git submodule update --init + +( + rm -rf "$LOCALE" + cd "$CONTRIB/deterministic-build/electrum-locale/" + if ! which msgfmt > /dev/null 2>&1; then + echo "Please install gettext" + exit 1 + fi + for i in ./locale/*; do + dir="$ROOT_FOLDER"/electrum/$i/LC_MESSAGES + mkdir -p $dir + msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true + done +) + ( cd "$ROOT_FOLDER" From f1516d60ec3fb37f2faa885a177330dc2d657d6a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Jul 2019 17:37:02 +0200 Subject: [PATCH 083/115] mac build: fix locale in binaries --- contrib/osx/base.sh | 4 ++++ contrib/osx/make_osx | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/contrib/osx/base.sh b/contrib/osx/base.sh index c2e3527c0..c11e270a4 100644 --- a/contrib/osx/base.sh +++ b/contrib/osx/base.sh @@ -21,3 +21,7 @@ function DoCodeSignMaybe { # ARGS: infoName fileOrDirName codesignIdentity info "Code signing ${infoName}..." codesign -f -v $deep -s "$identity" "$file" || fail "Could not code sign ${infoName}" } + +function realpath() { + [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" +} diff --git a/contrib/osx/make_osx b/contrib/osx/make_osx index 4217191e7..0cd74d971 100755 --- a/contrib/osx/make_osx +++ b/contrib/osx/make_osx @@ -9,6 +9,10 @@ LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7" . $(dirname "$0")/base.sh +CONTRIB_OSX="$(dirname "$(realpath "$0")")" +CONTRIB="$CONTRIB_OSX/.." +ROOT_FOLDER="$CONTRIB/.." + src_dir=$(dirname "$0") cd $src_dir/../.. @@ -65,13 +69,24 @@ pyinstaller --version rm -rf ./dist -git submodule init -git submodule update +git submodule update --init rm -rf $BUILDDIR > /dev/null 2>&1 mkdir $BUILDDIR -cp -R ./contrib/deterministic-build/electrum-locale/locale/ ./electrum/locale/ +info "generating locale" +( + if ! which msgfmt > /dev/null 2>&1; then + brew install gettext + brew link --force gettext + fi + cd "$CONTRIB"/deterministic-build/electrum-locale + for i in ./locale/*; do + dir="$ROOT_FOLDER"/electrum/$i/LC_MESSAGES + mkdir -p $dir + msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true + done +) || fail "failed generating locale" info "Downloading libusb..." From 5ed6a68d8ccfd54cd2761e2962162c2310d7c05c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Jul 2019 17:42:40 +0200 Subject: [PATCH 084/115] update make_locale doc references, and small nits --- README.rst | 2 +- contrib/build-linux/README.md | 10 ++-------- contrib/build-wine/build-electrum-git.sh | 7 +++---- contrib/make_apk | 2 +- contrib/make_tgz | 1 + electrum/gui/kivy/Readme.md | 2 +- 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index 6244b50a6..7c198e625 100644 --- a/README.rst +++ b/README.rst @@ -75,7 +75,7 @@ Compile the protobuf description file:: Create translations (optional):: sudo apt-get install python-requests gettext - ./contrib/make_locale + ./contrib/pull_locale diff --git a/contrib/build-linux/README.md b/contrib/build-linux/README.md index 22705a435..2bbe4c320 100644 --- a/contrib/build-linux/README.md +++ b/contrib/build-linux/README.md @@ -3,19 +3,13 @@ Source tarballs ✗ _This script does not produce reproducible output (yet!)._ -1. Build locale files - - ``` - contrib/make_locale - ``` - -2. Prepare python dependencies used by Electrum. +1. Prepare python dependencies used by Electrum. ``` contrib/make_packages ``` -3. Create source tarball. +2. Create source tarball. ``` contrib/make_tgz diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index d0c680603..145d9b35b 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -20,13 +20,12 @@ here="$(dirname "$(readlink -e "$0")")" pushd $WINEPREFIX/drive_c/electrum -# Load electrum-locale for this release -git submodule init -git submodule update - VERSION=`git describe --tags --dirty --always` info "Last commit: $VERSION" +# Load electrum-locale for this release +git submodule update --init + pushd ./contrib/deterministic-build/electrum-locale if ! which msgfmt > /dev/null 2>&1; then fail "Please install gettext" diff --git a/contrib/make_apk b/contrib/make_apk index 115b28ee9..d6d48d73e 100755 --- a/contrib/make_apk +++ b/contrib/make_apk @@ -8,7 +8,7 @@ PACKAGES="$ROOT_FOLDER"/packages/ LOCALE="$ROOT_FOLDER"/electrum/locale/ if [ ! -d "$LOCALE" ]; then - echo "Run make_locale first!" + echo "Run pull_locale first!" exit 1 fi diff --git a/contrib/make_tgz b/contrib/make_tgz index 9fcafbd59..4505d2c2e 100755 --- a/contrib/make_tgz +++ b/contrib/make_tgz @@ -25,6 +25,7 @@ git submodule update --init dir="$ROOT_FOLDER"/electrum/$i/LC_MESSAGES mkdir -p $dir msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true + cp $i/electrum.po "$ROOT_FOLDER"/electrum/$i/electrum.po done ) diff --git a/electrum/gui/kivy/Readme.md b/electrum/gui/kivy/Readme.md index f0b4b52cd..29c9678c5 100644 --- a/electrum/gui/kivy/Readme.md +++ b/electrum/gui/kivy/Readme.md @@ -30,7 +30,7 @@ folder. 3. Build locale files ``` - $ ./contrib/make_locale + $ ./contrib/pull_locale ``` 4. Prepare pure python dependencies From 194bf84418c0c4c9319669fbd214d43adde12626 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Jul 2019 21:09:11 +0200 Subject: [PATCH 085/115] build readme nits sudo is needed to rm FRESH_CLONE as docker is running as sudo. the proper fix would be to have docker not run as sudo... --- contrib/build-linux/appimage/README.md | 2 +- contrib/build-wine/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/build-linux/appimage/README.md b/contrib/build-linux/appimage/README.md index 6f6be280a..b4c8f8faa 100644 --- a/contrib/build-linux/appimage/README.md +++ b/contrib/build-linux/appimage/README.md @@ -25,7 +25,7 @@ see [issue #5159](https://github.com/spesmilo/electrum/issues/5159). 2. Build image ``` - $ sudo docker build --no-cache -t electrum-appimage-builder-img contrib/build-linux/appimage + $ sudo docker build -t electrum-appimage-builder-img contrib/build-linux/appimage ``` 3. Build binary diff --git a/contrib/build-wine/README.md b/contrib/build-wine/README.md index 9c6cb1d7b..218b3aee8 100644 --- a/contrib/build-wine/README.md +++ b/contrib/build-wine/README.md @@ -32,7 +32,7 @@ folder. ``` $ FRESH_CLONE=contrib/build-wine/fresh_clone && \ - rm -rf $FRESH_CLONE && \ + sudo rm -rf $FRESH_CLONE && \ mkdir -p $FRESH_CLONE && \ cd $FRESH_CLONE && \ git clone https://github.com/spesmilo/electrum.git && \ From 94b721baa484972cc360332a4d4ba7b6cb77a2dd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jul 2019 17:23:34 +0200 Subject: [PATCH 086/115] wallet: fix type error in _bump_fee_through_decreasing_outputs fixes #5483 --- electrum/coinchooser.py | 5 +++-- electrum/tests/test_wallet_vertical.py | 8 ++++---- electrum/transaction.py | 6 +++--- electrum/wallet.py | 10 ++++++---- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index f7ecffbe2..13787d45c 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -151,7 +151,7 @@ class CoinChooserBase(Logger): def penalty_func(self, base_tx, *, tx_from_buckets) -> Callable[[List[Bucket]], ScoredCandidate]: raise NotImplementedError - def _change_amounts(self, tx, count, fee_estimator_numchange): + def _change_amounts(self, tx, count, fee_estimator_numchange) -> List[int]: # Break change up if bigger than max_change output_amounts = [o.value for o in tx.outputs()] # Don't split change of less than 0.02 BTC @@ -197,7 +197,7 @@ class CoinChooserBase(Logger): # no more than 10**max_dp_to_round_for_privacy # e.g. a max of 2 decimal places means losing 100 satoshis to fees max_dp_to_round_for_privacy = 2 if self.enable_output_value_rounding else 0 - N = pow(10, min(max_dp_to_round_for_privacy, zeroes[0])) + N = int(pow(10, min(max_dp_to_round_for_privacy, zeroes[0]))) amount = (remaining // N) * N amounts.append(amount) @@ -209,6 +209,7 @@ class CoinChooserBase(Logger): amounts = self._change_amounts(tx, len(change_addrs), fee_estimator_numchange) assert min(amounts) >= 0 assert len(change_addrs) >= len(amounts) + assert all([isinstance(amt, int) for amt in amounts]) # If change is above dust threshold after accounting for the # size of the change output, add it to the transaction. amounts = [amount for amount in amounts if amount >= dust_threshold] diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 6a80e1a60..f72edab51 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -896,7 +896,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70, config=self.config) + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config) tx.locktime = 1325501 tx.version = 1 self.assertFalse(tx.is_complete()) @@ -985,7 +985,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70, config=self.config) + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config) tx.locktime = 1325500 tx.version = 1 self.assertFalse(tx.is_complete()) @@ -1039,7 +1039,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, 0, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70, config=self.config) + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config) tx.locktime = 1325500 tx.version = 1 self.assertFalse(tx.is_complete()) @@ -1102,7 +1102,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, 5_000_000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70, config=self.config) + tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config) tx.locktime = 1325500 tx.version = 1 self.assertFalse(tx.is_complete()) diff --git a/electrum/transaction.py b/electrum/transaction.py index 57753ca25..357f536a5 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1039,13 +1039,13 @@ class Transaction: self.raw = None self.BIP69_sort(inputs=False) - def input_value(self): + def input_value(self) -> int: return sum(x['value'] for x in self.inputs()) - def output_value(self): + def output_value(self) -> int: return sum(o.value for o in self.outputs()) - def get_fee(self): + def get_fee(self) -> int: return self.input_value() - self.output_value() def is_final(self): diff --git a/electrum/wallet.py b/electrum/wallet.py index 2ef89efc3..12ad89d59 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -993,18 +993,20 @@ class Abstract_Wallet(AddressSynchronizer): # prioritize low value outputs, to get rid of dust s = sorted(s, key=lambda o: o.value) for o in s: - target_fee = tx.estimated_size() * new_fee_rate + target_fee = int(round(tx.estimated_size() * new_fee_rate)) delta = target_fee - tx.get_fee() i = outputs.index(o) if o.value - delta >= self.dust_threshold(): - outputs[i] = o._replace(value=o.value - delta) + new_output_value = o.value - delta + assert isinstance(new_output_value, int) + outputs[i] = o._replace(value=new_output_value) delta = 0 break else: del outputs[i] delta -= o.value - if delta > 0: - continue + # note: delta might be negative now, in which case + # the value of the next output will be increased if delta > 0: raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs')) From 28ca561bba004e4f24b45ba3e680802678581986 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jul 2019 18:06:21 +0200 Subject: [PATCH 087/115] added trigger_crash method for testing crash reporter invoke via console as: electrum.base_crash_reporter.trigger_crash() --- electrum/base_crash_reporter.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py index b6d02d881..212a71411 100644 --- a/electrum/base_crash_reporter.py +++ b/electrum/base_crash_reporter.py @@ -131,3 +131,20 @@ class BaseCrashReporter(Logger): def get_wallet_type(self): raise NotImplementedError + + +def trigger_crash(): + # note: do not change the type of the exception, the message, + # or the name of this method. All reports generated through this + # method will be grouped together by the crash reporter, and thus + # don't spam the issue tracker. + + class TestingException(Exception): + pass + + def crash_test(): + raise TestingException("triggered crash for testing purposes") + + import threading + t = threading.Thread(target=crash_test) + t.start() From 650225e238a1a9db07d713cf202f0ed1e51a92e7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jul 2019 19:13:12 +0200 Subject: [PATCH 088/115] crash reporter UX see #5483 --- electrum/constants.py | 4 ++++ electrum/gui/qt/exception_window.py | 15 +++++++++++---- electrum/gui/qt/main_window.py | 2 +- electrum/logging.py | 3 ++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/electrum/constants.py b/electrum/constants.py index 2a78d40ea..26504312c 100644 --- a/electrum/constants.py +++ b/electrum/constants.py @@ -39,6 +39,10 @@ def read_json(filename, default): return r +GIT_REPO_URL = "https://github.com/spesmilo/electrum" +GIT_REPO_ISSUES_URL = "https://github.com/spesmilo/electrum/issues" + + class AbstractNet: @classmethod diff --git a/electrum/gui/qt/exception_window.py b/electrum/gui/qt/exception_window.py index 4ea065cd0..49aeaa3c5 100644 --- a/electrum/gui/qt/exception_window.py +++ b/electrum/gui/qt/exception_window.py @@ -33,6 +33,8 @@ from PyQt5.QtWidgets import (QWidget, QLabel, QPushButton, QTextEdit, from electrum.i18n import _ from electrum.base_crash_reporter import BaseCrashReporter from electrum.logging import Logger +from electrum import constants + from .util import MessageBoxMixin, read_QIcon, WaitingDialog @@ -97,17 +99,22 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger): def send_report(self): def on_success(response): + # note: 'response' coming from (remote) crash reporter server. + # It contains a URL to the GitHub issue, so we allow rich text. self.show_message(parent=self, title=_("Crash report"), - msg=response) + msg=response, + rich_text=True) self.close() def on_failure(exc_info): e = exc_info[1] self.logger.error('There was a problem with the automatic reporting', exc_info=exc_info) self.show_critical(parent=self, - msg=(_('There was a problem with the automatic reporting:') + '\n' + - str(e) + '\n' + - _("Please report this issue manually."))) + msg=(_('There was a problem with the automatic reporting:') + '
' + + repr(e)[:120] + '
' + + _("Please report this issue manually") + + f' on GitHub.'), + rich_text=True) proxy = self.main_window.network.proxy task = lambda: BaseCrashReporter.send_report(self, self.main_window.network.asyncio_loop, proxy) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 72add77a1..cb98033ef 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -666,7 +666,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def show_report_bug(self): msg = ' '.join([ _("Please report any bugs as issues on github:
"), - "https://github.com/spesmilo/electrum/issues

", + f'''{constants.GIT_REPO_ISSUES_URL}

''', _("Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report."), _("Try to explain not only what the bug is, but how it occurs.") ]) diff --git a/electrum/logging.py b/electrum/logging.py index bb3946485..84aea0b60 100644 --- a/electrum/logging.py +++ b/electrum/logging.py @@ -243,7 +243,8 @@ def configure_logging(config): logging.getLogger('kivy').propagate = False from . import ELECTRUM_VERSION - _logger.info(f"Electrum version: {ELECTRUM_VERSION} - https://electrum.org - https://github.com/spesmilo/electrum") + from .constants import GIT_REPO_URL + _logger.info(f"Electrum version: {ELECTRUM_VERSION} - https://electrum.org - {GIT_REPO_URL}") _logger.info(f"Python version: {sys.version}. On platform: {describe_os_version()}") _logger.info(f"Logging to file: {str(_logfile_path)}") _logger.info(f"Log filters: verbosity {repr(verbosity)}, verbosity_shortcuts {repr(verbosity_shortcuts)}") From 93d68a43611ec22369206f3edb03030b1d6fcb3e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jul 2019 19:55:03 +0200 Subject: [PATCH 089/115] exchange_rate: fix #5487 --- electrum/exchange_rate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index fcd3b9f1d..d19a7dcd0 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -563,7 +563,8 @@ class FxThread(ThreadJob): self.logger.info(f"using exchange {name}") if self.config_exchange() != name: self.config.set_key('use_exchange', name, True) - self.exchange = class_(self.on_quotes, self.on_history) + assert issubclass(class_, ExchangeBase), f"unexpected type {class_} for {name}" + self.exchange = class_(self.on_quotes, self.on_history) # type: ExchangeBase # A new exchange means new fx quotes, initially empty. Force # a quote refresh self.trigger_update() @@ -616,6 +617,8 @@ class FxThread(ThreadJob): # Use spot quotes in that case if rate == 'NaN' and (datetime.today().date() - d_t.date()).days <= 2: rate = self.exchange.quotes.get(self.ccy, 'NaN') + if rate is None: + rate = 'NaN' self.history_used_spot = True return Decimal(rate) From 1518c7d1337d6d64731427d493bf91e2a14c272b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jul 2019 20:53:24 +0200 Subject: [PATCH 090/115] build macOS README: mention how Qt affects min supported macOS version --- contrib/osx/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contrib/osx/README.md b/contrib/osx/README.md index fdba1913f..41ad97c13 100644 --- a/contrib/osx/README.md +++ b/contrib/osx/README.md @@ -13,6 +13,9 @@ This needs to be done on a system running macOS or OS X. We use El Capitan (10.1 on High Sierra (or later) makes the binaries [incompatible with older versions](https://github.com/pyinstaller/pyinstaller/issues/1191). +Another factor for the minimum supported macOS version is the +[bundled Qt version](https://github.com/spesmilo/electrum/issues/3685). + Before starting, make sure that the Xcode command line tools are installed (e.g. you have `git`). #### 1.1a Get Xcode From c9006032d96f454552e584bb2b81fcd3e560a9d3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 Jul 2019 21:46:11 +0200 Subject: [PATCH 091/115] qt network dialog: let user edit server host/port in peace incoming network updates could keep changing the text fields while user is editing them --- electrum/gui/qt/network_dialog.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 511700c35..92e3733d5 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -359,8 +359,9 @@ class NetworkChoiceLayout(object): net_params = self.network.get_parameters() host, port, protocol = net_params.host, net_params.port, net_params.protocol proxy_config, auto_connect = net_params.proxy, net_params.auto_connect - self.server_host.setText(host) - self.server_port.setText(str(port)) + if not self.server_host.hasFocus() and not self.server_port.hasFocus(): + self.server_host.setText(host) + self.server_port.setText(str(port)) self.autoconnect_cb.setChecked(auto_connect) interface = self.network.interface From 0d1a473bb08b773d655884fe4479734cabd94713 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Thu, 4 Jul 2019 22:31:56 +0200 Subject: [PATCH 092/115] AppImage: Disable pip warnings about script install locations It warns about scripts being installed in a location that is not on the path, but that is inconsequential as they are not used. ----- taken from Electron-Cash/Electron-Cash@9a29017c5d8906bb04f7e188bf483b0d3ff698f4 --- contrib/build-linux/appimage/build.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 64ff0938b..60827c0bc 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -132,10 +132,10 @@ info "preparing electrum-locale." info "installing electrum and its dependencies." mkdir -p "$CACHEDIR/pip_cache" -"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements.txt" -"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-binaries.txt" -"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-hw.txt" -"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" "$PROJECT_ROOT" +"$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements.txt" +"$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-binaries.txt" +"$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-hw.txt" +"$python" -m pip install --no-warn-script-location --cache-dir "$CACHEDIR/pip_cache" "$PROJECT_ROOT" info "copying zbar" From dcecf7db4b735d668b23af49f5c31c3aea58963d Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Thu, 4 Jul 2019 22:32:51 +0200 Subject: [PATCH 093/115] Wine Build: Make it less noisy This suppresses the pip script location warnings, like we already do for AppImage. It also disables the Wine debugging messages by setting WINEDEBUG=-all. ----- taken from Electron-Cash/Electron-Cash@d3685b038ef0dc3dc6a18345e51ff231c97623f5 --- contrib/build-wine/build-electrum-git.sh | 7 ++++--- contrib/build-wine/prepare-wine.sh | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index 145d9b35b..f54583f1e 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -4,6 +4,7 @@ NAME_ROOT=electrum # These settings probably don't need any change export WINEPREFIX=/opt/wine64 +export WINEDEBUG=-all export PYTHONDONTWRITEBYTECODE=1 export PYTHONHASHSEED=22 @@ -42,14 +43,14 @@ popd # Install frozen dependencies -$PYTHON -m pip install -r "$CONTRIB"/deterministic-build/requirements.txt +$PYTHON -m pip install --no-warn-script-location -r "$CONTRIB"/deterministic-build/requirements.txt -$PYTHON -m pip install -r "$CONTRIB"/deterministic-build/requirements-hw.txt +$PYTHON -m pip install --no-warn-script-location -r "$CONTRIB"/deterministic-build/requirements-hw.txt pushd $WINEPREFIX/drive_c/electrum # see https://github.com/pypa/pip/issues/2195 -- pip makes a copy of the entire directory info "Pip installing Electrum. This might take a long time if the project folder is large." -$PYTHON -m pip install . +$PYTHON -m pip install --no-warn-script-location . popd diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 4bc147841..e4b8c873b 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -21,7 +21,7 @@ PYTHON_VERSION=3.6.8 ## These settings probably don't need change export WINEPREFIX=/opt/wine64 -#export WINEARCH='win32' +export WINEDEBUG=-all PYTHON_FOLDER="python3" PYHOME="c:/$PYTHON_FOLDER" @@ -58,7 +58,7 @@ done info "Installing dependencies specific to binaries." # note that this also installs pinned versions of both pip and setuptools -$PYTHON -m pip install -r "$CONTRIB"/deterministic-build/requirements-binaries.txt +$PYTHON -m pip install --no-warn-script-location -r "$CONTRIB"/deterministic-build/requirements-binaries.txt info "Installing ZBar." download_if_not_exist "$CACHEDIR/$ZBAR_FILENAME" "$ZBAR_URL" @@ -107,6 +107,6 @@ info "Building PyInstaller." [[ -e PyInstaller/bootloader/Windows-32bit/runw.exe ]] || fail "Could not find runw.exe in target dir!" ) || fail "PyInstaller build failed" info "Installing PyInstaller." -$PYTHON -m pip install ./pyinstaller +$PYTHON -m pip install --no-warn-script-location ./pyinstaller info "Wine is configured." From 69b673b8a102d0907f96745254d659b711dab952 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Thu, 4 Jul 2019 23:35:52 +0200 Subject: [PATCH 094/115] AppImage: Bundle more binaries to increase compatibility This slightly increases the AppImage size but allows us to be more compatible with older distributions. ----- taken from Electron-Cash/Electron-Cash@96644acd6fd66f866a86613974bb68bb99f00d8c --- contrib/build-linux/appimage/Dockerfile | 2 +- contrib/build-linux/appimage/build.sh | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/contrib/build-linux/appimage/Dockerfile b/contrib/build-linux/appimage/Dockerfile index 65edb565e..cbed564ea 100644 --- a/contrib/build-linux/appimage/Dockerfile +++ b/contrib/build-linux/appimage/Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update -q && \ libudev-dev=204-5ubuntu20.31 \ gettext=0.18.3.1-1ubuntu3.1 \ libzbar0=0.10+doc-9build1 \ - faketime=0.9.5-2 \ + libdbus-1-3=1.6.18-0ubuntu4.5 \ && \ rm -rf /var/lib/apt/lists/* && \ apt-get autoremove -y && \ diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 60827c0bc..497c7dd6c 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -157,12 +157,8 @@ info "finalizing AppDir." cd "$APPDIR" # copy system dependencies - # note: temporarily move PyQt5 out of the way so - # we don't try to bundle its system dependencies. - mv "$APPDIR/usr/lib/python3.6/site-packages/PyQt5" "$BUILDDIR" copy_deps; copy_deps; copy_deps move_lib - mv "$BUILDDIR/PyQt5" "$APPDIR/usr/lib/python3.6/site-packages" # apply global appimage blacklist to exclude stuff # move usr/include out of the way to preserve usr/include/python3.6m. @@ -171,10 +167,12 @@ info "finalizing AppDir." mv usr/include.tmp usr/include ) || fail "Could not finalize AppDir" -# copy libusb here because it is on the AppImage excludelist and it can cause problems if we use system libusb -info "Copying libusb" -cp -f /usr/lib/x86_64-linux-gnu/libusb-1.0.so "$APPDIR/usr/lib/libusb-1.0.so" || fail "Could not copy libusb" - +# We copy some libraries here that are on the AppImage excludelist +info "Copying additional libraries" +( + # On some systems it can cause problems to use the system libusb + cp -f /usr/lib/x86_64-linux-gnu/libusb-1.0.so "$APPDIR/usr/lib/libusb-1.0.so" || fail "Could not copy libusb" +) info "stripping binaries from debug symbols." # "-R .note.gnu.build-id" also strips the build id From fc65cdaa8afb2304f2bf54a79c4a0af3d263baf4 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Fri, 5 Jul 2019 00:02:26 +0200 Subject: [PATCH 095/115] AppImage: Fix webbrowser.open not opening links There was an issue where webbrowser.open would invoke a program like kde-open5 that loaded the systems libQt5DBus, which was not satisfied with the AppImage's libdbus. To fix this we fork the process, unset LD_LIBRARY_PATH and then open the URL. fixes #5425 ----- taken from Electron-Cash/Electron-Cash@00939aafd1c8e9c1cbf56615bcf9a18db1ff15c2 --- electrum/gui/qt/address_list.py | 5 ++--- electrum/gui/qt/contact_list.py | 5 ++--- electrum/gui/qt/history_list.py | 5 ++--- electrum/gui/qt/main_window.py | 7 +++---- electrum/gui/qt/util.py | 15 +++++++++++++++ 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 9d3a3808f..96436d86d 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -23,7 +23,6 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import webbrowser from enum import IntEnum from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex @@ -36,7 +35,7 @@ from electrum.plugin import run_hook from electrum.bitcoin import is_address from electrum.wallet import InternalAddressCorruption -from .util import MyTreeView, MONOSPACE_FONT, ColorScheme +from .util import MyTreeView, MONOSPACE_FONT, ColorScheme, webopen class AddressList(MyTreeView): @@ -217,7 +216,7 @@ class AddressList(MyTreeView): menu.addAction(_("Remove from wallet"), lambda: self.parent.remove_address(addr)) addr_URL = block_explorer_URL(self.config, 'addr', addr) if addr_URL: - menu.addAction(_("View on block explorer"), lambda: webbrowser.open(addr_URL)) + menu.addAction(_("View on block explorer"), lambda: webopen(addr_URL)) if not self.wallet.is_frozen_address(addr): menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], True)) diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index 974fad5c0..d6df01bb5 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -23,7 +23,6 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import webbrowser from enum import IntEnum from PyQt5.QtGui import QStandardItemModel, QStandardItem @@ -35,7 +34,7 @@ from electrum.bitcoin import is_address from electrum.util import block_explorer_URL from electrum.plugin import run_hook -from .util import MyTreeView, import_meta_gui, export_meta_gui +from .util import MyTreeView, import_meta_gui, export_meta_gui, webopen class ContactList(MyTreeView): @@ -97,7 +96,7 @@ class ContactList(MyTreeView): 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: [webbrowser.open(u) for u in URLs]) + menu.addAction(_("View on block explorer"), lambda: [webopen(u) for u in URLs]) run_hook('create_contact_menu', menu, selected_keys) menu.exec_(self.viewport().mapToGlobal(position)) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 5edee2a54..83d41992e 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -24,7 +24,6 @@ # SOFTWARE. import os -import webbrowser import datetime from datetime import date from typing import TYPE_CHECKING, Tuple, Dict @@ -47,7 +46,7 @@ from electrum.logging import get_logger, Logger from .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton, filename_field, MyTreeView, AcceptFileDragDrop, WindowModalDialog, - CloseButton) + CloseButton, webopen) if TYPE_CHECKING: from electrum.wallet import Abstract_Wallet @@ -608,7 +607,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop): if pr_key: menu.addAction(read_QIcon("seal"), _("View invoice"), lambda: self.parent.show_invoice(pr_key)) if tx_URL: - menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL)) + menu.addAction(_("View on block explorer"), lambda: webopen(tx_URL)) menu.exec_(self.viewport().mapToGlobal(position)) def remove_local_tx(self, delete_tx): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index cb98033ef..4d539a3eb 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -30,7 +30,6 @@ import traceback import json import shutil import weakref -import webbrowser import csv from decimal import Decimal import base64 @@ -85,7 +84,7 @@ from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialo OkButton, InfoButton, WWLabel, TaskThread, CancelButton, CloseButton, HelpButton, MessageBoxMixin, EnterButton, expiration_values, ButtonsLineEdit, CopyCloseButton, import_meta_gui, export_meta_gui, - filename_field, address_field, char_width_in_lineedit) + filename_field, address_field, char_width_in_lineedit, webopen) from .installwizard import WIF_HELP_TEXT from .history_list import HistoryList, HistoryModel from .update_checker import UpdateCheck, UpdateCheckThread @@ -633,9 +632,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): help_menu = menubar.addMenu(_("&Help")) help_menu.addAction(_("&About"), self.show_about) help_menu.addAction(_("&Check for updates"), self.show_update_check) - help_menu.addAction(_("&Official website"), lambda: webbrowser.open("https://electrum.org")) + help_menu.addAction(_("&Official website"), lambda: webopen("https://electrum.org")) help_menu.addSeparator() - help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://docs.electrum.org/")).setShortcut(QKeySequence.HelpContents) + help_menu.addAction(_("&Documentation"), lambda: webopen("http://docs.electrum.org/")).setShortcut(QKeySequence.HelpContents) help_menu.addAction(_("&Report Bug"), self.show_report_bug) help_menu.addSeparator() help_menu.addAction(_("&Donate to server"), self.donate_to_server) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index ba5411b5c..dab448716 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -5,6 +5,8 @@ import sys import platform import queue import traceback +import os +import webbrowser from functools import partial, lru_cache from typing import NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List, Dict @@ -878,6 +880,19 @@ def char_width_in_lineedit() -> int: return max(9, char_width) +def webopen(url: str): + if sys.platform == 'linux' and os.environ.get('APPIMAGE'): + # When on Linux webbrowser.open can fail in AppImage because it can't find the correct libdbus. + # We just fork the process and unset LD_LIBRARY_PATH before opening the URL. + # See #5425 + if os.fork() == 0: + del os.environ['LD_LIBRARY_PATH'] + webbrowser.open(url) + sys.exit(0) + else: + webbrowser.open(url) + + if __name__ == "__main__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done")) From 5bf854edcb15906fd6eb6bb707eb5a89f349b73d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jul 2019 00:10:55 +0200 Subject: [PATCH 096/115] android build: make buildozer.spec more similar to upstream example --- electrum/gui/kivy/tools/buildozer.spec | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/electrum/gui/kivy/tools/buildozer.spec b/electrum/gui/kivy/tools/buildozer.spec index 77fb0291a..6016506de 100644 --- a/electrum/gui/kivy/tools/buildozer.spec +++ b/electrum/gui/kivy/tools/buildozer.spec @@ -86,6 +86,9 @@ android.ndk_path = /opt/android/android-ndk # (str) Android SDK directory (if empty, it will be automatically downloaded.) android.sdk_path = /opt/android/android-sdk +# (str) ANT directory (if empty, it will be automatically downloaded.) +#android.ant_path = + # (str) Android entry point, default is ok for Kivy-based app #android.entrypoint = org.renpy.android.PythonActivity @@ -128,6 +131,9 @@ android.manifest.launch_mode = singleTask # Don't forget to add the WAKE_LOCK permission if you set this to True #android.wakelock = False +# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 +android.arch = armeabi-v7a + # (list) Android application meta-data to set (key=value format) #android.meta_data = @@ -137,9 +143,27 @@ android.manifest.launch_mode = singleTask android.whitelist = lib-dynload/_csv.so -# local version that merges branch 866 + +# +# Python for android (p4a) specific +# + +# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) p4a.source_dir = /opt/python-for-android +# (str) The directory in which python-for-android should look for your own build recipes (if any) +#p4a.local_recipes = + +# (str) Filename to the hook for p4a +#p4a.hook = + +# (str) Bootstrap to use for android builds +# p4a.bootstrap = sdl2 + +# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) +#p4a.port = + + # # iOS specific # From 9b82321fc083f3fb4728d9f3785a04861a617011 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jul 2019 18:39:40 +0200 Subject: [PATCH 097/115] verifier: further sanity checks for SPV verification. Thanks to @JeremyRand --- electrum/verifier.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/electrum/verifier.py b/electrum/verifier.py index 9fef9fc98..d73dcb96d 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -142,13 +142,20 @@ class SPV(NetworkJobOnDefaultServer): try: h = hash_decode(tx_hash) merkle_branch_bytes = [hash_decode(item) for item in merkle_branch] - int(leaf_pos_in_tree) # raise if invalid + leaf_pos_in_tree = int(leaf_pos_in_tree) # raise if invalid except Exception as e: raise MerkleVerificationFailure(e) - - for i, item in enumerate(merkle_branch_bytes): - h = sha256d(item + h) if ((leaf_pos_in_tree >> i) & 1) else sha256d(h + item) + if leaf_pos_in_tree < 0: + raise MerkleVerificationFailure('leaf_pos_in_tree must be non-negative') + index = leaf_pos_in_tree + for item in merkle_branch_bytes: + if len(item) != 32: + raise MerkleVerificationFailure('all merkle branch items have to 32 bytes long') + h = sha256d(item + h) if (index & 1) else sha256d(h + item) + index >>= 1 cls._raise_if_valid_tx(bh2u(h)) + if index != 0: + raise MerkleVerificationFailure(f'leaf_pos_in_tree too large for branch') return hash_encode(h) @classmethod @@ -192,6 +199,8 @@ def verify_tx_is_in_block(tx_hash: str, merkle_branch: Sequence[str], if not block_header: raise MissingBlockHeader("merkle verification failed for {} (missing header {})" .format(tx_hash, block_height)) + if len(merkle_branch) > 30: + raise MerkleVerificationFailure(f"merkle branch too long: {len(merkle_branch)}") calc_merkle_root = SPV.hash_merkle_root(merkle_branch, tx_hash, leaf_pos_in_tree) if block_header.get('merkle_root') != calc_merkle_root: raise MerkleRootMismatch("merkle verification failed for {} ({} != {})".format( From cc9ad3ae9087831e668a55120ec279f8a7135584 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jul 2019 19:27:44 +0200 Subject: [PATCH 098/115] wallet: fix restore_wallet_from_text edge case closes #5490 --- electrum/tests/test_wallet.py | 7 +++++++ electrum/wallet.py | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py index f7ed78494..7d062501d 100644 --- a/electrum/tests/test_wallet.py +++ b/electrum/tests/test_wallet.py @@ -189,6 +189,13 @@ class TestCreateRestoreWallet(WalletTestCase): self.assertEqual(text, wallet.keystore.get_master_public_key()) self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0]) + def test_restore_wallet_from_text_xkey_that_is_also_a_valid_electrum_seed_by_chance(self): + text = 'yprvAJBpuoF4FKpK92ofzQ7ge6VJMtorow3maAGPvPGj38ggr2xd1xCrC9ojUVEf9jhW5L9SPu6fU2U3o64cLrRQ83zaQGNa6YP3ajZS6hHNPXj' + d = restore_wallet_from_text(text, path=self.wallet_path, network=None, gap_limit=1) + wallet = d['wallet'] # type: Standard_Wallet + self.assertEqual(text, wallet.keystore.get_master_private_key(password=None)) + self.assertEqual('3Pa4hfP3LFWqa2nfphYaF7PZfdJYNusAnp', wallet.get_receiving_addresses()[0]) + def test_restore_wallet_from_text_xprv(self): text = 'zprvAZzHPqhCMt51fskXBUYB1fTFYgG3CBjJUT4WEZTpGw6hPSDWBPZYZARC5sE9xAcX8NeWvvucFws8vZxEa65RosKAhy7r5MsmKTxr3hmNmea' d = restore_wallet_from_text(text, path=self.wallet_path, network=None, gap_limit=1) diff --git a/electrum/wallet.py b/electrum/wallet.py index 12ad89d59..051bba8b6 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2051,10 +2051,10 @@ def restore_wallet_from_text(text, *, path, network=None, 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) - elif keystore.is_master_key(text): + if keystore.is_master_key(text): k = keystore.from_master_key(text) + elif keystore.is_seed(text): + k = keystore.from_seed(text, passphrase) else: raise Exception("Seed or key not recognized") storage.put('keystore', k.dump()) From aadde9be17cbb7f9f9aefee1c9a509ae2ed5466c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 Jul 2019 21:16:58 +0200 Subject: [PATCH 099/115] transaction: fix remove_signatures closes #5491 --- electrum/tests/test_wallet_vertical.py | 53 +++++++++++++++++++------- electrum/transaction.py | 3 ++ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index f72edab51..ee9f50dab 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -860,7 +860,20 @@ class TestWalletSending(TestCaseForTestnet): @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') - def test_bump_fee_p2pkh_when_there_is_a_change_address(self, mock_write): + def test_rbf(self, mock_write): + for simulate_moving_txs in (False, True): + with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_p2pkh_when_there_is_a_change_address(simulate_moving_txs=simulate_moving_txs) + with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_p2wpkh_when_there_is_a_change_address(simulate_moving_txs=simulate_moving_txs) + with self.subTest(msg="_bump_fee_when_user_sends_max", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_when_user_sends_max(simulate_moving_txs=simulate_moving_txs) + with self.subTest(msg="_bump_fee_when_new_inputs_need_to_be_added", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_when_new_inputs_need_to_be_added(simulate_moving_txs=simulate_moving_txs) + with self.subTest(msg="_rbf_batching", simulate_moving_txs=simulate_moving_txs): + self._rbf_batching(simulate_moving_txs=simulate_moving_txs) + + def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs): wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean') # bootstrap wallet @@ -877,6 +890,8 @@ class TestWalletSending(TestCaseForTestnet): tx.set_rbf(True) tx.locktime = 1325501 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) @@ -899,6 +914,8 @@ class TestWalletSending(TestCaseForTestnet): tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config) tx.locktime = 1325501 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) self.assertFalse(tx.is_complete()) wallet.sign_transaction(tx, password=None) @@ -947,9 +964,7 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bump_fee_p2wpkh_when_there_is_a_change_address(self, mock_write): + def _bump_fee_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') # bootstrap wallet @@ -966,6 +981,8 @@ class TestWalletSending(TestCaseForTestnet): tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) @@ -988,6 +1005,8 @@ class TestWalletSending(TestCaseForTestnet): tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config) tx.locktime = 1325500 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) self.assertFalse(tx.is_complete()) wallet.sign_transaction(tx, password=None) @@ -1002,9 +1021,7 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 7490060, 0), wallet.get_balance()) - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bump_fee_when_user_sends_max(self, mock_write): + def _bump_fee_when_user_sends_max(self, *, simulate_moving_txs): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') # bootstrap wallet @@ -1020,6 +1037,8 @@ class TestWalletSending(TestCaseForTestnet): tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) @@ -1042,6 +1061,8 @@ class TestWalletSending(TestCaseForTestnet): tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config) tx.locktime = 1325500 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) self.assertFalse(tx.is_complete()) wallet.sign_transaction(tx, password=None) @@ -1056,9 +1077,7 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 0, 0), wallet.get_balance()) - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bump_fee_when_new_inputs_need_to_be_added(self, mock_write): + def _bump_fee_when_new_inputs_need_to_be_added(self, *, simulate_moving_txs): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') # bootstrap wallet (incoming funding_tx1) @@ -1075,6 +1094,8 @@ class TestWalletSending(TestCaseForTestnet): tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) @@ -1105,6 +1126,8 @@ class TestWalletSending(TestCaseForTestnet): tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0, config=self.config) tx.locktime = 1325500 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) self.assertFalse(tx.is_complete()) wallet.sign_transaction(tx, password=None) @@ -1119,9 +1142,7 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 4_990_300, 0), wallet.get_balance()) - @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_rbf_batching(self, mock_write): + def _rbf_batching(self, *, simulate_moving_txs): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') config = SimpleConfig({'electrum_path': self.electrum_path, 'batch_rbf': True}) @@ -1139,6 +1160,8 @@ class TestWalletSending(TestCaseForTestnet): tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) @@ -1173,6 +1196,8 @@ class TestWalletSending(TestCaseForTestnet): tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) @@ -1199,6 +1224,8 @@ class TestWalletSending(TestCaseForTestnet): tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 + if simulate_moving_txs: + tx = Transaction(str(tx)) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) diff --git a/electrum/transaction.py b/electrum/transaction.py index 357f536a5..34815dff7 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -688,7 +688,10 @@ class Transaction: def remove_signatures(self): for txin in self.inputs(): txin['signatures'] = [None] * len(txin['signatures']) + txin['scriptSig'] = None + txin['witness'] = None assert not self.is_complete() + self.raw = None def deserialize(self, force_full_parse=False): if self.raw is None: From eb92bda59749e347aeaa5ed938d76da59b587b89 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 6 Jul 2019 00:25:55 +0200 Subject: [PATCH 100/115] servers: rm phishing domain (and update a port) --- electrum/servers.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/electrum/servers.json b/electrum/servers.json index 634171a18..e3d19a242 100644 --- a/electrum/servers.json +++ b/electrum/servers.json @@ -220,12 +220,6 @@ "t": "50001", "version": "1.4" }, - "erbium1.sytes.net": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, "fedaykin.goip.de": { "pruning": "-", "s": "50002", @@ -404,7 +398,7 @@ }, "fortress.qtornado.com": { "pruning": "-", - "s": "50002", + "s": "443", "t": "50001", "version": "1.4" }, From 91d8f12f44fd5255fa63564267ee2e015d11c9e9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 6 Jul 2019 00:35:03 +0200 Subject: [PATCH 101/115] servers: follow-up prev --- electrum/servers.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/electrum/servers.json b/electrum/servers.json index e3d19a242..fa8e9b651 100644 --- a/electrum/servers.json +++ b/electrum/servers.json @@ -407,5 +407,11 @@ "s": "56002", "t": "56001", "version": "1.4" + }, + "electrumx.erbium.eu": { + "pruning": "-", + "s": "50002", + "t": "50001", + "version": "1.4" } } From b4bf39ee9268c6c453333490c01439a1ddd750ab Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 8 Jul 2019 05:20:26 +0200 Subject: [PATCH 102/115] qt coins tab: let user filter by prevout_hash/prevout_n --- 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 f549cc9cf..a013beab1 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -50,7 +50,7 @@ class UTXOList(MyTreeView): Columns.HEIGHT: _('Height'), Columns.OUTPOINT: _('Output point'), } - filter_columns = [Columns.ADDRESS, Columns.LABEL] + filter_columns = [Columns.ADDRESS, Columns.LABEL, Columns.OUTPOINT] def __init__(self, parent=None): super().__init__(parent, self.create_menu, From a14016275b028bb53e9a2151bb27bdb84d063d07 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 8 Jul 2019 05:58:57 +0200 Subject: [PATCH 103/115] transaction.serialize_preimage: trivial clean-up --- electrum/transaction.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index 34815dff7..cac7f26ed 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -958,14 +958,13 @@ class Transaction: s += script return s - def serialize_preimage(self, i): + def serialize_preimage(self, txin_index: int) -> str: nVersion = int_to_hex(self.version, 4) - nHashType = int_to_hex(1, 4) + nHashType = int_to_hex(1, 4) # SIGHASH_ALL nLocktime = int_to_hex(self.locktime, 4) inputs = self.inputs() outputs = self.outputs() - txin = inputs[i] - # TODO: py3 hex + txin = inputs[txin_index] if self.is_segwit_input(txin): 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)))) @@ -977,7 +976,8 @@ class Transaction: nSequence = int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType else: - txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.get_preimage_script(txin) if i==k else '') for k, txin in enumerate(inputs)) + txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.get_preimage_script(txin) if txin_index==k else '') + for k, txin in enumerate(inputs)) txouts = var_int(len(outputs)) + ''.join(self.serialize_output(o) for o in outputs) preimage = nVersion + txins + txouts + nLocktime + nHashType return preimage From cc42b4a226deeecb7b0cf02546de54c761714800 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 8 Jul 2019 05:52:45 +0200 Subject: [PATCH 104/115] transaction: segwit input signing was doing quadratic hashing performance improvements are negligible for typical transactions though. some measurements of wall clock time for Transaction.sign (with libsecp256k1): 0.11 sec -> 0.08 sec ( 61 p2wpkh-p2sh inputs, 1 output) 2.48 sec -> 0.75 sec ( 522 p2wpkh-p2sh inputs, 1 output) 13.2 sec -> 1.8 sec (1445 p2wpkh inputs, 1 output) 176.4 sec -> 7.6 sec (5542 p2wpkh inputs, 1 output) --- electrum/transaction.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index cac7f26ed..1501e0ef5 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -86,6 +86,12 @@ class TxOutputHwInfo(NamedTuple): script_type: str +class BIP143SharedTxDigestFields(NamedTuple): + hashPrevouts: str + hashSequence: str + hashOutputs: str + + class BCDataStream(object): """Workalike python implementation of Bitcoin's CDataStream class.""" @@ -958,7 +964,18 @@ class Transaction: s += script return s - def serialize_preimage(self, txin_index: int) -> str: + def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields: + inputs = self.inputs() + outputs = self.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)))) + return BIP143SharedTxDigestFields(hashPrevouts=hashPrevouts, + hashSequence=hashSequence, + hashOutputs=hashOutputs) + + def serialize_preimage(self, txin_index: int, *, + bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str: nVersion = int_to_hex(self.version, 4) nHashType = int_to_hex(1, 4) # SIGHASH_ALL nLocktime = int_to_hex(self.locktime, 4) @@ -966,9 +983,11 @@ class Transaction: outputs = self.outputs() txin = inputs[txin_index] if self.is_segwit_input(txin): - 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)))) + if bip143_shared_txdigest_fields is None: + bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() + hashPrevouts = bip143_shared_txdigest_fields.hashPrevouts + hashSequence = bip143_shared_txdigest_fields.hashSequence + hashOutputs = bip143_shared_txdigest_fields.hashOutputs outpoint = self.serialize_outpoint(txin) preimage_script = self.get_preimage_script(txin) scriptCode = var_int(len(preimage_script) // 2) + preimage_script @@ -1129,6 +1148,7 @@ class Transaction: def sign(self, keypairs) -> None: # keypairs: (x_)pubkey -> secret_bytes + bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() for i, txin in enumerate(self.inputs()): pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) for j, (pubkey, x_pubkey) in enumerate(zip(pubkeys, x_pubkeys)): @@ -1142,14 +1162,15 @@ class Transaction: continue _logger.info(f"adding signature for {_pubkey}") sec, compressed = keypairs.get(_pubkey) - sig = self.sign_txin(i, sec) + sig = self.sign_txin(i, sec, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields) self.add_signature_to_txin(i, j, sig) _logger.info(f"is_complete {self.is_complete()}") self.raw = self.serialize() - def sign_txin(self, txin_index, privkey_bytes) -> str: - pre_hash = sha256d(bfh(self.serialize_preimage(txin_index))) + def sign_txin(self, txin_index, privkey_bytes, *, bip143_shared_txdigest_fields=None) -> str: + pre_hash = sha256d(bfh(self.serialize_preimage(txin_index, + bip143_shared_txdigest_fields=bip143_shared_txdigest_fields))) privkey = ecc.ECPrivkey(privkey_bytes) sig = privkey.sign_transaction(pre_hash) sig = bh2u(sig) + '01' From 8a1052330d9a7bbb239d5ecea6c1bef94b3e16f2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 10 Jul 2019 16:35:40 +0200 Subject: [PATCH 105/115] wallet: loosen bump_fee sanity check further fixes #5502 --- electrum/wallet.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 051bba8b6..acb605a69 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -902,6 +902,7 @@ class Abstract_Wallet(AddressSynchronizer): """ if tx.is_final(): raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final')) + new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision old_tx_size = tx.estimated_size() old_fee = self.get_tx_fee(tx) if old_fee is None: @@ -925,10 +926,12 @@ class Abstract_Wallet(AddressSynchronizer): tx=tx, new_fee_rate=new_fee_rate) method_used = 2 - actual_new_fee_rate = tx_new.get_fee() / tx_new.estimated_size() - if quantize_feerate(actual_new_fee_rate) < quantize_feerate(new_fee_rate): - raise Exception(f"bump_fee feerate target was not met (method: {method_used}). " - f"got {actual_new_fee_rate}, expected >={new_fee_rate}") + target_min_fee = new_fee_rate * tx_new.estimated_size() + actual_fee = tx_new.get_fee() + if actual_fee + 1 < target_min_fee: + raise Exception(f"bump_fee fee target was not met (method: {method_used}). " + f"got {actual_fee}, expected >={target_min_fee}. " + f"target rate was {new_fee_rate}") tx_new.locktime = get_locktime_for_new_transaction(self.network) return tx_new From c67705e116fd2a0822c15b284feede3576ea4269 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 10 Jul 2019 20:26:25 +0200 Subject: [PATCH 106/115] appimage build: build was failing on some host systems On Ubuntu host, build succeeded; but e.g. on Manjaro host, it failed with: ``` ./build.sh: line 233: /opt/electrum/contrib/build-linux/appimage/../../../contrib/build-linux/appimage/.cache/appimage/appimagetool: No such file or directory ``` --- contrib/build-linux/appimage/build.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 497c7dd6c..9a654df02 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -229,8 +229,11 @@ find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + info "creating the AppImage." ( cd "$BUILDDIR" - chmod +x "$CACHEDIR/appimagetool" - "$CACHEDIR/appimagetool" --appimage-extract + cp "$CACHEDIR/appimagetool" "$CACHEDIR/appimagetool_copy" + # zero out "appimage" magic bytes, as on some systems they confuse the linker + sed -i 's|AI\x02|\x00\x00\x00|' "$CACHEDIR/appimagetool_copy" + chmod +x "$CACHEDIR/appimagetool_copy" + "$CACHEDIR/appimagetool_copy" --appimage-extract # We build a small wrapper for mksquashfs that removes the -mkfs-fixed-time option # that mksquashfs from squashfskit does not support. It is not needed for squashfskit. cat > ./squashfs-root/usr/lib/appimagekit/mksquashfs << EOF From 61bf5ce59aa7397b021d2f6f30289b09c1f3c2cb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 10 Jul 2019 23:44:51 +0200 Subject: [PATCH 107/115] windows build: calculate COFF checksum ourselves closes #5504 --- contrib/build-wine/build-electrum-git.sh | 42 ++++++++++++++++++++++++ contrib/build-wine/unsign.sh | 22 +------------ 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index f54583f1e..438c28ce6 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -73,4 +73,46 @@ cd dist mv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe cd .. +info "Padding binaries to 8-byte boundaries, and fixing COFF image checksum in PE header" +# note: 8-byte boundary padding is what osslsigncode uses: +# https://github.com/mtrojnar/osslsigncode/blob/6c8ec4427a0f27c145973450def818e35d4436f6/osslsigncode.c#L3047 +( + cd dist + for binary_file in ./*.exe; do + info ">> fixing $binary_file..." + # code based on https://github.com/erocarrera/pefile/blob/bbf28920a71248ed5c656c81e119779c131d9bd4/pefile.py#L5877 + python3 <> 32) + if checksum > 2 ** 32: + checksum = (checksum & 0xffffffff) + (checksum >> 32) + +checksum = (checksum & 0xffff) + (checksum >> 16) +checksum = (checksum) + (checksum >> 16) +checksum = checksum & 0xffff +checksum += len(binary) + +# Set the checksum +binary[checksum_offset : checksum_offset + 4] = int.to_bytes(checksum, byteorder="little", length=4) + +with open(pe_file, "wb") as f: + f.write(binary) +EOF + done +) + sha256sum dist/electrum*.exe diff --git a/contrib/build-wine/unsign.sh b/contrib/build-wine/unsign.sh index fd1e5da81..3caf5645c 100755 --- a/contrib/build-wine/unsign.sh +++ b/contrib/build-wine/unsign.sh @@ -24,28 +24,8 @@ for mine in $(ls dist/*.exe); do echo "Downloading https://download.electrum.org/$version/$f" wget -q https://download.electrum.org/$version/$f -O signed/$f out="signed/stripped/$f" - size=$( wc -c < $mine ) - # Step 1: Remove PE signature from signed binary + # Remove PE signature from signed binary osslsigncode remove-signature -in signed/$f -out $out > /dev/null 2>&1 - # Step 2: Remove checksum and padding from signed binary - python3 < 0: - if binary[-n:] != bytearray(n): - print('expecting failure for', str(pe_file)) - binary = binary[:size] -with open(pe_file, "wb") as f: - f.write(binary) -EOF chmod +x $out if cmp -s $out $mine; then echo "Success: $f" From e81f4bdcd11a072e7c4f38fb1c7eec19c2f7e1a8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 11 Jul 2019 14:51:54 +0200 Subject: [PATCH 108/115] prepare release 3.3.8 --- RELEASE-NOTES | 9 +++++++++ electrum/version.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 6bbdefd5b..154e3850e 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,12 @@ +# Release 3.3.8 - (July 11, 2019) + + * fix some bugs with recent bump fee (RBF) improvements (#5483, #5502) + * fix #5491: watch-only wallets could not bump fee in some cases + * appimage: URLs could not be opened on some desktop environments (#5425) + * faster tx signing for segwit inputs for really large txns (#5494) + * A few other minor bugfixes and usability improvements. + + # Release 3.3.7 - (July 3, 2019) * The AppImage Linux x86_64 binary and the Windows setup.exe diff --git a/electrum/version.py b/electrum/version.py index 19248c702..8c6926337 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '3.3.7' # version of the client package -APK_VERSION = '3.3.7.0' # read by buildozer.spec +ELECTRUM_VERSION = '3.3.8' # version of the client package +APK_VERSION = '3.3.8.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From 665d6540d7c5067a16071e139125ba29464346de Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 11 Jul 2019 16:34:33 +0200 Subject: [PATCH 109/115] pass host to upload script --- contrib/upload | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contrib/upload b/contrib/upload index 29c7e0276..e62cefa3c 100755 --- a/contrib/upload +++ b/contrib/upload @@ -2,16 +2,17 @@ set -e +host=$1 version=`git describe --tags` echo $version here=$(dirname "$0") cd $here/../dist -sftp -oBatchMode=no -b - thomasv@download.electrum.org << ! +sftp -oBatchMode=no -b - thomasv@$host << ! cd electrum-downloads mkdir $version cd $version mput * bye -! \ No newline at end of file +! From 16f56ccbf008cdb48d3391c732c55b43db6706e9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 11 Jul 2019 16:54:47 +0200 Subject: [PATCH 110/115] load version module in make_download --- contrib/make_download | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contrib/make_download b/contrib/make_download index bafc8912e..90bfbb948 100755 --- a/contrib/make_download +++ b/contrib/make_download @@ -2,8 +2,15 @@ import re import os import sys +import importlib -from electrum.version import ELECTRUM_VERSION, APK_VERSION +# load version.py; needlessly complicated alternative to "imp.load_source": +version_spec = importlib.util.spec_from_file_location('version', 'electrum/version.py') +version_module = importlib.util.module_from_spec(version_spec) +version_spec.loader.exec_module(version_module) + +ELECTRUM_VERSION = version_module.ELECTRUM_VERSION +APK_VERSION = version_module.APK_VERSION print("version", ELECTRUM_VERSION) dirname = sys.argv[1] From 40e2b1d6e72fff3b05969e146ccaca89beb83e2c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 14 Jul 2019 14:34:02 +0200 Subject: [PATCH 111/115] exchange_rate: fix #5495 --- electrum/exchange_rate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index d19a7dcd0..2f26ff5d1 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -615,11 +615,11 @@ class FxThread(ThreadJob): rate = self.exchange.historical_rate(self.ccy, d_t) # Frequently there is no rate for today, until tomorrow :) # Use spot quotes in that case - if rate == 'NaN' and (datetime.today().date() - d_t.date()).days <= 2: + if rate in ('NaN', None) and (datetime.today().date() - d_t.date()).days <= 2: rate = self.exchange.quotes.get(self.ccy, 'NaN') - if rate is None: - rate = 'NaN' self.history_used_spot = True + if rate is None: + rate = 'NaN' return Decimal(rate) def historical_value_str(self, satoshis, d_t): From f60f690ca944b9611a6cbf7b29a8a8aab5f13b21 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 17 Jul 2019 20:12:52 +0200 Subject: [PATCH 112/115] change many str(e) to repr(e) as some exceptions were cryptic it's often valuable to see the type of the exception (especially as for some exceptions str(e) == '') --- electrum/commands.py | 2 +- electrum/dnssec.py | 2 +- electrum/gui/kivy/uix/screens.py | 4 ++-- electrum/gui/qt/__init__.py | 6 ++--- electrum/gui/qt/address_dialog.py | 2 +- electrum/gui/qt/installwizard.py | 4 ++-- electrum/gui/qt/main_window.py | 26 ++++++++++----------- electrum/gui/qt/qrtextedit.py | 4 ++-- electrum/gui/qt/transaction_dialog.py | 2 +- electrum/gui/stdio.py | 2 +- electrum/gui/text.py | 2 +- electrum/jsonrpc.py | 4 ++-- electrum/plugin.py | 2 +- electrum/plugins/cosigner_pool/qt.py | 4 ++-- electrum/plugins/email_requests/qt.py | 4 ++-- electrum/plugins/greenaddress_instant/qt.py | 2 +- electrum/plugins/keepkey/keepkey.py | 2 +- electrum/plugins/labels/labels.py | 2 +- electrum/plugins/ledger/qt.py | 2 +- electrum/plugins/safe_t/safe_t.py | 2 +- electrum/plugins/trezor/trezor.py | 2 +- electrum/plugins/trustedcoin/kivy.py | 2 +- electrum/plugins/trustedcoin/qt.py | 4 ++-- electrum/plugins/trustedcoin/trustedcoin.py | 4 ++-- electrum/synchronizer.py | 2 +- electrum/verifier.py | 2 +- 26 files changed, 48 insertions(+), 48 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 4defc0523..77c695c94 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -415,7 +415,7 @@ class Commands: addr = self.wallet.import_private_key(privkey, password) out = "Keypair imported: " + addr except Exception as e: - out = "Error: " + str(e) + out = "Error: " + repr(e) return out def _resolver(self, x): diff --git a/electrum/dnssec.py b/electrum/dnssec.py index 84b1ef43f..77671eb23 100644 --- a/electrum/dnssec.py +++ b/electrum/dnssec.py @@ -265,7 +265,7 @@ def query(url, rtype): out = get_and_validate(ns, url, rtype) validated = True except BaseException as e: - _logger.info(f"DNSSEC error: {str(e)}") + _logger.info(f"DNSSEC error: {repr(e)}") resolver = dns.resolver.get_default_resolver() out = resolver.query(url, rtype) validated = False diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index 093a982fb..adb547fac 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -290,7 +290,7 @@ class SendScreen(CScreen): return except Exception as e: traceback.print_exc(file=sys.stdout) - self.app.show_error(str(e)) + self.app.show_error(repr(e)) return if rbf: tx.set_rbf(True) @@ -410,7 +410,7 @@ class ReceiveScreen(CScreen): self.app.wallet.add_payment_request(req, self.app.electrum_config) added_request = True except Exception as e: - self.app.show_error(_('Error adding payment request') + ':\n' + str(e)) + self.app.show_error(_('Error adding payment request') + ':\n' + repr(e)) added_request = False finally: self.app.update_tab('requests') diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 0fa9095b1..46e24ebf1 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -230,7 +230,7 @@ class ElectrumGui(Logger): custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), - text=_('Cannot load wallet') + ' (1):\n' + str(e)) + text=_('Cannot load wallet') + ' (1):\n' + repr(e)) # if app is starting, still let wizard to appear if not app_is_starting: return @@ -242,7 +242,7 @@ class ElectrumGui(Logger): custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), - text=_('Cannot load wallet') + ' (2):\n' + str(e)) + text=_('Cannot load wallet') + ' (2):\n' + repr(e)) if not wallet: return # create or raise window @@ -257,7 +257,7 @@ class ElectrumGui(Logger): custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), - text=_('Cannot create window for wallet') + ':\n' + str(e)) + text=_('Cannot create window for wallet') + ':\n' + repr(e)) if app_is_starting: wallet_dir = os.path.dirname(path) path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir)) diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index 083d51197..1e3e1af97 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -99,4 +99,4 @@ class AddressDialog(WindowModalDialog): try: self.parent.show_qrcode(text, 'Address', parent=self) except Exception as e: - self.show_message(str(e)) + self.show_message(repr(e)) diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index 722940042..708a79e17 100644 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -265,7 +265,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): continue except BaseException as e: self.logger.exception('') - self.show_message(title=_('Error'), msg=str(e)) + self.show_message(title=_('Error'), msg=repr(e)) raise UserCancelled() elif self.temp_storage.is_encrypted_with_hw_device(): try: @@ -278,7 +278,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): return self.select_storage(path, get_wallet_from_daemon) except BaseException as e: self.logger.exception('') - self.show_message(title=_('Error'), msg=str(e)) + self.show_message(title=_('Error'), msg=repr(e)) raise UserCancelled() if self.temp_storage.is_past_initial_decryption(): break diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 4d539a3eb..d001ba4df 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -345,7 +345,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.logger.error("on_error", exc_info=exc_info) except OSError: pass # see #4418 - self.show_error(str(e)) + self.show_error(repr(e)) def on_network(self, event, *args): if event == 'wallet_updated': @@ -1026,7 +1026,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): try: self.wallet.sign_payment_request(addr, alias, alias_addr, password) except Exception as e: - self.show_error(str(e)) + self.show_error(repr(e)) return else: return @@ -1045,7 +1045,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.wallet.add_payment_request(req, self.config) except Exception as e: self.logger.exception('Error adding payment request') - self.show_error(_('Error adding payment request') + ':\n' + str(e)) + self.show_error(_('Error adding payment request') + ':\n' + repr(e)) else: self.sign_payment_request(addr) self.save_request_button.setEnabled(False) @@ -2151,7 +2151,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): return except BaseException as e: self.logger.exception('') - self.show_error(str(e)) + self.show_error(repr(e)) return old_password = hw_dev_pw if self.wallet.has_password() else None new_password = hw_dev_pw if encrypt_file else None @@ -2288,7 +2288,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): seed = keystore.get_seed(password) passphrase = keystore.get_passphrase(password) except BaseException as e: - self.show_error(str(e)) + self.show_error(repr(e)) return from .seed_dialog import SeedDialog d = SeedDialog(self, seed, passphrase) @@ -2308,7 +2308,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): pk, redeem_script = self.wallet.export_private_key(address, password) except Exception as e: self.logger.exception('') - self.show_message(str(e)) + self.show_message(repr(e)) return xtype = bitcoin.deserialize_privkey(pk)[0] d = WindowModalDialog(self, _("Private key")) @@ -2502,7 +2502,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): tx = tx_from_str(txt) return Transaction(tx) except BaseException as e: - self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + str(e)) + self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e)) return def read_tx_from_qrcode(self): @@ -2510,7 +2510,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): try: data = qrscanner.scan_barcode(self.config.get_video_device()) except BaseException as e: - self.show_error(str(e)) + self.show_error(repr(e)) return if not data: return @@ -2563,7 +2563,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): 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)) + self.show_message(_("Error getting transaction from network") + ":\n" + repr(e)) return tx = transaction.Transaction(raw_tx) self.show_transaction(tx) @@ -2655,7 +2655,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.show_critical(txt, title=_("Unable to create csv")) except Exception as e: - self.show_message(str(e)) + self.show_message(repr(e)) return self.show_message(_("Private keys exported.")) @@ -2732,7 +2732,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): try: valid_privkeys = get_pk(raise_on_error=True) is not None except Exception as e: - button.setToolTip(f'{_("Error")}: {str(e)}') + button.setToolTip(f'{_("Error")}: {repr(e)}') else: button.setToolTip('') button.setEnabled(get_address() is not None and valid_privkeys) @@ -2753,7 +2753,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): try: coins, keypairs = sweep_preparations(get_pk(), self.network) except Exception as e: # FIXME too broad... - self.show_message(str(e)) + self.show_message(repr(e)) return self.do_clear() self.tx_external_keypairs = keypairs @@ -2946,7 +2946,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): SSL_error = None except BaseException as e: SSL_identity = "error" - SSL_error = str(e) + SSL_error = repr(e) else: SSL_identity = "" SSL_error = None diff --git a/electrum/gui/qt/qrtextedit.py b/electrum/gui/qt/qrtextedit.py index a7c087937..8bae313f8 100644 --- a/electrum/gui/qt/qrtextedit.py +++ b/electrum/gui/qt/qrtextedit.py @@ -49,7 +49,7 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin): with open(fileName, "r") as f: data = f.read() except BaseException as e: - self.show_error(_('Error opening file') + ':\n' + str(e)) + self.show_error(_('Error opening file') + ':\n' + repr(e)) else: self.setText(data) @@ -58,7 +58,7 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin): try: data = qrscanner.scan_barcode(get_config().get_video_device()) except BaseException as e: - self.show_error(str(e)) + self.show_error(repr(e)) data = '' if not data: data = '' diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index c3596b86d..9a5ed2fc4 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -191,7 +191,7 @@ class TxDialog(QDialog, MessageBoxMixin): self.show_error(_('Failed to display QR code.') + '\n' + _('Transaction is too large in size.')) except Exception as e: - self.show_error(_('Failed to display QR code.') + '\n' + str(e)) + self.show_error(_('Failed to display QR code.') + '\n' + repr(e)) def sign(self): def sign_done(success): diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py index 13b640c35..7e3573661 100644 --- a/electrum/gui/stdio.py +++ b/electrum/gui/stdio.py @@ -199,7 +199,7 @@ class ElectrumGui: tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)], password, self.config, fee) except Exception as e: - print(str(e)) + print(repr(e)) return if self.str_description: diff --git a/electrum/gui/text.py b/electrum/gui/text.py index 35e8b0307..1e7d1e4b9 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -362,7 +362,7 @@ class ElectrumGui: tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)], password, self.config, fee) except Exception as e: - self.show_message(str(e)) + self.show_message(repr(e)) return if self.str_description: diff --git a/electrum/jsonrpc.py b/electrum/jsonrpc.py index b545e340c..dfd16cfec 100644 --- a/electrum/jsonrpc.py +++ b/electrum/jsonrpc.py @@ -68,10 +68,10 @@ class VerifyingJSONRPCServer(SimpleJSONRPCServer, Logger): return True except (RPCAuthCredentialsInvalid, RPCAuthCredentialsMissing, RPCAuthUnsupportedType) as e: - myself.send_error(401, str(e)) + myself.send_error(401, repr(e)) except BaseException as e: self.logger.exception('') - myself.send_error(500, str(e)) + myself.send_error(500, repr(e)) return False SimpleJSONRPCServer.__init__( diff --git a/electrum/plugin.py b/electrum/plugin.py index a7313862e..f05596c38 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -606,7 +606,7 @@ class DeviceMgr(ThreadJob): new_devices = f() except BaseException as e: self.logger.error('custom device enum failed. func {}, error {}' - .format(str(f), str(e))) + .format(str(f), repr(e))) else: devices.extend(new_devices) diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py index 531cc1700..03e5b5831 100644 --- a/electrum/plugins/cosigner_pool/qt.py +++ b/electrum/plugins/cosigner_pool/qt.py @@ -178,7 +178,7 @@ class Plugin(BasePlugin): e = exc_info[1] try: self.logger.error("on_failure", exc_info=exc_info) except OSError: pass - window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + str(e)) + window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + repr(e)) for window, xpub, K, _hash in self.cosigner_list: if not self.cosigner_can_sign(tx, xpub): @@ -226,7 +226,7 @@ class Plugin(BasePlugin): message = bh2u(privkey.decrypt_message(message)) except Exception as e: self.logger.exception('') - window.show_error(_('Error decrypting message') + ':\n' + str(e)) + window.show_error(_('Error decrypting message') + ':\n' + repr(e)) return self.listener.clear(keyhash) diff --git a/electrum/plugins/email_requests/qt.py b/electrum/plugins/email_requests/qt.py index 000c60d62..3926b31e3 100644 --- a/electrum/plugins/email_requests/qt.py +++ b/electrum/plugins/email_requests/qt.py @@ -196,7 +196,7 @@ class Plugin(BasePlugin): self.processor.send(recipient, message, payload) except BaseException as e: self.logger.exception('') - window.show_message(str(e)) + window.show_message(repr(e)) else: window.show_message(_('Request sent.')) @@ -269,4 +269,4 @@ class CheckConnectionThread(QThread): conn = imaplib.IMAP4_SSL(self.server) conn.login(self.username, self.password) except BaseException as e: - self.connection_error_signal.emit(str(e)) + self.connection_error_signal.emit(repr(e)) diff --git a/electrum/plugins/greenaddress_instant/qt.py b/electrum/plugins/greenaddress_instant/qt.py index 3e9dc79a7..2006c1462 100644 --- a/electrum/plugins/greenaddress_instant/qt.py +++ b/electrum/plugins/greenaddress_instant/qt.py @@ -106,6 +106,6 @@ class Plugin(BasePlugin): d.show_warning(_('{} is not covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification failed!')) except BaseException as e: self.logger.exception('') - d.show_error(str(e)) + d.show_error(repr(e)) finally: d.verify_button.setText(self.button_label) diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 93cf709b2..549de1558 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -216,7 +216,7 @@ class KeepKeyPlugin(HW_PluginBase): exit_code = 1 except BaseException as e: self.logger.exception('') - handler.show_error(str(e)) + handler.show_error(repr(e)) exit_code = 1 finally: wizard.loop.exit(exit_code) diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py index 9118a9f89..c63ed92b2 100644 --- a/electrum/plugins/labels/labels.py +++ b/electrum/plugins/labels/labels.py @@ -160,7 +160,7 @@ class LabelsPlugin(BasePlugin): try: await self.pull_thread(wallet, force) except ErrorConnectingServer as e: - self.logger.info(str(e)) + self.logger.info(repr(e)) def pull(self, wallet, force): if not wallet.network: raise Exception(_('You are offline.')) diff --git a/electrum/plugins/ledger/qt.py b/electrum/plugins/ledger/qt.py index 225c5ef4f..e10aa50d6 100644 --- a/electrum/plugins/ledger/qt.py +++ b/electrum/plugins/ledger/qt.py @@ -60,7 +60,7 @@ class Ledger_Handler(QtHandlerBase): try: from .auth2fa import LedgerAuthDialog except ImportError as e: - self.message_dialog(str(e)) + self.message_dialog(repr(e)) return dialog = LedgerAuthDialog(self, data) dialog.exec_() diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index 379243d7b..c03885019 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -200,7 +200,7 @@ class SafeTPlugin(HW_PluginBase): exit_code = 1 except BaseException as e: self.logger.exception('') - handler.show_error(str(e)) + handler.show_error(repr(e)) exit_code = 1 finally: wizard.loop.exit(exit_code) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 75b168f26..ecf3ebf22 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -222,7 +222,7 @@ class TrezorPlugin(HW_PluginBase): exit_code = 1 except BaseException as e: self.logger.exception('') - handler.show_error(str(e)) + handler.show_error(repr(e)) exit_code = 1 finally: wizard.loop.exit(exit_code) diff --git a/electrum/plugins/trustedcoin/kivy.py b/electrum/plugins/trustedcoin/kivy.py index 4cd01fbe8..12eb7c0fa 100644 --- a/electrum/plugins/trustedcoin/kivy.py +++ b/electrum/plugins/trustedcoin/kivy.py @@ -74,7 +74,7 @@ class Plugin(TrustedCoinPlugin): def accept_terms_of_use(self, wizard): def handle_error(msg, e): - wizard.show_error(msg + ':\n' + str(e)) + wizard.show_error(msg + ':\n' + repr(e)) wizard.terminate() try: tos = server.get_terms_of_service() diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py index b11de233e..389dead49 100644 --- a/electrum/plugins/trustedcoin/qt.py +++ b/electrum/plugins/trustedcoin/qt.py @@ -132,7 +132,7 @@ class Plugin(TrustedCoinPlugin): e = exc_info[1] window.show_error("{header}\n{exc}\n\n{tor}" .format(header=_('Error getting TrustedCoin account info.'), - exc=str(e), + exc=repr(e), tor=_('If you keep experiencing network problems, try using a Tor proxy.'))) return WaitingDialog(parent=window, message=_('Requesting account info from TrustedCoin server...'), @@ -253,7 +253,7 @@ class Plugin(TrustedCoinPlugin): except Exception as e: self.logger.exception('Could not retrieve Terms of Service') tos_e.error_signal.emit(_('Could not retrieve Terms of Service:') - + '\n' + str(e)) + + '\n' + repr(e)) return self.TOS = tos tos_e.tos_signal.emit() diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index ed9ef6d18..809893999 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -485,7 +485,7 @@ class TrustedCoinPlugin(BasePlugin): billing_info = server.get(wallet.get_user_id()[1]) except ErrorConnectingServer as e: if suppress_connection_error: - self.logger.info(str(e)) + self.logger.info(repr(e)) return raise billing_index = billing_info['billing_index'] @@ -709,7 +709,7 @@ class TrustedCoinPlugin(BasePlugin): wizard.show_message(str(e)) wizard.terminate() except Exception as e: - wizard.show_message(str(e)) + wizard.show_message(repr(e)) wizard.terminate() else: k3 = keystore.from_xpub(xpub3) diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py index 313467a8d..fa2967b15 100644 --- a/electrum/synchronizer.py +++ b/electrum/synchronizer.py @@ -292,6 +292,6 @@ class Notifier(SynchronizerBase): async with session.post(url, json=data, headers=headers) as resp: await resp.text() except Exception as e: - self.logger.info(str(e)) + self.logger.info(repr(e)) else: self.logger.info(f'Got Response for {addr}') diff --git a/electrum/verifier.py b/electrum/verifier.py index d73dcb96d..e6b8ecf35 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -121,7 +121,7 @@ class SPV(NetworkJobOnDefaultServer): if self.network.config.get("skipmerklecheck"): self.logger.info(f"skipping merkle proof check {tx_hash}") else: - self.logger.info(str(e)) + self.logger.info(repr(e)) raise GracefulDisconnect(e) # we passed all the tests self.merkle_roots[tx_hash] = header.get('merkle_root') From 249e3d496b6f1e6478365d6a2c83faddb735f40d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 19 Jul 2019 04:52:26 +0200 Subject: [PATCH 113/115] appimage build: rm "build" folder if present as it makes build non-reproducible AFAICT the "build" is created if you "python setup.py install" electrum, which is now deprecated in any case. --- contrib/build-linux/appimage/build.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 9a654df02..8b9226b34 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -20,11 +20,13 @@ SQUASHFSKIT_COMMIT="ae0d656efa2d0df2fcac795b6823b44462f19386" VERSION=`git describe --tags --dirty --always` APPIMAGE="$DISTDIR/electrum-$VERSION-x86_64.AppImage" +. "$CONTRIB"/build_tools_util.sh + rm -rf "$BUILDDIR" mkdir -p "$APPDIR" "$CACHEDIR" "$DISTDIR" - -. "$CONTRIB"/build_tools_util.sh +# potential leftover from setuptools that might make pip put garbage in binary +rm -rf "$PROJECT_ROOT/build" info "downloading some dependencies." From 7dda20c49286bb04c8558a1d03f227fff5add77e Mon Sep 17 00:00:00 2001 From: ldz1 <43045711+ldz1@users.noreply.github.com> Date: Sun, 21 Jul 2019 13:13:51 +0200 Subject: [PATCH 114/115] Removed dead exchange. --- electrum/currencies.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/electrum/currencies.json b/electrum/currencies.json index 657072bef..ee04e65b7 100644 --- a/electrum/currencies.json +++ b/electrum/currencies.json @@ -352,10 +352,7 @@ ], "Bitcointoyou": [ "BRL" - ], - "Bitmarket": [ - "PLN" - ], + ], "Bitso": [ "MXN" ], @@ -895,4 +892,4 @@ "JPY" ], "itBit": [] -} \ No newline at end of file +} From d17489e9710d6d9ac58af45aecc8f2b8cb2af58e Mon Sep 17 00:00:00 2001 From: ldz1 <43045711+ldz1@users.noreply.github.com> Date: Sun, 21 Jul 2019 13:15:06 +0200 Subject: [PATCH 115/115] Removed dead exchange. --- electrum/exchange_rate.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 2f26ff5d1..40d4bb2e9 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -190,13 +190,6 @@ class BitFlyer(ExchangeBase): return {'JPY': Decimal(json['mid'])} -class Bitmarket(ExchangeBase): - - async def get_rates(self, ccy): - json = await self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json') - return {'PLN': Decimal(json['last'])} - - class BitPay(ExchangeBase): async def get_rates(self, ccy):