From c44427d33e5dddcb90292067e648535e567a6374 Mon Sep 17 00:00:00 2001 From: lillypad Date: Sun, 17 Dec 2017 15:22:10 -0400 Subject: [PATCH 01/91] requirements.txt support for user only pip requirements --- requirements.txt | 9 +++++++++ setup.py | 15 ++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..227ec1cd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +pyaes>=0.1a1 +ecdsa>=0.9 +pbkdf2 +requests +qrcode +protobuf +dnspython +jsonrpclib-pelix +PySocks>=1.6.6 diff --git a/setup.py b/setup.py index 9f19e576..5a7a3f72 100755 --- a/setup.py +++ b/setup.py @@ -9,6 +9,9 @@ import platform import imp import argparse +with open('requirements.txt') as f: + requirements = f.read().splitlines() + version = imp.load_source('version', 'lib/version.py') if sys.version_info[:3] < (3, 4, 0): @@ -35,17 +38,7 @@ if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']: setup( name="Electrum", version=version.ELECTRUM_VERSION, - install_requires=[ - 'pyaes>=0.1a1', - 'ecdsa>=0.9', - 'pbkdf2', - 'requests', - 'qrcode', - 'protobuf', - 'dnspython', - 'jsonrpclib-pelix', - 'PySocks>=1.6.6', - ], + install_requires=requirements, packages=[ 'electrum', 'electrum_gui', From 2bcb02d6099b68537816020f0462a5c6773976ac Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 25 Dec 2017 16:51:07 +0100 Subject: [PATCH 02/91] fix some crashes when the underlying QT (C/C++) object no longer exists --- gui/qt/main_window.py | 17 +++++++++++++++-- gui/qt/util.py | 14 +++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 26ccf6ab..e55788b7 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2030,7 +2030,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): task = partial(self.wallet.sign_message, address, message, password) def show_signed_message(sig): - signature.setText(base64.b64encode(sig).decode('ascii')) + try: + signature.setText(base64.b64encode(sig).decode('ascii')) + except RuntimeError: + # (signature) wrapped C/C++ object has been deleted + pass + self.wallet.thread.add(task, on_success=show_signed_message) def do_verify(self, address, message, signature): @@ -2091,7 +2096,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def do_decrypt(self, message_e, pubkey_e, encrypted_e, password): cyphertext = encrypted_e.toPlainText() task = partial(self.wallet.decrypt_message, pubkey_e.text(), cyphertext, password) - self.wallet.thread.add(task, on_success=lambda text: message_e.setText(text.decode('utf-8'))) + + def setText(text): + try: + message_e.setText(text.decode('utf-8')) + except RuntimeError: + # (message_e) wrapped C/C++ object has been deleted + pass + + self.wallet.thread.add(task, on_success=setText) def do_encrypt(self, message_e, pubkey_e, encrypted_e): message = message_e.toPlainText() diff --git a/gui/qt/util.py b/gui/qt/util.py index f1f2d0bd..082ea596 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -406,11 +406,15 @@ class MyTreeWidget(QTreeWidget): def editItem(self, item, column): if column in self.editable_columns: - self.editing_itemcol = (item, column, item.text(column)) - # Calling setFlags causes on_changed events for some reason - item.setFlags(item.flags() | Qt.ItemIsEditable) - QTreeWidget.editItem(self, item, column) - item.setFlags(item.flags() & ~Qt.ItemIsEditable) + try: + self.editing_itemcol = (item, column, item.text(column)) + # Calling setFlags causes on_changed events for some reason + item.setFlags(item.flags() | Qt.ItemIsEditable) + QTreeWidget.editItem(self, item, column) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + except RuntimeError: + # (item) wrapped C/C++ object has been deleted + pass def keyPressEvent(self, event): if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None: From a20a3f97144656337885046e28b0a3fda6284d2d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 17 Jan 2018 01:46:00 +0100 Subject: [PATCH 03/91] fix: sweeping into same wallet --- lib/wallet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index a9dc646b..50002e27 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -887,8 +887,9 @@ class Abstract_Wallet(PrintError): if fixed_fee is None and config.fee_per_kb() is None: raise NoDynamicFeeEstimates() - for item in inputs: - self.add_input_info(item) + if not is_sweep: + for item in inputs: + self.add_input_info(item) # change address if change_addr: From 6c4756dc3df877163b8b5e99377efd56bbae72bf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 7 Feb 2018 17:51:52 +0100 Subject: [PATCH 04/91] check trezorlib version --- lib/util.py | 6 +++++- plugins/hw_wallet/qt.py | 12 +++++++----- plugins/ledger/ledger.py | 11 ++++------- plugins/trezor/trezor.py | 16 +++++++++++++++- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/lib/util.py b/lib/util.py index b8e8ac8e..a59f2a5a 100644 --- a/lib/util.py +++ b/lib/util.py @@ -734,4 +734,8 @@ def setup_thread_excepthook(): self.run = run_with_except_hook - threading.Thread.__init__ = init \ No newline at end of file + threading.Thread.__init__ = init + + +def versiontuple(v): + return tuple(map(int, (v.split(".")))) diff --git a/plugins/hw_wallet/qt.py b/plugins/hw_wallet/qt.py index a9f7291c..d2a3bb5f 100644 --- a/plugins/hw_wallet/qt.py +++ b/plugins/hw_wallet/qt.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- mode: python -*- # # Electrum - lightweight Bitcoin client @@ -184,10 +184,12 @@ class QtPluginBase(object): if not isinstance(keystore, self.keystore_class): continue if not self.libraries_available: - window.show_error( - _("Cannot find python library for") + " '%s'.\n" % self.name \ - + _("Make sure you install it with python3") - ) + if hasattr(self, 'libraries_available_message'): + message = self.libraries_available_message + '\n' + else: + message = _("Cannot find python library for") + " '%s'.\n" % self.name + message += _("Make sure you install it with python3") + window.show_error(message) return tooltip = self.device + '\n' + (keystore.label or 'unnamed') cb = partial(self.show_settings_dialog, window, keystore) diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index cf56ee97..9bab6034 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -10,7 +10,7 @@ from electrum.plugins import BasePlugin from electrum.keystore import Hardware_KeyStore from electrum.transaction import Transaction from ..hw_wallet import HW_PluginBase -from electrum.util import print_error, is_verbose, bfh, bh2u +from electrum.util import print_error, is_verbose, bfh, bh2u, versiontuple try: import hid @@ -57,9 +57,6 @@ class Ledger_Client(): def i4b(self, x): return pack('>I', x) - def versiontuple(self, v): - return tuple(map(int, (v.split(".")))) - def test_pin_unlocked(func): """Function decorator to test the Ledger for being unlocked, and if not, raise a human-readable exception. @@ -140,9 +137,9 @@ class Ledger_Client(): try: firmwareInfo = self.dongleObject.getFirmwareVersion() firmware = firmwareInfo['version'] - self.multiOutputSupported = self.versiontuple(firmware) >= self.versiontuple(MULTI_OUTPUT_SUPPORT) - self.nativeSegwitSupported = self.versiontuple(firmware) >= self.versiontuple(SEGWIT_SUPPORT) - self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and self.versiontuple(firmware) >= self.versiontuple(SEGWIT_SUPPORT_SPECIAL)) + self.multiOutputSupported = versiontuple(firmware) >= versiontuple(MULTI_OUTPUT_SUPPORT) + self.nativeSegwitSupported = versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT) + self.segwitSupported = self.nativeSegwitSupported or (firmwareInfo['specialVersion'] == 0x20 and versiontuple(firmware) >= versiontuple(SEGWIT_SUPPORT_SPECIAL)) if not checkFirmware(firmwareInfo): self.dongleObject.dongle.close() diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py index df3c96f8..f80346c7 100644 --- a/plugins/trezor/trezor.py +++ b/plugins/trezor/trezor.py @@ -2,7 +2,7 @@ import threading from binascii import hexlify, unhexlify -from electrum.util import bfh, bh2u +from electrum.util import bfh, bh2u, versiontuple from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants) from electrum.i18n import _ @@ -86,6 +86,7 @@ class TrezorPlugin(HW_PluginBase): libraries_URL = 'https://github.com/trezor/python-trezor' minimum_firmware = (1, 5, 2) keystore_class = TrezorKeyStore + minimum_library = (0, 9, 0) MAX_LABEL_LEN = 32 @@ -96,6 +97,19 @@ class TrezorPlugin(HW_PluginBase): try: # Minimal test if python-trezor is installed import trezorlib + try: + library_version = trezorlib.__version__ + except AttributeError: + # python-trezor only introduced __version__ in 0.9.0 + library_version = 'unknown' + if library_version == 'unknown' or \ + versiontuple(library_version) < self.minimum_library: + self.libraries_available_message = ( + _("Library version for '{}' is too old.").format(name) + + '\nInstalled: {}, Needed: {}' + .format(library_version, self.minimum_library)) + self.print_stderr(self.libraries_available_message) + raise ImportError() self.libraries_available = True except ImportError: self.libraries_available = False From c9d93d30c7380a302ab7cc2310121154ce786dbf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 8 Feb 2018 17:33:57 +0100 Subject: [PATCH 05/91] fix #3877 --- gui/qt/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui/qt/__init__.py b/gui/qt/__init__.py index 16d07948..30270d17 100644 --- a/gui/qt/__init__.py +++ b/gui/qt/__init__.py @@ -94,6 +94,8 @@ class ElectrumGui: QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) + if hasattr(QGuiApplication, 'setDesktopFileName'): + QGuiApplication.setDesktopFileName('electrum.desktop') self.config = config self.daemon = daemon self.plugins = plugins From 95c5815fe3c922807af06547051339b082bcb926 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 8 Feb 2018 22:39:13 +0100 Subject: [PATCH 06/91] Fix CoinDesk exchange rates and update currencies.json --- lib/currencies.json | 1373 +++++++++++++++++++++++------------------- lib/exchange_rate.py | 15 +- 2 files changed, 779 insertions(+), 609 deletions(-) diff --git a/lib/currencies.json b/lib/currencies.json index 81680d10..a4e85f1f 100644 --- a/lib/currencies.json +++ b/lib/currencies.json @@ -1,631 +1,798 @@ { - "BTCChina": [ - "CNY" - ], "BitPay": [ - "AED", - "AFN", - "ALL", - "AMD", - "ANG", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BTC", - "BTN", - "BWP", - "BZD", - "CAD", - "CDF", - "CHF", - "CLF", - "CLP", - "CNY", - "COP", - "CRC", - "CUP", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ETB", - "EUR", - "FJD", - "FKP", - "GBP", - "GEL", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "INR", - "IQD", - "IRR", - "ISK", - "JEP", - "JMD", - "JOD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KPW", - "KRW", - "KWD", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "LYD", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MRO", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SDG", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "STD", - "SVC", - "SYP", - "SZL", - "THB", - "TJS", - "TMT", - "TND", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VEF", - "VND", - "VUV", - "WST", - "XAF", - "XAG", - "XAU", - "XCD", - "XOF", - "XPF", - "YER", - "ZAR", - "ZMW", + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BCH", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTC", + "BTN", + "BWP", + "BZD", + "CAD", + "CDF", + "CHF", + "CLF", + "CLP", + "CNY", + "COP", + "CRC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JEP", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XCD", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMW", "ZWL" - ], + ], "BitStamp": [ "USD" - ], + ], "BitcoinAverage": [ - "AED", - "AFN", - "ALL", - "AMD", - "ANG", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BTN", - "BWP", - "BYN", - "BZD", - "CAD", - "CDF", - "CHF", - "CLF", - "CLP", - "CNH", - "CNY", - "COP", - "CRC", - "CUC", - "CUP", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ERN", - "ETB", - "ETH", - "EUR", - "FJD", - "FKP", - "GBP", - "GEL", - "GGP", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "IMP", - "INR", - "IQD", - "IRR", - "ISK", - "JEP", - "JMD", - "JOD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KPW", - "KRW", - "KWD", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "LTC", - "LYD", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MRO", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SDG", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "SSP", - "STD", - "SVC", - "SYP", - "SZL", - "THB", - "TJS", - "TMT", - "TND", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VEF", - "VND", - "VUV", - "WST", - "XAF", - "XAG", - "XAU", - "XCD", - "XDR", - "XOF", - "XPD", - "XPF", - "XPT", - "XRP", - "YER", - "ZAR", - "ZEC", - "ZMW", + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHF", + "CLF", + "CLP", + "CNH", + "CNY", + "COP", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GGP", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "IMP", + "INR", + "IQD", + "IRR", + "ISK", + "JEP", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "YER", + "ZAR", + "ZMW", "ZWL" - ], + ], "Bitmarket": [ "PLN" - ], + ], "Bitso": [ "MXN" - ], + ], "Bitvalor": [ "BRL" - ], + ], "BlockchainInfo": [ - "AUD", - "BRL", - "CAD", - "CHF", - "CLP", - "CNY", - "DKK", - "EUR", - "GBP", - "HKD", - "INR", - "ISK", - "JPY", - "KRW", - "NZD", - "PLN", - "RUB", - "SEK", - "SGD", - "THB", - "TWD", + "AUD", + "BRL", + "CAD", + "CHF", + "CLP", + "CNY", + "DKK", + "EUR", + "GBP", + "HKD", + "INR", + "ISK", + "JPY", + "KRW", + "NZD", + "PLN", + "RUB", + "SEK", + "SGD", + "THB", + "TWD", "USD" - ], - "Coinbase": [ - "AED", - "AFN", - "ALL", - "AMD", - "ANG", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BTN", - "BWP", - "BYN", - "BYR", - "BZD", - "CAD", - "CDF", - "CHF", - "CLF", - "CLP", - "CNY", - "COP", - "CRC", - "CUC", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EEK", - "EGP", - "ERN", - "ETB", - "ETH", - "EUR", - "FJD", - "FKP", - "GBP", - "GEL", - "GGP", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "IMP", - "INR", - "IQD", - "ISK", - "JEP", - "JMD", - "JOD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KRW", - "KWD", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "LTC", - "LTL", - "LVL", - "LYD", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MRO", - "MTL", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "SSP", - "STD", - "SVC", - "SZL", - "THB", - "TJS", - "TMT", - "TND", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VEF", - "VND", - "VUV", - "WST", - "XAF", - "XAG", - "XAU", - "XCD", - "XDR", - "XOF", - "XPD", - "XPF", - "XPT", - "YER", - "ZAR", - "ZMK", - "ZMW", + ], + "CoinDesk": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTC", + "BTN", + "BWP", + "BYR", + "BZD", + "CAD", + "CDF", + "CHF", + "CLF", + "CLP", + "CNY", + "COP", + "CRC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EEK", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JEP", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LTL", + "LVL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MTL", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBT", + "XCD", + "XDR", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMK", + "ZMW", "ZWL" - ], - "Coinsecure": [ - "INR" - ], + ], + "Coinbase": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BCH", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BYR", + "BZD", + "CAD", + "CDF", + "CHF", + "CLF", + "CLP", + "CNH", + "CNY", + "COP", + "CRC", + "CUC", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EEK", + "EGP", + "ERN", + "ETB", + "ETH", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GGP", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "IMP", + "INR", + "IQD", + "ISK", + "JEP", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LTC", + "LTL", + "LVL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MTL", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "YER", + "ZAR", + "ZMK", + "ZMW", + "ZWL" + ], "Foxbit": [ "BRL" - ], + ], "Kraken": [ - "CAD", - "EUR", - "GBP", - "JPY", + "CAD", + "EUR", + "GBP", + "JPY", "USD" - ], + ], "LocalBitcoins": [ - "AED", - "ARS", - "AUD", - "BDT", - "BRL", - "BYN", - "CAD", - "CHF", - "CLP", - "CNY", - "COP", - "CRC", - "CZK", - "DKK", - "DOP", - "EGP", - "EUR", - "GBP", - "GHS", - "HKD", - "HRK", - "HUF", - "IDR", - "INR", - "IRR", - "ISK", - "JPY", - "KES", - "KZT", - "MAD", - "MMK", - "MXN", - "MYR", - "NGN", - "NOK", - "NZD", - "OMR", - "PAB", - "PEN", - "PHP", - "PKR", - "PLN", - "QAR", - "RON", - "RSD", - "RUB", - "SAR", - "SEK", - "SGD", - "THB", - "TRY", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "VEF", - "VND", - "XAF", - "ZAR" - ], + "AED", + "ARS", + "AUD", + "BAM", + "BDT", + "BHD", + "BOB", + "BRL", + "BYN", + "CAD", + "CHF", + "CLP", + "CNY", + "COP", + "CRC", + "CZK", + "DKK", + "DOP", + "EGP", + "ETH", + "EUR", + "GBP", + "GHS", + "HKD", + "HRK", + "HUF", + "IDR", + "ILS", + "INR", + "IRR", + "JOD", + "JPY", + "KES", + "KRW", + "KZT", + "LKR", + "MAD", + "MXN", + "MYR", + "NGN", + "NOK", + "NZD", + "PAB", + "PEN", + "PHP", + "PKR", + "PLN", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SEK", + "SGD", + "THB", + "TRY", + "TTD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "VEF", + "VND", + "XAR", + "ZAR", + "ZMW" + ], "MercadoBitcoin": [ "BRL" - ], + ], "NegocieCoins": [ "BRL" - ], - "Winkdex": [ - "USD" - ], + ], "WEX": [ "EUR", "RUB", diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index 23de311e..e6a56247 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -33,7 +33,7 @@ class ExchangeBase(PrintError): def get_json(self, site, get_string): # APIs must have https url = ''.join(['https://', site, get_string]) - response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}) + response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}, timeout=10) return response.json() def get_csv(self, site, get_string): @@ -199,18 +199,19 @@ class Coinbase(ExchangeBase): class CoinDesk(ExchangeBase): - def get_rates(self, ccy): + def get_currencies(self): dicts = self.get_json('api.coindesk.com', '/v1/bpi/supported-currencies.json') + return [d['currency'] for d in dicts] + + def get_rates(self, ccy): json = self.get_json('api.coindesk.com', '/v1/bpi/currentprice/%s.json' % ccy) - ccys = [d['currency'] for d in dicts] - result = dict.fromkeys(ccys) - result[ccy] = Decimal(json['bpi'][ccy]['rate_float']) + result = {ccy: Decimal(json['bpi'][ccy]['rate_float'])} return result def history_starts(self): - return { 'USD': '2012-11-30' } + return { 'USD': '2012-11-30', 'EUR': '2013-09-01' } def history_ccys(self): return self.history_starts().keys() @@ -346,7 +347,9 @@ def get_exchanges_and_currencies(): exchange = klass(None, None) try: d[name] = exchange.get_currencies() + print(name, "ok") except: + print(name, "error") continue with open(path, 'w') as f: f.write(json.dumps(d, indent=4, sort_keys=True)) From 710eda1a563003c15ac0d8f6edb26ebf0d394a64 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 8 Feb 2018 23:03:45 +0100 Subject: [PATCH 07/91] coinchooser: make output value rounding configurable (config var, qt) --- gui/qt/main_window.py | 15 ++++++++++++++- lib/coinchooser.py | 20 ++++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index e4f5085a..1c860edf 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1136,7 +1136,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def feerounding_onclick(): text = (self.feerounding_text + '\n\n' + _('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' + - _('At most 100 satoshis might be lost due to this rounding.') + '\n' + + _('At most 100 satoshis might be lost due to this rounding.') + ' ' + + _("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' + _('Also, dust is not kept as change, but added to the fee.')) QMessageBox.information(self, 'Fee rounding', text) @@ -2893,6 +2894,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): unconf_cb.stateChanged.connect(on_unconf) tx_widgets.append((unconf_cb, None)) + def on_outrounding(x): + self.config.set_key('coin_chooser_output_rounding', bool(x)) + enable_outrounding = self.config.get('coin_chooser_output_rounding', False) + outrounding_cb = QCheckBox(_('Enable output value rounding')) + outrounding_cb.setToolTip( + _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' + + _('This might improve your privacy somewhat.') + '\n' + + _('If enabled, at most 100 satoshis might be lost due to this, per transaction.')) + outrounding_cb.setChecked(enable_outrounding) + outrounding_cb.stateChanged.connect(on_outrounding) + tx_widgets.append((outrounding_cb, None)) + # Fiat Currency hist_checkbox = QCheckBox() fiat_address_checkbox = QCheckBox() diff --git a/lib/coinchooser.py b/lib/coinchooser.py index 472e3aa3..ffc5bfd8 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -87,6 +87,8 @@ def strip_unneeded(bkts, sufficient_funds): class CoinChooserBase(PrintError): + enable_output_value_rounding = False + def keys(self, coins): raise NotImplementedError @@ -135,7 +137,13 @@ class CoinChooserBase(PrintError): zeroes = [trailing_zeroes(i) for i in output_amounts] min_zeroes = min(zeroes) max_zeroes = max(zeroes) - zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1) + + if n > 1: + zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1) + else: + # if there is only one change output, this will ensure that we aim + # to have one that is exactly as precise as the most precise output + zeroes = [min_zeroes] # Calculate change; randomize it a bit if using more than 1 output remaining = change_amount @@ -150,8 +158,10 @@ class CoinChooserBase(PrintError): n -= 1 # Last change output. Round down to maximum precision but lose - # no more than 100 satoshis to fees (2dp) - N = pow(10, min(2, zeroes[0])) + # 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])) amount = (remaining // N) * N amounts.append(amount) @@ -370,4 +380,6 @@ def get_name(config): def get_coin_chooser(config): klass = COIN_CHOOSERS[get_name(config)] - return klass() + coinchooser = klass() + coinchooser.enable_output_value_rounding = config.get('coin_chooser_output_rounding', False) + return coinchooser From d8dad74267fe1bdcd98dce0d79e2196f7ced60dc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 9 Feb 2018 00:16:11 +0100 Subject: [PATCH 08/91] fee calculation: force back-end to use integer sat/bytes --- lib/simple_config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/simple_config.py b/lib/simple_config.py index b7a41ddc..c0bbb216 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -428,7 +428,12 @@ class SimpleConfig(PrintError): @classmethod def estimate_fee_for_feerate(cls, fee_per_kb, size): - return int(fee_per_kb * size / 1000.) + # note: We only allow integer sat/byte values atm. + # The GUI for simplicity reasons only displays integer sat/byte, + # and for the sake of consistency, we thus only use integer sat/byte in + # the backend too. + fee_per_byte = int(fee_per_kb / 1000) + return int(fee_per_byte * size) def update_fee_estimates(self, key, value): self.fee_estimates[key] = value From 3f954a8b3d39d2cb45927371a856802104f9182c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 9 Feb 2018 15:28:28 +0100 Subject: [PATCH 09/91] Factorize history export code used in GUI and command line. Add options to export history limits and exchange rate. Closes: #1752, #2604, Replaces: #2715, 3724 --- gui/qt/main_window.py | 27 ++++------------------ lib/commands.py | 52 +++++++++++-------------------------------- lib/wallet.py | 51 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 62 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 1c860edf..62fb8f05 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2494,32 +2494,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): plt.show() def do_export_history(self, wallet, fileName, is_csv): - history = wallet.get_history() + history = wallet.export_history(fx=self.fx) lines = [] for item in history: - tx_hash, height, confirmations, timestamp, value, balance = item - if height>0: - if timestamp is not None: - time_string = format_time(timestamp) - else: - time_string = _("unverified") - else: - time_string = _("unconfirmed") - - if value is not None: - value_string = format_satoshis(value, True) - else: - value_string = '--' - - if tx_hash: - label = wallet.get_label(tx_hash) - else: - label = "" - if is_csv: - lines.append([tx_hash, label, confirmations, value_string, time_string]) + lines.append([item['txid'], item.get('label', ''), item['confirmations'], item['value'], item['date']]) else: - lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string}) + lines.append(item) with open(fileName, "w+") as f: if is_csv: @@ -2529,7 +2510,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): transaction.writerow(line) else: import json - f.write(json.dumps(lines, indent = 4)) + f.write(json.dumps(lines, indent=4)) def sweep_key_dialog(self): d = WindowModalDialog(self, title=_('Sweep private keys')) diff --git a/lib/commands.py b/lib/commands.py index c0bfb48e..29bfd8b6 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -440,46 +440,16 @@ class Commands: return tx.as_dict() @command('w') - def history(self): + def history(self, year=None, show_addresses=False, show_fiat=False): """Wallet history. Returns the transaction history of your wallet.""" - balance = 0 - out = [] - for item in self.wallet.get_history(): - tx_hash, height, conf, timestamp, value, balance = item - if timestamp: - date = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] - else: - date = "----" - label = self.wallet.get_label(tx_hash) - tx = self.wallet.transactions.get(tx_hash) - tx.deserialize() - input_addresses = [] - output_addresses = [] - for x in tx.inputs(): - if x['type'] == 'coinbase': continue - addr = x.get('address') - if addr == None: continue - if addr == "(pubkey)": - prevout_hash = x.get('prevout_hash') - prevout_n = x.get('prevout_n') - _addr = self.wallet.find_pay_to_pubkey_address(prevout_hash, prevout_n) - if _addr: - addr = _addr - input_addresses.append(addr) - for addr, v in tx.get_outputs(): - output_addresses.append(addr) - out.append({ - 'txid': tx_hash, - 'timestamp': timestamp, - 'date': date, - 'input_addresses': input_addresses, - 'output_addresses': output_addresses, - 'label': label, - 'value': str(Decimal(value)/COIN) if value is not None else None, - 'height': height, - 'confirmations': conf - }) - return out + kwargs = {'show_addresses': show_addresses} + if year: + import time + start_date = datetime.datetime(year, 1, 1) + end_date = datetime.datetime(year+1, 1, 1) + kwargs['from_timestamp'] = time.mktime(start_date.timetuple()) + kwargs['to_timestamp'] = time.mktime(end_date.timetuple()) + return self.wallet.export_history(**kwargs) @command('w') def setlabel(self, key, label): @@ -736,6 +706,9 @@ command_options = { 'pending': (None, "Show only pending requests."), 'expired': (None, "Show only expired requests."), 'paid': (None, "Show only paid requests."), + 'show_addresses': (None, "Show input and output addresses"), + 'show_fiat': (None, "Show fiat value of transactions"), + 'year': (None, "Show history for a given year"), } @@ -746,6 +719,7 @@ arg_types = { 'num': int, 'nbits': int, 'imax': int, + 'year': int, 'entropy': int, 'tx': tx_from_str, 'pubkeys': json_loads, diff --git a/lib/wallet.py b/lib/wallet.py index 6a682deb..4c655015 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -914,6 +914,57 @@ class Abstract_Wallet(PrintError): return h2 + def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False): + from decimal import Decimal + from .util import format_time, format_satoshis, timestamp_to_datetime + h = self.get_history(domain) + out = [] + for tx_hash, height, conf, timestamp, value, balance in h: + if from_timestamp and timestamp < from_timestamp: + continue + if to_timestamp and timestamp >= to_timestamp: + continue + item = { + 'txid':tx_hash, + 'height':height, + 'confirmations':conf, + 'timestamp':timestamp, + 'value': format_satoshis(value, True) if value is not None else '--', + 'balance': format_satoshis(balance) + } + if item['height']>0: + date_str = format_time(timestamp) if timestamp is not None else _("unverified") + else: + date_str = _("unconfirmed") + item['date'] = date_str + item['label'] = self.get_label(tx_hash) + if show_addresses: + tx = self.transactions.get(tx_hash) + tx.deserialize() + input_addresses = [] + output_addresses = [] + for x in tx.inputs(): + if x['type'] == 'coinbase': continue + addr = x.get('address') + if addr == None: continue + if addr == "(pubkey)": + prevout_hash = x.get('prevout_hash') + prevout_n = x.get('prevout_n') + _addr = self.wallet.find_pay_to_pubkey_address(prevout_hash, prevout_n) + if _addr: + addr = _addr + input_addresses.append(addr) + for addr, v in tx.get_outputs(): + output_addresses.append(addr) + item['input_addresses'] = input_addresses + item['output_addresses'] = output_addresses + if fx is not None: + date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp) + item['fiat_value'] = fx.historical_value_str(value, date) + item['fiat_balance'] = fx.historical_value_str(balance, date) + out.append(item) + return out + def get_label(self, tx_hash): label = self.labels.get(tx_hash, '') if label is '': From 42a16d9c3e19785ad39f272517aa2ce54b9e0f7f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 6 Jan 2018 12:57:04 +0100 Subject: [PATCH 10/91] computation of capital gains for outgoing transactions --- gui/qt/history_list.py | 5 +++++ lib/exchange_rate.py | 5 +++++ lib/wallet.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 63a0b4b9..4c809031 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -62,6 +62,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): fx = self.parent.fx if fx and fx.show_history(): headers.extend(['%s '%fx.ccy + _('Amount'), '%s '%fx.ccy + _('Balance')]) + headers.extend(['%s '%fx.ccy + _('Capital Gains')]) self.update_headers(headers) def get_domain(self): @@ -91,6 +92,10 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): for amount in [value, balance]: text = fx.historical_value_str(amount, date) entry.append(text) + # fixme: should use is_mine + if value < 0: + cg = self.wallet.capital_gain(tx_hash, self.parent.fx.timestamp_rate) + entry.append("%.2f"%cg if cg is not None else _('No data')) item = QTreeWidgetItem(entry) item.setIcon(0, icon) item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else "")) diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index e6a56247..5003721d 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -493,3 +493,8 @@ class FxThread(ThreadJob): def historical_value_str(self, satoshis, d_t): rate = self.history_rate(d_t) return self.value_str(satoshis, rate) + + def timestamp_rate(self, timestamp): + from electrum.util import timestamp_to_datetime + date = timestamp_to_datetime(timestamp) + return self.history_rate(date) diff --git a/lib/wallet.py b/lib/wallet.py index d7b80a5c..96a3b760 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -962,6 +962,8 @@ class Abstract_Wallet(PrintError): date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp) item['fiat_value'] = fx.historical_value_str(value, date) item['fiat_balance'] = fx.historical_value_str(balance, date) + if value < 0: + item['capital_gain'] = self.capital_gain(tx_hash, fx.timestamp_rate) out.append(item) return out @@ -1586,6 +1588,46 @@ class Abstract_Wallet(PrintError): children |= self.get_depending_transactions(other_hash) return children + def txin_value(self, txin): + txid = txin['prevout_hash'] + prev_n = txin['prevout_n'] + for address, d in self.txo[txid].items(): + for n, v, cb in d: + if n == prev_n: + return v + raise BaseException('unknown txin value') + + def capital_gain(self, txid, price_func): + """ + Difference between the fiat price of coins leaving the wallet because of transaction txid, + and the price of these coins when they entered the wallet. + price_func: function that returns the fiat price given a timestamp + """ + height, conf, timestamp = self.get_tx_height(txid) + tx = self.transactions[txid] + out_value = sum([ (value if not self.is_mine(address) else 0) for otype, address, value in tx.outputs() ]) + try: + return out_value/1e8 * (price_func(timestamp) - self.average_price(tx, price_func)) + except: + return None + + def average_price(self, tx, price_func): + """ average price of the inputs of a transaction """ + return sum(self.coin_price(txin, price_func) * self.txin_value(txin) for txin in tx.inputs()) / sum(self.txin_value(txin) for txin in tx.inputs()) + + def coin_price(self, coin, price_func): + """ fiat price of acquisition of coin """ + txid = coin['prevout_hash'] + tx = self.transactions[txid] + if all([self.is_mine(txin['address']) for txin in tx.inputs()]): + return self.average_price(tx, price_func) + elif all([ not self.is_mine(txin['address']) for txin in tx.inputs()]): + height, conf, timestamp = self.get_tx_height(txid) + return price_func(timestamp) + else: + # could be some coinjoin transaction.. + return None + class Simple_Wallet(Abstract_Wallet): # wallet with a single keystore From 0df42fe046029fca9c92c20037a4f823a1709244 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 10 Feb 2018 15:03:45 +0100 Subject: [PATCH 11/91] use Decimal for exchange rates --- lib/exchange_rate.py | 2 +- lib/wallet.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index 5003721d..fbd8658e 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -488,7 +488,7 @@ class FxThread(ThreadJob): if rate is None and (datetime.today().date() - d_t.date()).days <= 2: rate = self.exchange.quotes.get(self.ccy) self.history_used_spot = True - return rate + return Decimal(rate) if rate is not None else None def historical_value_str(self, satoshis, d_t): rate = self.history_rate(d_t) diff --git a/lib/wallet.py b/lib/wallet.py index 96a3b760..6568aa9b 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -38,6 +38,7 @@ import traceback from functools import partial from collections import defaultdict from numbers import Number +from decimal import Decimal import sys @@ -915,7 +916,6 @@ class Abstract_Wallet(PrintError): return h2 def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False): - from decimal import Decimal from .util import format_time, format_satoshis, timestamp_to_datetime h = self.get_history(domain) out = [] @@ -1607,7 +1607,7 @@ class Abstract_Wallet(PrintError): tx = self.transactions[txid] out_value = sum([ (value if not self.is_mine(address) else 0) for otype, address, value in tx.outputs() ]) try: - return out_value/1e8 * (price_func(timestamp) - self.average_price(tx, price_func)) + return out_value/Decimal(COIN) * (price_func(timestamp) - self.average_price(tx, price_func)) except: return None From 264e80a7b731b15ac6ce38c90343aef70bfac751 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 10 Feb 2018 14:38:06 +0100 Subject: [PATCH 12/91] cache historical exchange rates --- lib/exchange_rate.py | 50 +++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index fbd8658e..83bce0d4 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -2,6 +2,8 @@ from datetime import datetime import inspect import requests import sys +import os +import json from threading import Thread import time import csv @@ -59,19 +61,34 @@ class ExchangeBase(PrintError): t.setDaemon(True) t.start() - def get_historical_rates_safe(self, ccy): - try: - self.print_error("requesting fx history for", ccy) - self.history[ccy] = self.historical_rates(ccy) - self.print_error("received fx history for", ccy) - self.on_history() - except BaseException as e: - self.print_error("failed fx history:", e) + def get_historical_rates_safe(self, ccy, cache_dir): + filename = os.path.join(cache_dir, self.name() + '_'+ ccy) + if os.path.exists(filename) and (time.time() - os.stat(filename).st_mtime) < 24*3600: + try: + with open(filename, 'r') as f: + h = json.loads(f.read()) + except: + h = None + else: + h = None + if h is None: + try: + self.print_error("requesting fx history for", ccy) + h = self.request_history(ccy) + self.print_error("received fx history for", ccy) + self.on_history() + except BaseException as e: + self.print_error("failed fx history:", e) + return + with open(filename, 'w') as f: + f.write(json.dumps(h)) + self.history[ccy] = h + self.on_history() - def get_historical_rates(self, ccy): + def get_historical_rates(self, ccy, cache_dir): result = self.history.get(ccy) if not result and ccy in self.history_ccys(): - t = Thread(target=self.get_historical_rates_safe, args=(ccy,)) + t = Thread(target=self.get_historical_rates_safe, args=(ccy, cache_dir)) t.setDaemon(True) t.start() return result @@ -99,7 +116,7 @@ class BitcoinAverage(ExchangeBase): 'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD', 'ZAR'] - def historical_rates(self, ccy): + def request_history(self, ccy): history = self.get_csv('apiv2.bitcoinaverage.com', "/indices/global/history/BTC%s?period=alltime&format=csv" % ccy) return dict([(h['DateTime'][:10], h['Average']) @@ -127,7 +144,7 @@ class BitcoinVenezuela(ExchangeBase): def history_ccys(self): return ['ARS', 'EUR', 'USD', 'VEF'] - def historical_rates(self, ccy): + def request_history(self, ccy): return self.get_json('api.bitcoinvenezuela.com', "/historical/index.php?coin=BTC")[ccy +'_BTC'] @@ -216,7 +233,7 @@ class CoinDesk(ExchangeBase): def history_ccys(self): return self.history_starts().keys() - def historical_rates(self, ccy): + def request_history(self, ccy): start = self.history_starts()[ccy] end = datetime.today().strftime('%Y-%m-%d') # Note ?currency and ?index don't work as documented. Sigh. @@ -314,7 +331,7 @@ class Winkdex(ExchangeBase): def history_ccys(self): return ['USD'] - def historical_rates(self, ccy): + def request_history(self, ccy): json = self.get_json('winkdex.com', "/api/v0/series?start_time=1342915200") history = json['series'][0]['results'] @@ -381,6 +398,9 @@ class FxThread(ThreadJob): self.ccy_combo = None self.hist_checkbox = None self.set_exchange(self.config_exchange()) + self.cache_dir = os.path.join(config.path, 'cache') + if not os.path.exists(self.cache_dir): + os.mkdir(self.cache_dir) def get_currencies(self, h): d = get_exchanges_by_ccy(h) @@ -403,7 +423,7 @@ class FxThread(ThreadJob): # This runs from the plugins thread which catches exceptions if self.is_enabled(): if self.timeout ==0 and self.show_history(): - self.exchange.get_historical_rates(self.ccy) + self.exchange.get_historical_rates(self.ccy, self.cache_dir) if self.timeout <= time.time(): self.timeout = time.time() + 150 self.exchange.update(self.ccy) From 4cc2575d7276a117010a7ac48e637e24515f9b12 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 10 Feb 2018 19:18:48 +0100 Subject: [PATCH 13/91] cli support for hw encrypted wallets --- electrum | 58 +++++++++++++++++++++----- lib/commands.py | 2 + plugins/digitalbitbox/cmdline.py | 3 ++ plugins/digitalbitbox/digitalbitbox.py | 3 +- plugins/keepkey/cmdline.py | 3 ++ plugins/ledger/cmdline.py | 3 ++ plugins/ledger/ledger.py | 3 +- plugins/trezor/cmdline.py | 3 ++ 8 files changed, 66 insertions(+), 12 deletions(-) diff --git a/electrum b/electrum index 25495f79..0e109e8e 100755 --- a/electrum +++ b/electrum @@ -91,7 +91,7 @@ if is_local or is_android: from electrum import bitcoin, util from electrum import SimpleConfig, Network from electrum.wallet import Wallet, Imported_Wallet -from electrum.storage import WalletStorage +from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption from electrum.util import print_msg, print_stderr, json_encode, json_decode from electrum.util import set_verbosity, InvalidPassword from electrum.commands import get_parser, known_commands, Commands, config_variables @@ -194,8 +194,9 @@ def init_daemon(config_options): sys.exit(0) if storage.is_encrypted(): if storage.is_encrypted_with_hw_device(): - raise NotImplementedError("CLI functionality of encrypted hw wallets") - if config.get('password'): + plugins = init_plugins(config, 'cmdline') + password = get_password_for_hw_device_encrypted_storage(plugins) + elif config.get('password'): password = config.get('password') else: password = prompt_password('Password:', False) @@ -222,7 +223,7 @@ def init_cmdline(config_options, server): if cmdname in ['payto', 'paytomany'] and config.get('broadcast'): cmd.requires_network = True - # instanciate wallet for command-line + # instantiate wallet for command-line storage = WalletStorage(config.get_wallet_path()) if cmd.requires_wallet and not storage.file_exists(): @@ -240,8 +241,9 @@ def init_cmdline(config_options, server): if (cmd.requires_wallet and storage.is_encrypted() and server is None)\ or (cmd.requires_password and (storage.get('use_encryption') or storage.is_encrypted())): if storage.is_encrypted_with_hw_device(): - raise NotImplementedError("CLI functionality of encrypted hw wallets") - if config.get('password'): + # this case is handled later in the control flow + password = None + elif config.get('password'): password = config.get('password') else: password = prompt_password('Password:', False) @@ -260,7 +262,42 @@ def init_cmdline(config_options, server): return cmd, password -def run_offline_command(config, config_options): +def get_connected_hw_devices(plugins): + support = plugins.get_hardware_support() + if not support: + print_msg('No hardware wallet support found on your system.') + sys.exit(1) + # scan devices + devices = [] + devmgr = plugins.device_manager + for name, description, plugin in support: + try: + u = devmgr.unpaired_device_infos(None, plugin) + except: + devmgr.print_error("error", name) + continue + devices += list(map(lambda x: (name, x), u)) + return devices + + +def get_password_for_hw_device_encrypted_storage(plugins): + devices = get_connected_hw_devices(plugins) + if len(devices) == 0: + print_msg("Error: No connected hw device found. Can not decrypt this wallet.") + sys.exit(1) + elif len(devices) > 1: + print_msg("Warning: multiple hardware devices detected. " + "The first one will be used to decrypt the wallet.") + # FIXME we use the "first" device, in case of multiple ones + name, device_info = devices[0] + plugin = plugins.get_plugin(name) + derivation = get_derivation_used_for_hw_device_encryption() + xpub = plugin.get_xpub(device_info.device.id_, derivation, 'standard', plugin.handler) + password = keystore.Xpub.get_pubkey_from_xpub(xpub, ()) + return password + + +def run_offline_command(config, config_options, plugins): cmdname = config.get('cmd') cmd = known_commands[cmdname] password = config_options.get('password') @@ -268,7 +305,8 @@ def run_offline_command(config, config_options): storage = WalletStorage(config.get_wallet_path()) if storage.is_encrypted(): if storage.is_encrypted_with_hw_device(): - raise NotImplementedError("CLI functionality of encrypted hw wallets") + password = get_password_for_hw_device_encrypted_storage(plugins) + config_options['password'] = password storage.decrypt(password) wallet = Wallet(storage) else: @@ -437,8 +475,8 @@ if __name__ == '__main__': print_msg("Daemon not running; try 'electrum daemon start'") sys.exit(1) else: - init_plugins(config, 'cmdline') - result = run_offline_command(config, config_options) + plugins = init_plugins(config, 'cmdline') + result = run_offline_command(config, config_options, plugins) # print result if isinstance(result, str): print_msg(result) diff --git a/lib/commands.py b/lib/commands.py index 29bfd8b6..f2230394 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -138,6 +138,8 @@ class Commands: @command('wp') def password(self, password=None, new_password=None): """Change wallet password. """ + if self.wallet.storage.is_encrypted_with_hw_device() and new_password: + raise Exception("Can't change the password of a wallet encrypted with a hw device.") b = self.wallet.storage.is_encrypted() self.wallet.update_password(password, new_password, b) self.wallet.storage.write() diff --git a/plugins/digitalbitbox/cmdline.py b/plugins/digitalbitbox/cmdline.py index 7902c98a..82192cfd 100644 --- a/plugins/digitalbitbox/cmdline.py +++ b/plugins/digitalbitbox/cmdline.py @@ -9,3 +9,6 @@ class Plugin(DigitalBitboxPlugin): if not isinstance(keystore, self.keystore_class): return keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/plugins/digitalbitbox/digitalbitbox.py b/plugins/digitalbitbox/digitalbitbox.py index c57e395d..2f63fda4 100644 --- a/plugins/digitalbitbox/digitalbitbox.py +++ b/plugins/digitalbitbox/digitalbitbox.py @@ -661,7 +661,8 @@ class DigitalBitboxPlugin(HW_PluginBase): def create_client(self, device, handler): if device.interface_number == 0 or device.usage_page == 0xffff: - self.handler = handler + if handler: + self.handler = handler client = self.get_dbb_device(device) if client is not None: client = DigitalBitbox_Client(self, client) diff --git a/plugins/keepkey/cmdline.py b/plugins/keepkey/cmdline.py index cd30bc0c..4262b701 100644 --- a/plugins/keepkey/cmdline.py +++ b/plugins/keepkey/cmdline.py @@ -9,3 +9,6 @@ class Plugin(KeepKeyPlugin): if not isinstance(keystore, self.keystore_class): return keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/plugins/ledger/cmdline.py b/plugins/ledger/cmdline.py index b0b252ac..5d8c9f46 100644 --- a/plugins/ledger/cmdline.py +++ b/plugins/ledger/cmdline.py @@ -9,3 +9,6 @@ class Plugin(LedgerPlugin): if not isinstance(keystore, self.keystore_class): return keystore.handler = self.handler + + def create_handler(self, window): + return self.handler diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index 9bab6034..3d34bc9d 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -516,7 +516,8 @@ class LedgerPlugin(HW_PluginBase): return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG) def create_client(self, device, handler): - self.handler = handler + if handler: + self.handler = handler client = self.get_btchip_device(device) if client is not None: diff --git a/plugins/trezor/cmdline.py b/plugins/trezor/cmdline.py index 9149eeee..630578ac 100644 --- a/plugins/trezor/cmdline.py +++ b/plugins/trezor/cmdline.py @@ -9,3 +9,6 @@ class Plugin(TrezorPlugin): if not isinstance(keystore, self.keystore_class): return keystore.handler = self.handler + + def create_handler(self, window): + return self.handler From f8df8d60c44b40b63bbeb5dbd61fbfbdb1abd7b4 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Sun, 11 Feb 2018 15:28:01 +0100 Subject: [PATCH 14/91] Add my public key --- pubkeys/bauerj.asc | 166 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 pubkeys/bauerj.asc diff --git a/pubkeys/bauerj.asc b/pubkeys/bauerj.asc new file mode 100644 index 00000000..b50bed1b --- /dev/null +++ b/pubkeys/bauerj.asc @@ -0,0 +1,166 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFRL5aABCACgnvbQOPgPeyBolejlFaY279tVUWaBeFEYQ17xfI3xo87Ywb7E +DOq1xsQx6RNGOiriKFWyM41S8lcIu7fOAtfkilWiqUCoapn7bQlDyTl7LPKOQgNA +txIKibKyfmDJ1xyMAcyF8kV+Gav3JgucpBlYjmTdNC3MvI/6MGd4GdxG1l/4aGLc +1xV9a38RvjZnDD0HOfyUGbqE1dY5nEVla0sgMp1h7mSyBebjLkOareidXJxK5N7v +o+/yFidN2BiyKSQLzpftx4OIJx2hWfaTRbn+l1WF35Bu6iYhBtsvrZFZBK1bjc/A +xHTu15kJsS+GuP3v8qH/QB5fcGah44QjM7FdABEBAAG0IEpvaGFubiBCYXVlciA8 +YmF1ZXJqQHR2NHVzZXIuZGU+iQE/BBMBAgApBQJUS/v2AhsDBQkB4MKABwsJCAcD +AgEGFQgCCQoLBBYCAwECHgECF4AACgkQhPG/klsfSE2JAQf7BE7GHWifVHMjiciN +bvS0SQ/hx33hn42Yd/jwYsXsIBuJcJ/81s0sq+O/JRXrhZxSrOx4ekKQ+8tQURvw +42MAXN8QTp9lXno3jPvyTHPLlmW3Ig1wQ31Kh5daKv/dmRTrsgP2aBH0YRLQ28Qr +gRiCEK8Ea1ujoUq6PzmmcRB3waKJm1eIUwEj1iP2rFB5MV+ESDfKXTyUiDpRRma1 +bgj4mKv6vDO0839Ho3tLyGnRYksCcS3XUqYU1nhsROzW+91YWQiD8zfTmnQ+q/t6 +VxXW9aRgq9EY8KZUy7I94f5ETRokhszOxxdv5zZRTKpWyKUt1e8zeLss2krUtJzl +T3GWtokBPwQTAQIAKQIbAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheABQJWlU5z +BQkEKpxPAAoJEITxv5JbH0hNycIH+wYbhniOrfrmWhgyjWKFqvdhNA9Z1t6DPAqJ +Di4Ow4GBEp6N4RmRrv6WateG/Mva+Fy1x/Rj6PgrJti+9CZUuvrlhCJ3SPQN6Ajr +cwih0QyiFAPRXZ8FVOds93GUKyMy4SzLU/d/OOJ/0MxPCjbWnz6J+0snwzYAykuL +WeB3PIeq3n97MM2XRSDMY3a5/6XpKBK+JPb95MwMbSeh6czqp1Xa96S2iW14Wa/v +4shHXwBgC32Sk6CUu4qidi+w2eGK/tVWRKAffONULFB7cT5sFgm1l4gScxH4GrBH +SsZWilFckkUXxxogh/FY5i60FJ58rLdGntZ8x7sO5lcdHTy5Uo6JAT8EEwECACkC +GwMHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAUCWKW/OgUJCBxAjAAKCRCE8b+S +Wx9ITZGmB/9CPtyBSOv9hMhf3NouFrrIZfVHW3RDvr0zPtF7Z1JQdQzccXMdboyc +m9kAP4OzkG2uRhJtaTvGuiCd/B9X7xsbI2JkQo67rgQiesByZIuBHwugg/nmGerM +vpApTqljTqd3yVxy68377mFRd2DU9byCyghPGyFMS8RAo5lMEEpk4kicfjSL75la +9W4MAcHM1HZ1h0roqN3Nxwhn4RsD6ssOiGEO4LQUhzsaU4LSYk1OjHb2zvd7UHsV +RNRLlSsj66y7nLuQFcJX0/YyqHWwhyUTKDRN24ifpCO3/HlD4PmO84FdF35b21DG +SE5ZOywtpPSqP6R3gF1qxvSXFLxI7nePiQE/BBMBAgApAhsDBwsJCAcDAgEGFQgC +CQoLBBYCAwECHgECF4AFAlilwkUFCQgcQ5gACgkQhPG/klsfSE0b8Qf+KBY+HW+z +lvZbEzsZ9s/4Er/0InGSHWD8o9K1V2M2woThXlbiZZjvnJQaEzXXjvgdqd2BhAp4 +fPwcd28ww7mVBycDMqffGq4M1xKzwXSXC8oSC+zqP5po7cFppYZi0QnwATtJDdS1 +qBOCx4r6+TXndMP8wlXOAIYVPFPgvsAICOhBfFz/BPx7V/gEWj03TC6P4+chbPfW +B9bFKUUlsW7IqM5nps9GHs/jkCArb29f2UiKEbMSlPzB30uHxqw1cma9CPvYjpXu +5Rnw+nIThBdOhuTcAqBwgBRwI4StMAd2mBEeCUJ8OrR/tQ7BDHXWdgNrQJdybeS1 +tuEwSDm6f52vHbQgSm9oYW5uIEJhdWVyIDxqaG5uLmJyQGdtYWlsLmNvbT6JAT8E +EwECACkFAlRL8mcCGwMFCQHgwoAHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAK +CRCE8b+SWx9ITYN9B/sHBt/PZ26zsHYu+b8mLGENm7lw2jYgYsde03NWf+dT7a8p +W5c1rt2ENmG2N68a8+aAgMxcn8ZJsXOF/APMmRbHfpHdshGTUMBs2wYaizlAwjYv +nerBfSOvWSZpk7VqI2/+0Q+sYn5w1MjRu60upyEGQVM+ZIftwwrp0FolJdkDgihM +zXcJuwxCSscqF6NsVukSxo1A5gKjJ1V9jvcXi4yUaYhfSw/hUSAjHo4hXeXbJNuA +aBjLiTq+QMQ7d9dAflZCAvd+KsG3BBXuG8IQIz+OxTtdDnFvQQxTPzlcIq5KHI7O +6IdXC+T7Fmf9x0h6QkhFuVS6OB81E0I000d2TMcViQE/BBMBAgApAhsDBwsJCAcD +AgEGFQgCCQoLBBYCAwECHgECF4AFAlaVTnMFCQQqnE8ACgkQhPG/klsfSE177AgA +hUXVzFWHpUXJbsMsdzuZ9d9ts72+NUY/0ilNaL3t6X1GFvKfTDxuc72ivP2W6Eo4 +aYWAHBYQb5a7SphvrknQetIwCM7ll5LZFlvkff0xb8DjLSLfVj4BBiT7N4pBJRsl +2VQoqhdcul+EilXb7bYcPQGIU0ZK2epBbm8VfO0hetQtb4DxT6viuSOmkntMcgHG +7zSgvhOkyZHjlw3sMqAr999xyV0hZRE3vUEHeO3f9L/nZ0msLpLrfKvczKrlHkNI +IHzG80Tm5JzmVtmnc3nVGbskqZgTLgR8sIdNdTBN9j6I03wwvve8BqNaeh3W6I3P +xgVgWxwF7ULLutld6z4mGokBPwQTAQIAKQIbAwcLCQgHAwIBBhUIAgkKCwQWAgMB +Ah4BAheABQJYpb86BQkIHECMAAoJEITxv5JbH0hNBxIH/jCr/qpjflwuWAIojmLQ +i/HOZTssUym36zseOW0BN0pMdbqrinrzSXxrn7C+Yzf/1EZTy1bgE3tI0fmcPOJS +dOCIIqeuMbF3uZ82imYg3aX1t4eaGF2/hnJWn8W054FCmR8iRO0/Ge8bPT8ZO79Z +pvZzY1w31qnOVIflFNJla0+fXhi+2Bys6WpvEdAo6PfUh775RE2bRGO7i08nyJUP +3fLuuWiF7rIrO14lCTBkwBYQUEfN2JbIFfckFJBieZPyirB+EHdHJG3qMZCeefee +o8vkSIX4NfLkHB5qXkdYYwBKlXuVTXwZpD2FyIAuKRcbWJgJ8Uw0sLSRyYDXdlKz +lgSJAT8EEwECACkCGwMHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAUCWKXCRQUJ +CBxDmAAKCRCE8b+SWx9ITQtfB/4gzmhMaFp3RE1Swel2G5dMbgfnU+RkutdHWUtN +QPZFzRE7aKDY5dNXU/NyjNgiD9EIrJwgalXo7m9TCBR4jwLqdFwLSQ1IgPNGoyRj +x6IVudLX2apzR2ZDnJCFaJKNxxLH9pIouORk30XsBVPRSyVYJJaksdR8nyae3jNl +LNgHTb9P+mMuMBErrFf9tEWOb4hqO52zTnKCeMdMneL7r1ZZthJhk4nKV7FUWjwZ ++8HEIhiJo2HgTUqdQlgJ+NKQw/FnO4XIJp+97eKD38W3rFjYKLH+gx+a6Ftxn2Hz +rcwKvn59/P3BbkaS+m48nROy2lOIzolNGel8L60OkIAkX8EHtB9Kb2hhbm4gQmF1 +ZXIgPGJhdWVyakBiYXVlcmouZXU+iQE/BBMBAgApAhsDBwsJCAcDAgEGFQgCCQoL +BBYCAwECHgECF4AFAlRL5gMFCQHgwoAACgkQhPG/klsfSE0DBggAkVZPbh84VxGs +lLqhj6FLOJFEP52TPbmNWhKe3C5KT+tWawuBQDcnlmyly9A+fVcW8BE5JnAn/Q+q +bwBZUZCF2tqgR0SHL3f1GOrpwWJ3VbCCodoeG/UFa3XSW9C1klre0m9vISl/NB4L +ga/ILmXy9Y7M4igHGgzxEGdn0jo9X9o0tp3iPwLlO5nAZwL74YlH5ay1e7RsZQ/0 +RJDvrATd9Fuqog5vXFq4xJay9p8/KsMMMeJwh11BsN48DDW/JytB1juTGoTAG4UT +0N8KFOsfKdEuEFJddyQAtS6ZtHKmmDDubYoAHPW0zXzkUXTFNM53xkjJOl0LwVPt +Z/7u7TU7sIkBPwQTAQIAKQIbAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheABQJW +lU5zBQkEKpxPAAoJEITxv5JbH0hNPcEIAJwDysT3uBCsaoVQvxJB4HOussnvz8hA +xuvB/GoUMF5lg9WUpImNM0iUEoCFWtYUBspPhP6XdVOHOwAUINqJTi+tEQZgRJvv +PD3Y+oXhIV9SzXhVRzPvkRhcU6VVQKd7DqDyZceGGn6CRahRMdDhDWZuEBjb/Std +Ov04GDwNYWSwpz+iU3pP5Ab2dT6oDrxKCLogu3LV2TuhTXypvOhTeFpspfGRacyf +bcVezL+kHT/EbWVp/qZnh5v4AdqxYQulzW3JWzWt2LTdPDO4AsE+2UAse2vyPgGP +//69RXfvrVoW9gilmP5sLuozP1AZ4KnFwOvTrv8BP/sSzUJumUdChR+JAT8EEwEC +ACkCGwMHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAUCWKW/OgUJCBxAjAAKCRCE +8b+SWx9ITdmHB/44opEJEEEboquNtYHiyjcvU6PI1jJPIRocE93klBDHfo91UbE3 +NwDp0TfeS6ooje+8Q+nWcTb19EdL+kDLRIj2i8O6amQ4p42ypd/6A04C0MJHM4Mw +9zamihy25+ORtl25BG+qhF57jWn+r828TGgx3PWQbdenacjXm4bkyb7f67HkaEAD +aiB1D0U2lrBaKoVYc4qTSC8mgcdh7hSB6iBMPsuqtriGqTeFsRs3Kl/P3IfWtbdN +VAE9Le5dcllAX0OORolXgvQBBVRWz0LcqdRitRIevcZ902P4Jl4trMq4bel0Spqy +PqNcn+Cswq95nSyLTEwlb+shK1vDs5icNiFriQE/BBMBAgApAhsDBwsJCAcDAgEG +FQgCCQoLBBYCAwECHgECF4AFAlilwkUFCQgcQ5gACgkQhPG/klsfSE10Sgf+JPsZ +5/dW/sDx+W3G0rfRU8PKiKgxvkmm7U4R9UuF1FoPv1iMrBMh/sdOeEwD6A6kZFmB +rXgb+gPToc8Vavmo9QumbNVW6msj403H0oReGxxbbQ++XimTGrGQjLsjGIdmDWJm +o1sZC1bVHMlRUEyaCRtBc5wJUGdo+m6zE6308XiSg9EcFw6ZQo15imevmiSdGSQ3 +ovlA9aJe878bJRy7MbilsDabXeasvUtCZ02zu46VfkbdlH5oDP/tKY2FdinVOED2 +94r2JJUid0chDb2FQW6cZ1WzidBfmJmwUKyMDx/Igmu4pNcYxt5q9KLuvoRMBbRg +ylmG9Uyo0r8dXZCgObQqSm9oYW5uIEJhdWVyIDxqb2hhbm4uYmF1ZXJAdW5pLXJv +c3RvY2suZGU+iQE/BBMBAgApBQJUS/JAAhsDBQkB4MKABwsJCAcDAgEGFQgCCQoL +BBYCAwECHgECF4AACgkQhPG/klsfSE2+dQf9GmR7T30orDcptqjVA+63hiNR8RKi +jJXRi8VsvX0gKacJ3E9o6MBMGWMuJAQ/oR2YYzS8T3vUbtLuvEOq3lkedyu032XO +vDwCuEzs751Y/6YR2mitats3ze7Ey280hqYbq+NjZthFe1Ezr//ZsDYeOBhRGB/Z +SBt7uhVmwc/17AbdrS5xJb+a2VmC5DdYTeR0bdE4A0TRKNQ/9kt9SIQ4aJ8b0ueh +8tXO8PgFUlsvO07N/k9UkAkwWC8kd3FTVNZt5zabRUoy98ygOIiL3YlfIjaBK2xp +n3DF5KRsmKmDtBXKs929KCgAolV8QjMJuZLe+UdynXA35E0gyUDT1j+hhYkBPwQT +AQIAKQIbAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheABQJWlU5zBQkEKpxPAAoJ +EITxv5JbH0hN2LQH/0nJgXlfI1YAf+mD5JmY4FThzcnud2PpYuIUAZ5bzgMp9KGC +idiuHa0m6HCGvZiQPJ+MEfVfZN0zvysrJhoo5uk6slf9hIaKgWQxaCSkw1pGj+2F +8Qbg9Lx49Be04DKnk8C9KCqzA2vpaD3p6aiXYJ05FB7b19GxT4v0FAQNmI3tR9fu +wrxMK/kl3lQok+I8fwVeWIvwia+DLJJa+Pf2bOrQginXPOrSr/Ysw0ZOJoDvrtXm +I/RVGQnR3kJW29wJXIeQzwFFgjHI3qC9jiQqij6SCgaunGKrfdZ56qe7SwfXcXlp +4C+FmA4tvPHwMHnrb9jXJutY1ECL3darU9QX5iGJAT8EEwECACkCGwMHCwkIBwMC +AQYVCAIJCgsEFgIDAQIeAQIXgAUCWKW/OgUJCBxAjAAKCRCE8b+SWx9ITcAxB/9/ +Zc52sOSeyoyITBJlz2uCXcpvBuQBN7GoVEDmQEP7EBYBy3o5xs7TFbep6dVamzIF +bp0V1TcW8aKk37Jac2WVbpdfBTu44AdLAYuOnLVuSu6sTGGct3tK41Op72RXXVYN +1l8JAFXpHtP32z4t6tq3Tc6Rgr4G2aozYQjOzbgmBcPeZRSz5ubMTIsTDaVZILku +YT8fwvBbRiiOoYfVThWlJxWtz7Xs23TFKwVdBYDyKQWQyvBnpIPKusd+GIjIAR4a ++P1Wujsxu88Mruhxp1iSB1gnbN7hum0MOu/ncEg4r2locX3133LU6t7fbAmleZ66 +uyYofllRyxY3FJrdBtsziQE/BBMBAgApAhsDBwsJCAcDAgEGFQgCCQoLBBYCAwEC +HgECF4AFAlilwkUFCQgcQ5gACgkQhPG/klsfSE34hQf/SPzpAjbpghUnPvYgUsRI +AuQbGZANBgSBNj6K65RNNCz78M/eUdNSqyx/n/wMPLewNW1aJzZDV533ADzckvd5 +l1qfsE6iJlQlTwjlfirmVJ3eKYAS/7gn6Yrked7KjKMzL7E0Ytz6idzSXkDPyPWb +Nl06Q70sU+kEKSEP5Q1W0u3BUOU3t0v4GsMeWK/OlIMUOxoEpj1sVnUFT8RtZBKp +Q9VKZTdOX3TBeEx9O9NjbjTt62SSB1WCH34d0o2GAYLJOEhFNKt92lzaygytfOAt +FY/TBJl/gnqY7CzMFtKgUHttrz98XdI2ze+GqZ2KRMCTfhWStAnwkxgMK0X++jIF +EbQfSm9oYW5uIEJhdWVyIDxiYXVlcmpAYmF1ZXJqLmRlPokBPwQTAQIAKQUCVEwi +OwIbAwUJAeDCgAcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJEITxv5JbH0hN +5OoH/0kWbdL7R4sznsrstkU+Z1Gi795M6tzk/1/oSkR8j9tf4B8RX2bSs6tVmHQP +ByTVdKV48b16//k4MmznziJuQmjs8rJvMsxKleD6UTncH0DNzYUxpxhsAGj9ekf9 +UB7uRtQ00DuK+6z+aqfbBh2FgnxtpQrpsLbHvW9WI2DX0zvKmec3WlrhU4lsVwBp +RWUyvAv++PB5ivkm4TBea10nVAy1RvLeBqPolniAW3nE+pTljQeMOMK0L5sDuMvA +fiIiBAjMq1WUGirRmZDWRbgzD86BaVnY3+IB8pCjnG/uxX3lrpz5n+hYYeNt6q5h +P3zixFFrA3W1+h/hBGBZtDV4iAiJAT8EEwECACkCGwMHCwkIBwMCAQYVCAIJCgsE +FgIDAQIeAQIXgAUCVpVObwUJBCqcTwAKCRCE8b+SWx9ITXIqB/oD66hPC7m2g7NA +cAe4sEp0qplr723lhn7fcJ3mBvCHUxUl01lQoKCSGIQX1ilVgd+xjPytPRhUy1Rr +O1z0pldDyJfVazYP7VSq8qwbYNcAeU/efVuE57hlQJ1mlhJ+h3j1qkYL0k9pf23m +Js1amiGb2FO7d0MSClERno4gJJ/BWSa47ZTtM/YJfvp2CV5mOD+LseEsCP4U+Uzd +ONP0mTV4WgX0jdH5kAl7PvXb3g2n72kWuRV3QrTF1PV+3Et1BJinhGU3+YJb4/OB +LnF0cufGiL8DR6A13pbskaFRBxqZs7x90E0lpAqGIz2Z/hy5KnqATUTF/TeDG0zg +goqxX9fxiQE/BBMBAgApAhsDBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AFAlil +vzoFCQgcQIwACgkQhPG/klsfSE2ViAf/a+Ayp4MdDT6zfRIt7RbAx4bdpYe3pWU6 +0jH3b4UJ36LtmqukPvoQzhfQBazwPPmOxnvo4Ias0XTgCx8lbNmLl9tlRbxYvgNx +Nk6/Wtz6h/y9i2TPtzDe9xmeH9/nK0HvaDxWfFTp94LfJqlpYLwpalK6uC7uczh0 +kEl6Y/3pYuEtXb/hk6XjiZWj73gKkrienktHj9lQBsfph8Jjuweym7zRacZaycd0 +CiDOWBStHvq1gDqy1lggne7OPRhWN2Ttp+gEmkSboL1dV+7BDvBhzZ1efhE/DSfk ++2BR8MROCgaAGA8FoZvxlwfKJBCLygCmXUG1pcCvxbmcgN7OK+iw6okBPwQTAQIA +KQIbAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheABQJYpcJFBQkIHEOYAAoJEITx +v5JbH0hNer0H/j1XV3GiMAzbEQje2oGWss381CJyVnqJFVklpssUgNjRikfbnj4w +06H4BCg3c5rzxVTd4aK4hyWWGH8tHwVhN7tfxLzr3OxnZOI8ftujvdwyOwHSXGJ7 +oad1Nsov7glzHFhkzBCjY1U9UERQ3T+9u+SJOZkhyTsipUIK7JPI0r4r2/A07jsJ +Aj09yREC+Jz+sdtXrEWo+dz1ewamHPkha3HHfkgnw4yWRQ3BxRoxb5xaotlzOuVD +z47oB8Y33FxdpXYZikajTZBPeX28zHjI5FPmkQBQ97sbyZTw5rg59Wg1A1gXV/jQ +N//Q34fhExbcLyeVv4drUkFL5mDXYzFCB/u5AQ0EVEvloAEIAK/PFf19cxUVxu6a +F5GXTqZXvhEszCWfurhPEiloSpoaH0aH31oFgi58KmivH2tworyUG8PeIBOcoUGm +QUFJrXPsnNu3hdFIEkI2eeT1FBezF+newY0S3oOQG5aISgzLu7r3vvbY4JW3AUFA +gVVwJmatBplNPrnoLwG+Nn8oBtOdMMvkOOaHnW3z62I4JLwCnFRG2eDDFYCWsxh5 +Ekh0DgJEdYGXSKIsHPm+UD/18WNG78C8zC9GyUmbsZ3zibc6GmdW3Sh08lNdraAR +S3V6Ty2aKXq6jdi682ehKzAeSvqtr0LEUPsmD5s6g2PhfXCX0Dc/9czmaGPVs05Z +X/3Y/skAEQEAAYkBHwQYAQIACQUCVEvloAIbDAAKCRCE8b+SWx9ITffQB/9Q5AMw +ElZu2g0cE5tfhh0dydN5D9Z3T892lYG3R2EQ/puCrLV8xg9R1/Oe3LYvpxavAeKQ +afmj8BIHYzuGYwMmNRRQEOGTlkisQlFmuAVgPniOf2AEgjwly0Me4eib7CHVIEP+ +tHTU7FzcVw4PPl3PbHKyPNi7MF/LL68xaJthIgzKCQkl7vGkChHJFRwphFinNHAZ +57und85/CMrDMK6/BHAkI+ShwxVGgZIwzOq9pKbaBUVeNWhvAQWl1JBRh+e/CCJT +9hnJJGKUTdUMjIDNfH9mEFEYkAYMH+SATTwTDumdS8ixmMVaSX3E1zblogE3NO2P +T2vtGNK2jhXLDcGeiQElBBgBAgAPAhsMBQJYpcJTBQkIHEOwAAoJEITxv5JbH0hN +mUMH/2roD8oBNjQrhzkT2N0amWa8Wlg0Kyc1qbkEdi57b9PVEAuTmR6AGzIlLcJG +7s8qZHMdyY/Rg62aJkJ+ma1YNF7cK4ALVW0LUjXNiyfTnUSBgwx/QobtMUcE3K+z +4DRLa4QYE28qaweNAA7VKeHSzC9G86BnxGIKvZolRASPW6hwDiUZfHLLdt6jLVwf +b/b7f/2fLQDzQmxm/nwMN+qLAkv/4+vhcKDcMNfAhz5DmuAAg3OrkZEghX54troN +tpb9QxdWdhrgTZ6OocAloqc5aFOsTY5CFqmc5lQupMsVzpXhqLiYA2OXRbh7eQIA +402TZWn+BlhGAFxa+Wzl46MVavI= +=bDjo +-----END PGP PUBLIC KEY BLOCK----- From afa0168e1427fa0849ef219d596e1e91477b4e28 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Sun, 11 Feb 2018 16:27:01 +0100 Subject: [PATCH 15/91] Add new requirements file for binaries in contrib --- .travis.yml | 2 +- contrib/build-osx/make_osx | 2 +- contrib/build-wine/prepare-wine.sh | 19 +++-------- .../requirements-binaries.txt | 5 +++ contrib/freeze_packages.sh | 33 +++++-------------- .../requirements/requirements-binaries.txt | 3 ++ .../requirements/requirements-hw.txt | 0 .../requirements/requirements-travis.txt | 0 .../requirements/requirements.txt | 0 setup.py | 6 ++-- 10 files changed, 25 insertions(+), 45 deletions(-) create mode 100644 contrib/deterministic-build/requirements-binaries.txt create mode 100644 contrib/requirements/requirements-binaries.txt rename requirements-hw.txt => contrib/requirements/requirements-hw.txt (100%) rename requirements_travis.txt => contrib/requirements/requirements-travis.txt (100%) rename requirements.txt => contrib/requirements/requirements.txt (100%) diff --git a/.travis.yml b/.travis.yml index 48355183..38a0acf8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - 3.5 - 3.6 install: - - pip install -r requirements_travis.txt + - pip install -r contrib/requirements/requirements-travis.txt cache: - pip script: diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx index d8af8c9d..ac265a7f 100755 --- a/contrib/build-osx/make_osx +++ b/contrib/build-osx/make_osx @@ -58,7 +58,7 @@ cp /tmp/electrum-build/electrum-icons/icons_rc.py ./gui/qt/ info "Installing requirements..." python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \ -python3 -m pip install pyqt5 --user || \ +python3 -m pip install -Ir ./contrib/deterministic-build/requirements-binaries.txt --user || \ fail "Could not install requirements" info "Installing hardware wallet requirements..." diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index ad26fca1..42828be7 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -73,27 +73,17 @@ done $PYTHON -m pip install pip --upgrade # Install pywin32-ctypes (needed by pyinstaller) -$PYTHON -m pip install pywin32-ctypes +$PYTHON -m pip install pywin32-ctypes==0.1.2 -# Install PyQt -$PYTHON -m pip install PyQt5 - -## Install pyinstaller -#$PYTHON -m pip install pyinstaller==3.3 +# install PySocks +$PYTHON -m pip install win_inet_pton==1.0.1 +$PYTHON -m pip install -r ../../deterministic-build/requirements-binaries.txt # Install ZBar #wget -q -O zbar.exe "https://sourceforge.net/projects/zbar/files/zbar/0.10/zbar-0.10-setup.exe/download" #wine zbar.exe -# install Cryptodome -$PYTHON -m pip install pycryptodomex - -# install PySocks -$PYTHON -m pip install win_inet_pton - -# install websocket (python2) -$PYTHON -m pip install websocket-client # Upgrade setuptools (so Electrum can be installed later) $PYTHON -m pip install setuptools --upgrade @@ -111,5 +101,4 @@ wine nsis.exe /S # add dlls needed for pyinstaller: cp $WINEPREFIX/drive_c/python$PYTHON_VERSION/Lib/site-packages/PyQt5/Qt/bin/* $WINEPREFIX/drive_c/python$PYTHON_VERSION/ - echo "Wine is configured. Please run prepare-pyinstaller.sh" diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt new file mode 100644 index 00000000..af90b89d --- /dev/null +++ b/contrib/deterministic-build/requirements-binaries.txt @@ -0,0 +1,5 @@ +pycryptodomex==3.4.12 +PyQt5==5.9 +sip==4.19.7 +six==1.11.0 +websocket-client==0.46.0 \ No newline at end of file diff --git a/contrib/freeze_packages.sh b/contrib/freeze_packages.sh index 2d6b0375..3471e528 100755 --- a/contrib/freeze_packages.sh +++ b/contrib/freeze_packages.sh @@ -6,34 +6,17 @@ contrib=$(dirname "$0") which virtualenv > /dev/null 2>&1 || { echo "Please install virtualenv" && exit 1; } -# standard Electrum dependencies +for i in '' '-hw' '-binaries'; do + rm "$venv_dir" -rf + virtualenv -p $(which python3) $venv_dir -rm "$venv_dir" -rf -virtualenv -p $(which python3) $venv_dir + source $venv_dir/bin/activate -source $venv_dir/bin/activate + echo "Installing $i dependencies" -echo "Installing main dependencies" - -pushd $contrib/.. -python setup.py install -popd - -pip freeze | sed '/^Electrum/ d' > $contrib/deterministic-build/requirements.txt - - -# hw wallet library dependencies - -rm "$venv_dir" -rf -virtualenv -p $(which python3) $venv_dir - -source $venv_dir/bin/activate - -echo "Installing hw wallet dependencies" - -python -m pip install -r $contrib/../requirements-hw.txt --upgrade - -pip freeze | sed '/^Electrum/ d' > $contrib/deterministic-build/requirements-hw.txt + python -m pip install -r $contrib/requirements/requirements${i}.txt --upgrade + pip freeze | sed '/^Electrum/ d' > $contrib/deterministic-build/requirements${i}.txt +done echo "Done. Updated requirements" diff --git a/contrib/requirements/requirements-binaries.txt b/contrib/requirements/requirements-binaries.txt new file mode 100644 index 00000000..68181dd3 --- /dev/null +++ b/contrib/requirements/requirements-binaries.txt @@ -0,0 +1,3 @@ +PyQt5 +pycryptodomex +websocket-client \ No newline at end of file diff --git a/requirements-hw.txt b/contrib/requirements/requirements-hw.txt similarity index 100% rename from requirements-hw.txt rename to contrib/requirements/requirements-hw.txt diff --git a/requirements_travis.txt b/contrib/requirements/requirements-travis.txt similarity index 100% rename from requirements_travis.txt rename to contrib/requirements/requirements-travis.txt diff --git a/requirements.txt b/contrib/requirements/requirements.txt similarity index 100% rename from requirements.txt rename to contrib/requirements/requirements.txt diff --git a/setup.py b/setup.py index 80330b1c..63581a61 100755 --- a/setup.py +++ b/setup.py @@ -9,10 +9,10 @@ import platform import imp import argparse -with open('requirements.txt') as f: +with open('contrib/requirements/requirements.txt') as f: requirements = f.read().splitlines() -with open('requirements-hw.txt') as f: +with open('contrib/requirements/requirements-hw.txt') as f: requirements_hw = f.read().splitlines() version = imp.load_source('version', 'lib/version.py') @@ -20,7 +20,7 @@ version = imp.load_source('version', 'lib/version.py') if sys.version_info[:3] < (3, 4, 0): sys.exit("Error: Electrum requires Python version >= 3.4.0...") -data_files = ['requirements.txt', 'requirements-hw.txt'] +data_files = ['contrib/requirements/' + r for r in ['requirements.txt', 'requirements-hw.txt']] if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']: parser = argparse.ArgumentParser() From 4cbdd25c93eb25be18cebdb99085921d046277d2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 11 Feb 2018 17:26:13 +0100 Subject: [PATCH 16/91] Capital gains: Let user enter fiat value of transactions. --- gui/qt/history_list.py | 34 ++++++++++++++++++---- lib/wallet.py | 65 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 4c809031..12471c0e 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -63,6 +63,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): if fx and fx.show_history(): headers.extend(['%s '%fx.ccy + _('Amount'), '%s '%fx.ccy + _('Balance')]) headers.extend(['%s '%fx.ccy + _('Capital Gains')]) + self.editable_columns.extend([6]) self.update_headers(headers) def get_domain(self): @@ -87,14 +88,20 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): balance_str = self.parent.format_amount(balance, whitespaces=True) label = self.wallet.get_label(tx_hash) entry = ['', tx_hash, status_str, label, v_str, balance_str] + fiat_value = None if fx and fx.show_history(): date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp) - for amount in [value, balance]: - text = fx.historical_value_str(amount, date) - entry.append(text) + fiat_value = self.wallet.get_fiat_value(tx_hash, fx.ccy) + if not fiat_value: + value_str = fx.historical_value_str(value, date) + else: + value_str = str(fiat_value) + entry.append(value_str) + balance_str = fx.historical_value_str(balance, date) + entry.append(balance_str) # fixme: should use is_mine if value < 0: - cg = self.wallet.capital_gain(tx_hash, self.parent.fx.timestamp_rate) + cg = self.wallet.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy) entry.append("%.2f"%cg if cg is not None else _('No data')) item = QTreeWidgetItem(entry) item.setIcon(0, icon) @@ -109,12 +116,27 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): if value and value < 0: item.setForeground(3, QBrush(QColor("#BC1E1E"))) item.setForeground(4, QBrush(QColor("#BC1E1E"))) + if fiat_value: + item.setForeground(6, QBrush(QColor("#1E1EFF"))) if tx_hash: item.setData(0, Qt.UserRole, tx_hash) self.insertTopLevelItem(0, item) if current_tx == tx_hash: self.setCurrentItem(item) + def on_edited(self, item, column, prior): + '''Called only when the text actually changes''' + key = item.data(0, Qt.UserRole) + text = item.text(column) + # fixme + if column == 3: + self.parent.wallet.set_label(key, text) + self.update_labels() + self.parent.update_completions() + elif column == 6: + self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text) + self.on_update() + def on_doubleclick(self, item, column): if self.permit_edit(item, column): super(HistoryList, self).on_doubleclick(item, column) @@ -170,8 +192,8 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) - if column in self.editable_columns: - menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column)) + for c in self.editable_columns: + menu.addAction(_("Edit {}").format(self.headerItem().text(c)), lambda: self.editItem(item, c)) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) diff --git a/lib/wallet.py b/lib/wallet.py index 6568aa9b..9e673e90 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -185,6 +185,7 @@ class Abstract_Wallet(PrintError): self.labels = storage.get('labels', {}) self.frozen_addresses = set(storage.get('frozen_addresses',[])) self.history = storage.get('addr_history',{}) # address -> list(txid, height) + self.fiat_value = storage.get('fiat_value', {}) self.load_keystore() self.load_addresses() @@ -342,13 +343,37 @@ class Abstract_Wallet(PrintError): if old_text: self.labels.pop(name) changed = True - if changed: run_hook('set_label', self, name, text) self.storage.put('labels', self.labels) - return changed + def set_fiat_value(self, txid, ccy, text): + if txid not in self.transactions: + return + if not text: + d = self.fiat_value.get(ccy, {}) + if d and txid in d: + d.pop(txid) + else: + return + else: + try: + Decimal(text) + except: + return + if ccy not in self.fiat_value: + self.fiat_value[ccy] = {} + self.fiat_value[ccy][txid] = text + self.storage.put('fiat_value', self.fiat_value) + + def get_fiat_value(self, txid, ccy): + fiat_value = self.fiat_value.get(ccy, {}).get(txid) + try: + return Decimal(fiat_value) + except: + return + def is_mine(self, address): return address in self.get_addresses() @@ -1597,33 +1622,49 @@ class Abstract_Wallet(PrintError): return v raise BaseException('unknown txin value') - def capital_gain(self, txid, price_func): + def price_at_timestamp(self, txid, price_func): + height, conf, timestamp = self.get_tx_height(txid) + return price_func(timestamp) + + def capital_gain(self, txid, price_func, ccy): """ Difference between the fiat price of coins leaving the wallet because of transaction txid, and the price of these coins when they entered the wallet. price_func: function that returns the fiat price given a timestamp """ - height, conf, timestamp = self.get_tx_height(txid) tx = self.transactions[txid] - out_value = sum([ (value if not self.is_mine(address) else 0) for otype, address, value in tx.outputs() ]) + ir, im, v, fee = self.get_wallet_delta(tx) + out_value = -v + fiat_value = self.get_fiat_value(txid, ccy) + if fiat_value is None: + p = self.price_at_timestamp(txid, price_func) + liquidation_price = None if p is None else out_value/Decimal(COIN) * p + else: + liquidation_price = - fiat_value + try: - return out_value/Decimal(COIN) * (price_func(timestamp) - self.average_price(tx, price_func)) + return liquidation_price - out_value/Decimal(COIN) * self.average_price(tx, price_func, ccy) except: return None - def average_price(self, tx, price_func): + def average_price(self, tx, price_func, ccy): """ average price of the inputs of a transaction """ - return sum(self.coin_price(txin, price_func) * self.txin_value(txin) for txin in tx.inputs()) / sum(self.txin_value(txin) for txin in tx.inputs()) + input_value = sum(self.txin_value(txin) for txin in tx.inputs()) / Decimal(COIN) + total_price = sum(self.coin_price(txin, price_func, ccy, self.txin_value(txin)) for txin in tx.inputs()) + return total_price / input_value - def coin_price(self, coin, price_func): + def coin_price(self, coin, price_func, ccy, txin_value): """ fiat price of acquisition of coin """ txid = coin['prevout_hash'] tx = self.transactions[txid] if all([self.is_mine(txin['address']) for txin in tx.inputs()]): - return self.average_price(tx, price_func) + return self.average_price(tx, price_func, ccy) * txin_value/Decimal(COIN) elif all([ not self.is_mine(txin['address']) for txin in tx.inputs()]): - height, conf, timestamp = self.get_tx_height(txid) - return price_func(timestamp) + fiat_value = self.get_fiat_value(txid, ccy) + if fiat_value is not None: + return fiat_value + else: + return self.price_at_timestamp(txid, price_func) * txin_value/Decimal(COIN) else: # could be some coinjoin transaction.. return None From cc55d78b7c84a3c63d4d5246a1b1d72cfced61aa Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 11 Feb 2018 19:14:47 +0100 Subject: [PATCH 17/91] capital gains: update release notes --- RELEASE-NOTES | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 6413c9e2..ddadee13 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -29,6 +29,17 @@ * Watching-only wallets and hardware wallets can be encrypted. * Semi-automated crash reporting * The SSL checkbox option was removed from the GUI. + * Capital gains: For each outgoing transaction, the difference + between the acquisition and liquidation prices of outgoing coins is + displayed in the wallet history. By default, historical exchange + rates are used to compute acquisition and liquidation prices. These + value can also be entered manually, in order to match the actual + price realized by the user. The order of liquidation of coins is + the natural order defined by the blockchain; this results in + capital gain values that are invariant to changes in the set of + addresses that are in the wallet. Any other ordering strategy (such + as FIFO, LIFO) would result in capital gain values that depend on + the set of addresses in the wallet. # Release 3.0.6 : From 2914090879d011100c100d96311f8204d39a86c4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 12 Feb 2018 16:12:16 +0100 Subject: [PATCH 18/91] wallet.synchronize: remove dead code --- lib/wallet.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 9e673e90..f68de646 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -322,6 +322,9 @@ class Abstract_Wallet(PrintError): def synchronize(self): pass + def is_deterministic(self): + return self.keystore.is_deterministic() + def set_up_to_date(self, up_to_date): with self.lock: self.up_to_date = up_to_date @@ -1883,9 +1886,6 @@ class Deterministic_Wallet(Abstract_Wallet): def has_seed(self): return self.keystore.has_seed() - def is_deterministic(self): - return self.keystore.is_deterministic() - def get_receiving_addresses(self): return self.receiving_addresses @@ -1971,16 +1971,8 @@ class Deterministic_Wallet(Abstract_Wallet): def synchronize(self): with self.lock: - if self.is_deterministic(): - self.synchronize_sequence(False) - self.synchronize_sequence(True) - else: - if len(self.receiving_addresses) != len(self.keystore.keypairs): - pubkeys = self.keystore.keypairs.keys() - self.receiving_addresses = [self.pubkeys_to_address(i) for i in pubkeys] - self.save_addresses() - for addr in self.receiving_addresses: - self.add_address(addr) + self.synchronize_sequence(False) + self.synchronize_sequence(True) def is_beyond_limit(self, address): is_change, i = self.get_address_index(address) From 3bfaaad77420f081e7b9a9e5d3aead99d5da6ddd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 12 Feb 2018 17:50:59 +0100 Subject: [PATCH 19/91] kivy: address filter "all" follow-up of #3841 --- gui/kivy/uix/screens.py | 7 ++++++- gui/kivy/uix/ui_screens/address.kv | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index ed1471d5..0133f789 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -522,7 +522,12 @@ class AddressScreen(CScreen): def update(self): self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)] wallet = self.app.wallet - _list = wallet.get_change_addresses() if self.screen.show_change else wallet.get_receiving_addresses() + if self.screen.show_change == 0: + _list = wallet.get_receiving_addresses() + elif self.screen.show_change == 1: + _list = wallet.get_change_addresses() + else: + _list = wallet.get_addresses() search = self.screen.message container = self.screen.ids.search_container container.clear_widgets() diff --git a/gui/kivy/uix/ui_screens/address.kv b/gui/kivy/uix/ui_screens/address.kv index 3d594c9c..d0247a34 100644 --- a/gui/kivy/uix/ui_screens/address.kv +++ b/gui/kivy/uix/ui_screens/address.kv @@ -50,7 +50,7 @@ AddressScreen: name: 'address' message: '' pr_status: 'Pending' - show_change: False + show_change: 0 show_used: 0 on_message: self.parent.update() @@ -70,9 +70,9 @@ AddressScreen: spacing: '5dp' AddressButton: id: search - text: _('Change') if root.show_change else _('Receiving') + text: {0:_('Receiving'), 1:_('Change'), 2:_('All')}[root.show_change] on_release: - root.show_change = not root.show_change + root.show_change = (root.show_change + 1) % 3 Clock.schedule_once(lambda dt: app.address_screen.update()) AddressFilter: opacity: 1 From 7e77baf4fb00096c5d2469a28a22b882b0723bf0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 12 Feb 2018 23:20:58 +0100 Subject: [PATCH 20/91] fix #3890 --- lib/wallet.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index f68de646..3a26b413 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -208,7 +208,7 @@ class Abstract_Wallet(PrintError): self.up_to_date = False # locks: if you need to take multiple ones, acquire them in the order they are defined here! - self.lock = threading.Lock() + self.lock = threading.RLock() self.transaction_lock = threading.RLock() self.check_history() @@ -1947,15 +1947,16 @@ class Deterministic_Wallet(Abstract_Wallet): def create_new_address(self, for_change=False): assert type(for_change) is bool - addr_list = self.change_addresses if for_change else self.receiving_addresses - n = len(addr_list) - x = self.derive_pubkeys(for_change, n) - address = self.pubkeys_to_address(x) - addr_list.append(address) - self._addr_to_addr_index[address] = (for_change, n) - self.save_addresses() - self.add_address(address) - return address + with self.lock: + addr_list = self.change_addresses if for_change else self.receiving_addresses + n = len(addr_list) + x = self.derive_pubkeys(for_change, n) + address = self.pubkeys_to_address(x) + addr_list.append(address) + self._addr_to_addr_index[address] = (for_change, n) + self.save_addresses() + self.add_address(address) + return address def synchronize_sequence(self, for_change): limit = self.gap_limit_for_change if for_change else self.gap_limit From 240ecee6ced32623a277f475ce7aeaab77713824 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Mon, 12 Feb 2018 23:31:14 +0100 Subject: [PATCH 21/91] macOS build: Prefer our pyinstaller over system installed --- contrib/build-osx/make_osx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx index ac265a7f..f0c7bc05 100755 --- a/contrib/build-osx/make_osx +++ b/contrib/build-osx/make_osx @@ -20,7 +20,7 @@ PYTHON_VERSION=3.6.4 info "Installing Python $PYTHON_VERSION" -export PATH="~/.pyenv/bin:~/.pyenv/shims:$PATH:~/Library/Python/3.6/bin" +export PATH="~/.pyenv/bin:~/.pyenv/shims:~/Library/Python/3.6/bin:$PATH" if [ -d "~/.pyenv" ]; then pyenv update else From 476ce3f1dbd6b8ce87b8db45f2630d357d55225e Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Mon, 12 Feb 2018 23:40:01 +0100 Subject: [PATCH 22/91] Follow-up 240ecee We don't care if some other pyinstaller is installed --- contrib/build-osx/make_osx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx index f0c7bc05..0626e9fd 100755 --- a/contrib/build-osx/make_osx +++ b/contrib/build-osx/make_osx @@ -31,10 +31,8 @@ pyenv global $PYTHON_VERSION || \ fail "Unable to use Python $PYTHON_VERSION" -if ! which pyinstaller > /dev/null; then - info "Installing pyinstaller" - python3 -m pip install pyinstaller -I --user || fail "Could not install pyinstaller" -fi +info "Installing pyinstaller" +python3 -m pip install pyinstaller -I --user || fail "Could not install pyinstaller" info "Using these versions for building Electrum:" sw_vers From 15f7e09131dea249008ca671cd750d39d2ebcccd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 13 Feb 2018 00:03:42 +0100 Subject: [PATCH 23/91] use config.is_dynfee and config.use_mempool_fees also fixes #3894 --- gui/kivy/uix/dialogs/bump_fee_dialog.py | 2 +- gui/kivy/uix/dialogs/fee_dialog.py | 4 ++-- gui/qt/fee_slider.py | 6 +++--- gui/qt/main_window.py | 4 ++-- lib/simple_config.py | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gui/kivy/uix/dialogs/bump_fee_dialog.py b/gui/kivy/uix/dialogs/bump_fee_dialog.py index a5c74cee..1a6dc622 100644 --- a/gui/kivy/uix/dialogs/bump_fee_dialog.py +++ b/gui/kivy/uix/dialogs/bump_fee_dialog.py @@ -73,7 +73,7 @@ class BumpFeeDialog(Factory.Popup): self.callback = callback self.config = app.electrum_config self.fee_step = self.config.max_fee_rate() / 10 - self.dynfees = self.config.get('dynamic_fees', True) and self.app.network + self.dynfees = self.config.is_dynfee() and self.app.network self.ids.old_fee.value = self.app.format_amount_and_units(self.init_fee) self.update_slider() self.update_text() diff --git a/gui/kivy/uix/dialogs/fee_dialog.py b/gui/kivy/uix/dialogs/fee_dialog.py index 25e9926c..1c61c6a2 100644 --- a/gui/kivy/uix/dialogs/fee_dialog.py +++ b/gui/kivy/uix/dialogs/fee_dialog.py @@ -78,8 +78,8 @@ class FeeDialog(Factory.Popup): self.config = config self.fee_rate = self.config.fee_per_kb() self.callback = callback - self.mempool = self.config.get('mempool_fees', False) - self.dynfees = self.config.get('dynamic_fees', True) + self.mempool = self.config.use_mempool_fees() + self.dynfees = self.config.is_dynfee() self.ids.mempool.active = self.mempool self.ids.dynfees.active = self.dynfees self.update_slider() diff --git a/gui/qt/fee_slider.py b/gui/qt/fee_slider.py index 209b0de7..04911d87 100644 --- a/gui/qt/fee_slider.py +++ b/gui/qt/fee_slider.py @@ -21,7 +21,7 @@ class FeeSlider(QSlider): def moved(self, pos): with self.lock: if self.dyn: - fee_rate = self.config.depth_to_fee(pos) if self.config.get('mempool_fees') else self.config.eta_to_fee(pos) + fee_rate = self.config.depth_to_fee(pos) if self.config.use_mempool_fees() else self.config.eta_to_fee(pos) else: fee_rate = self.config.static_fee(pos) tooltip = self.get_tooltip(pos, fee_rate) @@ -30,7 +30,7 @@ class FeeSlider(QSlider): self.callback(self.dyn, pos, fee_rate) def get_tooltip(self, pos, fee_rate): - mempool = self.config.get('mempool_fees') + mempool = self.config.use_mempool_fees() target, estimate = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate) if self.dyn: return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate @@ -40,7 +40,7 @@ class FeeSlider(QSlider): def update(self): with self.lock: self.dyn = self.config.is_dynfee() - mempool = self.config.get('mempool_fees') + mempool = self.config.use_mempool_fees() maxp, pos, fee_rate = self.config.get_fee_slider(self.dyn, mempool) self.setRange(0, maxp) self.setValue(pos) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 62fb8f05..df86e3e8 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1081,7 +1081,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def fee_cb(dyn, pos, fee_rate): if dyn: - if self.config.get('mempool_fees'): + if self.config.use_mempool_fees(): self.config.set_key('depth_level', pos, False) else: self.config.set_key('fee_level', pos, False) @@ -2669,7 +2669,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): fee_type_label = HelpLabel(_('Fee estimation') + ':', msg) fee_type_combo = QComboBox() fee_type_combo.addItems([_('Time based'), _('Mempool based')]) - fee_type_combo.setCurrentIndex(1 if self.config.get('mempool_fees') else 0) + fee_type_combo.setCurrentIndex(1 if self.config.use_mempool_fees() else 0) def on_fee_type(x): self.config.set_key('mempool_fees', x==1) self.fee_slider.update() diff --git a/lib/simple_config.py b/lib/simple_config.py index c0bbb216..072edccd 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -324,7 +324,7 @@ class SimpleConfig(PrintError): def get_fee_status(self): dyn = self.is_dynfee() - mempool = self.get('mempool_fees') + mempool = self.use_mempool_fees() pos = self.get_depth_level() if mempool else self.get_fee_level() fee_rate = self.fee_per_kb() target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate) @@ -395,10 +395,10 @@ class SimpleConfig(PrintError): return bool(self.mempool_fees) def is_dynfee(self): - return self.get('dynamic_fees', True) + return bool(self.get('dynamic_fees', True)) def use_mempool_fees(self): - return self.get('mempool_fees', False) + return bool(self.get('mempool_fees', False)) def fee_per_kb(self): """Returns sat/kvB fee to pay for a txn. From 2829de5d49460cfdc68a8e4eb371ecdfda0e2b3d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 13 Feb 2018 09:47:25 +0100 Subject: [PATCH 24/91] fix: missing parameter --- lib/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index 9e673e90..454e018c 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -988,7 +988,7 @@ class Abstract_Wallet(PrintError): item['fiat_value'] = fx.historical_value_str(value, date) item['fiat_balance'] = fx.historical_value_str(balance, date) if value < 0: - item['capital_gain'] = self.capital_gain(tx_hash, fx.timestamp_rate) + item['capital_gain'] = self.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy) out.append(item) return out From 14714899691c84291c642765310af95f3969323e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 13 Feb 2018 09:48:05 +0100 Subject: [PATCH 25/91] fix: value can be None --- gui/qt/history_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 12471c0e..dcb56614 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -100,7 +100,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): balance_str = fx.historical_value_str(balance, date) entry.append(balance_str) # fixme: should use is_mine - if value < 0: + if value is not None and value < 0: cg = self.wallet.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy) entry.append("%.2f"%cg if cg is not None else _('No data')) item = QTreeWidgetItem(entry) From cc19de9db3f3a64f5234c47b1b3cc2ac17f03c34 Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Tue, 13 Feb 2018 18:01:11 +0800 Subject: [PATCH 26/91] Parameterise the OSX builder --- contrib/build-osx/make_osx | 23 ++++++++++++++--------- contrib/build-osx/osx.spec | 35 ++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx index 0626e9fd..70793499 100755 --- a/contrib/build-osx/make_osx +++ b/contrib/build-osx/make_osx @@ -16,7 +16,12 @@ cd $build_dir/../.. export PYTHONHASHSEED=22 VERSION=`git describe --tags` + +# Paramterize PYTHON_VERSION=3.6.4 +BUILDDIR=/tmp/electrum-build +PACKAGE=Electrum +GIT_REPO=https://github.com/spesmilo/electrum info "Installing Python $PYTHON_VERSION" @@ -34,7 +39,7 @@ fail "Unable to use Python $PYTHON_VERSION" info "Installing pyinstaller" python3 -m pip install pyinstaller -I --user || fail "Could not install pyinstaller" -info "Using these versions for building Electrum:" +info "Using these versions for building $PACKAGE:" sw_vers python3 --version echo -n "Pyinstaller " @@ -43,16 +48,16 @@ pyinstaller --version rm -rf ./dist -rm -rf /tmp/electrum-build > /dev/null 2>&1 -mkdir /tmp/electrum-build +rm -rf $BUILDDIR > /dev/null 2>&1 +mkdir $BUILDDIR info "Downloading icons and locale..." for repo in icons locale; do - git clone https://github.com/spesmilo/electrum-$repo /tmp/electrum-build/electrum-$repo + git clone $GIT_REPO-$repo $BUILDDIR/electrum-$repo done -cp -R /tmp/electrum-build/electrum-locale/locale/ ./lib/locale/ -cp /tmp/electrum-build/electrum-icons/icons_rc.py ./gui/qt/ +cp -R $BUILDDIR/electrum-locale/locale/ ./lib/locale/ +cp $BUILDDIR/electrum-icons/icons_rc.py ./gui/qt/ info "Installing requirements..." python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \ @@ -63,11 +68,11 @@ info "Installing hardware wallet requirements..." python3 -m pip install -Ir ./contrib/deterministic-build/requirements-hw.txt --user || \ fail "Could not install hardware wallet requirements" -info "Building Electrum..." -python3 setup.py install --user > /dev/null || fail "Could not build Electrum" +info "Building $PACKAGE..." +python3 setup.py install --user > /dev/null || fail "Could not build $PACKAGE" info "Building binary" pyinstaller --noconfirm --ascii --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary" info "Creating .DMG" -hdiutil create -fs HFS+ -volname "Electrum" -srcfolder dist/Electrum.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG" +hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG" diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec index cfce7172..2efda9f1 100644 --- a/contrib/build-osx/osx.spec +++ b/contrib/build-osx/osx.spec @@ -5,6 +5,11 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules import sys import os +PACKAGE='Electrum' +PYPKG='electrum' +MAIN_SCRIPT='electrum' +ICONS_FILE='electrum.icns' + for i, x in enumerate(sys.argv): if x == '--name': VERSION = sys.argv[i+1] @@ -22,21 +27,21 @@ hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('keepkeylib') datas = [ - (electrum+'lib/currencies.json', 'electrum'), - (electrum+'lib/servers.json', 'electrum'), - (electrum+'lib/checkpoints.json', 'electrum'), - (electrum+'lib/servers_testnet.json', 'electrum'), - (electrum+'lib/checkpoints_testnet.json', 'electrum'), - (electrum+'lib/wordlist/english.txt', 'electrum/wordlist'), - (electrum+'lib/locale', 'electrum/locale'), - (electrum+'plugins', 'electrum_plugins'), + (electrum+'lib/currencies.json', PYPKG), + (electrum+'lib/servers.json', PYPKG), + (electrum+'lib/checkpoints.json', PYPKG), + (electrum+'lib/servers_testnet.json', PYPKG), + (electrum+'lib/checkpoints_testnet.json', PYPKG), + (electrum+'lib/wordlist/english.txt', PYPKG + '/wordlist'), + (electrum+'lib/locale', PYPKG + '/locale'), + (electrum+'plugins', PYPKG + '_plugins'), ] datas += collect_data_files('trezorlib') datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports -a = Analysis([electrum+'electrum', +a = Analysis([electrum+MAIN_SCRIPT, electrum+'gui/qt/main_window.py', electrum+'gui/text.py', electrum+'lib/util.py', @@ -58,7 +63,7 @@ a = Analysis([electrum+'electrum', # http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal for d in a.datas: - if 'pyconfig' in d[0]: + if 'pyconfig' in d[0]: a.datas.remove(d) break @@ -68,19 +73,19 @@ exe = EXE(pyz, a.scripts, a.binaries, a.datas, - name='Electrum', + name=PACKAGE, debug=False, strip=False, upx=True, - icon=electrum+'electrum.icns', + icon=electrum+ICONS_FILE, console=False) app = BUNDLE(exe, version = VERSION, - name='Electrum.app', - icon=electrum+'electrum.icns', + name=PACKAGE + '.app', + icon=electrum+ICONS_FILE, bundle_identifier=None, info_plist = { 'NSHighResolutionCapable':'True' } -) \ No newline at end of file +) From ea66333e488a24969be3b5b3dac681fb6e5cf66a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 13 Feb 2018 16:45:41 +0100 Subject: [PATCH 27/91] bip32 version numbers (xpub headers): use t/u/U/v/V for testnet --- lib/bitcoin.py | 58 ++++++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 84339755..8b9f7967 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -47,28 +47,6 @@ def read_json(filename, default): return r - - -# Version numbers for BIP32 extended keys -# standard: xprv, xpub -# segwit in p2sh: yprv, ypub -# native segwit: zprv, zpub -XPRV_HEADERS = { - 'standard': 0x0488ade4, - 'p2wpkh-p2sh': 0x049d7878, - 'p2wsh-p2sh': 0x295b005, - 'p2wpkh': 0x4b2430c, - 'p2wsh': 0x2aa7a99 -} -XPUB_HEADERS = { - 'standard': 0x0488b21e, - 'p2wpkh-p2sh': 0x049d7cb2, - 'p2wsh-p2sh': 0x295b43f, - 'p2wpkh': 0x4b24746, - 'p2wsh': 0x2aa7ed3 -} - - class NetworkConstants: @classmethod @@ -83,6 +61,21 @@ class NetworkConstants: cls.DEFAULT_SERVERS = read_json('servers.json', {}) cls.CHECKPOINTS = read_json('checkpoints.json', []) + cls.XPRV_HEADERS = { + 'standard': 0x0488ade4, # xprv + 'p2wpkh-p2sh': 0x049d7878, # yprv + 'p2wsh-p2sh': 0x0295b005, # Yprv + 'p2wpkh': 0x04b2430c, # zprv + 'p2wsh': 0x02aa7a99, # Zprv + } + cls.XPUB_HEADERS = { + 'standard': 0x0488b21e, # xpub + 'p2wpkh-p2sh': 0x049d7cb2, # ypub + 'p2wsh-p2sh': 0x0295b43f, # Ypub + 'p2wpkh': 0x04b24746, # zpub + 'p2wsh': 0x02aa7ed3, # Zpub + } + @classmethod def set_testnet(cls): cls.TESTNET = True @@ -95,6 +88,21 @@ class NetworkConstants: cls.DEFAULT_SERVERS = read_json('servers_testnet.json', {}) cls.CHECKPOINTS = read_json('checkpoints_testnet.json', []) + cls.XPRV_HEADERS = { + 'standard': 0x04358394, # tprv + 'p2wpkh-p2sh': 0x044a4e28, # uprv + 'p2wsh-p2sh': 0x024285b5, # Uprv + 'p2wpkh': 0x045f18bc, # vprv + 'p2wsh': 0x02575048, # Vprv + } + cls.XPUB_HEADERS = { + 'standard': 0x043587cf, # tpub + 'p2wpkh-p2sh': 0x044a5262, # upub + 'p2wsh-p2sh': 0x024285ef, # Upub + 'p2wpkh': 0x045f1cf6, # vpub + 'p2wsh': 0x02575483, # Vpub + } + NetworkConstants.set_mainnet() @@ -893,11 +901,11 @@ def _CKD_pub(cK, c, s): def xprv_header(xtype): - return bfh("%08x" % XPRV_HEADERS[xtype]) + return bfh("%08x" % NetworkConstants.XPRV_HEADERS[xtype]) def xpub_header(xtype): - return bfh("%08x" % XPUB_HEADERS[xtype]) + return bfh("%08x" % NetworkConstants.XPUB_HEADERS[xtype]) def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4): @@ -919,7 +927,7 @@ def deserialize_xkey(xkey, prv): child_number = xkey[9:13] c = xkey[13:13+32] header = int('0x' + bh2u(xkey[0:4]), 16) - headers = XPRV_HEADERS if prv else XPUB_HEADERS + headers = NetworkConstants.XPRV_HEADERS if prv else NetworkConstants.XPUB_HEADERS if header not in headers.values(): raise BaseException('Invalid xpub format', hex(header)) xtype = list(headers.keys())[list(headers.values()).index(header)] From 4b6a3e2e5d1a156e94b7c1eebf50ea4379eef977 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Feb 2018 01:20:38 +0100 Subject: [PATCH 28/91] fix #3899; and more aggressively catch exceptions in tx.deserialize() --- lib/tests/test_transaction.py | 5 ++ lib/transaction.py | 119 +++++++++++++++++++--------------- 2 files changed, 73 insertions(+), 51 deletions(-) diff --git a/lib/tests/test_transaction.py b/lib/tests/test_transaction.py index 609006cd..e63fb618 100644 --- a/lib/tests/test_transaction.py +++ b/lib/tests/test_transaction.py @@ -235,6 +235,11 @@ class TestTransaction(unittest.TestCase): tx = transaction.Transaction('0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000') self.assertEqual('e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d', tx.txid()) + # input: p2sh, not multisig + def test_txid_regression_issue_3899(self): + tx = transaction.Transaction('0100000004328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c010000000b0009630330472d5fae685bffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c020000000b0009630359646d5fae6858ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c030000000b000963034bd4715fae6854ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c040000000b000963036de8705fae6860ffffffff0130750000000000001976a914b5abca61d20f9062fb1fdbb880d9d93bac36675188ac00000000') + self.assertEqual('f570d5d1e965ee61bcc7005f8fefb1d3abbed9d7ddbe035e2a68fa07e5fc4a0d', tx.txid()) + class NetworkMock(object): diff --git a/lib/transaction.py b/lib/transaction.py index b23cf9cf..ddea8246 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -32,6 +32,8 @@ from .util import print_error, profiler from . import bitcoin from .bitcoin import * import struct +import traceback +import sys # # Workalike python implementation of Bitcoin's CDataStream class. @@ -303,7 +305,8 @@ def parse_scriptSig(d, _bytes): decoded = [ x for x in script_GetOp(_bytes) ] except Exception as e: # coinbase transactions raise an exception - print_error("cannot find address in input script", bh2u(_bytes)) + print_error("parse_scriptSig: cannot find address in input script (coinbase?)", + bh2u(_bytes)) return match = [ opcodes.OP_PUSHDATA4 ] @@ -334,9 +337,9 @@ def parse_scriptSig(d, _bytes): d['pubkeys'] = ["(pubkey)"] return - # non-generated TxIn transactions push a signature - # (seventy-something bytes) and then their public key - # (65 bytes) onto the stack: + # p2pkh TxIn transactions push a signature + # (71-73 bytes) and then their public key + # (33 or 65 bytes) onto the stack: match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ] if match_decoded(decoded, match): sig = bh2u(decoded[0][1]) @@ -345,7 +348,8 @@ def parse_scriptSig(d, _bytes): signatures = parse_sig([sig]) pubkey, address = xpubkey_to_address(x_pubkey) except: - print_error("cannot find address in input script", bh2u(_bytes)) + print_error("parse_scriptSig: cannot find address in input script (p2pkh?)", + bh2u(_bytes)) return d['type'] = 'p2pkh' d['signatures'] = signatures @@ -357,19 +361,26 @@ def parse_scriptSig(d, _bytes): # p2sh transaction, m of n match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1) - if not match_decoded(decoded, match): - print_error("cannot find address in input script", bh2u(_bytes)) + if match_decoded(decoded, match): + x_sig = [bh2u(x[1]) for x in decoded[1:-1]] + try: + m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1]) + except NotRecognizedRedeemScript: + # we could still guess: + # d['address'] = hash160_to_p2sh(hash_160(decoded[-1][1])) + return + # write result in d + d['type'] = 'p2sh' + d['num_sig'] = m + d['signatures'] = parse_sig(x_sig) + d['x_pubkeys'] = x_pubkeys + d['pubkeys'] = pubkeys + d['redeemScript'] = redeemScript + d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript))) return - x_sig = [bh2u(x[1]) for x in decoded[1:-1]] - m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1]) - # write result in d - d['type'] = 'p2sh' - d['num_sig'] = m - d['signatures'] = parse_sig(x_sig) - d['x_pubkeys'] = x_pubkeys - d['pubkeys'] = pubkeys - d['redeemScript'] = redeemScript - d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript))) + + print_error("parse_scriptSig: cannot find address in input script (unknown)", + bh2u(_bytes)) def parse_redeemScript(s): @@ -380,7 +391,7 @@ def parse_redeemScript(s): op_n = opcodes.OP_1 + n - 1 match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] if not match_decoded(dec2, match_multisig): - print_error("cannot find address in input script", bh2u(s)) + print_error("parse_redeemScript: not multisig", bh2u(s)) raise NotRecognizedRedeemScript() x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys] @@ -436,7 +447,11 @@ def parse_input(vds): d['num_sig'] = 0 if scriptSig: d['scriptSig'] = bh2u(scriptSig) - parse_scriptSig(d, scriptSig) + try: + parse_scriptSig(d, scriptSig) + except BaseException: + traceback.print_exc(file=sys.stderr) + print_error('failed to parse scriptSig', bh2u(scriptSig)) else: d['scriptSig'] = '' @@ -465,25 +480,40 @@ def parse_witness(vds, txin): # between p2wpkh and p2wsh; we do this based on number of witness items, # hence (FIXME) p2wsh with n==2 (maybe n==1 ?) will probably fail. # If v==0 and n==2, we need parent scriptPubKey to distinguish between p2wpkh and p2wsh. - if txin['type'] == 'coinbase': - pass - elif txin['type'] == 'p2wsh-p2sh' or n > 2: - try: - m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1])) - except NotRecognizedRedeemScript: + try: + if txin['type'] == 'coinbase': + pass + elif txin['type'] == 'p2wsh-p2sh' or n > 2: + try: + m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1])) + except NotRecognizedRedeemScript: + raise UnknownTxinType() + txin['signatures'] = parse_sig(w[1:-1]) + txin['num_sig'] = m + txin['x_pubkeys'] = x_pubkeys + txin['pubkeys'] = pubkeys + txin['witnessScript'] = witnessScript + if not txin.get('scriptSig'): # native segwit script + txin['type'] = 'p2wsh' + txin['address'] = bitcoin.script_to_p2wsh(txin['witnessScript']) + elif txin['type'] == 'p2wpkh-p2sh' or n == 2: + txin['num_sig'] = 1 + txin['x_pubkeys'] = [w[1]] + txin['pubkeys'] = [safe_parse_pubkey(w[1])] + txin['signatures'] = parse_sig([w[0]]) + if not txin.get('scriptSig'): # native segwit script + txin['type'] = 'p2wpkh' + txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0])) + else: raise UnknownTxinType() - txin['signatures'] = parse_sig(w[1:-1]) - txin['num_sig'] = m - txin['x_pubkeys'] = x_pubkeys - txin['pubkeys'] = pubkeys - txin['witnessScript'] = witnessScript - elif txin['type'] == 'p2wpkh-p2sh' or n == 2: - txin['num_sig'] = 1 - txin['x_pubkeys'] = [w[1]] - txin['pubkeys'] = [safe_parse_pubkey(w[1])] - txin['signatures'] = parse_sig([w[0]]) - else: - raise UnknownTxinType() + except UnknownTxinType: + txin['type'] = 'unknown' + # FIXME: GUI might show 'unknown' address (e.g. for a non-multisig p2wsh) + except BaseException: + txin['type'] = 'unknown' + traceback.print_exc(file=sys.stderr) + print_error('failed to parse witness', txin.get('witness')) + def parse_output(vds, i): d = {} @@ -513,20 +543,7 @@ def deserialize(raw): if is_segwit: for i in range(n_vin): txin = d['inputs'][i] - try: - parse_witness(vds, txin) - except UnknownTxinType: - txin['type'] = 'unknown' - # FIXME: GUI might show 'unknown' address (e.g. for a non-multisig p2wsh) - continue - # segwit-native script - if not txin.get('scriptSig'): - if txin['num_sig'] == 1: - txin['type'] = 'p2wpkh' - txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0])) - else: - txin['type'] = 'p2wsh' - txin['address'] = bitcoin.script_to_p2wsh(txin['witnessScript']) + parse_witness(vds, txin) d['lockTime'] = vds.read_uint32() return d From b2c035024006f72efc711b37b39010067a5487cf Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 14 Feb 2018 10:40:11 +0100 Subject: [PATCH 29/91] allow to use exchange rates while offline --- lib/commands.py | 4 ++++ lib/daemon.py | 5 ++--- lib/exchange_rate.py | 29 +++++++++++++++++++++++------ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/lib/commands.py b/lib/commands.py index 29bfd8b6..d6c71a8a 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -449,6 +449,10 @@ class Commands: end_date = datetime.datetime(year+1, 1, 1) kwargs['from_timestamp'] = time.mktime(start_date.timetuple()) kwargs['to_timestamp'] = time.mktime(end_date.timetuple()) + if show_fiat: + from .exchange_rate import FxThread + fx = FxThread(self.config, None) + kwargs['fx'] = fx return self.wallet.export_history(**kwargs) @command('w') diff --git a/lib/daemon.py b/lib/daemon.py index 38baeb15..f8497144 100644 --- a/lib/daemon.py +++ b/lib/daemon.py @@ -121,13 +121,12 @@ class Daemon(DaemonThread): self.config = config if config.get('offline'): self.network = None - self.fx = None else: self.network = Network(config) self.network.start() - self.fx = FxThread(config, self.network) + self.fx = FxThread(config, self.network) + if self.network: self.network.add_jobs([self.fx]) - self.gui = None self.wallets = {} # Setup JSONRPC server diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index 83bce0d4..26e11409 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -61,9 +61,10 @@ class ExchangeBase(PrintError): t.setDaemon(True) t.start() - def get_historical_rates_safe(self, ccy, cache_dir): + def read_historical_rates(self, ccy, cache_dir): filename = os.path.join(cache_dir, self.name() + '_'+ ccy) - if os.path.exists(filename) and (time.time() - os.stat(filename).st_mtime) < 24*3600: + if os.path.exists(filename): + timestamp = os.stat(filename).st_mtime try: with open(filename, 'r') as f: h = json.loads(f.read()) @@ -71,7 +72,15 @@ class ExchangeBase(PrintError): h = None else: h = None - if h is None: + timestamp = False + if h: + self.history[ccy] = h + self.on_history() + return h, timestamp + + def get_historical_rates_safe(self, ccy, cache_dir): + h, timestamp = self.read_historical_rates() + if h is None or time.time() - timestamp < 24*3600: try: self.print_error("requesting fx history for", ccy) h = self.request_history(ccy) @@ -397,8 +406,8 @@ class FxThread(ThreadJob): self.history_used_spot = False self.ccy_combo = None self.hist_checkbox = None - self.set_exchange(self.config_exchange()) self.cache_dir = os.path.join(config.path, 'cache') + self.set_exchange(self.config_exchange()) if not os.path.exists(self.cache_dir): os.mkdir(self.cache_dir) @@ -471,12 +480,15 @@ class FxThread(ThreadJob): # A new exchange means new fx quotes, initially empty. Force # a quote refresh self.timeout = 0 + self.exchange.read_historical_rates(self.ccy, self.cache_dir) def on_quotes(self): - self.network.trigger_callback('on_quotes') + if self.network: + self.network.trigger_callback('on_quotes') def on_history(self): - self.network.trigger_callback('on_history') + if self.network: + self.network.trigger_callback('on_history') def exchange_rate(self): '''Returns None, or the exchange rate as a Decimal''' @@ -514,6 +526,11 @@ class FxThread(ThreadJob): rate = self.history_rate(d_t) return self.value_str(satoshis, rate) + def historical_value(self, satoshis, d_t): + rate = self.history_rate(d_t) + if rate: + return Decimal(satoshis) / COIN * Decimal(rate) + def timestamp_rate(self, timestamp): from electrum.util import timestamp_to_datetime date = timestamp_to_datetime(timestamp) From 0f16bcdc1ff5cda748f3079d7efd983b426277fb Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 14 Feb 2018 10:42:09 +0100 Subject: [PATCH 30/91] Capital gains: * Show acquisition price in history. * Add summary to history command --- gui/qt/history_list.py | 23 +++++++++++-------- lib/exchange_rate.py | 10 +++++--- lib/wallet.py | 52 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index dcb56614..7e871aa2 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -61,7 +61,8 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): headers = ['', '', _('Date'), _('Description') , _('Amount'), _('Balance')] fx = self.parent.fx if fx and fx.show_history(): - headers.extend(['%s '%fx.ccy + _('Amount'), '%s '%fx.ccy + _('Balance')]) + headers.extend(['%s '%fx.ccy + _('Value')]) + headers.extend(['%s '%fx.ccy + _('Acquisition price')]) headers.extend(['%s '%fx.ccy + _('Capital Gains')]) self.editable_columns.extend([6]) self.update_headers(headers) @@ -89,20 +90,22 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): label = self.wallet.get_label(tx_hash) entry = ['', tx_hash, status_str, label, v_str, balance_str] fiat_value = None - if fx and fx.show_history(): + if value is not None and fx and fx.show_history(): date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp) fiat_value = self.wallet.get_fiat_value(tx_hash, fx.ccy) if not fiat_value: - value_str = fx.historical_value_str(value, date) + fiat_value = fx.historical_value(value, date) + fiat_default = True else: - value_str = str(fiat_value) + fiat_default = False + value_str = fx.format_fiat(fiat_value) entry.append(value_str) - balance_str = fx.historical_value_str(balance, date) - entry.append(balance_str) # fixme: should use is_mine - if value is not None and value < 0: - cg = self.wallet.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy) - entry.append("%.2f"%cg if cg is not None else _('No data')) + if value < 0: + ap, lp = self.wallet.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy) + cg = None if lp is None or ap is None else lp - ap + entry.append(fx.format_fiat(ap)) + entry.append(fx.format_fiat(cg)) item = QTreeWidgetItem(entry) item.setIcon(0, icon) item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else "")) @@ -116,7 +119,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): if value and value < 0: item.setForeground(3, QBrush(QColor("#BC1E1E"))) item.setForeground(4, QBrush(QColor("#BC1E1E"))) - if fiat_value: + if not fiat_default: item.setForeground(6, QBrush(QColor("#1E1EFF"))) if tx_hash: item.setData(0, Qt.UserRole, tx_hash) diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index 26e11409..ffd4c18a 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -506,10 +506,14 @@ class FxThread(ThreadJob): self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy) def value_str(self, satoshis, rate): - if satoshis is None: # Can happen with incomplete history - return _("Unknown") - if rate: + if satoshis is not None and rate is not None: value = Decimal(satoshis) / COIN * Decimal(rate) + else: + value = None + return self.format_fiat(value) + + def format_fiat(self, value): + if value is not None: return "%s" % (self.ccy_amount_str(value, True)) return _("No data") diff --git a/lib/wallet.py b/lib/wallet.py index 3244d977..1967b6c9 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -943,10 +943,21 @@ class Abstract_Wallet(PrintError): return h2 + def balance_at_timestamp(self, domain, target_timestamp): + h = self.get_history(domain) + for tx_hash, height, conf, timestamp, value, balance in h: + if timestamp > target_timestamp: + return balance - value + # return last balance + return balance + def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False): from .util import format_time, format_satoshis, timestamp_to_datetime h = self.get_history(domain) out = [] + init_balance = None + capital_gains = 0 + fiat_income = 0 for tx_hash, height, conf, timestamp, value, balance in h: if from_timestamp and timestamp < from_timestamp: continue @@ -960,6 +971,9 @@ class Abstract_Wallet(PrintError): 'value': format_satoshis(value, True) if value is not None else '--', 'balance': format_satoshis(balance) } + if init_balance is None: + init_balance = balance - value + end_balance = balance if item['height']>0: date_str = format_time(timestamp) if timestamp is not None else _("unverified") else: @@ -988,11 +1002,36 @@ class Abstract_Wallet(PrintError): item['output_addresses'] = output_addresses if fx is not None: date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp) - item['fiat_value'] = fx.historical_value_str(value, date) - item['fiat_balance'] = fx.historical_value_str(balance, date) + fiat_value = self.get_fiat_value(tx_hash, fx.ccy) + if fiat_value is None: + fiat_value = fx.historical_value(value, date) + item['fiat_value'] = fx.format_fiat(fiat_value) if value < 0: - item['capital_gain'] = self.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy) + ap, lp = self.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy) + cg = None if lp is None or ap is None else lp - ap + item['acquisition_price'] = fx.format_fiat(ap) + item['capital_gain'] = fx.format_fiat(cg) + capital_gains += cg + else: + fiat_income += fiat_value out.append(item) + + if from_timestamp and to_timestamp: + summary = { + 'start_date': format_time(from_timestamp), + 'end_date': format_time(to_timestamp), + 'initial_balance': format_satoshis(init_balance), + 'final_balance': format_satoshis(end_balance), + 'capital_gains': fx.format_fiat(capital_gains), + 'fiat_income': fx.format_fiat(fiat_income) + } + if fx: + start_date = timestamp_to_datetime(from_timestamp) + end_date = timestamp_to_datetime(to_timestamp) + summary['initial_fiat_value'] = fx.format_fiat(fx.historical_value(init_balance, start_date)) + summary['final_fiat_value'] = fx.format_fiat(fx.historical_value(end_balance, end_date)) + out.append(summary) + return out def get_label(self, tx_hash): @@ -1644,11 +1683,12 @@ class Abstract_Wallet(PrintError): liquidation_price = None if p is None else out_value/Decimal(COIN) * p else: liquidation_price = - fiat_value - try: - return liquidation_price - out_value/Decimal(COIN) * self.average_price(tx, price_func, ccy) + acquisition_price = out_value/Decimal(COIN) * self.average_price(tx, price_func, ccy) except: - return None + acquisition_price = None + return acquisition_price, liquidation_price + def average_price(self, tx, price_func, ccy): """ average price of the inputs of a transaction """ From 8bfe34277257d975b791df89e93545923df5c726 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 14 Feb 2018 13:55:01 +0100 Subject: [PATCH 31/91] minor fixes --- lib/wallet.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 1967b6c9..86241006 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1011,9 +1011,11 @@ class Abstract_Wallet(PrintError): cg = None if lp is None or ap is None else lp - ap item['acquisition_price'] = fx.format_fiat(ap) item['capital_gain'] = fx.format_fiat(cg) - capital_gains += cg + if cg is not None: + capital_gains += cg else: - fiat_income += fiat_value + if fiat_value is not None: + fiat_income += fiat_value out.append(item) if from_timestamp and to_timestamp: From acbad0a005031cbb886ed7cf788afe67eb40cd3a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 14 Feb 2018 14:25:51 +0100 Subject: [PATCH 32/91] change names --- lib/wallet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 86241006..7f787672 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1022,16 +1022,16 @@ class Abstract_Wallet(PrintError): summary = { 'start_date': format_time(from_timestamp), 'end_date': format_time(to_timestamp), - 'initial_balance': format_satoshis(init_balance), - 'final_balance': format_satoshis(end_balance), + 'start_balance': format_satoshis(init_balance), + 'end_balance': format_satoshis(end_balance), 'capital_gains': fx.format_fiat(capital_gains), 'fiat_income': fx.format_fiat(fiat_income) } if fx: start_date = timestamp_to_datetime(from_timestamp) end_date = timestamp_to_datetime(to_timestamp) - summary['initial_fiat_value'] = fx.format_fiat(fx.historical_value(init_balance, start_date)) - summary['final_fiat_value'] = fx.format_fiat(fx.historical_value(end_balance, end_date)) + summary['start_fiat_balance'] = fx.format_fiat(fx.historical_value(init_balance, start_date)) + summary['end_fiat_balance'] = fx.format_fiat(fx.historical_value(end_balance, end_date)) out.append(summary) return out From 89b43ee0cb7592a381e3c83a585b761accc59c08 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Feb 2018 15:58:58 +0100 Subject: [PATCH 33/91] tests: copied valid transactions from bitcoin core unit tests. try to deserialize all. --- lib/tests/test_transaction.py | 374 ++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) diff --git a/lib/tests/test_transaction.py b/lib/tests/test_transaction.py index 609006cd..1af87b04 100644 --- a/lib/tests/test_transaction.py +++ b/lib/tests/test_transaction.py @@ -236,6 +236,380 @@ class TestTransaction(unittest.TestCase): self.assertEqual('e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d', tx.txid()) +# these transactions are from Bitcoin Core unit tests ---> +# https://github.com/bitcoin/bitcoin/blob/11376b5583a283772c82f6d32d0007cdbf5b8ef0/src/test/data/tx_valid.json + + def test_txid_bitcoin_core_0001(self): + tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000') + self.assertEqual('23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63', tx.txid()) + + def test_txid_bitcoin_core_0002(self): + tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000') + self.assertEqual('fcabc409d8e685da28536e1e5ccc91264d755cd4c57ed4cae3dbaa4d3b93e8ed', tx.txid()) + + def test_txid_bitcoin_core_0003(self): + tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000') + self.assertEqual('c9aa95f2c48175fdb70b34c23f1c3fc44f869b073a6f79b1343fbce30c3cb575', tx.txid()) + + def test_txid_bitcoin_core_0004(self): + tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000495147304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000') + self.assertEqual('da94fda32b55deb40c3ed92e135d69df7efc4ee6665e0beb07ef500f407c9fd2', tx.txid()) + + def test_txid_bitcoin_core_0005(self): + tx = transaction.Transaction('0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000494f47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000') + self.assertEqual('f76f897b206e4f78d60fe40f2ccb542184cfadc34354d3bb9bdc30cc2f432b86', tx.txid()) + + def test_txid_bitcoin_core_0006(self): + tx = transaction.Transaction('01000000010276b76b07f4935c70acf54fbf1f438a4c397a9fb7e633873c4dd3bc062b6b40000000008c493046022100d23459d03ed7e9511a47d13292d3430a04627de6235b6e51a40f9cd386f2abe3022100e7d25b080f0bb8d8d5f878bba7d54ad2fda650ea8d158a33ee3cbd11768191fd004104b0e2c879e4daf7b9ab68350228c159766676a14f5815084ba166432aab46198d4cca98fa3e9981d0a90b2effc514b76279476550ba3663fdcaff94c38420e9d5000000000100093d00000000001976a9149a7b0f3b80c6baaeedce0a0842553800f832ba1f88ac00000000') + self.assertEqual('c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73', tx.txid()) + + def test_txid_bitcoin_core_0007(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000') + self.assertEqual('e41ffe19dff3cbedb413a2ca3fbbcd05cb7fd7397ffa65052f8928aa9c700092', tx.txid()) + + def test_txid_bitcoin_core_0008(self): + tx = transaction.Transaction('01000000023d6cf972d4dff9c519eff407ea800361dd0a121de1da8b6f4138a2f25de864b4000000008a4730440220ffda47bfc776bcd269da4832626ac332adfca6dd835e8ecd83cd1ebe7d709b0e022049cffa1cdc102a0b56e0e04913606c70af702a1149dc3b305ab9439288fee090014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff21ebc9ba20594737864352e95b727f1a565756f9d365083eb1a8596ec98c97b7010000008a4730440220503ff10e9f1e0de731407a4a245531c9ff17676eda461f8ceeb8c06049fa2c810220c008ac34694510298fa60b3f000df01caa244f165b727d4896eb84f81e46bcc4014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff01f0da5200000000001976a914857ccd42dded6df32949d4646dfa10a92458cfaa88ac00000000') + self.assertEqual('f7fdd091fa6d8f5e7a8c2458f5c38faffff2d3f1406b6e4fe2c99dcc0d2d1cbb', tx.txid()) + + def test_txid_bitcoin_core_0009(self): + tx = transaction.Transaction('01000000020002000000000000000000000000000000000000000000000000000000000000000000000151ffffffff0001000000000000000000000000000000000000000000000000000000000000000000006b483045022100c9cdd08798a28af9d1baf44a6c77bcc7e279f47dc487c8c899911bc48feaffcc0220503c5c50ae3998a733263c5c0f7061b483e2b56c4c41b456e7d2f5a78a74c077032102d5c25adb51b61339d2b05315791e21bbe80ea470a49db0135720983c905aace0ffffffff010000000000000000015100000000') + self.assertEqual('b56471690c3ff4f7946174e51df68b47455a0d29344c351377d712e6d00eabe5', tx.txid()) + + def test_txid_bitcoin_core_0010(self): + tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000009085768617420697320ffffffff010000000000000000015100000000') + self.assertEqual('99517e5b47533453cc7daa332180f578be68b80370ecfe84dbfff7f19d791da4', tx.txid()) + + def test_txid_bitcoin_core_0011(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100c66c9cdf4c43609586d15424c54707156e316d88b0a1534c9e6b0d4f311406310221009c0fe51dbc9c4ab7cc25d3fdbeccf6679fe6827f08edf2b4a9f16ee3eb0e438a0123210338e8034509af564c62644c07691942e0c056752008a173c89f60ab2a88ac2ebfacffffffff010000000000000000015100000000') + self.assertEqual('ab097537b528871b9b64cb79a769ae13c3c3cd477cc9dddeebe657eabd7fdcea', tx.txid()) + + def test_txid_bitcoin_core_0012(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100e1eadba00d9296c743cb6ecc703fd9ddc9b3cd12906176a226ae4c18d6b00796022100a71aef7d2874deff681ba6080f1b278bac7bb99c61b08a85f4311970ffe7f63f012321030c0588dc44d92bdcbf8e72093466766fdc265ead8db64517b0c542275b70fffbacffffffff010040075af0750700015100000000') + self.assertEqual('4d163e00f1966e9a1eab8f9374c3e37f4deb4857c247270e25f7d79a999d2dc9', tx.txid()) + + def test_txid_bitcoin_core_0013(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000006d483045022027deccc14aa6668e78a8c9da3484fbcd4f9dcc9bb7d1b85146314b21b9ae4d86022100d0b43dece8cfb07348de0ca8bc5b86276fa88f7f2138381128b7c36ab2e42264012321029bb13463ddd5d2cc05da6e84e37536cb9525703cfd8f43afdb414988987a92f6acffffffff020040075af075070001510000000000000000015100000000') + self.assertEqual('9fe2ef9dde70e15d78894a4800b7df3bbfb1addb9a6f7d7c204492fdb6ee6cc4', tx.txid()) + + def test_txid_bitcoin_core_0014(self): + tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025151ffffffff010000000000000000015100000000') + self.assertEqual('99d3825137602e577aeaf6a2e3c9620fd0e605323dc5265da4a570593be791d4', tx.txid()) + + def test_txid_bitcoin_core_0015(self): + tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff6451515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151ffffffff010000000000000000015100000000') + self.assertEqual('c0d67409923040cc766bbea12e4c9154393abef706db065ac2e07d91a9ba4f84', tx.txid()) + + def test_txid_bitcoin_core_0016(self): + tx = transaction.Transaction('010000000200010000000000000000000000000000000000000000000000000000000000000000000049483045022100d180fd2eb9140aeb4210c9204d3f358766eb53842b2a9473db687fa24b12a3cc022079781799cd4f038b85135bbe49ec2b57f306b2bb17101b17f71f000fcab2b6fb01ffffffff0002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000') + self.assertEqual('c610d85d3d5fdf5046be7f123db8a0890cee846ee58de8a44667cfd1ab6b8666', tx.txid()) + + def test_txid_bitcoin_core_0017(self): + tx = transaction.Transaction('01000000020001000000000000000000000000000000000000000000000000000000000000000000004948304502203a0f5f0e1f2bdbcd04db3061d18f3af70e07f4f467cbc1b8116f267025f5360b022100c792b6e215afc5afc721a351ec413e714305cb749aae3d7fee76621313418df101010000000002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000') + self.assertEqual('a647a7b3328d2c698bfa1ee2dd4e5e05a6cea972e764ccb9bd29ea43817ca64f', tx.txid()) + + def test_txid_bitcoin_core_0018(self): + tx = transaction.Transaction('010000000370ac0a1ae588aaf284c308d67ca92c69a39e2db81337e563bf40c59da0a5cf63000000006a4730440220360d20baff382059040ba9be98947fd678fb08aab2bb0c172efa996fd8ece9b702201b4fb0de67f015c90e7ac8a193aeab486a1f587e0f54d0fb9552ef7f5ce6caec032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff7d815b6447e35fbea097e00e028fb7dfbad4f3f0987b4734676c84f3fcd0e804010000006b483045022100c714310be1e3a9ff1c5f7cacc65c2d8e781fc3a88ceb063c6153bf950650802102200b2d0979c76e12bb480da635f192cc8dc6f905380dd4ac1ff35a4f68f462fffd032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff3f1f097333e4d46d51f5e77b53264db8f7f5d2e18217e1099957d0f5af7713ee010000006c493046022100b663499ef73273a3788dea342717c2640ac43c5a1cf862c9e09b206fcb3f6bb8022100b09972e75972d9148f2bdd462e5cb69b57c1214b88fc55ca638676c07cfc10d8032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff0380841e00000000001976a914bfb282c70c4191f45b5a6665cad1682f2c9cfdfb88ac80841e00000000001976a9149857cc07bed33a5cf12b9c5e0500b675d500c81188ace0fd1c00000000001976a91443c52850606c872403c0601e69fa34b26f62db4a88ac00000000') + self.assertEqual('afd9c17f8913577ec3509520bd6e5d63e9c0fd2a5f70c787993b097ba6ca9fae', tx.txid()) + + def test_txid_bitcoin_core_0019(self): + tx = transaction.Transaction('01000000012312503f2491a2a97fcd775f11e108a540a5528b5d4dee7a3c68ae4add01dab300000000fdfe0000483045022100f6649b0eddfdfd4ad55426663385090d51ee86c3481bdc6b0c18ea6c0ece2c0b0220561c315b07cffa6f7dd9df96dbae9200c2dee09bf93cc35ca05e6cdf613340aa0148304502207aacee820e08b0b174e248abd8d7a34ed63b5da3abedb99934df9fddd65c05c4022100dfe87896ab5ee3df476c2655f9fbe5bd089dccbef3e4ea05b5d121169fe7f5f4014c695221031d11db38972b712a9fe1fc023577c7ae3ddb4a3004187d41c45121eecfdbb5b7210207ec36911b6ad2382860d32989c7b8728e9489d7bbc94a6b5509ef0029be128821024ea9fac06f666a4adc3fc1357b7bec1fd0bdece2b9d08579226a8ebde53058e453aeffffffff0180380100000000001976a914c9b99cddf847d10685a4fabaa0baf505f7c3dfab88ac00000000') + self.assertEqual('f4b05f978689c89000f729cae187dcfbe64c9819af67a4f05c0b4d59e717d64d', tx.txid()) + + def test_txid_bitcoin_core_0020(self): + tx = transaction.Transaction('0100000001f709fa82596e4f908ee331cb5e0ed46ab331d7dcfaf697fe95891e73dac4ebcb000000008c20ca42095840735e89283fec298e62ac2ddea9b5f34a8cbb7097ad965b87568100201b1b01dc829177da4a14551d2fc96a9db00c6501edfa12f22cd9cefd335c227f483045022100a9df60536df5733dd0de6bc921fab0b3eee6426501b43a228afa2c90072eb5ca02201c78b74266fac7d1db5deff080d8a403743203f109fbcabf6d5a760bf87386d20100ffffffff01c075790000000000232103611f9a45c18f28f06f19076ad571c344c82ce8fcfe34464cf8085217a2d294a6ac00000000') + self.assertEqual('cc60b1f899ec0a69b7c3f25ddf32c4524096a9c5b01cbd84c6d0312a0c478984', tx.txid()) + + def test_txid_bitcoin_core_0021(self): + tx = transaction.Transaction('01000000012c651178faca83be0b81c8c1375c4b0ad38d53c8fe1b1c4255f5e795c25792220000000049483045022100d6044562284ac76c985018fc4a90127847708c9edb280996c507b28babdc4b2a02203d74eca3f1a4d1eea7ff77b528fde6d5dc324ec2dbfdb964ba885f643b9704cd01ffffffff010100000000000000232102c2410f8891ae918cab4ffc4bb4a3b0881be67c7a1e7faa8b5acf9ab8932ec30cac00000000') + self.assertEqual('1edc7f214659d52c731e2016d258701911bd62a0422f72f6c87a1bc8dd3f8667', tx.txid()) + + def test_txid_bitcoin_core_0022(self): + tx = transaction.Transaction('0100000001f725ea148d92096a79b1709611e06e94c63c4ef61cbae2d9b906388efd3ca99c000000000100ffffffff0101000000000000002321028a1d66975dbdf97897e3a4aef450ebeb5b5293e4a0b4a6d3a2daaa0b2b110e02ac00000000') + self.assertEqual('018adb7133fde63add9149a2161802a1bcf4bdf12c39334e880c073480eda2ff', tx.txid()) + + def test_txid_bitcoin_core_0023(self): + tx = transaction.Transaction('0100000001be599efaa4148474053c2fa031c7262398913f1dc1d9ec201fd44078ed004e44000000004900473044022022b29706cb2ed9ef0cb3c97b72677ca2dfd7b4160f7b4beb3ba806aa856c401502202d1e52582412eba2ed474f1f437a427640306fd3838725fab173ade7fe4eae4a01ffffffff010100000000000000232103ac4bba7e7ca3e873eea49e08132ad30c7f03640b6539e9b59903cf14fd016bbbac00000000') + self.assertEqual('1464caf48c708a6cc19a296944ded9bb7f719c9858986d2501cf35068b9ce5a2', tx.txid()) + + def test_txid_bitcoin_core_0024(self): + tx = transaction.Transaction('010000000112b66d5e8c7d224059e946749508efea9d66bf8d0c83630f080cf30be8bb6ae100000000490047304402206ffe3f14caf38ad5c1544428e99da76ffa5455675ec8d9780fac215ca17953520220779502985e194d84baa36b9bd40a0dbd981163fa191eb884ae83fc5bd1c86b1101ffffffff010100000000000000232103905380c7013e36e6e19d305311c1b81fce6581f5ee1c86ef0627c68c9362fc9fac00000000') + self.assertEqual('1fb73fbfc947d52f5d80ba23b67c06a232ad83fdd49d1c0a657602f03fbe8f7a', tx.txid()) + + def test_txid_bitcoin_core_0025(self): + tx = transaction.Transaction('0100000001b0ef70cc644e0d37407e387e73bfad598d852a5aa6d691d72b2913cebff4bceb000000004a00473044022068cd4851fc7f9a892ab910df7a24e616f293bcb5c5fbdfbc304a194b26b60fba022078e6da13d8cb881a22939b952c24f88b97afd06b4c47a47d7f804c9a352a6d6d0100ffffffff0101000000000000002321033bcaa0a602f0d44cc9d5637c6e515b0471db514c020883830b7cefd73af04194ac00000000') + self.assertEqual('24cecfce0fa880b09c9b4a66c5134499d1b09c01cc5728cd182638bea070e6ab', tx.txid()) + + def test_txid_bitcoin_core_0026(self): + tx = transaction.Transaction('0100000001c188aa82f268fcf08ba18950f263654a3ea6931dabc8bf3ed1d4d42aaed74cba000000004b0000483045022100940378576e069aca261a6b26fb38344e4497ca6751bb10905c76bb689f4222b002204833806b014c26fd801727b792b1260003c55710f87c5adbd7a9cb57446dbc9801ffffffff0101000000000000002321037c615d761e71d38903609bf4f46847266edc2fb37532047d747ba47eaae5ffe1ac00000000') + self.assertEqual('9eaa819e386d6a54256c9283da50c230f3d8cd5376d75c4dcc945afdeb157dd7', tx.txid()) + + def test_txid_bitcoin_core_0027(self): + tx = transaction.Transaction('01000000012432b60dc72cebc1a27ce0969c0989c895bdd9e62e8234839117f8fc32d17fbc000000004a493046022100a576b52051962c25e642c0fd3d77ee6c92487048e5d90818bcf5b51abaccd7900221008204f8fb121be4ec3b24483b1f92d89b1b0548513a134e345c5442e86e8617a501ffffffff010000000000000000016a00000000') + self.assertEqual('46224764c7870f95b58f155bce1e38d4da8e99d42dbb632d0dd7c07e092ee5aa', tx.txid()) + + def test_txid_bitcoin_core_0028(self): + tx = transaction.Transaction('01000000014710b0e7cf9f8930de259bdc4b84aa5dfb9437b665a3e3a21ff26e0bf994e183000000004a493046022100a166121a61b4eeb19d8f922b978ff6ab58ead8a5a5552bf9be73dc9c156873ea02210092ad9bc43ee647da4f6652c320800debcf08ec20a094a0aaf085f63ecb37a17201ffffffff010000000000000000016a00000000') + self.assertEqual('8d66836045db9f2d7b3a75212c5e6325f70603ee27c8333a3bce5bf670d9582e', tx.txid()) + + def test_txid_bitcoin_core_0029(self): + tx = transaction.Transaction('01000000015ebaa001d8e4ec7a88703a3bcf69d98c874bca6299cca0f191512bf2a7826832000000004948304502203bf754d1c6732fbf87c5dcd81258aefd30f2060d7bd8ac4a5696f7927091dad1022100f5bcb726c4cf5ed0ed34cc13dadeedf628ae1045b7cb34421bc60b89f4cecae701ffffffff010000000000000000016a00000000') + self.assertEqual('aab7ef280abbb9cc6fbaf524d2645c3daf4fcca2b3f53370e618d9cedf65f1f8', tx.txid()) + + def test_txid_bitcoin_core_0030(self): + tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a900000000924830450221009c0a27f886a1d8cb87f6f595fbc3163d28f7a81ec3c4b252ee7f3ac77fd13ffa02203caa8dfa09713c8c4d7ef575c75ed97812072405d932bd11e6a1593a98b679370148304502201e3861ef39a526406bad1e20ecad06be7375ad40ddb582c9be42d26c3a0d7b240221009d0a3985e96522e59635d19cc4448547477396ce0ef17a58e7d74c3ef464292301ffffffff010000000000000000016a00000000') + self.assertEqual('6327783a064d4e350c454ad5cd90201aedf65b1fc524e73709c52f0163739190', tx.txid()) + + def test_txid_bitcoin_core_0031(self): + tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a48304502207a6974a77c591fa13dff60cabbb85a0de9e025c09c65a4b2285e47ce8e22f761022100f0efaac9ff8ac36b10721e0aae1fb975c90500b50c56e8a0cc52b0403f0425dd0100ffffffff010000000000000000016a00000000') + self.assertEqual('892464645599cc3c2d165adcc612e5f982a200dfaa3e11e9ce1d228027f46880', tx.txid()) + + def test_txid_bitcoin_core_0032(self): + tx = transaction.Transaction('010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a483045022100fa4a74ba9fd59c59f46c3960cf90cbe0d2b743c471d24a3d5d6db6002af5eebb02204d70ec490fd0f7055a7c45f86514336e3a7f03503dacecabb247fc23f15c83510151ffffffff010000000000000000016a00000000') + self.assertEqual('578db8c6c404fec22c4a8afeaf32df0e7b767c4dda3478e0471575846419e8fc', tx.txid()) + + def test_txid_bitcoin_core_0033(self): + tx = transaction.Transaction('0100000001e0be9e32f1f89c3d916c4f21e55cdcd096741b895cc76ac353e6023a05f4f7cc00000000d86149304602210086e5f736a2c3622ebb62bd9d93d8e5d76508b98be922b97160edc3dcca6d8c47022100b23c312ac232a4473f19d2aeb95ab7bdf2b65518911a0d72d50e38b5dd31dc820121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac4730440220508fa761865c8abd81244a168392876ee1d94e8ed83897066b5e2df2400dad24022043f5ee7538e87e9c6aef7ef55133d3e51da7cc522830a9c4d736977a76ef755c0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000') + self.assertEqual('974f5148a0946f9985e75a240bb24c573adbbdc25d61e7b016cdbb0a5355049f', tx.txid()) + + def test_txid_bitcoin_core_0034(self): + tx = transaction.Transaction('01000000013c6f30f99a5161e75a2ce4bca488300ca0c6112bde67f0807fe983feeff0c91001000000e608646561646265656675ab61493046022100ce18d384221a731c993939015e3d1bcebafb16e8c0b5b5d14097ec8177ae6f28022100bcab227af90bab33c3fe0a9abfee03ba976ee25dc6ce542526e9b2e56e14b7f10121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac493046022100c3b93edcc0fd6250eb32f2dd8a0bba1754b0f6c3be8ed4100ed582f3db73eba2022100bf75b5bd2eff4d6bf2bda2e34a40fcc07d4aa3cf862ceaa77b47b81eff829f9a01ab21038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000') + self.assertEqual('b0097ec81df231893a212657bf5fe5a13b2bff8b28c0042aca6fc4159f79661b', tx.txid()) + + def test_txid_bitcoin_core_0035(self): + tx = transaction.Transaction('01000000016f3dbe2ca96fa217e94b1017860be49f20820dea5c91bdcb103b0049d5eb566000000000fd1d0147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac47304402203757e937ba807e4a5da8534c17f9d121176056406a6465054bdd260457515c1a02200f02eccf1bec0f3a0d65df37889143c2e88ab7acec61a7b6f5aa264139141a2b0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000') + self.assertEqual('feeba255656c80c14db595736c1c7955c8c0a497622ec96e3f2238fbdd43a7c9', tx.txid()) + + def test_txid_bitcoin_core_0036(self): + tx = transaction.Transaction('01000000012139c555ccb81ee5b1e87477840991ef7b386bc3ab946b6b682a04a621006b5a01000000fdb40148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f2204148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390175ac4830450220646b72c35beeec51f4d5bc1cbae01863825750d7f490864af354e6ea4f625e9c022100f04b98432df3a9641719dbced53393022e7249fb59db993af1118539830aab870148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a580039017521038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000') + self.assertEqual('a0c984fc820e57ddba97f8098fa640c8a7eb3fe2f583923da886b7660f505e1e', tx.txid()) + + def test_txid_bitcoin_core_0037(self): + tx = transaction.Transaction('0100000002f9cbafc519425637ba4227f8d0a0b7160b4e65168193d5af39747891de98b5b5000000006b4830450221008dd619c563e527c47d9bd53534a770b102e40faa87f61433580e04e271ef2f960220029886434e18122b53d5decd25f1f4acb2480659fea20aabd856987ba3c3907e0121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffff42e7988254800876b69f24676b3e0205b77be476512ca4d970707dd5c60598ab00000000fd260100483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a53034930460221008431bdfa72bc67f9d41fe72e94c88fb8f359ffa30b33c72c121c5a877d922e1002210089ef5fc22dd8bfc6bf9ffdb01a9862d27687d424d1fefbab9e9c7176844a187a014c9052483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c7153aeffffffff01a08601000000000017a914d8dacdadb7462ae15cd906f1878706d0da8660e68700000000') + self.assertEqual('5df1375ffe61ac35ca178ebb0cab9ea26dedbd0e96005dfcee7e379fa513232f', tx.txid()) + + def test_txid_bitcoin_core_0038(self): + tx = transaction.Transaction('0100000002dbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce000000006b4830450221009627444320dc5ef8d7f68f35010b4c050a6ed0d96b67a84db99fda9c9de58b1e02203e4b4aaa019e012e65d69b487fdf8719df72f488fa91506a80c49a33929f1fd50121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffffdbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce010000009300483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303ffffffff01a0860100000000001976a9149bc0bbdd3024da4d0c38ed1aecf5c68dd1d3fa1288ac00000000') + self.assertEqual('ded7ff51d89a4e1ec48162aee5a96447214d93dfb3837946af2301a28f65dbea', tx.txid()) + + def test_txid_bitcoin_core_0039(self): + tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000') + self.assertEqual('3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e', tx.txid()) + + def test_txid_bitcoin_core_0040(self): + tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d') + self.assertEqual('abd62b4627d8d9b2d95fcfd8c87e37d2790637ce47d28018e3aece63c1d62649', tx.txid()) + + def test_txid_bitcoin_core_0041(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d') + self.assertEqual('58b6de8413603b7f556270bf48caedcf17772e7105f5419f6a80be0df0b470da', tx.txid()) + + def test_txid_bitcoin_core_0042(self): + tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff') + self.assertEqual('5f99c0abf511294d76cbe144d86b77238a03e086974bc7a8ea0bdb2c681a0324', tx.txid()) + + def test_txid_bitcoin_core_0043(self): + tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000') + self.assertEqual('25d35877eaba19497710666473c50d5527d38503e3521107a3fc532b74cd7453', tx.txid()) + + def test_txid_bitcoin_core_0044(self): + tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000feffffff') + self.assertEqual('1b9aef851895b93c62c29fbd6ca4d45803f4007eff266e2f96ff11e9b6ef197b', tx.txid()) + + def test_txid_bitcoin_core_0045(self): + tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000') + self.assertEqual('3444be2e216abe77b46015e481d8cc21abd4c20446aabf49cd78141c9b9db87e', tx.txid()) + + def test_txid_bitcoin_core_0046(self): + tx = transaction.Transaction('01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000001000000') + self.assertEqual('f53761038a728b1f17272539380d96e93f999218f8dcb04a8469b523445cd0fd', tx.txid()) + + def test_txid_bitcoin_core_0047(self): + tx = transaction.Transaction('0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000001000000') + self.assertEqual('d193f0f32fceaf07bb25c897c8f99ca6f69a52f6274ca64efc2a2e180cb97fc1', tx.txid()) + + def test_txid_bitcoin_core_0048(self): + tx = transaction.Transaction('010000000132211bdd0d568506804eef0d8cc3db68c3d766ab9306cdfcc0a9c89616c8dbb1000000006c493045022100c7bb0faea0522e74ff220c20c022d2cb6033f8d167fb89e75a50e237a35fd6d202203064713491b1f8ad5f79e623d0219ad32510bfaa1009ab30cbee77b59317d6e30001210237af13eb2d84e4545af287b919c2282019c9691cc509e78e196a9d8274ed1be0ffffffff0100000000000000001976a914f1b3ed2eda9a2ebe5a9374f692877cdf87c0f95b88ac00000000') + self.assertEqual('50a1e0e6a134a564efa078e3bd088e7e8777c2c0aec10a752fd8706470103b89', tx.txid()) + + def test_txid_bitcoin_core_0049(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000') + self.assertEqual('e2207d1aaf6b74e5d98c2fa326d2dc803b56b30a3f90ce779fa5edb762f38755', tx.txid()) + + def test_txid_bitcoin_core_0050(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000') + self.assertEqual('f335864f7c12ec7946d2c123deb91eb978574b647af125a414262380c7fbd55c', tx.txid()) + + def test_txid_bitcoin_core_0051(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000') + self.assertEqual('d1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2', tx.txid()) + + def test_txid_bitcoin_core_0052(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000') + self.assertEqual('3a13e1b6371c545147173cc4055f0ed73686a9f73f092352fb4b39ca27d360e6', tx.txid()) + + def test_txid_bitcoin_core_0053(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff40000100000000000000000000000000') + self.assertEqual('bffda23e40766d292b0510a1b556453c558980c70c94ab158d8286b3413e220d', tx.txid()) + + def test_txid_bitcoin_core_0054(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000') + self.assertEqual('01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd', tx.txid()) + + def test_txid_bitcoin_core_0055(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000') + self.assertEqual('f6d2359c5de2d904e10517d23e7c8210cca71076071bbf46de9fbd5f6233dbf1', tx.txid()) + + def test_txid_bitcoin_core_0056(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000') + self.assertEqual('19c2b7377229dae7aa3e50142a32fd37cef7171a01682f536e9ffa80c186f6c9', tx.txid()) + + def test_txid_bitcoin_core_0057(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000') + self.assertEqual('c9dda3a24cc8a5acb153d1085ecd2fecf6f87083122f8cdecc515b1148d4c40d', tx.txid()) + + def test_txid_bitcoin_core_0058(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000') + self.assertEqual('d1edbcde44691e98a7b7f556bd04966091302e29ad9af3c2baac38233667e0d2', tx.txid()) + + def test_txid_bitcoin_core_0059(self): + tx = transaction.Transaction('020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000') + self.assertEqual('01a86c65460325dc6699714d26df512a62a854a669f6ed2e6f369a238e048cfd', tx.txid()) + + def test_txid_bitcoin_core_0060(self): + tx = transaction.Transaction('02000000010001000000000000000000000000000000000000000000000000000000000000000000000251b2010000000100000000000000000000000000') + self.assertEqual('4b5e0aae1251a9dc66b4d5f483f1879bf518ea5e1765abc5a9f2084b43ed1ea7', tx.txid()) + + def test_txid_bitcoin_core_0061(self): + tx = transaction.Transaction('0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2010000000100000000000000000000000000') + self.assertEqual('5f16eb3ca4581e2dfb46a28140a4ee15f85e4e1c032947da8b93549b53c105f5', tx.txid()) + + def test_txid_bitcoin_core_0062(self): + tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000') + self.assertEqual('b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004', tx.txid()) + + def test_txid_bitcoin_core_0063(self): + tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000') + self.assertEqual('b2ce556154e5ab22bec0a2f990b2b843f4f4085486c0d2cd82873685c0012004', tx.txid()) + + def test_txid_bitcoin_core_0064(self): + tx = transaction.Transaction('01000000000101000100000000000000000000000000000000000000000000000000000000000000000000171600144c9c3dfac4207d5d8cb89df5722cb3d712385e3fffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000') + self.assertEqual('fee125c6cd142083fabd0187b1dd1f94c66c89ec6e6ef6da1374881c0c19aece', tx.txid()) + + def test_txid_bitcoin_core_0065(self): + tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000023220020ff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000') + self.assertEqual('5f32557914351fee5f89ddee6c8983d476491d29e601d854e3927299e50450da', tx.txid()) + + def test_txid_bitcoin_core_0066(self): + tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff05540b0000000000000151d0070000000000000151840300000000000001513c0f00000000000001512c010000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71000000000000') + self.assertEqual('07dfa2da3d67c8a2b9f7bd31862161f7b497829d5da90a88ba0f1a905e7a43f7', tx.txid()) + + def test_txid_bitcoin_core_0067(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0068(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff0484030000000000000151d0070000000000000151540b0000000000000151c800000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('f92bb6e4f3ff89172f23ef647f74c13951b665848009abb5862cdf7a0412415a', tx.txid()) + + def test_txid_bitcoin_core_0069(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0070(self): + tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff04b60300000000000001519e070000000000000151860b00000000000001009600000000000000015100000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('e657e25fc9f2b33842681613402759222a58cf7dd504d6cdc0b69a0b8c2e7dcb', tx.txid()) + + def test_txid_bitcoin_core_0071(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0072(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff04b60300000000000001519e070000000000000151860b0000000000000100960000000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('4ede5e22992d43d42ccdf6553fb46e448aa1065ba36423f979605c1e5ab496b8', tx.txid()) + + def test_txid_bitcoin_core_0073(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0074(self): + tx = transaction.Transaction('01000000000103000100000000000000000000000000000000000000000000000000000000000000000000000200000000010000000000000000000000000000000000000000000000000000000000000100000000ffffffff000100000000000000000000000000000000000000000000000000000000000002000000000200000003e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('cfe9f4b19f52b8366860aec0d2b5815e329299b2e9890d477edd7f1182be7ac8', tx.txid()) + + def test_txid_bitcoin_core_0075(self): + tx = transaction.Transaction('0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('aee8f4865ca40fa77ff2040c0d7de683bea048b103d42ca406dc07dd29d539cb', tx.txid()) + + def test_txid_bitcoin_core_0076(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0077(self): + tx = transaction.Transaction('0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623ffffffffff1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('8a1bddf924d24570074b09d7967c145e54dc4cee7972a92fd975a2ad9e64b424', tx.txid()) + + def test_txid_bitcoin_core_0078(self): + tx = transaction.Transaction('0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015102fd08020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002755100000000') + self.assertEqual('d93ab9e12d7c29d2adc13d5cdf619d53eec1f36eb6612f55af52be7ba0448e97', tx.txid()) + + def test_txid_bitcoin_core_0079(self): + tx = transaction.Transaction('0100000000010c00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff0001000000000000000000000000000000000000000000000000000000000000020000006a473044022026c2e65b33fcd03b2a3b0f25030f0244bd23cc45ae4dec0f48ae62255b1998a00220463aa3982b718d593a6b9e0044513fd67a5009c2fdccc59992cffc2b167889f4012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000030000006a4730440220008bd8382911218dcb4c9f2e75bf5c5c3635f2f2df49b36994fde85b0be21a1a02205a539ef10fb4c778b522c1be852352ea06c67ab74200977c722b0bc68972575a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000040000006b483045022100d9436c32ff065127d71e1a20e319e4fe0a103ba0272743dbd8580be4659ab5d302203fd62571ee1fe790b182d078ecfd092a509eac112bea558d122974ef9cc012c7012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000050000006a47304402200e2c149b114ec546015c13b2b464bbcb0cdc5872e6775787527af6cbc4830b6c02207e9396c6979fb15a9a2b96ca08a633866eaf20dc0ff3c03e512c1d5a1654f148012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000060000006b483045022100b20e70d897dc15420bccb5e0d3e208d27bdd676af109abbd3f88dbdb7721e6d6022005836e663173fbdfe069f54cde3c2decd3d0ea84378092a5d9d85ec8642e8a41012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff00010000000000000000000000000000000000000000000000000000000000000700000000ffffffff00010000000000000000000000000000000000000000000000000000000000000800000000ffffffff00010000000000000000000000000000000000000000000000000000000000000900000000ffffffff00010000000000000000000000000000000000000000000000000000000000000a00000000ffffffff00010000000000000000000000000000000000000000000000000000000000000b0000006a47304402206639c6e05e3b9d2675a7f3876286bdf7584fe2bbd15e0ce52dd4e02c0092cdc60220757d60b0a61fc95ada79d23746744c72bac1545a75ff6c2c7cdb6ae04e7e9592012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0ce8030000000000000151e9030000000000000151ea030000000000000151eb030000000000000151ec030000000000000151ed030000000000000151ee030000000000000151ef030000000000000151f0030000000000000151f1030000000000000151f2030000000000000151f30300000000000001510248304502210082219a54f61bf126bfc3fa068c6e33831222d1d7138c6faa9d33ca87fd4202d6022063f9902519624254d7c2c8ea7ba2d66ae975e4e229ae38043973ec707d5d4a83012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022017fb58502475848c1b09f162cb1688d0920ff7f142bed0ef904da2ccc88b168f02201798afa61850c65e77889cbcd648a5703b487895517c88f85cdd18b021ee246a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000000247304402202830b7926e488da75782c81a54cd281720890d1af064629ebf2e31bf9f5435f30220089afaa8b455bbeb7d9b9c3fe1ed37d07685ade8455c76472cda424d93e4074a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022026326fcdae9207b596c2b05921dbac11d81040c4d40378513670f19d9f4af893022034ecd7a282c0163b89aaa62c22ec202cef4736c58cd251649bad0d8139bcbf55012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71024730440220214978daeb2f38cd426ee6e2f44131a33d6b191af1c216247f1dd7d74c16d84a02205fdc05529b0bc0c430b4d5987264d9d075351c4f4484c16e91662e90a72aab24012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402204a6e9f199dc9672cf2ff8094aaa784363be1eb62b679f7ff2df361124f1dca3302205eeb11f70fab5355c9c8ad1a0700ea355d315e334822fa182227e9815308ee8f012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000') + self.assertEqual('b83579db5246aa34255642768167132a0c3d2932b186cd8fb9f5490460a0bf91', tx.txid()) + + def test_txid_bitcoin_core_0080(self): + tx = transaction.Transaction('010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000') + self.assertEqual('2b1e44fff489d09091e5e20f9a01bbc0e8d80f0662e629fd10709cdb4922a874', tx.txid()) + + def test_txid_bitcoin_core_0081(self): + tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff01d00700000000000001510003483045022100e078de4e96a0e05dcdc0a414124dd8475782b5f3f0ed3f607919e9a5eeeb22bf02201de309b3a3109adb3de8074b3610d4cf454c49b61247a2779a0bcbf31c889333032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc711976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac00000000') + self.assertEqual('60ebb1dd0b598e20dd0dd462ef6723dd49f8f803b6a2492926012360119cfdd7', tx.txid()) + + def test_txid_bitcoin_core_0082(self): + tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff02e8030000000000000151e90300000000000001510247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000') + self.assertEqual('ed0c7f4163e275f3f77064f471eac861d01fdf55d03aa6858ebd3781f70bf003', tx.txid()) + + def test_txid_bitcoin_core_0083(self): + tx = transaction.Transaction('0100000000010200010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff02e9030000000000000151e80300000000000001510248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000') + self.assertEqual('f531ddf5ce141e1c8a7fdfc85cc634e5ff686f446a5cf7483e9dbe076b844862', tx.txid()) + + def test_txid_bitcoin_core_0084(self): + tx = transaction.Transaction('01000000020001000000000000000000000000000000000000000000000000000000000000000000004847304402202a0b4b1294d70540235ae033d78e64b4897ec859c7b6f1b2b1d8a02e1d46006702201445e756d2254b0f1dfda9ab8e1e1bc26df9668077403204f32d16a49a36eb6983ffffffff00010000000000000000000000000000000000000000000000000000000000000100000049483045022100acb96cfdbda6dc94b489fd06f2d720983b5f350e31ba906cdbd800773e80b21c02200d74ea5bdf114212b4bbe9ed82c36d2e369e302dff57cb60d01c428f0bd3daab83ffffffff02e8030000000000000151e903000000000000015100000000') + self.assertEqual('98229b70948f1c17851a541f1fe532bf02c408267fecf6d7e174c359ae870654', tx.txid()) + + def test_txid_bitcoin_core_0085(self): + tx = transaction.Transaction('01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000') + self.assertEqual('570e3730deeea7bd8bc92c836ccdeb4dd4556f2c33f2a1f7b889a4cb4e48d3ab', tx.txid()) + + def test_txid_bitcoin_core_0086(self): + tx = transaction.Transaction('01000000000102e9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff80e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffff0280969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac80969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000') + self.assertEqual('e0b8142f587aaa322ca32abce469e90eda187f3851043cc4f2a0fff8c13fc84e', tx.txid()) + + def test_txid_bitcoin_core_0087(self): + tx = transaction.Transaction('0100000000010280e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffffe9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff0280969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac80969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000') + self.assertEqual('b9ecf72df06b8f98f8b63748d1aded5ffc1a1186f8a302e63cf94f6250e29f4d', tx.txid()) + + def test_txid_bitcoin_core_0088(self): + tx = transaction.Transaction('0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000') + self.assertEqual('27eae69aff1dd4388c0fa05cbbfe9a3983d1b0b5811ebcd4199b86f299370aac', tx.txid()) + + def test_txid_bitcoin_core_0089(self): + tx = transaction.Transaction('010000000169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f1581b0000b64830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0121037a3fb04bcdb09eba90f69961ba1692a3528e45e67c85b200df820212d7594d334aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01ffffffff0101000000000000000000000000') + self.assertEqual('22d020638e3b7e1f2f9a63124ac76f5e333c74387862e3675f64b25e960d3641', tx.txid()) + + def test_txid_bitcoin_core_0090(self): + tx = transaction.Transaction('0100000000010169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f14c1d000000ffffffff01010000000000000000034830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e012102a9781d66b61fb5a7ef00ac5ad5bc6ffc78be7b44a566e3c87870e1079368df4c4aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0100000000') + self.assertEqual('2862bc0c69d2af55da7284d1b16a7cddc03971b77e5a97939cca7631add83bf5', tx.txid()) + + def test_txid_bitcoin_core_0091(self): + tx = transaction.Transaction('01000000019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a662896581b0000fd6f01004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c03959601522102cd74a2809ffeeed0092bc124fd79836706e41f048db3f6ae9df8708cefb83a1c2102e615999372426e46fd107b76eaf007156a507584aa2cc21de9eee3bdbd26d36c4c9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175ffffffff0101000000000000000000000000') + self.assertEqual('1aebf0c98f01381765a8c33d688f8903e4d01120589ac92b78f1185dc1f4119c', tx.txid()) + + def test_txid_bitcoin_core_0092(self): + tx = transaction.Transaction('010000000001019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a6628964c1d000000ffffffff0101000000000000000007004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960101022102966f109c54e85d3aee8321301136cedeb9fc710fdef58a9de8a73942f8e567c021034ffc99dd9a79dd3cb31e2ab3e0b09e0e67db41ac068c625cd1f491576016c84e9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c039596017500000000') + self.assertEqual('45d17fb7db86162b2b6ca29fa4e163acf0ef0b54110e49b819bda1f948d423a3', tx.txid()) + +# txns from Bitcoin Core ends <--- + + class NetworkMock(object): def __init__(self, unspent): From 063e40bf18309c4499a8e3518c9612bb1cb8fc8d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Feb 2018 16:20:22 +0100 Subject: [PATCH 34/91] catch IndexError in parse_redeemScript --- lib/transaction.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/transaction.py b/lib/transaction.py index ddea8246..4ee57d4e 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -366,6 +366,8 @@ def parse_scriptSig(d, _bytes): try: m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1]) except NotRecognizedRedeemScript: + print_error("parse_scriptSig: cannot find address in input script (p2sh?)", + bh2u(_bytes)) # we could still guess: # d['address'] = hash160_to_p2sh(hash_160(decoded[-1][1])) return @@ -385,13 +387,15 @@ def parse_scriptSig(d, _bytes): def parse_redeemScript(s): dec2 = [ x for x in script_GetOp(s) ] - m = dec2[0][0] - opcodes.OP_1 + 1 - n = dec2[-2][0] - opcodes.OP_1 + 1 + try: + m = dec2[0][0] - opcodes.OP_1 + 1 + n = dec2[-2][0] - opcodes.OP_1 + 1 + except IndexError: + raise NotRecognizedRedeemScript() op_m = opcodes.OP_1 + m - 1 op_n = opcodes.OP_1 + n - 1 match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] if not match_decoded(dec2, match_multisig): - print_error("parse_redeemScript: not multisig", bh2u(s)) raise NotRecognizedRedeemScript() x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys] From 172efb3611e1a493c324d9e78ae9e461852e32bd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Feb 2018 17:48:51 +0100 Subject: [PATCH 35/91] follow-up 0f16bcdc1ff5cda748f3079d7efd983b426277fb --- gui/qt/history_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 7e871aa2..04d27f6d 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -119,7 +119,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): if value and value < 0: item.setForeground(3, QBrush(QColor("#BC1E1E"))) item.setForeground(4, QBrush(QColor("#BC1E1E"))) - if not fiat_default: + if fiat_value and not fiat_default: item.setForeground(6, QBrush(QColor("#1E1EFF"))) if tx_hash: item.setData(0, Qt.UserRole, tx_hash) From 7f04c305676df739b8ba00137df22011f9477488 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Feb 2018 18:10:58 +0100 Subject: [PATCH 36/91] qt: if cannot load wallet, print trace --- gui/qt/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gui/qt/__init__.py b/gui/qt/__init__.py index 30270d17..f7abac67 100644 --- a/gui/qt/__init__.py +++ b/gui/qt/__init__.py @@ -25,6 +25,7 @@ import signal import sys +import traceback try: @@ -192,7 +193,8 @@ class ElectrumGui: else: try: wallet = self.daemon.load_wallet(path, None) - except BaseException as e: + except BaseException as e: + traceback.print_exc(file=sys.stdout) d = QMessageBox(QMessageBox.Warning, _('Error'), 'Cannot load wallet:\n' + str(e)) d.exec_() return @@ -243,8 +245,7 @@ class ElectrumGui: return except GoBack: return - except: - import traceback + except BaseException as e: traceback.print_exc(file=sys.stdout) return self.timer.start() From 909c063eb1922d0a7e4b61d5538cb5b34edbf1e7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Feb 2018 19:42:35 +0100 Subject: [PATCH 37/91] contact/invoice import: better exception handling. see #3904 --- gui/qt/contact_list.py | 7 +++++-- gui/qt/invoice_list.py | 7 +++++-- lib/contacts.py | 11 +++++++++-- lib/paymentrequest.py | 8 ++++++-- lib/util.py | 13 +++++++++++++ 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py index 7e8dda1e..a1794459 100644 --- a/gui/qt/contact_list.py +++ b/gui/qt/contact_list.py @@ -26,7 +26,7 @@ import webbrowser from electrum.i18n import _ from electrum.bitcoin import is_address -from electrum.util import block_explorer_URL +from electrum.util import block_explorer_URL, FileImportFailed from electrum.plugins import run_hook from PyQt5.QtGui import * from PyQt5.QtCore import * @@ -57,7 +57,10 @@ class ContactList(MyTreeWidget): filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder) if not filename: return - self.parent.contacts.import_file(filename) + try: + self.parent.contacts.import_file(filename) + except FileImportFailed as e: + self.parent.show_message(str(e)) self.on_update() def create_menu(self, position): diff --git a/gui/qt/invoice_list.py b/gui/qt/invoice_list.py index 19cfea60..a4a8374f 100644 --- a/gui/qt/invoice_list.py +++ b/gui/qt/invoice_list.py @@ -25,7 +25,7 @@ from .util import * from electrum.i18n import _ -from electrum.util import format_time +from electrum.util import format_time, FileImportFailed class InvoiceList(MyTreeWidget): @@ -61,7 +61,10 @@ class InvoiceList(MyTreeWidget): filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder) if not filename: return - self.parent.invoices.import_file(filename) + try: + self.parent.invoices.import_file(filename) + except FileImportFailed as e: + self.parent.show_message(str(e)) self.on_update() def create_menu(self, position): diff --git a/lib/contacts.py b/lib/contacts.py index 3b5a3255..5157adc4 100644 --- a/lib/contacts.py +++ b/lib/contacts.py @@ -23,9 +23,12 @@ import re import dns import json +import traceback +import sys from . import bitcoin from . import dnssec +from .util import FileImportFailed, FileImportFailedEncrypted class Contacts(dict): @@ -51,8 +54,12 @@ class Contacts(dict): try: with open(path, 'r') as f: d = self._validate(json.loads(f.read())) - except: - return + except json.decoder.JSONDecodeError: + traceback.print_exc(file=sys.stderr) + raise FileImportFailedEncrypted() + except BaseException: + traceback.print_exc(file=sys.stdout) + raise FileImportFailed() self.update(d) self.save() diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py index 8c9c6009..c1e25441 100644 --- a/lib/paymentrequest.py +++ b/lib/paymentrequest.py @@ -40,6 +40,7 @@ except ImportError: from . import bitcoin from . import util from .util import print_error, bh2u, bfh +from .util import FileImportFailed, FileImportFailedEncrypted from . import transaction from . import x509 from . import rsakey @@ -471,9 +472,12 @@ class InvoiceStore(object): with open(path, 'r') as f: d = json.loads(f.read()) self.load(d) - except: + except json.decoder.JSONDecodeError: traceback.print_exc(file=sys.stderr) - return + raise FileImportFailedEncrypted() + except BaseException: + traceback.print_exc(file=sys.stdout) + raise FileImportFailed() self.save() def save(self): diff --git a/lib/util.py b/lib/util.py index a59f2a5a..60723810 100644 --- a/lib/util.py +++ b/lib/util.py @@ -58,6 +58,19 @@ class InvalidPassword(Exception): def __str__(self): return _("Incorrect password") + +class FileImportFailed(Exception): + def __str__(self): + return _("Failed to import file.") + + +class FileImportFailedEncrypted(FileImportFailed): + def __str__(self): + return (_('Failed to import file.') + ' ' + + _('Perhaps it is encrypted...') + '\n' + + _('Importing encrypted files is not supported.')) + + # Throw this exception to unwind the stack like when an error occurs. # However unlike other exceptions the user won't be informed. class UserCancelled(Exception): From 08b9908f6e9a86162061293e1dad9caf6b14e3e0 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Wed, 14 Feb 2018 21:48:28 +0100 Subject: [PATCH 38/91] Make it harder for altcoins to accidentally use our crashhub --- gui/qt/exception_window.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gui/qt/exception_window.py b/gui/qt/exception_window.py index a603af5e..a15bbe25 100644 --- a/gui/qt/exception_window.py +++ b/gui/qt/exception_window.py @@ -34,7 +34,7 @@ from PyQt5.QtWidgets import * from electrum.i18n import _ import sys -from electrum import ELECTRUM_VERSION +from electrum import ELECTRUM_VERSION, bitcoin issue_template = """

Traceback

@@ -105,6 +105,10 @@ class Exception_Window(QWidget):
         self.show()
 
     def send_report(self):
+        if bitcoin.NetworkConstants.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in report_server:
+            # Gah! Some kind of altcoin wants to send us crash reports.
+            self.main_window.show_critical("Please report this issue manually.")
+            return
         report = self.get_traceback_info()
         report.update(self.get_additional_info())
         report = json.dumps(report)

From e3a082d58dff5d2cf4ca8b42fc285ff34b995aba Mon Sep 17 00:00:00 2001
From: Johann Bauer 
Date: Wed, 14 Feb 2018 21:58:35 +0100
Subject: [PATCH 39/91] Fix #3907

---
 lib/exchange_rate.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py
index ffd4c18a..cb43cd6e 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -79,7 +79,7 @@ class ExchangeBase(PrintError):
         return h, timestamp
 
     def get_historical_rates_safe(self, ccy, cache_dir):
-        h, timestamp = self.read_historical_rates()
+        h, timestamp = self.read_historical_rates(ccy, cache_dir)
         if h is None or time.time() - timestamp < 24*3600:
             try:
                 self.print_error("requesting fx history for", ccy)
@@ -89,6 +89,7 @@ class ExchangeBase(PrintError):
             except BaseException as e:
                 self.print_error("failed fx history:", e)
                 return
+            filename = os.path.join(cache_dir, self.name() + '_' + ccy)
             with open(filename, 'w') as f:
                 f.write(json.dumps(h))
         self.history[ccy] = h

From 7ff32877f7062bef773b48d56c92cb06b08e10ce Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Thu, 15 Feb 2018 15:31:27 +0100
Subject: [PATCH 40/91] replace test that should never happen

---
 lib/wallet.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index 7f787672..81091ac1 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -731,10 +731,7 @@ class Abstract_Wallet(PrintError):
                 if spending_tx_hash is None:
                     continue
                 # this outpoint (ser) has already been spent, by spending_tx
-                if spending_tx_hash not in self.transactions:
-                    # can't find this txn: delete and ignore it
-                    self.spent_outpoints.pop(ser)
-                    continue
+                assert spending_tx_hash in self.transactions
                 conflicting_txns |= {spending_tx_hash}
             txid = tx.txid()
             if txid in conflicting_txns:

From 6b09d478a5d29dbae5181c61dabcfd7c2efd0bc2 Mon Sep 17 00:00:00 2001
From: Calin Culianu 
Date: Thu, 15 Feb 2018 14:01:00 +0200
Subject: [PATCH 41/91] Fixup to get PyQt5 5.10 working ok and looking right on
 Mac

---
 contrib/build-osx/osx.spec                      | 17 ++++++++++++++++-
 .../requirements-binaries.txt                   |  4 ++--
 2 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec
index 2efda9f1..bb48dddf 100644
--- a/contrib/build-osx/osx.spec
+++ b/contrib/build-osx/osx.spec
@@ -1,6 +1,6 @@
 # -*- mode: python -*-
 
-from PyInstaller.utils.hooks import collect_data_files, collect_submodules
+from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
 
 import sys
 import os
@@ -40,6 +40,20 @@ datas += collect_data_files('trezorlib')
 datas += collect_data_files('btchip')
 datas += collect_data_files('keepkeylib')
 
+# We had an issue with PyQt 5.10 not picking up the libqmacstyles.dylib properly,
+# and thus Electrum looking terrible on Mac.
+# The below 3 statements are a workaround for that issue.
+# This should 'do nothing bad' in any case should a future version of PyQt5 not even
+# need this.
+binaries = []
+dylibs_in_pyqt5 = collect_dynamic_libs('PyQt5', 'DUMMY_NOT_USED')
+for tuple in dylibs_in_pyqt5:
+    # find libqmacstyle.dylib ...
+    if "libqmacstyle.dylib" in tuple[0]:
+        # .. and include all the .dylibs in that dir in our 'binaries' PyInstaller spec
+        binaries += [( os.path.dirname(tuple[0]) + '/*.dylib', 'PyQt5/Qt/plugins/styles' )]
+        break
+ 
 # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
 a = Analysis([electrum+MAIN_SCRIPT,
               electrum+'gui/qt/main_window.py',
@@ -57,6 +71,7 @@ a = Analysis([electrum+MAIN_SCRIPT,
               electrum+'plugins/keepkey/qt.py',
               electrum+'plugins/ledger/qt.py',
               ],
+             binaries=binaries,
              datas=datas,
              hiddenimports=hiddenimports,
              hookspath=[])
diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt
index af90b89d..381b4378 100644
--- a/contrib/deterministic-build/requirements-binaries.txt
+++ b/contrib/deterministic-build/requirements-binaries.txt
@@ -1,5 +1,5 @@
 pycryptodomex==3.4.12
-PyQt5==5.9
+PyQt5==5.10
 sip==4.19.7
 six==1.11.0
-websocket-client==0.46.0
\ No newline at end of file
+websocket-client==0.46.0

From fe1e412f010f410c266c0f73dab64c3ab0488348 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Thu, 15 Feb 2018 17:30:40 +0100
Subject: [PATCH 42/91] catch some exceptions during GUI init

---
 gui/qt/__init__.py | 12 ++++++++++--
 lib/daemon.py      |  8 +++++++-
 2 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/gui/qt/__init__.py b/gui/qt/__init__.py
index f7abac67..0879208f 100644
--- a/gui/qt/__init__.py
+++ b/gui/qt/__init__.py
@@ -195,7 +195,8 @@ class ElectrumGui:
                 wallet = self.daemon.load_wallet(path, None)
             except BaseException as e:
                 traceback.print_exc(file=sys.stdout)
-                d = QMessageBox(QMessageBox.Warning, _('Error'), 'Cannot load wallet:\n' + str(e))
+                d = QMessageBox(QMessageBox.Warning, _('Error'),
+                                _('Cannot load wallet:') + '\n' + str(e))
                 d.exec_()
                 return
             if not wallet:
@@ -212,7 +213,14 @@ class ElectrumGui:
                     return
                 wallet.start_threads(self.daemon.network)
                 self.daemon.add_wallet(wallet)
-            w = self.create_window_for_wallet(wallet)
+            try:
+                w = self.create_window_for_wallet(wallet)
+            except BaseException as e:
+                traceback.print_exc(file=sys.stdout)
+                d = QMessageBox(QMessageBox.Warning, _('Error'),
+                                _('Cannot create window for wallet:') + '\n' + str(e))
+                d.exec_()
+                return
         if uri:
             w.pay_to_URI(uri)
         w.bring_to_top()
diff --git a/lib/daemon.py b/lib/daemon.py
index f8497144..bebcf404 100644
--- a/lib/daemon.py
+++ b/lib/daemon.py
@@ -25,6 +25,8 @@
 import ast
 import os
 import time
+import traceback
+import sys
 
 # from jsonrpc import JSONRPCResponseManager
 import jsonrpclib
@@ -300,4 +302,8 @@ class Daemon(DaemonThread):
             gui_name = 'qt'
         gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui'])
         self.gui = gui.ElectrumGui(config, self, plugins)
-        self.gui.main()
+        try:
+            self.gui.main()
+        except BaseException as e:
+            traceback.print_exc(file=sys.stdout)
+            # app will exit now

From e512e9c0e81890326b0e9ce3edaa2c6feb06bcbc Mon Sep 17 00:00:00 2001
From: Johann Bauer 
Date: Thu, 15 Feb 2018 22:23:10 +0100
Subject: [PATCH 43/91] Simplify pyinstaller installation

---
 contrib/build-osx/make_osx                |  2 +-
 contrib/build-wine/build.sh               |  3 +--
 contrib/build-wine/prepare-pyinstaller.sh | 24 -----------------------
 contrib/build-wine/prepare-wine.sh        |  4 ++++
 4 files changed, 6 insertions(+), 27 deletions(-)
 delete mode 100755 contrib/build-wine/prepare-pyinstaller.sh

diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx
index 70793499..a2f7b50f 100755
--- a/contrib/build-osx/make_osx
+++ b/contrib/build-osx/make_osx
@@ -37,7 +37,7 @@ fail "Unable to use Python $PYTHON_VERSION"
 
 
 info "Installing pyinstaller"
-python3 -m pip install pyinstaller -I --user || fail "Could not install pyinstaller"
+python3 -m pip install git+https://github.com/ecdsa/pyinstaller@fix_2952 -I --user || fail "Could not install pyinstaller"
 
 info "Using these versions for building $PACKAGE:"
 sw_vers
diff --git a/contrib/build-wine/build.sh b/contrib/build-wine/build.sh
index a4e39adf..8bf65062 100755
--- a/contrib/build-wine/build.sh
+++ b/contrib/build-wine/build.sh
@@ -13,8 +13,7 @@ echo "Clearing $here/build and $here/dist..."
 rm "$here"/build/* -rf
 rm "$here"/dist/* -rf
 
-$here/prepare-wine.sh && \
-$here/prepare-pyinstaller.sh || exit 1
+$here/prepare-wine.sh || exit 1
 
 echo "Resetting modification time in C:\Python..."
 # (Because of some bugs in pyinstaller)
diff --git a/contrib/build-wine/prepare-pyinstaller.sh b/contrib/build-wine/prepare-pyinstaller.sh
deleted file mode 100755
index cf8a326c..00000000
--- a/contrib/build-wine/prepare-pyinstaller.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/bash
-PYTHON_VERSION=3.5.4
-
-PYINSTALLER_GIT_URL=https://github.com/ecdsa/pyinstaller.git
-BRANCH=fix_2952
-
-export WINEPREFIX=/opt/wine64
-PYHOME=c:/python$PYTHON_VERSION
-PYTHON="wine $PYHOME/python.exe -OO -B"
-
-cd `dirname $0`
-set -e
-cd tmp
-if [ ! -d "pyinstaller" ]; then
-    git clone -b $BRANCH $PYINSTALLER_GIT_URL pyinstaller
-fi
-
-cd pyinstaller
-git pull
-git checkout $BRANCH
-$PYTHON setup.py install
-cd ..
-
-wine "C:/python$PYTHON_VERSION/scripts/pyinstaller.exe" -v
diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh
index 42828be7..d62b4c63 100755
--- a/contrib/build-wine/prepare-wine.sh
+++ b/contrib/build-wine/prepare-wine.sh
@@ -80,6 +80,10 @@ $PYTHON -m pip install win_inet_pton==1.0.1
 
 $PYTHON -m pip install -r ../../deterministic-build/requirements-binaries.txt
 
+# Install PyInstaller
+
+$PYTHON -m pip install git+https://github.com/ecdsa/pyinstaller@fix_2952
+
 # Install ZBar
 #wget -q -O zbar.exe "https://sourceforge.net/projects/zbar/files/zbar/0.10/zbar-0.10-setup.exe/download"
 #wine zbar.exe

From 945ba8decf2fb1c2d117edcb9252af8717788eba Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Fri, 16 Feb 2018 13:20:56 +0100
Subject: [PATCH 44/91] fix #3912

---
 lib/exchange_rate.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py
index cb43cd6e..6931e338 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -107,6 +107,8 @@ class ExchangeBase(PrintError):
         return []
 
     def historical_rate(self, ccy, d_t):
+        if d_t is None:
+            return None
         return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'))
 
     def get_currencies(self):
@@ -519,6 +521,8 @@ class FxThread(ThreadJob):
         return _("No data")
 
     def history_rate(self, d_t):
+        if d_t is None:
+            return None
         rate = self.exchange.historical_rate(self.ccy, d_t)
         # Frequently there is no rate for today, until tomorrow :)
         # Use spot quotes in that case

From 63e402c2d7c063d5f078968c338534337fe89545 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Fri, 16 Feb 2018 13:37:38 +0100
Subject: [PATCH 45/91] wallet.clear_history: clear txns and verified txns too

---
 lib/wallet.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/lib/wallet.py b/lib/wallet.py
index 81091ac1..9a3c3346 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -271,6 +271,8 @@ class Abstract_Wallet(PrintError):
                 self.pruned_txo = {}
                 self.spent_outpoints = {}
                 self.history = {}
+                self.verified_tx = {}
+                self.transactions = {}
                 self.save_transactions()
 
     @profiler

From 76bf53b2624813a4b4e95d1b59bdc2b19673d3d9 Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Fri, 16 Feb 2018 13:54:18 +0100
Subject: [PATCH 46/91] simplify add_transaction

---
 lib/wallet.py | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index 9a3c3346..b1bad2ac 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -735,15 +735,11 @@ class Abstract_Wallet(PrintError):
                 # this outpoint (ser) has already been spent, by spending_tx
                 assert spending_tx_hash in self.transactions
                 conflicting_txns |= {spending_tx_hash}
-            txid = tx.txid()
-            if txid in conflicting_txns:
-                # this tx is already in history, so it conflicts with itself
-                if len(conflicting_txns) > 1:
-                    raise Exception('Found conflicting transactions already in wallet history.')
-                conflicting_txns -= {txid}
             return conflicting_txns
 
     def add_transaction(self, tx_hash, tx):
+        if tx in self.transactions:
+            return True
         is_coinbase = tx.inputs()[0]['type'] == 'coinbase'
         related = False
         with self.transaction_lock:

From bd333f16e022ccab972b7cfc9121a2bff2dcb665 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Fri, 16 Feb 2018 15:17:55 +0100
Subject: [PATCH 47/91] follow-up 76bf53b2624813a4b4e95d1b59bdc2b19673d3d9

---
 lib/wallet.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index b1bad2ac..3d59e293 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -720,8 +720,7 @@ class Abstract_Wallet(PrintError):
     def get_conflicting_transactions(self, tx):
         """Returns a set of transaction hashes from the wallet history that are
         directly conflicting with tx, i.e. they have common outpoints being
-        spent with tx. If the tx is already in wallet history, that will not be
-        reported as a conflict.
+        spent with tx.
         """
         conflicting_txns = set()
         with self.transaction_lock:

From c3fd7db3107cf0dc64c68b6a069fac4aec148db5 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Fri, 16 Feb 2018 15:20:12 +0100
Subject: [PATCH 48/91] fix minor bug in qt/history_list

context menu could have duplicated entries
---
 gui/qt/history_list.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
index 04d27f6d..142d9828 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -64,7 +64,9 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
             headers.extend(['%s '%fx.ccy + _('Value')])
             headers.extend(['%s '%fx.ccy + _('Acquisition price')])
             headers.extend(['%s '%fx.ccy + _('Capital Gains')])
-            self.editable_columns.extend([6])
+            fiat_value_column = 6
+            if fiat_value_column not in self.editable_columns:
+                self.editable_columns.extend([fiat_value_column])
         self.update_headers(headers)
 
     def get_domain(self):

From c4d31674abec59b8fddbbd480dfe9d96c9cca9e8 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Fri, 16 Feb 2018 16:12:08 +0100
Subject: [PATCH 49/91] follow-up c3fd7db3107cf0dc64c68b6a069fac4aec148db5:
 editable_columns is now a set

---
 gui/qt/history_list.py | 8 ++++----
 gui/qt/util.py         | 4 +++-
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
index 142d9828..fcd4deb4 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -58,15 +58,15 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
         self.setColumnHidden(1, True)
 
     def refresh_headers(self):
-        headers = ['', '', _('Date'), _('Description') , _('Amount'), _('Balance')]
+        headers = ['', '', _('Date'), _('Description'), _('Amount'), _('Balance')]
         fx = self.parent.fx
         if fx and fx.show_history():
             headers.extend(['%s '%fx.ccy + _('Value')])
             headers.extend(['%s '%fx.ccy + _('Acquisition price')])
             headers.extend(['%s '%fx.ccy + _('Capital Gains')])
-            fiat_value_column = 6
-            if fiat_value_column not in self.editable_columns:
-                self.editable_columns.extend([fiat_value_column])
+            self.editable_columns |= {6}
+        else:
+            self.editable_columns -= {6}
         self.update_headers(headers)
 
     def get_domain(self):
diff --git a/gui/qt/util.py b/gui/qt/util.py
index 5dbda84a..c0bdf62e 100644
--- a/gui/qt/util.py
+++ b/gui/qt/util.py
@@ -389,7 +389,9 @@ class MyTreeWidget(QTreeWidget):
         self.editor = None
         self.pending_update = False
         if editable_columns is None:
-            editable_columns = [stretch_column]
+            editable_columns = {stretch_column}
+        else:
+            editable_columns = set(editable_columns)
         self.editable_columns = editable_columns
         self.setItemDelegate(ElectrumItemDelegate(self))
         self.itemDoubleClicked.connect(self.on_doubleclick)

From 586074cb0f4b2506c0ee3021eec90210aa52ba0a Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Sat, 17 Feb 2018 10:58:02 +0100
Subject: [PATCH 50/91] simplify local transactions:  - restrict conflict
 detection own inputs  - save local transactions only if they are own

---
 lib/wallet.py | 103 ++++++++++++++++++++++++--------------------------
 1 file changed, 50 insertions(+), 53 deletions(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index 3d59e293..46b7de51 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -278,12 +278,10 @@ class Abstract_Wallet(PrintError):
     @profiler
     def build_spent_outpoints(self):
         self.spent_outpoints = {}
-        for txid, tx in self.transactions.items():
-            for txi in tx.inputs():
-                ser = Transaction.get_outpoint_from_txin(txi)
-                if ser is None:
-                    continue
-                self.spent_outpoints[ser] = txid
+        for txid in self.txi:
+            for addr, l in self.txi[txid].items():
+                for ser, v in l:
+                    self.spent_outpoints[ser] = txid
 
     @profiler
     def check_history(self):
@@ -709,7 +707,12 @@ class Abstract_Wallet(PrintError):
                     h.append((tx_hash, tx_height))
         return h
 
-    def find_pay_to_pubkey_address(self, prevout_hash, prevout_n):
+    def get_txin_address(self, txi):
+        addr = txi.get('address')
+        if addr != "(pubkey)":
+            return addr
+        prevout_hash = x.get('prevout_hash')
+        prevout_n = x.get('prevout_n')
         dd = self.txo.get(prevout_hash, {})
         for addr, l in dd.items():
             for n, v, is_cb in l:
@@ -717,6 +720,16 @@ class Abstract_Wallet(PrintError):
                     self.print_error("found pay-to-pubkey address:", addr)
                     return addr
 
+    def get_txout_address(self, txo):
+        _type, x, v = txo
+        if _type == TYPE_ADDRESS:
+            addr = x
+        elif _type == TYPE_PUBKEY:
+            addr = bitcoin.public_key_to_p2pkh(bfh(x))
+        else:
+            addr = None
+        return addr
+
     def get_conflicting_transactions(self, tx):
         """Returns a set of transaction hashes from the wallet history that are
         directly conflicting with tx, i.e. they have common outpoints being
@@ -737,11 +750,19 @@ class Abstract_Wallet(PrintError):
             return conflicting_txns
 
     def add_transaction(self, tx_hash, tx):
-        if tx in self.transactions:
-            return True
-        is_coinbase = tx.inputs()[0]['type'] == 'coinbase'
-        related = False
         with self.transaction_lock:
+            if tx in self.transactions:
+                return True
+            is_coinbase = tx.inputs()[0]['type'] == 'coinbase'
+            tx_height = self.get_tx_height(tx_hash)[0]
+            is_mine = any([self.is_mine(txin['address']) for txin in tx.inputs()])
+            # do not save if tx is local and not mine
+            if tx_height == TX_HEIGHT_LOCAL and not is_mine:
+                return False
+            # raise exception if unrelated to wallet
+            is_for_me = any([self.is_mine(self.get_txout_address(txo)) for txo in tx.outputs()])
+            if not is_mine and not is_for_me:
+                raise UnrelatedTransactionException()
             # Find all conflicting transactions.
             # In case of a conflict,
             #     1. confirmed > mempool > local
@@ -751,7 +772,6 @@ class Abstract_Wallet(PrintError):
             #     or drop this txn
             conflicting_txns = self.get_conflicting_transactions(tx)
             if conflicting_txns:
-                tx_height = self.get_tx_height(tx_hash)[0]
                 existing_mempool_txn = any(
                     self.get_tx_height(tx_hash2)[0] in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT)
                     for tx_hash2 in conflicting_txns)
@@ -771,21 +791,17 @@ class Abstract_Wallet(PrintError):
                     to_remove |= self.get_depending_transactions(conflicting_tx_hash)
                 for tx_hash2 in to_remove:
                     self.remove_transaction(tx_hash2)
-
             # add inputs
             self.txi[tx_hash] = d = {}
             for txi in tx.inputs():
-                addr = txi.get('address')
+                addr = self.get_txin_address(txi)
                 if txi['type'] != 'coinbase':
                     prevout_hash = txi['prevout_hash']
                     prevout_n = txi['prevout_n']
                     ser = prevout_hash + ':%d'%prevout_n
                     self.spent_outpoints[ser] = tx_hash
-                if addr == "(pubkey)":
-                    addr = self.find_pay_to_pubkey_address(prevout_hash, prevout_n)
                 # find value from prev output
                 if addr and self.is_mine(addr):
-                    related = True
                     dd = self.txo.get(prevout_hash, {})
                     for n, v, is_cb in dd.get(addr, []):
                         if n == prevout_n:
@@ -795,20 +811,13 @@ class Abstract_Wallet(PrintError):
                             break
                     else:
                         self.pruned_txo[ser] = tx_hash
-
             # add outputs
             self.txo[tx_hash] = d = {}
             for n, txo in enumerate(tx.outputs()):
+                v = txo[2]
                 ser = tx_hash + ':%d'%n
-                _type, x, v = txo
-                if _type == TYPE_ADDRESS:
-                    addr = x
-                elif _type == TYPE_PUBKEY:
-                    addr = bitcoin.public_key_to_p2pkh(bfh(x))
-                else:
-                    addr = None
+                addr = self.get_txout_address(txo)
                 if addr and self.is_mine(addr):
-                    related = True
                     if d.get(addr) is None:
                         d[addr] = []
                     d[addr].append((n, v, is_coinbase))
@@ -820,30 +829,20 @@ class Abstract_Wallet(PrintError):
                     if dd.get(addr) is None:
                         dd[addr] = []
                     dd[addr].append((ser, v))
-
-            if not related:
-                raise UnrelatedTransactionException()
-
             # save
             self.transactions[tx_hash] = tx
             return True
 
     def remove_transaction(self, tx_hash):
         def undo_spend(outpoint_to_txid_map):
-            if tx:
-                # if we have the tx, this should often be faster
-                for txi in tx.inputs():
-                    ser = Transaction.get_outpoint_from_txin(txi)
-                    outpoint_to_txid_map.pop(ser, None)
-            else:
-                for ser, hh in list(outpoint_to_txid_map.items()):
-                    if hh == tx_hash:
+            for addr, l in self.txi[tx_hash].items():
+                for ser, v in l:
+                    if ser in outpoint_to_txid:
                         outpoint_to_txid_map.pop(ser)
 
         with self.transaction_lock:
             self.print_error("removing tx from history", tx_hash)
-            #tx = self.transactions.pop(tx_hash)
-            tx = self.transactions.get(tx_hash, None)
+            self.transactions.pop(tx_hash)
             undo_spend(self.pruned_txo)
             undo_spend(self.spent_outpoints)
 
@@ -873,13 +872,16 @@ class Abstract_Wallet(PrintError):
 
     def receive_history_callback(self, addr, hist, tx_fees):
         with self.lock:
-            old_hist = self.history.get(addr, [])
+            old_hist = self.get_address_history(addr)
             for tx_hash, height in old_hist:
                 if (tx_hash, height) not in hist:
-                    # make tx local
-                    self.unverified_tx.pop(tx_hash, None)
-                    self.verified_tx.pop(tx_hash, None)
-                    self.verifier.merkle_roots.pop(tx_hash, None)
+                    # make tx local if is_mine, else remove it
+                    if self.txi[tx_hash] != {}:
+                        self.unverified_tx.pop(tx_hash, None)
+                        self.verified_tx.pop(tx_hash, None)
+                        self.verifier.merkle_roots.pop(tx_hash, None)
+                    else:
+                        self.remove_transaction(tx_hash)
             self.history[addr] = hist
 
         for tx_hash, tx_height in hist:
@@ -981,14 +983,9 @@ class Abstract_Wallet(PrintError):
                 output_addresses = []
                 for x in tx.inputs():
                     if x['type'] == 'coinbase': continue
-                    addr = x.get('address')
-                    if addr == None: continue
-                    if addr == "(pubkey)":
-                        prevout_hash = x.get('prevout_hash')
-                        prevout_n = x.get('prevout_n')
-                        _addr = self.wallet.find_pay_to_pubkey_address(prevout_hash, prevout_n)
-                        if _addr:
-                            addr = _addr
+                    addr = self.get_txin_address(x)
+                    if addr is None:
+                        continue
                     input_addresses.append(addr)
                 for addr, v in tx.get_outputs():
                     output_addresses.append(addr)

From 0d758a650da149fdd866411370be21f48a5f2a9b Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Sat, 17 Feb 2018 15:51:33 +0100
Subject: [PATCH 51/91] follow-up 586074cb0f4b2506c0ee3021eec90210aa52ba0a

---
 lib/wallet.py | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index 46b7de51..7f9ec812 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -278,8 +278,8 @@ class Abstract_Wallet(PrintError):
     @profiler
     def build_spent_outpoints(self):
         self.spent_outpoints = {}
-        for txid in self.txi:
-            for addr, l in self.txi[txid].items():
+        for txid, items in self.txi.items():
+            for addr, l in items.items():
                 for ser, v in l:
                     self.spent_outpoints[ser] = txid
 
@@ -711,8 +711,8 @@ class Abstract_Wallet(PrintError):
         addr = txi.get('address')
         if addr != "(pubkey)":
             return addr
-        prevout_hash = x.get('prevout_hash')
-        prevout_n = x.get('prevout_n')
+        prevout_hash = txi.get('prevout_hash')
+        prevout_n = txi.get('prevout_n')
         dd = self.txo.get(prevout_hash, {})
         for addr, l in dd.items():
             for n, v, is_cb in l:
@@ -837,12 +837,11 @@ class Abstract_Wallet(PrintError):
         def undo_spend(outpoint_to_txid_map):
             for addr, l in self.txi[tx_hash].items():
                 for ser, v in l:
-                    if ser in outpoint_to_txid:
-                        outpoint_to_txid_map.pop(ser)
+                    outpoint_to_txid_map.pop(ser, None)
 
         with self.transaction_lock:
             self.print_error("removing tx from history", tx_hash)
-            self.transactions.pop(tx_hash)
+            self.transactions.pop(tx_hash, None)
             undo_spend(self.pruned_txo)
             undo_spend(self.spent_outpoints)
 

From 008bffcea717efdbea422a3b432b7e45f113d2b2 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Sat, 17 Feb 2018 16:28:15 +0100
Subject: [PATCH 52/91] undo verification when removing txn

---
 lib/wallet.py | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index 7f9ec812..921a1bb8 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -758,6 +758,7 @@ class Abstract_Wallet(PrintError):
             is_mine = any([self.is_mine(txin['address']) for txin in tx.inputs()])
             # do not save if tx is local and not mine
             if tx_height == TX_HEIGHT_LOCAL and not is_mine:
+                # FIXME the test here should be for "not all is_mine"; cannot detect conflict in some cases
                 return False
             # raise exception if unrelated to wallet
             is_for_me = any([self.is_mine(self.get_txout_address(txo)) for txo in tx.outputs()])
@@ -874,12 +875,13 @@ class Abstract_Wallet(PrintError):
             old_hist = self.get_address_history(addr)
             for tx_hash, height in old_hist:
                 if (tx_hash, height) not in hist:
-                    # make tx local if is_mine, else remove it
-                    if self.txi[tx_hash] != {}:
-                        self.unverified_tx.pop(tx_hash, None)
-                        self.verified_tx.pop(tx_hash, None)
-                        self.verifier.merkle_roots.pop(tx_hash, None)
-                    else:
+                    # make tx local
+                    self.unverified_tx.pop(tx_hash, None)
+                    self.verified_tx.pop(tx_hash, None)
+                    self.verifier.merkle_roots.pop(tx_hash, None)
+                    # but remove completely if not is_mine
+                    if self.txi[tx_hash] == {}:
+                        # FIXME the test here should be for "not all is_mine"; cannot detect conflict in some cases
                         self.remove_transaction(tx_hash)
             self.history[addr] = hist
 

From 72a443b688cec8a922041aa9b2360100fd9a2c6f Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Sun, 18 Feb 2018 20:13:27 +0100
Subject: [PATCH 53/91] fix: disabling "use change addresses" did not work
 correctly

---
 lib/coinchooser.py | 9 ++++++++-
 lib/wallet.py      | 3 ++-
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/lib/coinchooser.py b/lib/coinchooser.py
index ffc5bfd8..c4ca7a15 100644
--- a/lib/coinchooser.py
+++ b/lib/coinchooser.py
@@ -25,7 +25,7 @@
 from collections import defaultdict, namedtuple
 from math import floor, log10
 
-from .bitcoin import sha256, COIN, TYPE_ADDRESS
+from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
 from .transaction import Transaction
 from .util import NotEnoughFunds, PrintError
 
@@ -240,6 +240,13 @@ class CoinChooserBase(PrintError):
         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)
diff --git a/lib/wallet.py b/lib/wallet.py
index 3d59e293..7d7488e2 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1123,7 +1123,8 @@ class Abstract_Wallet(PrintError):
                 if not change_addrs:
                     change_addrs = [random.choice(addrs)]
             else:
-                change_addrs = [inputs[0]['address']]
+                # coin_chooser will set change address
+                change_addrs = []
 
         # Fee estimator
         if fixed_fee is None:

From 826cf467d8638b0e1d17150815738ecb5ffc6c36 Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Thu, 15 Feb 2018 14:59:05 +0100
Subject: [PATCH 54/91] Improve wallet history tab: - use json-serializable
 types - add toolbar to history tab - add button to display time interval

---
 gui/qt/history_list.py | 206 ++++++++++++++++++++++++++++++++++++-----
 gui/qt/main_window.py  |  63 +------------
 lib/commands.py        |   2 +-
 lib/plot.py            |  11 +--
 lib/util.py            |  36 +++++++
 lib/wallet.py          |  80 +++++++++-------
 6 files changed, 273 insertions(+), 125 deletions(-)

diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
index fcd4deb4..cfe412db 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -24,6 +24,7 @@
 # SOFTWARE.
 
 import webbrowser
+import datetime
 
 from electrum.wallet import UnrelatedTransactionException, TX_HEIGHT_LOCAL
 from .util import *
@@ -31,6 +32,10 @@ from electrum.i18n import _
 from electrum.util import block_explorer_URL
 from electrum.util import timestamp_to_datetime, profiler
 
+try:
+    from electrum.plot import plot_history
+except:
+    plot_history = None
 
 # note: this list needs to be kept in sync with another in kivy
 TX_ICONS = [
@@ -56,6 +61,9 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
         AcceptFileDragDrop.__init__(self, ".txn")
         self.refresh_headers()
         self.setColumnHidden(1, True)
+        self.start_timestamp = None
+        self.end_timestamp = None
+        self.years = []
 
     def refresh_headers(self):
         headers = ['', '', _('Date'), _('Description'), _('Amount'), _('Balance')]
@@ -73,41 +81,154 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
         '''Replaced in address_dialog.py'''
         return self.wallet.get_addresses()
 
+    def on_combo(self, x):
+        s = self.period_combo.itemText(x)
+        if s == _('All'):
+            self.start_timestamp = None
+            self.end_timestamp = None
+        elif s == _('Custom'):
+            start_date = self.select_date()
+        else:
+            try:
+                year = int(s)
+            except:
+                return
+            start_date = datetime.datetime(year, 1, 1)
+            end_date = datetime.datetime(year+1, 1, 1)
+            self.start_timestamp = time.mktime(start_date.timetuple())
+            self.end_timestamp = time.mktime(end_date.timetuple())
+        self.update()
+
+    def get_list_header(self):
+        self.period_combo = QComboBox()
+        self.period_combo.addItems([_('All'), _('Custom')])
+        self.period_combo.activated.connect(self.on_combo)
+        self.summary_button = QPushButton(_('Summary'))
+        self.summary_button.pressed.connect(self.show_summary)
+        self.export_button = QPushButton(_('Export'))
+        self.export_button.pressed.connect(self.export_history_dialog)
+        self.plot_button = QPushButton(_('Plot'))
+        self.plot_button.pressed.connect(self.plot_history_dialog)
+        return self.period_combo, self.summary_button, self.export_button, self.plot_button
+
+    def select_date(self):
+        h = self.summary
+        d = WindowModalDialog(self, _("Custom dates"))
+        d.setMinimumSize(600, 150)
+        d.b = True
+        d.start_date = None
+        d.end_date = None
+        vbox = QVBoxLayout()
+        grid = QGridLayout()
+        start_edit = QPushButton()
+        def on_start():
+            start_edit.setText('')
+            d.b = True
+            d.start_date = None
+        start_edit.pressed.connect(on_start)
+        def on_end():
+            end_edit.setText('')
+            d.b = False
+            d.end_date = None
+        end_edit = QPushButton()
+        end_edit.pressed.connect(on_end)
+        grid.addWidget(QLabel(_("Start date")), 0, 0)
+        grid.addWidget(start_edit, 0, 1)
+        grid.addWidget(QLabel(_("End date")), 1, 0)
+        grid.addWidget(end_edit, 1, 1)
+        def on_date(date):
+            ts = time.mktime(date.toPyDate().timetuple())
+            if d.b:
+                d.start_date = ts
+                start_edit.setText(date.toString())
+            else:
+                d.end_date = ts
+                end_edit.setText(date.toString())
+        cal = QCalendarWidget()
+        cal.setGridVisible(True)
+        cal.clicked[QDate].connect(on_date)
+        vbox.addLayout(grid)
+        vbox.addWidget(cal)
+        vbox.addLayout(Buttons(OkButton(d), CancelButton(d)))
+        d.setLayout(vbox)
+        if d.exec_():
+            self.start_timestamp = d.start_date
+            self.end_timestamp = d.end_date
+            self.update()
+
+    def show_summary(self):
+        h = self.summary
+        format_amount = lambda x: self.parent.format_amount(x) + ' '+ self.parent.base_unit()
+        d = WindowModalDialog(self, _("Summary"))
+        d.setMinimumSize(600, 150)
+        vbox = QVBoxLayout()
+        grid = QGridLayout()
+        grid.addWidget(QLabel(_("Start")), 0, 0)
+        grid.addWidget(QLabel(h.get('start_date').isoformat(' ')), 0, 1)
+        grid.addWidget(QLabel(_("End")), 1, 0)
+        grid.addWidget(QLabel(h.get('end_date').isoformat(' ')), 1, 1)
+        grid.addWidget(QLabel(_("Initial balance")), 2, 0)
+        grid.addWidget(QLabel(format_amount(h['start_balance'].value)), 2, 1)
+        grid.addWidget(QLabel(str(h.get('start_fiat_balance'))), 2, 2)
+        grid.addWidget(QLabel(_("Final balance")), 4, 0)
+        grid.addWidget(QLabel(format_amount(h['end_balance'].value)), 4, 1)
+        grid.addWidget(QLabel(str(h.get('end_fiat_balance'))), 4, 2)
+        grid.addWidget(QLabel(_("Income")), 6, 0)
+        grid.addWidget(QLabel(str(h.get('fiat_income'))), 6, 2)
+        grid.addWidget(QLabel(_("Capital gains")), 7, 0)
+        grid.addWidget(QLabel(str(h.get('capital_gains'))), 7, 2)
+        grid.addWidget(QLabel(_("Unrealized gains")), 8, 0)
+        grid.addWidget(QLabel(str(h.get('unrealized_gains', ''))), 8, 2)
+        vbox.addLayout(grid)
+        vbox.addLayout(Buttons(CloseButton(d)))
+        d.setLayout(vbox)
+        d.exec_()
+
+    def plot_history_dialog(self):
+        if plot_history is None:
+            return
+        if len(self.transactions) > 0:
+            plt = plot_history(self.transactions)
+            plt.show()
+
     @profiler
     def on_update(self):
         self.wallet = self.parent.wallet
-        h = self.wallet.get_history(self.get_domain())
+        fx = self.parent.fx
+        r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=self.start_timestamp, to_timestamp=self.end_timestamp, fx=fx)
+        self.transactions = r['transactions']
+        self.summary = r['summary']
+        if not self.years and self.start_timestamp is None and self.end_timestamp is None:
+            self.years = [str(i) for i in range(self.summary['start_date'].year, self.summary['end_date'].year + 1)]
+            self.period_combo.insertItems(1, self.years)
         item = self.currentItem()
         current_tx = item.data(0, Qt.UserRole) if item else None
         self.clear()
-        fx = self.parent.fx
         if fx: fx.history_used_spot = False
-        for h_item in h:
-            tx_hash, height, conf, timestamp, value, balance = h_item
+        for tx_item in self.transactions:
+            tx_hash = tx_item['txid']
+            height = tx_item['height']
+            conf = tx_item['confirmations']
+            timestamp = tx_item['timestamp']
+            value = tx_item['value'].value
+            balance = tx_item['balance'].value
+            label = tx_item['label']
             status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
             has_invoice = self.wallet.invoices.paid.get(tx_hash)
             icon = QIcon(":icons/" + TX_ICONS[status])
             v_str = self.parent.format_amount(value, True, whitespaces=True)
             balance_str = self.parent.format_amount(balance, whitespaces=True)
-            label = self.wallet.get_label(tx_hash)
             entry = ['', tx_hash, status_str, label, v_str, balance_str]
             fiat_value = None
             if value is not None and fx and fx.show_history():
                 date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp)
-                fiat_value = self.wallet.get_fiat_value(tx_hash, fx.ccy)
-                if not fiat_value:
-                    fiat_value = fx.historical_value(value, date)
-                    fiat_default = True
-                else:
-                    fiat_default = False
+                fiat_value = tx_item['fiat_value'].value
                 value_str = fx.format_fiat(fiat_value)
                 entry.append(value_str)
                 # fixme: should use is_mine
                 if value < 0:
-                    ap, lp = self.wallet.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy)
-                    cg = None if lp is None or ap is None else lp - ap
-                    entry.append(fx.format_fiat(ap))
-                    entry.append(fx.format_fiat(cg))
+                    entry.append(fx.format_fiat(tx_item['acquisition_price'].value))
+                    entry.append(fx.format_fiat(tx_item['capital_gain'].value))
             item = QTreeWidgetItem(entry)
             item.setIcon(0, icon)
             item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else ""))
@@ -121,7 +242,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
             if value and value < 0:
                 item.setForeground(3, QBrush(QColor("#BC1E1E")))
                 item.setForeground(4, QBrush(QColor("#BC1E1E")))
-            if fiat_value and not fiat_default:
+            if fiat_value and not tx_item['fiat_default']:
                 item.setForeground(6, QBrush(QColor("#1E1EFF")))
             if tx_hash:
                 item.setData(0, Qt.UserRole, tx_hash)
@@ -183,25 +304,19 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
         else:
             column_title = self.headerItem().text(column)
             column_data = item.text(column)
-
         tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
         height, conf, timestamp = self.wallet.get_tx_height(tx_hash)
         tx = self.wallet.transactions.get(tx_hash)
         is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
         is_unconfirmed = height <= 0
         pr_key = self.wallet.invoices.paid.get(tx_hash)
-
         menu = QMenu()
-
         if height == TX_HEIGHT_LOCAL:
             menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash))
-
         menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
         for c in self.editable_columns:
             menu.addAction(_("Edit {}").format(self.headerItem().text(c)), lambda: self.editItem(item, c))
-
         menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
-
         if is_unconfirmed and tx:
             rbf = is_mine and not tx.is_final()
             if rbf:
@@ -219,13 +334,11 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
     def remove_local_tx(self, delete_tx):
         to_delete = {delete_tx}
         to_delete |= self.wallet.get_depending_transactions(delete_tx)
-
         question = _("Are you sure you want to remove this transaction?")
         if len(to_delete) > 1:
             question = _(
                 "Are you sure you want to remove this transaction and {} child transactions?".format(len(to_delete) - 1)
             )
-
         answer = QMessageBox.question(self.parent, _("Please confirm"), question, QMessageBox.Yes, QMessageBox.No)
         if answer == QMessageBox.No:
             return
@@ -246,3 +359,48 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
                 self.wallet.save_transactions(write=True)
                 # need to update at least: history_list, utxo_list, address_list
                 self.parent.need_update.set()
+
+    def export_history_dialog(self):
+        d = WindowModalDialog(self, _('Export History'))
+        d.setMinimumSize(400, 200)
+        vbox = QVBoxLayout(d)
+        defaultname = os.path.expanduser('~/electrum-history.csv')
+        select_msg = _('Select file to export your wallet transactions to')
+        hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
+        vbox.addLayout(hbox)
+        vbox.addStretch(1)
+        hbox = Buttons(CancelButton(d), OkButton(d, _('Export')))
+        vbox.addLayout(hbox)
+        #run_hook('export_history_dialog', self, hbox)
+        self.update()
+        if not d.exec_():
+            return
+        filename = filename_e.text()
+        if not filename:
+            return
+        try:
+            self.do_export_history(self.wallet, filename, csv_button.isChecked())
+        except (IOError, os.error) as reason:
+            export_error_label = _("Electrum was unable to produce a transaction export.")
+            self.parent.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
+            return
+        self.parent.show_message(_("Your wallet history has been successfully exported."))
+
+    def do_export_history(self, wallet, fileName, is_csv):
+        history = self.transactions
+        lines = []
+        for item in history:
+            if is_csv:
+                lines.append([item['txid'], item.get('label', ''), item['confirmations'], item['value'], item['date']])
+            else:
+                lines.append(item)
+        with open(fileName, "w+") as f:
+            if is_csv:
+                import csv
+                transaction = csv.writer(f, lineterminator='\n')
+                transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"])
+                for line in lines:
+                    transaction.writerow(line)
+            else:
+                from electrum.util import json_encode
+                f.write(json_encode(history))
diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
index df86e3e8..ad0dd77b 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -52,10 +52,6 @@ from electrum import Transaction
 from electrum import util, bitcoin, commands, coinchooser
 from electrum import paymentrequest
 from electrum.wallet import Multisig_Wallet
-try:
-    from electrum.plot import plot_history
-except:
-    plot_history = None
 
 from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
 from .qrcodewidget import QRCodeWidget, QRDialog
@@ -490,9 +486,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
         contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts())
         invoices_menu = wallet_menu.addMenu(_("Invoices"))
         invoices_menu.addAction(_("Import"), lambda: self.invoice_list.import_invoices())
-        hist_menu = wallet_menu.addMenu(_("&History"))
-        hist_menu.addAction("Plot", self.plot_history_dialog).setEnabled(plot_history is not None)
-        hist_menu.addAction("Export", self.export_history_dialog)
 
         wallet_menu.addSeparator()
         wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F"))
@@ -755,7 +748,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
         from .history_list import HistoryList
         self.history_list = l = HistoryList(self)
         l.searchable_list = l
-        return l
+        return self.create_list_tab(l, l.get_list_header())
 
     def show_address(self, addr):
         from . import address_dialog
@@ -2458,60 +2451,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
         except (IOError, os.error) as reason:
             self.show_critical(_("Electrum was unable to export your labels.") + "\n" + str(reason))
 
-    def export_history_dialog(self):
-        d = WindowModalDialog(self, _('Export History'))
-        d.setMinimumSize(400, 200)
-        vbox = QVBoxLayout(d)
-        defaultname = os.path.expanduser('~/electrum-history.csv')
-        select_msg = _('Select file to export your wallet transactions to')
-        hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
-        vbox.addLayout(hbox)
-        vbox.addStretch(1)
-        hbox = Buttons(CancelButton(d), OkButton(d, _('Export')))
-        vbox.addLayout(hbox)
-        run_hook('export_history_dialog', self, hbox)
-        self.update()
-        if not d.exec_():
-            return
-        filename = filename_e.text()
-        if not filename:
-            return
-        try:
-            self.do_export_history(self.wallet, filename, csv_button.isChecked())
-        except (IOError, os.error) as reason:
-            export_error_label = _("Electrum was unable to produce a transaction export.")
-            self.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
-            return
-        self.show_message(_("Your wallet history has been successfully exported."))
-
-    def plot_history_dialog(self):
-        if plot_history is None:
-            return
-        wallet = self.wallet
-        history = wallet.get_history()
-        if len(history) > 0:
-            plt = plot_history(self.wallet, history)
-            plt.show()
-
-    def do_export_history(self, wallet, fileName, is_csv):
-        history = wallet.export_history(fx=self.fx)
-        lines = []
-        for item in history:
-            if is_csv:
-                lines.append([item['txid'], item.get('label', ''), item['confirmations'], item['value'], item['date']])
-            else:
-                lines.append(item)
-
-        with open(fileName, "w+") as f:
-            if is_csv:
-                transaction = csv.writer(f, lineterminator='\n')
-                transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"])
-                for line in lines:
-                    transaction.writerow(line)
-            else:
-                import json
-                f.write(json.dumps(lines, indent=4))
-
     def sweep_key_dialog(self):
         d = WindowModalDialog(self, title=_('Sweep private keys'))
         d.setMinimumSize(600, 300)
diff --git a/lib/commands.py b/lib/commands.py
index d6c71a8a..59ddbac9 100644
--- a/lib/commands.py
+++ b/lib/commands.py
@@ -453,7 +453,7 @@ class Commands:
             from .exchange_rate import FxThread
             fx = FxThread(self.config, None)
             kwargs['fx'] = fx
-        return self.wallet.export_history(**kwargs)
+        return self.wallet.get_full_history(**kwargs)
 
     @command('w')
     def setlabel(self, key, label):
diff --git a/lib/plot.py b/lib/plot.py
index 06f8edd7..82a83fe6 100644
--- a/lib/plot.py
+++ b/lib/plot.py
@@ -14,17 +14,16 @@ from matplotlib.patches import Ellipse
 from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker
 
 
-def plot_history(wallet, history):
+def plot_history(history):
     hist_in = defaultdict(int)
     hist_out = defaultdict(int)
     for item in history:
-        tx_hash, height, confirmations, timestamp, value, balance = item
-        if not confirmations:
+        if not item['confirmations']:
             continue
-        if timestamp is None:
+        if item['timestamp'] is None:
             continue
-        value = value*1./COIN
-        date = datetime.datetime.fromtimestamp(timestamp)
+        value = item['value'].value/COIN
+        date = item['date']
         datenum = int(md.date2num(datetime.date(date.year, date.month, 1)))
         if value > 0:
             hist_in[datenum] += value
diff --git a/lib/util.py b/lib/util.py
index 60723810..fd9bd059 100644
--- a/lib/util.py
+++ b/lib/util.py
@@ -77,11 +77,47 @@ class UserCancelled(Exception):
     '''An exception that is suppressed from the user'''
     pass
 
+class Satoshis(object):
+    def __new__(cls, value):
+        self = super(Satoshis, cls).__new__(cls)
+        self.value = value
+        return self
+
+    def __repr__(self):
+        return 'Satoshis(%d)'%self.value
+
+    def __str__(self):
+        return format_satoshis(self.value) + " BTC"
+
+class Fiat(object):
+    def __new__(cls, value, ccy):
+        self = super(Fiat, cls).__new__(cls)
+        self.ccy = ccy
+        self.value = value
+        return self
+
+    def __repr__(self):
+        return 'Fiat(%s)'% self.__str__()
+
+    def __str__(self):
+        if self.value is None:
+            return _('No Data')
+        else:
+            return "{:.2f}".format(self.value) + ' ' + self.ccy
+
 class MyEncoder(json.JSONEncoder):
     def default(self, obj):
         from .transaction import Transaction
         if isinstance(obj, Transaction):
             return obj.as_dict()
+        if isinstance(obj, Satoshis):
+            return str(obj)
+        if isinstance(obj, Fiat):
+            return str(obj)
+        if isinstance(obj, Decimal):
+            return str(obj)
+        if isinstance(obj, datetime):
+            return obj.isoformat(' ')[:-3]
         return super(MyEncoder, self).default(obj)
 
 class PrintError(object):
diff --git a/lib/wallet.py b/lib/wallet.py
index 921a1bb8..ce14706f 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -948,13 +948,14 @@ class Abstract_Wallet(PrintError):
         # return last balance
         return balance
 
-    def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False):
-        from .util import format_time, format_satoshis, timestamp_to_datetime
-        h = self.get_history(domain)
+    def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False):
+        from .util import timestamp_to_datetime, Satoshis, Fiat
         out = []
         init_balance = None
+        end_balance = 0
         capital_gains = 0
         fiat_income = 0
+        h = self.get_history(domain)
         for tx_hash, height, conf, timestamp, value, balance in h:
             if from_timestamp and timestamp < from_timestamp:
                 continue
@@ -965,17 +966,15 @@ class Abstract_Wallet(PrintError):
                 'height':height,
                 'confirmations':conf,
                 'timestamp':timestamp,
-                'value': format_satoshis(value, True) if value is not None else '--',
-                'balance': format_satoshis(balance)
+                'value': Satoshis(value),
+                'balance': Satoshis(balance)
             }
             if init_balance is None:
                 init_balance = balance - value
+                init_timestamp = timestamp
             end_balance = balance
-            if item['height']>0:
-                date_str = format_time(timestamp) if timestamp is not None else _("unverified")
-            else:
-                date_str = _("unconfirmed")
-            item['date'] = date_str
+            end_timestamp = timestamp
+            item['date'] = timestamp_to_datetime(timestamp) if timestamp is not None else None
             item['label'] = self.get_label(tx_hash)
             if show_addresses:
                 tx = self.transactions.get(tx_hash)
@@ -997,36 +996,44 @@ class Abstract_Wallet(PrintError):
                 fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
                 if fiat_value is None:
                     fiat_value = fx.historical_value(value, date)
-                item['fiat_value'] = fx.format_fiat(fiat_value)
+                    fiat_default = True
+                else:
+                    fiat_default = False
+                item['fiat_value'] = Fiat(fiat_value, fx.ccy)
+                item['fiat_default'] = fiat_default
                 if value < 0:
                     ap, lp = self.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy)
                     cg = None if lp is None or ap is None else lp - ap
-                    item['acquisition_price'] = fx.format_fiat(ap)
-                    item['capital_gain'] = fx.format_fiat(cg)
+                    item['acquisition_price'] = Fiat(ap, fx.ccy)
+                    item['capital_gain'] = Fiat(cg, fx.ccy)
                     if cg is not None:
                         capital_gains += cg
                 else:
                     if fiat_value is not None:
                         fiat_income += fiat_value
             out.append(item)
-
-        if from_timestamp and to_timestamp:
-            summary = {
-                'start_date': format_time(from_timestamp),
-                'end_date': format_time(to_timestamp),
-                'start_balance': format_satoshis(init_balance),
-                'end_balance': format_satoshis(end_balance),
-                'capital_gains': fx.format_fiat(capital_gains),
-                'fiat_income': fx.format_fiat(fiat_income)
-            }
-            if fx:
-                start_date = timestamp_to_datetime(from_timestamp)
-                end_date = timestamp_to_datetime(to_timestamp)
-                summary['start_fiat_balance'] = fx.format_fiat(fx.historical_value(init_balance, start_date))
-                summary['end_fiat_balance'] = fx.format_fiat(fx.historical_value(end_balance, end_date))
-            out.append(summary)
-
-        return out
+        result = {'transactions': out}
+        if from_timestamp is not None and to_timestamp is not None:
+            start_date = timestamp_to_datetime(from_timestamp)
+            end_date = timestamp_to_datetime(to_timestamp)
+        else:
+            start_date = timestamp_to_datetime(init_timestamp)
+            end_date = timestamp_to_datetime(end_timestamp)
+        summary = {
+            'start_date': start_date,
+            'end_date': end_date,
+            'start_balance': Satoshis(init_balance),
+            'end_balance': Satoshis(end_balance)
+        }
+        result['summary'] = summary
+        if fx:
+            unrealized = self.unrealized_gains(domain, fx.timestamp_rate, fx.ccy)
+            summary['start_fiat_balance'] = Fiat(fx.historical_value(init_balance, start_date), fx.ccy)
+            summary['end_fiat_balance'] = Fiat(fx.historical_value(end_balance, end_date), fx.ccy)
+            summary['capital_gains'] = Fiat(capital_gains, fx.ccy)
+            summary['fiat_income'] = Fiat(fiat_income, fx.ccy)
+            summary['unrealized_gains'] = Fiat(unrealized, fx.ccy)
+        return result
 
     def get_label(self, tx_hash):
         label = self.labels.get(tx_hash, '')
@@ -1662,6 +1669,16 @@ class Abstract_Wallet(PrintError):
         height, conf, timestamp = self.get_tx_height(txid)
         return price_func(timestamp)
 
+    def unrealized_gains(self, domain, price_func, ccy):
+        coins = self.get_utxos(domain)
+        now = time.time()
+        p = price_func(now)
+        if p is None:
+            return
+        ap = sum(self.coin_price(coin, price_func, ccy, self.txin_value(coin)) for coin in coins)
+        lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN)
+        return None if ap is None or lp is None else lp - ap
+
     def capital_gain(self, txid, price_func, ccy):
         """
         Difference between the fiat price of coins leaving the wallet because of transaction txid,
@@ -1683,7 +1700,6 @@ class Abstract_Wallet(PrintError):
             acquisition_price = None
         return acquisition_price, liquidation_price
 
-
     def average_price(self, tx, price_func, ccy):
         """ average price of the inputs of a transaction """
         input_value = sum(self.txin_value(txin) for txin in tx.inputs()) / Decimal(COIN)

From 9110c0542c40e00604a3d86eaf71e1646a9365d4 Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Mon, 19 Feb 2018 14:16:11 +0100
Subject: [PATCH 55/91] follow-up previous commit

---
 gui/qt/history_list.py | 15 +++++++++++----
 lib/wallet.py          |  3 +++
 2 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
index cfe412db..49a01f9b 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -163,10 +163,14 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
         d.setMinimumSize(600, 150)
         vbox = QVBoxLayout()
         grid = QGridLayout()
+        start_date = h.get('start_date')
+        end_date = h.get('end_date')
+        if start_date is None and end_date is None:
+            return
         grid.addWidget(QLabel(_("Start")), 0, 0)
-        grid.addWidget(QLabel(h.get('start_date').isoformat(' ')), 0, 1)
+        grid.addWidget(QLabel(start_date.isoformat(' ')), 0, 1)
         grid.addWidget(QLabel(_("End")), 1, 0)
-        grid.addWidget(QLabel(h.get('end_date').isoformat(' ')), 1, 1)
+        grid.addWidget(QLabel(end_date.isoformat(' ')), 1, 1)
         grid.addWidget(QLabel(_("Initial balance")), 2, 0)
         grid.addWidget(QLabel(format_amount(h['start_balance'].value)), 2, 1)
         grid.addWidget(QLabel(str(h.get('start_fiat_balance'))), 2, 2)
@@ -199,8 +203,11 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
         self.transactions = r['transactions']
         self.summary = r['summary']
         if not self.years and self.start_timestamp is None and self.end_timestamp is None:
-            self.years = [str(i) for i in range(self.summary['start_date'].year, self.summary['end_date'].year + 1)]
-            self.period_combo.insertItems(1, self.years)
+            start_date = self.summary['start_date']
+            end_date = self.summary['end_date']
+            if start_date and end_date:
+                self.years = [str(i) for i in range(start_date.year, end_date.year + 1)]
+                self.period_combo.insertItems(1, self.years)
         item = self.currentItem()
         current_tx = item.data(0, Qt.UserRole) if item else None
         self.clear()
diff --git a/lib/wallet.py b/lib/wallet.py
index ce14706f..90e47c43 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -952,6 +952,9 @@ class Abstract_Wallet(PrintError):
         from .util import timestamp_to_datetime, Satoshis, Fiat
         out = []
         init_balance = None
+        init_timestamp = None
+        end_balance = None
+        end_timestamp = None
         end_balance = 0
         capital_gains = 0
         fiat_income = 0

From 51c235a8be5bc0cce04ebea2faac4561150ec16d Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Mon, 19 Feb 2018 18:58:27 +0100
Subject: [PATCH 56/91] privkeys WIF: store in extended WIF internally; export
 as "txin_type:old_wif"

---
 gui/qt/main_window.py |  4 +---
 gui/qt/util.py        |  2 +-
 lib/bitcoin.py        | 44 +++++++++++++++++++++++++++++--------------
 lib/keystore.py       |  5 ++++-
 lib/wallet.py         | 36 ++++++++++++++++++-----------------
 5 files changed, 55 insertions(+), 36 deletions(-)

diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
index ad0dd77b..231d1891 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -2094,8 +2094,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
             rds_e = ShowQRTextEdit(text=redeem_script)
             rds_e.addCopyButton(self.app)
             vbox.addWidget(rds_e)
-        if xtype in ['p2wpkh', 'p2wsh', 'p2wpkh-p2sh', 'p2wsh-p2sh']:
-            vbox.addWidget(WWLabel(_("Warning: the format of private keys associated to segwit addresses may not be compatible with other wallets")))
         vbox.addLayout(Buttons(CloseButton(d)))
         d.setLayout(vbox)
         d.exec_()
@@ -2334,7 +2332,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                               _('It can not be "backed up" by simply exporting these private keys.'))
 
         d = WindowModalDialog(self, _('Private keys'))
-        d.setMinimumSize(850, 300)
+        d.setMinimumSize(980, 300)
         vbox = QVBoxLayout(d)
 
         msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
diff --git a/gui/qt/util.py b/gui/qt/util.py
index c0bdf62e..7f0cb238 100644
--- a/gui/qt/util.py
+++ b/gui/qt/util.py
@@ -254,7 +254,7 @@ def line_dialog(parent, title, label, ok_label, default=None):
 def text_dialog(parent, title, label, ok_label, default=None, allow_multi=False):
     from .qrtextedit import ScanQRTextEdit
     dialog = WindowModalDialog(parent, title)
-    dialog.setMinimumWidth(500)
+    dialog.setMinimumWidth(600)
     l = QVBoxLayout()
     dialog.setLayout(l)
     l.addWidget(QLabel(label))
diff --git a/lib/bitcoin.py b/lib/bitcoin.py
index 84339755..65d50bbf 100644
--- a/lib/bitcoin.py
+++ b/lib/bitcoin.py
@@ -508,9 +508,8 @@ def DecodeBase58Check(psz):
         return key
 
 
-
-# extended key export format for segwit
-
+# backwards compat
+# extended WIF for segwit (used in 3.0.x; but still used internally)
 SCRIPT_TYPES = {
     'p2pkh':0,
     'p2wpkh':1,
@@ -521,26 +520,43 @@ SCRIPT_TYPES = {
 }
 
 
-def serialize_privkey(secret, compressed, txin_type):
-    prefix = bytes([(SCRIPT_TYPES[txin_type]+NetworkConstants.WIF_PREFIX)&255])
+def serialize_privkey(secret, compressed, txin_type, internal_use=False):
+    if internal_use:
+        prefix = bytes([(SCRIPT_TYPES[txin_type] + NetworkConstants.WIF_PREFIX) & 255])
+    else:
+        prefix = bytes([NetworkConstants.WIF_PREFIX])
     suffix = b'\01' if compressed else b''
     vchIn = prefix + secret + suffix
-    return EncodeBase58Check(vchIn)
+    base58_wif = EncodeBase58Check(vchIn)
+    if internal_use:
+        return base58_wif
+    else:
+        return '{}:{}'.format(txin_type, base58_wif)
 
 
 def deserialize_privkey(key):
-    # whether the pubkey is compressed should be visible from the keystore
-    vch = DecodeBase58Check(key)
     if is_minikey(key):
         return 'p2pkh', minikey_to_private_key(key), True
-    elif vch:
-        txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - NetworkConstants.WIF_PREFIX]
-        assert len(vch) in [33, 34]
-        compressed = len(vch) == 34
-        return txin_type, vch[1:33], compressed
-    else:
+
+    txin_type = None
+    if ':' in key:
+        txin_type, key = key.split(sep=':', maxsplit=1)
+        assert txin_type in SCRIPT_TYPES
+    vch = DecodeBase58Check(key)
+    if not vch:
         raise BaseException("cannot deserialize", key)
 
+    if txin_type is None:
+        # keys exported in version 3.0.x encoded script type in first byte
+        txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - NetworkConstants.WIF_PREFIX]
+    else:
+        assert vch[0] == NetworkConstants.WIF_PREFIX
+
+    assert len(vch) in [33, 34]
+    compressed = len(vch) == 34
+    return txin_type, vch[1:33], compressed
+
+
 def regenerate_key(pk):
     assert len(pk) == 32
     return EC_KEY(pk)
diff --git a/lib/keystore.py b/lib/keystore.py
index e3579958..011602ec 100644
--- a/lib/keystore.py
+++ b/lib/keystore.py
@@ -139,7 +139,10 @@ class Imported_KeyStore(Software_KeyStore):
     def import_privkey(self, sec, password):
         txin_type, privkey, compressed = deserialize_privkey(sec)
         pubkey = public_key_from_private_key(privkey, compressed)
-        self.keypairs[pubkey] = pw_encode(sec, password)
+        # re-serialize the key so the internal storage format is consistent
+        serialized_privkey = serialize_privkey(
+            privkey, compressed, txin_type, internal_use=True)
+        self.keypairs[pubkey] = pw_encode(serialized_privkey, password)
         return txin_type, pubkey
 
     def delete_imported_key(self, key):
diff --git a/lib/wallet.py b/lib/wallet.py
index 90e47c43..ec7d720b 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -388,23 +388,21 @@ class Abstract_Wallet(PrintError):
     def get_address_index(self, address):
         raise NotImplementedError()
 
+    def get_redeem_script(self, address):
+        return None
+
     def export_private_key(self, address, password):
-        """ extended WIF format """
         if self.is_watching_only():
             return []
         index = self.get_address_index(address)
         pk, compressed = self.keystore.get_private_key(index, password)
-        if self.txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
-            pubkeys = self.get_public_keys(address)
-            redeem_script = self.pubkeys_to_redeem_script(pubkeys)
-        else:
-            redeem_script = None
-        return bitcoin.serialize_privkey(pk, compressed, self.txin_type), redeem_script
-
+        txin_type = self.get_txin_type(address)
+        redeem_script = self.get_redeem_script(address)
+        serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type)
+        return serialized_privkey, redeem_script
 
     def get_public_keys(self, address):
-        sequence = self.get_address_index(address)
-        return self.get_pubkeys(*sequence)
+        return [self.get_public_key(address)]
 
     def add_unverified_tx(self, tx_hash, tx_height):
         if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \
@@ -1896,12 +1894,10 @@ class Imported_Wallet(Simple_Wallet):
         self.add_address(addr)
         return addr
 
-    def export_private_key(self, address, password):
+    def get_redeem_script(self, address):
         d = self.addresses[address]
-        pubkey = d['pubkey']
         redeem_script = d['redeem_script']
-        sec = pw_decode(self.keystore.keypairs[pubkey], password)
-        return sec, redeem_script
+        return redeem_script
 
     def get_txin_type(self, address):
         return self.addresses[address].get('type', 'address')
@@ -2079,9 +2075,6 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
     def get_pubkey(self, c, i):
         return self.derive_pubkeys(c, i)
 
-    def get_public_keys(self, address):
-        return [self.get_public_key(address)]
-
     def add_input_sig_info(self, txin, address):
         derivation = self.get_address_index(address)
         x_pubkey = self.keystore.get_xpubkey(*derivation)
@@ -2119,6 +2112,10 @@ class Multisig_Wallet(Deterministic_Wallet):
     def get_pubkeys(self, c, i):
         return self.derive_pubkeys(c, i)
 
+    def get_public_keys(self, address):
+        sequence = self.get_address_index(address)
+        return self.get_pubkeys(*sequence)
+
     def pubkeys_to_address(self, pubkeys):
         redeem_script = self.pubkeys_to_redeem_script(pubkeys)
         return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)
@@ -2126,6 +2123,11 @@ class Multisig_Wallet(Deterministic_Wallet):
     def pubkeys_to_redeem_script(self, pubkeys):
         return transaction.multisig_script(sorted(pubkeys), self.m)
 
+    def get_redeem_script(self, address):
+        pubkeys = self.get_public_keys(address)
+        redeem_script = self.pubkeys_to_redeem_script(pubkeys)
+        return redeem_script
+
     def derive_pubkeys(self, c, i):
         return [k.derive_pubkey(c, i) for k in self.get_keystores()]
 

From 7a4338ea219d26217d1fd36c6ce46c7038d8c863 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Mon, 19 Feb 2018 21:16:12 +0100
Subject: [PATCH 57/91] fix tests

---
 lib/tests/test_bitcoin.py | 45 +++++++++++++++++++++++++++++++++++++--
 1 file changed, 43 insertions(+), 2 deletions(-)

diff --git a/lib/tests/test_bitcoin.py b/lib/tests/test_bitcoin.py
index dbae5005..03f1d00f 100644
--- a/lib/tests/test_bitcoin.py
+++ b/lib/tests/test_bitcoin.py
@@ -271,6 +271,7 @@ class Test_keyImport(unittest.TestCase):
 
     priv_pub_addr = (
            {'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
+            'exported_privkey': 'p2pkh:KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
             'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997',
             'address': '17azqT8T16coRmWKYFj3UjzJuxiYrYFRBR',
             'minikey' : False,
@@ -278,7 +279,17 @@ class Test_keyImport(unittest.TestCase):
             'compressed': True,
             'addr_encoding': 'base58',
             'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'},
+           {'priv': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD',
+            'exported_privkey': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD',
+            'pub': '0352d78b4b37e0f6d4e164423436f2925fa57817467178eca550a88f2821973c41',
+            'address': '1GXgZ5Qi6gmXTHVSpUPZLy4Ci2nbfb3ZNb',
+            'minikey': False,
+            'txin_type': 'p2pkh',
+            'compressed': True,
+            'addr_encoding': 'base58',
+            'scripthash': 'a9b2a76fc196c553b352186dfcca81fcf323a721cd8431328f8e9d54216818c1'},
            {'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
+            'exported_privkey': 'p2pkh:5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
             'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f',
             'address': '1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6',
             'minikey': False,
@@ -286,7 +297,17 @@ class Test_keyImport(unittest.TestCase):
             'compressed': False,
             'addr_encoding': 'base58',
             'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'},
+           {'priv': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN',
+            'exported_privkey': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN',
+            'pub': '048f0431b0776e8210376c81280011c2b68be43194cb00bd47b7e9aa66284b713ce09556cde3fee606051a07613f3c159ef3953b8927c96ae3dae94a6ba4182e0e',
+            'address': '147kiRHHm9fqeMQSgqf4k35XzuWLP9fmmS',
+            'minikey': False,
+            'txin_type': 'p2pkh',
+            'compressed': False,
+            'addr_encoding': 'base58',
+            'scripthash': '6dd2e07ad2de9ba8eec4bbe8467eb53f8845acff0d9e6f5627391acc22ff62df'},
            {'priv': 'LHJnnvRzsdrTX2j5QeWVsaBkabK7gfMNqNNqxnbBVRaJYfk24iJz',
+            'exported_privkey': 'p2wpkh-p2sh:Kz9XebiCXL2BZzhYJViiHDzn5iup1povWV8aqstzWU4sz1K5nVva',
             'pub': '0279ad237ca0d812fb503ab86f25e15ebd5fa5dd95c193639a8a738dcd1acbad81',
             'address': '3GeVJB3oKr7psgKR6BTXSxKtWUkfsHHhk7',
             'minikey': False,
@@ -294,7 +315,17 @@ class Test_keyImport(unittest.TestCase):
             'compressed': True,
             'addr_encoding': 'base58',
             'scripthash': 'd7b04e882fa6b13246829ac552a2b21461d9152eb00f0a6adb58457a3e63d7c5'},
+           {'priv': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW',
+            'exported_privkey': 'p2wpkh-p2sh:L3CZH1pm87X4bbE6mSGvZnAZ1KcFDRomBudUkrkBG7EZhDtBVXMW',
+            'pub': '0229da20a15b3363b2c28e3c5093c180b56c439df0b968a970366bb1f38435361e',
+            'address': '3C79goMwT7zSTjXnPoCg6VFGAnUpZAkyus',
+            'minikey': False,
+            'txin_type': 'p2wpkh-p2sh',
+            'compressed': True,
+            'addr_encoding': 'base58',
+            'scripthash': '714bf6bfe1083e69539f40d4c7a7dca85d187471b35642e55f20d7e866494cf7'},
            {'priv': 'L8g5V8kFFeg2WbecahRSdobARbHz2w2STH9S8ePHVSY4fmia7Rsj',
+            'exported_privkey': 'p2wpkh:Kz6SuyPM5VktY5dr2d2YqdVgBA6LCWkiHqXJaC3BzxnMPSUuYzmF',
             'pub': '03e9f948421aaa89415dc5f281a61b60dde12aae3181b3a76cd2d849b164fc6d0b',
             'address': 'bc1qqmpt7u5e9hfznljta5gnvhyvfd2kdd0r90hwue',
             'minikey': False,
@@ -302,8 +333,18 @@ class Test_keyImport(unittest.TestCase):
             'compressed': True,
             'addr_encoding': 'bech32',
             'scripthash': '1929acaaef3a208c715228e9f1ca0318e3a6b9394ab53c8d026137f847ecf97b'},
+           {'priv': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo',
+            'exported_privkey': 'p2wpkh:KyDWy5WbjLA58Zesh1o8m3pADGdJ3v33DKk4m7h8BD5zDKDmDFwo',
+            'pub': '038c57657171c1f73e34d5b3971d05867d50221ad94980f7e87cbc2344425e6a1e',
+            'address': 'bc1qpakeeg4d9ydyjxd8paqrw4xy9htsg532xzxn50',
+            'minikey': False,
+            'txin_type': 'p2wpkh',
+            'compressed': True,
+            'addr_encoding': 'bech32',
+            'scripthash': '242f02adde84ebb2a7dd778b2f3a81b3826f111da4d8960d826d7a4b816cb261'},
            # from http://bitscan.com/articles/security/spotlight-on-mini-private-keys
            {'priv': 'SzavMBLoXU6kDrqtUVmffv',
+            'exported_privkey': 'p2pkh:L53fCHmQhbNp1B4JipfBtfeHZH7cAibzG9oK19XfiFzxHgAkz6JK',
             'pub': '02588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9',
             'address': '19GuvDvMMUZ8vq84wT79fvnvhMd5MnfTkR',
             'minikey': True,
@@ -344,6 +385,7 @@ class Test_keyImport(unittest.TestCase):
     def test_is_private_key(self):
         for priv_details in self.priv_pub_addr:
             self.assertTrue(is_private_key(priv_details['priv']))
+            self.assertTrue(is_private_key(priv_details['exported_privkey']))
             self.assertFalse(is_private_key(priv_details['pub']))
             self.assertFalse(is_private_key(priv_details['address']))
         self.assertFalse(is_private_key("not a privkey"))
@@ -352,8 +394,7 @@ class Test_keyImport(unittest.TestCase):
         for priv_details in self.priv_pub_addr:
             txin_type, privkey, compressed = deserialize_privkey(priv_details['priv'])
             priv2 = serialize_privkey(privkey, compressed, txin_type)
-            if not priv_details['minikey']:
-                self.assertEqual(priv_details['priv'], priv2)
+            self.assertEqual(priv_details['exported_privkey'], priv2)
 
     def test_address_to_scripthash(self):
         for priv_details in self.priv_pub_addr:

From 0a1542e2496769a114ab9c1370b4ace4c6505a91 Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Tue, 20 Feb 2018 09:58:36 +0100
Subject: [PATCH 58/91] fix #3929

---
 lib/wallet.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index 90e47c43..4d513ea2 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1720,7 +1720,8 @@ class Abstract_Wallet(PrintError):
             if fiat_value is not None:
                 return fiat_value
             else:
-                return self.price_at_timestamp(txid, price_func) * txin_value/Decimal(COIN)
+                p = self.price_at_timestamp(txid, price_func)
+                return None if p is None else p * txin_value/Decimal(COIN)
         else:
             # could be some coinjoin transaction..
             return None

From 26d09b49151b4eb7a6e486c96e47654b59ef3045 Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Tue, 20 Feb 2018 10:52:11 +0100
Subject: [PATCH 59/91] fix timestamp of data in get_historical_rates

---
 lib/exchange_rate.py | 37 +++++++++++++++++++------------------
 1 file changed, 19 insertions(+), 18 deletions(-)

diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py
index 6931e338..51bb7402 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -68,40 +68,41 @@ class ExchangeBase(PrintError):
             try:
                 with open(filename, 'r') as f:
                     h = json.loads(f.read())
+                h['timestamp'] = timestamp
             except:
                 h = None
         else:
             h = None
-            timestamp = False
         if h:
             self.history[ccy] = h
             self.on_history()
-        return h, timestamp
+        return h
 
     def get_historical_rates_safe(self, ccy, cache_dir):
-        h, timestamp = self.read_historical_rates(ccy, cache_dir)
-        if h is None or time.time() - timestamp < 24*3600:
-            try:
-                self.print_error("requesting fx history for", ccy)
-                h = self.request_history(ccy)
-                self.print_error("received fx history for", ccy)
-                self.on_history()
-            except BaseException as e:
-                self.print_error("failed fx history:", e)
-                return
-            filename = os.path.join(cache_dir, self.name() + '_' + ccy)
-            with open(filename, 'w') as f:
-                f.write(json.dumps(h))
+        try:
+            self.print_error("requesting fx history for", ccy)
+            h = self.request_history(ccy)
+            self.print_error("received fx history for", ccy)
+        except BaseException as e:
+            self.print_error("failed fx history:", e)
+            return
+        filename = os.path.join(cache_dir, self.name() + '_' + ccy)
+        with open(filename, 'w') as f:
+            f.write(json.dumps(h))
+        h['timestamp'] = time.time()
         self.history[ccy] = h
         self.on_history()
 
     def get_historical_rates(self, ccy, cache_dir):
-        result = self.history.get(ccy)
-        if not result and ccy in self.history_ccys():
+        if ccy not in self.history_ccys():
+            return
+        h = self.history.get(ccy)
+        if h is None:
+            h = self.read_historical_rates(ccy, cache_dir)
+        if h is None or h['timestamp'] < time.time() - 24*3600:
             t = Thread(target=self.get_historical_rates_safe, args=(ccy, cache_dir))
             t.setDaemon(True)
             t.start()
-        return result
 
     def history_ccys(self):
         return []

From 98a91c9306b47b8319b1f169051fe51f1b76cf1c Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Tue, 20 Feb 2018 14:45:12 +0100
Subject: [PATCH 60/91] update release notes

---
 RELEASE-NOTES | 58 ++++++++++++++++++++++++++++++---------------------
 1 file changed, 34 insertions(+), 24 deletions(-)

diff --git a/RELEASE-NOTES b/RELEASE-NOTES
index ddadee13..0ed9b70f 100644
--- a/RELEASE-NOTES
+++ b/RELEASE-NOTES
@@ -1,31 +1,41 @@
+
 # Release 3.1 - (to be released)
 
- * Mempory pool based fee estimates. If this option is activated,
-   users can set transaction fees that target a desired depth in the
-   memory pool. This feature might be controversial, because miners
-   could conspire and fill the memory pool with expensive transactions
-   that never get mined. However, our current time-based fee estimates
-   results in sticky fees, which cause inexperienced users to overpay,
-   while more advanced users visit (and trust) websites that display
-   memorypool data, and set their fee accordingly.
- * Local transactions: Transactions that have not been broadcasted can
-   be saved in the wallet file, and their outputs can be used in
-   subsequent transactions. Transactions that disapear from the memory
-   pool stay in the wallet, and can be rebroadcasted. This feature can
-   be combined with cold storage, to create several transactions
-   before broadcasting.
- * The initial headers download was replaced with hardcoded
-   checkpoints, one per retargeting period. Past headers are
-   downloaded when needed.
- * The two coin selection policies have been merged, and the policy
-   choice was removed from preferences. Previously, the 'privacy'
-   policy has been unusable because it was was not prioritizing
-   confirmed coins.
+ * Memory-pool based transaction fees. Users can set dynamic fees that
+   target a desired depth in the memory pool. This feature is
+   optional, and ETA-based estimates (from Bitcoin Core) remain the
+   default. Note that miners could exploit this feature, if they
+   conspired and filled the memory pool with expensive transactions
+   that never get mined. However, since the Electrum client already
+   trusts an Electrum server with fee estimates, activating this
+   feature does not introduce any new vulnerability; the client uses a
+   hard threshold to detect unusually high fees. In practice,
+   ETA-based estimates have resulted in sticky fees, and caused many
+   users to overpay for transactions. Advanced users tend to visit
+   (and trust) websites that display memory-pool data in order to set
+   their fees.
+ * Local transactions: Transactions can be saved in the wallet without
+   being broadcast. The inputs of local transactions are considered as
+   spent, and their change outputs can be re-used in subsequent
+   transactions. This can be combined with cold storage, in order to
+   create several transactions before broadcasting them. Outgoing
+   transactions that have been removed from the memory pool are also
+   saved in the wallet, and can be broadcast again.
+ * Checkpoints: The initial download of a headers file was replaced
+   with hardcoded checkpoints. The wallet uses one checkpoint per
+   retargetting period. The headers for a retargetting period are
+   downloaded only if transactions need to be verified in this period.
+ * The 'privacy' and 'priority' coin selection policies have been
+   merged into one. Previously, the 'privacy' policy has been unusable
+   because it was was not prioritizing confirmed coins. The new policy
+   is similar to 'privacy', except that it de-prioritizes addresses
+   that have unconfirmed coins.
  * The 'Send' tab of the Qt GUI displays how transaction fees are
    computed from transaction size.
- * RBF is enabled by default. This might cause some issues with
-   merchants that use wallets that do not display RBF transactions
-   until they are confirmed.
+ * The wallet history can be filtered by time interval.
+ * Replace-by-fee is enabled by default. Note that this might cause
+   some issues with wallets that do not display RBF transactions until
+   they are confirmed.
  * Watching-only wallets and hardware wallets can be encrypted.
  * Semi-automated crash reporting
  * The SSL checkbox option was removed from the GUI.

From febaedcd3696b2989aaa2136dc50c4edfe9fa807 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Tue, 20 Feb 2018 16:06:34 +0100
Subject: [PATCH 61/91] crash reporting: catch exceptions from requests.post

---
 gui/qt/exception_window.py | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/gui/qt/exception_window.py b/gui/qt/exception_window.py
index a15bbe25..091da186 100644
--- a/gui/qt/exception_window.py
+++ b/gui/qt/exception_window.py
@@ -107,14 +107,22 @@ class Exception_Window(QWidget):
     def send_report(self):
         if bitcoin.NetworkConstants.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in report_server:
             # Gah! Some kind of altcoin wants to send us crash reports.
-            self.main_window.show_critical("Please report this issue manually.")
+            self.main_window.show_critical(_("Please report this issue manually."))
             return
         report = self.get_traceback_info()
         report.update(self.get_additional_info())
         report = json.dumps(report)
-        response = requests.post(report_server, data=report)
-        QMessageBox.about(self, "Crash report", response.text)
-        self.close()
+        try:
+            response = requests.post(report_server, data=report, timeout=20)
+        except BaseException as e:
+            traceback.print_exc(file=sys.stderr)
+            self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' +
+                                           str(e) + '\n' +
+                                           _("Please report this issue manually."))
+            return
+        else:
+            QMessageBox.about(self, "Crash report", response.text)
+            self.close()
 
     def on_close(self):
         Exception_Window._active_window = None

From 7b3c45454269e54334f9f33a26936dc0e9b705a4 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Tue, 20 Feb 2018 18:16:25 +0100
Subject: [PATCH 62/91] wallet.add_transaction should not return if tx has
 already been added. only track spent_outpoints for is_mine inputs.

---
 lib/wallet.py | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index 4d513ea2..1d99f57f 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -733,7 +733,8 @@ class Abstract_Wallet(PrintError):
     def get_conflicting_transactions(self, tx):
         """Returns a set of transaction hashes from the wallet history that are
         directly conflicting with tx, i.e. they have common outpoints being
-        spent with tx.
+        spent with tx. If the tx is already in wallet history, that will not be
+        reported as a conflict.
         """
         conflicting_txns = set()
         with self.transaction_lock:
@@ -747,12 +748,20 @@ class Abstract_Wallet(PrintError):
                 # this outpoint (ser) has already been spent, by spending_tx
                 assert spending_tx_hash in self.transactions
                 conflicting_txns |= {spending_tx_hash}
+            txid = tx.txid()
+            if txid in conflicting_txns:
+                # this tx is already in history, so it conflicts with itself
+                if len(conflicting_txns) > 1:
+                    raise Exception('Found conflicting transactions already in wallet history.')
+                conflicting_txns -= {txid}
             return conflicting_txns
 
     def add_transaction(self, tx_hash, tx):
         with self.transaction_lock:
-            if tx in self.transactions:
-                return True
+            # NOTE: returning if tx in self.transactions might seem like a good idea
+            # BUT we track is_mine inputs in a txn, and during subsequent calls
+            # of add_transaction tx, we might learn of more-and-more inputs of
+            # being is_mine, as we roll the gap_limit forward
             is_coinbase = tx.inputs()[0]['type'] == 'coinbase'
             tx_height = self.get_tx_height(tx_hash)[0]
             is_mine = any([self.is_mine(txin['address']) for txin in tx.inputs()])
@@ -800,7 +809,6 @@ class Abstract_Wallet(PrintError):
                     prevout_hash = txi['prevout_hash']
                     prevout_n = txi['prevout_n']
                     ser = prevout_hash + ':%d'%prevout_n
-                    self.spent_outpoints[ser] = tx_hash
                 # find value from prev output
                 if addr and self.is_mine(addr):
                     dd = self.txo.get(prevout_hash, {})
@@ -809,6 +817,8 @@ class Abstract_Wallet(PrintError):
                             if d.get(addr) is None:
                                 d[addr] = []
                             d[addr].append((ser, v))
+                            # we only track is_mine spends
+                            self.spent_outpoints[ser] = tx_hash
                             break
                     else:
                         self.pruned_txo[ser] = tx_hash

From d77e522721b98c8d455f866bc40b778c6a0fb74e Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Tue, 20 Feb 2018 21:53:12 +0100
Subject: [PATCH 63/91] fix #3912: Use Decimal('NaN') instead of None when
 exchange rate is not available.

---
 lib/exchange_rate.py | 35 ++++++++++++++++-------------------
 lib/wallet.py        | 22 +++++++++-------------
 2 files changed, 25 insertions(+), 32 deletions(-)

diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py
index 51bb7402..589ef6ed 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -109,8 +109,8 @@ class ExchangeBase(PrintError):
 
     def historical_rate(self, ccy, d_t):
         if d_t is None:
-            return None
-        return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'))
+            return 'NaN'
+        return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'), 'NaN')
 
     def get_currencies(self):
         rates = self.get_rates('')
@@ -497,40 +497,38 @@ class FxThread(ThreadJob):
     def exchange_rate(self):
         '''Returns None, or the exchange rate as a Decimal'''
         rate = self.exchange.quotes.get(self.ccy)
-        if rate:
-            return Decimal(rate)
+        if rate is None:
+            return Decimal('NaN')
+        return Decimal(rate)
 
     def format_amount_and_units(self, btc_balance):
         rate = self.exchange_rate()
-        return '' if rate is None else "%s %s" % (self.value_str(btc_balance, rate), self.ccy)
+        return '' if rate.is_nan() else "%s %s" % (self.value_str(btc_balance, rate), self.ccy)
 
     def get_fiat_status_text(self, btc_balance, base_unit, decimal_point):
         rate = self.exchange_rate()
-        return _("  (No FX rate available)") if rate is None else " 1 %s~%s %s" % (base_unit,
+        return _("  (No FX rate available)") if rate.is_nan() else " 1 %s~%s %s" % (base_unit,
             self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy)
 
     def value_str(self, satoshis, rate):
-        if satoshis is not None and rate is not None:
-            value = Decimal(satoshis) / COIN * Decimal(rate)
-        else:
-            value = None
+        value = Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate)
         return self.format_fiat(value)
 
     def format_fiat(self, value):
-        if value is not None:
-            return "%s" % (self.ccy_amount_str(value, True))
-        return _("No data")
+        if value.is_nan():
+            return _("No data")
+        return "%s" % (self.ccy_amount_str(value, True))
 
     def history_rate(self, d_t):
         if d_t is None:
-            return None
+            return Decimal('NaN')
         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 is None and (datetime.today().date() - d_t.date()).days <= 2:
-            rate = self.exchange.quotes.get(self.ccy)
+        if rate == 'NaN' and (datetime.today().date() - d_t.date()).days <= 2:
+            rate = self.exchange.quotes.get(self.ccy, 'NaN')
             self.history_used_spot = True
-        return Decimal(rate) if rate is not None else None
+        return Decimal(rate)
 
     def historical_value_str(self, satoshis, d_t):
         rate = self.history_rate(d_t)
@@ -538,8 +536,7 @@ class FxThread(ThreadJob):
 
     def historical_value(self, satoshis, d_t):
         rate = self.history_rate(d_t)
-        if rate:
-            return Decimal(satoshis) / COIN * Decimal(rate)
+        return Decimal(satoshis) / COIN * Decimal(rate)
 
     def timestamp_rate(self, timestamp):
         from electrum.util import timestamp_to_datetime
diff --git a/lib/wallet.py b/lib/wallet.py
index 1d99f57f..b14f5831 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -958,6 +958,7 @@ class Abstract_Wallet(PrintError):
         # return last balance
         return balance
 
+    @profiler
     def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False):
         from .util import timestamp_to_datetime, Satoshis, Fiat
         out = []
@@ -1016,11 +1017,10 @@ class Abstract_Wallet(PrintError):
                 item['fiat_default'] = fiat_default
                 if value < 0:
                     ap, lp = self.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy)
-                    cg = None if lp is None or ap is None else lp - ap
+                    cg = lp - ap
                     item['acquisition_price'] = Fiat(ap, fx.ccy)
                     item['capital_gain'] = Fiat(cg, fx.ccy)
-                    if cg is not None:
-                        capital_gains += cg
+                    capital_gains += cg
                 else:
                     if fiat_value is not None:
                         fiat_income += fiat_value
@@ -1686,11 +1686,9 @@ class Abstract_Wallet(PrintError):
         coins = self.get_utxos(domain)
         now = time.time()
         p = price_func(now)
-        if p is None:
-            return
         ap = sum(self.coin_price(coin, price_func, ccy, self.txin_value(coin)) for coin in coins)
         lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN)
-        return None if ap is None or lp is None else lp - ap
+        return lp - ap
 
     def capital_gain(self, txid, price_func, ccy):
         """
@@ -1704,13 +1702,11 @@ class Abstract_Wallet(PrintError):
         fiat_value = self.get_fiat_value(txid, ccy)
         if fiat_value is None:
             p = self.price_at_timestamp(txid, price_func)
-            liquidation_price = None if p is None else out_value/Decimal(COIN) * p
+            liquidation_price = out_value/Decimal(COIN) * p
         else:
             liquidation_price = - fiat_value
-        try:
-            acquisition_price = out_value/Decimal(COIN) * self.average_price(tx, price_func, ccy)
-        except:
-            acquisition_price = None
+
+        acquisition_price = out_value/Decimal(COIN) * self.average_price(tx, price_func, ccy)
         return acquisition_price, liquidation_price
 
     def average_price(self, tx, price_func, ccy):
@@ -1731,10 +1727,10 @@ class Abstract_Wallet(PrintError):
                 return fiat_value
             else:
                 p = self.price_at_timestamp(txid, price_func)
-                return None if p is None else p * txin_value/Decimal(COIN)
+                return p * txin_value/Decimal(COIN)
         else:
             # could be some coinjoin transaction..
-            return None
+            return Decimal('NaN')
 
 
 class Simple_Wallet(Abstract_Wallet):

From fcae5eaa92b981fcca1601f88106c70e8450c933 Mon Sep 17 00:00:00 2001
From: Johann Bauer 
Date: Sun, 11 Feb 2018 16:51:17 +0100
Subject: [PATCH 64/91] Workaround for PyBlake2 build issues

---
 contrib/build-wine/build-electrum-git.sh | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh
index a8f74358..f0c346a4 100755
--- a/contrib/build-wine/build-electrum-git.sh
+++ b/contrib/build-wine/build-electrum-git.sh
@@ -56,6 +56,12 @@ cp electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/gui/qt/
 
 # Install frozen dependencies
 $PYTHON -m pip install -r ../../deterministic-build/requirements.txt
+
+# Workaround until they upload binary wheels themselves:
+wget 'https://ci.appveyor.com/api/buildjobs/bwr3yfghdemoryy8/artifacts/dist%2Fpyblake2-1.1.0-cp35-cp35m-win32.whl' -O pyblake2-1.1.0-cp35-cp35m-win32.whl
+$PYTHON -m pip install ./pyblake2-1.1.0-cp35-cp35m-win32.whl
+ 
+
 $PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt
 
 pushd $WINEPREFIX/drive_c/electrum

From 78a9424c48bd47d0d89470c6960bbe75f49bae64 Mon Sep 17 00:00:00 2001
From: Johann Bauer 
Date: Wed, 21 Feb 2018 01:39:57 +0100
Subject: [PATCH 65/91] Add libusb dll to Windows binary

So that Trezor still works...

Closes: #3931
---
 contrib/build-wine/deterministic.spec |  1 +
 contrib/build-wine/prepare-wine.sh    | 12 ++++++++++--
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec
index 3dc5953b..ac5962ed 100644
--- a/contrib/build-wine/deterministic.spec
+++ b/contrib/build-wine/deterministic.spec
@@ -51,6 +51,7 @@ a = Analysis([home+'electrum',
               home+'plugins/ledger/qt.py',
               #home+'packages/requests/utils.py'
               ],
+             binaries=[("c:/python3.5.4/libusb-1.0.dll", ".")],
              datas=datas,
              #pathex=[home+'lib', home+'gui', home+'plugins'],
              hiddenimports=hiddenimports,
diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh
index d62b4c63..adbff4c0 100755
--- a/contrib/build-wine/prepare-wine.sh
+++ b/contrib/build-wine/prepare-wine.sh
@@ -3,6 +3,10 @@
 # Please update these carefully, some versions won't work under Wine
 NSIS_URL=https://prdownloads.sourceforge.net/nsis/nsis-3.02.1-setup.exe?download
 NSIS_SHA256=736c9062a02e297e335f82252e648a883171c98e0d5120439f538c81d429552e
+
+LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.21/libusb-1.0.21.7z?download
+LIBUSB_SHA256=acdde63a40b1477898aee6153f9d91d1a2e8a5d93f832ca8ab876498f3a6d2b8
+
 PYTHON_VERSION=3.5.4
 
 ## These settings probably don't need change
@@ -81,8 +85,7 @@ $PYTHON -m pip install win_inet_pton==1.0.1
 $PYTHON -m pip install -r ../../deterministic-build/requirements-binaries.txt
 
 # Install PyInstaller
-
-$PYTHON -m pip install git+https://github.com/ecdsa/pyinstaller@fix_2952
+$PYTHON -m pip install https://github.com/ecdsa/pyinstaller/archive/fix_2952.zip
 
 # Install ZBar
 #wget -q -O zbar.exe "https://sourceforge.net/projects/zbar/files/zbar/0.10/zbar-0.10-setup.exe/download"
@@ -97,6 +100,11 @@ wget -q -O nsis.exe "$NSIS_URL"
 verify_hash nsis.exe $NSIS_SHA256
 wine nsis.exe /S
 
+wget -q -O libusb.7z "$LIBUSB_URL"
+verify_hash libusb.7z "$LIBUSB_SHA256"
+7z x -olibusb libusb.7z
+cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/python$PYTHON_VERSION/
+
 # Install UPX
 #wget -O upx.zip "https://downloads.sourceforge.net/project/upx/upx/3.08/upx308w.zip"
 #unzip -o upx.zip

From 363f3766d753f905bafe770114a30f489c448620 Mon Sep 17 00:00:00 2001
From: Johann Bauer 
Date: Wed, 21 Feb 2018 02:00:21 +0100
Subject: [PATCH 66/91] Add Qt Windows style to the binary

Closes: #3813
---
 contrib/build-wine/deterministic.spec | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec
index ac5962ed..5292cbb7 100644
--- a/contrib/build-wine/deterministic.spec
+++ b/contrib/build-wine/deterministic.spec
@@ -1,6 +1,6 @@
 # -*- mode: python -*-
 
-from PyInstaller.utils.hooks import collect_data_files, collect_submodules
+from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
 
 import sys
 for i, x in enumerate(sys.argv):
@@ -19,6 +19,12 @@ hiddenimports += collect_submodules('trezorlib')
 hiddenimports += collect_submodules('btchip')
 hiddenimports += collect_submodules('keepkeylib')
 
+# Add libusb binary
+binaries = [("c:/python3.5.4/libusb-1.0.dll", ".")]
+
+# Workaround for "Retro Look":
+binaries += [b for b in collect_dynamic_libs('PyQt5') if 'qwindowsvista' in b[0]]
+
 datas = [
     (home+'lib/currencies.json', 'electrum'),
     (home+'lib/servers.json', 'electrum'),
@@ -51,7 +57,7 @@ a = Analysis([home+'electrum',
               home+'plugins/ledger/qt.py',
               #home+'packages/requests/utils.py'
               ],
-             binaries=[("c:/python3.5.4/libusb-1.0.dll", ".")],
+             binaries=binaries,
              datas=datas,
              #pathex=[home+'lib', home+'gui', home+'plugins'],
              hiddenimports=hiddenimports,

From 4ddda74dad1a176e1dd7c2509a0495450e7b19ff Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Wed, 21 Feb 2018 03:22:26 +0100
Subject: [PATCH 67/91] clean up fees a bit

---
 gui/kivy/uix/dialogs/bump_fee_dialog.py | 49 ++++++++-------
 gui/kivy/uix/dialogs/fee_dialog.py      |  1 -
 gui/kivy/uix/dialogs/settings.py        |  1 -
 gui/qt/main_window.py                   |  2 +-
 gui/qt/transaction_dialog.py            |  9 ++-
 lib/bitcoin.py                          |  4 --
 lib/simple_config.py                    | 81 ++++++++++++++++---------
 lib/util.py                             |  1 -
 lib/wallet.py                           |  4 +-
 9 files changed, 89 insertions(+), 63 deletions(-)

diff --git a/gui/kivy/uix/dialogs/bump_fee_dialog.py b/gui/kivy/uix/dialogs/bump_fee_dialog.py
index 1a6dc622..e27c9e54 100644
--- a/gui/kivy/uix/dialogs/bump_fee_dialog.py
+++ b/gui/kivy/uix/dialogs/bump_fee_dialog.py
@@ -3,7 +3,6 @@ from kivy.factory import Factory
 from kivy.properties import ObjectProperty
 from kivy.lang import Builder
 
-from electrum.util import fee_levels
 from electrum_gui.kivy.i18n import _
 
 Builder.load_string('''
@@ -29,7 +28,11 @@ Builder.load_string('''
                 text: _('New Fee')
                 value: ''
         Label:
-            id: tooltip
+            id: tooltip1
+            text: ''
+            size_hint_y: None
+        Label:
+            id: tooltip2
             text: ''
             size_hint_y: None
         Slider:
@@ -72,39 +75,39 @@ class BumpFeeDialog(Factory.Popup):
         self.tx_size = size
         self.callback = callback
         self.config = app.electrum_config
-        self.fee_step = self.config.max_fee_rate() / 10
-        self.dynfees = self.config.is_dynfee() and self.app.network
+        self.mempool = self.config.use_mempool_fees()
+        self.dynfees = self.config.is_dynfee() and 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.update_slider()
         self.update_text()
 
     def update_text(self):
-        value = int(self.ids.slider.value)
-        self.ids.new_fee.value = self.app.format_amount_and_units(self.get_fee())
-        if self.dynfees:
-            value = int(self.ids.slider.value)
-            self.ids.tooltip.text = fee_levels[value]
+        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)
+        self.ids.tooltip1.text = text
+        self.ids.tooltip2.text = tooltip
 
     def update_slider(self):
         slider = self.ids.slider
+        maxp, pos, fee_rate = self.config.get_fee_slider(self.dynfees, self.mempool)
+        slider.range = (0, maxp)
+        slider.step = 1
+        slider.value = pos
+
+    def get_fee_rate(self):
+        pos = int(self.ids.slider.value)
         if self.dynfees:
-            slider.range = (0, 4)
-            slider.step = 1
-            slider.value = 3
+            fee_rate = self.config.depth_to_fee(pos) if self.mempool else self.config.eta_to_fee(pos)
         else:
-            slider.range = (1, 10)
-            slider.step = 1
-            rate = self.init_fee*1000//self.tx_size
-            slider.value = min( rate * 2 // self.fee_step, 10)
+            fee_rate = self.config.static_fee(pos)
+        return fee_rate
 
     def get_fee(self):
-        value = int(self.ids.slider.value)
-        if self.dynfees:
-            if self.config.has_fee_estimates():
-                dynfee = self.config.dynfee(value)
-                return int(dynfee * self.tx_size // 1000)
-        else:
-            return int(value*self.fee_step * self.tx_size // 1000)
+        fee_rate = self.get_fee_rate()
+        return int(fee_rate * self.tx_size // 1000)
 
     def on_ok(self):
         new_fee = self.get_fee()
diff --git a/gui/kivy/uix/dialogs/fee_dialog.py b/gui/kivy/uix/dialogs/fee_dialog.py
index 1c61c6a2..cf29f36b 100644
--- a/gui/kivy/uix/dialogs/fee_dialog.py
+++ b/gui/kivy/uix/dialogs/fee_dialog.py
@@ -3,7 +3,6 @@ from kivy.factory import Factory
 from kivy.properties import ObjectProperty
 from kivy.lang import Builder
 
-from electrum.util import fee_levels
 from electrum_gui.kivy.i18n import _
 
 Builder.load_string('''
diff --git a/gui/kivy/uix/dialogs/settings.py b/gui/kivy/uix/dialogs/settings.py
index e73f3365..dad215e8 100644
--- a/gui/kivy/uix/dialogs/settings.py
+++ b/gui/kivy/uix/dialogs/settings.py
@@ -8,7 +8,6 @@ from electrum.i18n import languages
 from electrum_gui.kivy.i18n import _
 from electrum.plugins import run_hook
 from electrum import coinchooser
-from electrum.util import fee_levels
 
 from .choice_dialog import ChoiceDialog
 
diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
index ad0dd77b..36d087fe 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -1512,7 +1512,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
             x_fee_address, x_fee_amount = x_fee
             msg.append( _("Additional fees") + ": " + self.format_amount_and_units(x_fee_amount) )
 
-        confirm_rate = 2 * self.config.max_fee_rate()
+        confirm_rate = simple_config.FEERATE_WARNING_HIGH_FEE
         if fee > confirm_rate * tx.estimated_size() / 1000:
             msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high."))
 
diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py
index 5097af28..b417b6b8 100644
--- a/gui/qt/transaction_dialog.py
+++ b/gui/qt/transaction_dialog.py
@@ -33,6 +33,7 @@ from PyQt5.QtWidgets import *
 from electrum.bitcoin import base_encode
 from electrum.i18n import _
 from electrum.plugins import run_hook
+from electrum import simple_config
 
 from electrum.util import bfh
 from electrum.wallet import UnrelatedTransactionException
@@ -236,9 +237,13 @@ class TxDialog(QDialog, MessageBoxMixin):
         else:
             amount_str = _("Amount sent:") + ' %s'% format_amount(-amount) + ' ' + base_unit
         size_str = _("Size:") + ' %d bytes'% size
-        fee_str = _("Fee") + ': %s'% (format_amount(fee) + ' ' + base_unit if fee is not None else _('unknown'))
+        fee_str = _("Fee") + ': %s' % (format_amount(fee) + ' ' + base_unit if fee is not None else _('unknown'))
         if fee is not None:
-            fee_str += '  ( %s ) '%  self.main_window.format_fee_rate(fee/size*1000)
+            fee_rate = fee/size*1000
+            fee_str += '  ( %s ) ' % self.main_window.format_fee_rate(fee_rate)
+            confirm_rate = simple_config.FEERATE_WARNING_HIGH_FEE
+            if fee_rate > confirm_rate:
+                fee_str += ' - ' + _('Warning') + ': ' + _("high fee") + '!'
         self.amount_label.setText(amount_str)
         self.fee_label.setText(fee_str)
         self.size_label.setText(size_str)
diff --git a/lib/bitcoin.py b/lib/bitcoin.py
index 8b9f7967..b84f6a2c 100644
--- a/lib/bitcoin.py
+++ b/lib/bitcoin.py
@@ -108,10 +108,6 @@ NetworkConstants.set_mainnet()
 
 ################################## transactions
 
-FEE_STEP = 10000
-MAX_FEE_RATE = 300000
-
-
 COINBASE_MATURITY = 100
 COIN = 100000000
 
diff --git a/lib/simple_config.py b/lib/simple_config.py
index 072edccd..bf57d5b5 100644
--- a/lib/simple_config.py
+++ b/lib/simple_config.py
@@ -5,14 +5,22 @@ import os
 import stat
 
 from copy import deepcopy
+
 from .util import (user_dir, print_error, PrintError,
                    NoDynamicFeeEstimates, format_satoshis)
-
-from .bitcoin import MAX_FEE_RATE
+from .i18n import _
 
 FEE_ETA_TARGETS = [25, 10, 5, 2]
 FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000]
 
+# satoshi per kbyte
+FEERATE_MAX_DYNAMIC = 1500000
+FEERATE_WARNING_HIGH_FEE = 600000
+FEERATE_FALLBACK_STATIC_FEE = 150000
+FEERATE_DEFAULT_RELAY = 1000
+FEERATE_STATIC_VALUES = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000]
+
+
 config = None
 
 
@@ -39,7 +47,6 @@ class SimpleConfig(PrintError):
         2. User configuration (in the user's config directory)
     They are taken in order (1. overrides config options set in 2.)
     """
-    fee_rates = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000]
 
     def __init__(self, options=None, read_user_config_function=None,
                  read_user_dir_function=None):
@@ -261,13 +268,19 @@ class SimpleConfig(PrintError):
             path = wallet.storage.path
             self.set_key('gui_last_wallet', path)
 
-    def max_fee_rate(self):
-        f = self.get('max_fee_rate', MAX_FEE_RATE)
-        if f==0:
-            f = MAX_FEE_RATE
-        return f
+    def impose_hard_limits_on_fee(func):
+        def get_fee_within_limits(self, *args, **kwargs):
+            fee = func(self, *args, **kwargs)
+            if fee is None:
+                return fee
+            fee = min(FEERATE_MAX_DYNAMIC, fee)
+            fee = max(FEERATE_DEFAULT_RELAY, fee)
+            return fee
+        return get_fee_within_limits
 
+    @impose_hard_limits_on_fee
     def eta_to_fee(self, i):
+        """Returns fee in sat/kbyte."""
         if i < 4:
             j = FEE_ETA_TARGETS[i]
             fee = self.fee_estimates.get(j)
@@ -276,8 +289,6 @@ class SimpleConfig(PrintError):
             fee = self.fee_estimates.get(2)
             if fee is not None:
                 fee += fee/2
-        if fee is not None:
-            fee = min(5*MAX_FEE_RATE, fee)
         return fee
 
     def fee_to_depth(self, target_fee):
@@ -290,7 +301,9 @@ class SimpleConfig(PrintError):
             return 0
         return depth
 
+    @impose_hard_limits_on_fee
     def depth_to_fee(self, i):
+        """Returns fee in sat/kbyte."""
         target = self.depth_target(i)
         depth = 0
         for fee, s in self.mempool_fees:
@@ -305,6 +318,8 @@ class SimpleConfig(PrintError):
         return FEE_DEPTH_TARGETS[i]
 
     def eta_target(self, i):
+        if i == len(FEE_ETA_TARGETS):
+            return 1
         return FEE_ETA_TARGETS[i]
 
     def fee_to_eta(self, fee_per_kb):
@@ -320,7 +335,12 @@ class SimpleConfig(PrintError):
         return "%.1f MB from tip"%(depth/1000000)
 
     def eta_tooltip(self, x):
-        return 'Low fee' if x < 0 else 'Within %d blocks'%x
+        if x < 0:
+            return _('Low fee')
+        elif x == 1:
+            return _('In the next block')
+        else:
+            return _('Within {} blocks').format(x)
 
     def get_fee_status(self):
         dyn = self.is_dynfee()
@@ -331,6 +351,10 @@ class SimpleConfig(PrintError):
         return target
 
     def get_fee_text(self, pos, dyn, mempool, fee_rate):
+        """Returns (text, tooltip) where
+        text is what we target: static fee / num blocks to confirm in / mempool depth
+        tooltip is the corresponding estimate (e.g. num blocks for a static fee)
+        """
         rate_str = (format_satoshis(fee_rate/1000, False, 0, 0, False)  + ' sat/byte') if fee_rate is not None else 'unknown'
         if dyn:
             if mempool:
@@ -342,18 +366,14 @@ class SimpleConfig(PrintError):
             tooltip = rate_str
         else:
             text = rate_str
-            if mempool:
-                if self.has_fee_mempool():
-                    depth = self.fee_to_depth(fee_rate)
-                    tooltip = self.depth_tooltip(depth)
-                else:
-                    tooltip = ''
+            if mempool and self.has_fee_mempool():
+                depth = self.fee_to_depth(fee_rate)
+                tooltip = self.depth_tooltip(depth)
+            elif not mempool and self.has_fee_etas():
+                eta = self.fee_to_eta(fee_rate)
+                tooltip = self.eta_tooltip(eta)
             else:
-                if self.has_fee_etas():
-                    eta = self.fee_to_eta(fee_rate)
-                    tooltip = self.eta_tooltip(eta)
-                else:
-                    tooltip = ''
+                tooltip = ''
         return text, tooltip
 
     def get_depth_level(self):
@@ -361,7 +381,7 @@ class SimpleConfig(PrintError):
         return min(maxp, self.get('depth_level', 2))
 
     def get_fee_level(self):
-        maxp = len(FEE_ETA_TARGETS) - 1
+        maxp = len(FEE_ETA_TARGETS)  # not (-1) to have "next block"
         return min(maxp, self.get('fee_level', 2))
 
     def get_fee_slider(self, dyn, mempool):
@@ -372,7 +392,7 @@ class SimpleConfig(PrintError):
                 fee_rate = self.depth_to_fee(pos)
             else:
                 pos = self.get_fee_level()
-                maxp = len(FEE_ETA_TARGETS) - 1
+                maxp = len(FEE_ETA_TARGETS)  # not (-1) to have "next block"
                 fee_rate = self.eta_to_fee(pos)
         else:
             fee_rate = self.fee_per_kb()
@@ -380,12 +400,11 @@ class SimpleConfig(PrintError):
             maxp = 9
         return maxp, pos, fee_rate
 
-
     def static_fee(self, i):
-        return self.fee_rates[i]
+        return FEERATE_STATIC_VALUES[i]
 
     def static_fee_index(self, value):
-        dist = list(map(lambda x: abs(x - value), self.fee_rates))
+        dist = list(map(lambda x: abs(x - value), FEERATE_STATIC_VALUES))
         return min(range(len(dist)), key=dist.__getitem__)
 
     def has_fee_etas(self):
@@ -394,6 +413,12 @@ class SimpleConfig(PrintError):
     def has_fee_mempool(self):
         return bool(self.mempool_fees)
 
+    def has_dynamic_fees_ready(self):
+        if self.use_mempool_fees():
+            return self.has_fee_mempool()
+        else:
+            return self.has_fee_etas()
+
     def is_dynfee(self):
         return bool(self.get('dynamic_fees', True))
 
@@ -410,7 +435,7 @@ class SimpleConfig(PrintError):
             else:
                 fee_rate = self.eta_to_fee(self.get_fee_level())
         else:
-            fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2)
+            fee_rate = self.get('fee_per_kb', FEERATE_FALLBACK_STATIC_FEE)
         return fee_rate
 
     def fee_per_byte(self):
diff --git a/lib/util.py b/lib/util.py
index fd9bd059..4368d12b 100644
--- a/lib/util.py
+++ b/lib/util.py
@@ -41,7 +41,6 @@ def inv_dict(d):
 
 
 base_units = {'BTC':8, 'mBTC':5, 'uBTC':2}
-fee_levels = [_('Within 25 blocks'), _('Within 10 blocks'), _('Within 5 blocks'), _('Within 2 blocks'), _('In the next block')]
 
 def normalize_version(v):
     return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
diff --git a/lib/wallet.py b/lib/wallet.py
index b14f5831..b43d540c 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -78,9 +78,9 @@ TX_HEIGHT_UNCONFIRMED = 0
 
 
 def relayfee(network):
-    RELAY_FEE = 1000
+    from .simple_config import FEERATE_DEFAULT_RELAY
     MAX_RELAY_FEE = 50000
-    f = network.relay_fee if network and network.relay_fee else RELAY_FEE
+    f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY
     return min(f, MAX_RELAY_FEE)
 
 def dust_threshold(network):

From 6f5751977bdb3175e6b6b42c570ec7468f3115bd Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Wed, 21 Feb 2018 03:58:08 +0100
Subject: [PATCH 68/91] local tx: restructure exception handling wrt
 wallet.add_transaction and QT

---
 gui/qt/history_list.py       | 18 +++++++-----------
 gui/qt/main_window.py        | 20 +++++++++++++++++++-
 gui/qt/transaction_dialog.py | 16 ++++------------
 lib/wallet.py                | 17 +++++++++++++----
 4 files changed, 43 insertions(+), 28 deletions(-)

diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
index 49a01f9b..92e6fc0c 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -26,7 +26,7 @@
 import webbrowser
 import datetime
 
-from electrum.wallet import UnrelatedTransactionException, TX_HEIGHT_LOCAL
+from electrum.wallet import AddTransactionException, TX_HEIGHT_LOCAL
 from .util import *
 from electrum.i18n import _
 from electrum.util import block_explorer_URL
@@ -356,16 +356,12 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
         self.parent.need_update.set()
 
     def onFileAdded(self, fn):
-        with open(fn) as f:
-            tx = self.parent.tx_from_text(f.read())
-            try:
-                self.wallet.add_transaction(tx.txid(), tx)
-            except UnrelatedTransactionException as e:
-                self.parent.show_error(e)
-            else:
-                self.wallet.save_transactions(write=True)
-                # need to update at least: history_list, utxo_list, address_list
-                self.parent.need_update.set()
+        try:
+            with open(fn) as f:
+                tx = self.parent.tx_from_text(f.read())
+                self.parent.save_transaction_into_wallet(tx)
+        except IOError as e:
+            self.parent.show_error(e)
 
     def export_history_dialog(self):
         d = WindowModalDialog(self, _('Export History'))
diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
index ad0dd77b..cd2d5f00 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -51,7 +51,7 @@ from electrum.util import (format_time, format_satoshis, PrintError,
 from electrum import Transaction
 from electrum import util, bitcoin, commands, coinchooser
 from electrum import paymentrequest
-from electrum.wallet import Multisig_Wallet
+from electrum.wallet import Multisig_Wallet, AddTransactionException
 
 from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
 from .qrcodewidget import QRCodeWidget, QRDialog
@@ -3125,3 +3125,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
         if is_final:
             new_tx.set_rbf(False)
         self.show_transaction(new_tx, tx_label)
+
+    def save_transaction_into_wallet(self, tx):
+        try:
+            if not self.wallet.add_transaction(tx.txid(), tx):
+                self.show_error(_("Transaction could not be saved.") + "\n" +
+                                       _("It conflicts with current history."))
+                return False
+        except AddTransactionException as e:
+            self.show_error(e)
+            return False
+        else:
+            self.wallet.save_transactions(write=True)
+            # need to update at least: history_list, utxo_list, address_list
+            self.need_update.set()
+            self.show_message(_("Transaction saved successfully"))
+            return True
+
+
diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py
index 5097af28..49ce31dc 100644
--- a/gui/qt/transaction_dialog.py
+++ b/gui/qt/transaction_dialog.py
@@ -35,7 +35,7 @@ from electrum.i18n import _
 from electrum.plugins import run_hook
 
 from electrum.util import bfh
-from electrum.wallet import UnrelatedTransactionException
+from electrum.wallet import AddTransactionException
 
 from .util import *
 
@@ -179,17 +179,9 @@ class TxDialog(QDialog, MessageBoxMixin):
         self.main_window.sign_tx(self.tx, sign_done)
 
     def save(self):
-        if not self.wallet.add_transaction(self.tx.txid(), self.tx):
-            self.show_error(_("Transaction could not be saved. It conflicts with current history."))
-            return
-        self.wallet.save_transactions(write=True)
-
-        # need to update at least: history_list, utxo_list, address_list
-        self.main_window.need_update.set()
-
-        self.save_button.setDisabled(True)
-        self.show_message(_("Transaction saved successfully"))
-        self.saved = True
+        if self.main_window.save_transaction_into_wallet(self.tx):
+            self.save_button.setDisabled(True)
+            self.saved = True
 
 
     def export(self):
diff --git a/lib/wallet.py b/lib/wallet.py
index b14f5831..49c0f972 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -157,9 +157,18 @@ def sweep(privkeys, network, config, recipient, fee=None, imax=100):
     return tx
 
 
-class UnrelatedTransactionException(Exception):
-    def __init__(self):
-        self.args = ("Transaction is unrelated to this wallet ", )
+class AddTransactionException(Exception):
+    pass
+
+
+class UnrelatedTransactionException(AddTransactionException):
+    def __str__(self):
+        return _("Transaction is unrelated to this wallet.")
+
+
+class NotIsMineTransactionException(AddTransactionException):
+    def __str__(self):
+        return _("Only transactions with inputs owned by the wallet can be added.")
 
 
 class Abstract_Wallet(PrintError):
@@ -768,7 +777,7 @@ class Abstract_Wallet(PrintError):
             # do not save if tx is local and not mine
             if tx_height == TX_HEIGHT_LOCAL and not is_mine:
                 # FIXME the test here should be for "not all is_mine"; cannot detect conflict in some cases
-                return False
+                raise NotIsMineTransactionException()
             # raise exception if unrelated to wallet
             is_for_me = any([self.is_mine(self.get_txout_address(txo)) for txo in tx.outputs()])
             if not is_mine and not is_for_me:

From 9f7e256e39176905715a0fe4018e151a8dedfccd Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Wed, 21 Feb 2018 11:52:40 +0100
Subject: [PATCH 69/91] cleanup get_full_history. fix #3939

---
 gui/qt/history_list.py |  4 +--
 lib/exchange_rate.py   |  4 ---
 lib/util.py            |  5 +---
 lib/wallet.py          | 60 +++++++++++++++++++++---------------------
 4 files changed, 33 insertions(+), 40 deletions(-)

diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
index 92e6fc0c..b2029d37 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -203,8 +203,8 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
         self.transactions = r['transactions']
         self.summary = r['summary']
         if not self.years and self.start_timestamp is None and self.end_timestamp is None:
-            start_date = self.summary['start_date']
-            end_date = self.summary['end_date']
+            start_date = self.summary.get('start_date')
+            end_date = self.summary.get('end_date')
             if start_date and end_date:
                 self.years = [str(i) for i in range(start_date.year, end_date.year + 1)]
                 self.period_combo.insertItems(1, self.years)
diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py
index 589ef6ed..75979050 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -108,8 +108,6 @@ class ExchangeBase(PrintError):
         return []
 
     def historical_rate(self, ccy, d_t):
-        if d_t is None:
-            return 'NaN'
         return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'), 'NaN')
 
     def get_currencies(self):
@@ -520,8 +518,6 @@ class FxThread(ThreadJob):
         return "%s" % (self.ccy_amount_str(value, True))
 
     def history_rate(self, d_t):
-        if d_t is None:
-            return Decimal('NaN')
         rate = self.exchange.historical_rate(self.ccy, d_t)
         # Frequently there is no rate for today, until tomorrow :)
         # Use spot quotes in that case
diff --git a/lib/util.py b/lib/util.py
index fd9bd059..9dc95b91 100644
--- a/lib/util.py
+++ b/lib/util.py
@@ -416,10 +416,7 @@ def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespa
     return result
 
 def timestamp_to_datetime(timestamp):
-    try:
-        return datetime.fromtimestamp(timestamp)
-    except:
-        return None
+    return datetime.fromtimestamp(timestamp)
 
 def format_time(timestamp):
     date = timestamp_to_datetime(timestamp)
diff --git a/lib/wallet.py b/lib/wallet.py
index df7b119c..c652902d 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -969,11 +969,6 @@ class Abstract_Wallet(PrintError):
     def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx=None, show_addresses=False):
         from .util import timestamp_to_datetime, Satoshis, Fiat
         out = []
-        init_balance = None
-        init_timestamp = None
-        end_balance = None
-        end_timestamp = None
-        end_balance = 0
         capital_gains = 0
         fiat_income = 0
         h = self.get_history(domain)
@@ -990,11 +985,6 @@ class Abstract_Wallet(PrintError):
                 'value': Satoshis(value),
                 'balance': Satoshis(balance)
             }
-            if init_balance is None:
-                init_balance = balance - value
-                init_timestamp = timestamp
-            end_balance = balance
-            end_timestamp = timestamp
             item['date'] = timestamp_to_datetime(timestamp) if timestamp is not None else None
             item['label'] = self.get_label(tx_hash)
             if show_addresses:
@@ -1032,28 +1022,38 @@ class Abstract_Wallet(PrintError):
                     if fiat_value is not None:
                         fiat_income += fiat_value
             out.append(item)
-        result = {'transactions': out}
-        if from_timestamp is not None and to_timestamp is not None:
-            start_date = timestamp_to_datetime(from_timestamp)
-            end_date = timestamp_to_datetime(to_timestamp)
+        # add summary
+        if out:
+            start_balance = out[0]['balance'].value - out[0]['value'].value
+            end_balance = out[-1]['balance'].value
+            if from_timestamp is not None and to_timestamp is not None:
+                start_date = timestamp_to_datetime(from_timestamp)
+                end_date = timestamp_to_datetime(to_timestamp)
+            else:
+                start_date = out[0]['date']
+                end_date = out[-1]['date']
+
+            summary = {
+                'start_date': start_date,
+                'end_date': end_date,
+                'start_balance': Satoshis(start_balance),
+                'end_balance': Satoshis(end_balance)
+            }
+            if fx:
+                unrealized = self.unrealized_gains(domain, fx.timestamp_rate, fx.ccy)
+                summary['capital_gains'] = Fiat(capital_gains, fx.ccy)
+                summary['fiat_income'] = Fiat(fiat_income, fx.ccy)
+                summary['unrealized_gains'] = Fiat(unrealized, fx.ccy)
+                if start_date:
+                    summary['start_fiat_balance'] = Fiat(fx.historical_value(start_balance, start_date), fx.ccy)
+                if end_date:
+                    summary['end_fiat_balance'] = Fiat(fx.historical_value(end_balance, end_date), fx.ccy)
         else:
-            start_date = timestamp_to_datetime(init_timestamp)
-            end_date = timestamp_to_datetime(end_timestamp)
-        summary = {
-            'start_date': start_date,
-            'end_date': end_date,
-            'start_balance': Satoshis(init_balance),
-            'end_balance': Satoshis(end_balance)
+            summary = {}
+        return {
+            'transactions': out,
+            'summary': summary
         }
-        result['summary'] = summary
-        if fx:
-            unrealized = self.unrealized_gains(domain, fx.timestamp_rate, fx.ccy)
-            summary['start_fiat_balance'] = Fiat(fx.historical_value(init_balance, start_date), fx.ccy)
-            summary['end_fiat_balance'] = Fiat(fx.historical_value(end_balance, end_date), fx.ccy)
-            summary['capital_gains'] = Fiat(capital_gains, fx.ccy)
-            summary['fiat_income'] = Fiat(fiat_income, fx.ccy)
-            summary['unrealized_gains'] = Fiat(unrealized, fx.ccy)
-        return result
 
     def get_label(self, tx_hash):
         label = self.labels.get(tx_hash, '')

From 93619c8341dd57c7d4e7c3bd2a2d82bf47207919 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Wed, 21 Feb 2018 13:31:01 +0100
Subject: [PATCH 70/91] make qt gui even more resistant against ill-formed txns

see #3945
---
 gui/qt/main_window.py        | 20 ++++++--------------
 gui/qt/transaction_dialog.py | 20 ++++++++++++++++----
 2 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
index ccf82bec..6b4a1f9b 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -2288,25 +2288,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
         return self.tx_from_text(file_content)
 
     def do_process_from_text(self):
-        from electrum.transaction import SerializationError
         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
         if not text:
             return
-        try:
-            tx = self.tx_from_text(text)
-            if tx:
-                self.show_transaction(tx)
-        except SerializationError as e:
-            self.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
+        tx = self.tx_from_text(text)
+        if tx:
+            self.show_transaction(tx)
 
     def do_process_from_file(self):
-        from electrum.transaction import SerializationError
-        try:
-            tx = self.read_tx_from_file()
-            if tx:
-                self.show_transaction(tx)
-        except SerializationError as e:
-            self.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
+        tx = self.read_tx_from_file()
+        if tx:
+            self.show_transaction(tx)
 
     def do_process_from_txid(self):
         from electrum import transaction
diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py
index 49ce31dc..20f78a06 100644
--- a/gui/qt/transaction_dialog.py
+++ b/gui/qt/transaction_dialog.py
@@ -25,6 +25,7 @@
 import copy
 import datetime
 import json
+import traceback
 
 from PyQt5.QtCore import *
 from PyQt5.QtGui import *
@@ -36,15 +37,23 @@ from electrum.plugins import run_hook
 
 from electrum.util import bfh
 from electrum.wallet import AddTransactionException
+from electrum.transaction import SerializationError
 
 from .util import *
 
 dialogs = []  # Otherwise python randomly garbage collects the dialogs...
 
+
 def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False):
-    d = TxDialog(tx, parent, desc, prompt_if_unsaved)
-    dialogs.append(d)
-    d.show()
+    try:
+        d = TxDialog(tx, parent, desc, prompt_if_unsaved)
+    except SerializationError as e:
+        traceback.print_exc(file=sys.stderr)
+        parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
+    else:
+        dialogs.append(d)
+        d.show()
+
 
 class TxDialog(QDialog, MessageBoxMixin):
 
@@ -58,7 +67,10 @@ class TxDialog(QDialog, MessageBoxMixin):
         # e.g. the FX plugin.  If this happens during or after a long
         # sign operation the signatures are lost.
         self.tx = copy.deepcopy(tx)
-        self.tx.deserialize()
+        try:
+            self.tx.deserialize()
+        except BaseException as e:
+            raise SerializationError(e)
         self.main_window = parent
         self.wallet = parent.wallet
         self.prompt_if_unsaved = prompt_if_unsaved

From e7c3712181e304b8661d52391c5ca4ab3c49f575 Mon Sep 17 00:00:00 2001
From: Johann Bauer 
Date: Wed, 21 Feb 2018 13:56:42 +0100
Subject: [PATCH 71/91] Add libusb dylib to binary so Trezor will work

Closes: #3946
---
 contrib/build-osx/make_osx |  6 ++++++
 contrib/build-osx/osx.spec | 20 ++++++--------------
 2 files changed, 12 insertions(+), 14 deletions(-)

diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx
index a2f7b50f..e5a65604 100755
--- a/contrib/build-osx/make_osx
+++ b/contrib/build-osx/make_osx
@@ -59,6 +59,12 @@ done
 cp -R $BUILDDIR/electrum-locale/locale/ ./lib/locale/
 cp    $BUILDDIR/electrum-icons/icons_rc.py ./gui/qt/
 
+
+info "Downloading libusb..."
+curl https://homebrew.bintray.com/bottles/libusb-1.0.21.el_capitan.bottle.tar.gz | \
+tar xz --directory $BUILDDIR
+cp $BUILDDIR/libusb/1.0.21/lib/libusb-1.0.dylib contrib/build-osx
+
 info "Installing requirements..."
 python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \
 python3 -m pip install -Ir ./contrib/deterministic-build/requirements-binaries.txt --user || \
diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec
index bb48dddf..caef2519 100644
--- a/contrib/build-osx/osx.spec
+++ b/contrib/build-osx/osx.spec
@@ -40,20 +40,12 @@ datas += collect_data_files('trezorlib')
 datas += collect_data_files('btchip')
 datas += collect_data_files('keepkeylib')
 
-# We had an issue with PyQt 5.10 not picking up the libqmacstyles.dylib properly,
-# and thus Electrum looking terrible on Mac.
-# The below 3 statements are a workaround for that issue.
-# This should 'do nothing bad' in any case should a future version of PyQt5 not even
-# need this.
-binaries = []
-dylibs_in_pyqt5 = collect_dynamic_libs('PyQt5', 'DUMMY_NOT_USED')
-for tuple in dylibs_in_pyqt5:
-    # find libqmacstyle.dylib ...
-    if "libqmacstyle.dylib" in tuple[0]:
-        # .. and include all the .dylibs in that dir in our 'binaries' PyInstaller spec
-        binaries += [( os.path.dirname(tuple[0]) + '/*.dylib', 'PyQt5/Qt/plugins/styles' )]
-        break
- 
+# Add libusb so Trezor will work
+binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
+
+# Workaround for "Retro Look":
+binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]]
+
 # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
 a = Analysis([electrum+MAIN_SCRIPT,
               electrum+'gui/qt/main_window.py',

From 51f04d4e7bdb1021c5535c47b676c3d2d084eebf Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Wed, 21 Feb 2018 14:29:39 +0100
Subject: [PATCH 72/91] compute capital gains using wallet.txi and txo

---
 lib/wallet.py | 59 ++++++++++++++++++++++++++++-----------------------
 1 file changed, 32 insertions(+), 27 deletions(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index c652902d..20a12490 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -503,6 +503,17 @@ class Abstract_Wallet(PrintError):
             delta += v
         return delta
 
+    def get_tx_value(self, txid):
+        " effect of tx on the entire domain"
+        delta = 0
+        for addr, d in self.txi.get(txid, {}).items():
+            for n, v in d:
+                delta -= v
+        for addr, d in self.txo.get(txid, {}).items():
+            for n, v, cb in d:
+                delta += v
+        return delta
+
     def get_wallet_delta(self, tx):
         """ effect of tx on wallet """
         addresses = self.get_addresses()
@@ -1694,7 +1705,7 @@ class Abstract_Wallet(PrintError):
         coins = self.get_utxos(domain)
         now = time.time()
         p = price_func(now)
-        ap = sum(self.coin_price(coin, price_func, ccy, self.txin_value(coin)) for coin in coins)
+        ap = sum(self.coin_price(coin['prevout_hash'], price_func, ccy, self.txin_value(coin)) for coin in coins)
         lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN)
         return lp - ap
 
@@ -1704,42 +1715,36 @@ class Abstract_Wallet(PrintError):
         and the price of these coins when they entered the wallet.
         price_func: function that returns the fiat price given a timestamp
         """
-        tx = self.transactions[txid]
-        ir, im, v, fee = self.get_wallet_delta(tx)
-        out_value = -v
+        out_value = - self.get_tx_value(txid)/Decimal(COIN)
         fiat_value = self.get_fiat_value(txid, ccy)
-        if fiat_value is None:
-            p = self.price_at_timestamp(txid, price_func)
-            liquidation_price = out_value/Decimal(COIN) * p
-        else:
-            liquidation_price = - fiat_value
-
-        acquisition_price = out_value/Decimal(COIN) * self.average_price(tx, price_func, ccy)
+        liquidation_price = - fiat_value if fiat_value else out_value * self.price_at_timestamp(txid, price_func)
+        acquisition_price = out_value * self.average_price(txid, price_func, ccy)
         return acquisition_price, liquidation_price
 
-    def average_price(self, tx, price_func, ccy):
-        """ average price of the inputs of a transaction """
-        input_value = sum(self.txin_value(txin) for txin in tx.inputs()) / Decimal(COIN)
-        total_price = sum(self.coin_price(txin, price_func, ccy, self.txin_value(txin)) for txin in tx.inputs())
-        return total_price / input_value
+    def average_price(self, txid, price_func, ccy):
+        """ Average acquisition price of the inputs of a transaction """
+        input_value = 0
+        total_price = 0
+        for addr, d in self.txi.get(txid, {}).items():
+            for ser, v in d:
+                input_value += v
+                total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v)
+        return total_price / (input_value/Decimal(COIN))
 
-    def coin_price(self, coin, price_func, ccy, txin_value):
-        """ fiat price of acquisition of coin """
-        txid = coin['prevout_hash']
-        tx = self.transactions[txid]
-        if all([self.is_mine(txin['address']) for txin in tx.inputs()]):
-            return self.average_price(tx, price_func, ccy) * txin_value/Decimal(COIN)
-        elif all([ not self.is_mine(txin['address']) for txin in tx.inputs()]):
+    def coin_price(self, txid, price_func, ccy, txin_value):
+        """
+        Acquisition price of a coin.
+        This assumes that either all inputs are mine, or no input is mine.
+        """
+        if self.txi.get(txid, {}) != {}:
+            return self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN)
+        else:
             fiat_value = self.get_fiat_value(txid, ccy)
             if fiat_value is not None:
                 return fiat_value
             else:
                 p = self.price_at_timestamp(txid, price_func)
                 return p * txin_value/Decimal(COIN)
-        else:
-            # could be some coinjoin transaction..
-            return Decimal('NaN')
-
 
 class Simple_Wallet(Abstract_Wallet):
     # wallet with a single keystore

From 8b9b0d3cf30805a04859700c62f21cb71917ffb2 Mon Sep 17 00:00:00 2001
From: Johann Bauer 
Date: Fri, 9 Feb 2018 13:07:57 +0100
Subject: [PATCH 73/91] Test Windows build using Travis

---
 .travis.yml | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/.travis.yml b/.travis.yml
index 38a0acf8..9b129e33 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,3 +12,17 @@ script:
 after_success:
     - if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi
     - coveralls
+jobs:
+  include:
+    - stage: windows build
+      sudo: true
+      python: 3.5
+      install:
+        - sudo dpkg --add-architecture i386
+        - wget -nc https://dl.winehq.org/wine-builds/Release.key
+        - sudo apt-key add Release.key
+        - sudo apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/
+        - sudo apt-get update -qq
+        - sudo apt-get install -qq winehq-stable dirmngr gnupg2 p7zip-full
+      script: ./contrib/build-wine/build.sh
+      after_success: true

From d971a75ef80d2a50a1ba942c60865a3159624ffc Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Wed, 21 Feb 2018 15:28:00 +0100
Subject: [PATCH 74/91] fix #3941

---
 gui/kivy/i18n.py                   | 2 +-
 gui/kivy/uix/ui_screens/network.kv | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/gui/kivy/i18n.py b/gui/kivy/i18n.py
index e0be3908..1eb005f9 100644
--- a/gui/kivy/i18n.py
+++ b/gui/kivy/i18n.py
@@ -15,7 +15,7 @@ class _(str):
 
     @staticmethod
     def translate(s, *args, **kwargs):
-        return _.lang(s).format(args, kwargs)
+        return _.lang(s).format(*args, **kwargs)
 
     @staticmethod
     def bind(label):
diff --git a/gui/kivy/uix/ui_screens/network.kv b/gui/kivy/uix/ui_screens/network.kv
index f499618a..db5f0ed2 100644
--- a/gui/kivy/uix/ui_screens/network.kv
+++ b/gui/kivy/uix/ui_screens/network.kv
@@ -11,7 +11,7 @@ Popup:
                 height: self.minimum_height
                 padding: '10dp'
                 SettingsItem:
-                    value: _("{} connections.").format(app.num_nodes) if app.num_nodes else _("Not connected")
+                    value: _("{} connections.", app.num_nodes) if app.num_nodes else _("Not connected")
                     title: _("Status") + ': ' + self.value
                     description: _("Connections with Electrum servers")
                     action: lambda x: None
@@ -46,7 +46,7 @@ Popup:
 
                 CardSeparator
                 SettingsItem:
-                    title: _('Fork detected at block {}').format(app.blockchain_checkpoint) if app.num_chains>1 else _('No fork detected')
+                    title: _('Fork detected at block {}', app.blockchain_checkpoint) if app.num_chains>1 else _('No fork detected')
                     fork_description: (_('You are following branch') if app.auto_connect else _("Your server is on branch")) + ' ' + app.blockchain_name
                     description: self.fork_description if app.num_chains>1 else _('Connected nodes are on the same chain')
                     action: app.choose_blockchain_dialog

From 180480099926946de19e6f5db8296fc5b56683dc Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Wed, 21 Feb 2018 16:08:32 +0100
Subject: [PATCH 75/91] fix #3941

follow-up d971a75ef80d2a50a1ba942c60865a3159624ffc
---
 gui/kivy/i18n.py                   | 7 ++++---
 gui/kivy/uix/ui_screens/network.kv | 4 ++--
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/gui/kivy/i18n.py b/gui/kivy/i18n.py
index 1eb005f9..733249d3 100644
--- a/gui/kivy/i18n.py
+++ b/gui/kivy/i18n.py
@@ -1,21 +1,22 @@
 import gettext
 
+
 class _(str):
 
     observers = set()
     lang = None
 
-    def __new__(cls, s, *args, **kwargs):
+    def __new__(cls, s):
         if _.lang is None:
             _.switch_lang('en')
-        t = _.translate(s, *args, **kwargs)
+        t = _.translate(s)
         o = super(_, cls).__new__(cls, t)
         o.source_text = s
         return o
 
     @staticmethod
     def translate(s, *args, **kwargs):
-        return _.lang(s).format(*args, **kwargs)
+        return _.lang(s)
 
     @staticmethod
     def bind(label):
diff --git a/gui/kivy/uix/ui_screens/network.kv b/gui/kivy/uix/ui_screens/network.kv
index db5f0ed2..f499618a 100644
--- a/gui/kivy/uix/ui_screens/network.kv
+++ b/gui/kivy/uix/ui_screens/network.kv
@@ -11,7 +11,7 @@ Popup:
                 height: self.minimum_height
                 padding: '10dp'
                 SettingsItem:
-                    value: _("{} connections.", app.num_nodes) if app.num_nodes else _("Not connected")
+                    value: _("{} connections.").format(app.num_nodes) if app.num_nodes else _("Not connected")
                     title: _("Status") + ': ' + self.value
                     description: _("Connections with Electrum servers")
                     action: lambda x: None
@@ -46,7 +46,7 @@ Popup:
 
                 CardSeparator
                 SettingsItem:
-                    title: _('Fork detected at block {}', app.blockchain_checkpoint) if app.num_chains>1 else _('No fork detected')
+                    title: _('Fork detected at block {}').format(app.blockchain_checkpoint) if app.num_chains>1 else _('No fork detected')
                     fork_description: (_('You are following branch') if app.auto_connect else _("Your server is on branch")) + ' ' + app.blockchain_name
                     description: self.fork_description if app.num_chains>1 else _('Connected nodes are on the same chain')
                     action: app.choose_blockchain_dialog

From 89e0f90e1f8208a0309194ec22034598e7eeb9c6 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Wed, 21 Feb 2018 16:45:34 +0100
Subject: [PATCH 76/91] fix #3949

---
 lib/wallet.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index 20a12490..3bfd3c08 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -718,7 +718,9 @@ class Abstract_Wallet(PrintError):
 
     def get_address_history(self, addr):
         h = []
-        with self.transaction_lock:
+        # we need self.transaction_lock but get_tx_height will take self.lock
+        # so we need to take that too here, to enforce order of locks
+        with self.lock, self.transaction_lock:
             for tx_hash in self.transactions:
                 if addr in self.txi.get(tx_hash, []) or addr in self.txo.get(tx_hash, []):
                     tx_height = self.get_tx_height(tx_hash)[0]
@@ -775,7 +777,9 @@ class Abstract_Wallet(PrintError):
             return conflicting_txns
 
     def add_transaction(self, tx_hash, tx):
-        with self.transaction_lock:
+        # we need self.transaction_lock but get_tx_height will take self.lock
+        # so we need to take that too here, to enforce order of locks
+        with self.lock, self.transaction_lock:
             # NOTE: returning if tx in self.transactions might seem like a good idea
             # BUT we track is_mine inputs in a txn, and during subsequent calls
             # of add_transaction tx, we might learn of more-and-more inputs of

From 5997c18aef7200d8367014707f9212b440372010 Mon Sep 17 00:00:00 2001
From: Abdussamad 
Date: Thu, 15 Feb 2018 18:07:00 +0100
Subject: [PATCH 77/91] better code organization

function parameters should be lowercase

Fix crash on invalid labels import

Added invoice exporting and reduced duplicate code

Better exception handling

removed json module import

some more cleanup

Cleaned up some stuff

Added exporting contacts
---
 gui/qt/contact_list.py | 19 ++++++++-----------
 gui/qt/invoice_list.py | 15 +++++----------
 gui/qt/main_window.py  | 40 ++++++++++++++++++----------------------
 gui/qt/util.py         | 23 +++++++++++++++++++++++
 lib/contacts.py        | 22 ++++++++++------------
 lib/paymentrequest.py  | 32 ++++++++++++++++++--------------
 lib/util.py            | 35 +++++++++++++++++++++++++++++------
 7 files changed, 111 insertions(+), 75 deletions(-)

diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py
index a1794459..81b6ca86 100644
--- a/gui/qt/contact_list.py
+++ b/gui/qt/contact_list.py
@@ -23,16 +23,17 @@
 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 import webbrowser
+import os
 
 from electrum.i18n import _
 from electrum.bitcoin import is_address
-from electrum.util import block_explorer_URL, FileImportFailed
+from electrum.util import block_explorer_URL
 from electrum.plugins import run_hook
 from PyQt5.QtGui import *
 from PyQt5.QtCore import *
 from PyQt5.QtWidgets import (
     QAbstractItemView, QFileDialog, QMenu, QTreeWidgetItem)
-from .util import MyTreeWidget
+from .util import MyTreeWidget, import_meta_gui, export_meta_gui
 
 
 class ContactList(MyTreeWidget):
@@ -53,15 +54,10 @@ class ContactList(MyTreeWidget):
         self.parent.set_contact(item.text(0), item.text(1))
 
     def import_contacts(self):
-        wallet_folder = self.parent.get_wallet_folder()
-        filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder)
-        if not filename:
-            return
-        try:
-            self.parent.contacts.import_file(filename)
-        except FileImportFailed as e:
-            self.parent.show_message(str(e))
-        self.on_update()
+        import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.on_update)
+
+    def export_contacts(self):
+        export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file)
 
     def create_menu(self, position):
         menu = QMenu()
@@ -69,6 +65,7 @@ class ContactList(MyTreeWidget):
         if not selected:
             menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog())
             menu.addAction(_("Import file"), lambda: self.import_contacts())
+            menu.addAction(_("Export file"), lambda: self.export_contacts())
         else:
             names = [item.text(0) for item in selected]
             keys = [item.text(1) for item in selected]
diff --git a/gui/qt/invoice_list.py b/gui/qt/invoice_list.py
index a4a8374f..d36c4866 100644
--- a/gui/qt/invoice_list.py
+++ b/gui/qt/invoice_list.py
@@ -25,7 +25,7 @@
 
 from .util import *
 from electrum.i18n import _
-from electrum.util import format_time, FileImportFailed
+from electrum.util import format_time
 
 
 class InvoiceList(MyTreeWidget):
@@ -57,15 +57,10 @@ class InvoiceList(MyTreeWidget):
         self.parent.invoices_label.setVisible(len(inv_list))
 
     def import_invoices(self):
-        wallet_folder = self.parent.get_wallet_folder()
-        filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder)
-        if not filename:
-            return
-        try:
-            self.parent.invoices.import_file(filename)
-        except FileImportFailed as e:
-            self.parent.show_message(str(e))
-        self.on_update()
+        import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.on_update)
+
+    def export_invoices(self):
+        export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file)
 
     def create_menu(self, position):
         menu = QMenu()
diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
index 6b4a1f9b..89c0ce1d 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -61,7 +61,7 @@ from .fee_slider import FeeSlider
 
 from .util import *
 
-from electrum.util import profiler
+from electrum.util import profiler, export_meta, import_meta
 
 class StatusBarButton(QPushButton):
     def __init__(self, icon, tooltip, func):
@@ -484,8 +484,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
         contacts_menu = wallet_menu.addMenu(_("Contacts"))
         contacts_menu.addAction(_("&New"), self.new_contact_dialog)
         contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts())
+        contacts_menu.addAction(_("Export"), lambda: self.contact_list.export_contacts())
         invoices_menu = wallet_menu.addMenu(_("Invoices"))
         invoices_menu.addAction(_("Import"), lambda: self.invoice_list.import_invoices())
+        invoices_menu.addAction(_("Export"), lambda: self.invoice_list.export_invoices())
 
         wallet_menu.addSeparator()
         wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F"))
@@ -2417,29 +2419,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                 f.write(json.dumps(pklist, indent = 4))
 
     def do_import_labels(self):
-        labelsFile = self.getOpenFileName(_("Open labels file"), "*.json")
-        if not labelsFile: return
-        try:
-            with open(labelsFile, 'r') as f:
-                data = f.read()
-            for key, value in json.loads(data).items():
-                self.wallet.set_label(key, value)
-            self.show_message(_("Your labels were imported from") + " '%s'" % str(labelsFile))
-        except (IOError, os.error) as reason:
-            self.show_critical(_("Electrum was unable to import your labels.") + "\n" + str(reason))
-        self.address_list.update()
-        self.history_list.update()
+        def import_labels(path):
+            #TODO: Import labels validation
+            def import_labels_validate(data):
+                return data
+            def import_labels_assign(data):
+                for key, value in data.items():
+                    self.wallet.set_label(key, value)
+            import_meta(path, import_labels_validate, import_labels_assign)
+        def on_import():
+            self.address_list.update()
+            self.history_list.update()
+        import_meta_gui(self, _('labels'), import_labels, on_import)
 
     def do_export_labels(self):
-        labels = self.wallet.labels
-        try:
-            fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.json', "*.json")
-            if fileName:
-                with open(fileName, 'w+') as f:
-                    json.dump(labels, f, indent=4, sort_keys=True)
-                self.show_message(_("Your labels were exported to") + " '%s'" % str(fileName))
-        except (IOError, os.error) as reason:
-            self.show_critical(_("Electrum was unable to export your labels.") + "\n" + str(reason))
+        def export_labels(filename):
+            export_meta(self.wallet.labels, filename)
+        export_meta_gui(self, _('labels'), export_labels)
 
     def sweep_key_dialog(self):
         d = WindowModalDialog(self, title=_('Sweep private keys'))
diff --git a/gui/qt/util.py b/gui/qt/util.py
index 7f0cb238..60a594bb 100644
--- a/gui/qt/util.py
+++ b/gui/qt/util.py
@@ -11,6 +11,7 @@ from PyQt5.QtGui import *
 from PyQt5.QtCore import *
 from PyQt5.QtWidgets import *
 
+from electrum.util import FileImportFailed, FileExportFailed
 if platform.system() == 'Windows':
     MONOSPACE_FONT = 'Lucida Console'
 elif platform.system() == 'Darwin':
@@ -674,6 +675,28 @@ class AcceptFileDragDrop:
     def onFileAdded(self, fn):
         raise NotImplementedError()
 
+def import_meta_gui(electrum_window, title, importer, on_success):
+    filename = electrum_window.getOpenFileName(_("Open {} file").format(title)  , "*.json")
+    if not filename:
+       return
+    try:
+       importer(filename)
+    except FileImportFailed as e:
+       electrum_window.show_critical(str(e))
+    else:
+       electrum_window.show_message(_("Your {} were successfully imported" ).format(title))
+       on_success()
+
+def export_meta_gui(electrum_window, title, exporter):
+    filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title), 'electrum_{}.json'.format(title), "*.json")
+    if  not filename:
+        return
+    try:
+        exporter(filename)
+    except FileExportFailed as e:
+        electrum_window.show_critical(str(e))
+    else:
+        electrum_window.show_message(_("Your {0} were exported to '{1}'").format(title,str(filename)))
 
 if __name__ == "__main__":
     app = QApplication([])
diff --git a/lib/contacts.py b/lib/contacts.py
index 5157adc4..df10e086 100644
--- a/lib/contacts.py
+++ b/lib/contacts.py
@@ -25,10 +25,11 @@ import dns
 import json
 import traceback
 import sys
+import os
 
 from . import bitcoin
 from . import dnssec
-from .util import FileImportFailed, FileImportFailedEncrypted
+from .util import export_meta, import_meta
 
 
 class Contacts(dict):
@@ -51,18 +52,15 @@ class Contacts(dict):
         self.storage.put('contacts', dict(self))
 
     def import_file(self, path):
-        try:
-            with open(path, 'r') as f:
-                d = self._validate(json.loads(f.read()))
-        except json.decoder.JSONDecodeError:
-            traceback.print_exc(file=sys.stderr)
-            raise FileImportFailedEncrypted()
-        except BaseException:
-            traceback.print_exc(file=sys.stdout)
-            raise FileImportFailed()
-        self.update(d)
+        import_meta(path, self.validate, self.load_meta)
+
+    def load_meta(self, data):
+        self.update(data)
         self.save()
 
+    def export_file(self, fileName):
+        export_meta(self, fileName)
+
     def __setitem__(self, key, value):
         dict.__setitem__(self, key, value)
         self.save()
@@ -119,7 +117,7 @@ class Contacts(dict):
         except AttributeError:
             return None
             
-    def _validate(self, data):
+    def validate(self, data):
         for k,v in list(data.items()):
             if k == 'contacts':
                 return self._validate(v)
diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py
index c1e25441..878c541e 100644
--- a/lib/paymentrequest.py
+++ b/lib/paymentrequest.py
@@ -40,7 +40,7 @@ except ImportError:
 from . import bitcoin
 from . import util
 from .util import print_error, bh2u, bfh
-from .util import FileImportFailed, FileImportFailedEncrypted
+from .util import export_meta, import_meta
 from . import transaction
 from . import x509
 from . import rsakey
@@ -468,27 +468,31 @@ class InvoiceStore(object):
                 continue
 
     def import_file(self, path):
-        try:
-            with open(path, 'r') as f:
-                d = json.loads(f.read())
-                self.load(d)
-        except json.decoder.JSONDecodeError:
-            traceback.print_exc(file=sys.stderr)
-            raise FileImportFailedEncrypted()
-        except BaseException:
-            traceback.print_exc(file=sys.stdout)
-            raise FileImportFailed()
+        import_meta(path, self.validate, self.on_import)
+
+    #TODO: Invoice import validation
+    def validate(self, data):
+        return data
+
+    def on_import(self, data):
+        self.load(data)
         self.save()
 
-    def save(self):
-        l = {}
+    def export_file(self, fileName):
+        export_meta(self.before_save(), fileName)
+
+    def before_save(self):
+        l= {}
         for k, pr in self.invoices.items():
             l[k] = {
                 'hex': bh2u(pr.raw),
                 'requestor': pr.requestor,
                 'txid': pr.tx
             }
-        self.storage.put('invoices', l)
+        return l
+
+    def save(self):
+        self.storage.put('invoices', self.before_save())
 
     def get_status(self, key):
         pr = self.get(key)
diff --git a/lib/util.py b/lib/util.py
index 9dc95b91..ee66ff3f 100644
--- a/lib/util.py
+++ b/lib/util.py
@@ -60,16 +60,18 @@ class InvalidPassword(Exception):
 
 
 class FileImportFailed(Exception):
+    def __init__(self, message=''):
+        self.message = str(message)
+
     def __str__(self):
-        return _("Failed to import file.")
+        return _("Failed to import from file.") + "\n" + self.message
 
+class FileExportFailed(Exception):
+    def __init__(self, reason=''):
+        self.message = str(reason)
 
-class FileImportFailedEncrypted(FileImportFailed):
     def __str__(self):
-        return (_('Failed to import file.') + ' ' +
-                _('Perhaps it is encrypted...') + '\n' +
-                _('Importing encrypted files is not supported.'))
-
+        return( _("Failed to export to file.") + "\n" + self.message )
 
 # Throw this exception to unwind the stack like when an error occurs.
 # However unlike other exceptions the user won't be informed.
@@ -785,3 +787,24 @@ def setup_thread_excepthook():
 
 def versiontuple(v):
     return tuple(map(int, (v.split("."))))
+
+def import_meta(path, validater, load_meta):
+    try:
+        with open(path, 'r') as f:
+            d = validater(json.loads(f.read()))
+        load_meta(d)
+    #backwards compatibility for JSONDecodeError
+    except ValueError:
+        traceback.print_exc(file=sys.stderr)
+        raise FileImportFailed(_("Invalid JSON code."))
+    except BaseException as e:
+         traceback.print_exc(file=sys.stdout)
+         raise FileImportFailed(e)
+
+def export_meta(meta, fileName):
+     try:
+         with open(fileName, 'w+') as f:
+            json.dump(meta, f, indent=4, sort_keys=True)
+     except (IOError, os.error) as reason:
+         traceback.print_exc(file=sys.stderr)
+         raise FileExportFailed(str(reason))

From b7b592fd6eb59049896ec644bdfb86cc7bf1685b Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Wed, 21 Feb 2018 19:04:33 +0100
Subject: [PATCH 78/91] fix #3948

---
 lib/exchange_rate.py | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py
index 75979050..1c8c8a95 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -508,9 +508,11 @@ class FxThread(ThreadJob):
         return _("  (No FX rate available)") if rate.is_nan() else " 1 %s~%s %s" % (base_unit,
             self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy)
 
+    def fiat_value(self, satoshis, rate):
+        return Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate)
+
     def value_str(self, satoshis, rate):
-        value = Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate)
-        return self.format_fiat(value)
+        return self.format_fiat(self.fiat_value(satoshis, rate))
 
     def format_fiat(self, value):
         if value.is_nan():
@@ -527,12 +529,10 @@ class FxThread(ThreadJob):
         return Decimal(rate)
 
     def historical_value_str(self, satoshis, d_t):
-        rate = self.history_rate(d_t)
-        return self.value_str(satoshis, rate)
+        return self.format_fiat(self.historical_value(satoshis, d_t))
 
     def historical_value(self, satoshis, d_t):
-        rate = self.history_rate(d_t)
-        return Decimal(satoshis) / COIN * Decimal(rate)
+        return self.fiat_value(satoshis, self.history_rate(d_t))
 
     def timestamp_rate(self, timestamp):
         from electrum.util import timestamp_to_datetime

From 500c0493d062f5fb239601587f9f466212590fc1 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Wed, 21 Feb 2018 18:55:37 +0100
Subject: [PATCH 79/91] clean up prev commit

---
 gui/qt/contact_list.py |  1 -
 gui/qt/invoice_list.py |  3 ++-
 gui/qt/main_window.py  | 19 ++++++++-----------
 gui/qt/util.py         | 32 ++++++++++++++++++++------------
 lib/contacts.py        | 13 ++++++-------
 lib/paymentrequest.py  | 22 ++++++++++------------
 lib/util.py            | 24 ++++++++++++++----------
 7 files changed, 60 insertions(+), 54 deletions(-)

diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py
index 81b6ca86..27c9efb5 100644
--- a/gui/qt/contact_list.py
+++ b/gui/qt/contact_list.py
@@ -23,7 +23,6 @@
 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 import webbrowser
-import os
 
 from electrum.i18n import _
 from electrum.bitcoin import is_address
diff --git a/gui/qt/invoice_list.py b/gui/qt/invoice_list.py
index d36c4866..586dd71c 100644
--- a/gui/qt/invoice_list.py
+++ b/gui/qt/invoice_list.py
@@ -23,10 +23,11 @@
 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-from .util import *
 from electrum.i18n import _
 from electrum.util import format_time
 
+from .util import *
+
 
 class InvoiceList(MyTreeWidget):
     filter_columns = [0, 1, 2, 3]  # Date, Requestor, Description, Amount
diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
index 89c0ce1d..8ab63b87 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -39,15 +39,14 @@ import PyQt5.QtCore as QtCore
 from .exception_window import Exception_Hook
 from PyQt5.QtWidgets import *
 
-from electrum.util import bh2u, bfh
-
 from electrum import keystore, simple_config
 from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS, NetworkConstants
 from electrum.plugins import run_hook
 from electrum.i18n import _
 from electrum.util import (format_time, format_satoshis, PrintError,
                            format_satoshis_plain, NotEnoughFunds,
-                           UserCancelled, NoDynamicFeeEstimates)
+                           UserCancelled, NoDynamicFeeEstimates, profiler,
+                           export_meta, import_meta, bh2u, bfh)
 from electrum import Transaction
 from electrum import util, bitcoin, commands, coinchooser
 from electrum import paymentrequest
@@ -58,10 +57,8 @@ from .qrcodewidget import QRCodeWidget, QRDialog
 from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
 from .transaction_dialog import show_transaction
 from .fee_slider import FeeSlider
-
 from .util import *
 
-from electrum.util import profiler, export_meta, import_meta
 
 class StatusBarButton(QPushButton):
     def __init__(self, icon, tooltip, func):
@@ -2420,16 +2417,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
 
     def do_import_labels(self):
         def import_labels(path):
-            #TODO: Import labels validation
-            def import_labels_validate(data):
-                return data
+            def _validate(data):
+                return data  # TODO
+
             def import_labels_assign(data):
                 for key, value in data.items():
                     self.wallet.set_label(key, value)
-            import_meta(path, import_labels_validate, import_labels_assign)
+            import_meta(path, _validate, import_labels_assign)
+
         def on_import():
-            self.address_list.update()
-            self.history_list.update()
+            self.need_update.set()
         import_meta_gui(self, _('labels'), import_labels, on_import)
 
     def do_export_labels(self):
diff --git a/gui/qt/util.py b/gui/qt/util.py
index 60a594bb..91676216 100644
--- a/gui/qt/util.py
+++ b/gui/qt/util.py
@@ -6,12 +6,15 @@ import queue
 from collections import namedtuple
 from functools import partial
 
-from electrum.i18n import _
 from PyQt5.QtGui import *
 from PyQt5.QtCore import *
 from PyQt5.QtWidgets import *
 
+from electrum.i18n import _
 from electrum.util import FileImportFailed, FileExportFailed
+from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
+
+
 if platform.system() == 'Windows':
     MONOSPACE_FONT = 'Lucida Console'
 elif platform.system() == 'Darwin':
@@ -22,8 +25,6 @@ else:
 
 dialogs = []
 
-from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
-
 pr_icons = {
     PR_UNPAID:":icons/unpaid.png",
     PR_PAID:":icons/confirmed.png",
@@ -675,28 +676,35 @@ class AcceptFileDragDrop:
     def onFileAdded(self, fn):
         raise NotImplementedError()
 
+
 def import_meta_gui(electrum_window, title, importer, on_success):
-    filename = electrum_window.getOpenFileName(_("Open {} file").format(title)  , "*.json")
+    filter_ = "JSON (*.json);;All files (*)"
+    filename = electrum_window.getOpenFileName(_("Open {} file").format(title), filter_)
     if not filename:
-       return
+        return
     try:
-       importer(filename)
+        importer(filename)
     except FileImportFailed as e:
-       electrum_window.show_critical(str(e))
+        electrum_window.show_critical(str(e))
     else:
-       electrum_window.show_message(_("Your {} were successfully imported" ).format(title))
-       on_success()
+        electrum_window.show_message(_("Your {} were successfully imported").format(title))
+        on_success()
+
 
 def export_meta_gui(electrum_window, title, exporter):
-    filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title), 'electrum_{}.json'.format(title), "*.json")
-    if  not filename:
+    filter_ = "JSON (*.json);;All files (*)"
+    filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title),
+                                               'electrum_{}.json'.format(title), filter_)
+    if not filename:
         return
     try:
         exporter(filename)
     except FileExportFailed as e:
         electrum_window.show_critical(str(e))
     else:
-        electrum_window.show_message(_("Your {0} were exported to '{1}'").format(title,str(filename)))
+        electrum_window.show_message(_("Your {0} were exported to '{1}'")
+                                     .format(title, str(filename)))
+
 
 if __name__ == "__main__":
     app = QApplication([])
diff --git a/lib/contacts.py b/lib/contacts.py
index df10e086..0015a861 100644
--- a/lib/contacts.py
+++ b/lib/contacts.py
@@ -25,7 +25,6 @@ import dns
 import json
 import traceback
 import sys
-import os
 
 from . import bitcoin
 from . import dnssec
@@ -52,14 +51,14 @@ class Contacts(dict):
         self.storage.put('contacts', dict(self))
 
     def import_file(self, path):
-        import_meta(path, self.validate, self.load_meta)
+        import_meta(path, self._validate, self.load_meta)
 
     def load_meta(self, data):
         self.update(data)
         self.save()
 
-    def export_file(self, fileName):
-        export_meta(self, fileName)
+    def export_file(self, filename):
+        export_meta(self, filename)
 
     def __setitem__(self, key, value):
         dict.__setitem__(self, key, value)
@@ -117,14 +116,14 @@ class Contacts(dict):
         except AttributeError:
             return None
             
-    def validate(self, data):
-        for k,v in list(data.items()):
+    def _validate(self, data):
+        for k, v in list(data.items()):
             if k == 'contacts':
                 return self._validate(v)
             if not bitcoin.is_address(k):
                 data.pop(k)
             else:
-                _type,_ = v
+                _type, _ = v
                 if _type != 'address':
                     data.pop(k)
         return data
diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py
index 878c541e..47118670 100644
--- a/lib/paymentrequest.py
+++ b/lib/paymentrequest.py
@@ -468,31 +468,29 @@ class InvoiceStore(object):
                 continue
 
     def import_file(self, path):
-        import_meta(path, self.validate, self.on_import)
-
-    #TODO: Invoice import validation
-    def validate(self, data):
-        return data
+        def validate(data):
+            return data  # TODO
+        import_meta(path, validate, self.on_import)
 
     def on_import(self, data):
         self.load(data)
         self.save()
 
-    def export_file(self, fileName):
-        export_meta(self.before_save(), fileName)
+    def export_file(self, filename):
+        export_meta(self.dump(), filename)
 
-    def before_save(self):
-        l= {}
+    def dump(self):
+        d = {}
         for k, pr in self.invoices.items():
-            l[k] = {
+            d[k] = {
                 'hex': bh2u(pr.raw),
                 'requestor': pr.requestor,
                 'txid': pr.tx
             }
-        return l
+        return d
 
     def save(self):
-        self.storage.put('invoices', self.before_save())
+        self.storage.put('invoices', self.dump())
 
     def get_status(self, key):
         pr = self.get(key)
diff --git a/lib/util.py b/lib/util.py
index ee66ff3f..7099141b 100644
--- a/lib/util.py
+++ b/lib/util.py
@@ -66,12 +66,14 @@ class FileImportFailed(Exception):
     def __str__(self):
         return _("Failed to import from file.") + "\n" + self.message
 
+
 class FileExportFailed(Exception):
-    def __init__(self, reason=''):
-        self.message = str(reason)
+    def __init__(self, message=''):
+        self.message = str(message)
 
     def __str__(self):
-        return( _("Failed to export to file.") + "\n" + self.message )
+        return _("Failed to export to file.") + "\n" + self.message
+
 
 # Throw this exception to unwind the stack like when an error occurs.
 # However unlike other exceptions the user won't be informed.
@@ -788,6 +790,7 @@ def setup_thread_excepthook():
 def versiontuple(v):
     return tuple(map(int, (v.split("."))))
 
+
 def import_meta(path, validater, load_meta):
     try:
         with open(path, 'r') as f:
@@ -798,13 +801,14 @@ def import_meta(path, validater, load_meta):
         traceback.print_exc(file=sys.stderr)
         raise FileImportFailed(_("Invalid JSON code."))
     except BaseException as e:
-         traceback.print_exc(file=sys.stdout)
-         raise FileImportFailed(e)
+        traceback.print_exc(file=sys.stdout)
+        raise FileImportFailed(e)
+
 
 def export_meta(meta, fileName):
-     try:
-         with open(fileName, 'w+') as f:
+    try:
+        with open(fileName, 'w+') as f:
             json.dump(meta, f, indent=4, sort_keys=True)
-     except (IOError, os.error) as reason:
-         traceback.print_exc(file=sys.stderr)
-         raise FileExportFailed(str(reason))
+    except (IOError, os.error) as e:
+        traceback.print_exc(file=sys.stderr)
+        raise FileExportFailed(e)

From 99710099fb3566b2c4c1a976e8cf79d431ad56db Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Wed, 21 Feb 2018 20:57:45 +0100
Subject: [PATCH 80/91] fix #3952

---
 lib/wallet.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index 3bfd3c08..e12b401f 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1027,7 +1027,7 @@ class Abstract_Wallet(PrintError):
                     fiat_default = False
                 item['fiat_value'] = Fiat(fiat_value, fx.ccy)
                 item['fiat_default'] = fiat_default
-                if value < 0:
+                if value is not None and value < 0:
                     ap, lp = self.capital_gain(tx_hash, fx.timestamp_rate, fx.ccy)
                     cg = lp - ap
                     item['acquisition_price'] = Fiat(ap, fx.ccy)

From f3440f5a20bee6c64f738ffc32010e94ad8c6dab Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Wed, 21 Feb 2018 21:09:07 +0100
Subject: [PATCH 81/91] fix 3954

---
 lib/wallet.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index e12b401f..67e1493b 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1039,7 +1039,8 @@ class Abstract_Wallet(PrintError):
             out.append(item)
         # add summary
         if out:
-            start_balance = out[0]['balance'].value - out[0]['value'].value
+            b, v = out[0]['balance'].value, out[0]['value'].value
+            start_balance = None if b is None or v is None else b - v
             end_balance = out[-1]['balance'].value
             if from_timestamp is not None and to_timestamp is not None:
                 start_date = timestamp_to_datetime(from_timestamp)

From 1f1844ac13235b39b04d7f7cf935747d25ab40a3 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Thu, 22 Feb 2018 13:08:48 +0100
Subject: [PATCH 82/91] kivy readme: manual download of crystax

---
 gui/kivy/Readme.md | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/gui/kivy/Readme.md b/gui/kivy/Readme.md
index 2c8a55f2..faf8e567 100644
--- a/gui/kivy/Readme.md
+++ b/gui/kivy/Readme.md
@@ -22,7 +22,7 @@ git merge agilewalker/master
 ```
 
 ## 2. Install buildozer
-Buildozer is a frontend to p4a. Luckily we don't need to patch it:
+2.1 Buildozer is a frontend to p4a. Luckily we don't need to patch it:
 
 ```sh
 cd /opt
@@ -31,6 +31,9 @@ cd buildozer
 sudo python3 setup.py install
 ```
 
+2.2 Download the [Crystax NDK](https://www.crystax.net/en/download) manually.
+Extract into `/opt/crystax-ndk-10.3.2`
+
 ## 3. Update the Android SDK build tools
 3.1 Start the Android SDK manager:
 
@@ -40,7 +43,7 @@ sudo python3 setup.py install
 
 3.3 Close the SDK manager.
 
-3.3 Reopen the SDK manager, scroll to the bottom and install the latest build tools (probably v27)
+3.4 Reopen the SDK manager, scroll to the bottom and install the latest build tools (probably v27)
 
 ## 4. Install the Support Library Repository
 Install "Android Support Library Repository" from the SDK manager.

From 0928ac961a33faab0a831f3d79fe9a52fe338a25 Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Thu, 22 Feb 2018 16:33:39 +0100
Subject: [PATCH 83/91] fix #3955: fix interference between verifier and
 catch_up

---
 lib/blockchain.py |  5 +++--
 lib/network.py    |  9 +++++----
 lib/verifier.py   | 10 +++++++---
 3 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/lib/blockchain.py b/lib/blockchain.py
index 8a69276f..d592e584 100644
--- a/lib/blockchain.py
+++ b/lib/blockchain.py
@@ -181,7 +181,8 @@ class Blockchain(util.PrintError):
         if d < 0:
             chunk = chunk[-d:]
             d = 0
-        self.write(chunk, d, index > len(self.checkpoints))
+        truncate = index >= len(self.checkpoints)
+        self.write(chunk, d, truncate)
         self.swap_with_parent()
 
     def swap_with_parent(self):
@@ -338,7 +339,7 @@ class Blockchain(util.PrintError):
             self.save_chunk(idx, data)
             return True
         except BaseException as e:
-            self.print_error('verify_chunk failed', str(e))
+            self.print_error('verify_chunk %d failed'%idx, str(e))
             return False
 
     def get_checkpoints(self):
diff --git a/lib/network.py b/lib/network.py
index bf7d4eb4..08a68489 100644
--- a/lib/network.py
+++ b/lib/network.py
@@ -777,6 +777,7 @@ class Network(util.DaemonThread):
         error = response.get('error')
         result = response.get('result')
         params = response.get('params')
+        blockchain = interface.blockchain
         if result is None or params is None or error is not None:
             interface.print_error(error or 'bad response')
             return
@@ -785,17 +786,17 @@ class Network(util.DaemonThread):
         if index not in self.requested_chunks:
             return
         self.requested_chunks.remove(index)
-        connect = interface.blockchain.connect_chunk(index, result)
+        connect = blockchain.connect_chunk(index, result)
         if not connect:
             self.connection_down(interface.server)
             return
         # If not finished, get the next chunk
-        if interface.blockchain.height() < interface.tip:
+        if index >= len(blockchain.checkpoints) and blockchain.height() < interface.tip:
             self.request_chunk(interface, index+1)
         else:
             interface.mode = 'default'
-            interface.print_error('catch up done', interface.blockchain.height())
-            interface.blockchain.catch_up = None
+            interface.print_error('catch up done', blockchain.height())
+            blockchain.catch_up = None
         self.notify('updated')
 
     def request_header(self, interface, height):
diff --git a/lib/verifier.py b/lib/verifier.py
index 20e83fd2..89694080 100644
--- a/lib/verifier.py
+++ b/lib/verifier.py
@@ -36,15 +36,19 @@ class SPV(ThreadJob):
         self.merkle_roots = {}
 
     def run(self):
+        if not self.network.interface:
+            return
         lh = self.network.get_local_height()
         unverified = self.wallet.get_unverified_txs()
+        blockchain = self.network.blockchain()
         for tx_hash, tx_height in unverified.items():
             # do not request merkle branch before headers are available
             if (tx_height > 0) and (tx_height <= lh):
-                header = self.network.blockchain().read_header(tx_height)
-                if header is None and self.network.interface:
+                header = blockchain.read_header(tx_height)
+                if header is None:
                     index = tx_height // 2016
-                    self.network.request_chunk(self.network.interface, index)
+                    if index < len(blockchain.checkpoints):
+                        self.network.request_chunk(self.network.interface, index)
                 else:
                     if tx_hash not in self.merkle_roots:
                         request = ('blockchain.transaction.get_merkle',

From 02c7524d759b5d65b92b69dc0af78756aaf091cb Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Thu, 22 Feb 2018 16:44:22 +0100
Subject: [PATCH 84/91] logging: some extra network-related lines

---
 lib/blockchain.py |  3 +++
 lib/network.py    |  5 ++++-
 lib/verifier.py   | 16 ++++++++++++----
 3 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/lib/blockchain.py b/lib/blockchain.py
index d592e584..4de3e076 100644
--- a/lib/blockchain.py
+++ b/lib/blockchain.py
@@ -226,6 +226,9 @@ class Blockchain(util.PrintError):
                 if truncate and offset != self._size*80:
                     f.seek(offset)
                     f.truncate()
+                    self.print_error(
+                        'write. truncating to offset {}, which is around chunk {}'
+                        .format(offset, offset//80//2016))
                 f.seek(offset)
                 f.write(data)
                 f.flush()
diff --git a/lib/network.py b/lib/network.py
index 08a68489..28071e41 100644
--- a/lib/network.py
+++ b/lib/network.py
@@ -549,7 +549,7 @@ class Network(util.DaemonThread):
                 self.donation_address = result
         elif method == 'mempool.get_fee_histogram':
             if error is None:
-                self.print_error(result)
+                self.print_error('fee_histogram', result)
                 self.config.mempool_fees = result
                 self.notify('fee_histogram')
         elif method == 'blockchain.estimatefee':
@@ -784,7 +784,10 @@ class Network(util.DaemonThread):
         index = params[0]
         # Ignore unsolicited chunks
         if index not in self.requested_chunks:
+            interface.print_error("received chunk %d (unsolicited)" % index)
             return
+        else:
+            interface.print_error("received chunk %d" % index)
         self.requested_chunks.remove(index)
         connect = blockchain.connect_chunk(index, result)
         if not connect:
diff --git a/lib/verifier.py b/lib/verifier.py
index 89694080..8784b98c 100644
--- a/lib/verifier.py
+++ b/lib/verifier.py
@@ -74,10 +74,18 @@ class SPV(ThreadJob):
         pos = merkle.get('pos')
         merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos)
         header = self.network.blockchain().read_header(tx_height)
-        if not header or header.get('merkle_root') != merkle_root:
-            # FIXME: we should make a fresh connection to a server to
-            # recover from this, as this TX will now never verify
-            self.print_error("merkle verification failed for", tx_hash)
+        # FIXME: if verification fails below,
+        # we should make a fresh connection to a server to
+        # recover from this, as this TX will now never verify
+        if not header:
+            self.print_error(
+                "merkle verification failed for {} (missing header {})"
+                .format(tx_hash, tx_height))
+            return
+        if header.get('merkle_root') != merkle_root:
+            self.print_error(
+                "merkle verification failed for {} (merkle root mismatch {} != {})"
+                .format(tx_hash, header.get('merkle_root'), merkle_root))
             return
         # we passed all the tests
         self.merkle_roots[tx_hash] = merkle_root

From 151aa9d13596ab714cb1ddf712e171faa67fb2a7 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Thu, 22 Feb 2018 16:59:37 +0100
Subject: [PATCH 85/91] fix prev; offset is relative to last forking height

---
 lib/blockchain.py | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/lib/blockchain.py b/lib/blockchain.py
index 4de3e076..d592e584 100644
--- a/lib/blockchain.py
+++ b/lib/blockchain.py
@@ -226,9 +226,6 @@ class Blockchain(util.PrintError):
                 if truncate and offset != self._size*80:
                     f.seek(offset)
                     f.truncate()
-                    self.print_error(
-                        'write. truncating to offset {}, which is around chunk {}'
-                        .format(offset, offset//80//2016))
                 f.seek(offset)
                 f.write(data)
                 f.flush()

From 8329faf760ebafa6d13fa18f43690dd15bb773ed Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Fri, 23 Feb 2018 09:11:25 +0100
Subject: [PATCH 86/91] price_at_timestamp: minor fix

---
 lib/wallet.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index 67e1493b..888e69dd 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1704,7 +1704,7 @@ class Abstract_Wallet(PrintError):
 
     def price_at_timestamp(self, txid, price_func):
         height, conf, timestamp = self.get_tx_height(txid)
-        return price_func(timestamp)
+        return price_func(timestamp if timestamp else time.time())
 
     def unrealized_gains(self, domain, price_func, ccy):
         coins = self.get_utxos(domain)

From d38a50b119aa15a2be5cba504eb5d7c0a144d56b Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Fri, 23 Feb 2018 09:35:07 +0100
Subject: [PATCH 87/91] fix #3922: wrong parameter passed to connection_down

---
 lib/network.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/network.py b/lib/network.py
index 28071e41..92edda95 100644
--- a/lib/network.py
+++ b/lib/network.py
@@ -991,7 +991,7 @@ class Network(util.DaemonThread):
         if not height:
             return
         if height < self.max_checkpoint():
-            self.connection_down(interface)
+            self.connection_down(interface.server)
             return
         interface.tip_header = header
         interface.tip = height

From aaf89d2325ce114fab152b17d6cacbe150938e89 Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Fri, 23 Feb 2018 11:30:59 +0100
Subject: [PATCH 88/91] fix #3858

---
 lib/verifier.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/lib/verifier.py b/lib/verifier.py
index 8784b98c..c2d0f125 100644
--- a/lib/verifier.py
+++ b/lib/verifier.py
@@ -36,11 +36,14 @@ class SPV(ThreadJob):
         self.merkle_roots = {}
 
     def run(self):
-        if not self.network.interface:
+        interface = self.network.interface
+        if not interface:
+            return
+        blockchain = interface.blockchain
+        if not blockchain:
             return
         lh = self.network.get_local_height()
         unverified = self.wallet.get_unverified_txs()
-        blockchain = self.network.blockchain()
         for tx_hash, tx_height in unverified.items():
             # do not request merkle branch before headers are available
             if (tx_height > 0) and (tx_height <= lh):
@@ -48,7 +51,7 @@ class SPV(ThreadJob):
                 if header is None:
                     index = tx_height // 2016
                     if index < len(blockchain.checkpoints):
-                        self.network.request_chunk(self.network.interface, index)
+                        self.network.request_chunk(interface, index)
                 else:
                     if tx_hash not in self.merkle_roots:
                         request = ('blockchain.transaction.get_merkle',

From 2ee010a4432e7a11c97caed8b9fd2a0fac438850 Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Fri, 23 Feb 2018 11:57:59 +0100
Subject: [PATCH 89/91] add issue template

---
 .github/ISSUE_TEMPLATE.md | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100644 .github/ISSUE_TEMPLATE.md

diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000..8dabcca2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,4 @@
+

From c559077007237300a4c4509636631714f766a7e4 Mon Sep 17 00:00:00 2001
From: ThomasV 
Date: Fri, 23 Feb 2018 12:01:01 +0100
Subject: [PATCH 90/91] follow-up prev commit: use less space

---
 .github/ISSUE_TEMPLATE.md | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 8dabcca2..ef05ebc2 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,4 +1,2 @@
-
+

From 4aff20273ac16f26dc663265c8405453ff61fa4a Mon Sep 17 00:00:00 2001
From: David Cooper 
Date: Fri, 23 Feb 2018 05:38:24 -0600
Subject: [PATCH 91/91] Add Support for Python 3.6+ in ./electrum-env

---
 electrum-env | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/electrum-env b/electrum-env
index 42220eda..c05b2d1a 100755
--- a/electrum-env
+++ b/electrum-env
@@ -9,6 +9,8 @@
 # python-qt and its dependencies will still need to be installed with
 # your package manager.
 
+PYTHON_VER="$(python3 -c 'import sys; print(sys.version[:3])')"
+
 if [ -e ./env/bin/activate ]; then
     source ./env/bin/activate
 else
@@ -17,7 +19,7 @@ else
     python3 setup.py install
 fi
 
-export PYTHONPATH="/usr/local/lib/python3.5/site-packages:$PYTHONPATH"
+export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH"
 
 ./electrum "$@"