From c44427d33e5dddcb90292067e648535e567a6374 Mon Sep 17 00:00:00 2001 From: lillypad Date: Sun, 17 Dec 2017 15:22:10 -0400 Subject: [PATCH 001/270] 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 000000000000..227ec1cd90d7 --- /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 9f19e576dd00..5a7a3f72a00a 100755 --- a/setup.py +++ b/setup.py @@ -9,6 +9,9 @@ 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 @@ 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 002/270] 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 26ccf6aba83f..e55788b7b194 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2030,7 +2030,12 @@ def do_sign(self, address, message, signature, password): 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 @@ def sign_verify_message(self, address=''): 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 f1f2d0bd20b1..082ea5967613 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -406,11 +406,15 @@ def update_headers(self, headers): 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 70331251861badd17f0544bb97a77d75c39e9db7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Jan 2018 18:08:10 +0100 Subject: [PATCH 003/270] fee ui - rounding: display info icon with tooltip. show pre-rounding values in ui. --- gui/qt/fee_slider.py | 6 +++ gui/qt/main_window.py | 86 ++++++++++++++++++++++++++++++++++-------- icons.qrc | 1 + icons/info.png | Bin 0 -> 1771 bytes lib/coinchooser.py | 2 + lib/simple_config.py | 18 ++++++++- lib/wallet.py | 1 + 7 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 icons/info.png diff --git a/gui/qt/fee_slider.py b/gui/qt/fee_slider.py index c8b6637fa573..3d93849254e3 100644 --- a/gui/qt/fee_slider.py +++ b/gui/qt/fee_slider.py @@ -18,6 +18,7 @@ def __init__(self, window, config, callback): self.lock = threading.RLock() self.update() self.valueChanged.connect(self.moved) + self._active = True def moved(self, pos): with self.lock: @@ -56,9 +57,11 @@ def update(self): self.setToolTip(tooltip) def activate(self): + self._active = True self.setStyleSheet('') def deactivate(self): + self._active = False # TODO it would be nice to find a platform-independent solution # that makes the slider look as if it was disabled self.setStyleSheet( @@ -79,3 +82,6 @@ def deactivate(self): } """ ) + + def is_active(self): + return self._active diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index ea2fe96d21a9..b6c098559a3c 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -619,7 +619,7 @@ def format_amount(self, x, is_diff=False, whitespaces=False): def format_amount_and_units(self, amount): text = self.format_amount(amount) + ' '+ self.base_unit() - x = self.fx.format_amount_and_units(amount) + x = self.fx.format_amount_and_units(amount) if self.fx else None if text and x: text += ' (%s)'%x return text @@ -1070,6 +1070,8 @@ def fee_cb(dyn, pos, fee_rate): if fee_rate: self.feerate_e.setAmount(fee_rate // 1000) + else: + self.feerate_e.setAmount(None) self.fee_e.setModified(False) self.fee_slider.activate() @@ -1103,6 +1105,7 @@ def setAmount(self, byte_size): self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) self.feerate_e = FeerateEdit(lambda: 2 if self.fee_unit else 0) + self.feerate_e.setAmount(self.config.fee_per_byte()) self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False)) self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True)) @@ -1110,6 +1113,18 @@ def setAmount(self, byte_size): self.fee_e.textEdited.connect(partial(on_fee_or_feerate, self.fee_e, False)) self.fee_e.editingFinished.connect(partial(on_fee_or_feerate, self.fee_e, True)) + def feerounding_onclick(): + text = (_('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' + + _('Also, dust is not kept as change, but added to the fee.')) + QMessageBox.information(self, 'Fee rounding', text) + + self.feerounding_icon = QPushButton(QIcon(':icons/info.png'), '') + self.feerounding_icon.setFixedWidth(20) + self.feerounding_icon.setFlat(True) + self.feerounding_icon.clicked.connect(feerounding_onclick) + self.feerounding_icon.setVisible(False) + self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e) vbox_feelabel = QVBoxLayout() @@ -1123,12 +1138,14 @@ def setAmount(self, byte_size): hbox.addWidget(self.feerate_e) hbox.addWidget(self.size_e) hbox.addWidget(self.fee_e) + hbox.addWidget(self.feerounding_icon, Qt.AlignLeft) + hbox.addStretch(1) vbox_feecontrol = QVBoxLayout() vbox_feecontrol.addWidget(self.fee_adv_controls) vbox_feecontrol.addWidget(self.fee_slider) - grid.addLayout(vbox_feecontrol, 5, 1, 1, 3) + grid.addLayout(vbox_feecontrol, 5, 1, 1, -1) if not self.config.get('show_fee', False): self.fee_adv_controls.setVisible(False) @@ -1252,15 +1269,22 @@ def do_update_fee(self): try: tx = make_tx(fee_estimator) self.not_enough_funds = False - except NotEnoughFunds: - self.not_enough_funds = True + except (NotEnoughFunds, NoDynamicFeeEstimates) as e: if not freeze_fee: self.fee_e.setAmount(None) - return - except NoDynamicFeeEstimates: - tx = make_tx(0) - size = tx.estimated_size() - self.size_e.setAmount(size) + if not freeze_feerate: + self.feerate_e.setAmount(None) + self.feerounding_icon.setVisible(False) + + if isinstance(e, NotEnoughFunds): + self.not_enough_funds = True + elif isinstance(e, NoDynamicFeeEstimates): + try: + tx = make_tx(0) + size = tx.estimated_size() + self.size_e.setAmount(size) + except BaseException: + pass return except BaseException: traceback.print_exc(file=sys.stderr) @@ -1270,12 +1294,35 @@ def do_update_fee(self): self.size_e.setAmount(size) fee = tx.get_fee() - if not freeze_fee: - fee = None if self.not_enough_funds else fee - self.fee_e.setAmount(fee) - if not freeze_feerate: - fee_rate = fee // size if fee is not None else None - self.feerate_e.setAmount(fee_rate) + fee = None if self.not_enough_funds else fee + + # Displayed fee/fee_rate values are set according to user input. + # Due to rounding or dropping dust in CoinChooser, + # actual fees often differ somewhat. + if freeze_feerate or self.fee_slider.is_active(): + displayed_feerate = self.feerate_e.get_amount() + displayed_feerate = displayed_feerate // 1000 if displayed_feerate else 0 + displayed_fee = displayed_feerate * size + self.fee_e.setAmount(displayed_fee) + else: + if freeze_fee: + displayed_fee = self.fee_e.get_amount() + else: + # fallback to actual fee if nothing is frozen + displayed_fee = fee + self.fee_e.setAmount(displayed_fee) + displayed_fee = displayed_fee if displayed_fee else 0 + displayed_feerate = displayed_fee // size if displayed_fee is not None else None + self.feerate_e.setAmount(displayed_feerate) + + # show/hide fee rounding icon + feerounding = (fee - displayed_fee) if fee else 0 + if feerounding: + self.feerounding_icon.setToolTip( + _('additional {} satoshis will be added').format(feerounding)) + self.feerounding_icon.setVisible(True) + else: + self.feerounding_icon.setVisible(False) if self.is_max: amount = tx.output_value() @@ -1354,7 +1401,7 @@ def get_send_fee_estimator(self): fee_estimator = self.fee_e.get_amount() elif self.is_send_feerate_frozen(): amount = self.feerate_e.get_amount() - amount = 0 if amount is None else float(amount) + amount = 0 if amount is None else amount fee_estimator = partial( simple_config.SimpleConfig.estimate_fee_for_feerate, amount) else: @@ -1440,6 +1487,10 @@ def do_send(self, preview = False): self.show_transaction(tx, tx_desc) return + if not self.network: + self.show_error(_("You can't broadcast a transaction without a live network connection.")) + return + # confirmation dialog msg = [ _("Amount to be sent") + ": " + self.format_amount_and_units(amount), @@ -1640,7 +1691,9 @@ def do_clear(self): e.setText('') e.setFrozen(False) self.fee_slider.activate() + self.feerate_e.setAmount(self.config.fee_per_byte()) self.size_e.setAmount(0) + self.feerounding_icon.setVisible(False) self.set_pay_from([]) self.tx_external_keypairs = {} self.update_status() @@ -3009,6 +3062,7 @@ def cpfp(self, parent_tx, new_tx): grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0) grid.addWidget(output_amount, 2, 1) fee_e = BTCAmountEdit(self.get_decimal_point) + # FIXME with dyn fees, without estimates, there are all kinds of crashes here def f(x): a = max_fee - fee_e.get_amount() output_amount.setText((self.format_amount(a) + ' ' + self.base_unit()) if a else '') diff --git a/icons.qrc b/icons.qrc index e1007e58ee9d..92e19a1a13e8 100644 --- a/icons.qrc +++ b/icons.qrc @@ -14,6 +14,7 @@ icons/electrum_light_icon.png icons/electrum_dark_icon.png icons/file.png + icons/info.png icons/keepkey.png icons/keepkey_unpaired.png icons/key.png diff --git a/icons/info.png b/icons/info.png new file mode 100644 index 0000000000000000000000000000000000000000..f11f99694aae1c743899495533965d61c94a73f5 GIT binary patch literal 1771 zcmV;P)PuVoCRJ^rMeGA4;gSl{3nYP-%Mu8& zr`eMk%rF^RoC#M@|pZ|9piGv9yCnQy*xW`Jdau(Gk;Z=Py`G(#Y?NFmw) zu>yd1wm1&O5mp#h3YR2_6JP8P{@bd)%NFvhcX~H6@Pd%^9Gj|{NSIYFSJqqQbU199 z&@>4E$QmZ}tfAtGbS9BX*_f3iqRY(uJ|O>j_`tUBmtKfcVs}sflalB)X5Q-cxH8R+ z0Y_kk7d1W)#3Icqr7#(vMmQSBmFwerJejr=(P?J9et7@3&mn}m??Uz*>f1~Z|6s-= ztLkgHt+`Rw`aGpJGlys_fsrdW&Ff=PA}P)o%6Rdsmw$P-ls1YMva9=;=5RUR6iU3b z((g6f*F9*fayv`+dI1v26wVD@$&N?jk_CFIS6}_(2hTrm7ISY=LOKuj*E?+DQz5Wr z^Muv~cY_YqX zc5A#{gk(B{lY;1F;5+Zmg*8J~DP?z(O5Efo_103-yqx3_9ukE`Bp6CdyDKBg6{SIAg5 zWxOSb9tpO$+O;KqoeH5z!jAS9JCWGxbh_RsRA=5$KYr-MCdqPO+ruris;<1~WS79& z+L-$R6*%>p}{1@=8e zu*h%jO8#`%dp3#cE6Yx(96Kbvg4Se2zQUJ_oR-w*0yF@VaK3 zX{sH2dwM(b?Q?_>$?m3xfbzJ?U$G0t2P;BGmD`D?20s(zpYrXug*@?A|9X=-*xFoQ z<_j>x#R-L2=FW92Gc!-DpzXm1X(|rw?K!?KXS*HBOo`vrcwD-#%5BNgt((Ay=h@Q9 zp2Jt=hR@^DrxNL31Ly+so`_9|XPX)VmgXP237iiroDW+kB1coB->ymVZ0?!>;PIa0 zs|@DVb$*|IZR`_5tw{#xtuLov@kkUFNkx}h+SJt}{sPTG0Bv{(G1DKs7Lh$-3 zk0&c23NHVp5^53%xaP;Gp@_mz#F_~&gg}+cnT<`S)&uxV0{{hT^}1b-MeZ${ND>IR zfJX0J`)II`_BZ z=Y%FT5JU}dC;V0}PqzpH0L5z3Ff0!AOMqb-P)bFCSs@HWGP=Q)b*pqTS<_^t#(~_c z#cC*)NM;ti>wl1VEU7DqVIU_&hzrxHj7{PGM;fb8N+FR>+d+H*m=$8MIBqJIld+8!aIOW}!lWxXqb@MIh|A%_nCAv6KxWxs6jdslCUQ$;x$E0Plv3};5-C?Kk*thoB@jy_5lg0=On5hEyDj8MSLgx}^^IJMii(}!bfzk0O{@Z_B zktCp-h1aBb8O!eIttiGLF-hRx-Cb62cy{;nAF@mOeP?jvl5Ha_Mk<}b*L~mVS%d$4 zobk0$en zl{-{(5BB_}cZJZ=!Jv&AVS|DQAn%xyfC?Lo?+N^^vg35_qv-$S@gGKx0F)dbM&JMd N002ovPDHLkV1lvGV4na0 literal 0 HcmV?d00001 diff --git a/lib/coinchooser.py b/lib/coinchooser.py index c4004c870d39..a29aa7326bec 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -273,6 +273,8 @@ def bucket_candidates_any(self, buckets, sufficient_funds): candidates.add(tuple(sorted(permutation[:count + 1]))) break else: + # FIXME this assumes that the effective value of any bkt is >= 0 + # we should make sure not to choose buckets with <= 0 eff. val. raise NotEnoughFunds() candidates = [[buckets[n] for n in c] for c in candidates] diff --git a/lib/simple_config.py b/lib/simple_config.py index b3da52deeb66..33f426bdb13a 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -5,7 +5,8 @@ import stat from copy import deepcopy -from .util import user_dir, print_error, print_stderr, PrintError +from .util import (user_dir, print_error, print_stderr, PrintError, + NoDynamicFeeEstimates) from .bitcoin import MAX_FEE_RATE, FEE_TARGETS @@ -245,6 +246,9 @@ def is_dynfee(self): return self.get('dynamic_fees', True) def fee_per_kb(self): + """Returns sat/kvB fee to pay for a txn. + Note: might return None. + """ dyn = self.is_dynfee() if dyn: fee_rate = self.dynfee(self.get('fee_level', 2)) @@ -252,8 +256,18 @@ def fee_per_kb(self): fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2) return fee_rate + def fee_per_byte(self): + """Returns sat/vB fee to pay for a txn. + Note: might return None. + """ + fee_per_kb = self.fee_per_kb() + return fee_per_kb / 1000 if fee_per_kb is not None else None + def estimate_fee(self, size): - return self.estimate_fee_for_feerate(self.fee_per_kb(), size) + fee_per_kb = self.fee_per_kb() + if fee_per_kb is None: + raise NoDynamicFeeEstimates() + return self.estimate_fee_for_feerate(fee_per_kb, size) @classmethod def estimate_fee_for_feerate(cls, fee_per_kb, size): diff --git a/lib/wallet.py b/lib/wallet.py index 3a3ed8f89174..a9dc646bcb40 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -923,6 +923,7 @@ def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None, tx = coin_chooser.make_tx(inputs, outputs, change_addrs[:max_change], fee_estimator, self.dust_threshold()) else: + # FIXME?? this might spend inputs with negative effective value... sendable = sum(map(lambda x:x['value'], inputs)) _type, data, value = outputs[i_max] outputs[i_max] = (_type, data, 0) From a58d01ed54320e2e8eb2a495a8d9b48d8d4c4949 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 13 Jan 2018 22:43:57 +0100 Subject: [PATCH 004/270] setconfig rpcpassword: don't try to evaluate or reencode --- electrum | 3 ++- lib/commands.py | 8 +++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/electrum b/electrum index d260952a60a0..9a9f41855dbf 100755 --- a/electrum +++ b/electrum @@ -278,7 +278,8 @@ def run_offline_command(config, config_options): # arguments passed to function args = [config.get(x) for x in cmd.params] # decode json arguments - args = list(map(json_decode, args)) + if cmdname not in ('setconfig',): + args = list(map(json_decode, args)) # options kwargs = {} for x in cmd.options: diff --git a/lib/commands.py b/lib/commands.py index bbca25f13761..ff13ae682c26 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -34,7 +34,7 @@ from decimal import Decimal from .import util -from .util import bfh, bh2u, format_satoshis +from .util import bfh, bh2u, format_satoshis, json_decode from .import bitcoin from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS from .i18n import _ @@ -151,10 +151,8 @@ def getconfig(self, key): @command('') def setconfig(self, key, value): """Set a configuration variable. 'value' may be a string or a Python expression.""" - try: - value = ast.literal_eval(value) - except: - pass + if key not in ('rpcuser', 'rpcpassword'): + value = json_decode(value) self.config.set_key(key, value) return True From 7d52cfd37447dbabd2b99615ffbd2f35389d4140 Mon Sep 17 00:00:00 2001 From: racquemis Date: Sun, 14 Jan 2018 08:42:38 +0100 Subject: [PATCH 005/270] Handle invalid PIN on exporting private key Prevent Android App from crashing when a wrong PIN is entered. --- gui/kivy/main_window.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index d9b4eac4a1d7..621beaf163d3 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -928,6 +928,10 @@ def show_private_key(addr, pk_label, password): return if not self.wallet.can_export(): return - key = str(self.wallet.export_private_key(addr, password)[0]) - pk_label.data = key + try: + key = str(self.wallet.export_private_key(addr, password)[0]) + pk_label.data = key + except InvalidPassword: + self.show_error("Invalid PIN") + return self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label)) From 21e44bdb57c3d60db91c877931df7847f272503a Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Mon, 15 Jan 2018 18:38:30 +0100 Subject: [PATCH 006/270] Update kivy docs to current build process --- gui/kivy/Readme.md | 80 +++++++++++++++++++++++++++++++++++++++++++++ gui/kivy/Readme.txt | 24 -------------- 2 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 gui/kivy/Readme.md delete mode 100644 gui/kivy/Readme.txt diff --git a/gui/kivy/Readme.md b/gui/kivy/Readme.md new file mode 100644 index 000000000000..6b880b8677cc --- /dev/null +++ b/gui/kivy/Readme.md @@ -0,0 +1,80 @@ +# Kivy GUI + +The Kivy GUI is used with Electrum on Android devices. To generate an APK file, follow these instructions. + +## 1. Install python-for-android (p4a) +p4a is used to package Electrum, Python, SDL and a bootstrap Java app into an APK file. +We patched p4a to add some functionality we need for Electrum. Until those changes are +merged into p4a, you need to merge them locally (into the stable branch): + +1. [kivy/python-for-android#1213](https://github.com/kivy/python-for-android/pull/1213) +2. [kivy/python-for-android#1217](https://github.com/kivy/python-for-android/pull/1217) + +Something like this should work: + +```sh +cd /opt +git clone https://github.com/kivy/python-for-android +cd python-for-android +git remote add agilewalker https://github.com/agilewalker/python-for-android +git remote add bauerj https://github.com/bauerj/python-for-android +git checkout stable +git fetch agilewalker +git merge agilewalker/master +git fetch bauerj +git merge bauerj/add-activity +``` + +## 2. Install buildozer +Buildozer is a frontend to p4a. Of course it needs to be patched too: + +1. [kivy/buildozer#612](https://github.com/kivy/python-for-android/pull/1213) + +```sh +cd /opt +git clone https://github.com/kivy/buildozer +cd buildozer +git remote add bauerj https://github.com/bauerj/buildozer +git fetch bauerj +git merge bauerj/add-activity +``` + +You also want to install it: + +```sh +sudo python3 setup.py install +``` + +## 3. Update the Android SDK build tools +3.1 Start the Android SDK manager: + + ~/.buildozer/android/platform/android-sdk-20/tools/android + +3.2 Check the latest SDK available and install it. + +3.3 Close the SDK manager. + +3.3 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. + +## 5. Create the UI Atlas +In the `gui/kivy` directory of Electrum, run `make theming`. + +## 6. Download Electrum dependencies +Run `contrib/make_packages`. + +## 7. Build the APK +Run `contrib/make_apk`. + +# FAQ +## Why do I get errors like `package me.dm7.barcodescanner.zxing does not exist` while compiling? +Update your Android build tools to version 27 like described above. + +## Why do I get errors like `(use -source 7 or higher to enable multi-catch statement)` while compiling? +Use the `stable` branch of python-for-android. +[This commit](https://github.com/kivy/python-for-android/commit/3534a761b17040755accf941f898cc66b905e8db) in master is the culprit. + +## I changed something but I don't see any differences on the phone. What did I do wrong? +You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}` diff --git a/gui/kivy/Readme.txt b/gui/kivy/Readme.txt deleted file mode 100644 index ce85523ccc82..000000000000 --- a/gui/kivy/Readme.txt +++ /dev/null @@ -1,24 +0,0 @@ -Before compiling, create packages: `contrib/make_packages` - -Commands:: - - `make theming` to make a atlas out of a list of pngs - - `make apk` to make a apk - - -If something in included modules like kivy or any other module changes -then you need to rebuild the distribution. To do so: - - rm -rf .buildozer/android/platform/python-for-android/dist - - -how to build with ssl: - - rm -rf .buildozer/android/platform/build/ - ./contrib/make_apk - pushd /opt/electrum/.buildozer/android/platform/build/build/libs_collections/Electrum/armeabi-v7a - cp libssl1.0.2g.so /opt/crystax-ndk-10.3.2/sources/openssl/1.0.2g/libs/armeabi-v7a/libssl.so - cp libcrypto1.0.2g.so /opt/crystax-ndk-10.3.2/sources/openssl/1.0.2g/libs/armeabi-v7a/libcrypto.so - popd - ./contrib/make_apk From 04a1809969a24f7fc176aa505b7fed807e55ad4e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 16 Jan 2018 10:31:06 +0100 Subject: [PATCH 007/270] config: implement config upgrades. remove system config. --- lib/simple_config.py | 142 +++++++++++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 52 deletions(-) diff --git a/lib/simple_config.py b/lib/simple_config.py index 33f426bdb13a..71818e7f570a 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -5,13 +5,11 @@ import stat from copy import deepcopy -from .util import (user_dir, print_error, print_stderr, PrintError, +from .util import (user_dir, print_error, PrintError, NoDynamicFeeEstimates) from .bitcoin import MAX_FEE_RATE, FEE_TARGETS -SYSTEM_CONFIG_PATH = "/etc/electrum.conf" - config = None @@ -25,22 +23,26 @@ def set_config(c): config = c +FINAL_CONFIG_VERSION = 2 + + class SimpleConfig(PrintError): """ The SimpleConfig class is responsible for handling operations involving configuration files. - There are 3 different sources of possible configuration values: + There are two different sources of possible configuration values: 1. Command line options. 2. User configuration (in the user's config directory) - 3. System configuration (in /etc/) - They are taken in order (1. overrides config options set in 2., that - override config set in 3.) + 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={}, read_system_config_function=None, - read_user_config_function=None, read_user_dir_function=None): + def __init__(self, options=None, read_user_config_function=None, + read_user_dir_function=None): + + if options is None: + options = {} # This lock needs to be acquired for updating and reading the config in # a thread-safe way. @@ -52,8 +54,6 @@ def __init__(self, options={}, read_system_config_function=None, # The following two functions are there for dependency injection when # testing. - if read_system_config_function is None: - read_system_config_function = read_system_config if read_user_config_function is None: read_user_config_function = read_user_config if read_user_dir_function is None: @@ -63,24 +63,30 @@ def __init__(self, options={}, read_system_config_function=None, # The command line options self.cmdline_options = deepcopy(options) - - # Portable wallets don't use a system config - if self.cmdline_options.get('portable', False): - self.system_config = {} - else: - self.system_config = read_system_config_function() + # don't allow to be set on CLI: + self.cmdline_options.pop('config_version', None) # Set self.path and read the user config self.user_config = {} # for self.get in electrum_path() self.path = self.electrum_path() self.user_config = read_user_config_function(self.path) - # Upgrade obsolete keys - self.fixup_keys({'auto_cycle': 'auto_connect'}) + if not self.user_config: + # avoid new config getting upgraded + self.user_config = {'config_version': FINAL_CONFIG_VERSION} + + # config "upgrade" - CLI options + self.rename_config_keys( + self.cmdline_options, {'auto_cycle': 'auto_connect'}, True) + + # config upgrade - user config + if self.requires_upgrade(): + self.upgrade() + # Make a singleton instance of 'self' set_config(self) def electrum_path(self): - # Read electrum_path from command line / system configuration + # Read electrum_path from command line # Otherwise use the user's default data directory. path = self.get('electrum_path') if path is None: @@ -102,45 +108,92 @@ def make_dir(path): self.print_error("electrum directory", path) return path - def fixup_config_keys(self, config, keypairs): + def rename_config_keys(self, config, keypairs, deprecation_warning=False): + """Migrate old key names to new ones""" updated = False for old_key, new_key in keypairs.items(): if old_key in config: - if not new_key in config: + if new_key not in config: config[new_key] = config[old_key] + if deprecation_warning: + self.print_stderr('Note that the {} variable has been deprecated. ' + 'You should use {} instead.'.format(old_key, new_key)) del config[old_key] updated = True return updated - def fixup_keys(self, keypairs): - '''Migrate old key names to new ones''' - self.fixup_config_keys(self.cmdline_options, keypairs) - self.fixup_config_keys(self.system_config, keypairs) - if self.fixup_config_keys(self.user_config, keypairs): - self.save_user_config() - - def set_key(self, key, value, save = True): + def set_key(self, key, value, save=True): if not self.is_modifiable(key): - print_stderr("Warning: not changing config key '%s' set on the command line" % key) + self.print_stderr("Warning: not changing config key '%s' set on the command line" % key) return + self._set_key_in_user_config(key, value, save) + def _set_key_in_user_config(self, key, value, save=True): with self.lock: - self.user_config[key] = value + if value is not None: + self.user_config[key] = value + else: + self.user_config.pop(key, None) if save: self.save_user_config() - return def get(self, key, default=None): with self.lock: out = self.cmdline_options.get(key) if out is None: - out = self.user_config.get(key) - if out is None: - out = self.system_config.get(key, default) + out = self.user_config.get(key, default) return out + def requires_upgrade(self): + return self.get_config_version() < FINAL_CONFIG_VERSION + + def upgrade(self): + with self.lock: + self.print_error('upgrading config') + + self.convert_version_2() + + self.set_key('config_version', FINAL_CONFIG_VERSION, save=True) + + def convert_version_2(self): + if not self._is_upgrade_method_needed(1, 1): + return + + self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'}) + + try: + # migrate server string FROM host:port:proto TO host:port + server_str = self.user_config.get('server') + host, port, protocol = str(server_str).rsplit(':', 2) + assert protocol in ('s', 't') + int(port) # Throw if cannot be converted to int + server_str = str('{}:{}'.format(host, port)) + self._set_key_in_user_config('server', server_str) + except BaseException: + self._set_key_in_user_config('server', None) + + self.set_key('config_version', 2) + + def _is_upgrade_method_needed(self, min_version, max_version): + cur_version = self.get_config_version() + if cur_version > max_version: + return False + elif cur_version < min_version: + raise BaseException( + ('config upgrade: unexpected version %d (should be %d-%d)' + % (cur_version, min_version, max_version))) + else: + return True + + def get_config_version(self): + config_version = self.get('config_version', 1) + if config_version > FINAL_CONFIG_VERSION: + self.print_stderr('WARNING: config version ({}) is higher than ours ({})' + .format(config_version, FINAL_CONFIG_VERSION)) + return config_version + def is_modifiable(self, key): - return not key in self.cmdline_options + return key not in self.cmdline_options def save_user_config(self): if not self.path: @@ -298,21 +351,6 @@ def get_video_device(self): return device -def read_system_config(path=SYSTEM_CONFIG_PATH): - """Parse and return the system config settings in /etc/electrum.conf.""" - result = {} - if os.path.exists(path): - import configparser - p = configparser.ConfigParser() - try: - p.read(path) - for k, v in p.items('client'): - result[k] = v - except (configparser.NoSectionError, configparser.MissingSectionHeaderError): - pass - - return result - def read_user_config(path): """Parse and store the user config settings in electrum.conf into user_config[].""" if not path: From 0d1ea09ed10a5796c71f59cc44d0c2a92f27ee54 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 16 Jan 2018 10:52:41 +0100 Subject: [PATCH 008/270] fix tests --- lib/tests/test_simple_config.py | 107 +------------------------------- 1 file changed, 3 insertions(+), 104 deletions(-) diff --git a/lib/tests/test_simple_config.py b/lib/tests/test_simple_config.py index c9c648dc89f4..ad7fca2b9d55 100644 --- a/lib/tests/test_simple_config.py +++ b/lib/tests/test_simple_config.py @@ -6,8 +6,7 @@ import shutil from io import StringIO -from lib.simple_config import (SimpleConfig, read_system_config, - read_user_config) +from lib.simple_config import (SimpleConfig, read_user_config) class Test_SimpleConfig(unittest.TestCase): @@ -37,18 +36,15 @@ def tearDown(self): def test_simple_config_key_rename(self): """auto_cycle was renamed auto_connect""" - fake_read_system = lambda : {} fake_read_user = lambda _: {"auto_cycle": True} read_user_dir = lambda : self.user_dir config = SimpleConfig(options=self.options, - read_system_config_function=fake_read_system, read_user_config_function=fake_read_user, read_user_dir_function=read_user_dir) self.assertEqual(config.get("auto_connect"), True) self.assertEqual(config.get("auto_cycle"), None) fake_read_user = lambda _: {"auto_connect": False, "auto_cycle": True} config = SimpleConfig(options=self.options, - read_system_config_function=fake_read_system, read_user_config_function=fake_read_user, read_user_dir_function=read_user_dir) self.assertEqual(config.get("auto_connect"), False) @@ -57,110 +53,51 @@ def test_simple_config_key_rename(self): def test_simple_config_command_line_overrides_everything(self): """Options passed by command line override all other configuration sources""" - fake_read_system = lambda : {"electrum_path": "a"} fake_read_user = lambda _: {"electrum_path": "b"} read_user_dir = lambda : self.user_dir config = SimpleConfig(options=self.options, - read_system_config_function=fake_read_system, read_user_config_function=fake_read_user, read_user_dir_function=read_user_dir) self.assertEqual(self.options.get("electrum_path"), config.get("electrum_path")) - def test_simple_config_user_config_overrides_system_config(self): - """Options passed in user config override system config.""" - fake_read_system = lambda : {"electrum_path": self.electrum_dir} - fake_read_user = lambda _: {"electrum_path": "b"} - read_user_dir = lambda : self.user_dir - config = SimpleConfig(options={}, - read_system_config_function=fake_read_system, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - self.assertEqual("b", config.get("electrum_path")) - - def test_simple_config_system_config_ignored_if_portable(self): - """If electrum is started with the "portable" flag, system - configuration is completely ignored.""" - fake_read_system = lambda : {"some_key": "some_value"} - fake_read_user = lambda _: {} - read_user_dir = lambda : self.user_dir - config = SimpleConfig(options={"portable": True}, - read_system_config_function=fake_read_system, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - self.assertEqual(config.get("some_key"), None) - def test_simple_config_user_config_is_used_if_others_arent_specified(self): """If no system-wide configuration and no command-line options are specified, the user configuration is used instead.""" - fake_read_system = lambda : {} fake_read_user = lambda _: {"electrum_path": self.electrum_dir} read_user_dir = lambda : self.user_dir config = SimpleConfig(options={}, - read_system_config_function=fake_read_system, read_user_config_function=fake_read_user, read_user_dir_function=read_user_dir) self.assertEqual(self.options.get("electrum_path"), config.get("electrum_path")) def test_cannot_set_options_passed_by_command_line(self): - fake_read_system = lambda : {} fake_read_user = lambda _: {"electrum_path": "b"} read_user_dir = lambda : self.user_dir config = SimpleConfig(options=self.options, - read_system_config_function=fake_read_system, read_user_config_function=fake_read_user, read_user_dir_function=read_user_dir) config.set_key("electrum_path", "c") self.assertEqual(self.options.get("electrum_path"), config.get("electrum_path")) - def test_can_set_options_from_system_config(self): - fake_read_system = lambda : {"electrum_path": self.electrum_dir} - fake_read_user = lambda _: {} - read_user_dir = lambda : self.user_dir - config = SimpleConfig(options={}, - read_system_config_function=fake_read_system, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - config.set_key("electrum_path", "c") - self.assertEqual("c", config.get("electrum_path")) - def test_can_set_options_set_in_user_config(self): another_path = tempfile.mkdtemp() - fake_read_system = lambda : {} fake_read_user = lambda _: {"electrum_path": self.electrum_dir} read_user_dir = lambda : self.user_dir config = SimpleConfig(options={}, - read_system_config_function=fake_read_system, - read_user_config_function=fake_read_user, - read_user_dir_function=read_user_dir) - config.set_key("electrum_path", another_path) - self.assertEqual(another_path, config.get("electrum_path")) - - def test_can_set_options_from_system_config_if_portable(self): - """If the "portable" flag is set, the user can overwrite system - configuration options.""" - another_path = tempfile.mkdtemp() - fake_read_system = lambda : {"electrum_path": self.electrum_dir} - fake_read_user = lambda _: {} - read_user_dir = lambda : self.user_dir - config = SimpleConfig(options={"portable": True}, - read_system_config_function=fake_read_system, read_user_config_function=fake_read_user, read_user_dir_function=read_user_dir) config.set_key("electrum_path", another_path) self.assertEqual(another_path, config.get("electrum_path")) def test_user_config_is_not_written_with_read_only_config(self): - """The user config does not contain command-line options or system - options when saved.""" - fake_read_system = lambda : {"something": "b"} + """The user config does not contain command-line options when saved.""" fake_read_user = lambda _: {"something": "a"} read_user_dir = lambda : self.user_dir self.options.update({"something": "c"}) config = SimpleConfig(options=self.options, - read_system_config_function=fake_read_system, read_user_config_function=fake_read_user, read_user_dir_function=read_user_dir) config.save_user_config() @@ -168,48 +105,10 @@ def test_user_config_is_not_written_with_read_only_config(self): with open(os.path.join(self.electrum_dir, "config"), "r") as f: contents = f.read() result = ast.literal_eval(contents) + result.pop('config_version', None) self.assertEqual({"something": "a"}, result) -class TestSystemConfig(unittest.TestCase): - - sample_conf = """ -[client] -gap_limit = 5 - -[something_else] -everything = 42 -""" - - def setUp(self): - super(TestSystemConfig, self).setUp() - self.thefile = tempfile.mkstemp(suffix=".electrum.test.conf")[1] - - def tearDown(self): - super(TestSystemConfig, self).tearDown() - os.remove(self.thefile) - - def test_read_system_config_file_does_not_exist(self): - somefile = "/foo/I/do/not/exist/electrum.conf" - result = read_system_config(somefile) - self.assertEqual({}, result) - - def test_read_system_config_file_returns_file_options(self): - with open(self.thefile, "w") as f: - f.write(self.sample_conf) - - result = read_system_config(self.thefile) - self.assertEqual({"gap_limit": "5"}, result) - - def test_read_system_config_file_no_sections(self): - - with open(self.thefile, "w") as f: - f.write("gap_limit = 5") # The file has no sections at all - - result = read_system_config(self.thefile) - self.assertEqual({}, result) - - class TestUserConfig(unittest.TestCase): def setUp(self): From f77f029d728456e2a080abd7cf688afc270a20bc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 16 Jan 2018 11:53:31 +0100 Subject: [PATCH 009/270] config v2: change server protocol to :s instead of removing it --- lib/simple_config.py | 4 ++-- lib/util.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/simple_config.py b/lib/simple_config.py index 71818e7f570a..3b747b0aea35 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -162,12 +162,12 @@ def convert_version_2(self): self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'}) try: - # migrate server string FROM host:port:proto TO host:port + # change server string FROM host:port:proto TO host:port:s server_str = self.user_config.get('server') host, port, protocol = str(server_str).rsplit(':', 2) assert protocol in ('s', 't') int(port) # Throw if cannot be converted to int - server_str = str('{}:{}'.format(host, port)) + server_str = '{}:{}:s'.format(host, port) self._set_key_in_user_config('server', server_str) except BaseException: self._set_key_in_user_config('server', None) diff --git a/lib/util.py b/lib/util.py index d9eaea6cc41b..db5fe896f5d3 100644 --- a/lib/util.py +++ b/lib/util.py @@ -77,8 +77,12 @@ def diagnostic_name(self): return self.__class__.__name__ def print_error(self, *msg): + # only prints with --verbose flag print_error("[%s]" % self.diagnostic_name(), *msg) + def print_stderr(self, *msg): + print_stderr("[%s]" % self.diagnostic_name(), *msg) + def print_msg(self, *msg): print_msg("[%s]" % self.diagnostic_name(), *msg) From 1c10d5dce1ed4ef15f014f13e2034aa10068d2b5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 16 Jan 2018 12:36:39 +0100 Subject: [PATCH 010/270] remove --nossl option (follow-up dc388d4c7c541fadb9869727e359edace4c9f6f0) --- gui/qt/network_dialog.py | 8 ++++---- lib/commands.py | 1 - lib/network.py | 19 ++++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/gui/qt/network_dialog.py b/gui/qt/network_dialog.py index 28daf14cb9dd..f1c578e36c29 100644 --- a/gui/qt/network_dialog.py +++ b/gui/qt/network_dialog.py @@ -33,6 +33,7 @@ from electrum.i18n import _ from electrum.bitcoin import NetworkConstants from electrum.util import print_error +from electrum.network import serialize_server, deserialize_server from .util import * @@ -145,7 +146,7 @@ def create_menu(self, position): menu.exec_(self.viewport().mapToGlobal(position)) def set_server(self, s): - host, port, protocol = s.split(':') + host, port, protocol = deserialize_server(s) self.parent.server_host.setText(host) self.parent.server_port.setText(port) self.parent.set_server() @@ -170,7 +171,7 @@ def update(self, servers, protocol, use_tor): port = d.get(protocol) if port: x = QTreeWidgetItem([_host, port]) - server = _host+':'+port+':'+protocol + server = serialize_server(_host, port, protocol) x.setData(1, Qt.UserRole, server) self.addTopLevelItem(x) @@ -408,7 +409,7 @@ def follow_branch(self, index): def follow_server(self, server): self.network.switch_to_interface(server) host, port, protocol, proxy, auto_connect = self.network.get_parameters() - host, port, protocol = server.split(':') + host, port, protocol = deserialize_server(server) self.network.set_parameters(host, port, protocol, proxy, auto_connect) self.update() @@ -441,7 +442,6 @@ def set_server(self): host, port, protocol, proxy, auto_connect = self.network.get_parameters() host = str(self.server_host.text()) port = str(self.server_port.text()) - protocol = 't' if self.config.get('nossl') else 's' auto_connect = self.autoconnect_cb.isChecked() self.network.set_parameters(host, port, protocol, proxy, auto_connect) diff --git a/lib/commands.py b/lib/commands.py index ff13ae682c26..1987d467da94 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -823,7 +823,6 @@ def add_global_options(parser): group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory") group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path") group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet") - group.add_argument("--nossl", action="store_true", dest="nossl", default=False, help="Disable SSL") def get_parser(): # create main parser diff --git a/lib/network.py b/lib/network.py index 16e321fa847b..8456ad5ab505 100644 --- a/lib/network.py +++ b/lib/network.py @@ -137,7 +137,7 @@ def deserialize_proxy(s): def deserialize_server(server_str): - host, port, protocol = str(server_str).split(':') + host, port, protocol = str(server_str).rsplit(':', 2) assert protocol in 'st' int(port) # Throw if cannot be converted to int return host, port, protocol @@ -171,15 +171,15 @@ def __init__(self, config=None): self.blockchain_index = config.get('blockchain_index', 0) if self.blockchain_index not in self.blockchains.keys(): self.blockchain_index = 0 - self.protocol = 't' if self.config.get('nossl') else 's' # Server for addresses and transactions - self.default_server = self.config.get('server') + self.default_server = self.config.get('server', None) # Sanitize default server - try: - host, port, protocol = deserialize_server(self.default_server) - assert protocol == self.protocol - except: - self.default_server = None + if self.default_server: + try: + deserialize_server(self.default_server) + except: + self.print_error('Warning: failed to parse server-string; falling back to random.') + self.default_server = None if not self.default_server: self.default_server = pick_random_server() self.lock = threading.Lock() @@ -220,7 +220,8 @@ def __init__(self, config=None): self.connecting = set() self.requested_chunks = set() self.socket_queue = queue.Queue() - self.start_network(self.protocol, deserialize_proxy(self.config.get('proxy'))) + self.start_network(deserialize_server(self.default_server)[2], + deserialize_proxy(self.config.get('proxy'))) def register_callback(self, callback, events): with self.lock: From 5ec3d2888f9ce8097e0bbbf8b26b01f67c0766fd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 16 Jan 2018 16:02:58 +0100 Subject: [PATCH 011/270] websocket: migrate to scripthashes --- lib/network.py | 3 +++ lib/websockets.py | 14 +++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/network.py b/lib/network.py index 16e321fa847b..06b6db33e3dd 100644 --- a/lib/network.py +++ b/lib/network.py @@ -604,6 +604,9 @@ def process_responses(self, interface): elif method == 'blockchain.scripthash.subscribe': response['params'] = [params[0]] # addr response['result'] = params[1] + elif method == 'blockchain.address.subscribe': + response['params'] = [params[0]] + response['result'] = params[1] callbacks = self.subscriptions.get(k, []) # update cache if it's a subscription diff --git a/lib/websockets.py b/lib/websockets.py index f2bd9149de55..415556b33632 100644 --- a/lib/websockets.py +++ b/lib/websockets.py @@ -84,7 +84,8 @@ def reading_thread(self): l = self.subscriptions.get(addr, []) l.append((ws, amount)) self.subscriptions[addr] = l - self.network.send([('blockchain.address.subscribe', [addr])], self.response_queue.put) + h = self.network.addr_to_scripthash(addr) + self.network.send([('blockchain.scripthash.subscribe', [h])], self.response_queue.put) def run(self): @@ -100,10 +101,13 @@ def run(self): result = r.get('result') if result is None: continue - if method == 'blockchain.address.subscribe': - self.network.send([('blockchain.address.get_balance', params)], self.response_queue.put) - elif method == 'blockchain.address.get_balance': - addr = params[0] + if method == 'blockchain.scripthash.subscribe': + self.network.send([('blockchain.scripthash.get_balance', params)], self.response_queue.put) + elif method == 'blockchain.scripthash.get_balance': + h = params[0] + addr = self.network.h2addr.get(h, None) + if addr is None: + util.print_error("can't find address for scripthash: %s" % h) l = self.subscriptions.get(addr, []) for ws, amount in l: if not ws.closed: From d808bf057e21ef728e71f926523ad58056f8c526 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 16 Jan 2018 16:35:29 +0100 Subject: [PATCH 012/270] fix notify command and migrate to use scripthashes --- lib/commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/commands.py b/lib/commands.py index 1987d467da94..bb10161c672a 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -656,13 +656,15 @@ def callback(x): import urllib.request headers = {'content-type':'application/json'} data = {'address':address, 'status':x.get('result')} + serialized_data = util.to_bytes(json.dumps(data)) try: - req = urllib.request.Request(URL, json.dumps(data), headers) + req = urllib.request.Request(URL, serialized_data, headers) response_stream = urllib.request.urlopen(req, timeout=5) util.print_error('Got Response for %s' % address) except BaseException as e: util.print_error(str(e)) - self.network.send([('blockchain.address.subscribe', [address])], callback) + h = self.network.addr_to_scripthash(address) + self.network.send([('blockchain.scripthash.subscribe', [h])], callback) return True @command('wn') From fbad16b95cea4f57c881bc19dc2aa0d9e4b65161 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 16 Jan 2018 17:10:54 +0100 Subject: [PATCH 013/270] revert adding handling of 'blockchain.address.subscribe' in network.py --- lib/network.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/network.py b/lib/network.py index 06b6db33e3dd..16e321fa847b 100644 --- a/lib/network.py +++ b/lib/network.py @@ -604,9 +604,6 @@ def process_responses(self, interface): elif method == 'blockchain.scripthash.subscribe': response['params'] = [params[0]] # addr response['result'] = params[1] - elif method == 'blockchain.address.subscribe': - response['params'] = [params[0]] - response['result'] = params[1] callbacks = self.subscriptions.get(k, []) # update cache if it's a subscription From 3dd4285fc58132e00e5c50a8ee0d6e9218240a65 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 16 Jan 2018 21:52:45 +0100 Subject: [PATCH 014/270] Change number of zero when base unit changes --- gui/qt/main_window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index c66e86e8d225..fb66005eb544 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2736,7 +2736,7 @@ def on_alias_edit(): unit_combo = QComboBox() unit_combo.addItems(units) unit_combo.setCurrentIndex(units.index(self.base_unit())) - def on_unit(x): + def on_unit(x, nz): unit_result = units[unit_combo.currentIndex()] if self.base_unit() == unit_result: return @@ -2751,13 +2751,14 @@ def on_unit(x): else: raise Exception('Unknown base unit') self.config.set_key('decimal_point', self.decimal_point, True) + nz.setMaximum(self.decimal_point) self.history_list.update() self.request_list.update() self.address_list.update() for edit, amount in zip(edits, amounts): edit.setAmount(amount) self.update_status() - unit_combo.currentIndexChanged.connect(on_unit) + unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz)) gui_widgets.append((unit_label, unit_combo)) block_explorers = sorted(util.block_explorer_info().keys()) From a20a3f97144656337885046e28b0a3fda6284d2d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 17 Jan 2018 01:46:00 +0100 Subject: [PATCH 015/270] 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 a9dc646bcb40..50002e276215 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -887,8 +887,9 @@ def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None, 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 e6dd3e6ad84c5b2978e6aff8a07c4e8e348c62ba Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 18 Jan 2018 11:56:21 +0100 Subject: [PATCH 016/270] allow to save unbroadcasted transactions in wallet --- gui/qt/history_list.py | 1 + lib/commands.py | 9 ++++++ lib/synchronizer.py | 2 +- lib/wallet.py | 66 ++++++++++++++++++++++-------------------- 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 6e17037ba72b..4126662d5f5d 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -37,6 +37,7 @@ "warning.png", "unconfirmed.png", "unconfirmed.png", + "warning.png", "clock1.png", "clock2.png", "clock3.png", diff --git a/lib/commands.py b/lib/commands.py index bb10161c672a..c52b4637f77d 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -629,6 +629,15 @@ def addrequest(self, amount, memo='', expiration=None, force=False): out = self.wallet.get_payment_request(addr, self.config) return self._format_request(out) + @command('w') + def addtransaction(self, tx): + """ Add a transaction to the wallet history """ + #fixme: we should ensure that tx is related to wallet + tx = Transaction(tx) + self.wallet.add_transaction(tx.txid(), tx) + self.wallet.save_transactions() + return tx.txid() + @command('wp') def signrequest(self, address, password=None): "Sign payment request with an OpenAlias" diff --git a/lib/synchronizer.py b/lib/synchronizer.py index b6534e3868a7..70e13e529934 100644 --- a/lib/synchronizer.py +++ b/lib/synchronizer.py @@ -88,7 +88,7 @@ def on_address_status(self, response): if not params: return addr = params[0] - history = self.wallet.get_address_history(addr) + history = self.wallet.history.get(addr, []) if self.get_status(history) != result: if self.requested_histories.get(addr) is None: self.requested_histories[addr] = result diff --git a/lib/wallet.py b/lib/wallet.py index a9dc646bcb40..93c9ff3161df 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -69,6 +69,7 @@ _('Low fee'), _('Unconfirmed'), _('Not Verified'), + _('Local'), ] @@ -404,28 +405,30 @@ def get_local_height(self): return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) def get_tx_height(self, tx_hash): - """ return the height and timestamp of a verified transaction. """ + """ return the height and timestamp of a transaction. """ with self.lock: if tx_hash in self.verified_tx: height, timestamp, pos = self.verified_tx[tx_hash] conf = max(self.get_local_height() - height + 1, 0) return height, conf, timestamp - else: + elif tx_hash in self.unverified_tx: height = self.unverified_tx[tx_hash] return height, 0, False + else: + # local transaction + return -2, 0, False def get_txpos(self, tx_hash): "return position, even if the tx is unverified" with self.lock: - x = self.verified_tx.get(tx_hash) - y = self.unverified_tx.get(tx_hash) - if x: - height, timestamp, pos = x + if tx_hash in self.verified_tx: + height, timestamp, pos = self.verified_tx[tx_hash] return height, pos - elif y > 0: - return y, 0 + elif tx_hash in self.unverified_tx: + height = self.unverified_tx[tx_hash] + return (height, 0) if height>0 else (1e9 - height), 0 else: - return 1e12 - y, 0 + return (1e9+1, 0) def is_found(self): return self.history.values() != [[]] * len(self.history) @@ -520,7 +523,7 @@ def get_tx_info(self, tx): status = _("%d confirmations") % conf else: status = _('Not verified') - else: + elif height in [-1,0]: status = _('Unconfirmed') if fee is None: fee = self.tx_fees.get(tx_hash) @@ -529,6 +532,9 @@ def get_tx_info(self, tx): fee_per_kb = fee * 1000 / size exp_n = self.network.config.reverse_dynfee(fee_per_kb) can_bump = is_mine and not tx.is_final() + else: + status = _('Local') + can_broadcast = self.network is not None else: status = _("Signed") can_broadcast = self.network is not None @@ -550,7 +556,7 @@ def get_tx_info(self, tx): return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n def get_addr_io(self, address): - h = self.history.get(address, []) + h = self.get_address_history(address) received = {} sent = {} for tx_hash, height in h: @@ -649,9 +655,14 @@ def get_balance(self, domain=None): xx += x return cc, uu, xx - def get_address_history(self, address): - with self.lock: - return self.history.get(address, []) + def get_address_history(self, addr): + h = [] + with 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] + h.append((tx_hash, tx_height)) + return h def find_pay_to_pubkey_address(self, prevout_hash, prevout_n): dd = self.txo.get(prevout_hash, {}) @@ -748,10 +759,9 @@ def receive_history_callback(self, addr, hist, tx_fees): old_hist = self.history.get(addr, []) for tx_hash, height in old_hist: if (tx_hash, height) not in hist: - # remove tx if it's not referenced in histories - self.tx_addr_hist[tx_hash].remove(addr) - if not self.tx_addr_hist[tx_hash]: - self.remove_transaction(tx_hash) + # make tx local + self.unverified_tx.pop(tx_hash, None) + self.verified_tx.pop(tx_hash, None) self.history[addr] = hist for tx_hash, tx_height in hist: @@ -844,10 +854,12 @@ def get_tx_status(self, tx_hash, height, conf, timestamp): is_lowfee = fee < low_fee * 0.5 else: is_lowfee = False - if height==0 and not is_final: - status = 0 - elif height < 0: + if height == -2: + status = 5 + elif height == -1: status = 1 + elif height==0 and not is_final: + status = 0 elif height == 0 and is_lowfee: status = 2 elif height == 0: @@ -855,9 +867,9 @@ def get_tx_status(self, tx_hash, height, conf, timestamp): else: status = 4 else: - status = 4 + min(conf, 6) + status = 5 + min(conf, 6) time_str = format_time(timestamp) if timestamp else _("unknown") - status_str = TX_STATUS[status] if status < 5 else time_str + status_str = TX_STATUS[status] if status < 6 else time_str return status, status_str def relayfee(self): @@ -967,14 +979,6 @@ def prepare_for_verifier(self): # add it in case it was previously unconfirmed self.add_unverified_tx(tx_hash, tx_height) - # if we are on a pruning server, remove unverified transactions - with self.lock: - vr = list(self.verified_tx.keys()) + list(self.unverified_tx.keys()) - for tx_hash in list(self.transactions): - if tx_hash not in vr: - self.print_error("removing transaction", tx_hash) - self.transactions.pop(tx_hash) - def start_threads(self, network): self.network = network if self.network is not None: From 35878b437ac88bedbf49c17ea004885a7859aaa6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 19 Jan 2018 15:48:20 +0100 Subject: [PATCH 017/270] network: send version first --- lib/network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/network.py b/lib/network.py index 8456ad5ab505..4855c0fcb5c1 100644 --- a/lib/network.py +++ b/lib/network.py @@ -306,6 +306,9 @@ def send_subscriptions(self): # Resend unanswered requests requests = self.unanswered_requests.values() self.unanswered_requests = {} + if self.interface.ping_required(): + params = [ELECTRUM_VERSION, PROTOCOL_VERSION] + self.queue_request('server.version', params, self.interface) for request in requests: message_id = self.queue_request(request[0], request[1]) self.unanswered_requests[message_id] = request @@ -314,9 +317,6 @@ def send_subscriptions(self): self.queue_request('server.peers.subscribe', []) self.request_fee_estimates() self.queue_request('blockchain.relayfee', []) - if self.interface.ping_required(): - params = [ELECTRUM_VERSION, PROTOCOL_VERSION] - self.queue_request('server.version', params, self.interface) for h in self.subscribed_addresses: self.queue_request('blockchain.scripthash.subscribe', [h]) From 98daf2c7cc65189aa195999bb0236fe3e03c4ac8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 19 Jan 2018 15:48:20 +0100 Subject: [PATCH 018/270] network: send version first --- lib/network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/network.py b/lib/network.py index 8456ad5ab505..4855c0fcb5c1 100644 --- a/lib/network.py +++ b/lib/network.py @@ -306,6 +306,9 @@ def send_subscriptions(self): # Resend unanswered requests requests = self.unanswered_requests.values() self.unanswered_requests = {} + if self.interface.ping_required(): + params = [ELECTRUM_VERSION, PROTOCOL_VERSION] + self.queue_request('server.version', params, self.interface) for request in requests: message_id = self.queue_request(request[0], request[1]) self.unanswered_requests[message_id] = request @@ -314,9 +317,6 @@ def send_subscriptions(self): self.queue_request('server.peers.subscribe', []) self.request_fee_estimates() self.queue_request('blockchain.relayfee', []) - if self.interface.ping_required(): - params = [ELECTRUM_VERSION, PROTOCOL_VERSION] - self.queue_request('server.version', params, self.interface) for h in self.subscribed_addresses: self.queue_request('blockchain.scripthash.subscribe', [h]) From 98663d7e20bb391febaffe213bb52b763717e135 Mon Sep 17 00:00:00 2001 From: Serge Victor Date: Thu, 18 Jan 2018 07:07:42 +0700 Subject: [PATCH 019/270] Intruduction of getfee command, fixes #3704 --- lib/commands.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/commands.py b/lib/commands.py index bb10161c672a..dead8b82b8db 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -672,6 +672,12 @@ def is_synchronized(self): """ return wallet synchronization status """ return self.wallet.is_up_to_date() + @command('n') + def getfeerate(self): + """Return current optimal fee rate per kilobyte, according + to config settings (static/dynamic)""" + return self.config.fee_per_kb() + @command('') def help(self): # for the python console From fe54ce946c0437c037e69eddf318e1a8781c73ed Mon Sep 17 00:00:00 2001 From: Alexx Saver Date: Sun, 21 Jan 2018 02:05:27 +0400 Subject: [PATCH 020/270] Fixed rm -rf Notice: it is unsafe to do rm -rf ${variable}/path/* without checking the variable, quotes are also required. --- contrib/build-wine/build.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contrib/build-wine/build.sh b/contrib/build-wine/build.sh index 5f00e824bea6..efd23bc17c0f 100755 --- a/contrib/build-wine/build.sh +++ b/contrib/build-wine/build.sh @@ -7,10 +7,11 @@ if [ ! -z "$1" ]; then fi here=$(dirname "$0") +test -n "$here" -a -d "$here" || exit echo "Clearing $here/build and $here/dist..." -rm $here/build/* -rf -rm $here/dist/* -rf +rm "$here"/build/* -rf +rm "$here"/dist/* -rf $here/prepare-wine.sh && \ $here/prepare-pyinstaller.sh && \ From 452d60033a91c7d8d8968890adc56b601fca0dc0 Mon Sep 17 00:00:00 2001 From: ken2812221 Date: Sun, 21 Jan 2018 22:59:27 +0800 Subject: [PATCH 021/270] Shows correct bech32 address on ledger --- plugins/ledger/ledger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index 2d1a593c46d9..908af27853c3 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -463,8 +463,9 @@ def show_address(self, sequence, txin_type): address_path = self.get_derivation()[2:] + "/%d/%d"%sequence self.handler.show_message(_("Showing address ...")) segwit = Transaction.is_segwit_inputtype(txin_type) + segwitNative = txin_type == 'p2wpkh' try: - client.getWalletPublicKey(address_path, showOnScreen=True, segwit=segwit) + client.getWalletPublicKey(address_path, showOnScreen=True, segwit=segwit, segwitNative=segwitNative) except BTChipException as e: if e.sw == 0x6985: # cancelled by user pass From 20443aa068d18a1fc0e327f30ed23bddf7d98ec5 Mon Sep 17 00:00:00 2001 From: Alexx Saver Date: Mon, 22 Jan 2018 21:40:16 +0400 Subject: [PATCH 022/270] Follow-up fe54ce9 --- contrib/make_packages | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/make_packages b/contrib/make_packages index 1f7c1fa9c693..b3a36049722e 100755 --- a/contrib/make_packages +++ b/contrib/make_packages @@ -1,11 +1,12 @@ #!/bin/bash contrib=$(dirname "$0") +test -n "$contrib" -a -d "$contrib" || exit whereis pip3 if [ $? -ne 0 ] ; then echo "Install pip3" ; exit ; fi -rm $contrib/../packages/ -r +rm "$contrib"/../packages/ -r #Install pure python modules in electrum directory pip3 install -r $contrib/requirements.txt -t $contrib/../packages From 9ceaac63105c7344182528aa2a383a1395188889 Mon Sep 17 00:00:00 2001 From: Alexx Saver Date: Tue, 23 Jan 2018 00:20:38 +0400 Subject: [PATCH 023/270] Use python3 for virtualenv --- contrib/freeze_packages.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/freeze_packages.sh b/contrib/freeze_packages.sh index 139fb6ee7887..c8e4527b968f 100755 --- a/contrib/freeze_packages.sh +++ b/contrib/freeze_packages.sh @@ -6,8 +6,8 @@ contrib=$(dirname "$0") which virtualenv > /dev/null 2>&1 || { echo "Please install virtualenv" && exit 1; } -rm $venv_dir -rf -virtualenv $venv_dir +rm "$venv_dir" -rf +virtualenv -p $(which python3) $venv_dir source $venv_dir/bin/activate From 5e9d901794f8e8205281833d9ffe2a78baaba23e Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 23 Jan 2018 19:11:12 +0100 Subject: [PATCH 024/270] Allow to remove local transactions from the GUI --- gui/qt/history_list.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 4126662d5f5d..7a554b08388e 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -159,11 +159,15 @@ def create_menu(self, position): menu = QMenu() + if height == -2: + menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) + menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) if column in self.editable_columns: menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column)) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) + if is_unconfirmed and tx: rbf = is_mine and not tx.is_final() if rbf: @@ -177,3 +181,20 @@ def create_menu(self, position): if tx_URL: menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL)) menu.exec_(self.viewport().mapToGlobal(position)) + + def remove_local_tx(self, tx_hash): + answer = QMessageBox.question(self.parent, + _("Please confirm"), + _("Are you sure you want to remove this transaction?"), + QMessageBox.Yes, + QMessageBox.No) + if answer == QMessageBox.No: + return + self.wallet.remove_transaction(tx_hash) + root = self.invisibleRootItem() + child_count = root.childCount() + for i in range(child_count): + item = root.child(i) + if item.data(0, Qt.UserRole) == tx_hash: + root.removeChild(item) + return From 887e06eebb75161d68c7f51f7ad3f1feda64561e Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 23 Jan 2018 23:50:02 +0100 Subject: [PATCH 025/270] Set icon for offline transactions --- gui/qt/history_list.py | 2 +- icons.qrc | 1 + icons/offline_tx.png | Bin 0 -> 2451 bytes lib/wallet.py | 2 +- 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 icons/offline_tx.png diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 7a554b08388e..a5ca141b3339 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -37,7 +37,7 @@ "warning.png", "unconfirmed.png", "unconfirmed.png", - "warning.png", + "offline_tx.png", "clock1.png", "clock2.png", "clock3.png", diff --git a/icons.qrc b/icons.qrc index 92e19a1a13e8..195e9af9dd7f 100644 --- a/icons.qrc +++ b/icons.qrc @@ -23,6 +23,7 @@ icons/lock.png icons/microphone.png icons/network.png + icons/offline_tx.png icons/qrcode.png icons/qrcode_white.png icons/preferences.png diff --git a/icons/offline_tx.png b/icons/offline_tx.png new file mode 100644 index 0000000000000000000000000000000000000000..55d2b2a471ed3f96214b239dba9c6e18f1437d17 GIT binary patch literal 2451 zcmV;E32gR>P)& z&=rwK}t!M1dgkNnMnT|vp4U_;JW_{Na+XAU=vZgd&P{_p<)XJBt7D*dyx{1HPz0W zb}ld1op#zkAVDTPIc9{9aYk(nbtR=FA12g2H)5FQ7kL@JiK>zIY=4(Hzs9{WmlgTkF1ED%shVF63Rt$WCPiWN@Au6q z@VgX-vllcdoOVb;J{~Ga!z5c`rszCovi^pmbWd)& z4_#p$FaEnREDUS5p~gxvM2)lek!j~lT`YWlcS9%`HIO^pkK(I_yIf#T#{n*Qyk?|; zT()phwh90*S2adWA=bUKviR6#?QSW_+^1@aows(bD*ni@u<4cR(_sLZ|MQViIwClIE@IFyU*DUgx}r2lsND4R&+i8*CAn?m;eRmD{kJ?> zTOMa}b*?#iHWX{_jKJ^aC@xB63MCZ)K$mRY$|Ufck_j0u0N7F29I|b(dPiVVv^T+= zwFj~&*z@y?$AZ!C(cMy#1sjjNG%V4*Wbv$VlyS1H{OP(8eN|juuA7z}cI8W!rm3UL}$UUuepbJzMQS7oYWvr@63xPXFOT6f>F z{?IkTvhFIqF5j1(o`{JB846`&tnIKBDWykQ;y zIMLDrx5}!V)hl2Bey+5|;_C`B6#zK+X-kL#591Qv$#)-~_6Z1Cx#xq%2mnkf%2K2) zmpS9Mub2_?DD=RImL34Wgi*s>3XwNcH)k`)WF#m6aP&+t8qv)+Kf(>=m^Vije=C7*_E!=pxvTRMwSZA78G`2Yy);gQdcf+l6 z6y&4`2ENGY6V}8V$G!|k0iZA|LG`G-gi2c$j!1R`0Gd0(rY-EFaiv$-vb+&VUH~|L zx;-j{+`Mh+Qt{vJCPxWKKBHvT?Iq*=cb84{Pb$p3i7V`b-cAI7>OiSwS@tuZo$1g3 zAU`wN>sHyFam5hgqo($-4geX+9@w@VM=eXHCi^dbr^7LeDT?cKT#POA(uVl}04?o3 z!i22s8?>S{sHn8V=XFU@pT zQv3*pH2iDd=Ll;C#*a?tmTi|fecfDs8PhE{0C?RBOktHba0QEREP@j_2{{!*VG+4)V?YCnV#v*6FKRB-i8O8URE!6AGhUPP@>PlH4kb z0YJvxivhBex`lrYZ9a?+C&0B8?JKq*c+b1|FcCwjRAfMCP`XQYD?N*daN8UP?C z&96{KCpv=+(|ql8TXz%yuFgyMa!xCjY~9Lw?YmYNpU{Ol;Yj`2y)V4|k@jlkG0hNG z6$tqRAftQN$&x@7=bSDdKRV3~0F7sZQOyvqImd#O*X5@9TmWz`7y~7=o@=Jqcd9Kk zCN`olT?u1Ykq2&z_YtngGOA zwq#RPvP*aG0x+3MR^{>W*OUG3)STf-&UpA@|}(qrSCAi|S?- z6~f;8$(ai}0F28{L`=74Eess-_6EKEXz^E|Z0nKwR+9iInV9Q?kZW&Vb2x5^=&$~} zAY0gfo<1SR2LMOvTTMdA>&~M@NubIf)y?VSvJ(LS$D4wt1-h3e<+mK_j+x?ow+64u zQ8Y5q5*~54b1>b~SAE>jW}ayaV%OnQ5zg2XdsaZmAbEc`0 zl}=x;*K<$N$Rx|Fa-8joAsiF>^w^XS>9(ay1(kTI>R6ir0E?#OBruLAoK@VtVn(YW z z+1*@Edt^*zqAw@ahp=YhV11`0B-so&7SdZc)IE1YVcM_nxN)>67&Y+t)=$G>-I!gu zqPXr-f&Q1Ajf1a`wA`i{Za3femu1%`Bzsl-^T3%%&FAf}?s$0W{ea_4VutL84mNb? zr`keD_NusV?pVLV*zW$jk{_Z0#|N+BDyJ%HOvErQ<>kZHA4jl4D9Yx=OR**+F$$o4Z7P{5;(4I3tVjs6`WIpHI8!u|H0d#%b{ Date: Wed, 24 Jan 2018 12:40:32 +0400 Subject: [PATCH 026/270] Added default keyserver Notice: GPG will use hkp://keys.gnupg.net anyway if there is no config. --- contrib/build-wine/prepare-wine.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 542a2085a8c1..44c20ab2c1f7 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -57,8 +57,10 @@ cd tmp # Install Python # note: you might need "sudo apt-get install dirmngr" for the following # keys from https://www.python.org/downloads/#pubkeys -KEYRING_PYTHON_DEV=keyring-electrum-build-python-dev.gpg -gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --recv-keys 531F072D39700991925FED0C0EDDC5F26A45C816 26DEA9D4613391EF3E25C9FF0A5B101836580288 CBC547978A3964D14B9AB36A6AF053F07D9DC8D2 C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF 12EF3DC38047DA382D18A5B999CDEA9DA4135B38 8417157EDBE73D9EAC1E539B126EB563A74B06BF DBBF2EEBF925FAADCF1F3FFFD9866941EA5BBD71 2BA0DB82515BBB9EFFAC71C5C9BE28DEE6DF025C 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D C9B104B3DD3AA72D7CCB1066FB9921286F5E1540 97FC712E4C024BBEA48A61ED3A5CA953F73C700D 7ED10B6531D7C8E1BC296021FC624643487034E5 +KEYLIST_PYTHON_DEV="531F072D39700991925FED0C0EDDC5F26A45C816 26DEA9D4613391EF3E25C9FF0A5B101836580288 CBC547978A3964D14B9AB36A6AF053F07D9DC8D2 C01E1CAD5EA2C4F0B8E3571504C367C218ADD4FF 12EF3DC38047DA382D18A5B999CDEA9DA4135B38 8417157EDBE73D9EAC1E539B126EB563A74B06BF DBBF2EEBF925FAADCF1F3FFFD9866941EA5BBD71 2BA0DB82515BBB9EFFAC71C5C9BE28DEE6DF025C 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D C9B104B3DD3AA72D7CCB1066FB9921286F5E1540 97FC712E4C024BBEA48A61ED3A5CA953F73C700D 7ED10B6531D7C8E1BC296021FC624643487034E5" +KEYRING_PYTHON_DEV="keyring-electrum-build-python-dev.gpg" +KEYSERVER_PYTHON_DEV="hkp://keys.gnupg.net" +gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --keyserver $KEYSERVER_PYTHON_DEV --recv-keys $KEYLIST_PYTHON_DEV for msifile in core dev exe lib pip tools; do echo "Installing $msifile..." wget "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi" From fbcee9a6f6ccc858c670571d8b9d2645d0e91556 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Wed, 24 Jan 2018 18:17:50 +0100 Subject: [PATCH 027/270] Also remove child transactions --- gui/qt/history_list.py | 27 +++++++++++++++++---------- lib/wallet.py | 10 ++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index a5ca141b3339..32aec4982704 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -182,19 +182,26 @@ def create_menu(self, position): menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL)) menu.exec_(self.viewport().mapToGlobal(position)) - def remove_local_tx(self, tx_hash): - answer = QMessageBox.question(self.parent, - _("Please confirm"), - _("Are you sure you want to remove this transaction?"), - QMessageBox.Yes, - QMessageBox.No) + 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 - self.wallet.remove_transaction(tx_hash) + for tx in to_delete: + self.wallet.remove_transaction(tx) root = self.invisibleRootItem() child_count = root.childCount() + _offset = 0 for i in range(child_count): - item = root.child(i) - if item.data(0, Qt.UserRole) == tx_hash: + item = root.child(i - _offset) + if item.data(0, Qt.UserRole) in to_delete: root.removeChild(item) - return + _offset += 1 diff --git a/lib/wallet.py b/lib/wallet.py index 8a7770d82f20..ae2c2a74565a 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1377,6 +1377,16 @@ def decrypt_message(self, pubkey, message, password): index = self.get_address_index(addr) return self.keystore.decrypt_message(index, message, password) + def get_depending_transactions(self, tx_hash): + """Returns all (grand-)children of tx_hash in this wallet.""" + children = set() + for other_hash, tx in self.transactions.items(): + for input in (tx.inputs()): + if input["prevout_hash"] == tx_hash: + children.add(other_hash) + children |= self.get_depending_transactions(other_hash) + return children + class Simple_Wallet(Abstract_Wallet): # wallet with a single keystore From 95da5a8bed7136bf0defd36b60732d3bfe0680b3 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Wed, 24 Jan 2018 21:32:51 +0100 Subject: [PATCH 028/270] Enable adding transactions from file through Drag and Drop --- gui/qt/history_list.py | 12 +++++++++++- gui/qt/util.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 32aec4982704..06d68f2ef997 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -47,11 +47,12 @@ ] -class HistoryList(MyTreeWidget): +class HistoryList(MyTreeWidget, AcceptFileDragDrop): filter_columns = [2, 3, 4] # Date, Description, Amount def __init__(self, parent=None): MyTreeWidget.__init__(self, parent, self.create_menu, [], 3) + AcceptFileDragDrop.__init__(self, ".txn") self.refresh_headers() self.setColumnHidden(1, True) @@ -205,3 +206,12 @@ def remove_local_tx(self, delete_tx): if item.data(0, Qt.UserRole) in to_delete: root.removeChild(item) _offset += 1 + + def onFileAdded(self, fn): + with open(fn) as f: + tx = self.parent.tx_from_text(f.read()) + self.wallet.add_transaction(tx.txid(), tx) + self.wallet.save_transactions() + self.on_update() + + diff --git a/gui/qt/util.py b/gui/qt/util.py index 2d2626a1819e..cd552e405d14 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -635,6 +635,40 @@ def update_from_widget(widget): if ColorScheme.has_dark_background(widget): ColorScheme.dark_scheme = True + +class AcceptFileDragDrop: + def __init__(self, file_type=""): + assert isinstance(self, QWidget) + self.setAcceptDrops(True) + self.file_type = file_type + + def validateEvent(self, event): + if not event.mimeData().hasUrls(): + event.ignore() + return False + for url in event.mimeData().urls(): + if not url.toLocalFile().endswith(self.file_type): + event.ignore() + return False + event.accept() + return True + + def dragEnterEvent(self, event): + self.validateEvent(event) + + def dragMoveEvent(self, event): + if self.validateEvent(event): + event.setDropAction(Qt.CopyAction) + + def dropEvent(self, event): + if self.validateEvent(event): + for url in event.mimeData().urls(): + self.onFileAdded(url.toLocalFile()) + + def onFileAdded(self, fn): + raise NotImplementedError() + + if __name__ == "__main__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done")) From e184ac888f85c0bea1e97d58471713dc68ef1c30 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Wed, 24 Jan 2018 21:41:35 +0100 Subject: [PATCH 029/270] Make sure to save changes to transactions on disk --- gui/qt/history_list.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 06d68f2ef997..d270a1af1fc4 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -198,6 +198,7 @@ def remove_local_tx(self, delete_tx): return for tx in to_delete: self.wallet.remove_transaction(tx) + self.wallet.save_transactions(write=True) root = self.invisibleRootItem() child_count = root.childCount() _offset = 0 @@ -211,7 +212,5 @@ def onFileAdded(self, fn): with open(fn) as f: tx = self.parent.tx_from_text(f.read()) self.wallet.add_transaction(tx.txid(), tx) - self.wallet.save_transactions() + self.wallet.save_transactions(write=True) self.on_update() - - From 7ab9fa5be48cf5aefec98d94657241c73a6e280c Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Fri, 26 Jan 2018 21:33:41 +0100 Subject: [PATCH 030/270] Update Kivy build instructions My PRs have been merged! --- gui/kivy/Readme.md | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/gui/kivy/Readme.md b/gui/kivy/Readme.md index 6b880b8677cc..2c8a55f2f630 100644 --- a/gui/kivy/Readme.md +++ b/gui/kivy/Readme.md @@ -5,9 +5,8 @@ The Kivy GUI is used with Electrum on Android devices. To generate an APK file, ## 1. Install python-for-android (p4a) p4a is used to package Electrum, Python, SDL and a bootstrap Java app into an APK file. We patched p4a to add some functionality we need for Electrum. Until those changes are -merged into p4a, you need to merge them locally (into the stable branch): +merged into p4a, you need to merge them locally (into the master branch): -1. [kivy/python-for-android#1213](https://github.com/kivy/python-for-android/pull/1213) 2. [kivy/python-for-android#1217](https://github.com/kivy/python-for-android/pull/1217) Something like this should work: @@ -17,31 +16,18 @@ cd /opt git clone https://github.com/kivy/python-for-android cd python-for-android git remote add agilewalker https://github.com/agilewalker/python-for-android -git remote add bauerj https://github.com/bauerj/python-for-android -git checkout stable +git checkout a036f4442b6a23 git fetch agilewalker git merge agilewalker/master -git fetch bauerj -git merge bauerj/add-activity ``` ## 2. Install buildozer -Buildozer is a frontend to p4a. Of course it needs to be patched too: - -1. [kivy/buildozer#612](https://github.com/kivy/python-for-android/pull/1213) +Buildozer is a frontend to p4a. Luckily we don't need to patch it: ```sh cd /opt git clone https://github.com/kivy/buildozer cd buildozer -git remote add bauerj https://github.com/bauerj/buildozer -git fetch bauerj -git merge bauerj/add-activity -``` - -You also want to install it: - -```sh sudo python3 setup.py install ``` @@ -73,8 +59,7 @@ Run `contrib/make_apk`. Update your Android build tools to version 27 like described above. ## Why do I get errors like `(use -source 7 or higher to enable multi-catch statement)` while compiling? -Use the `stable` branch of python-for-android. -[This commit](https://github.com/kivy/python-for-android/commit/3534a761b17040755accf941f898cc66b905e8db) in master is the culprit. +Make sure that your p4a installation includes commit a3cc78a6d1a107cd3b6bd28db8b80f89e3ecddd2. ## I changed something but I don't see any differences on the phone. What did I do wrong? You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}` From d09138446ffc553658c7987b3fe1d18ba3dc7d43 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 27 Jan 2018 04:24:15 +0100 Subject: [PATCH 031/270] fix wallet.check_history --- lib/wallet.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index a9dc646bcb40..f59632d42fd0 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -272,10 +272,15 @@ def build_reverse_history(self): @profiler def check_history(self): save = False - mine_addrs = list(filter(lambda k: self.is_mine(self.history[k]), self.history.keys())) - if len(mine_addrs) != len(self.history.keys()): + + hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.history.keys())) + hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.history.keys())) + + for addr in hist_addrs_not_mine: + self.history.pop(addr) save = True - for addr in mine_addrs: + + for addr in hist_addrs_mine: hist = self.history[addr] for tx_hash, tx_height in hist: From 7f3b627aa4d9e55e25a445de47b5badd9ed5d304 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 27 Jan 2018 17:33:26 +0100 Subject: [PATCH 032/270] make wallet.get_address_index faster by storing an addr->index dict --- gui/qt/main_window.py | 2 ++ lib/wallet.py | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index fb66005eb544..593673213cf7 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -62,6 +62,7 @@ from .util import * +from electrum.util import profiler class StatusBarButton(QPushButton): def __init__(self, icon, tooltip, func): @@ -325,6 +326,7 @@ def close_wallet(self): self.print_error('close_wallet', self.wallet.storage.path) run_hook('close_wallet', self.wallet) + @profiler def load_wallet(self, wallet): wallet.thread = TaskThread(self, self.on_error) self.wallet = wallet diff --git a/lib/wallet.py b/lib/wallet.py index a9dc646bcb40..11337bc94d24 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -340,6 +340,8 @@ def is_change(self, address): return address in self.change_addresses def get_address_index(self, address): + if hasattr(self, '_addr_to_addr_index'): + return self._addr_to_addr_index[address] if address in self.receiving_addresses: return False, self.receiving_addresses.index(address) if address in self.change_addresses: @@ -1029,8 +1031,10 @@ def can_export(self): def is_used(self, address): h = self.history.get(address,[]) + if len(h) == 0: + return False c, u, x = self.get_addr_balance(address) - return len(h) > 0 and c + u + x == 0 + return c + u + x == 0 def is_empty(self, address): c, u, x = self.get_addr_balance(address) @@ -1647,6 +1651,14 @@ def min_acceptable_gap(self): if n > nmax: nmax = n return nmax + 1 + def load_addresses(self): + super().load_addresses() + self._addr_to_addr_index = {} # key: address, value: (is_change, index) + for i, addr in enumerate(self.receiving_addresses): + self._addr_to_addr_index[addr] = (False, i) + for i, addr in enumerate(self.change_addresses): + self._addr_to_addr_index[addr] = (True, i) + 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 @@ -1654,6 +1666,7 @@ def create_new_address(self, for_change=False): 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 @@ -1685,12 +1698,11 @@ def synchronize(self): def is_beyond_limit(self, address, is_change): addr_list = self.get_change_addresses() if is_change else self.get_receiving_addresses() - i = addr_list.index(address) - prev_addresses = addr_list[:max(0, i)] + i = self.get_address_index(address)[1] limit = self.gap_limit_for_change if is_change else self.gap_limit - if len(prev_addresses) < limit: + if i < limit: return False - prev_addresses = prev_addresses[max(0, i - limit):] + prev_addresses = addr_list[max(0, i - limit):max(0, i)] for addr in prev_addresses: if self.history.get(addr): return False From 1c67cfe01b5cb21e2aa806eac1b0acb8a6004073 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 27 Jan 2018 17:51:24 +0100 Subject: [PATCH 033/270] optimise wallet.is_mine --- lib/wallet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/wallet.py b/lib/wallet.py index 11337bc94d24..b836394ad621 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -332,6 +332,10 @@ def set_label(self, name, text = None): return changed def is_mine(self, address): + if hasattr(self, '_addr_to_addr_index'): # Deterministic_Wallet + return address in self._addr_to_addr_index + if hasattr(self, 'addresses'): # Imported_Wallet + return address in self.addresses return address in self.get_addresses() def is_change(self, address): From d1e2ca4bc543e6e5a13cc82a5e9bc72a7d1486ef Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 27 Jan 2018 18:16:31 +0100 Subject: [PATCH 034/270] make wallet.is_change faster --- lib/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index b836394ad621..e38901e8e166 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -341,7 +341,7 @@ def is_mine(self, address): def is_change(self, address): if not self.is_mine(address): return False - return address in self.change_addresses + return self.get_address_index(address)[0] def get_address_index(self, address): if hasattr(self, '_addr_to_addr_index'): From 743ef9ec8f1e69c56f587359f00de19f4f05ff0a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 28 Jan 2018 01:56:26 +0100 Subject: [PATCH 035/270] fix #3783 --- lib/exchange_rate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index ff739e92d2e0..23de311ec5b9 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -5,6 +5,7 @@ from threading import Thread import time import csv +import decimal from decimal import Decimal from .bitcoin import COIN @@ -389,7 +390,11 @@ def get_exchanges_by_ccy(self, ccy, h): def ccy_amount_str(self, amount, commas): prec = CCY_PRECISIONS.get(self.ccy, 2) fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) - return fmt_str.format(round(amount, prec)) + try: + rounded_amount = round(amount, prec) + except decimal.InvalidOperation: + rounded_amount = amount + return fmt_str.format(rounded_amount) def run(self): # This runs from the plugins thread which catches exceptions From c811c5c9d9cbd5132ebfe9d592bcfbe2385cd657 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 7 Dec 2017 11:35:10 +0100 Subject: [PATCH 036/270] allow encrypting watch-only wallets. initial support for hw wallet storage encryption. --- electrum | 8 +- gui/kivy/uix/dialogs/installwizard.py | 2 +- gui/qt/installwizard.py | 85 ++++++++++++++----- gui/qt/main_window.py | 53 ++++++++---- gui/qt/password_dialog.py | 113 ++++++++++++++++++++++--- lib/base_wizard.py | 107 +++++++++++++++++------ lib/bitcoin.py | 14 +-- lib/commands.py | 2 +- lib/keystore.py | 20 +++-- lib/storage.py | 92 ++++++++++++++++++-- lib/wallet.py | 102 ++++++++++++++++------ plugins/cosigner_pool/qt.py | 2 +- plugins/digitalbitbox/digitalbitbox.py | 7 +- plugins/greenaddress_instant/qt.py | 7 +- plugins/hw_wallet/plugin.py | 7 ++ plugins/hw_wallet/qt.py | 7 +- plugins/keepkey/plugin.py | 5 +- plugins/ledger/ledger.py | 2 +- plugins/trezor/plugin.py | 5 +- plugins/trustedcoin/trustedcoin.py | 15 +++- 20 files changed, 508 insertions(+), 147 deletions(-) diff --git a/electrum b/electrum index 9a9f41855dbf..bcc38d92369c 100755 --- a/electrum +++ b/electrum @@ -192,6 +192,8 @@ def init_daemon(config_options): print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option") 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'): password = config.get('password') else: @@ -236,6 +238,8 @@ def init_cmdline(config_options, server): # commands needing password 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'): password = config.get('password') else: @@ -262,12 +266,14 @@ def run_offline_command(config, config_options): if cmd.requires_wallet: 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") storage.decrypt(password) wallet = Wallet(storage) else: wallet = None # check password - if cmd.requires_password and storage.get('use_encryption'): + if cmd.requires_password and wallet.has_password(): try: seed = wallet.check_password(password) except InvalidPassword: diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py index a8dade46b509..cf7b44cf3dd0 100644 --- a/gui/kivy/uix/dialogs/installwizard.py +++ b/gui/kivy/uix/dialogs/installwizard.py @@ -807,7 +807,7 @@ def password_dialog(self, message, callback): popup.init(message, callback) popup.open() - def request_password(self, run_next): + def request_password(self, run_next, force_disable_encrypt_cb=False): def callback(pin): if pin: self.run('confirm_password', pin, run_next) diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py index 25af07e678a1..e446f57a8726 100644 --- a/gui/qt/installwizard.py +++ b/gui/qt/installwizard.py @@ -10,13 +10,13 @@ from electrum import Wallet, WalletStorage from electrum.util import UserCancelled, InvalidPassword -from electrum.base_wizard import BaseWizard +from electrum.base_wizard import BaseWizard, HWD_SETUP_DECRYPT_WALLET from electrum.i18n import _ from .seed_dialog import SeedLayout, KeysLayout from .network_dialog import NetworkChoiceLayout from .util import * -from .password_dialog import PasswordLayout, PW_NEW +from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW class GoBack(Exception): @@ -29,6 +29,10 @@ class GoBack(Exception): MSG_COSIGNER = _("Please enter the master public key of cosigner #%d:") MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\ + _("Leave this field empty if you want to disable encryption.") +MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\ + + _("Your wallet file does not contain secrets, mostly just metadata. ") \ + + _("It also contains your master public key that allows watching your addresses.") + '\n\n'\ + + _("Note: If you enable this setting, you will need your hardware device to open your wallet.") MSG_RESTORE_PASSPHRASE = \ _("Please enter your seed derivation passphrase. " "Note: this is NOT your encryption password. " @@ -196,12 +200,18 @@ def on_filename(filename): msg =_("This file does not exist.") + '\n' \ + _("Press 'Next' to create this wallet, or choose another file.") pw = False - elif self.storage.file_exists() and self.storage.is_encrypted(): - msg = _("This file is encrypted.") + '\n' + _('Enter your password or choose another file.') - pw = True else: - msg = _("Press 'Next' to open this wallet.") - pw = False + if self.storage.is_encrypted_with_user_pw(): + msg = _("This file is encrypted with a password.") + '\n' \ + + _('Enter your password or choose another file.') + pw = True + elif self.storage.is_encrypted_with_hw_device(): + msg = _("This file is encrypted using a hardware device.") + '\n' \ + + _("Press 'Next' to choose device to decrypt.") + pw = False + else: + msg = _("Press 'Next' to open this wallet.") + pw = False else: msg = _('Cannot read file') pw = False @@ -227,17 +237,40 @@ def on_filename(filename): if not self.storage.file_exists(): break if self.storage.file_exists() and self.storage.is_encrypted(): - password = self.pw_e.text() - try: - self.storage.decrypt(password) - break - except InvalidPassword as e: - QMessageBox.information(None, _('Error'), str(e)) - continue - except BaseException as e: - traceback.print_exc(file=sys.stdout) - QMessageBox.information(None, _('Error'), str(e)) - return + if self.storage.is_encrypted_with_user_pw(): + password = self.pw_e.text() + try: + self.storage.decrypt(password) + break + except InvalidPassword as e: + QMessageBox.information(None, _('Error'), str(e)) + continue + except BaseException as e: + traceback.print_exc(file=sys.stdout) + QMessageBox.information(None, _('Error'), str(e)) + return + elif self.storage.is_encrypted_with_hw_device(): + try: + self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET) + except InvalidPassword as e: + # FIXME if we get here because of mistyped passphrase + # then that passphrase gets "cached" + QMessageBox.information( + None, _('Error'), + _('Failed to decrypt using this hardware device.') + '\n' + + _('If you use a passphrase, make sure it is correct.')) + self.stack = [] + return self.run_and_get_wallet() + except BaseException as e: + traceback.print_exc(file=sys.stdout) + QMessageBox.information(None, _('Error'), str(e)) + return + if self.storage.is_past_initial_decryption(): + break + else: + return + else: + raise Exception('Unexpected encryption version') path = self.storage.path if self.storage.requires_split(): @@ -386,17 +419,25 @@ def show_seed_dialog(self, run_next, seed_text): self.exec_layout(slayout) return slayout.is_ext - def pw_layout(self, msg, kind): - playout = PasswordLayout(None, msg, kind, self.next_button) + def pw_layout(self, msg, kind, force_disable_encrypt_cb): + playout = PasswordLayout(None, msg, kind, self.next_button, + force_disable_encrypt_cb=force_disable_encrypt_cb) playout.encrypt_cb.setChecked(True) self.exec_layout(playout.layout()) return playout.new_password(), playout.encrypt_cb.isChecked() @wizard_dialog - def request_password(self, run_next): + def request_password(self, run_next, force_disable_encrypt_cb=False): """Request the user enter a new password and confirm it. Return the password or None for no password.""" - return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW) + return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW, force_disable_encrypt_cb) + + @wizard_dialog + def request_storage_encryption(self, run_next): + playout = PasswordLayoutForHW(None, MSG_HW_STORAGE_ENCRYPTION, PW_NEW, self.next_button) + playout.encrypt_cb.setChecked(True) + self.exec_layout(playout.layout()) + return playout.encrypt_cb.isChecked() def show_restore(self, wallet, network): # FIXME: these messages are shown after the install wizard is diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index fb66005eb544..b8fc28679c19 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -372,7 +372,7 @@ def watching_only_changed(self): extra.append(_('watching only')) title += ' [%s]'% ', '.join(extra) self.setWindowTitle(title) - self.password_menu.setEnabled(self.wallet.can_change_password()) + self.password_menu.setEnabled(self.wallet.may_have_password()) self.import_privkey_menu.setVisible(self.wallet.can_import_privkey()) self.import_address_menu.setVisible(self.wallet.can_import_address()) self.export_menu.setEnabled(self.wallet.can_export()) @@ -877,14 +877,15 @@ def sign_payment_request(self, addr): if alias_addr: if self.wallet.is_mine(alias_addr): msg = _('This payment request will be signed.') + '\n' + _('Please enter your password') - password = self.password_dialog(msg) - if password: - try: - self.wallet.sign_payment_request(addr, alias, alias_addr, password) - except Exception as e: - self.show_error(str(e)) + password = None + if self.wallet.has_keystore_encryption(): + password = self.password_dialog(msg) + if not password: return - else: + try: + self.wallet.sign_payment_request(addr, alias, alias_addr, password) + except Exception as e: + self.show_error(str(e)) return else: return @@ -1372,7 +1373,7 @@ def protected(func): def request_password(self, *args, **kwargs): parent = self.top_level_window() password = None - while self.wallet.has_password(): + while self.wallet.has_keystore_encryption(): password = self.password_dialog(parent=parent) if password is None: # User cancelled password input @@ -1506,7 +1507,7 @@ def do_send(self, preview = False): if fee > confirm_rate * tx.estimated_size() / 1000: msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high.")) - if self.wallet.has_password(): + if self.wallet.has_keystore_encryption(): msg.append("") msg.append(_("Enter your password to proceed")) password = self.password_dialog('\n'.join(msg)) @@ -1909,17 +1910,37 @@ def update_lock_icon(self): def update_buttons_on_seed(self): self.seed_button.setVisible(self.wallet.has_seed()) - self.password_button.setVisible(self.wallet.can_change_password()) + self.password_button.setVisible(self.wallet.may_have_password()) self.send_button.setVisible(not self.wallet.is_watching_only()) def change_password_dialog(self): - from .password_dialog import ChangePasswordDialog - d = ChangePasswordDialog(self, self.wallet) - ok, password, new_password, encrypt_file = d.run() + from electrum.storage import STO_EV_XPUB_PW + if self.wallet.get_available_storage_encryption_version() == STO_EV_XPUB_PW: + from .password_dialog import ChangePasswordDialogForHW + d = ChangePasswordDialogForHW(self, self.wallet) + ok, encrypt_file = d.run() + if not ok: + return + + try: + hw_dev_pw = self.wallet.keystore.get_password_for_storage_encryption() + except UserCancelled: + return + except BaseException as e: + traceback.print_exc(file=sys.stderr) + self.show_error(str(e)) + return + old_password = hw_dev_pw if self.wallet.has_password() else None + new_password = hw_dev_pw if encrypt_file else None + else: + from .password_dialog import ChangePasswordDialogForSW + d = ChangePasswordDialogForSW(self, self.wallet) + ok, old_password, new_password, encrypt_file = d.run() + if not ok: return try: - self.wallet.update_password(password, new_password, encrypt_file) + self.wallet.update_password(old_password, new_password, encrypt_file) except BaseException as e: self.show_error(str(e)) return @@ -1927,7 +1948,7 @@ def change_password_dialog(self): traceback.print_exc(file=sys.stdout) self.show_error(_('Failed to update password')) return - msg = _('Password was updated successfully') if new_password else _('Password is disabled, this wallet is not protected') + msg = _('Password was updated successfully') if self.wallet.has_password() else _('Password is disabled, this wallet is not protected') self.show_message(msg, title=_("Success")) self.update_lock_icon() diff --git a/gui/qt/password_dialog.py b/gui/qt/password_dialog.py index be4ea6352645..e0da43021c8e 100644 --- a/gui/qt/password_dialog.py +++ b/gui/qt/password_dialog.py @@ -57,7 +57,7 @@ class PasswordLayout(object): titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")] - def __init__(self, wallet, msg, kind, OK_button): + def __init__(self, wallet, msg, kind, OK_button, force_disable_encrypt_cb=False): self.wallet = wallet self.pw = QLineEdit() @@ -126,7 +126,8 @@ def __init__(self, wallet, msg, kind, OK_button): def enable_OK(): ok = self.new_pw.text() == self.conf_pw.text() OK_button.setEnabled(ok) - self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text())) + self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text()) + and not force_disable_encrypt_cb) self.new_pw.textChanged.connect(enable_OK) self.conf_pw.textChanged.connect(enable_OK) @@ -163,11 +164,84 @@ def new_password(self): return pw -class ChangePasswordDialog(WindowModalDialog): +class PasswordLayoutForHW(object): + + def __init__(self, wallet, msg, kind, OK_button): + self.wallet = wallet + + self.kind = kind + self.OK_button = OK_button + + vbox = QVBoxLayout() + label = QLabel(msg + "\n") + label.setWordWrap(True) + + grid = QGridLayout() + grid.setSpacing(8) + grid.setColumnMinimumWidth(0, 150) + grid.setColumnMinimumWidth(1, 100) + grid.setColumnStretch(1,1) + + logo_grid = QGridLayout() + logo_grid.setSpacing(8) + logo_grid.setColumnMinimumWidth(0, 70) + logo_grid.setColumnStretch(1,1) + + logo = QLabel() + logo.setAlignment(Qt.AlignCenter) + + logo_grid.addWidget(logo, 0, 0) + logo_grid.addWidget(label, 0, 1, 1, 2) + vbox.addLayout(logo_grid) + + if wallet and wallet.has_storage_encryption(): + lockfile = ":icons/lock.png" + else: + lockfile = ":icons/unlock.png" + logo.setPixmap(QPixmap(lockfile).scaledToWidth(36)) + + vbox.addLayout(grid) + + self.encrypt_cb = QCheckBox(_('Encrypt wallet file')) + grid.addWidget(self.encrypt_cb, 1, 0, 1, 2) + + self.vbox = vbox + + def title(self): + return _("Toggle Encryption") + + def layout(self): + return self.vbox + + +class ChangePasswordDialogBase(WindowModalDialog): def __init__(self, parent, wallet): WindowModalDialog.__init__(self, parent) - is_encrypted = wallet.storage.is_encrypted() + is_encrypted = wallet.has_storage_encryption() + OK_button = OkButton(self) + + self.create_password_layout(wallet, is_encrypted, OK_button) + + self.setWindowTitle(self.playout.title()) + vbox = QVBoxLayout(self) + vbox.addLayout(self.playout.layout()) + vbox.addStretch(1) + vbox.addLayout(Buttons(CancelButton(self), OK_button)) + self.playout.encrypt_cb.setChecked(is_encrypted) + + def create_password_layout(self, wallet, is_encrypted, OK_button): + raise NotImplementedError() + + +class ChangePasswordDialogForSW(ChangePasswordDialogBase): + + def __init__(self, parent, wallet): + ChangePasswordDialogBase.__init__(self, parent, wallet) + if not wallet.has_password(): + self.playout.encrypt_cb.setChecked(True) + + def create_password_layout(self, wallet, is_encrypted, OK_button): if not wallet.has_password(): msg = _('Your wallet is not protected.') msg += ' ' + _('Use this dialog to add a password to your wallet.') @@ -177,14 +251,9 @@ def __init__(self, parent, wallet): else: msg = _('Your wallet is password protected and encrypted.') msg += ' ' + _('Use this dialog to change your password.') - OK_button = OkButton(self) - self.playout = PasswordLayout(wallet, msg, PW_CHANGE, OK_button) - self.setWindowTitle(self.playout.title()) - vbox = QVBoxLayout(self) - vbox.addLayout(self.playout.layout()) - vbox.addStretch(1) - vbox.addLayout(Buttons(CancelButton(self), OK_button)) - self.playout.encrypt_cb.setChecked(is_encrypted or not wallet.has_password()) + self.playout = PasswordLayout( + wallet, msg, PW_CHANGE, OK_button, + force_disable_encrypt_cb=not wallet.can_have_keystore_encryption()) def run(self): if not self.exec_(): @@ -192,6 +261,26 @@ def run(self): return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked() +class ChangePasswordDialogForHW(ChangePasswordDialogBase): + + def __init__(self, parent, wallet): + ChangePasswordDialogBase.__init__(self, parent, wallet) + + def create_password_layout(self, wallet, is_encrypted, OK_button): + if not is_encrypted: + msg = _('Your wallet file is NOT encrypted.') + else: + msg = _('Your wallet file is encrypted.') + msg += '\n' + _('Note: If you enable this setting, you will need your hardware device to open your wallet.') + msg += '\n' + _('Use this dialog to toggle encryption.') + self.playout = PasswordLayoutForHW(wallet, msg, PW_CHANGE, OK_button) + + def run(self): + if not self.exec_(): + return False, None + return True, self.playout.encrypt_cb.isChecked() + + class PasswordDialog(WindowModalDialog): def __init__(self, parent=None, msg=None): diff --git a/lib/base_wizard.py b/lib/base_wizard.py index 9093061c9a4e..e586f4d1b103 100644 --- a/lib/base_wizard.py +++ b/lib/base_wizard.py @@ -24,12 +24,19 @@ # SOFTWARE. import os +import sys +import traceback + from . import bitcoin from . import keystore from .keystore import bip44_derivation from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types +from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption from .i18n import _ +from .util import UserCancelled +# hardware device setup purpose +HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2) class ScriptTypeNotSupported(Exception): pass @@ -147,17 +154,22 @@ def import_addresses_or_keys(self): is_valid=v, allow_multi=True) def on_import(self, text): + # create a temporary wallet and exploit that modifications + # will be reflected on self.storage if keystore.is_address_list(text): - self.wallet = Imported_Wallet(self.storage) + w = Imported_Wallet(self.storage) for x in text.split(): - self.wallet.import_address(x) + w.import_address(x) elif keystore.is_private_key_list(text): k = keystore.Imported_KeyStore({}) self.storage.put('keystore', k.dump()) - self.wallet = Imported_Wallet(self.storage) + w = Imported_Wallet(self.storage) for x in text.split(): - self.wallet.import_private_key(x, None) - self.terminate() + w.import_private_key(x, None) + self.keystores.append(w.keystore) + else: + return self.terminate() + return self.run('create_wallet') def restore_from_key(self): if self.wallet_type == 'standard': @@ -176,7 +188,7 @@ def on_restore_from_key(self, text): k = keystore.from_master_key(text) self.on_keystore(k) - def choose_hw_device(self): + def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET): title = _('Hardware Keystore') # check available plugins support = self.plugins.get_hardware_support() @@ -185,7 +197,7 @@ def choose_hw_device(self): _('No hardware wallet support found on your system.'), _('Please install the relevant libraries (eg python-trezor for Trezor).'), ]) - self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device()) + self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose)) return # scan devices devices = [] @@ -205,7 +217,7 @@ def choose_hw_device(self): _('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ', _('On Linux, you might have to add a new permission to your udev rules.'), ]) - self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device()) + self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose)) return # select device self.devices = devices @@ -216,23 +228,31 @@ def choose_hw_device(self): descr = "%s [%s, %s]" % (label, name, state) choices.append(((name, info), descr)) msg = _('Select a device') + ':' - self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_device) + self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose)) - def on_device(self, name, device_info): + def on_device(self, name, device_info, *, purpose): self.plugin = self.plugins.get_plugin(name) try: - self.plugin.setup_device(device_info, self) + self.plugin.setup_device(device_info, self, purpose) except BaseException as e: self.show_error(str(e)) - self.choose_hw_device() + self.choose_hw_device(purpose) return - if self.wallet_type=='multisig': - # There is no general standard for HD multisig. - # This is partially compatible with BIP45; assumes index=0 - self.on_hw_derivation(name, device_info, "m/45'/0") + if purpose == HWD_SETUP_NEW_WALLET: + if self.wallet_type=='multisig': + # There is no general standard for HD multisig. + # This is partially compatible with BIP45; assumes index=0 + self.on_hw_derivation(name, device_info, "m/45'/0") + else: + f = lambda x: self.run('on_hw_derivation', name, device_info, str(x)) + self.derivation_dialog(f) + elif purpose == HWD_SETUP_DECRYPT_WALLET: + derivation = get_derivation_used_for_hw_device_encryption() + xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self) + password = keystore.Xpub.get_pubkey_from_xpub(xpub, ()) + self.storage.decrypt(password) else: - f = lambda x: self.run('on_hw_derivation', name, device_info, str(x)) - self.derivation_dialog(f) + raise Exception('unknown purpose: %s' % purpose) def derivation_dialog(self, f): default = bip44_derivation(0, bip43_purpose=44) @@ -365,13 +385,45 @@ def on_keystore(self, k): self.run('create_wallet') def create_wallet(self): - if any(k.may_have_password() for k in self.keystores): - self.request_password(run_next=self.on_password) + encrypt_keystore = any(k.may_have_password() for k in self.keystores) + # note: the following condition ("if") is duplicated logic from + # wallet.get_available_storage_encryption_version() + if self.wallet_type == 'standard' and isinstance(self.keystores[0], keystore.Hardware_KeyStore): + # offer encrypting with a pw derived from the hw device + k = self.keystores[0] + try: + k.handler = self.plugin.create_handler(self) + password = k.get_password_for_storage_encryption() + except UserCancelled: + devmgr = self.plugins.device_manager + devmgr.unpair_xpub(k.xpub) + self.choose_hw_device() + return + except BaseException as e: + traceback.print_exc(file=sys.stderr) + self.show_error(str(e)) + return + self.request_storage_encryption( + run_next=lambda encrypt_storage: self.on_password( + password, + encrypt_storage=encrypt_storage, + storage_enc_version=STO_EV_XPUB_PW, + encrypt_keystore=False)) else: - self.on_password(None, False) - - def on_password(self, password, encrypt): - self.storage.set_password(password, encrypt) + # prompt the user to set an arbitrary password + self.request_password( + run_next=lambda password, encrypt_storage: self.on_password( + password, + encrypt_storage=encrypt_storage, + storage_enc_version=STO_EV_USER_PW, + encrypt_keystore=encrypt_keystore), + force_disable_encrypt_cb=not encrypt_keystore) + + def on_password(self, password, *, encrypt_storage, + storage_enc_version=STO_EV_USER_PW, encrypt_keystore): + self.storage.set_keystore_encryption(bool(password) and encrypt_keystore) + if encrypt_storage: + self.storage.set_password(password, enc_version=storage_enc_version) for k in self.keystores: if k.may_have_password(): k.update_password(None, password) @@ -387,6 +439,13 @@ def on_password(self, password, encrypt): self.storage.write() self.wallet = Multisig_Wallet(self.storage) self.run('create_addresses') + elif self.wallet_type == 'imported': + if len(self.keystores) > 0: + keys = self.keystores[0].dump() + self.storage.put('keystore', keys) + self.wallet = Imported_Wallet(self.storage) + self.wallet.storage.write() + self.terminate() def show_xpub_and_add_cosigners(self, xpub): self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore')) diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 8c4e872615b0..2a788777b602 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -643,8 +643,8 @@ def verify_message(address, sig, message): return False -def encrypt_message(message, pubkey): - return EC_KEY.encrypt_message(message, bfh(pubkey)) +def encrypt_message(message, pubkey, magic=b'BIE1'): + return EC_KEY.encrypt_message(message, bfh(pubkey), magic) def chunks(l, n): @@ -789,7 +789,7 @@ def verify_message(self, sig, message): # ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac @classmethod - def encrypt_message(self, message, pubkey): + def encrypt_message(self, message, pubkey, magic=b'BIE1'): assert_bytes(message) pk = ser_to_point(pubkey) @@ -803,20 +803,20 @@ def encrypt_message(self, message, pubkey): iv, key_e, key_m = key[0:16], key[16:32], key[32:] ciphertext = aes_encrypt_with_iv(key_e, iv, message) ephemeral_pubkey = bfh(ephemeral.get_public_key(compressed=True)) - encrypted = b'BIE1' + ephemeral_pubkey + ciphertext + encrypted = magic + ephemeral_pubkey + ciphertext mac = hmac.new(key_m, encrypted, hashlib.sha256).digest() return base64.b64encode(encrypted + mac) - def decrypt_message(self, encrypted): + def decrypt_message(self, encrypted, magic=b'BIE1'): encrypted = base64.b64decode(encrypted) if len(encrypted) < 85: raise Exception('invalid ciphertext: length') - magic = encrypted[:4] + magic_found = encrypted[:4] ephemeral_pubkey = encrypted[4:37] ciphertext = encrypted[37:-32] mac = encrypted[-32:] - if magic != b'BIE1': + if magic_found != magic: raise Exception('invalid ciphertext: invalid magic bytes') try: ephemeral_pubkey = ser_to_point(ephemeral_pubkey) diff --git a/lib/commands.py b/lib/commands.py index dead8b82b8db..704202eccad3 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -82,7 +82,7 @@ def func_wrapper(*args, **kwargs): password = kwargs.get('password') if c.requires_wallet and wallet is None: raise BaseException("wallet not loaded. Use 'electrum daemon load_wallet'") - if c.requires_password and password is None and wallet.storage.get('use_encryption'): + if c.requires_password and password is None and wallet.has_password(): return {'error': 'Password required' } return func(*args, **kwargs) return func_wrapper diff --git a/lib/keystore.py b/lib/keystore.py index 40a747db0ce5..e3579958a285 100644 --- a/lib/keystore.py +++ b/lib/keystore.py @@ -45,6 +45,10 @@ def is_watching_only(self): def can_import(self): return False + def may_have_password(self): + """Returns whether the keystore can be encrypted with a password.""" + raise NotImplementedError() + def get_tx_derivations(self, tx): keypairs = {} for txin in tx.inputs(): @@ -116,9 +120,6 @@ def __init__(self, d): def is_deterministic(self): return False - def can_change_password(self): - return True - def get_master_public_key(self): return None @@ -196,9 +197,6 @@ def has_seed(self): def is_watching_only(self): return not self.has_seed() - def can_change_password(self): - return not self.is_watching_only() - def add_seed(self, seed): if self.seed: raise Exception("a seed exists") @@ -522,9 +520,13 @@ def is_watching_only(self): assert not self.has_seed() return False - def can_change_password(self): - return False - + def get_password_for_storage_encryption(self): + from .storage import get_derivation_used_for_hw_device_encryption + client = self.plugin.get_client(self) + derivation = get_derivation_used_for_hw_device_encryption() + xpub = client.get_xpub(derivation, "standard") + password = self.get_pubkey_from_xpub(xpub, ()) + return password def bip39_normalize_passphrase(passphrase): diff --git a/lib/storage.py b/lib/storage.py index fa9911149d1e..ada874045bdf 100644 --- a/lib/storage.py +++ b/lib/storage.py @@ -33,7 +33,7 @@ import base64 import zlib -from .util import PrintError, profiler +from .util import PrintError, profiler, InvalidPassword from .plugins import run_hook, plugin_loaders from .keystore import bip44_derivation from . import bitcoin @@ -56,6 +56,13 @@ def multisig_type(wallet_type): match = [int(x) for x in match.group(1, 2)] return match +def get_derivation_used_for_hw_device_encryption(): + return ("m" + "/4541509'" # ascii 'ELE' as decimal ("BIP43 purpose") + "/1112098098'") # ascii 'BIE2' as decimal + +# storage encryption version +STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW = range(0, 3) class WalletStorage(PrintError): @@ -70,9 +77,11 @@ def __init__(self, path, manual_upgrades=False): if self.file_exists(): with open(self.path, "r") as f: self.raw = f.read() + self._encryption_version = self._init_encryption_version() if not self.is_encrypted(): self.load_data(self.raw) else: + self._encryption_version = STO_EV_PLAINTEXT # avoid new wallets getting 'upgraded' self.put('seed_version', FINAL_SEED_VERSION) @@ -106,11 +115,47 @@ def load_data(self, s): if self.requires_upgrade(): self.upgrade() + def is_past_initial_decryption(self): + """Return if storage is in a usable state for normal operations. + + The value is True exactly + if encryption is disabled completely (self.is_encrypted() == False), + or if encryption is enabled but the contents have already been decrypted. + """ + return bool(self.data) + def is_encrypted(self): + """Return if storage encryption is currently enabled.""" + return self.get_encryption_version() != STO_EV_PLAINTEXT + + def is_encrypted_with_user_pw(self): + return self.get_encryption_version() == STO_EV_USER_PW + + def is_encrypted_with_hw_device(self): + return self.get_encryption_version() == STO_EV_XPUB_PW + + def get_encryption_version(self): + """Return the version of encryption used for this storage. + + 0: plaintext / no encryption + + ECIES, private key derived from a password, + 1: password is provided by user + 2: password is derived from an xpub; used with hw wallets + """ + return self._encryption_version + + def _init_encryption_version(self): try: - return base64.b64decode(self.raw)[0:4] == b'BIE1' + magic = base64.b64decode(self.raw)[0:4] + if magic == b'BIE1': + return STO_EV_USER_PW + elif magic == b'BIE2': + return STO_EV_XPUB_PW + else: + return STO_EV_PLAINTEXT except: - return False + return STO_EV_PLAINTEXT def file_exists(self): return self.path and os.path.exists(self.path) @@ -120,20 +165,50 @@ def get_key(self, password): ec_key = bitcoin.EC_KEY(secret) return ec_key + def _get_encryption_magic(self): + v = self._encryption_version + if v == STO_EV_USER_PW: + return b'BIE1' + elif v == STO_EV_XPUB_PW: + return b'BIE2' + else: + raise Exception('no encryption magic for version: %s' % v) + def decrypt(self, password): ec_key = self.get_key(password) - s = zlib.decompress(ec_key.decrypt_message(self.raw)) if self.raw else None + if self.raw: + enc_magic = self._get_encryption_magic() + s = zlib.decompress(ec_key.decrypt_message(self.raw, enc_magic)) + else: + s = None self.pubkey = ec_key.get_public_key() s = s.decode('utf8') self.load_data(s) - def set_password(self, password, encrypt): - self.put('use_encryption', bool(password)) - if encrypt and password: + def check_password(self, password): + """Raises an InvalidPassword exception on invalid password""" + if not self.is_encrypted(): + return + if self.pubkey and self.pubkey != self.get_key(password).get_public_key(): + raise InvalidPassword() + + def set_keystore_encryption(self, enable): + self.put('use_encryption', enable) + + def set_password(self, password, enc_version=None): + """Set a password to be used for encrypting this storage.""" + if enc_version is None: + enc_version = self._encryption_version + if password and enc_version != STO_EV_PLAINTEXT: ec_key = self.get_key(password) self.pubkey = ec_key.get_public_key() + self._encryption_version = enc_version else: self.pubkey = None + self._encryption_version = STO_EV_PLAINTEXT + # make sure next storage.write() saves changes + with self.lock: + self.modified = True def get(self, key, default=None): with self.lock: @@ -175,7 +250,8 @@ def _write(self): if self.pubkey: s = bytes(s, 'utf8') c = zlib.compress(s) - s = bitcoin.encrypt_message(c, self.pubkey) + enc_magic = self._get_encryption_magic() + s = bitcoin.encrypt_message(c, self.pubkey, enc_magic) s = s.decode('utf8') temp_path = "%s.tmp.%s" % (self.path, os.getpid()) diff --git a/lib/wallet.py b/lib/wallet.py index a9dc646bcb40..ce812beb9f24 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -48,7 +48,7 @@ from .bitcoin import * from .version import * from .keystore import load_keystore, Hardware_KeyStore -from .storage import multisig_type +from .storage import multisig_type, STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW from . import transaction from .transaction import Transaction @@ -1359,10 +1359,65 @@ def add_address(self, address): self.synchronizer.add(address) def has_password(self): - return self.storage.get('use_encryption', False) + return self.has_keystore_encryption() or self.has_storage_encryption() + + def can_have_keystore_encryption(self): + return self.keystore and self.keystore.may_have_password() + + def get_available_storage_encryption_version(self): + """Returns the type of storage encryption offered to the user. + + A wallet file (storage) is either encrypted with this version + or is stored in plaintext. + """ + if isinstance(self.keystore, Hardware_KeyStore): + return STO_EV_XPUB_PW + else: + return STO_EV_USER_PW + + def has_keystore_encryption(self): + """Returns whether encryption is enabled for the keystore. + + If True, e.g. signing a transaction will require a password. + """ + if self.can_have_keystore_encryption(): + return self.storage.get('use_encryption', False) + return False + + def has_storage_encryption(self): + """Returns whether encryption is enabled for the wallet file on disk.""" + return self.storage.is_encrypted() + + @classmethod + def may_have_password(cls): + return True def check_password(self, password): - self.keystore.check_password(password) + if self.has_keystore_encryption(): + self.keystore.check_password(password) + self.storage.check_password(password) + + def update_password(self, old_pw, new_pw, encrypt_storage=False): + if old_pw is None and self.has_password(): + raise InvalidPassword() + self.check_password(old_pw) + + if encrypt_storage: + enc_version = self.get_available_storage_encryption_version() + else: + enc_version = STO_EV_PLAINTEXT + self.storage.set_password(new_pw, enc_version) + + # note: Encrypting storage with a hw device is currently only + # allowed for non-multisig wallets. Further, + # Hardware_KeyStore.may_have_password() == False. + # If these were not the case, + # extra care would need to be taken when encrypting keystores. + self._update_password_for_keystore(old_pw, new_pw) + encrypt_keystore = self.can_have_keystore_encryption() + self.storage.set_keystore_encryption(bool(new_pw) and encrypt_keystore) + + self.storage.write() def sign_message(self, address, message, password): index = self.get_address_index(address) @@ -1386,16 +1441,10 @@ def get_keystores(self): def is_watching_only(self): return self.keystore.is_watching_only() - def can_change_password(self): - return self.keystore.can_change_password() - - def update_password(self, old_pw, new_pw, encrypt=False): - if old_pw is None and self.has_password(): - raise InvalidPassword() - self.keystore.update_password(old_pw, new_pw) - self.save_keystore() - self.storage.set_password(new_pw, encrypt) - self.storage.write() + def _update_password_for_keystore(self, old_pw, new_pw): + if self.keystore and self.keystore.may_have_password(): + self.keystore.update_password(old_pw, new_pw) + self.save_keystore() def save_keystore(self): self.storage.put('keystore', self.keystore.dump()) @@ -1434,9 +1483,6 @@ def load_addresses(self): def save_addresses(self): self.storage.put('addresses', self.addresses) - def can_change_password(self): - return not self.is_watching_only() - def can_import_address(self): return self.is_watching_only() @@ -1798,22 +1844,28 @@ def get_keystore(self): def get_keystores(self): return [self.keystores[i] for i in sorted(self.keystores.keys())] - def update_password(self, old_pw, new_pw, encrypt=False): - if old_pw is None and self.has_password(): - raise InvalidPassword() + def can_have_keystore_encryption(self): + return any([k.may_have_password() for k in self.get_keystores()]) + + def _update_password_for_keystore(self, old_pw, new_pw): for name, keystore in self.keystores.items(): - if keystore.can_change_password(): + if keystore.may_have_password(): keystore.update_password(old_pw, new_pw) self.storage.put(name, keystore.dump()) - self.storage.set_password(new_pw, encrypt) - self.storage.write() + + def check_password(self, password): + for name, keystore in self.keystores.items(): + if keystore.may_have_password(): + keystore.check_password(password) + self.storage.check_password(password) + + def get_available_storage_encryption_version(self): + # multisig wallets are not offered hw device encryption + return STO_EV_USER_PW def has_seed(self): return self.keystore.has_seed() - def can_change_password(self): - return self.keystore.can_change_password() - def is_watching_only(self): return not any([not k.is_watching_only() for k in self.get_keystores()]) diff --git a/plugins/cosigner_pool/qt.py b/plugins/cosigner_pool/qt.py index e9ee8ad61b8a..52d6a8f3451b 100644 --- a/plugins/cosigner_pool/qt.py +++ b/plugins/cosigner_pool/qt.py @@ -194,7 +194,7 @@ def on_receive(self, keyhash, message): return wallet = window.wallet - if wallet.has_password(): + if wallet.has_keystore_encryption(): password = window.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease enter your password to decrypt it.') if not password: return diff --git a/plugins/digitalbitbox/digitalbitbox.py b/plugins/digitalbitbox/digitalbitbox.py index 622bba527f98..3d08f40459fb 100644 --- a/plugins/digitalbitbox/digitalbitbox.py +++ b/plugins/digitalbitbox/digitalbitbox.py @@ -12,7 +12,7 @@ from electrum.keystore import Hardware_KeyStore from ..hw_wallet import HW_PluginBase from electrum.util import print_error, to_string, UserCancelled - from electrum.base_wizard import ScriptTypeNotSupported + from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET import time import hid @@ -670,12 +670,13 @@ def create_client(self, device, handler): return None - def setup_device(self, device_info, wizard): + def setup_device(self, device_info, wizard, purpose): devmgr = self.device_manager() device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) client.handler = self.create_handler(wizard) - client.setupRunning = True + if purpose == HWD_SETUP_NEW_WALLET: + client.setupRunning = True client.get_xpub("m/44'/0'", 'standard') diff --git a/plugins/greenaddress_instant/qt.py b/plugins/greenaddress_instant/qt.py index 5a01091ebad7..137390b44ade 100644 --- a/plugins/greenaddress_instant/qt.py +++ b/plugins/greenaddress_instant/qt.py @@ -65,9 +65,14 @@ def do_verify(self, d): tx = d.tx wallet = d.wallet window = d.main_window + + if wallet.is_watching_only(): + d.show_critical(_('This feature is not available for watch-only wallets.')) + return + # 1. get the password and sign the verification request password = None - if wallet.has_password(): + if wallet.has_keystore_encryption(): msg = _('GreenAddress requires your signature \n' 'to verify that transaction is instant.\n' 'Please enter your password to sign a\n' diff --git a/plugins/hw_wallet/plugin.py b/plugins/hw_wallet/plugin.py index 6ed8635f2fe2..34573cf99f08 100644 --- a/plugins/hw_wallet/plugin.py +++ b/plugins/hw_wallet/plugin.py @@ -51,3 +51,10 @@ def close_wallet(self, wallet): for keystore in wallet.get_keystores(): if isinstance(keystore, self.keystore_class): self.device_manager().unpair_xpub(keystore.xpub) + + def setup_device(self, device_info, wizard, purpose): + """Called when creating a new wallet or when using the device to decrypt + an existing wallet. Select the device to use. If the device is + uninitialized, go through the initialization process. + """ + raise NotImplementedError() diff --git a/plugins/hw_wallet/qt.py b/plugins/hw_wallet/qt.py index bd7cc223abd3..e6451157cac9 100644 --- a/plugins/hw_wallet/qt.py +++ b/plugins/hw_wallet/qt.py @@ -70,9 +70,10 @@ def update_status(self, paired): self.status_signal.emit(paired) def _update_status(self, paired): - button = self.button - icon = button.icon_paired if paired else button.icon_unpaired - button.setIcon(QIcon(icon)) + if hasattr(self, 'button'): + button = self.button + icon = button.icon_paired if paired else button.icon_unpaired + button.setIcon(QIcon(icon)) def query_choice(self, msg, labels): self.done.clear() diff --git a/plugins/keepkey/plugin.py b/plugins/keepkey/plugin.py index 057c44e1f80b..a273d6b33214 100644 --- a/plugins/keepkey/plugin.py +++ b/plugins/keepkey/plugin.py @@ -194,10 +194,7 @@ def _initialize_device(self, settings, method, device_id, wizard, handler): label, language) wizard.loop.exit(0) - def setup_device(self, device_info, wizard): - '''Called when creating a new wallet. Select the device to use. If - the device is uninitialized, go through the intialization - process.''' + def setup_device(self, device_info, wizard, purpose): devmgr = self.device_manager() device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index 908af27853c3..7d8866b4df57 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -522,7 +522,7 @@ def create_client(self, device, handler): client = Ledger_Client(client) return client - def setup_device(self, device_info, wizard): + def setup_device(self, device_info, wizard, purpose): devmgr = self.device_manager() device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py index c1dde4817f0b..19cdc026852a 100644 --- a/plugins/trezor/plugin.py +++ b/plugins/trezor/plugin.py @@ -214,10 +214,7 @@ def _initialize_device(self, settings, method, device_id, wizard, handler): label, language) wizard.loop.exit(0) - def setup_device(self, device_info, wizard): - '''Called when creating a new wallet. Select the device to use. If - the device is uninitialized, go through the intialization - process.''' + def setup_device(self, device_info, wizard, purpose): devmgr = self.device_manager() device_id = device_info.device.id_ client = devmgr.client_by_id(device_id) diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py index a8f808224936..fda0351f183b 100644 --- a/plugins/trustedcoin/trustedcoin.py +++ b/plugins/trustedcoin/trustedcoin.py @@ -40,6 +40,7 @@ from electrum.i18n import _ from electrum.plugins import BasePlugin, hook from electrum.util import NotEnoughFunds +from electrum.storage import STO_EV_USER_PW # signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server signing_xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" @@ -420,9 +421,11 @@ def create_keystore(self, wizard, seed, passphrase): k2 = keystore.from_xpub(xpub2) wizard.request_password(run_next=lambda pw, encrypt: self.on_password(wizard, pw, encrypt, k1, k2)) - def on_password(self, wizard, password, encrypt, k1, k2): + def on_password(self, wizard, password, encrypt_storage, k1, k2): k1.update_password(None, password) - wizard.storage.set_password(password, encrypt) + wizard.storage.set_keystore_encryption(bool(password)) + if encrypt_storage: + wizard.storage.set_password(password, enc_version=STO_EV_USER_PW) wizard.storage.put('x1/', k1.dump()) wizard.storage.put('x2/', k2.dump()) wizard.storage.write() @@ -470,7 +473,7 @@ def on_choice(self, wizard, seed, passphrase, x): else: self.create_keystore(wizard, seed, passphrase) - def on_restore_pw(self, wizard, seed, passphrase, password, encrypt): + def on_restore_pw(self, wizard, seed, passphrase, password, encrypt_storage): storage = wizard.storage xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) k1 = keystore.from_xprv(xprv1) @@ -484,7 +487,11 @@ def on_restore_pw(self, wizard, seed, passphrase, password, encrypt): xpub3 = make_xpub(signing_xpub, long_user_id) k3 = keystore.from_xpub(xpub3) storage.put('x3/', k3.dump()) - storage.set_password(password, encrypt) + + storage.set_keystore_encryption(bool(password)) + if encrypt_storage: + storage.set_password(password, enc_version=STO_EV_USER_PW) + wizard.wallet = Wallet_2fa(storage) wizard.create_addresses() From f0e7ae20f2197fa8224d5cbf76e29e32e52a7942 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 28 Jan 2018 22:20:27 +0100 Subject: [PATCH 037/270] fix #3790 --- lib/wallet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index a9dc646bcb40..8366e8c62c3c 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -367,7 +367,8 @@ def get_public_keys(self, address): def add_unverified_tx(self, tx_hash, tx_height): if tx_height == 0 and tx_hash in self.verified_tx: self.verified_tx.pop(tx_hash) - self.verifier.merkle_roots.pop(tx_hash, None) + if self.verifier: + self.verifier.merkle_roots.pop(tx_hash, None) # tx will be verified only if height > 0 if tx_hash not in self.verified_tx: From 8676e870f3ccccd2837f3a72ac59b1b22697df26 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Sun, 28 Jan 2018 22:56:33 +0100 Subject: [PATCH 038/270] Raise exception if transaction is not related to wallet --- gui/qt/history_list.py | 11 ++++++++--- lib/commands.py | 1 - lib/wallet.py | 12 ++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index d270a1af1fc4..8d6b3123b19a 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -25,6 +25,7 @@ import webbrowser +from electrum.wallet import UnrelatedTransactionException from .util import * from electrum.i18n import _ from electrum.util import block_explorer_URL @@ -211,6 +212,10 @@ def remove_local_tx(self, delete_tx): def onFileAdded(self, fn): with open(fn) as f: tx = self.parent.tx_from_text(f.read()) - self.wallet.add_transaction(tx.txid(), tx) - self.wallet.save_transactions(write=True) - self.on_update() + try: + self.wallet.add_transaction(tx.txid(), tx) + except UnrelatedTransactionException as e: + self.parent.show_error(e) + else: + self.wallet.save_transactions(write=True) + self.on_update() diff --git a/lib/commands.py b/lib/commands.py index c52b4637f77d..c2b3d4808350 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -632,7 +632,6 @@ def addrequest(self, amount, memo='', expiration=None, force=False): @command('w') def addtransaction(self, tx): """ Add a transaction to the wallet history """ - #fixme: we should ensure that tx is related to wallet tx = Transaction(tx) self.wallet.add_transaction(tx.txid(), tx) self.wallet.save_transactions() diff --git a/lib/wallet.py b/lib/wallet.py index ae2c2a74565a..62b096eda4e7 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -154,6 +154,11 @@ 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 Abstract_Wallet(PrintError): """ Wallet classes are created to handle various address generation methods. @@ -674,6 +679,7 @@ def find_pay_to_pubkey_address(self, prevout_hash, prevout_n): def add_transaction(self, tx_hash, tx): is_coinbase = tx.inputs()[0]['type'] == 'coinbase' + related = False with self.transaction_lock: # add inputs self.txi[tx_hash] = d = {} @@ -687,6 +693,7 @@ def add_transaction(self, tx_hash, tx): 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: @@ -709,6 +716,7 @@ def add_transaction(self, tx_hash, tx): else: addr = None if addr and self.is_mine(addr): + related = True if d.get(addr) is None: d[addr] = [] d[addr].append((n, v, is_coinbase)) @@ -720,6 +728,10 @@ def add_transaction(self, tx_hash, tx): if dd.get(addr) is None: dd[addr] = [] dd[addr].append((ser, v)) + + if not related: + raise UnrelatedTransactionException() + # save self.transactions[tx_hash] = tx From 2a1e5238c85da0bd4b45c04fb7d8312505a10923 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Sun, 28 Jan 2018 23:11:43 +0100 Subject: [PATCH 039/270] Allow user to save transaction from dialog --- gui/qt/transaction_dialog.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index c93fee15b5e6..6be77736c7bb 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -35,6 +35,8 @@ from electrum.plugins import run_hook from electrum.util import bfh +from electrum.wallet import UnrelatedTransactionException + from .util import * dialogs = [] # Otherwise python randomly garbage collects the dialogs... @@ -98,8 +100,13 @@ def __init__(self, tx, parent, desc, prompt_if_unsaved): self.broadcast_button = b = QPushButton(_("Broadcast")) b.clicked.connect(self.do_broadcast) - self.save_button = b = QPushButton(_("Save")) - b.clicked.connect(self.save) + self.save_button = QPushButton(_("Save")) + self.save_button.setDisabled(True) + self.save_button.setToolTip(_("Please sign this transaction in order to save it")) + self.save_button.clicked.connect(self.save) + + self.export_button = b = QPushButton(_("Export")) + b.clicked.connect(self.export) self.cancel_button = b = QPushButton(_("Close")) b.clicked.connect(self.close) @@ -112,9 +119,9 @@ def __init__(self, tx, parent, desc, prompt_if_unsaved): self.copy_button = CopyButton(lambda: str(self.tx), parent.app) # Action buttons - self.buttons = [self.sign_button, self.broadcast_button, self.cancel_button] + self.buttons = [self.sign_button, self.broadcast_button, self.save_button, self.cancel_button] # Transaction sharing buttons - self.sharing_buttons = [self.copy_button, self.qr_button, self.save_button] + self.sharing_buttons = [self.copy_button, self.qr_button, self.export_button] run_hook('transaction_dialog', self) @@ -155,6 +162,8 @@ def sign_done(success): if success: self.prompt_if_unsaved = True self.saved = False + self.save_button.setDisabled(False) + self.save_button.setToolTip("") self.update() self.main_window.pop_top_level_window(self) @@ -163,12 +172,23 @@ def sign_done(success): self.main_window.sign_tx(self.tx, sign_done) def save(self): + self.wallet.add_transaction(self.tx.txid(), self.tx) + self.wallet.save_transactions(write=True) + + self.main_window.history_list.update() + + self.save_button.setDisabled(True) + self.show_message(_("Transaction saved successfully")) + self.saved = True + + + def export(self): name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn' fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn") if fileName: with open(fileName, "w+") as f: f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n') - self.show_message(_("Transaction saved successfully")) + self.show_message(_("Transaction exported successfully")) self.saved = True def update(self): From 70aa1f1db9ccb3954501849f9c872ab7053c8ff1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Jan 2018 02:00:43 +0100 Subject: [PATCH 040/270] fix #3788 --- lib/tests/test_transaction.py | 4 ++++ lib/transaction.py | 44 +++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/lib/tests/test_transaction.py b/lib/tests/test_transaction.py index 99c6d001b5c8..609006cdd000 100644 --- a/lib/tests/test_transaction.py +++ b/lib/tests/test_transaction.py @@ -231,6 +231,10 @@ def test_txid_p2wpkh_to_p2wpkh(self): tx = transaction.Transaction('010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000') self.assertEqual('51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e', tx.txid()) + def test_txid_input_p2wsh_p2sh_not_multisig(self): + tx = transaction.Transaction('0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000') + self.assertEqual('e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d', tx.txid()) + class NetworkMock(object): diff --git a/lib/transaction.py b/lib/transaction.py index 3fd17b2b36fd..281151a71fde 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -45,6 +45,14 @@ class SerializationError(Exception): """ Thrown when there's a problem deserializing or serializing """ +class UnknownTxinType(Exception): + pass + + +class NotRecognizedRedeemScript(Exception): + pass + + class BCDataStream(object): def __init__(self): self.input = None @@ -302,8 +310,14 @@ def parse_scriptSig(d, _bytes): if match_decoded(decoded, match): item = decoded[0][1] if item[0] == 0: + # segwit embedded into p2sh d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item)) - d['type'] = 'p2wpkh-p2sh' if len(item) == 22 else 'p2wsh-p2sh' + if len(item) == 22: + d['type'] = 'p2wpkh-p2sh' + elif len(item) == 34: + d['type'] = 'p2wsh-p2sh' + else: + print_error("unrecognized txin type", bh2u(item)) else: # payto_pubkey d['type'] = 'p2pk' @@ -361,7 +375,7 @@ def parse_redeemScript(s): 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)) - return + raise NotRecognizedRedeemScript() x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys] redeemScript = multisig_script(pubkeys, m) @@ -431,20 +445,29 @@ def parse_witness(vds, txin): txin['value'] = vds.read_uint64() n = vds.read_compact_size() w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n)) + + add_w = lambda x: var_int(len(x) // 2) + x + txin['witness'] = var_int(n) + ''.join(add_w(i) for i in w) + if txin['type'] == 'coinbase': pass elif 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]) - m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1])) txin['num_sig'] = m txin['x_pubkeys'] = x_pubkeys txin['pubkeys'] = pubkeys txin['witnessScript'] = witnessScript - else: + elif 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() def parse_output(vds, i): d = {} @@ -474,7 +497,12 @@ def deserialize(raw): if is_segwit: for i in range(n_vin): txin = d['inputs'][i] - parse_witness(vds, txin) + 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: @@ -674,7 +702,9 @@ def serialize_witness(self, txin, estimate_size=False): witness_script = multisig_script(pubkeys, txin['num_sig']) witness = var_int(n) + '00' + ''.join(add_w(x) for x in sig_list) + add_w(witness_script) else: - raise BaseException('wrong txin type') + witness = txin.get('witness', None) + if not witness: + raise BaseException('wrong txin type:', txin['type']) if self.is_txin_complete(txin) or estimate_size: value_field = '' else: @@ -683,7 +713,7 @@ def serialize_witness(self, txin, estimate_size=False): @classmethod def is_segwit_input(cls, txin): - return cls.is_segwit_inputtype(txin['type']) + return cls.is_segwit_inputtype(txin['type']) or bool(txin.get('witness', False)) @classmethod def is_segwit_inputtype(cls, txin_type): From c7b4ef82bcfceabddaf59eacb742120af05502b8 Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Tue, 23 Jan 2018 13:15:53 -0400 Subject: [PATCH 041/270] Fix PNG file Avoids libpng warning: iCCP: known incorrect sRGB profile --- icons/unpaid.png | Bin 27679 -> 23805 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/icons/unpaid.png b/icons/unpaid.png index e0f3639df66d088a384073442d6451b0f34ecfaa..579ec4eb533866a6d97ee922e15257446649c66f 100644 GIT binary patch literal 23805 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
8K2Dws_YGPuXD)jzB_2(rO zZ9^N;Rz|jI)nn5VlJpiVSWqmfSnm)?6wazfKmhs=HAu}41c(GoXEmTS-xPit2^?k}^YB zfj&c85uc>M8rmu7^9AC-P9j&Se6hsR&ZR-FaW`v&ptDYq!tJ4e~{^) z6BQCE6tUt_c^UJ|ou|0cd5SAjNiOd#hC$R&HG1il984{RV3uT4$}b78R6>P8&p=8g zt3K}0#`GZ7pbaGzZN#MDHFAJxyR-LAOJ0JcR8XRbKEk%btifQXU&DSDUKeu8$`ea0 z)s`$XG|wzcr2qf2_oT4CVXw;{YUZLlhN`i$XOCnk_HjqN8FgXR8Qb{Q9 z@uiYdp^-i!y4N_V9&~M?Hd$p!m1)}wQW-fx?atVEVv#B8wF?n<4EC_DzpzUEe%i+H z#1cy?ZKI%*9;uTR_!GluCN?q>&c#74h(&%hIXnP@Q2}nL#-{!^dW`b6wCK?u(Uh^} zVHQM#Ajaj@VmGH2`vgJMfhp{+u>rw+PFEno5>#Yyk}MV3%7RJ75Gk2V69PiH>*HGE zgc_T|o*Ee4v%Pn<0c}B>&^ELYZAF`{k|Xq;g3y;~jqZJw9lVpmNtH#MJv0=zulfi> z+-H3No#2ihp`LlLPtWb(k?qBt8aDmt1A#c(G_ znu!DC7Q1zGI)Zk_Z-SmWgqAUo=wX9a6TJ$!T^%lEk7R9;x1;amsT~>sXUdgtX z?KB^n7@tV61d=5a?b%X@SJJ){lgJbjYJ3nvcszGQTgpo*Z%++$s?U>3Z9rSlCbSK0 zL|f5jv|XVB(B|%#`A`V}U6u7P@Z?j?Ow}r#@yer*2o|@&#wSU!2`NZ6v4|vg z9mN*{JeNeoi7Ci5AxS!o4{WH%gSb?v_Pl{^rBsf+H*chK&z4kb1KNT%v2CL^DzKzV zH*4f&udk^LUO7j1$c{ZVGuxb`5}yaEo-E-9a_S%sa|fvg$e1DlZM;AdH>rk!@WT>I2iqj*09AD2pZ%&}Tnyd=X{{LYTr@5$RRJj?0U#RN&lO`S)a>1tn>4jy zh-XEvrAV$j1GVTv5jM`%I*8DR9-~){q3tz1@EphFCaTC#iiITeL`x={>d>!zEZ;nF zc_q{R@a;^$LbGz+I!UfpR&qTh3HqxVXN4!13q{0( zs6I)slgo-omR`jXY?PaE9TF@gSxB@bvaxE$yv`Nvf>K>#Ii_)J8#79)YLLAdCr~Jn zLL!A^3T>z{IYkm`c%pI6J!~MQx;lFbBfV8bD%ym$St(MjQLuTShRpV;5Zh?OJuEH) z$?yViWkPcn@8Z@&A^tP;6i zBSS*#ACA$?p&zdeLJ{`_5&EX-MMiyxKzj9Zw$}qm>XEqDP&ecH5f`~19Z8Cmk}M#a zEfxMmv~tFr_7zRDij~bU*3HpcK#ypt7Q3Rx6Aq8(B@!uxWU?6TBIl2|Yw{8*RC^CN zTdG$er8)#gdei7ZqfH>y3bj$)f2Q^X$yPY;yz}hzEgUQI*wH5BT=_MVLXg^wL9afX zBZBQi*kH4WrGsoT;;szLh!rlU?K$aqdV1RPYET;Ia$hBGq z{Rbou=2kR=^yGbYNfbo2RRU|fu|;QhdDUo>1-_giwI@I z2t5KR6{5NeFhnKQ?xIEA%FgY^)Hi+HTD z6jQ45CczR*#Mo?zQ2UBd3;cRJo?O(dBpHwtSELt`ih54e&ICv;{$HrU>NUEWxmrf! zfq(2_i;R4Ds#^ za}zY$gc2;81;liITPvbCk_}2EnhjeFcW{Jbso^;-@)L=rmCVdz*+r6QMA7foBok>+ zcJXBD?XIy$6heqgs5a3(ac|rs_sTtU?<@3xtyNo$Jw7xE$N7R+9Ac>amrxzuE^9%zKc%12x$%F!l$8L~|-?a88_X ztYbZ=X$oSEjuxGY6KHTw(eE9R?6E^Kd3D#j$kX!@%6fZki;Q!t_L8L3N`aW)lp{2U zFiCy!&~EMvt&$|UK4nhLCKKzr>#i#oD8Lp|ajuwBFltFXu4kar1NRkjp%zKC;U6Ks0OXJQNU4U^%?m4f7I+HA3qz@_C#*o3HSg@M zt$n2OtYMx^sJhWTBBi+Jc^67jX)^FdNuMA4N#^V^>w)!lo~0+7Bb*tSWoFe2 zh^19yN1Rc~uEA)@(=XV+uk+}7yJLF0skE-qML0P{9x&vZ;E>zemh^5GGOp7|j}bk?Q=E#u*p>H&Tzd+&1pz z?JqCE8b^k2TurvlEDBY0bfv<(L=t3G64~skW#lN;Wr0uYg@T^h2_%Xi zuS=A9V9ymNQ=sm+w#I>kx;K(g%bnU=BDMFHR7luus>!9o>zUwTo1wMlvr$yz$1d5^NaBU>kj z&QXjEmw$b)Of#!iL@X9bXPs-7C1&+lYaexWv8K+!?>jgZ=R7-mu9g9rnxw}AoLr+< zc@Sz9gi`Uk_q2%npuA{V;YZKE(0n=lHQVVOWT*O0XK5N%ktpxiXVMTHJ0Mro#~*)e8;`ilk3arsR-?lH4ku_*3&t)w=!slsAwYuB#bB8xPU^$-y2 z$*q!Dl3LhZw~A29Lvo=SvAujXUP2P6Mh2`Q);!IqhGc*lyWq<&zqD~0nv=%p*+oWvVa;fG$a<1@yHerzMT;EREDEnO<75j6r$OFTBFPW+GELt@ zDw!rbY-d{TvaNaR>Rhv?P4vBmCr~qYjK=9jDvop1i)!CWa1=kTLpKfMq7;I%>pcU@JppY0 zEi7WV$HOiKEOIgQKyVM~g^nj_5F?)O1EP8E+_|=4kz5cC+iST~kKhN9Z<>ZJ`tQBJ z?VLdx-GAkXC`>`-1X2uDi9u{7S?xfY-5W^}Yn#tUB}iHE_SJ=Z@7`y{)(}W9^0`orNG*nf zkYZ#*Kqe#=Bx4yh8X0oY;Hbw!J=u_p&RDAYP@gD7@#s0{nfohjYo*vQ4G~R~M3{Sl z)XnwL3L2|w#%u>K_lCbG+ue=~bsXwOSRsj1STDlzdy77g?{|;;?cI^B1PBu#l~(CI z7~)@F5ve+)Y9G7KbegSk@oI}!=KBvnir)jOk7Z1lP|Cdm?jK??70Y)x^{ckjD=ft3 zh_&6;POO-0tDS^Lv9UsU6-iDPxk9K$YC$j*gzbe9&kzKS%zKChh}i%1(@%B`%v&66 z7s)<*yJs&m`iyhT{W%qEwZZzssI zq7Vz=1AlMaC*U(o0@9Lvhf`f|OT9=n4~R8mt5tmdCK#S0UL#5UzTgN1 z!*k@RrIAsEkZhq+Z+NmH7yl!{3N+n}PFn9v_2~W2dg}kD-1|v4-D-v$ag4b?Bgd{v z-%A)GwK-m7o2ucXw~vdNkClQl@87BbeOb(WtGW|ETpLNcU#dn1He_6MO(cOn|4+=! zR$T3Ud2!PfzP!f*g`PW37xjac~$YzeRbjwas*~S_PBYq zX*2W0#h01Enyvl7u6vpLwXmlr1QLb?s8~hz0SFR{2*Z0+Sx>hg`}auCHodpq#SGZ* zU^D8J)6H}8Sl{pWpjr6Fn`Wu5Y3=%mO8n(LMLT!^e*XDq`;c(m{@VE;W1a6%{S4d_ zP{uUi!SfwXwr6>+{N-Or#&89Dq17LbSd1@fyOS_jp5uC4=n#+#qOsZ-HU$!lnIw=5 zf)V?oBPb_LM?fe#B?JaAo}?w2Qli%uQ-ExVq{Vk4?EL4RH*-X~4;~(5-s;-jyw<9% zd8KI!^HRNr=H*6B&GZ(n%^Mv$nfLniGc!kyHuIjDXclTX>Ss9^g}ROn+;=CLD2N7` zxK1fvHvt*MLSzjJ% z1QJO?a$KYmWDc(s5f@hnoge*OA# zBNg^5HP8ZL%@lI}(8MAnA@Xdm_dP_Y6(PAi!B7R*U#y$R1OOTy`->qWLKVykB3h3W zfH)ZRkciHsb3Kp@Y$2KNSoY;N5W*AALPYs30RQ{%Jc0NfB=YCtxXI`8=L^rp|9E{Q zKr&CjS4QN)p}s2v%q;HiO#~h;GDE|GVrJcxS74MyxRM5;Yi~7!(3?Xn_9JmnBsxZt z>v5zPjmg#nl0h&&$ABgEmQFxbKrlcQpFk_aLO$l@B?$!ZgadSRD){e9LRo(6N#xIu zq=HP(YtaQ}$nbD*{g4Xj!~?_1YK*CJpFB|h-g&5yTI65(&S$-&@>mR$IARj1-V^fv zG{jQF+5gp$NU+O9q}OOr0GT{!Xm5*=HNm_%UzZ#}FbcF1wqX7o8LLF}=~y}%iNjM0 z7#iV!FMT!^UdllpyefZ8Z=@44cvBuBaXF^PA(gl`w!T6!WubUbxHqH|4^Vi&fT0%Q zD^-e_9Fli-; z{lrWm$b}@J69Ml@I3(MW*+NA3@q%#kAsmgosV+JM!mC3XVJne#&BRa=8&PeL2+0JI zxDISZ@}DSkjZyAZJ_Jg2kGl7t6zA)zw3gkKE|aeQ8y}JZPYz>$W2z=IOvYN@DJEmw zwb`l3e*j|H)ef}HS4Qg9r<`)iOWJp>dwLV#r5YsDes<87F*UoGl(H%4SPXvO$&DH{ zvg9M2K^l)QI!3iSqzf~OnD~J-@M+YjFfw3?E0OZ!OJ<^ek&YZ62?Ts;YhRF`&!VI6>-^}Q9Um*F*Pnq{_PsvK)$fnh{vGxH zJz+Jfuq1?p3!Q_Ics%g{0;0Ou0zTsyAz4#Nq2kW%fA6qB7^C0)SHIbXdls=`B35i8 zh{bC!LCGDl`DG*%!Oxe4D}~HhIllf(#InCyGi=|d%gdyG@=zpyppi7 z$8`8rB>YI*&N{{g+U^ow|7H-&zjhX`5H#u(dqJ2Zd?|b(eCWph>I)~tw$SU(N-PUW z7Q4Svp?6eAU7+t831ju$Y++Ri5~Mt_Ug!Ky+o{_2(6*{@2==kO>DEV(=-hlJKuf3A2;`TCNpyn_St*w zwbn!_D@vgt5+Z^?AQTyCaaG`+_TL2$3;gAWaM=fL2+qL_)>K2gc&4Fd|1IuE8CW{frMOij1Ck+528( zx6=J^JpJCXDtJFGJ&{KaJ`fuu>=Q3q3KEut z2+Yl(Rs$74gN!H5Ob4W_sH#I$YKboy_76yU#-2La@8CjYL1NmSB9o~OZ zT|iqKA{Bglh_P%$6K(+&oUUu#IRCdc5`{lHe*HQ+JY16<64f)A(DHk?8ZZKDz1ctc z^S?ga?e^|d2C*9iNkKpE4ouz3mk`e;A%vQ49w$h>wjq7Ik}Q%B$(VL%(_TLxfz9%x38<;`e%h8}~yMy;1K%na`r~V~c zc&I?@kiA*Yk4vF9sRAlcpruTb3kYN=PRXo3(Ihkq4+4o71ku-v5Z(6UGlF4>`(ZZw zkzS3tLq#cv21Lr|U`Rjain2NjSkO*5S#{sLTTz?Wh9IXwN3_ zdHuBAK?u+yWAIoeWb2U-rqS9I@$h(=VUJ|}($JVOl%(U4*ec|5iCofbs?qAC8qy@! z0ZYj2uphgveCFB;dj`A;rp^`t5=FHvMvJ!9$vgcfz2?b)g z3s2^%tkKpJ1jR;K!N2k0IEL(5nMVdi8+aHIlFcR?$g7d7MhNRQtD(B0?#yNzJfSgz zB)|yV(4L~CCZ9y4wdJ)H*QCoyi77cT))BrSK!vId(2ysW%QaJXVmuAuo3gV-XiGCv ztY9W!v7mpC6dU?Ni5VrvPSf!NQl_C;eF1Ypa=}rR;*_x>n^T&QB6)n>+^!LhH-(Xu zVJz%0_i*r#{<^o*HqZ0_)%@2R&~Nwmf#5ei1P^6B70kPo;jR|AxBt zL`Dh62^o#Fq9w@2hr_ob@WijnkS`@zj3F3RXZ^xGjSByYq zMIo;khb)Iylj=>1r4ZafhRb7)yCT8~ZCUz_fAlu?CEY$9Q7NZ<2b+byU1eH&ns$|R zl}eS=Sr2#Zd0B30k3^TwOAxBas4R^>jg(cw3{38c+Ap=OGQ=`m1%a=3wak@vB}9c? z;+}nuWfvKFy@r8#@=j&j-aRMJ#C|Mq>TeFuWp`4@+OSQ?l&~AfGx+?&TrN)hBrKyF z|5U^b28G2+Oj1qK_d*jp=J|`3CF3N~Vr@qB4=4{DMjS?ps9~wesO1#?C|qQzDs+_6 zmGa5+$&XG?P5+!;$>howU|VHB$v(+m&E9D<*7ee5Y%OnXw~%Q()GgJyXzg|k)8*AE z(bmv4Xp*eSt)W`!s_81jDC(MC{zvGNq-Z& zxx6WdIZ7ChIznkGess&D$ZutT&BM*|Aa!6D_}ar3l9c$vMYNJkA-UD+kJYiI$7Hod zSmXP!V@w;%r&PFcyQUx2Fgi?#t)ivB)pcoC>ERl!EVL~9rlTf!)2i|qrq8FECN_S2 z$+^t`IL92o+)HXL376AiBGEqXskY^9;KVF=fRt>$l!Zl!lvR;oJJO0V_r zq4rU*3o`7V2<~rzuscK2Ks*ZbJKJ16H&D`M(b;J+c@EDd62ys$uEpNKOUDUVquum6 zB8qUD=*!$pX|AXm>M);@L5-q=%lRnO0@NEE1#0VR$I>#%f`Gbe!V1Hg6zb^b z-uK)~PbL&@6pgr?k$nnhaqq*^2ZWu6Pbgp4Jz~FC@4+p8_8_q$VX+m>y3Uf!R?B+P zGu7)UuUdH#OJKpZRIic;jd5_YuCod|EcI9+s)pqFH zbzJ!Lw0yUGI3>#aGP8Bt#>eSl_~C8`cgx?E=51!O>z60ZMNPL- zx6!TexyUcEt%zkIz4y(v%gq2=uxG)|{HD|cY*@(bd*WRJnixz%X2N7RO88kGk03Aa zzL4wN>1&zTtobZ;9^U)U_sLp3To(Vw<+Ih9`HcC(^y~EGJ_p~Dw?E)Bg1U{7fss$W z<6azWhPRq8=l!0Wo+Ot=J7`LWzN7)PAJ$I|cWQHGFUbVS$sf>A88@$H984Pk-Xt}X zR#gCje5gR6zz`7V=>xc*fE$1Fx0so(tB@=)*oa z)(@dKEfSK<24~Lp##Sra)5F7x<|OkXPf_FK^}T#2rb zOL>c=Y}CrvM|tu@(&TaY0!2F#Jkg;bXk!RfIt!3X0c{B*B&so#PR(62I(b=(qEyj* zcMAJsLbwU^DEPx6aC}_m&)HeX+pW=}t{Fb;lW!jIFarY;SvulTg!h=0EHC~~#72{i z%Ccmk;8q6^@}RJ+j9@-Z^0=~|9`J8_?wAzzwG>rpIy&5#ewSWiF}t{NmdpA;Z{g$v zo*J;KF~;0q2PR=A^h8xjkeXc$zc5Z9F)M5=s0duJ(mCmpBWO&FfA1JTHyU7T{KN8@F>Q|mNV=J5&A>tfrbQjK6N=(S+d1i&<*mZGN&UtbbujBmEvkRiMT{uU7}% zGtN1fSNKP=D}L5D;42j6N$SzPD6SZE&#GEhzTwo(vc|PKm~C*!AE3t!OH(HUTfP5} z=bu;3^Ro{jAtCUJIC-M68!_BQYY5oH)HE1a{e@G`4*mGkQ=8JVGMTbP5mM7E>Tr`O z=Ouyr&El4AXkQQy@?X$9%vxCsb1!0bzCb~-3BKouX+s-t{LRqd@oK>0v`EM*7AQK$?rdKHF**XVJsyHaZ~ITA;t)8P&UmXl<~ZCd6gb7A8*wyAY?S0@GBjz!nn|15O*T38(H^ z^3-C?=}7WUWteqx7iCNLAv=}D3cNT8)+LCfToNpC$dqSY?1T9Dw+TM6QgcBnH z?ESZI-}F7VzHPeA3fS-T=BB0L$}b}0+jx0p6c&=f!og)`WWW=92u0t#m21fg-4M)v z)QFRd=;;wTI5;%7wJol#Ns}kSvm;Bz6YtGSeLjk!$=luRaDyj{lq|A%p$!$b%f%FZ z%{}1BZ(+3itzm2cU5`B2KGAxv1bWbL8*=L)zB!panc=%Ue1EyL8VpAP0tiTr73$Ib z{UV)RT?F{}K%6Ku;)X!IA=+rNAlSdN{hK~aqLRxH3J?TV10p$BC?Ie2b3jhSXa9`B z2(&I6$jK}+GwYgJV9BjyD_CUcV(K8P5bKLmWXOg-)0S*sB4wr~ZvB*q@mkE@9`bwG z8m-i*LKg*saT+)$Rx>$;ZEflQ{{3T5^BNbv-eD0x^?&rv7ieMLLK6x|99*4AQ3XbM zJe`dvUIDCMM(Oz48vZQ*4YJY4^D3|Nt}tnWI)M^?_`ir&hs*Cc+DG6{s7JNkKemGE za&kyc{x{iAEk-X~X-s;s!t|;8uTM8Qf__=Oy@JuhreI$@=;Re^x_(x$6~rh+i7;KL zsxl+FD{rVnx*--MGL!?p^<=O%agYxOg@HgI+Q!^kEhYfhK58O2F-fkC)cd3 zta`>6Ame-rcy;N$Z>e*++7&?_1UFVs)MY{nLcNBW;CC2#7)#tdy-J0me}*OBBbhvM z9WrIR=y@>Q{ul3AZ!!qmyi&Yq^Ixv;_=^wZ3fdB8MES4CMBzVp1DO9MjX&;zcTwQc~WJdl6430q?*j@QU$&s*5F`*ZmmcoZd-S z{q$x5v*J^8caoy&shQPd+Ay}PWi);Hwtd;)u)z(~+2rxLU%$rZi=+q$2@!pxbRdp0 z*-4@TRmUpm4zJ)PmC&v?y>G8*P3a}nk0PNk5(1NCB1gAgRFJ0NrPIrm2~(irgGC`+ zy~j>SbHo2Ozvvr^;6l+DhFDiiG3f^CcH(n6eX1;kAc+ZbFhVyMjG;=F(X7=U@BDUV zZDT`*88x@HHLcZqjnn%t&gsXrrB#L$*-!@hK3b~kM}}fq0B>OGQK&STupZV=q30$}^>xhJ1)PJjpk`T6X-*cX%q99sv0XVgxDY} zH>?hfH$`-lGZ~{!XpaxhOt<%M>m{yi4ax-Bz~!Z-|F(Li&PWM>9lCb@&x`rjPqP7e zZ!ZsKIxEQcknh~dWbzK2*@SsB66((k`Mnv5Dfh)qbiS+4U>4R{?5pv<10p|yqffG68VaDOj@ z)@0ADh7d%1m3-t7{QX~uU+MMT=`F3UCXbu*_`mYJJX~7Y+D1!Ls%{-3>_H;3zC1?~ z^M*g|t5h%XOuJ~e!>2KYl}bueis0{BnGlP5m(D3qI`zb8tVPTTjx^#Bfzz*}u2_@JE%@#1i8r>TO}B zj6naOY4;j1KSfgZ*RTr9LO`lhnL#WB-Zmd5lDLGAQiVgb86}(DR694=3 zIxQlG!bVBjI!CjzL!QhGoG!Aag@{h_M{600_-}nxcVPOdUHvD*rUt0Vzz5f@@Au*% zc;ArF0A#PmUyL-0IlE6M`827r(-W%eoFaC3%CnbbuFudY)YZC2jtX(46pkvLxTMH< z$`{n&xmxl-V|tb`K1jMk?oyc{1GSS72Eut_KBB#;26M(%5K7Y1atu-L#b)}R(3!PL86!wfpM#T)c@Ftf^m^bIG-hiyq z5;Y*9J!MZ}haPYy7`G_HY%eAo`xSh_Ceyjg76np6Jm#`l&*tjCvxF!mCFfG0SK9so zPZ1pHbHr()Y@=d==vOxNMi0gR3~gbT!lWF37Nf(Jbw^~fEc^R3XfQ&G=i}hB$@!Mj zMJdr(tsY$IDZ6%R&T$M&?-)?~zU=sNkdTnP&Ro8t%B3?OspUUoLqS2A^i6_#NlRiO z0{+S7eLryZ&lI8<;Rw<~BI`wBL<^CX9shXE|48=id)WeN$n(pK_uqz59`9@Of|Ljb z-jO+FB1R+BL>``S;cQ3@40xo6l>lN06GU_IU1^ck zddgfVv35#oe)h~P+##&sEZrb(=tSCBMSNmG^EqwWA8FKgB(Pfctph~gmA%ZK6@ z;_&Y~tPo-;NYIB>&3~6F|6o+u7<9*JMO=V~rT5=5) z2q~fwl?HDId9o${A>u^oPymQ&))`I%0nF?E8~yF=%_6ZGecv%bCl6B;xPjvJ@mn1QlZN#U0q5~iU(I^sU}`%Da;^N z(jXji7NYmULa}31e;N~Wy}mL6tAXaN2$eiuLpUAnnzOYKu^_yk-`obx?=#Pbl7`{C z%g|d0*m#e=q7s5MmXIv`Vm=yWxC9eyq{EF3l{A<!d1%<`py~GRV@2lbfZLmFsaO|D-S7dnkuD-tnIVCJqvW z6seOdD&;Dqp);pdKMqlo_&n<8#+)a)*BW8=TXv=S!v{9HvHmn9e?Qy7gU2d{F-s11 zFYOeh>Yh@R>jK9HVFc~5jqknTfIAmP=E>n7d;>e=kFxbwKXuouxgDpxtj)fzaU-L4 zJnNXn2tR!@>>bD5CH(x}fF>+e-NN`=+F4S98Vz zffySb%j&evzrMb{*6KjCwY3GbOFyqgdq-Vz++-{8e53Y{G%E#x=w#XP#uI&%btbve zsJR=df2yjhJNfS*w{3Y4EiNwpU2loW<@1Ea$=LfN-+k5YacTG`&p*CZJBY|JqC`%A zwA;E}xZjF=wyocD2?ZnPkd%3?8bYhpc8Xhx$V8=H`=E(gWG zzVTyV&_UadWJ9fExS3NH^?yg_W#>b%&U@O<+RpCazcIHuG?|UY7clDCF#%h)qn*ZO zjXzRUp*}Gi-_76ln>{QY03_Zucju{s%cFw#Y>qv5K%qzG^SnS7K2pMaPzq;9rp2~? z9zWm&>T0DojNyZ>b}O!p+g`@p!K~J;>@GI=A3Rgnyk#P6$qRV*_PE$2SbK05NHWX9ufS&7c@adBGCW+5b6#~& z-T+CQ28(+Ix}drLZm_~Fp_(vCc%+E{qYYwfBLv}Oca4#zFFOw5y69BBMk26UpyM0+ z2y@8wnTea&D|2xDVOLwGhb8)>N2;lJB$R>k!FHRUN~c_Ee1q#XZ7`QdRC2V_Cw9HY zM!e{Lw>H)`PJnn3i$5gB~sa05Ww>K4aWG&oKEPc)hG+jS|=MH4wI z|CPJ#$B{3pwAjH>P`z)cA1)h@8YKm7~pU@jma#0HZ`O#2T{5C7r;T z)8i1kQw622oHX^nR_oyawFW&C=yZUA_rAf2^`L}I99?=5&_y!aF+uPf# z-$e=RK!ZIHhV*nG^lp~MsH19mpQ5VAIQiU_l|{%qS-g>tQcs>ZmL*Mj-4?h9MV&o; z4TeIy+6_T7-QfO@t2iyoNBx;5t33P+XZ>=4W<4G2{6JFgn0qF7T7TLkI-jp`QT>A2 z?es^S61o)KY>zDs18c}G#WaxtV3VJz5PlEYoI zBhjQK8U|$^no*ldxp7lH{nR;87WRI+o1q3fte{uNd3LMbGEr=?X?CvtdmBwxVVP1xa=N>Xaq$7UN20Pj8X5^mJ;)6ZW34eE%KtoG2BF4VewUqY-q&! z*JP{1#((Hhlo7Y6sVVvM+i$5LR>F90bxKiRsSn24_3rDHSdJm^2;SWnqe3iAnRbn`Ps&nb3oTrSGG%*$Z5hYo!NBYa7 zO<@D_JWEyR@^{#iXy?1eEQZ9Ye;jA!i`zta8ft&mS+P};LoN)nSH^e@h0qrVcg5b1t2vyU$-2^nzn;8%u`yYcASlb7J@!Sa9zp!`E59+_cJ!=-x+y0`hkK@%t_`am&o@LP@L`aU>jw8!@WG|B)LC)a4 zsSdF|4z#E;8XtQtRME6wfPip`o{3esU7{wiRvJBgnvG)qKl=#>@K@Ye<6qBU)=v%tG zWejxji2oMF2&C`y-;Mjy0TyU>Vv$7*uWg6qEs8|W^Sjpp2=xDW#7m#muHS$EnxD+% ziWsWc^*}tTkqrfOiSKuKMc@2phj=YX|A`{MyRq}`0R*umclJ@HRo>p;UriR{<~3aL zCdQ?UQvt6sx_>!6H}~07?QS^%Bf;eY#z9(u#cV)yI4>{moMwt zFN9Vg$z{HfaB7)nxy8gsEEEwz46#oqHn{YVG@ zO?T(RNaBrDHzy!>9HJ$6K2WIlK;XRmB{mp^wT5%je|dEGc#r_cKJ>EvAefb%&7j8D zM;Gel*Iol2wg(+AEiDc6nTLxo;=YhNLNygAUycwt9rnY09&#Z@{VX^Y1O*4F99D&8 zE;u_tlV57%nxl8=ZdYor;;QWc-2o|`4E7Z5oX`1DYss@vmw}hAEtAkOz<88z{Nd7G zt(KA@CoRp5xIp2v;_A&;H?Gk2mwj*^k3%js3}g`4i)A}wjb31u>>hN`ZjYl*Z#_3) z#CeHTU=BSOf81vOKc)hAy#{dQ8MS9;p@;64Aiq8{4u z+4=G7ZUeBk|4|UO1)}dym8l7B(LjU1%@o9>LDBF(dkkMr8<&>~EDxuMEtN$Sk_-Ga z3QVSYKl1c3rKu8CmuM*pD^V_grua7#e@ifHhIQ8^(7fr_m17-1Ki`(@LW6HfT4UsQd}ZxO9`4FT3&HZw30c=*rMFUNv>!FH?;4ppSdNeRvjf(Br4AkL zBt{sfj5}qB9NJWi@R6%AzlL%56ywJfJH8DM(7|ol!$`l4;yf{m|7?oyP;C5qsd8IatQR)R)ADct>2O4*?W^q%;Ow5Kd(Zb7%9Z})<*>}3m`a_s`#Uz zLj`cp-r==|mcg*z&a)=*=w29};qFp}ng}%hN~NvOi}g-Zek#&lCBvqBMxAy9jy(sn z^imfU4TtcN0F7^rHR1@IySE{0x5iSf)=HC9ayw#o7LJ~I*%dJu$-t$S;4g3W4^t13 zrC4w?4WDREemuBzy*WBO$lXQpt{QugPe9uahVCz3&nc5g!sRe?mthJNzq{ z3LFGCy#zSpk=X-w-U%QpjmY1Z)RYosOSF4@zU{7rstJllDs`tEHtUFdZ$c>KDz))f zwPW)Aw$;eE{WRnGA9W5i3bTSQ_66M$Z}b#=)4?a6i+T0GJOuFB>IOJyM;KIkxeHD5 z3d*^j^kK>&SLhq&59`096XAJ3#lk`B_HUP>~W~2KW)hB_p>gHx zH+}i^zr}qgJ`XlN|2qAf7CRxAa(QK9tkHtwwWhX~`M;h-;mg)g_Vz7xp6|8H{oh|= z0OgJ~=?9Q0e0Wf-1eWQ)xOYX!hDz%P{U5eG-L}51CQe&Eohk)PJU)7LG}dy(uQ0BC zK*p&Z=m!WaeF;n|v>v@8=My@ygr6!eC=tVqdTA8(9uA&W?49LcE09*QZqt`c;kybN zWMlB^P2{U0tt@jqPv~ z{QpSvBB_L#6x`zC!m^0tnMHg3v$S4pgiLV3LUN-5kug=FMMHMV-5VZ8D;pV!oE^SE z-FQjMq_2jyZEK=6dQ-2|?$@hdOJRLd3SyTrO?>CN9saAqEw0DC8X2bqrE^gEv4I#k zf8T2d(cZVIzmWWqQC6|bYcMItGnMfqcjSjfxpO{Ys#jKOx&@OJ3!zb}2Ym%F*i0_p zxMh?6_w$xbj|~Tl4so)fyEKcQr^ZL1`|K67>sj9k@)evtGy3pZsn!t#B!G^SJf!A? z0!ah-Df_fAIJAZWMYki|w!CO*9-M;`lK~migg5|mgcOtvnzj9CmyvL%Mh{4MD1G`&+fo&EW*-mKBYkRue<(?jR^FOovRJz z|c0#=Lx- z`jmDp>o-+V<`M$FGHV^Wt%pAyK^DVX^dYaef2UIA_I_Rc@ioc_48n5D5IKi*?DsKS zdCSDnN5JQ+ch)GlZE_Zo!bC61Rtow1O$v`Y#V5|YF3os`O;)#!ez z3giTU>;U$t(PokQqVL21Kk;Yb)LnVO9)IK#f$ZI(mV&=ZxH5%3MfLLjGae$0uAUyy zY8yg<0nLOr;N6$Ch`XDK-!;}U+WwbnW#ui55(Xsp-fc{A`pgk4 zSq5DmiR0!#YUkQ$b7Enrnb+MZPU*B!403OMwp`?N;!tqQ{*c^g?Kn!))9!SqQ&Us> zyVd16lFl10axjNJ?blPi1J5)~)rz_Io8_vCUcNv`Y8Ly5ijKOOF_lL)QE7GyLlU2L zzK)GiVT&I_9=`^b(L|(Lv0(O0Cn!~}t5>f$ymDvRZ!VKBjKaA8YgFv_`#+*d8R;DS z_X63Yy~?E77O-j(L3lxRs(y6;Dct|*DYab}U%8#Or@Fmw04IP*K!e-K%)cQlr7Pfa zet&)1bpr$*fN!epeYOR%g_M;Qjb5w$-nY8%Bnbld+N@8*6qBCEt{qm)C7pZcy*tG% z)S2?Gmpu>UC5MOO$+!3Su-Br_dFN06`um~bCVum%ar7sDk+arX}5<5}Ok z-S$=Z7kzZ~cR-Xm#>#($PT;?W2l&$d8-Jx$peXzJ_y7X;f2w(E4<{kh)q_^h@P@bl<3S@|m z#&?5afL)}>y5vu>nteFm&8m{LPJrksJh|w)6zu(YyKHhfC{e2Kc)Q;KFi-Cw5QxDi zGkF4&9d0K)KDV@hf>EQ_2BeJzb|X2uJIxaH>Pq3SF*kQ<^+WZ}d!n^{?>>M#03bWa zv!~NESa?L0`28d~)%8S(2x|Gao<=V1e z_K7ak%T4cG@fesRsZSo6fwpmOZP`eqUMq|x>0;fqM{lS#-*R{U3Sve%pSP?6HXwk`1wz|Hye89M(GEQrQLaA~Fh! zik{woXj%O!F)seV9?@(J%0{Q>n!n~w!$hyc`{uZSSJ%qwBo-0YWCIxPVp8bSO;0pB zMM^H39FpXi$wQFo6yppQJ136Jl#21mowT(9GaD0gISrX{vtEZy)*NHD^)!s76jDas z6}I%E+}@$pmt|d%4Mdsd4pHouP2L!xcl)5SkB-PWJV)iyZadZ{W!U#V3mpsUGuH&T zPhO<$MvSHb2p+Jx)dHtSBnq zwS_z`Jx-#^50|=5^|X*ZQ?Le*+O%m|{yNEwDRucLUCSmUqu@P02WKB%2<#i6%pwF&6F~uC;(1KJBnJo0XGO+}4IS>Z09fE>rT?m?z#%V$J)DSP!^vNq6;8 zIz_e7aqBDbXkl3yjdneswHPJUxO_M@jnoVOgr4Cfy{^XaS#Ob}xQVN9)4`-fCmLNe zfdVD7wLYn~Urc}W8FE&-j5Oj{giqP5hb1)UH|b~?2@n0QYF@bZ(r_G2{UrOWBJRm9 z{Fw{2VyWXXl&{J=WYz{$^R31}rg%KnO@$-n(!Km_IQCOp?1WQAm-eGRBXjrj>U00E)| zvjqEOHor`pF-_sjHMGFSHaa8F+rbeIXZ7_|IG_I8FDwyW>9w+?(z7n#6Q!72_lkl@ zru^mGZ?-;ParpW)&HG_PaK7MIclSFp;9`Cn%h9RCMb-+(r=>?E5U^s)$7bEGDtEvb zvW4kb`g{5L%meA!k|~jHAgKx^CHQjy^=$tr^Nh?xT{6)Q;ok8Ft_y>}bqNV^7Nb+? zeG^DiO$`eer^v^HX#+l}3h*jY$)!2JKK`rEZ;`Rfg`@*7T~9?t1sUME zW<*@XjI)U5^6hmM#0*879Nrv{LHD~VnqKXKzJ1Y5hU=;dtnTACTn0xj z{lzkd_im^IIs=aZqY26bIg)Ynr03^ncA+Y5Ms`+K@blf7PPdnfg3KYeCxOE>+hL^Z$aP^!AP>W+eN5h=a}ZyDL677j&$UrJK?Q=jPqjs;s=Sq9pd zGig+0 zw3LRaE7E?-u>_;a35r>fQ~cy?xR8%qxXA^UPOvN=JqAD6uWLzb>YLS@W{bSYMo9}h zM1bj}N4_uR%=ivrMj{#`Ab4!oQ0AJz3cxC&*p0kkKa;1p@5mT6NVv{^maNdDB;{y> z(uB|S1`NK`V0MM3Sc138Z~*kVb`S&64uHG)jSV8p8t|W|neTEuN=`bsfI)4obsAV* zjSu?F=_J-I>PJ))tg=Zf3J$(jT4zfjN&v01rq$-)u?#>iAJ1>!5IO{Fx@=R>G!Z;--Ro$IP~FmoMeRSRZBJC$Z>m=JTaVh)lJ>yn|@BQJfYd z7WRO+{>p3=%sWViJ;~zuKHKasT?Sc>Vy&jVo2wIwwE(Y4QPGoJCwts(ztQc+tR$GeloGV?N631fgGt+h1`N(f-o!19{*Xb%Sadp zTjf=qTi;6PVqv?a;mgKMAzkN9hVt(!-_ zscW}-sux{^MmRWA!ET0RIY+2cdWCmNk0G;9bD>iovntFwZq;IBbxD|^Tj`=WCLaOwkHKg0y#45Cq$pmgB2V7GdFZ!{^SuWatPp!ok&>n z@gn3fa7btWy}YtQUIO^R5@%0B*bjfS$7N1pCCnZRJ)P#zry@bDO}w$5pKug2U{!Z+ z886qsWpU1qML$W$G9B!lOHP@sspV%6VEq);-#IOLX%D%}5T4*r_U+C4!Q-`Ql zX8lz`qU_Io$9c$k2)qUe))2)p|9`&PgV8*w3v`%q8snI=4ZW^EKAXoQ!Up^st4=3& z#H#LEHQut5AL%0YL8Xrua###d?T|}WuC(t}|6yb7D$nsRB_v2pp{c;8u(%ZmanbOP zl`_Zn<=6LX@+)PKrW2Rf^k;3#6Qj^%LA!GLa+KV@hK2@q>L(ot2K|V1#WE|J2b)A2Bd$rv*O$WU zd#8_+^f_V3xSkeV<5#ux-c|t|k-VZ}?@tR3!SYvjy3ag2ysQMvGrVN!Jbe*8@DV+S zHF)D~WjSf(4pysY+59nO|8SAp3XL|rQ@!4Kr9?a}6*@~H=y?B?PUw2${S3p=mv#R_ z{M{nY1jd2P_=()Yt8#^b4e+i2_I)ak2Xzl2vh$W7$mC1P?CV#lo>-Bg6rj)Atq$oe z_5aWery74q7rR3$^P>8mR6x`4p^%5?nQYOEsMf>iIi~Sn<^9Yq`d*>s3PEc<1wXaJ znAyiQfl?IRe~@*cw2`xM!YWw)X1gC#OVEuzcO3i5*nS{@OSE%^NzunUU#T<4`1X*y|a*+3Z!%iQ(C-}BqIo_#PO0Ri1j!z)+%>oD}d zy5{g#{?-ToCeqM)29hs*Yb=;iDJprYh#Z6x@KK+0cn~oWrX5xfdDbmOhFtB^Dwi6O z_U>`d=bVpFB&st!RueI(e(g1yGZGuG*96J5B69ce4wSBD%IB=+{5o*{Ly#Sm9DOSK zl=CayG%g05Ww$xTGEZ&Tnd6)&q@In==!k`^*iO^f)y3C-WmMq;F!$`E0zeMsx!Q1# zaM{0%vB`ola&)FoM(TaK=^J8=U7){_C1XM=b`Cl!5hsS@uA_HeE+zj;|G$>bI;g7e zYvZ((bax2S4T5w_N?$-4=?cS%Xz!@k=M#` z19?Z5-glGF@d1RBdQl=#x!A3v-p|VDa6>-PLly|{@s*vh-P&l8vuk;ES2X=i4K4({a!yPm1UT_b~EqhL4?BxE?oC_~r?Z=6%48U2ld|kbb z8I|g~;9_8O5lnhhlGPKK{)re%d1<-7AEmFaPnvjZgC;L8@BHEdHQus@EZGVeiwo_( zt1{z1!6&g!3#o-W$n23@c&Rc`96SV#MV;?KX;Y(LK1PJOu_M(TRFW1b^EO}c@>)%07IC~!yuw8QFC%c~=ziVqr&spHn6k2EyZg=`^$B-Z7QLMi|N zB#FU~9~vc7p+G}jV4hx7^)2ptohtU(eQ$_eim1KZSlhto6Fd)*L@qgf-sjV}i_qM? z!pvx=-yNj+%eQ@bGzP46)Yt}|T_qLZ>-HW9JUri-vBDAr>;X#mDYeg#Tk4M=I6P~y z0w#00L^Ao|c*y31r&wEvz%k9MR_8dQMPTQ^BzmC!k>yo*YpKD1h?UA`H)q0_Gzm8e zf|x%fFY15vpIY~+kx|)sLyzO-o@F;MdRe4rWddeWK(UNXK&xFa ztpL%LGJ0_iCrP+vg4RkShHll4iY4uKM;0?>-q}_|$ zcg^-z82aMk;#So0+<$=zLnYzgJl2JYJZe)828W=^O^f!VD&kIX!@9NttlEjUzl?+U zMn9PPMlpkl^;Xno_AhNNuODh6!tlh+#Vu@Yqhvt8m6l&v7;)U% zJ<>QlU@7-*fsEE`YX(Eto*<+vuog3I5G!O)x3!h7T8JY_PNsLhGvJQvZeo9-sfr^> z8v-Hy&u>P+&e2g0y$6`h=Nub3AS?x9nsc-cNWxoPPY4YR_Eso^3DB;AdS(O-%#`t# zjY7O)Cd2rSCaAO6&jQ`b-akx#7x4$O{nH1{)fK6d0ccN7 znH)V2^oa=4pACE>UE8o7qji!pGDav@pB}X-Y=8tky`MxmkhMQ#1QjYx?)^Fe`%)WEc57)wp=e3Ikc` zp4n6gQ?~Ud#XW<2j#9-qUD(;~@v2X=wJwG}cpdj)UGUm~2S695tu6ijO?3TNOBpbw zQA2C0Z{N45R zF1byAJ(h`W6;Z4;M1%AGq1HFpP+aT>Lab!YbUpNWKc{C1Dc|oK0#_YyTcsTA;evTh zf4|)EtmIMPuqxoylC4l5ru_&0omJtyVT}oiCze?>pzpVXX?jpa9v&WM7igvuJn!p9 zP)N*mJ0rRMB`dMjNcLfzZX<&*aR0_Yc_Q)UdD)i_^d8NXvbFAktgrnq=WJ}o3R`}k zJbCq+k0vP3IaBS@1q1{DA`Q03FL80nO3!7Y0<5SXWDXvuWuhL;2O0p97afg}J7s&! z7=pyj)VCuL3vspN<-lJpE7uJavrwAtEFq$hfZv~4`E;l- z_hDY1`SD-NC8#pn+1dgs6d38)_jB%T47{GE| zT%@|z4-^@noy2orLyMBz*+JZi$yJV2rMHDqMKH)4)61i5)?)rNL52K;e?fOPG(D{w z92y#BXi=dqQqNokj+ve{pgJe%HUba579dM&^w;~N3C#(kpsgPM{`dLuj0kC}%qLB6 zUeqF|UP_uR2!X$oaZm#^)`_>+aUI7f1HXt;Yfi}KIAn5ehvZ;o8}kg}TaL_Mg3q5H zh50WAqGoY1spj!u7<&7DnCfv`QPOwL6!4%+wZnulB%GM4*+-_}rhwE)PDO=1mc3OB zXGv3KqgAxh%-I^Joi_BkH9f3S+r}zbY0sH3>B!5ghxt&3Ch+jB5=TV%vr%6*)U)s= zPFN>lf8b@<AVMm5o zyMLeZ{%@ekX=|OesV+vNAUW15*uDO0mj9dj@Jsd>h6_jce(qPrz4G6@Obw9qfX{Hw zCaYupXsO2X8F1wHti{E}NtplxZbSqcI0L}84?HQYs@8ZiVt@pi#2zgDZX?2H27}TdF^)3W`%+tszI3o8UXA*gQ zt0|O5{0w^xj`|$>^Ll)dz&m%vb#q4>bbm6rs(ShPxiA*2JmRo==l_uabIwy@85QhB;ZwaKXplotIM<;TxRKlhhzm+M`F0l(Y z$x@r}{h)JEH*dJIOI)z+3hNB8B2?x6_@0j(SdKuA=jSI16c*{yL6^deaqA~%$AFq-9TUyn*>;=wk%&7hh>EYm*U@Jp1r^`Y2TvkqG#A67RMG`zhf@~xcFwh-uf}qSjG7v_*EApy2no4i^1v{uY5vOP zf}LS!Ix=^&@sYCQMs12-wv&rCz2Z_y6wRNq<@Dz|J#MVs6_hA$?AvMG0QxXFb@9Jy zgFQdYC=0WFK@diF zLD^3(D+wFTnW-1eLOSsL6&g5c;)7Hi#PJ$4pW#pe1lG(zkH>@2NZDh(8@u0v=Bg+ICySGxdz@3uXe!^gip zLH=dHqDP<#tXtk{J^G>tm5PcAb-{g|nVL^($|5ND+Ot%bmzVCR>%(T75#WSVs^^HQ z`G)^1KHlR(QR0m$W*qg|ZP{G5ld+*lZyT-HQS;FWwZBuOwiNLdJVEiZZ*pn8{ZMG` z=C6xS#YMVo1twaa&6Yzr(>q$E2_N>LsL}>}Y5f9f0Nn?0CN8s@_kzgsyE=25#$ukI zh#2&pNcl4Ha}5rpbB;o`JhXbNVC;VU_)!1PDfq!~wJ4gD&H8X8#Cb$I9uSj}I6=C0-_E9VWfOIQ68 zJLPjo_1FKLQOy6yV}BuI?Naofr+0xWrNMLZPCSt{g~BODtH?^pZmcFhKY^5z zcjG}QD!QZbdYd}3R}IgTF{=L2dDjdD+&w)o9|EJe(XmYs`~bFEP*>u| z3?Mbmw|TkvP_okxf`!rLnNwmj?NTYUAS5Vwv86TQ$dWK#Y1{^k71A`UV&WVG(Z{PH z$o)c2tEE@>K~xf0nVDvvzwewi8K_zOgiU|E(WOWY(?DF|uC@DJXGN}~pFFj(e?ZN? z(QN9vMS)*_k}$C~x?8%*7Ya`5o;9@$Aylx>Bt6x^wVxzhe(gvC+lq&W`QX-95k;;P zKi;f~AaNsgkH>o-6YrI9`&-gvE8fguS6g_X&_n#yRINFdWB^X#p!91BeH!p{yaJy&$3c8CPkLLelhw^P8Y+x5g*MI1V8*K z)s==D^zo39lbc^1{VuZ3y>ewD?q^k3#gNOgyhg8ZcXffK#c=urmV;@=*R`6=DJJcFK#vmf0*3esgE zW!)DE=O$s)5<(wo4DFX&HP+I=8W|Vaas{53to!wsJ!`sUdZ_XNkBxp;&QHYvie|xY zm{r}}kC+?3O^`akbi|V29647qr^2ig@f2osn{w-EG0s zYy8yDu4i7hbm8p2>*fk08(pIhN~(7CB;`rDynYjTY2+=1#hpW7at9gvF;;0bc5ciKAGxKvUC zuRMlea884~xS=1%B*{ASPS17D{m~|Bing5t&$UT7eJS*3=WuiTzjBeh=(2G;WM^kR zM{OjJyK+3raDZyc_ml3sH0yER2i?=XJcnT@j>^9}uE9!gMr<;X3GFJYY-O{>nuoz8 z`^-sY)(aA8g|HceYJ?GafOg#SbscZ_s>vXJ?R0Ll{34 z8XXs54kAgE-m2GLLf_dL*8GQT8k9QhHa;zX$j2JuLqH>*B%Hra8BWn0J%WXF_akJ% z8Ik-?vBj{p9gO2v{hQY$a*@QkSI$40-1%dFc7u`2JL}0HoSobEJ!iSCd6LLd7C4lz zBLgzO2}9_!NX8tnM271?3W7e`Vm3VGIBLYiX#B4a-%#PE-Q zSEne#a-=cpSBOa{OyaeV#1T{PMO?x3MX%y2gpqBEF!=lFpFZNlVWK(~cv1(8^`35y z3=ATaz=~3;@mWPwBFbon#gRHVhCz-vHO?AlcipzN()@Y(J^%ep75uejUtdrE8o+Rq zxlMn8hyq(DCnn%tyuWq>IR&JLOqkBHomUhQydPL&T+&mTi!E@xoA6dy>E-HhD?#xG znyxXB?4kBL6KBNm0q2D>t9quGlGwN9`S>6z<9eUNw{Vna%{RAM1?+=PX4!qFj;pY4A|73DpSDRE5%aI8W(i+M(L3Q)%*I97s~F=BFvIsZ3gK)f za{WbRTY{q|Si6q_*3(v2i_!|cIqP1&!CExk-zi=X3Bb2duTOc&KDuHx%5=>4WQNZE z`$wJcPQq!}-|*LDbgs?@S(4o|QyE(JvjXGv^!*vVy@oC> z+(3u`nLWU304#&jtPw60LqmhbB_-zL8B0;WtM68detzj&y*VUcF$_OPpl*Gxo}Rkn z6;9EWBwl&mQ+(Ks#c6G;!iQu$gxz@-k5<#k5!&(nlX>+G+B4%~t!XXJQti4INqq#L z)NFf)QSVuYh*R9U>jm*CA9v)(T_h0WFI)Fs80~}koWJH$No=z&}`gBN`&CY zoQ}b7$>wNT58ILLDa2T1_P&>Ai$SN6a>U@uEmc^YlaH&L5g*$ym9sk9-{lyamd*SA z$$e3cMt4rI6!#ooc1E3siAnigf#IW+MRRk@x0OwnT*qGpHkZ9=C|yYX(1HcLYE@J= zTv}fo2`}5tXghm*i#=$?6UW!u3j8JJ;ILgAimwZTKW0zQcrE(*`GJZkr={hl8E-Yy z$eZ=NMQvr&Kx4|XMADPW2E;K~n+mHH&4BY+@_#m4pw$5wv>NQkz|`tW^-^Oqo>qM1 z_3}J!Q0n`4l#)IZ@a1nu1ub_U)fW7#v2yceGHCL=&DeN9xsKMSDh(-M;$4Rjg7VFxMse->EQhBd^m79y9$C|Nz-aq2m5|s4q8~#&qvHW47v5BR? zvEVoVBcD6>2fyu{NS^zj2OExYYpMu-4bUqr`c(;6ARmxXQd)w3ELe06`YZ)ks(>hg zkT6wi+U?>GN6VIT1P-k^tZu8i$lQcoA77)?T`*?SQn%v1Uoa`;D4$Jwv&I-5Jy&Kn zkw5_rCGsOVlNo1{V@qii8&95^w!!1Cstw2=Q;z{n{M6>!_1diBCRrRm{QT#<1vi`K z*WEuf?9$DTn}Y6|{K0KuL)%+YpK{k8u3&G)XtqvvN9}+&0Cy@_4OCRI0t2VHM`?i zmPP#gcPq}ftvN0DyADQ0CXt~{UuQVQc%qdNG*?n1r3H|qHja41hVyWvC+A8hZ(H& z-U0!FW{PWylxmbevbf@5Pti$0W;caG0Tv}Iiws^1MewC7)I+Z@z0AnaEG;d8KwfP- z#SjKS(Ks|U1%Cug`Zonyvn^B?oSfy&M-u6fip}+eHj(zAk<9!#B>cV@3SD9|lrM~> zdPZncowce3)1J8nyo>f0oZ2#@UmDc4=wJb#IttaiA^GSE<{u$@$-?Q22^` z@yyOSO~nVy=9h~zo?v4DYm9`;D9W5n8dl-z!QICLx}W#_$9;ix_ZQ|-sP6;oAYFwO@cihz^r>dp{5)xf*Uz`N=#ZQN=97Z+Gyhv zt(MUop(T$v>0;{<6cZbJN(2cCk^~LJuV3moRKj@WLYuE9{uuJ}@JPX0Jcw!ypyI~} z@Qt*WEh~An1rDWYE)v*p4}wqVV@m`g4mOcJJ3YS#WG9OM+FKSc)o7({=RT6C^3Ns; z$Wm_$%+yATZ0+{ad0aBOdx7aZJ^KSnB~p9?eYG%@+;={HfEFVe3%opb>tou{uX>-8VNR9>H3SVo7U2&ea|05PHAgM_(RCvU zQfm)V5zx@U@^)NmPXjCmN=&9hMOu0~L(1sX;-YGR1N$rZ32ohF$g^2^%f#6#1fG`n za;DrEgA(eH{?7Q!N|fQvUNe7aSljnR?$p~0Lx%HatqFqcdIe&^I1*Kr1TnO@=WJE~ zimT^8+t7PZ*_x#93f{#v`57ks2&?20_QZ>*tv+t0^jU-Z)3U?-B@69VSq9>?)(3$p zm`j*&OKT7mRI}|q9?iJ@n^~e}K4TX*Z(e+7BZ)CZ_#XJd4#@aGlm#-U`v5WI>*&T) zw_66Ovi{flK`w79o3dK#^U8`ja65o0Oup5F@8OMP&}cI3f!gFcqibWfD=?Jw?b{lE ziE`s-F(h{7&3+ctD9W}~M22+O2N*>tfAB(fxgK8R+|4%TR6wm)8005hIq|jYoZWC< z?%l}f1hdGH z=PcQiXe#PeSuS;?^}Uaj(YMN|g%7(NhptD(Ncn;IS@O$~fVsv4>17qEHd+k(3K0<1 z1$I)S4}Ww8goNDBw>2XoBj;+Zu+BEevE*J=2+lbsgF)57xd2$O^`<^dUhqCx-Ngeu zlo5XZmI@B-^4N`5H_WZJ7vwNOs;EeR^E>uM(ks+))Nfpzjs}fMTB41?uT{=WkH)`_ zVqwc_6aEEE(-;hyh#1Qkk?!)JoO+OQOwZUGP`051T{v8CEL0 zLD(kP*yGlCfO#95Vh&)Rs@a=UNY?QN7u}@?1_!s`vFtJzeSb{v&o zL>jXx+Jy^Co0V+aE2H52QkFz|ZA8%MOtfoc*VKo<0^%r*B)?#7@BSTFM$HuJOl=M! z(brr9?EalxjYtQQIQlESy!8DJ!4t?`B`@5X@!<4@N@A=fOYGN@5t)L;mEwF(W&3T;hzw8I8w`w9pB6hV)ngQR#rc5DC?rfihDe;BOUietKcgI zLA#N>h=0Lz8v>=#Pk8ALGZcbB`K6$spx^e9b2Nn;B#@nq@lQGD^xSOIfPl3wq-2Y{K(lFE+QB9j`E0%H=`Z1MEsy>>loIG zR$LM8O<>_x&7t-Jsc@z6WB&Of6RNUwPlOB&ChAXXx`c=lrBt%WXx5*;W~yBJGNYGj z<((^rB(M)J$0>#I%F1LB8rMLqAV{-VcW+tFJs}@4^>+usvOuv-(tQDn*>;APimbZO zBWhu|*7$M|QMHja{YI94@`Znn#x+v4#Fz<)`~`!|>A!!60BZ$owpgvNygWStrIjpY zdtrYPdWsvw7ind1_?qV%dMrC+y4{^ABk1f^8V&8sf3S(|Z+Y0X!rjL!j9)%v7I=C7 z*E{E?!$n-q?wsuJpOPeVTTjOW*Xhkp(m64O+~_D>7b9ecVX;&1o#-{^*ZQ zFJ@|@{c{T>#jSU|3xfBJF5k950mSRRKg$bvBM>TznC%byJz-*0>9*P*T}#~Ee)>Mp zJb2?q|1DAzQV>JfH7Bg?WX=K1qXD`8)QSPPtOO(+f!XgFs~WO(ULKGP+=ZgW%KVHi~zyi*DQEv#k{tUk*KinXWZMbS*hK>RM&`N zgm1*A1$a5I7)h86-zk=t;FopAd22JCK=lZmocc$pO`E>_bNlI&ebr=UEQu@kD8C^b zCn~wTt*H)@zSxp_YdI3N%mbqNZGKe)Ir}LH#{}we3W5m*+dp+6i+Lz*UAjj*KIUTv znZF!z`-Ds!Gw>;Dkb4792E_r(53)xIjwUprh)l&QU!>7meX!^>nm1MO0k=?YP!|Ez z#i_|jyi}zwk#M;2%DGnYMe``0$<Yb1uQ#OIwLAe&@Xzf$_Dqw&xc6Xc>3L6`f3sh2S|37rs*w}au14mJZ zI1?N#OqKg*a;VEj!^+QK+M1QdgXt?UOvRo@Jr*Kx3AtruF@!JGdHDDUjO4(z0y*gk zhh$5_RNVml$;f|uLD!K%%TE24u3NzRy2c)d->F0=y-x+JDfd zZ;zF|;7!rY(+mfMR{}>T85I@qaH4K94;qF>}jf<4TXi9S0G56*>Sp;zE+aF4s6^&v;o<% z#T6CLrxiOrKvb!%y*(yHjgXKKTF0J)y*-zbw~jK%XI5iNVgMuo@UsIS2WU?Lcm*h3 zphgMD5!O?KavL-a;Gyg5BY$` z&-=&^Mmg5lGWkR%9tFYgMoDZ>2it??=$%z}tG672fpltO}t4`wrTJ--KobK4cmv0J;@5VCdKyENI z%ci-0wH$ZM+k83i;Pba_X#j{>!0dnWWtgaw`7#K81m6H{uU2)Z8O!4}@-nw+W!EJw z+pBhzajb>LBCPPcS+Y#%=heg4R#5RxY+Le*lrE-Pe$&AgQih-Q5LOhF?Jl+^XD_GrLVz6z9o62Wy0soUTtZqgV zh7d@Iu7taZcRTL2u=7Q~VBA`}I=Wj@Vr{``mtK0UeV|N(RL-;<1o;>0APAHavq|5? z?YK#O#*MFv(KrGtJD^O_`MHQNySF{6vDgRu>nLa3H~tPNkvTw}*1&ljO=@?unbSFA;@ zuu6`vr=D3D1;6noxeNBu)Y0j2kRQ5cCyaoS(0YOP#U9M8b?9=}Nb;=0> From 1bf54a0bc51351fa3a914270440960a6e05e7fc1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Jan 2018 15:05:30 +0100 Subject: [PATCH 042/270] clean-up in gui/qt/main_window.py: use string.format for translated strings, and some indentation --- gui/qt/main_window.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index fb66005eb544..69fde286b5b6 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -553,24 +553,24 @@ def notify_transactions(self): return self.print_error("Notifying GUI") if len(self.tx_notifications) > 0: - # Combine the transactions if there are more then three - tx_amount = len(self.tx_notifications) - if(tx_amount >= 3): + # Combine the transactions if there are at least three + num_txns = len(self.tx_notifications) + if num_txns >= 3: total_amount = 0 for tx in self.tx_notifications: is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) - if(v > 0): + if v > 0: total_amount += v - self.notify(_("%(txs)s new transactions received: Total amount received in the new transactions %(amount)s") \ - % { 'txs' : tx_amount, 'amount' : self.format_amount_and_units(total_amount)}) + self.notify(_("{} new transactions received: Total amount received in the new transactions {}") + .format(num_txns, self.format_amount_and_units(total_amount))) self.tx_notifications = [] else: - for tx in self.tx_notifications: - if tx: - self.tx_notifications.remove(tx) - is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) - if(v > 0): - self.notify(_("New transaction received: %(amount)s") % { 'amount' : self.format_amount_and_units(v)}) + for tx in self.tx_notifications: + if tx: + self.tx_notifications.remove(tx) + is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) + if v > 0: + self.notify(_("New transaction received: {}").format(self.format_amount_and_units(v))) def notify(self, message): if self.tray: @@ -698,7 +698,7 @@ def update_status(self): text = _("Synchronizing...") icon = QIcon(":icons/status_waiting.png") elif server_lag > 1: - text = _("Server is lagging (%d blocks)"%server_lag) + text = _("Server is lagging ({} blocks)").format(server_lag) icon = QIcon(":icons/status_lagging.png") else: c, u, x = self.wallet.get_balance() @@ -1425,7 +1425,8 @@ def read_send_tab(self): if self.payto_e.is_alias and self.payto_e.validated is False: alias = self.payto_e.toPlainText() - msg = _('WARNING: the alias "%s" could not be validated via an additional security check, DNSSEC, and thus may not be correct.'%alias) + '\n' + msg = _('WARNING: the alias "{}" could not be validated via an additional ' + 'security check, DNSSEC, and thus may not be correct.').format(alias) + '\n' msg += _('Do you wish to continue?') if not self.question(msg): return @@ -1788,8 +1789,8 @@ def set_contact(self, label, address): return True def delete_contacts(self, labels): - if not self.question(_("Remove %s from your list of contacts?") - % " + ".join(labels)): + if not self.question(_("Remove {} from your list of contacts?") + .format(" + ".join(labels))): return for label in labels: self.contacts.pop(label) From 8a71e46e9b9940932e1d25b75047ea36f535541c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Jan 2018 15:39:46 +0100 Subject: [PATCH 043/270] fix conflicting local txns, e.g. when using RBF --- lib/wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 62b096eda4e7..4f2418fbc0b3 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -607,7 +607,7 @@ def get_addr_balance(self, address): x += v elif tx_height > 0: c += v - else: + elif tx_height != -2: # local tx u += v if txo in sent: if sent[txo] > 0: @@ -824,7 +824,7 @@ def get_history(self, domain=None): h2.append((tx_hash, height, conf, timestamp, delta, balance)) if balance is None or delta is None: balance = None - else: + elif height != -2: # local tx balance -= delta h2.reverse() From 2dca7bd39c07c9ef7e08d0d75a5cd9e114d9b9fb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Jan 2018 17:42:39 +0100 Subject: [PATCH 044/270] subtle typo --- lib/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index 4f2418fbc0b3..93cd57b635e8 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -431,7 +431,7 @@ def get_txpos(self, tx_hash): return height, pos elif tx_hash in self.unverified_tx: height = self.unverified_tx[tx_hash] - return (height, 0) if height>0 else (1e9 - height), 0 + return (height, 0) if height > 0 else ((1e9 - height), 0) else: return (1e9+1, 0) From 889456dc77d58034013021733963403316ac9491 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Jan 2018 22:45:43 +0100 Subject: [PATCH 045/270] follow-up prev commit. better handling of p2sh-segwit. added comment to describe the problem for native segwit. --- lib/transaction.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/transaction.py b/lib/transaction.py index 281151a71fde..95cb593ccad3 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -310,6 +310,8 @@ def parse_scriptSig(d, _bytes): if match_decoded(decoded, match): item = decoded[0][1] if item[0] == 0: + # segwit embedded into p2sh + # witness version 0 # segwit embedded into p2sh d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item)) if len(item) == 22: @@ -318,8 +320,13 @@ def parse_scriptSig(d, _bytes): d['type'] = 'p2wsh-p2sh' else: print_error("unrecognized txin type", bh2u(item)) + elif opcodes.OP_1 <= item[0] <= opcodes.OP_16: + # segwit embedded into p2sh + # witness version 1-16 + pass else: - # payto_pubkey + # assert item[0] == 0x30 + # pay-to-pubkey d['type'] = 'p2pk' d['address'] = "(pubkey)" d['signatures'] = [bh2u(item)] @@ -444,14 +451,24 @@ def parse_witness(vds, txin): if n == 0xffffffff: txin['value'] = vds.read_uint64() n = vds.read_compact_size() + # now 'n' is the number of items in the witness w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n)) add_w = lambda x: var_int(len(x) // 2) + x txin['witness'] = var_int(n) + ''.join(add_w(i) for i in w) + # FIXME: witness version > 0 will probably fail here. + # For native segwit, we would need the scriptPubKey of the parent txn + # to determine witness program version, and properly parse the witness. + # In case of p2sh-segwit, we can tell based on the scriptSig in this txn. + # The code below assumes witness version 0. + # p2sh-segwit should work in that case; for native segwit we need to tell + # 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 n > 2: + elif txin['type'] == 'p2wsh-p2sh' or n > 2: try: m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1])) except NotRecognizedRedeemScript: @@ -461,7 +478,7 @@ def parse_witness(vds, txin): txin['x_pubkeys'] = x_pubkeys txin['pubkeys'] = pubkeys txin['witnessScript'] = witnessScript - elif n == 2: + elif txin['type'] == 'p2wpkh-p2sh' or n == 2: txin['num_sig'] = 1 txin['x_pubkeys'] = [w[1]] txin['pubkeys'] = [safe_parse_pubkey(w[1])] From 704bdedea1aab3624afa150fa5698de92597297e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Jan 2018 23:04:12 +0100 Subject: [PATCH 046/270] tx heights: replace magic numbers with named constants --- lib/wallet.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 93cd57b635e8..c5b4c428fb70 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -72,6 +72,9 @@ _('Local only'), ] +TX_HEIGHT_LOCAL = -2 +TX_HEIGHT_UNCONF_PARENT = -1 +TX_HEIGHT_UNCONFIRMED = 0 def relayfee(network): @@ -371,7 +374,8 @@ def get_public_keys(self, address): return self.get_pubkeys(*sequence) def add_unverified_tx(self, tx_hash, tx_height): - if tx_height == 0 and tx_hash in self.verified_tx: + if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \ + and tx_hash in self.verified_tx: self.verified_tx.pop(tx_hash) self.verifier.merkle_roots.pop(tx_hash, None) @@ -421,7 +425,7 @@ def get_tx_height(self, tx_hash): return height, 0, False else: # local transaction - return -2, 0, False + return TX_HEIGHT_LOCAL, 0, False def get_txpos(self, tx_hash): "return position, even if the tx is unverified" @@ -528,7 +532,7 @@ def get_tx_info(self, tx): status = _("%d confirmations") % conf else: status = _('Not verified') - elif height in [-1,0]: + elif height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED): status = _('Unconfirmed') if fee is None: fee = self.tx_fees.get(tx_hash) @@ -607,7 +611,7 @@ def get_addr_balance(self, address): x += v elif tx_height > 0: c += v - elif tx_height != -2: # local tx + elif tx_height != TX_HEIGHT_LOCAL: u += v if txo in sent: if sent[txo] > 0: @@ -824,7 +828,7 @@ def get_history(self, domain=None): h2.append((tx_hash, height, conf, timestamp, delta, balance)) if balance is None or delta is None: balance = None - elif height != -2: # local tx + elif height != TX_HEIGHT_LOCAL: balance -= delta h2.reverse() @@ -866,15 +870,15 @@ def get_tx_status(self, tx_hash, height, conf, timestamp): is_lowfee = fee < low_fee * 0.5 else: is_lowfee = False - if height == -2: + if height == TX_HEIGHT_LOCAL: status = 5 - elif height == -1: + elif height == TX_HEIGHT_UNCONF_PARENT: status = 1 - elif height==0 and not is_final: + elif height == TX_HEIGHT_UNCONFIRMED and not is_final: status = 0 - elif height == 0 and is_lowfee: + elif height == TX_HEIGHT_UNCONFIRMED and is_lowfee: status = 2 - elif height == 0: + elif height == TX_HEIGHT_UNCONFIRMED: status = 3 else: status = 4 @@ -1056,7 +1060,7 @@ def address_is_old(self, address, age_limit=2): age = -1 h = self.history.get(address, []) for tx_hash, tx_height in h: - if tx_height == 0: + if tx_height <= 0: tx_age = 0 else: tx_age = self.get_local_height() - tx_height + 1 From d92a6d46eb4b534100ceafefa7379ea4f425dd5c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 Jan 2018 00:44:40 +0100 Subject: [PATCH 047/270] wallet.is_mine: use inheritance for more readable code --- lib/wallet.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index e38901e8e166..7087e9181d66 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -332,10 +332,6 @@ def set_label(self, name, text = None): return changed def is_mine(self, address): - if hasattr(self, '_addr_to_addr_index'): # Deterministic_Wallet - return address in self._addr_to_addr_index - if hasattr(self, 'addresses'): # Imported_Wallet - return address in self.addresses return address in self.get_addresses() def is_change(self, address): @@ -1466,6 +1462,9 @@ def get_master_public_keys(self): def is_beyond_limit(self, address, is_change): return False + def is_mine(self, address): + return address in self.addresses + def get_fingerprint(self): return '' @@ -1712,6 +1711,9 @@ def is_beyond_limit(self, address, is_change): return False return True + def is_mine(self, address): + return address in self._addr_to_addr_index + def get_master_public_keys(self): return [self.get_master_public_key()] From ac69f628fe889e18695852cd3f9033a8f5b38aad Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 Jan 2018 00:59:12 +0100 Subject: [PATCH 048/270] clean-up wallet.get_address_index --- lib/wallet.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 7087e9181d66..adc285e832ee 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -340,13 +340,7 @@ def is_change(self, address): return self.get_address_index(address)[0] def get_address_index(self, address): - if hasattr(self, '_addr_to_addr_index'): - return self._addr_to_addr_index[address] - if address in self.receiving_addresses: - return False, self.receiving_addresses.index(address) - if address in self.change_addresses: - return True, self.change_addresses.index(address) - raise Exception("Address not found", address) + raise NotImplementedError() def export_private_key(self, address, password): """ extended WIF format """ @@ -1714,6 +1708,9 @@ def is_beyond_limit(self, address, is_change): def is_mine(self, address): return address in self._addr_to_addr_index + def get_address_index(self, address): + return self._addr_to_addr_index[address] + def get_master_public_keys(self): return [self.get_master_public_key()] From 6faef7efe34393e91e33afdfba0746f9794ddb4d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 Jan 2018 01:47:51 +0100 Subject: [PATCH 049/270] remove comment --- lib/transaction.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/transaction.py b/lib/transaction.py index 95cb593ccad3..fdf81ed7c44b 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -312,7 +312,6 @@ def parse_scriptSig(d, _bytes): if item[0] == 0: # segwit embedded into p2sh # witness version 0 - # segwit embedded into p2sh d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item)) if len(item) == 22: d['type'] = 'p2wpkh-p2sh' From 91e44ba6fc961b15121b246dfe3bfa21048a79b6 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 28 Mar 2017 15:49:50 +0200 Subject: [PATCH 050/270] Show error window for unhandled exceptions Use exception hook from main thread for all threads Use signal to delegate error window creation to GUI thread Add more information to issue template Update to PyQt5 Switch from Github to REST-Service Report to web service instead of opening the browser Fix imports Change crashhub URL to electrum.org server Explain that exception hooks are only used in the Qt Gui now --- electrum | 7 +- gui/qt/exception_window.py | 181 +++++++++++++++++++++++++++++++++++++ gui/qt/main_window.py | 11 ++- lib/util.py | 26 ++++++ 4 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 gui/qt/exception_window.py diff --git a/electrum b/electrum index 9a9f41855dbf..9df3e612af2c 100755 --- a/electrum +++ b/electrum @@ -87,7 +87,8 @@ if is_local or is_android: imp.load_module('electrum_plugins', *imp.find_module('plugins')) -from electrum import bitcoin + +from electrum import bitcoin, util from electrum import SimpleConfig, Network from electrum.wallet import Wallet, Imported_Wallet from electrum.storage import WalletStorage @@ -296,8 +297,10 @@ def init_plugins(config, gui_name): from electrum.plugins import Plugins return Plugins(config, is_local or is_android, gui_name) -if __name__ == '__main__': +if __name__ == '__main__': + # The hook will only be used in the Qt GUI right now + util.setup_thread_excepthook() # on osx, delete Process Serial Number arg generated for apps launched in Finder sys.argv = list(filter(lambda x: not x.startswith('-psn'), sys.argv)) diff --git a/gui/qt/exception_window.py b/gui/qt/exception_window.py new file mode 100644 index 000000000000..a603af5ef416 --- /dev/null +++ b/gui/qt/exception_window.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import json +import locale +import platform +import traceback + +import requests +from PyQt5.QtCore import QObject +import PyQt5.QtCore as QtCore +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import * + +from electrum.i18n import _ +import sys +from electrum import ELECTRUM_VERSION + +issue_template = """

Traceback

+
+{traceback}
+
+ +

Additional information

+
    +
  • Electrum version: {electrum_version}
  • +
  • Operating system: {os}
  • +
  • Wallet type: {wallet_type}
  • +
  • Locale: {locale}
  • +
+""" +report_server = "https://crashhub.electrum.org/crash" + + +class Exception_Window(QWidget): + _active_window = None + + def __init__(self, main_window, exctype, value, tb): + self.exc_args = (exctype, value, tb) + self.main_window = main_window + QWidget.__init__(self) + self.setWindowTitle('Electrum - ' + _('An Error Occured')) + self.setMinimumSize(600, 300) + + main_box = QVBoxLayout() + + heading = QLabel('

' + _('Sorry!') + '

') + main_box.addWidget(heading) + main_box.addWidget(QLabel(_('Something went wrong while executing Electrum.'))) + + main_box.addWidget(QLabel( + _('To help us diagnose and fix the problem, you can send us a bug report that contains useful debug ' + 'information:'))) + + collapse_info = QPushButton(_("Show report contents")) + collapse_info.clicked.connect(lambda: QMessageBox.about(self, "Report contents", self.get_report_string())) + main_box.addWidget(collapse_info) + + main_box.addWidget(QLabel(_("Please briefly describe what led to the error (optional):"))) + + self.description_textfield = QTextEdit() + self.description_textfield.setFixedHeight(50) + main_box.addWidget(self.description_textfield) + + main_box.addWidget(QLabel(_("Do you want to send this report?"))) + + buttons = QHBoxLayout() + + report_button = QPushButton(_('Send Bug Report')) + report_button.clicked.connect(self.send_report) + report_button.setIcon(QIcon(":icons/tab_send.png")) + buttons.addWidget(report_button) + + never_button = QPushButton(_('Never')) + never_button.clicked.connect(self.show_never) + buttons.addWidget(never_button) + + close_button = QPushButton(_('Not Now')) + close_button.clicked.connect(self.close) + buttons.addWidget(close_button) + + main_box.addLayout(buttons) + + self.setLayout(main_box) + self.show() + + def send_report(self): + 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() + + def on_close(self): + Exception_Window._active_window = None + sys.__excepthook__(*self.exc_args) + self.close() + + def show_never(self): + self.main_window.config.set_key("show_crash_reporter", False) + self.close() + + def closeEvent(self, event): + self.on_close() + event.accept() + + def get_traceback_info(self): + exc_string = str(self.exc_args[1]) + stack = traceback.extract_tb(self.exc_args[2]) + readable_trace = "".join(traceback.format_list(stack)) + id = { + "file": stack[-1].filename, + "name": stack[-1].name, + "type": self.exc_args[0].__name__ + } + return { + "exc_string": exc_string, + "stack": readable_trace, + "id": id + } + + def get_additional_info(self): + args = { + "electrum_version": ELECTRUM_VERSION, + "os": platform.platform(), + "wallet_type": "unknown", + "locale": locale.getdefaultlocale()[0], + "description": self.description_textfield.toPlainText() + } + try: + args["wallet_type"] = self.main_window.wallet.wallet_type + except: + # Maybe the wallet isn't loaded yet + pass + return args + + def get_report_string(self): + info = self.get_additional_info() + info["traceback"] = "".join(traceback.format_exception(*self.exc_args)) + return issue_template.format(**info) + + +def _show_window(*args): + if not Exception_Window._active_window: + Exception_Window._active_window = Exception_Window(*args) + + +class Exception_Hook(QObject): + _report_exception = QtCore.pyqtSignal(object, object, object, object) + + def __init__(self, main_window, *args, **kwargs): + super(Exception_Hook, self).__init__(*args, **kwargs) + if not main_window.config.get("show_crash_reporter", default=True): + return + self.main_window = main_window + sys.excepthook = self.handler + self._report_exception.connect(_show_window) + + def handler(self, *args): + self._report_exception.emit(self.main_window, *args) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index fb66005eb544..7dcc5cd0a6b7 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -32,8 +32,11 @@ import base64 from functools import partial -from PyQt5.QtCore import Qt from PyQt5.QtGui import * +from PyQt5.QtCore import * +import PyQt5.QtCore as QtCore + +from .exception_window import Exception_Hook from PyQt5.QtWidgets import * from electrum.util import bh2u, bfh @@ -102,6 +105,9 @@ def __init__(self, gui_object, wallet): self.gui_object = gui_object self.config = config = gui_object.config + + self.setup_exception_hook() + self.network = gui_object.daemon.network self.fx = gui_object.daemon.fx self.invoices = wallet.invoices @@ -203,6 +209,9 @@ def add_optional_tab(tabs, tab, icon, description, name): def on_history(self, b): self.new_fx_history_signal.emit() + def setup_exception_hook(self): + Exception_Hook(self) + def on_fx_history(self): self.history_list.refresh_headers() self.history_list.update() diff --git a/lib/util.py b/lib/util.py index db5fe896f5d3..d13b3ff9d935 100644 --- a/lib/util.py +++ b/lib/util.py @@ -707,3 +707,29 @@ def send_all(self, requests): self.send(request) + + +def setup_thread_excepthook(): + """ + Workaround for `sys.excepthook` thread bug from: + http://bugs.python.org/issue1230540 + + Call once from the main thread before creating any threads. + """ + + init_original = threading.Thread.__init__ + + def init(self, *args, **kwargs): + + init_original(self, *args, **kwargs) + run_original = self.run + + def run_with_except_hook(*args2, **kwargs2): + try: + run_original(*args2, **kwargs2) + except Exception: + sys.excepthook(*sys.exc_info()) + + self.run = run_with_except_hook + + threading.Thread.__init__ = init \ No newline at end of file From ed686f9e0543394b7583b50aa61c2751f6b3117c Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 30 Jan 2018 14:41:35 +0100 Subject: [PATCH 051/270] Fix base directory for OS X spec file --- contrib/osx.spec | 57 ++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/contrib/osx.spec b/contrib/osx.spec index 1938ab6f959b..9dabf291cb70 100644 --- a/contrib/osx.spec +++ b/contrib/osx.spec @@ -3,6 +3,8 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules import sys +import os + for i, x in enumerate(sys.argv): if x == '--name': VERSION = sys.argv[i+1] @@ -10,7 +12,7 @@ for i, x in enumerate(sys.argv): else: raise BaseException('no version') -home = '/Users/voegtlin/electrum/' +electrum = "../" block_cipher=None # see https://github.com/pyinstaller/pyinstaller/issues/2005 @@ -20,35 +22,35 @@ hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('keepkeylib') datas = [ - (home+'lib/currencies.json', 'electrum'), - (home+'lib/servers.json', 'electrum'), - (home+'lib/checkpoints.json', 'electrum'), - (home+'lib/servers_testnet.json', 'electrum'), - (home+'lib/checkpoints_testnet.json', 'electrum'), - (home+'lib/wordlist/english.txt', 'electrum/wordlist'), - (home+'lib/locale', 'electrum/locale'), - (home+'plugins', 'electrum_plugins'), + (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'), ] 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([home+'electrum', - home+'gui/qt/main_window.py', - home+'gui/text.py', - home+'lib/util.py', - home+'lib/wallet.py', - home+'lib/simple_config.py', - home+'lib/bitcoin.py', - home+'lib/dnssec.py', - home+'lib/commands.py', - home+'plugins/cosigner_pool/qt.py', - home+'plugins/email_requests/qt.py', - home+'plugins/trezor/client.py', - home+'plugins/trezor/qt.py', - home+'plugins/keepkey/qt.py', - home+'plugins/ledger/qt.py', +a = Analysis([electrum+'electrum', + electrum+'gui/qt/main_window.py', + electrum+'gui/text.py', + electrum+'lib/util.py', + electrum+'lib/wallet.py', + electrum+'lib/simple_config.py', + electrum+'lib/bitcoin.py', + electrum+'lib/dnssec.py', + electrum+'lib/commands.py', + electrum+'plugins/cosigner_pool/qt.py', + electrum+'plugins/email_requests/qt.py', + electrum+'plugins/trezor/client.py', + electrum+'plugins/trezor/qt.py', + electrum+'plugins/keepkey/qt.py', + electrum+'plugins/ledger/qt.py', ], datas=datas, hiddenimports=hiddenimports, @@ -70,16 +72,15 @@ exe = EXE(pyz, debug=False, strip=False, upx=True, - icon=home+'electrum.icns', + icon=electrum+'electrum.icns', console=False) app = BUNDLE(exe, version = VERSION, name='Electrum.app', - icon=home+'electrum.icns', + icon=electrum+'electrum.icns', bundle_identifier=None, info_plist = { 'NSHighResolutionCapable':'True' } -) - +) \ No newline at end of file From e5f239dd287a72d8ac5711d31afe8e886bcb3814 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 Jan 2018 17:00:19 +0100 Subject: [PATCH 052/270] follow-up 70aa1f1db9ccb3954501849f9c872ab7053c8ff1 --- lib/transaction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/transaction.py b/lib/transaction.py index fdf81ed7c44b..5688dd911367 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -729,7 +729,8 @@ def serialize_witness(self, txin, estimate_size=False): @classmethod def is_segwit_input(cls, txin): - return cls.is_segwit_inputtype(txin['type']) or bool(txin.get('witness', False)) + has_nonzero_witness = txin.get('witness', '00') != '00' + return cls.is_segwit_inputtype(txin['type']) or has_nonzero_witness @classmethod def is_segwit_inputtype(cls, txin_type): From 0693837538d08225adcd65f8114b3ef91b990846 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 26 Dec 2017 01:27:44 +0100 Subject: [PATCH 053/270] freeze hw wallet dependencies into separate file. update versions using freeze_packages.sh. introduce "hardware" setuptools extra. --- contrib/build-wine/build-electrum-git.sh | 3 +- contrib/build-wine/build.sh | 3 +- contrib/build-wine/prepare-hw.sh | 28 ------------------- .../deterministic-build/requirements-hw.txt | 15 ++++++++++ .../requirements.txt | 4 +-- contrib/freeze_packages.sh | 23 +++++++++++++-- contrib/make_packages | 2 +- requirements-hw.txt | 4 +++ setup.py | 8 +++++- 9 files changed, 52 insertions(+), 38 deletions(-) delete mode 100755 contrib/build-wine/prepare-hw.sh create mode 100644 contrib/deterministic-build/requirements-hw.txt rename contrib/{ => deterministic-build}/requirements.txt (82%) create mode 100644 requirements-hw.txt diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index c849a672d813..a8f743588f98 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -55,7 +55,8 @@ cp -r electrum-locale/locale $WINEPREFIX/drive_c/electrum/lib/ cp electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/gui/qt/ # Install frozen dependencies -$PYTHON -m pip install -r ../../requirements.txt +$PYTHON -m pip install -r ../../deterministic-build/requirements.txt +$PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt pushd $WINEPREFIX/drive_c/electrum $PYTHON setup.py install diff --git a/contrib/build-wine/build.sh b/contrib/build-wine/build.sh index efd23bc17c0f..a4e39adf1a3f 100755 --- a/contrib/build-wine/build.sh +++ b/contrib/build-wine/build.sh @@ -14,8 +14,7 @@ rm "$here"/build/* -rf rm "$here"/dist/* -rf $here/prepare-wine.sh && \ -$here/prepare-pyinstaller.sh && \ -$here/prepare-hw.sh || exit 1 +$here/prepare-pyinstaller.sh || exit 1 echo "Resetting modification time in C:\Python..." # (Because of some bugs in pyinstaller) diff --git a/contrib/build-wine/prepare-hw.sh b/contrib/build-wine/prepare-hw.sh deleted file mode 100755 index 1851b7b0ffd9..000000000000 --- a/contrib/build-wine/prepare-hw.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -TREZOR_GIT_URL=https://github.com/trezor/python-trezor.git -KEEPKEY_GIT_URL=https://github.com/keepkey/python-keepkey.git -BTCHIP_GIT_URL=https://github.com/LedgerHQ/btchip-python.git - -BRANCH=master - -PYTHON_VERSION=3.5.4 - -# These settings probably don't need any change -export WINEPREFIX=/opt/wine64 - -PYHOME=c:/python$PYTHON_VERSION -PYTHON="wine $PYHOME/python.exe -OO -B" - -# Let's begin! -cd `dirname $0` -set -e - -cd tmp - -$PYTHON -m pip install setuptools --upgrade -$PYTHON -m pip install cython --upgrade -$PYTHON -m pip install trezor==0.7.16 --upgrade -$PYTHON -m pip install keepkey==4.0.0 --upgrade -$PYTHON -m pip install btchip-python==0.1.23 --upgrade - diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt new file mode 100644 index 000000000000..fc312796fa16 --- /dev/null +++ b/contrib/deterministic-build/requirements-hw.txt @@ -0,0 +1,15 @@ +btchip-python==0.1.24 +certifi==2017.11.5 +chardet==3.0.4 +Cython==0.27.3 +ecdsa==0.13 +hidapi==0.7.99.post21 +idna==2.6 +keepkey==4.0.2 +mnemonic==0.18 +pbkdf2==1.3 +protobuf==3.5.1 +requests==2.18.4 +six==1.11.0 +trezor==0.7.16 +urllib3==1.22 diff --git a/contrib/requirements.txt b/contrib/deterministic-build/requirements.txt similarity index 82% rename from contrib/requirements.txt rename to contrib/deterministic-build/requirements.txt index 52fada94ec89..8594f0bccf59 100644 --- a/contrib/requirements.txt +++ b/contrib/deterministic-build/requirements.txt @@ -5,9 +5,9 @@ ecdsa==0.13 idna==2.6 jsonrpclib-pelix==0.3.1 pbkdf2==1.3 -protobuf==3.5.0.post1 +protobuf==3.5.1 pyaes==1.6.1 -PySocks==1.6.7 +PySocks==1.6.8 qrcode==5.3 requests==2.18.4 six==1.11.0 diff --git a/contrib/freeze_packages.sh b/contrib/freeze_packages.sh index c8e4527b968f..073df5f18173 100755 --- a/contrib/freeze_packages.sh +++ b/contrib/freeze_packages.sh @@ -6,17 +6,34 @@ contrib=$(dirname "$0") which virtualenv > /dev/null 2>&1 || { echo "Please install virtualenv" && exit 1; } +# standard Electrum dependencies + rm "$venv_dir" -rf virtualenv -p $(which python3) $venv_dir source $venv_dir/bin/activate -echo "Installing dependencies" +echo "Installing main dependencies" pushd $contrib/.. python setup.py install popd -pip freeze | sed '/^Electrum/ d' > $contrib/requirements.txt +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 ../requirements-hw.txt --upgrade + +pip freeze | sed '/^Electrum/ d' > $contrib/deterministic-build/requirements-hw.txt + -echo "Updated requirements" +echo "Done. Updated requirements" diff --git a/contrib/make_packages b/contrib/make_packages index b3a36049722e..9cfd32bb24cc 100755 --- a/contrib/make_packages +++ b/contrib/make_packages @@ -9,5 +9,5 @@ if [ $? -ne 0 ] ; then echo "Install pip3" ; exit ; fi rm "$contrib"/../packages/ -r #Install pure python modules in electrum directory -pip3 install -r $contrib/requirements.txt -t $contrib/../packages +pip3 install -r $contrib/deterministic-build/requirements.txt -t $contrib/../packages diff --git a/requirements-hw.txt b/requirements-hw.txt new file mode 100644 index 000000000000..e5f460aaf62a --- /dev/null +++ b/requirements-hw.txt @@ -0,0 +1,4 @@ +Cython>=0.27 +trezor +keepkey +btchip-python diff --git a/setup.py b/setup.py index 5c95d4b6a1f5..3375061b1e99 100755 --- a/setup.py +++ b/setup.py @@ -9,12 +9,15 @@ import imp import argparse +with open('requirements-hw.txt') as f: + requirements_hw = f.read().splitlines() + 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 = [] +data_files = ['requirements-hw.txt'] if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']: parser = argparse.ArgumentParser() @@ -46,6 +49,9 @@ 'jsonrpclib-pelix', 'PySocks>=1.6.6', ], + extras_require={ + 'hardware': requirements_hw, + }, packages=[ 'electrum', 'electrum_gui', From 5c1da002add5a8280737e67a5d9de7979bf08783 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 Jan 2018 05:27:28 +0100 Subject: [PATCH 054/270] local_tx: allow saving already signed tx through tx dialog --- gui/qt/transaction_dialog.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 6be77736c7bb..8f5507ae8660 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -101,8 +101,12 @@ def __init__(self, tx, parent, desc, prompt_if_unsaved): b.clicked.connect(self.do_broadcast) self.save_button = QPushButton(_("Save")) - self.save_button.setDisabled(True) - self.save_button.setToolTip(_("Please sign this transaction in order to save it")) + save_button_disabled = not tx.is_complete() + self.save_button.setDisabled(save_button_disabled) + if save_button_disabled: + self.save_button.setToolTip(_("Please sign this transaction in order to save it")) + else: + self.save_button.setToolTip("") self.save_button.clicked.connect(self.save) self.export_button = b = QPushButton(_("Export")) From b80eacf7bf8e6fe1581c6bc6afb04f2f02baae8d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 Jan 2018 16:44:50 +0100 Subject: [PATCH 055/270] fix #3016 --- gui/qt/transaction_dialog.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 8f5507ae8660..243c1927813c 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -147,11 +147,14 @@ def do_broadcast(self): def closeEvent(self, event): if (self.prompt_if_unsaved and not self.saved - and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))): + and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))): event.ignore() else: event.accept() - dialogs.remove(self) + try: + dialogs.remove(self) + except ValueError: + pass # was not in list already def show_qr(self): text = bfh(str(self.tx)) From 3ae1dd3e6416001ed8a6052549d0093c31e360dc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 Jan 2018 16:45:47 +0100 Subject: [PATCH 056/270] allow coinchooser to spend local txns --- lib/coinchooser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/coinchooser.py b/lib/coinchooser.py index a29aa7326bec..472e3aa3820f 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -286,7 +286,7 @@ def bucket_candidates_prefer_confirmed(self, buckets, sufficient_funds): Any bucket can be: 1. "confirmed" if it only contains confirmed coins; else 2. "unconfirmed" if it does not contain coins with unconfirmed parents - 3. "unconfirmed parent" otherwise + 3. other: e.g. "unconfirmed parent" or "local" This method tries to only use buckets of type 1, and if the coins there are not enough, tries to use the next type but while also selecting @@ -294,9 +294,9 @@ def bucket_candidates_prefer_confirmed(self, buckets, sufficient_funds): """ conf_buckets = [bkt for bkt in buckets if bkt.min_height > 0] unconf_buckets = [bkt for bkt in buckets if bkt.min_height == 0] - unconf_par_buckets = [bkt for bkt in buckets if bkt.min_height == -1] + other_buckets = [bkt for bkt in buckets if bkt.min_height < 0] - bucket_sets = [conf_buckets, unconf_buckets, unconf_par_buckets] + bucket_sets = [conf_buckets, unconf_buckets, other_buckets] already_selected_buckets = [] for bkts_choose_from in bucket_sets: From 48e8bbff69f35d970cfaaade8a20fb0899fb91e4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 Jan 2018 16:48:09 +0100 Subject: [PATCH 057/270] Revert "fix conflicting local txns, e.g. when using RBF" This reverts commit 8a71e46e9b9940932e1d25b75047ea36f535541c. --- lib/wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 0405a52cc7de..db310e612ba5 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -613,7 +613,7 @@ def get_addr_balance(self, address): x += v elif tx_height > 0: c += v - elif tx_height != TX_HEIGHT_LOCAL: + else: u += v if txo in sent: if sent[txo] > 0: @@ -830,7 +830,7 @@ def get_history(self, domain=None): h2.append((tx_hash, height, conf, timestamp, delta, balance)) if balance is None or delta is None: balance = None - elif height != TX_HEIGHT_LOCAL: + else: balance -= delta h2.reverse() From 5522e9ea9f1e1060fcfec03fbdbc3d651a36c8f6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 Jan 2018 17:03:46 +0100 Subject: [PATCH 058/270] TX_ICONS list between kivy and qt needs to be kept in sync --- gui/kivy/uix/screens.py | 2 ++ gui/qt/history_list.py | 1 + 2 files changed, 3 insertions(+) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index d7fff76af139..40713ab99830 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -87,12 +87,14 @@ def show_menu(self, obj): self.add_widget(self.context_menu) +# note: this list needs to be kept in sync with another in qt TX_ICONS = [ "close", "close", "close", "unconfirmed", "close", + "close", "clock1", "clock2", "clock3", diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 8d6b3123b19a..a912ac5c23d4 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -32,6 +32,7 @@ from electrum.util import timestamp_to_datetime, profiler +# note: this list needs to be kept in sync with another in kivy TX_ICONS = [ "warning.png", "warning.png", From 964e06dd10140199f8f8c32f765d6e24f03cc235 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 Jan 2018 22:08:09 +0100 Subject: [PATCH 059/270] use pywin32-ctypes instead of pypiwin32 pypiwin32 might be going away for pywin32 at some point; and while looking into this I've found that pywin32-ctypes should be enough for our needs --- contrib/build-wine/prepare-wine.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 44c20ab2c1f7..ad26fca130c2 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -72,8 +72,8 @@ done # upgrade pip $PYTHON -m pip install pip --upgrade -# Install PyWin32 -$PYTHON -m pip install pypiwin32 +# Install pywin32-ctypes (needed by pyinstaller) +$PYTHON -m pip install pywin32-ctypes # Install PyQt $PYTHON -m pip install PyQt5 From 5fc830580a98dbf48649dd4b98555f6a10634917 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 1 Feb 2018 03:28:44 +0100 Subject: [PATCH 060/270] add block explorer: blockonomics.co --- lib/util.py | 64 +++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/util.py b/lib/util.py index d13b3ff9d935..b8e8ac8e50e3 100644 --- a/lib/util.py +++ b/lib/util.py @@ -431,39 +431,41 @@ def time_difference(distance_in_time, include_seconds): return "over %d years" % (round(distance_in_minutes / 525600)) mainnet_block_explorers = { - 'Biteasy.com': ('https://www.biteasy.com/blockchain', - {'tx': 'transactions', 'addr': 'addresses'}), - 'Bitflyer.jp': ('https://chainflyer.bitflyer.jp', - {'tx': 'Transaction', 'addr': 'Address'}), - 'Blockchain.info': ('https://blockchain.info', - {'tx': 'tx', 'addr': 'address'}), - 'blockchainbdgpzk.onion': ('https://blockchainbdgpzk.onion', - {'tx': 'tx', 'addr': 'address'}), - 'Blockr.io': ('https://btc.blockr.io', - {'tx': 'tx/info', 'addr': 'address/info'}), - 'Blocktrail.com': ('https://www.blocktrail.com/BTC', - {'tx': 'tx', 'addr': 'address'}), - 'BTC.com': ('https://chain.btc.com', - {'tx': 'tx', 'addr': 'address'}), - 'Chain.so': ('https://www.chain.so', - {'tx': 'tx/BTC', 'addr': 'address/BTC'}), - 'Insight.is': ('https://insight.bitpay.com', - {'tx': 'tx', 'addr': 'address'}), - 'TradeBlock.com': ('https://tradeblock.com/blockchain', - {'tx': 'tx', 'addr': 'address'}), - 'BlockCypher.com': ('https://live.blockcypher.com/btc', - {'tx': 'tx', 'addr': 'address'}), - 'Blockchair.com': ('https://blockchair.com/bitcoin', - {'tx': 'transaction', 'addr': 'address'}), - 'system default': ('blockchain:', - {'tx': 'tx', 'addr': 'address'}), + 'Biteasy.com': ('https://www.biteasy.com/blockchain/', + {'tx': 'transactions/', 'addr': 'addresses/'}), + 'Bitflyer.jp': ('https://chainflyer.bitflyer.jp/', + {'tx': 'Transaction/', 'addr': 'Address/'}), + 'Blockchain.info': ('https://blockchain.info/', + {'tx': 'tx/', 'addr': 'address/'}), + 'blockchainbdgpzk.onion': ('https://blockchainbdgpzk.onion/', + {'tx': 'tx/', 'addr': 'address/'}), + 'Blockr.io': ('https://btc.blockr.io/', + {'tx': 'tx/info/', 'addr': 'address/info/'}), + 'Blocktrail.com': ('https://www.blocktrail.com/BTC/', + {'tx': 'tx/', 'addr': 'address/'}), + 'BTC.com': ('https://chain.btc.com/', + {'tx': 'tx/', 'addr': 'address/'}), + 'Chain.so': ('https://www.chain.so/', + {'tx': 'tx/BTC/', 'addr': 'address/BTC/'}), + 'Insight.is': ('https://insight.bitpay.com/', + {'tx': 'tx/', 'addr': 'address/'}), + 'TradeBlock.com': ('https://tradeblock.com/blockchain/', + {'tx': 'tx/', 'addr': 'address/'}), + 'BlockCypher.com': ('https://live.blockcypher.com/btc/', + {'tx': 'tx/', 'addr': 'address/'}), + 'Blockchair.com': ('https://blockchair.com/bitcoin/', + {'tx': 'transaction/', 'addr': 'address/'}), + 'blockonomics.co': ('https://www.blockonomics.co/', + {'tx': 'api/tx?txid=', 'addr': '#/search?q='}), + 'system default': ('blockchain:/', + {'tx': 'tx/', 'addr': 'address/'}), } testnet_block_explorers = { - 'Blocktrail.com': ('https://www.blocktrail.com/tBTC', - {'tx': 'tx', 'addr': 'address'}), - 'system default': ('blockchain:', - {'tx': 'tx', 'addr': 'address'}), + 'Blocktrail.com': ('https://www.blocktrail.com/tBTC/', + {'tx': 'tx/', 'addr': 'address/'}), + 'system default': ('blockchain://000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943/', + {'tx': 'tx/', 'addr': 'address/'}), } def block_explorer_info(): @@ -484,7 +486,7 @@ def block_explorer_URL(config, kind, item): if not kind_str: return url_parts = [be_tuple[0], kind_str, item] - return "/".join(url_parts) + return ''.join(url_parts) # URL decode #_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) From 52acb7ab66d68da9ed5f6ceda58181abf1153825 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 1 Feb 2018 23:48:25 +0100 Subject: [PATCH 061/270] gui remove_local_tx: update all tabs --- gui/qt/history_list.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index a912ac5c23d4..1e43eb4bebe2 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -25,7 +25,7 @@ import webbrowser -from electrum.wallet import UnrelatedTransactionException +from electrum.wallet import UnrelatedTransactionException, TX_HEIGHT_LOCAL from .util import * from electrum.i18n import _ from electrum.util import block_explorer_URL @@ -71,6 +71,7 @@ def get_domain(self): @profiler def on_update(self): + # TODO save and restore scroll position (maybe based on y coord or selected item?) self.wallet = self.parent.wallet h = self.wallet.get_history(self.get_domain()) item = self.currentItem() @@ -162,7 +163,7 @@ def create_menu(self, position): menu = QMenu() - if height == -2: + if height == TX_HEIGHT_LOCAL: menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) @@ -201,14 +202,8 @@ def remove_local_tx(self, delete_tx): for tx in to_delete: self.wallet.remove_transaction(tx) self.wallet.save_transactions(write=True) - root = self.invisibleRootItem() - child_count = root.childCount() - _offset = 0 - for i in range(child_count): - item = root.child(i - _offset) - if item.data(0, Qt.UserRole) in to_delete: - root.removeChild(item) - _offset += 1 + # need to update at least: history_list, utxo_list, address_list + self.parent.need_update.set() def onFileAdded(self, fn): with open(fn) as f: From 4737aa6c2a5a8afcb84bcb3fd523d7edc6ca4b22 Mon Sep 17 00:00:00 2001 From: ghost43 Date: Fri, 2 Feb 2018 23:26:25 +0100 Subject: [PATCH 062/270] migrated some commands from addr to scripthash (#3826) * migrated some commands from addr to scripthash * remove getproof command --- lib/commands.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/commands.py b/lib/commands.py index 63f63c76ed96..6b204a850ba5 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -34,7 +34,7 @@ from decimal import Decimal from .import util -from .util import bfh, bh2u, format_satoshis, json_decode +from .util import bfh, bh2u, format_satoshis, json_decode, print_error from .import bitcoin from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS from .i18n import _ @@ -175,7 +175,8 @@ def getaddresshistory(self, address): """Return the transaction history of any address. Note: This is a walletless server query, results are not checked by SPV. """ - return self.network.synchronous_get(('blockchain.address.get_history', [address])) + sh = bitcoin.address_to_scripthash(address) + return self.network.synchronous_get(('blockchain.scripthash.get_history', [sh])) @command('w') def listunspent(self): @@ -192,7 +193,8 @@ def getaddressunspent(self, address): """Returns the UTXO list of any address. Note: This is a walletless server query, results are not checked by SPV. """ - return self.network.synchronous_get(('blockchain.address.listunspent', [address])) + sh = bitcoin.address_to_scripthash(address) + return self.network.synchronous_get(('blockchain.scripthash.listunspent', [sh])) @command('') def serialize(self, jsontx): @@ -314,20 +316,12 @@ def getaddressbalance(self, address): """Return the balance of any address. Note: This is a walletless server query, results are not checked by SPV. """ - out = self.network.synchronous_get(('blockchain.address.get_balance', [address])) + sh = bitcoin.address_to_scripthash(address) + out = self.network.synchronous_get(('blockchain.scripthash.get_balance', [sh])) out["confirmed"] = str(Decimal(out["confirmed"])/COIN) out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN) return out - @command('n') - def getproof(self, address): - """Get Merkle branch of an address in the UTXO set""" - p = self.network.synchronous_get(('blockchain.address.get_proof', [address])) - out = [] - for i,s in p: - out.append(i) - return out - @command('n') def getmerkle(self, txid, height): """Get Merkle branch of a transaction included in a block. Electrum From b9fa89ec19f867d6a91d8583293ce4d41d6da89c Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 30 Jan 2018 21:14:28 +0100 Subject: [PATCH 063/270] Update macOS build --- README.rst | 12 ++--------- contrib/build-osx/README.md | 17 ++++++++++++++++ contrib/build-osx/make_osx | 34 ++++++++++++++++++++++++++++++++ contrib/{ => build-osx}/osx.spec | 4 ++-- contrib/make_osx | 6 ------ 5 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 contrib/build-osx/README.md create mode 100755 contrib/build-osx/make_osx rename contrib/{ => build-osx}/osx.spec (95%) delete mode 100755 contrib/make_osx diff --git a/README.rst b/README.rst index 43d3257f640b..9b183235477a 100644 --- a/README.rst +++ b/README.rst @@ -93,20 +93,12 @@ This directory contains the python dependencies used by Electrum. Mac OS X / macOS -------- -:: - - # On MacPorts installs: - sudo python3 setup-release.py py2app - - # On Homebrew installs: - ARCHFLAGS="-arch i386 -arch x86_64" sudo python3 setup-release.py py2app --includes sip - - sudo hdiutil create -fs HFS+ -volname "Electrum" -srcfolder dist/Electrum.app dist/electrum-VERSION-macosx.dmg +See `contrib/build-osx/`. Windows ------- -See `contrib/build-wine/README` file. +See `contrib/build-wine/`. Android diff --git a/contrib/build-osx/README.md b/contrib/build-osx/README.md new file mode 100644 index 000000000000..48a271d465c3 --- /dev/null +++ b/contrib/build-osx/README.md @@ -0,0 +1,17 @@ +Building Mac OS binaries +======================== + +This guide explains how to build Electrum binaries for macOS systems. +We build our binaries on El Capitan (10.11.6) as building it on High Sierra +makes the binaries incompatible with older versions. + + +## 1. Run the script + + + + ./make_osx + +## 2. Done + +Hopefully it will be that simple. \ No newline at end of file diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx new file mode 100755 index 000000000000..360b29968c42 --- /dev/null +++ b/contrib/build-osx/make_osx @@ -0,0 +1,34 @@ +#!/bin/bash +build_dir=$(dirname "$0") +test -n "$build_dir" -a -d "$build_dir" || exit +cd $build_dir/../.. + +export PYTHONHASHSEED=22 +VERSION=`git describe --tags` + +sw_vers +python3 --version +echo -n "Pyinstaller version " +pyinstaller --version + +rm -rf ./dist + + +rm -rf /tmp/electrum-build > /dev/null 2>&1 +mkdir /tmp/electrum-build + + +echo "Downloading icons and locale..." +for repo in icons locale; do + git clone https://github.com/spesmilo/electrum-$repo /tmp/electrum-build/electrum-$repo +done + +cp -R /tmp/electrum-build/electrum-locale/locale/ ./lib/locale/ +cp /tmp/electrum-build/electrum-icons/icons_rc.py ./gui/qt/ + +echo "Building Electrum..." +python3 setup.py install --user > /dev/null +python3 -m pip install pyqt5 --user + +pyinstaller --noconfirm --ascii --name $VERSION contrib/build-osx/osx.spec +hdiutil create -fs HFS+ -volname "Electrum" -srcfolder dist/Electrum.app dist/electrum-$VERSION.dmg diff --git a/contrib/osx.spec b/contrib/build-osx/osx.spec similarity index 95% rename from contrib/osx.spec rename to contrib/build-osx/osx.spec index 9dabf291cb70..bd3d8cf48b60 100644 --- a/contrib/osx.spec +++ b/contrib/build-osx/osx.spec @@ -12,8 +12,8 @@ for i, x in enumerate(sys.argv): else: raise BaseException('no version') -electrum = "../" -block_cipher=None +electrum = os.path.abspath("../../") +block_cipher = None # see https://github.com/pyinstaller/pyinstaller/issues/2005 hiddenimports = [] diff --git a/contrib/make_osx b/contrib/make_osx deleted file mode 100755 index f354e331a9e8..000000000000 --- a/contrib/make_osx +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -rm -rf dist -export PYTHONHASHSEED=22 -VERSION=`git describe --tags` -pyinstaller --noconfirm --ascii --name $VERSION contrib/osx.spec -hdiutil create -fs HFS+ -volname "Electrum" -srcfolder dist/Electrum.app dist/electrum-$VERSION.dmg From 0f40c147924d466eb1bc2caf86680e6b22b06dfc Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Wed, 31 Jan 2018 13:16:27 +0100 Subject: [PATCH 064/270] Use requirements files --- contrib/build-osx/README.md | 4 +-- contrib/build-osx/make_osx | 57 +++++++++++++++++++++++++++++++------ contrib/build-osx/osx.spec | 2 +- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/contrib/build-osx/README.md b/contrib/build-osx/README.md index 48a271d465c3..af43e05ce64f 100644 --- a/contrib/build-osx/README.md +++ b/contrib/build-osx/README.md @@ -5,6 +5,8 @@ This guide explains how to build Electrum binaries for macOS systems. We build our binaries on El Capitan (10.11.6) as building it on High Sierra makes the binaries incompatible with older versions. +This assumes that the Xcode command line tools (and thus git) are already installed. + ## 1. Run the script @@ -13,5 +15,3 @@ makes the binaries incompatible with older versions. ./make_osx ## 2. Done - -Hopefully it will be that simple. \ No newline at end of file diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx index 360b29968c42..d8af8c9d648d 100755 --- a/contrib/build-osx/make_osx +++ b/contrib/build-osx/make_osx @@ -1,14 +1,45 @@ #!/bin/bash +RED='\033[0;31m' +BLUE='\033[0,34m' +NC='\033[0m' # No Color +function info { + printf "\r💬 ${BLUE}INFO:${NC} ${1}\n" +} +function fail { + printf "\r🗯 ${RED}ERROR:${NC} ${1}\n" + exit 1 +} + build_dir=$(dirname "$0") test -n "$build_dir" -a -d "$build_dir" || exit cd $build_dir/../.. export PYTHONHASHSEED=22 VERSION=`git describe --tags` +PYTHON_VERSION=3.6.4 + + +info "Installing Python $PYTHON_VERSION" +export PATH="~/.pyenv/bin:~/.pyenv/shims:$PATH:~/Library/Python/3.6/bin" +if [ -d "~/.pyenv" ]; then + pyenv update +else + curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash > /dev/null 2>&1 +fi +PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install -s $PYTHON_VERSION && \ +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 "Using these versions for building Electrum:" sw_vers python3 --version -echo -n "Pyinstaller version " +echo -n "Pyinstaller " pyinstaller --version rm -rf ./dist @@ -17,8 +48,7 @@ rm -rf ./dist rm -rf /tmp/electrum-build > /dev/null 2>&1 mkdir /tmp/electrum-build - -echo "Downloading icons and locale..." +info "Downloading icons and locale..." for repo in icons locale; do git clone https://github.com/spesmilo/electrum-$repo /tmp/electrum-build/electrum-$repo done @@ -26,9 +56,20 @@ done cp -R /tmp/electrum-build/electrum-locale/locale/ ./lib/locale/ cp /tmp/electrum-build/electrum-icons/icons_rc.py ./gui/qt/ -echo "Building Electrum..." -python3 setup.py install --user > /dev/null -python3 -m pip install pyqt5 --user +info "Installing requirements..." +python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \ +python3 -m pip install pyqt5 --user || \ +fail "Could not install requirements" + +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 binary" +pyinstaller --noconfirm --ascii --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary" -pyinstaller --noconfirm --ascii --name $VERSION contrib/build-osx/osx.spec -hdiutil create -fs HFS+ -volname "Electrum" -srcfolder dist/Electrum.app dist/electrum-$VERSION.dmg +info "Creating .DMG" +hdiutil create -fs HFS+ -volname "Electrum" -srcfolder dist/Electrum.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 bd3d8cf48b60..cfce7172f488 100644 --- a/contrib/build-osx/osx.spec +++ b/contrib/build-osx/osx.spec @@ -12,7 +12,7 @@ for i, x in enumerate(sys.argv): else: raise BaseException('no version') -electrum = os.path.abspath("../../") +electrum = os.path.abspath(".") + "/" block_cipher = None # see https://github.com/pyinstaller/pyinstaller/issues/2005 From ea2a2aaf09011fe98c22268f640ddecad246dfa5 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 3 Feb 2018 07:38:36 +0100 Subject: [PATCH 065/270] prepare release 3.0.6 --- RELEASE-NOTES | 5 +++++ lib/version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 2a06061e836b..f05b0ac5e6cd 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,8 @@ +# Release 3.0.6 : + + * Fix transaction parsing bug #3788 + + # Release 3.0.5 : (Security update) This is a follow-up to the 3.0.4 release, which did not completely fix diff --git a/lib/version.py b/lib/version.py index 2f71ac17b536..877fc1388c4a 100644 --- a/lib/version.py +++ b/lib/version.py @@ -1,4 +1,4 @@ -ELECTRUM_VERSION = '3.0.5' # version of the client package +ELECTRUM_VERSION = '3.0.6' # version of the client package PROTOCOL_VERSION = '1.1' # protocol version requested # The hash of the mnemonic seed must begin with this From 37904bc1109564e481253f656ef00f5bd9a44771 Mon Sep 17 00:00:00 2001 From: Wampum Date: Sun, 4 Feb 2018 05:21:25 +0000 Subject: [PATCH 066/270] show address on trezor for multisig (#3823) * show address on trezor for multisig * Show menu option properly. * remove useless line --- plugins/trezor/plugin.py | 38 +++++++++++++++++++++++++----------- plugins/trezor/qt_generic.py | 13 ++++++------ 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py index 19cdc026852a..a381ed8768fa 100644 --- a/plugins/trezor/plugin.py +++ b/plugins/trezor/plugin.py @@ -243,23 +243,39 @@ def sign_transaction(self, keystore, tx, prev_tx, xpub_path): raw = bh2u(signed_tx) tx.update_signatures(raw) - def show_address(self, wallet, address): - client = self.get_client(wallet.keystore) + def show_address(self, wallet, keystore, address): + client = self.get_client(keystore) if not client.atleast_version(1, 3): - wallet.keystore.handler.show_error(_("Your device firmware is too old")) + keystore.handler.show_error(_("Your device firmware is too old")) return change, index = wallet.get_address_index(address) - derivation = wallet.keystore.derivation + derivation = keystore.derivation address_path = "%s/%d/%d"%(derivation, change, index) address_n = client.expand_path(address_path) - script_gen = wallet.keystore.get_script_gen() - if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: - script_type = self.types.InputScriptType.SPENDWITNESS - elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: - script_type = self.types.InputScriptType.SPENDP2SHWITNESS + xpubs = wallet.get_master_public_keys() + if len(xpubs) == 1: + script_gen = keystore.get_script_gen() + if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: + script_type = self.types.InputScriptType.SPENDWITNESS + elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: + script_type = self.types.InputScriptType.SPENDP2SHWITNESS + else: + script_type = self.types.InputScriptType.SPENDADDRESS + client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) else: - script_type = self.types.InputScriptType.SPENDADDRESS - client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) + def f(xpub): + node = self.ckd_public.deserialize(xpub) + return self.types.HDNodePathType(node=node, address_n=[change, index]) + pubkeys = wallet.get_public_keys(address) + # sort xpubs using the order of pubkeys + sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) + pubkeys = list(map(f, sorted_xpubs)) + multisig = self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=[b''] * wallet.n, + m=wallet.m, + ) + client.get_address(self.get_coin_name(), address_n, True, multisig=multisig) def tx_inputs(self, tx, for_sig=False, script_gen=SCRIPT_GEN_LEGACY): inputs = [] diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py index f0510e131cea..a4b9c07c79d5 100644 --- a/plugins/trezor/qt_generic.py +++ b/plugins/trezor/qt_generic.py @@ -188,13 +188,14 @@ def create_handler(self, window): @hook def receive_menu(self, menu, addrs, wallet): - if type(wallet) is not Standard_Wallet: + if len(addrs) != 1: return - keystore = wallet.get_keystore() - if type(keystore) == self.keystore_class and len(addrs) == 1: - def show_address(): - keystore.thread.add(partial(self.show_address, wallet, addrs[0])) - menu.addAction(_("Show on %s") % self.device, show_address) + for keystore in wallet.get_keystores(): + if type(keystore) == self.keystore_class: + def show_address(): + keystore.thread.add(partial(self.show_address, wallet, keystore, addrs[0])) + menu.addAction(_("Show on %s") % self.device, show_address) + break def show_settings_dialog(self, window, keystore): device_id = self.choose_device(window, keystore) From ffdc36285ba813c6907d9eea05dbba5731facddc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 4 Feb 2018 07:26:55 +0100 Subject: [PATCH 067/270] use string.format instead of old style (%) formatting --- gui/kivy/main_window.py | 4 +-- gui/kivy/uix/dialogs/installwizard.py | 4 +-- gui/kivy/uix/dialogs/tx_dialog.py | 2 +- gui/kivy/uix/ui_screens/network.kv | 4 +-- gui/qt/address_list.py | 4 +-- gui/qt/console.py | 3 +- gui/qt/contact_list.py | 4 +-- gui/qt/history_list.py | 4 +-- gui/qt/installwizard.py | 12 +++---- gui/qt/invoice_list.py | 2 +- gui/qt/request_list.py | 2 +- gui/qt/transaction_dialog.py | 2 +- lib/base_wizard.py | 2 +- lib/commands.py | 2 +- lib/plugins.py | 10 +++--- lib/wallet.py | 2 +- plugins/digitalbitbox/digitalbitbox.py | 2 +- plugins/digitalbitbox/qt.py | 2 +- plugins/greenaddress_instant/qt.py | 4 +-- plugins/hw_wallet/qt.py | 2 +- plugins/keepkey/clientbase.py | 48 +++++++++++++------------- plugins/keepkey/plugin.py | 14 ++++---- plugins/keepkey/qt_generic.py | 18 +++++----- plugins/ledger/auth2fa.py | 2 +- plugins/ledger/ledger.py | 2 +- plugins/trezor/clientbase.py | 48 +++++++++++++------------- plugins/trezor/plugin.py | 14 ++++---- plugins/trezor/qt_generic.py | 16 ++++----- plugins/trustedcoin/qt.py | 2 +- plugins/trustedcoin/trustedcoin.py | 8 ++--- 30 files changed, 123 insertions(+), 122 deletions(-) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 621beaf163d3..020388fadcd1 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -631,7 +631,7 @@ def update_status(self, *dt): if not self.wallet.up_to_date or server_height == 0: status = _("Synchronizing...") elif server_lag > 1: - status = _("Server lagging (%d blocks)"%server_lag) + status = _("Server lagging ({} blocks)").format(server_lag) else: c, u, x = self.wallet.get_balance() text = self.format_amount(c+x+u) @@ -846,7 +846,7 @@ def delete_wallet(self): def _delete_wallet(self, b): if b: basename = os.path.basename(self.wallet.storage.path) - self.protected(_("Enter your PIN code to confirm deletion of %s") % basename, self.__delete_wallet, ()) + self.protected(_("Enter your PIN code to confirm deletion of {}").format(basename), self.__delete_wallet, ()) def __delete_wallet(self, pw): wallet_path = self.get_wallet_path() diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py index cf7b44cf3dd0..651810907cdc 100644 --- a/gui/kivy/uix/dialogs/installwizard.py +++ b/gui/kivy/uix/dialogs/installwizard.py @@ -135,7 +135,7 @@ height: self.minimum_height Label: color: root.text_color - text: _('From %d cosigners')%n.value + text: _('From {} cosigners').format(n.value) Slider: id: n range: 2, 5 @@ -143,7 +143,7 @@ value: 2 Label: color: root.text_color - text: _('Require %d signatures')%m.value + text: _('Require {} signatures').format(m.value) Slider: id: m range: 1, n.value diff --git a/gui/kivy/uix/dialogs/tx_dialog.py b/gui/kivy/uix/dialogs/tx_dialog.py index d5b876993239..5339b0d39c01 100644 --- a/gui/kivy/uix/dialogs/tx_dialog.py +++ b/gui/kivy/uix/dialogs/tx_dialog.py @@ -112,7 +112,7 @@ def update(self): if timestamp: self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] elif exp_n: - self.date_str = _('Within %d blocks') % exp_n if exp_n > 0 else _('unknown (low fee)') + self.date_str = _('Within {} blocks').format(exp_n) if exp_n > 0 else _('unknown (low fee)') else: self.date_str = '' diff --git a/gui/kivy/uix/ui_screens/network.kv b/gui/kivy/uix/ui_screens/network.kv index 71e18ab4950a..f499618a8d51 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: _("%d 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 %d')%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 diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index 2310489bad17..ac563a6d8e6f 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -135,10 +135,10 @@ def create_menu(self, position): if not multi_select: column_title = self.headerItem().text(col) copy_text = item.text(col) - menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(copy_text)) + menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(copy_text)) menu.addAction(_('Details'), lambda: self.parent.show_address(addr)) if col in self.editable_columns: - menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, col)) + menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, col)) menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr)) if self.wallet.can_export(): menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr)) diff --git a/gui/qt/console.py b/gui/qt/console.py index 5016e45144c4..bde05a3dfa2d 100644 --- a/gui/qt/console.py +++ b/gui/qt/console.py @@ -203,7 +203,8 @@ def write(self, text): self.skip = not self.skip if type(self.namespace.get(command)) == type(lambda:None): - self.appendPlainText("'%s' is a function. Type '%s()' to use it in the Python console."%(command, command)) + self.appendPlainText("'{}' is a function. Type '{}()' to use it in the Python console." + .format(command, command)) self.newPrompt() return diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py index 8c1b3aa7ccac..7e8dda1eddda 100644 --- a/gui/qt/contact_list.py +++ b/gui/qt/contact_list.py @@ -72,10 +72,10 @@ def create_menu(self, position): column = self.currentColumn() column_title = self.headerItem().text(column) column_data = '\n'.join([item.text(column) for item in selected]) - menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) + menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) if column in self.editable_columns: item = self.currentItem() - menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column)) + menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column)) menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(keys)) menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(keys)) URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, keys)] diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 1e43eb4bebe2..d34fafe6754b 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -166,9 +166,9 @@ def create_menu(self, position): if height == TX_HEIGHT_LOCAL: menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) - menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) + menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) if column in self.editable_columns: - menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column)) + menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column)) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py index e446f57a8726..d23631aca1a2 100644 --- a/gui/qt/installwizard.py +++ b/gui/qt/installwizard.py @@ -26,7 +26,7 @@ class GoBack(Exception): MSG_ENTER_ANYTHING = _("Please enter a seed phrase, a master key, a list of " "Bitcoin addresses, or a list of private keys") MSG_ENTER_SEED_OR_MPK = _("Please enter a seed phrase or a master key (xpub or xprv):") -MSG_COSIGNER = _("Please enter the master public key of cosigner #%d:") +MSG_COSIGNER = _("Please enter the master public key of cosigner #{}:") MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\ + _("Leave this field empty if you want to disable encryption.") MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\ @@ -275,8 +275,8 @@ def on_filename(filename): path = self.storage.path if self.storage.requires_split(): self.hide() - msg = _("The wallet '%s' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n" - "Do you want to split your wallet into multiple files?"%path) + msg = _("The wallet '{}' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n" + "Do you want to split your wallet into multiple files?").format(path) if not self.question(msg): return file_list = '\n'.join(self.storage.split_accounts()) @@ -294,10 +294,10 @@ def on_filename(filename): action = self.storage.get_action() if action and action != 'new': self.hide() - msg = _("The file '%s' contains an incompletely created wallet.\n" - "Do you want to complete its creation now?") % path + msg = _("The file '{}' contains an incompletely created wallet.\n" + "Do you want to complete its creation now?").format(path) if not self.question(msg): - if self.question(_("Do you want to delete '%s'?") % path): + if self.question(_("Do you want to delete '{}'?").format(path)): os.remove(path) self.show_warning(_('The file was removed')) return diff --git a/gui/qt/invoice_list.py b/gui/qt/invoice_list.py index 761ac4088845..19cfea60aaec 100644 --- a/gui/qt/invoice_list.py +++ b/gui/qt/invoice_list.py @@ -76,7 +76,7 @@ def create_menu(self, position): pr = self.parent.invoices.get(key) status = self.parent.invoices.get_status(key) if column_data: - menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) + menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Details"), lambda: self.parent.show_invoice(key)) if status == PR_UNPAID: menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(key)) diff --git a/gui/qt/request_list.py b/gui/qt/request_list.py index f8d5ea92e7b4..59b084ff9583 100644 --- a/gui/qt/request_list.py +++ b/gui/qt/request_list.py @@ -115,7 +115,7 @@ def create_menu(self, position): column_title = self.headerItem().text(column) column_data = item.text(column) menu = QMenu(self) - menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) + menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Copy URI"), lambda: self.parent.view_and_paste('URI', '', self.parent.get_request_URI(addr))) menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr)) menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr)) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 243c1927813c..03169bebaed9 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -218,7 +218,7 @@ def update(self): if timestamp: time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] - self.date_label.setText(_("Date: %s")%time_str) + self.date_label.setText(_("Date: {}").format(time_str)) self.date_label.show() elif exp_n: text = '%d blocks'%(exp_n) if exp_n > 0 else _('unknown (low fee)') diff --git a/lib/base_wizard.py b/lib/base_wizard.py index e586f4d1b103..b73e195379d5 100644 --- a/lib/base_wizard.py +++ b/lib/base_wizard.py @@ -224,7 +224,7 @@ def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET): choices = [] for name, info in devices: state = _("initialized") if info.initialized else _("wiped") - label = info.label or _("An unnamed %s")%name + label = info.label or _("An unnamed {}").format(name) descr = "%s [%s, %s]" % (label, name, state) choices.append(((name, info), descr)) msg = _('Select a device') + ':' diff --git a/lib/commands.py b/lib/commands.py index 6b578f7147aa..49760268e344 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -808,7 +808,7 @@ def subparser_call(self, parser, namespace, values, option_string=None): parser = self._name_parser_map[parser_name] except KeyError: tup = parser_name, ', '.join(self._name_parser_map) - msg = _('unknown parser %r (choices: %s)') % tup + msg = _('unknown parser {!r} (choices: {})').format(*tup) raise ArgumentError(self, msg) # parse all the remaining options into the namespace # store any unrecognized options on the object, so that the top diff --git a/lib/plugins.py b/lib/plugins.py index c35d174607ef..e37fe08a5d71 100644 --- a/lib/plugins.py +++ b/lib/plugins.py @@ -442,11 +442,11 @@ def force_pair_xpub(self, plugin, handler, info, xpub, derivation, devices): # The user input has wrong PIN or passphrase, or cancelled input, # or it is not pairable raise DeviceUnpairableError( - _('Electrum cannot pair with your %s.\n\n' + _('Electrum cannot pair with your {}.\n\n' 'Before you request bitcoins to be sent to addresses in this ' 'wallet, ensure you can pair with your device, or that you have ' 'its seed (and passphrase, if any). Otherwise all bitcoins you ' - 'receive will be unspendable.') % plugin.device) + 'receive will be unspendable.').format(plugin.device)) def unpaired_device_infos(self, handler, plugin, devices=None): '''Returns a list of DeviceInfo objects: one for each connected, @@ -472,9 +472,9 @@ def select_device(self, plugin, handler, keystore, devices=None): infos = self.unpaired_device_infos(handler, plugin, devices) if infos: break - msg = _('Please insert your %s. Verify the cable is ' + msg = _('Please insert your {}. Verify the cable is ' 'connected and that no other application is using it.\n\n' - 'Try to connect again?') % plugin.device + 'Try to connect again?').format(plugin.device) if not handler.yes_no_question(msg): raise UserCancelled() devices = None @@ -484,7 +484,7 @@ def select_device(self, plugin, handler, keystore, devices=None): for info in infos: if info.label == keystore.label: return info - msg = _("Please select which %s device to use:") % plugin.device + msg = _("Please select which {} device to use:").format(plugin.device) descriptions = [info.label + ' (%s)'%(_("initialized") if info.initialized else _("wiped")) for info in infos] c = handler.query_choice(msg, descriptions) if c is None: diff --git a/lib/wallet.py b/lib/wallet.py index cf7c514d64e1..29c0f9f021d4 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -531,7 +531,7 @@ def get_tx_info(self, tx): height, conf, timestamp = self.get_tx_height(tx_hash) if height > 0: if conf: - status = _("%d confirmations") % conf + status = _("{} confirmations").format(conf) else: status = _('Not verified') elif height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED): diff --git a/plugins/digitalbitbox/digitalbitbox.py b/plugins/digitalbitbox/digitalbitbox.py index 3d08f40459fb..c57e395dd4e2 100644 --- a/plugins/digitalbitbox/digitalbitbox.py +++ b/plugins/digitalbitbox/digitalbitbox.py @@ -421,7 +421,7 @@ def give_error(self, message, clear_client = False): def decrypt_message(self, pubkey, message, password): - raise RuntimeError(_('Encryption and decryption are currently not supported for %s') % self.device) + raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) def sign_message(self, sequence, message, password): diff --git a/plugins/digitalbitbox/qt.py b/plugins/digitalbitbox/qt.py index 389dcdb5f72d..8930d6244852 100644 --- a/plugins/digitalbitbox/qt.py +++ b/plugins/digitalbitbox/qt.py @@ -39,7 +39,7 @@ def show_address(): } self.comserver_post_notification(verify_request_payload) - menu.addAction(_("Show on %s") % self.device, show_address) + menu.addAction(_("Show on {}").format(self.device), show_address) class DigitalBitbox_Handler(QtHandlerBase): diff --git a/plugins/greenaddress_instant/qt.py b/plugins/greenaddress_instant/qt.py index 137390b44ade..96cd87b24d6d 100644 --- a/plugins/greenaddress_instant/qt.py +++ b/plugins/greenaddress_instant/qt.py @@ -96,9 +96,9 @@ def do_verify(self, d): # 3. display the result if response.get('verified'): - d.show_message(_('%s is covered by GreenAddress instant confirmation') % (tx.txid()), title=_('Verification successful!')) + d.show_message(_('{} is covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification successful!')) else: - d.show_critical(_('%s is not covered by GreenAddress instant confirmation') % (tx.txid()), title=_('Verification failed!')) + d.show_critical(_('{} is not covered by GreenAddress instant confirmation').format(tx.txid()), title=_('Verification failed!')) except BaseException as e: import traceback traceback.print_exc(file=sys.stdout) diff --git a/plugins/hw_wallet/qt.py b/plugins/hw_wallet/qt.py index e6451157cac9..a9f7291c41f4 100644 --- a/plugins/hw_wallet/qt.py +++ b/plugins/hw_wallet/qt.py @@ -144,7 +144,7 @@ def word_dialog(self, msg): def message_dialog(self, msg, on_cancel): # Called more than once during signing, to confirm output and fee self.clear_dialog() - title = _('Please check your %s device') % self.device + title = _('Please check your {} device').format(self.device) self.dialog = dialog = WindowModalDialog(self.top_level_window(), title) l = QLabel(msg) vbox = QVBoxLayout(dialog) diff --git a/plugins/keepkey/clientbase.py b/plugins/keepkey/clientbase.py index de7d47c144ff..4d5cf7c85f25 100644 --- a/plugins/keepkey/clientbase.py +++ b/plugins/keepkey/clientbase.py @@ -11,15 +11,15 @@ class GuiMixin(object): # Requires: self.proto, self.device messages = { - 3: _("Confirm the transaction output on your %s device"), - 4: _("Confirm internal entropy on your %s device to begin"), - 5: _("Write down the seed word shown on your %s"), - 6: _("Confirm on your %s that you want to wipe it clean"), - 7: _("Confirm on your %s device the message to sign"), + 3: _("Confirm the transaction output on your {} device"), + 4: _("Confirm internal entropy on your {} device to begin"), + 5: _("Write down the seed word shown on your {}"), + 6: _("Confirm on your {} that you want to wipe it clean"), + 7: _("Confirm on your {} device the message to sign"), 8: _("Confirm the total amount spent and the transaction fee on your " - "%s device"), - 10: _("Confirm wallet address on your %s device"), - 'default': _("Check your %s device to continue"), + "{} device"), + 10: _("Confirm wallet address on your {} device"), + 'default': _("Check your {} device to continue"), } def callback_Failure(self, msg): @@ -38,18 +38,18 @@ def callback_ButtonRequest(self, msg): message = self.msg if not message: message = self.messages.get(msg.code, self.messages['default']) - self.handler.show_message(message % self.device, self.cancel) + self.handler.show_message(message.format(self.device), self.cancel) return self.proto.ButtonAck() def callback_PinMatrixRequest(self, msg): if msg.type == 2: - msg = _("Enter a new PIN for your %s:") + msg = _("Enter a new PIN for your {}:") elif msg.type == 3: - msg = (_("Re-enter the new PIN for your %s.\n\n" + msg = (_("Re-enter the new PIN for your {}.\n\n" "NOTE: the positions of the numbers have changed!")) else: - msg = _("Enter your current %s PIN:") - pin = self.handler.get_pin(msg % self.device) + msg = _("Enter your current {} PIN:") + pin = self.handler.get_pin(msg.format(self.device)) if not pin: return self.proto.Cancel() return self.proto.PinMatrixAck(pin=pin) @@ -57,9 +57,9 @@ def callback_PinMatrixRequest(self, msg): def callback_PassphraseRequest(self, req): if self.creating_wallet: msg = _("Enter a passphrase to generate this wallet. Each time " - "you use this wallet your %s will prompt you for the " + "you use this wallet your {} will prompt you for the " "passphrase. If you forget the passphrase you cannot " - "access the bitcoins in the wallet.") % self.device + "access the bitcoins in the wallet.").format(self.device) else: msg = _("Enter the passphrase to unlock this wallet:") passphrase = self.handler.get_passphrase(msg, self.creating_wallet) @@ -70,8 +70,8 @@ def callback_PassphraseRequest(self, req): def callback_WordRequest(self, msg): self.step += 1 - msg = _("Step %d/24. Enter seed word as explained on " - "your %s:") % (self.step, self.device) + msg = _("Step {}/24. Enter seed word as explained on " + "your {}:").format(self.step, self.device) word = self.handler.get_word(msg) # Unfortunately the device can't handle self.proto.Cancel() return self.proto.WordAck(word=word) @@ -155,27 +155,27 @@ def get_xpub(self, bip32_path, xtype): def toggle_passphrase(self): if self.features.passphrase_protection: - self.msg = _("Confirm on your %s device to disable passphrases") + self.msg = _("Confirm on your {} device to disable passphrases") else: - self.msg = _("Confirm on your %s device to enable passphrases") + self.msg = _("Confirm on your {} device to enable passphrases") enabled = not self.features.passphrase_protection self.apply_settings(use_passphrase=enabled) def change_label(self, label): - self.msg = _("Confirm the new label on your %s device") + self.msg = _("Confirm the new label on your {} device") self.apply_settings(label=label) def change_homescreen(self, homescreen): - self.msg = _("Confirm on your %s device to change your home screen") + self.msg = _("Confirm on your {} device to change your home screen") self.apply_settings(homescreen=homescreen) def set_pin(self, remove): if remove: - self.msg = _("Confirm on your %s device to disable PIN protection") + self.msg = _("Confirm on your {} device to disable PIN protection") elif self.features.pin_protection: - self.msg = _("Confirm on your %s device to change your PIN") + self.msg = _("Confirm on your {} device to change your PIN") else: - self.msg = _("Confirm on your %s device to set a PIN") + self.msg = _("Confirm on your {} device to set a PIN") self.change_pin(remove) def clear_session(self): diff --git a/plugins/keepkey/plugin.py b/plugins/keepkey/plugin.py index a273d6b33214..acbad49c092b 100644 --- a/plugins/keepkey/plugin.py +++ b/plugins/keepkey/plugin.py @@ -30,7 +30,7 @@ def get_client(self, force_pair=True): return self.plugin.get_client(self, force_pair) def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by %s') % self.device) + raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) def sign_message(self, sequence, message, password): client = self.get_client() @@ -119,9 +119,9 @@ def create_client(self, device, handler): return None if not client.atleast_version(*self.minimum_firmware): - msg = (_('Outdated %s firmware for device labelled %s. Please ' - 'download the updated firmware from %s') % - (self.device, client.label(), self.firmware_URL)) + msg = (_('Outdated {} firmware for device labelled {}. Please ' + 'download the updated firmware from {}') + .format(self.device, client.label(), self.firmware_URL)) self.print_error(msg) handler.show_error(msg) return None @@ -143,14 +143,14 @@ def get_coin_name(self): def initialize_device(self, device_id, wizard, handler): # Initialization method - msg = _("Choose how you want to initialize your %s.\n\n" + msg = _("Choose how you want to initialize your {}.\n\n" "The first two methods are secure as no secret information " "is entered into your computer.\n\n" "For the last two methods you input secrets on your keyboard " - "and upload them to your %s, and so you should " + "and upload them to your {}, and so you should " "only do those on a computer you know to be trustworthy " "and free of malware." - ) % (self.device, self.device) + ).format(self.device, self.device) choices = [ # Must be short as QT doesn't word-wrap radio button text (TIM_NEW, _("Let the device generate a completely new seed randomly")), diff --git a/plugins/keepkey/qt_generic.py b/plugins/keepkey/qt_generic.py index d564be6ddeb5..a66e8f3d23f6 100644 --- a/plugins/keepkey/qt_generic.py +++ b/plugins/keepkey/qt_generic.py @@ -194,7 +194,7 @@ def receive_menu(self, menu, addrs, wallet): if type(keystore) == self.keystore_class and len(addrs) == 1: def show_address(): keystore.thread.add(partial(self.show_address, wallet, addrs[0])) - menu.addAction(_("Show on %s") % self.device, show_address) + menu.addAction(_("Show on {}").format(self.device), show_address) def show_settings_dialog(self, window, keystore): device_id = self.choose_device(window, keystore) @@ -227,7 +227,7 @@ def clean_text(widget): bg = QButtonGroup() for i, count in enumerate([12, 18, 24]): rb = QRadioButton(gb) - rb.setText(_("%d words") % count) + rb.setText(_("{} words").format(count)) bg.addButton(rb) bg.setId(rb, i) hbox1.addWidget(rb) @@ -292,7 +292,7 @@ class SettingsDialog(WindowModalDialog): their PIN.''' def __init__(self, window, plugin, keystore, device_id): - title = _("%s Settings") % plugin.device + title = _("{} Settings").format(plugin.device) super(SettingsDialog, self).__init__(window, title) self.setMaximumWidth(540) @@ -457,9 +457,9 @@ def slider_released(): settings_glayout = QGridLayout() # Settings tab - Label - label_msg = QLabel(_("Name this %s. If you have mutiple devices " + label_msg = QLabel(_("Name this {}. If you have mutiple devices " "their labels help distinguish them.") - % plugin.device) + .format(plugin.device)) label_msg.setWordWrap(True) label_label = QLabel(_("Device Label")) label_edit = QLineEdit() @@ -482,7 +482,7 @@ def slider_released(): pin_msg = QLabel(_("PIN protection is strongly recommended. " "A PIN is your only protection against someone " "stealing your bitcoins if they obtain physical " - "access to your %s.") % plugin.device) + "access to your {}.").format(plugin.device)) pin_msg.setWordWrap(True) pin_msg.setStyleSheet("color: red") settings_glayout.addWidget(pin_msg, 3, 1, 1, -1) @@ -497,8 +497,8 @@ def slider_released(): homescreen_clear_button.clicked.connect(clear_homescreen) homescreen_msg = QLabel(_("You can set the homescreen on your " "device to personalize it. You must " - "choose a %d x %d monochrome black and " - "white image.") % (hs_rows, hs_cols)) + "choose a {} x {} monochrome black and " + "white image.").format(hs_rows, hs_cols)) homescreen_msg.setWordWrap(True) settings_glayout.addWidget(homescreen_label, 4, 0) settings_glayout.addWidget(homescreen_change_button, 4, 1) @@ -541,7 +541,7 @@ def slider_released(): clear_pin_button.clicked.connect(clear_pin) clear_pin_warning = QLabel( _("If you disable your PIN, anyone with physical access to your " - "%s device can spend your bitcoins.") % plugin.device) + "{} device can spend your bitcoins.").format(plugin.device)) clear_pin_warning.setWordWrap(True) clear_pin_warning.setStyleSheet("color: red") advanced_glayout.addWidget(clear_pin_button, 0, 2) diff --git a/plugins/ledger/auth2fa.py b/plugins/ledger/auth2fa.py index c5f50a0e4c0c..add619a82fdf 100644 --- a/plugins/ledger/auth2fa.py +++ b/plugins/ledger/auth2fa.py @@ -164,7 +164,7 @@ def populate_modes(self): if not self.cfg['pair']: self.modes.addItem(_("Mobile - Not paired")) else: - self.modes.addItem(_("Mobile - %s") % self.cfg['pair'][1]) + self.modes.addItem(_("Mobile - {}").format(self.cfg['pair'][1])) self.modes.blockSignals(False) def update_dlg(self): diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index 7d8866b4df57..4e181c4f7ba1 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -236,7 +236,7 @@ def address_id_stripped(self, address): return address_path[2:] def decrypt_message(self, pubkey, message, password): - raise RuntimeError(_('Encryption and decryption are currently not supported for %s') % self.device) + raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) def sign_message(self, sequence, message, password): self.signing = True diff --git a/plugins/trezor/clientbase.py b/plugins/trezor/clientbase.py index 31a97ca12a38..f890c59b0840 100644 --- a/plugins/trezor/clientbase.py +++ b/plugins/trezor/clientbase.py @@ -11,15 +11,15 @@ class GuiMixin(object): # Requires: self.proto, self.device messages = { - 3: _("Confirm the transaction output on your %s device"), - 4: _("Confirm internal entropy on your %s device to begin"), - 5: _("Write down the seed word shown on your %s"), - 6: _("Confirm on your %s that you want to wipe it clean"), - 7: _("Confirm on your %s device the message to sign"), + 3: _("Confirm the transaction output on your {} device"), + 4: _("Confirm internal entropy on your {} device to begin"), + 5: _("Write down the seed word shown on your {}"), + 6: _("Confirm on your {} that you want to wipe it clean"), + 7: _("Confirm on your {} device the message to sign"), 8: _("Confirm the total amount spent and the transaction fee on your " - "%s device"), - 10: _("Confirm wallet address on your %s device"), - 'default': _("Check your %s device to continue"), + "{} device"), + 10: _("Confirm wallet address on your {} device"), + 'default': _("Check your {} device to continue"), } def callback_Failure(self, msg): @@ -38,18 +38,18 @@ def callback_ButtonRequest(self, msg): message = self.msg if not message: message = self.messages.get(msg.code, self.messages['default']) - self.handler.show_message(message % self.device, self.cancel) + self.handler.show_message(message.format(self.device), self.cancel) return self.proto.ButtonAck() def callback_PinMatrixRequest(self, msg): if msg.type == 2: - msg = _("Enter a new PIN for your %s:") + msg = _("Enter a new PIN for your {}:") elif msg.type == 3: - msg = (_("Re-enter the new PIN for your %s.\n\n" + msg = (_("Re-enter the new PIN for your {}.\n\n" "NOTE: the positions of the numbers have changed!")) else: - msg = _("Enter your current %s PIN:") - pin = self.handler.get_pin(msg % self.device) + msg = _("Enter your current {} PIN:") + pin = self.handler.get_pin(msg.format(self.device)) if not pin: return self.proto.Cancel() return self.proto.PinMatrixAck(pin=pin) @@ -57,9 +57,9 @@ def callback_PinMatrixRequest(self, msg): def callback_PassphraseRequest(self, req): if self.creating_wallet: msg = _("Enter a passphrase to generate this wallet. Each time " - "you use this wallet your %s will prompt you for the " + "you use this wallet your {} will prompt you for the " "passphrase. If you forget the passphrase you cannot " - "access the bitcoins in the wallet.") % self.device + "access the bitcoins in the wallet.").format(self.device) else: msg = _("Enter the passphrase to unlock this wallet:") passphrase = self.handler.get_passphrase(msg, self.creating_wallet) @@ -70,8 +70,8 @@ def callback_PassphraseRequest(self, req): def callback_WordRequest(self, msg): self.step += 1 - msg = _("Step %d/24. Enter seed word as explained on " - "your %s:") % (self.step, self.device) + msg = _("Step {}/24. Enter seed word as explained on " + "your {}:").format(self.step, self.device) word = self.handler.get_word(msg) # Unfortunately the device can't handle self.proto.Cancel() return self.proto.WordAck(word=word) @@ -155,27 +155,27 @@ def get_xpub(self, bip32_path, xtype): def toggle_passphrase(self): if self.features.passphrase_protection: - self.msg = _("Confirm on your %s device to disable passphrases") + self.msg = _("Confirm on your {} device to disable passphrases") else: - self.msg = _("Confirm on your %s device to enable passphrases") + self.msg = _("Confirm on your {} device to enable passphrases") enabled = not self.features.passphrase_protection self.apply_settings(use_passphrase=enabled) def change_label(self, label): - self.msg = _("Confirm the new label on your %s device") + self.msg = _("Confirm the new label on your {} device") self.apply_settings(label=label) def change_homescreen(self, homescreen): - self.msg = _("Confirm on your %s device to change your home screen") + self.msg = _("Confirm on your {} device to change your home screen") self.apply_settings(homescreen=homescreen) def set_pin(self, remove): if remove: - self.msg = _("Confirm on your %s device to disable PIN protection") + self.msg = _("Confirm on your {} device to disable PIN protection") elif self.features.pin_protection: - self.msg = _("Confirm on your %s device to change your PIN") + self.msg = _("Confirm on your {} device to change your PIN") else: - self.msg = _("Confirm on your %s device to set a PIN") + self.msg = _("Confirm on your {} device to set a PIN") self.change_pin(remove) def clear_session(self): diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py index a381ed8768fa..15fdb7526914 100644 --- a/plugins/trezor/plugin.py +++ b/plugins/trezor/plugin.py @@ -42,7 +42,7 @@ def get_client(self, force_pair=True): return self.plugin.get_client(self, force_pair) def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by %s') % self.device) + raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) def sign_message(self, sequence, message, password): client = self.get_client() @@ -126,9 +126,9 @@ def create_client(self, device, handler): return None if not client.atleast_version(*self.minimum_firmware): - msg = (_('Outdated %s firmware for device labelled %s. Please ' - 'download the updated firmware from %s') % - (self.device, client.label(), self.firmware_URL)) + msg = (_('Outdated {} firmware for device labelled {}. Please ' + 'download the updated firmware from {}') + .format(self.device, client.label(), self.firmware_URL)) self.print_error(msg) handler.show_error(msg) return None @@ -150,14 +150,14 @@ def get_coin_name(self): def initialize_device(self, device_id, wizard, handler): # Initialization method - msg = _("Choose how you want to initialize your %s.\n\n" + msg = _("Choose how you want to initialize your {}.\n\n" "The first two methods are secure as no secret information " "is entered into your computer.\n\n" "For the last two methods you input secrets on your keyboard " - "and upload them to your %s, and so you should " + "and upload them to your {}, and so you should " "only do those on a computer you know to be trustworthy " "and free of malware." - ) % (self.device, self.device) + ).format(self.device, self.device) choices = [ # Must be short as QT doesn't word-wrap radio button text (TIM_NEW, _("Let the device generate a completely new seed randomly")), diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py index a4b9c07c79d5..68fbcf849a40 100644 --- a/plugins/trezor/qt_generic.py +++ b/plugins/trezor/qt_generic.py @@ -194,7 +194,7 @@ def receive_menu(self, menu, addrs, wallet): if type(keystore) == self.keystore_class: def show_address(): keystore.thread.add(partial(self.show_address, wallet, keystore, addrs[0])) - menu.addAction(_("Show on %s") % self.device, show_address) + menu.addAction(_("Show on {}").format(self.device), show_address) break def show_settings_dialog(self, window, keystore): @@ -293,7 +293,7 @@ class SettingsDialog(WindowModalDialog): their PIN.''' def __init__(self, window, plugin, keystore, device_id): - title = _("%s Settings") % plugin.device + title = _("{} Settings").format(plugin.device) super(SettingsDialog, self).__init__(window, title) self.setMaximumWidth(540) @@ -464,9 +464,9 @@ def slider_released(): settings_glayout = QGridLayout() # Settings tab - Label - label_msg = QLabel(_("Name this %s. If you have mutiple devices " + label_msg = QLabel(_("Name this {}. If you have mutiple devices " "their labels help distinguish them.") - % plugin.device) + .format(plugin.device)) label_msg.setWordWrap(True) label_label = QLabel(_("Device Label")) label_edit = QLineEdit() @@ -489,7 +489,7 @@ def slider_released(): pin_msg = QLabel(_("PIN protection is strongly recommended. " "A PIN is your only protection against someone " "stealing your bitcoins if they obtain physical " - "access to your %s.") % plugin.device) + "access to your {}.").format(plugin.device)) pin_msg.setWordWrap(True) pin_msg.setStyleSheet("color: red") settings_glayout.addWidget(pin_msg, 3, 1, 1, -1) @@ -504,8 +504,8 @@ def slider_released(): homescreen_clear_button.clicked.connect(clear_homescreen) homescreen_msg = QLabel(_("You can set the homescreen on your " "device to personalize it. You must " - "choose a %d x %d monochrome black and " - "white image.") % (hs_rows, hs_cols)) + "choose a {} x {} monochrome black and " + "white image.").format(hs_rows, hs_cols)) homescreen_msg.setWordWrap(True) settings_glayout.addWidget(homescreen_label, 4, 0) settings_glayout.addWidget(homescreen_change_button, 4, 1) @@ -548,7 +548,7 @@ def slider_released(): clear_pin_button.clicked.connect(clear_pin) clear_pin_warning = QLabel( _("If you disable your PIN, anyone with physical access to your " - "%s device can spend your bitcoins.") % plugin.device) + "{} device can spend your bitcoins.").format(plugin.device)) clear_pin_warning.setWordWrap(True) clear_pin_warning.setStyleSheet("color: red") advanced_glayout.addWidget(clear_pin_button, 0, 2) diff --git a/plugins/trustedcoin/qt.py b/plugins/trustedcoin/qt.py index cc0280cf4475..38d4fa1c0599 100644 --- a/plugins/trustedcoin/qt.py +++ b/plugins/trustedcoin/qt.py @@ -173,7 +173,7 @@ def show_settings_dialog(self, window, success): i += 1 n = wallet.billing_info.get('tx_remaining', 0) - grid.addWidget(QLabel(_("Your wallet has %d prepaid transactions.")%n), i, 0) + grid.addWidget(QLabel(_("Your wallet has {} prepaid transactions.").format(n)), i, 0) vbox.addLayout(Buttons(CloseButton(d))) d.exec_() diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py index fda0351f183b..dc4eeb11b079 100644 --- a/plugins/trustedcoin/trustedcoin.py +++ b/plugins/trustedcoin/trustedcoin.py @@ -430,13 +430,13 @@ def on_password(self, wizard, password, encrypt_storage, k1, k2): wizard.storage.put('x2/', k2.dump()) wizard.storage.write() msg = [ - _("Your wallet file is: %s.")%os.path.abspath(wizard.storage.path), + _("Your wallet file is: {}.").format(os.path.abspath(wizard.storage.path)), _("You need to be online in order to complete the creation of " "your wallet. If you generated your seed on an offline " - 'computer, click on "%s" to close this window, move your ' + 'computer, click on "{}" to close this window, move your ' "wallet file to an online computer, and reopen it with " - "Electrum.") % _('Cancel'), - _('If you are online, click on "%s" to continue.') % _('Next') + "Electrum.").format(_('Cancel')), + _('If you are online, click on "{}" to continue.').format(_('Next')) ] msg = '\n\n'.join(msg) wizard.stack = [] From 0021dbfceb9e10e2cc2875effbe4668a887db42d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 4 Feb 2018 19:17:21 +0100 Subject: [PATCH 068/270] address filter: (qt gui) add "all" filter option besides "receiving" and "change" --- gui/qt/address_list.py | 20 ++++++++++++-------- lib/wallet.py | 6 +++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index ac563a6d8e6f..8377e6bf0c65 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -38,11 +38,11 @@ def __init__(self, parent=None): MyTreeWidget.__init__(self, parent, self.create_menu, [], 1) self.refresh_headers() self.setSelectionMode(QAbstractItemView.ExtendedSelection) - self.show_change = False + self.show_change = 0 self.show_used = 0 self.change_button = QComboBox(self) self.change_button.currentIndexChanged.connect(self.toggle_change) - for t in [_('Receiving'), _('Change')]: + for t in [_('Receiving'), _('Change'), _('All')]: self.change_button.addItem(t) self.used_button = QComboBox(self) self.used_button.currentIndexChanged.connect(self.toggle_used) @@ -60,11 +60,10 @@ def refresh_headers(self): headers.extend([_('Tx')]) self.update_headers(headers) - def toggle_change(self, show): - show = bool(show) - if show == self.show_change: + def toggle_change(self, state): + if state == self.show_change: return - self.show_change = show + self.show_change = state self.update() def toggle_used(self, state): @@ -77,7 +76,12 @@ def on_update(self): self.wallet = self.parent.wallet item = self.currentItem() current_address = item.data(0, Qt.UserRole) if item else None - addr_list = self.wallet.get_change_addresses() if self.show_change else self.wallet.get_receiving_addresses() + if self.show_change == 0: + addr_list = self.wallet.get_receiving_addresses() + elif self.show_change == 1: + addr_list = self.wallet.get_change_addresses() + else: + addr_list = self.wallet.get_addresses() self.clear() for address in addr_list: num = len(self.wallet.history.get(address,[])) @@ -106,7 +110,7 @@ def on_update(self): address_item.setData(0, Qt.UserRole+1, True) # label can be edited if self.wallet.is_frozen(address): address_item.setBackground(0, ColorScheme.BLUE.as_color(True)) - if self.wallet.is_beyond_limit(address, self.show_change): + if self.wallet.is_beyond_limit(address): address_item.setBackground(0, ColorScheme.RED.as_color(True)) self.addChild(address_item) if address == current_address: diff --git a/lib/wallet.py b/lib/wallet.py index 29c0f9f021d4..fb2439e96733 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1535,7 +1535,7 @@ def is_change(self, address): def get_master_public_keys(self): return [] - def is_beyond_limit(self, address, is_change): + def is_beyond_limit(self, address): return False def is_mine(self, address): @@ -1775,9 +1775,9 @@ def synchronize(self): for addr in self.receiving_addresses: self.add_address(addr) - def is_beyond_limit(self, address, is_change): + def is_beyond_limit(self, address): + is_change, i = self.get_address_index(address) addr_list = self.get_change_addresses() if is_change else self.get_receiving_addresses() - i = self.get_address_index(address)[1] limit = self.gap_limit_for_change if is_change else self.gap_limit if i < limit: return False From ac2d4eb0efe856def0ee2327c5a1c6986bbaa762 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 4 Feb 2018 21:59:58 +0100 Subject: [PATCH 069/270] fix #3508 --- lib/base_wizard.py | 9 +++++++++ lib/plugins.py | 13 +++++++++---- plugins/ledger/ledger.py | 4 ++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/base_wizard.py b/lib/base_wizard.py index b73e195379d5..ab18b7617a60 100644 --- a/lib/base_wizard.py +++ b/lib/base_wizard.py @@ -234,6 +234,15 @@ def on_device(self, name, device_info, *, purpose): self.plugin = self.plugins.get_plugin(name) try: self.plugin.setup_device(device_info, self, purpose) + except OSError as e: + self.show_error(_('We encountered an error while connecting to your device:') + + '\n' + str(e) + '\n' + + _('To try to fix this, we will now re-pair with your device.') + '\n' + + _('Please try again.')) + devmgr = self.plugins.device_manager + devmgr.unpair_id(device_info.device.id_) + self.choose_hw_device(purpose) + return except BaseException as e: self.show_error(str(e)) self.choose_hw_device(purpose) diff --git a/lib/plugins.py b/lib/plugins.py index e37fe08a5d71..e94013a24a92 100644 --- a/lib/plugins.py +++ b/lib/plugins.py @@ -362,15 +362,20 @@ def unpair_xpub(self, xpub): if not xpub in self.xpub_ids: return _id = self.xpub_ids.pop(xpub) - client = self.client_lookup(_id) - self.clients.pop(client, None) - if client: - client.close() + self._close_client(_id) def unpair_id(self, id_): xpub = self.xpub_by_id(id_) if xpub: self.unpair_xpub(xpub) + else: + self._close_client(id_) + + def _close_client(self, id_): + client = self.client_lookup(id_) + self.clients.pop(client, None) + if client: + client.close() def pair_xpub(self, xpub, id_): with self.lock: diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py index 4e181c4f7ba1..cf56ee973acc 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -172,6 +172,10 @@ def perform_hw1_preflight(self): raise Exception("Dongle is temporarily locked - please unplug it and replug it again") if ((e.sw & 0xFFF0) == 0x63c0): raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") + if e.sw == 0x6f00 and e.message == 'Invalid channel': + # based on docs 0x6f00 might be a more general error, hence we also compare message to be sure + raise Exception("Invalid channel.\n" + "Please make sure that 'Browser support' is disabled on your device.") raise e def checkDevice(self): From c3f3843cc3f31862f7e3bea1126cb42150c405c4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 22 Nov 2017 12:09:56 +0100 Subject: [PATCH 070/270] Add memory pool based fee estimates - fee estimates can use ETA or mempool - require protocol version 1.2 - remove fee_unit preference --- gui/kivy/uix/dialogs/fee_dialog.py | 57 ++++++++------ gui/kivy/uix/dialogs/settings.py | 5 +- gui/qt/amountedit.py | 7 +- gui/qt/fee_slider.py | 34 +++------ gui/qt/main_window.py | 50 ++++++------ gui/qt/transaction_dialog.py | 4 +- lib/bitcoin.py | 2 +- lib/network.py | 11 ++- lib/simple_config.py | 118 ++++++++++++++++++++++++++--- lib/version.py | 2 +- lib/wallet.py | 17 +++-- 11 files changed, 204 insertions(+), 103 deletions(-) diff --git a/gui/kivy/uix/dialogs/fee_dialog.py b/gui/kivy/uix/dialogs/fee_dialog.py index 792ce60d0f45..8417fd00152b 100644 --- a/gui/kivy/uix/dialogs/fee_dialog.py +++ b/gui/kivy/uix/dialogs/fee_dialog.py @@ -32,7 +32,15 @@ text: _('Dynamic Fees') CheckBox: id: dynfees - on_active: root.on_checkbox(self.active) + on_active: root.on_dynfees(self.active) + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + Label: + text: _('Use mempool') + CheckBox: + id: mempool + on_active: root.on_mempool(self.active) Widget: size_hint: 1, 1 BoxLayout: @@ -60,7 +68,9 @@ def __init__(self, app, config, callback): 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.ids.mempool.active = self.mempool self.ids.dynfees.active = self.dynfees self.update_slider() self.update_text() @@ -71,34 +81,30 @@ def update_text(self): def update_slider(self): slider = self.ids.slider - if self.dynfees: - slider.range = (0, 4) - slider.step = 1 - slider.value = self.config.get('fee_level', 2) - else: - slider.range = (0, 9) - slider.step = 1 - slider.value = self.config.static_fee_index(self.fee_rate) + 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_text(self, value): - if self.ids.dynfees.active: - tooltip = fee_levels[value] - if self.config.has_fee_estimates(): - dynfee = self.config.dynfee(value) - tooltip += '\n' + (self.app.format_amount_and_units(dynfee)) + '/kB' + def get_fee_text(self, pos): + dyn = self.dynfees + mempool = self.mempool + if dyn: + fee_rate = self.config.depth_to_fee(pos) if mempool else self.config.eta_to_fee(pos) else: - fee_rate = self.config.static_fee(value) - tooltip = self.app.format_amount_and_units(fee_rate) + '/kB' - if self.config.has_fee_estimates(): - i = self.config.reverse_dynfee(fee_rate) - tooltip += '\n' + (_('low fee') if i < 0 else 'Within %d blocks'%i) - return tooltip + fee_rate = self.config.static_fee(pos) + target, tooltip = self.config.get_fee_text(pos, dyn, mempool, fee_rate) + return target def on_ok(self): value = int(self.ids.slider.value) self.config.set_key('dynamic_fees', self.dynfees, False) + self.config.set_key('mempool_fees', self.mempool, False) if self.dynfees: - self.config.set_key('fee_level', value, True) + if self.mempool: + self.config.set_key('depth_level', value, True) + else: + self.config.set_key('fee_level', value, True) else: self.config.set_key('fee_per_kb', self.config.static_fee(value), True) self.callback() @@ -106,7 +112,12 @@ def on_ok(self): def on_slider(self, value): self.update_text() - def on_checkbox(self, b): + def on_dynfees(self, b): self.dynfees = b self.update_slider() self.update_text() + + def on_mempool(self, b): + self.mempool = b + self.update_slider() + self.update_text() diff --git a/gui/kivy/uix/dialogs/settings.py b/gui/kivy/uix/dialogs/settings.py index e0778a3d8c68..e73f33650466 100644 --- a/gui/kivy/uix/dialogs/settings.py +++ b/gui/kivy/uix/dialogs/settings.py @@ -204,10 +204,7 @@ def callback(status): d.open() def fee_status(self): - if self.config.get('dynamic_fees', True): - return fee_levels[self.config.get('fee_level', 2)] - else: - return self.app.format_amount_and_units(self.config.fee_per_kb()) + '/kB' + return self.config.get_fee_status() def fee_dialog(self, label, dt): if self._fee_dialog is None: diff --git a/gui/qt/amountedit.py b/gui/qt/amountedit.py index 97177aef843d..551353289368 100644 --- a/gui/qt/amountedit.py +++ b/gui/qt/amountedit.py @@ -106,12 +106,7 @@ def setAmount(self, amount): class FeerateEdit(BTCAmountEdit): def _base_unit(self): - p = self.decimal_point() - if p == 2: - return 'mBTC/kB' - if p == 0: - return 'sat/byte' - raise Exception('Unknown base unit') + return 'sat/byte' def get_amount(self): sat_per_byte_amount = BTCAmountEdit.get_amount(self) diff --git a/gui/qt/fee_slider.py b/gui/qt/fee_slider.py index 3d93849254e3..7e5231806303 100644 --- a/gui/qt/fee_slider.py +++ b/gui/qt/fee_slider.py @@ -1,6 +1,4 @@ - from electrum.i18n import _ - from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import QSlider, QToolTip @@ -22,37 +20,27 @@ def __init__(self, window, config, callback): def moved(self, pos): with self.lock: - fee_rate = self.config.dynfee(pos) if self.dyn else self.config.static_fee(pos) + if self.dyn: + fee_rate = self.config.depth_to_fee(pos) if self.config.get('mempool_fees') else self.config.eta_to_fee(pos) + else: + fee_rate = self.config.static_fee(pos) tooltip = self.get_tooltip(pos, fee_rate) QToolTip.showText(QCursor.pos(), tooltip, self) self.setToolTip(tooltip) self.callback(self.dyn, pos, fee_rate) def get_tooltip(self, pos, fee_rate): - from electrum.util import fee_levels - rate_str = self.window.format_fee_rate(fee_rate) if fee_rate else _('unknown') - if self.dyn: - tooltip = fee_levels[pos] + '\n' + rate_str - else: - tooltip = 'Fixed rate: ' + rate_str - if self.config.has_fee_estimates(): - i = self.config.reverse_dynfee(fee_rate) - tooltip += '\n' + (_('Low fee') if i < 0 else 'Within %d blocks'%i) - return tooltip + mempool = self.config.get('mempool_fees') + text, tooltip = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate) + return text + '\n' + tooltip def update(self): with self.lock: self.dyn = self.config.is_dynfee() - if self.dyn: - pos = self.config.get('fee_level', 2) - fee_rate = self.config.dynfee(pos) - self.setRange(0, 4) - self.setValue(pos) - else: - fee_rate = self.config.fee_per_kb() - pos = self.config.static_fee_index(fee_rate) - self.setRange(0, 9) - self.setValue(pos) + mempool = self.config.get('mempool_fees') + maxp, pos, fee_rate = self.config.get_fee_slider(self.dyn, mempool) + self.setRange(0, maxp) + self.setValue(pos) tooltip = self.get_tooltip(pos, fee_rate) self.setToolTip(tooltip) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 738ff5e436c2..d7cf962c8868 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -131,7 +131,6 @@ def __init__(self, gui_object, wallet): self.need_update = threading.Event() self.decimal_point = config.get('decimal_point', 5) - self.fee_unit = config.get('fee_unit', 0) self.num_zeros = int(config.get('num_zeros',0)) self.completions = QStringListModel() @@ -293,7 +292,6 @@ def on_network(self, event, *args): self.need_update.set() self.gui_object.network_updated_signal_obj.network_updated_signal \ .emit(event, args) - elif event == 'new_transaction': self.tx_notifications.append(args[0]) self.notify_transactions_signal.emit() @@ -315,6 +313,12 @@ def on_network_qt(self, event, args=None): if self.config.is_dynfee(): self.fee_slider.update() self.do_update_fee() + elif event == 'fee_histogram': + if self.config.is_dynfee(): + self.fee_slider.update() + self.do_update_fee() + # todo: update only unconfirmed tx + self.history_list.update() else: self.print_error("unexpected network_qt signal:", event, args) @@ -636,10 +640,7 @@ def format_amount_and_units(self, amount): return text def format_fee_rate(self, fee_rate): - if self.fee_unit == 0: - return format_satoshis(fee_rate/1000, False, self.num_zeros, 0, False) + ' sat/byte' - else: - return self.format_amount(fee_rate) + ' ' + self.base_unit() + '/kB' + return format_satoshis(fee_rate/1000, False, self.num_zeros, 0, False) + ' sat/byte' def get_decimal_point(self): return self.decimal_point @@ -1076,7 +1077,10 @@ def create_send_tab(self): def fee_cb(dyn, pos, fee_rate): if dyn: - self.config.set_key('fee_level', pos, False) + if self.config.get('mempool_fees'): + self.config.set_key('depth_level', pos, False) + else: + self.config.set_key('fee_level', pos, False) else: self.config.set_key('fee_per_kb', fee_rate, False) @@ -1116,7 +1120,7 @@ def setAmount(self, byte_size): self.size_e.setFixedWidth(140) self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) - self.feerate_e = FeerateEdit(lambda: 2 if self.fee_unit else 0) + self.feerate_e = FeerateEdit(lambda: 0) self.feerate_e.setAmount(self.config.fee_per_byte()) self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False)) self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True)) @@ -1256,9 +1260,6 @@ def do_update_fee(self): '''Recalculate the fee. If the fee was manually input, retain it, but still build the TX to see if there are enough funds. ''' - if not self.config.get('offline') and self.config.is_dynfee() and not self.config.has_fee_estimates(): - self.statusBar().showMessage(_('Waiting for fee estimates...')) - return False freeze_fee = self.is_send_fee_frozen() freeze_feerate = self.is_send_feerate_frozen() amount = '!' if self.is_max else self.amount_e.get_amount() @@ -2670,6 +2671,21 @@ def on_nz(): nz.valueChanged.connect(on_nz) gui_widgets.append((nz_label, nz)) + msg = '\n'.join([ + _('Time based: fee rate is based on average confirmation time estimates'), + _('Mempool based: fee rate is targetting a depth in the memory pool') + ] + ) + 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) + def on_fee_type(x): + self.config.set_key('mempool_fees', x==1) + self.fee_slider.update() + fee_type_combo.currentIndexChanged.connect(on_fee_type) + fee_widgets.append((fee_type_label, fee_type_combo)) + def on_dynfee(x): self.config.set_key('dynamic_fees', x == Qt.Checked) self.fee_slider.update() @@ -2699,18 +2715,6 @@ def on_use_rbf(x): use_rbf_cb.stateChanged.connect(on_use_rbf) fee_widgets.append((use_rbf_cb, None)) - self.fee_unit = self.config.get('fee_unit', 0) - fee_unit_label = HelpLabel(_('Fee Unit') + ':', '') - fee_unit_combo = QComboBox() - fee_unit_combo.addItems([_('sat/byte'), _('mBTC/kB')]) - fee_unit_combo.setCurrentIndex(self.fee_unit) - def on_fee_unit(x): - self.fee_unit = x - self.config.set_key('fee_unit', x) - self.fee_slider.update() - fee_unit_combo.currentIndexChanged.connect(on_fee_unit) - fee_widgets.append((fee_unit_label, fee_unit_combo)) - msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\ + _('The following alias providers are available:') + '\n'\ + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\ diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 03169bebaed9..23a2f87fac45 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -221,8 +221,8 @@ def update(self): self.date_label.setText(_("Date: {}").format(time_str)) self.date_label.show() elif exp_n: - text = '%d blocks'%(exp_n) if exp_n > 0 else _('unknown (low fee)') - self.date_label.setText(_('Expected confirmation time') + ': ' + text) + text = '%.2f MB'%(exp_n/1000000) + self.date_label.setText(_('Position in mempool') + ': ' + text + _('from tip')) self.date_label.show() else: self.date_label.hide() diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 2a788777b602..84339755986f 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -102,7 +102,7 @@ def set_testnet(cls): FEE_STEP = 10000 MAX_FEE_RATE = 300000 -FEE_TARGETS = [25, 10, 5, 2] + COINBASE_MATURITY = 100 COIN = 100000000 diff --git a/lib/network.py b/lib/network.py index 4855c0fcb5c1..bf7d4eb415c1 100644 --- a/lib/network.py +++ b/lib/network.py @@ -321,8 +321,10 @@ def send_subscriptions(self): self.queue_request('blockchain.scripthash.subscribe', [h]) def request_fee_estimates(self): + from .simple_config import FEE_ETA_TARGETS self.config.requested_fee_estimates() - for i in bitcoin.FEE_TARGETS: + self.queue_request('mempool.get_fee_histogram', []) + for i in FEE_ETA_TARGETS: self.queue_request('blockchain.estimatefee', [i]) def get_status_value(self, key): @@ -332,6 +334,8 @@ def get_status_value(self, key): value = self.banner elif key == 'fee': value = self.config.fee_estimates + elif key == 'fee_histogram': + value = self.config.mempool_fees elif key == 'updated': value = (self.get_local_height(), self.get_server_height()) elif key == 'servers': @@ -543,6 +547,11 @@ def process_response(self, interface, response, callbacks): elif method == 'server.donation_address': if error is None: self.donation_address = result + elif method == 'mempool.get_fee_histogram': + if error is None: + self.print_error(result) + self.config.mempool_fees = result + self.notify('fee_histogram') elif method == 'blockchain.estimatefee': if error is None and result > 0: i = params[0] diff --git a/lib/simple_config.py b/lib/simple_config.py index 3b747b0aea35..33fca56d8b74 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -6,9 +6,12 @@ from copy import deepcopy from .util import (user_dir, print_error, PrintError, - NoDynamicFeeEstimates) + NoDynamicFeeEstimates, format_satoshis) -from .bitcoin import MAX_FEE_RATE, FEE_TARGETS +from .bitcoin import MAX_FEE_RATE + +FEE_ETA_TARGETS = [25, 10, 5, 2] +FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000] config = None @@ -48,6 +51,7 @@ def __init__(self, options=None, read_user_config_function=None, # a thread-safe way. self.lock = threading.RLock() + self.mempool_fees = {} self.fee_estimates = {} self.fee_estimates_last_updated = {} self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees @@ -263,9 +267,9 @@ def max_fee_rate(self): f = MAX_FEE_RATE return f - def dynfee(self, i): + def eta_to_fee(self, i): if i < 4: - j = FEE_TARGETS[i] + j = FEE_ETA_TARGETS[i] fee = self.fee_estimates.get(j) else: assert i == 4 @@ -276,15 +280,99 @@ def dynfee(self, i): fee = min(5*MAX_FEE_RATE, fee) return fee - def reverse_dynfee(self, fee_per_kb): + def fee_to_depth(self, target_fee): + depth = 0 + for fee, s in self.mempool_fees: + depth += s + if fee < target_fee: + break + else: + return 0 + return depth + + def depth_to_fee(self, i): + target = self.depth_target(i) + depth = 0 + for fee, s in self.mempool_fees: + depth += s + if depth > target: + break + else: + return 0 + return fee * 1000 + + def depth_target(self, i): + return FEE_DEPTH_TARGETS[i] + + def eta_target(self, i): + return FEE_ETA_TARGETS[i] + + def fee_to_eta(self, fee_per_kb): import operator - l = list(self.fee_estimates.items()) + [(1, self.dynfee(4))] + l = list(self.fee_estimates.items()) + [(1, self.eta_to_fee(4))] dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l) min_target, min_value = min(dist, key=operator.itemgetter(1)) if fee_per_kb < self.fee_estimates.get(25)/2: min_target = -1 return min_target + def depth_tooltip(self, depth): + return "%.1f MB from tip"%(depth/1000000) + + def eta_tooltip(self, x): + return 'Low fee' if x < 0 else 'Within %d blocks'%x + + def get_fee_status(self): + dyn = self.is_dynfee() + mempool = self.get('mempool_fees') + pos = self.get('fee_level', 2) if mempool else self.get('depth_level', 2) + fee_rate = self.fee_per_kb() + target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate) + return target + + def get_fee_text(self, pos, dyn, mempool, fee_rate): + 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: + depth = self.depth_target(pos) + text = self.depth_tooltip(depth) + else: + eta = self.eta_target(pos) + text = self.eta_tooltip(eta) + 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 = '' + else: + if self.has_fee_etas(): + eta = self.fee_to_eta(fee_rate) + tooltip = self.eta_tooltip(eta) + else: + tooltip = '' + return text, tooltip + + def get_fee_slider(self, dyn, mempool): + if dyn: + if mempool: + maxp = len(FEE_DEPTH_TARGETS) - 1 + pos = min(maxp, self.get('depth_level', 2)) + fee_rate = self.depth_to_fee(pos) + else: + maxp = len(FEE_ETA_TARGETS) - 1 + pos = min(maxp, self.get('fee_level', 2)) + fee_rate = self.eta_to_fee(pos) + else: + fee_rate = self.fee_per_kb() + pos = self.static_fee_index(fee_rate) + maxp= 9 + return maxp, pos, fee_rate + + def static_fee(self, i): return self.fee_rates[i] @@ -292,19 +380,27 @@ def static_fee_index(self, value): dist = list(map(lambda x: abs(x - value), self.fee_rates)) return min(range(len(dist)), key=dist.__getitem__) - def has_fee_estimates(self): - return len(self.fee_estimates)==4 + def has_fee_etas(self): + return len(self.fee_estimates) == 4 + + def has_fee_mempool(self): + return bool(self.mempool_fees) def is_dynfee(self): return self.get('dynamic_fees', True) + def use_mempool_fees(self): + return self.get('mempool_fees', False) + def fee_per_kb(self): """Returns sat/kvB fee to pay for a txn. Note: might return None. """ - dyn = self.is_dynfee() - if dyn: - fee_rate = self.dynfee(self.get('fee_level', 2)) + if self.is_dynfee(): + if self.use_mempool_fees(): + fee_rate = self.depth_to_fee(self.get('depth_level', 2)) + else: + fee_rate = self.eta_to_fee(self.get('fee_level', 2)) else: fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2) return fee_rate diff --git a/lib/version.py b/lib/version.py index 877fc1388c4a..0f48aa66f2b6 100644 --- a/lib/version.py +++ b/lib/version.py @@ -1,5 +1,5 @@ ELECTRUM_VERSION = '3.0.6' # version of the client package -PROTOCOL_VERSION = '1.1' # protocol version requested +PROTOCOL_VERSION = '1.2' # protocol version requested # The hash of the mnemonic seed must begin with this SEED_PREFIX = '01' # Standard wallet diff --git a/lib/wallet.py b/lib/wallet.py index fb2439e96733..8d4eaea15d8b 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -538,10 +538,10 @@ def get_tx_info(self, tx): status = _('Unconfirmed') if fee is None: fee = self.tx_fees.get(tx_hash) - if fee and self.network.config.has_fee_estimates(): + if fee and self.network.config.has_fee_etas(): size = tx.estimated_size() fee_per_kb = fee * 1000 / size - exp_n = self.network.config.reverse_dynfee(fee_per_kb) + exp_n = self.network.config.fee_to_eta(fee_per_kb) can_bump = is_mine and not tx.is_final() else: status = _('Local') @@ -860,18 +860,17 @@ def get_default_label(self, tx_hash): def get_tx_status(self, tx_hash, height, conf, timestamp): from .util import format_time + exp_n = False if conf == 0: tx = self.transactions.get(tx_hash) if not tx: return 3, 'unknown' is_final = tx and tx.is_final() fee = self.tx_fees.get(tx_hash) - if fee and self.network and self.network.config.has_fee_estimates(): - size = len(tx.raw)/2 - low_fee = int(self.network.config.dynfee(0)*size/1000) - is_lowfee = fee < low_fee * 0.5 - else: - is_lowfee = False + if fee and self.network and self.network.config.has_fee_mempool(): + size = tx.estimated_size() + fee_per_kb = fee * 1000 / size + exp_n = self.network.config.fee_to_depth(fee_per_kb//1000) if height == TX_HEIGHT_LOCAL: status = 5 elif height == TX_HEIGHT_UNCONF_PARENT: @@ -888,6 +887,8 @@ def get_tx_status(self, tx_hash, height, conf, timestamp): status = 5 + min(conf, 6) time_str = format_time(timestamp) if timestamp else _("unknown") status_str = TX_STATUS[status] if status < 6 else time_str + if exp_n: + status_str += ' [%d sat/b, %.2f MB]'%(fee_per_kb//1000, exp_n/1000000) return status, status_str def relayfee(self): From f39f43d58c403e54209316834a99f5602022a9cd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 5 Feb 2018 16:08:57 +0100 Subject: [PATCH 071/270] fix #3845 --- gui/kivy/uix/screens.py | 1 - gui/qt/history_list.py | 1 - lib/wallet.py | 11 ++++------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 40713ab99830..ed1471d52530 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -89,7 +89,6 @@ def show_menu(self, obj): # note: this list needs to be kept in sync with another in qt TX_ICONS = [ - "close", "close", "close", "unconfirmed", diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index d34fafe6754b..084b527dd354 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -34,7 +34,6 @@ # note: this list needs to be kept in sync with another in kivy TX_ICONS = [ - "warning.png", "warning.png", "warning.png", "unconfirmed.png", diff --git a/lib/wallet.py b/lib/wallet.py index 8d4eaea15d8b..7b54af3a626d 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -66,7 +66,6 @@ TX_STATUS = [ _('Replaceable'), _('Unconfirmed parent'), - _('Low fee'), _('Unconfirmed'), _('Not Verified'), _('Local only'), @@ -877,16 +876,14 @@ def get_tx_status(self, tx_hash, height, conf, timestamp): status = 1 elif height == TX_HEIGHT_UNCONFIRMED and not is_final: status = 0 - elif height == TX_HEIGHT_UNCONFIRMED and is_lowfee: - status = 2 elif height == TX_HEIGHT_UNCONFIRMED: - status = 3 + status = 2 else: - status = 4 + status = 3 else: - status = 5 + min(conf, 6) + status = 4 + min(conf, 6) time_str = format_time(timestamp) if timestamp else _("unknown") - status_str = TX_STATUS[status] if status < 6 else time_str + status_str = TX_STATUS[status] if status < 5 else time_str if exp_n: status_str += ' [%d sat/b, %.2f MB]'%(fee_per_kb//1000, exp_n/1000000) return status, status_str From 73cc1293f102cb26a7a1600f559f6c74cb6d0e9a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 5 Feb 2018 17:47:15 +0100 Subject: [PATCH 072/270] initial release notes for version 3.1 --- RELEASE-NOTES | 35 +++++++++++++++++++++++++++++++++-- lib/version.py | 2 +- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index f05b0ac5e6cd..31c3d48c303d 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,7 +1,38 @@ -# Release 3.0.6 : +# 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. + * 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. + * Watching-only wallets and hardware wallets can be encrypted. + * Semi-automated crash reporting + * The SSH checkbox option was removed from the GUI. - * Fix transaction parsing bug #3788 +# Release 3.0.6 : + * Fix transaction parsing bug #3788 # Release 3.0.5 : (Security update) diff --git a/lib/version.py b/lib/version.py index 0f48aa66f2b6..1d653e591a3c 100644 --- a/lib/version.py +++ b/lib/version.py @@ -1,4 +1,4 @@ -ELECTRUM_VERSION = '3.0.6' # version of the client package +ELECTRUM_VERSION = '3.1' # version of the client package PROTOCOL_VERSION = '1.2' # protocol version requested # The hash of the mnemonic seed must begin with this From 245cd24f34ac6250150c92bd619dd4c84f644895 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 5 Feb 2018 20:13:05 +0100 Subject: [PATCH 073/270] follow-up f39f43d58c403e54209316834a99f5602022a9cd --- lib/wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 7b54af3a626d..4d961dc56e94 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -863,7 +863,7 @@ def get_tx_status(self, tx_hash, height, conf, timestamp): if conf == 0: tx = self.transactions.get(tx_hash) if not tx: - return 3, 'unknown' + return 2, 'unknown' is_final = tx and tx.is_final() fee = self.tx_fees.get(tx_hash) if fee and self.network and self.network.config.has_fee_mempool(): @@ -871,7 +871,7 @@ def get_tx_status(self, tx_hash, height, conf, timestamp): fee_per_kb = fee * 1000 / size exp_n = self.network.config.fee_to_depth(fee_per_kb//1000) if height == TX_HEIGHT_LOCAL: - status = 5 + status = 4 elif height == TX_HEIGHT_UNCONF_PARENT: status = 1 elif height == TX_HEIGHT_UNCONFIRMED and not is_final: From 4b4ad22e905b8596195336cde0ac50be0d737c85 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Mon, 5 Feb 2018 20:16:57 +0100 Subject: [PATCH 074/270] Change SSH to SSL in release notes --- RELEASE-NOTES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 31c3d48c303d..6413c9e22bca 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -28,7 +28,7 @@ until they are confirmed. * Watching-only wallets and hardware wallets can be encrypted. * Semi-automated crash reporting - * The SSH checkbox option was removed from the GUI. + * The SSL checkbox option was removed from the GUI. # Release 3.0.6 : From 9e057e5446a9b64feabc61aa9748db7add908c98 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Mon, 5 Feb 2018 22:11:29 +0100 Subject: [PATCH 075/270] Reset scroll position after updating list --- gui/qt/history_list.py | 4 ++-- gui/qt/util.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 084b527dd354..63a0b4b9b8df 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -70,7 +70,6 @@ def get_domain(self): @profiler def on_update(self): - # TODO save and restore scroll position (maybe based on y coord or selected item?) self.wallet = self.parent.wallet h = self.wallet.get_history(self.get_domain()) item = self.currentItem() @@ -213,4 +212,5 @@ def onFileAdded(self, fn): self.parent.show_error(e) else: self.wallet.save_transactions(write=True) - self.on_update() + # need to update at least: history_list, utxo_list, address_list + self.parent.need_update.set() diff --git a/gui/qt/util.py b/gui/qt/util.py index cd552e405d14..5dbda84a6e65 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -478,8 +478,12 @@ def update(self): self.pending_update = True else: self.setUpdatesEnabled(False) + scroll_pos = self.verticalScrollBar().value() self.on_update() self.setUpdatesEnabled(True) + # To paint the list before resetting the scroll position + self.parent.app.processEvents() + self.verticalScrollBar().setValue(scroll_pos) if self.current_filter: self.filter(self.current_filter) From d466ef4b73606ab8f57f16b0e0f91da015026da7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 6 Feb 2018 03:28:46 +0100 Subject: [PATCH 076/270] missing whitespace in transaction_dialog.py --- gui/qt/transaction_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 23a2f87fac45..a7f754a82d7e 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -222,7 +222,7 @@ def update(self): self.date_label.show() elif exp_n: text = '%.2f MB'%(exp_n/1000000) - self.date_label.setText(_('Position in mempool') + ': ' + text + _('from tip')) + self.date_label.setText(_('Position in mempool') + ': ' + text + ' ' + _('from tip')) self.date_label.show() else: self.date_label.hide() From d6c1a933fe0f44191306300d6c34142c54e8ab92 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 6 Feb 2018 05:13:37 +0100 Subject: [PATCH 077/270] some local txns are not getting SPV verified --- lib/wallet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/wallet.py b/lib/wallet.py index 4d961dc56e94..72fd982f7591 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -779,6 +779,7 @@ def receive_history_callback(self, addr, hist, tx_fees): # 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) self.history[addr] = hist for tx_hash, tx_height in hist: From ca19a3647829b67842e21ce10c028c9880b29e21 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 6 Feb 2018 05:39:26 +0100 Subject: [PATCH 078/270] conflicting transactions --- gui/qt/transaction_dialog.py | 7 +- lib/commands.py | 3 +- lib/transaction.py | 8 +++ lib/wallet.py | 127 +++++++++++++++++++++++++++-------- 4 files changed, 115 insertions(+), 30 deletions(-) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 23a2f87fac45..3211fd7ce215 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -179,10 +179,13 @@ def sign_done(success): self.main_window.sign_tx(self.tx, sign_done) def save(self): - self.wallet.add_transaction(self.tx.txid(), self.tx) + 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) - self.main_window.history_list.update() + # 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")) diff --git a/lib/commands.py b/lib/commands.py index 49760268e344..c0bfb48ed51e 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -627,7 +627,8 @@ def addrequest(self, amount, memo='', expiration=None, force=False): def addtransaction(self, tx): """ Add a transaction to the wallet history """ tx = Transaction(tx) - self.wallet.add_transaction(tx.txid(), tx) + if not self.wallet.add_transaction(tx.txid(), tx): + return False self.wallet.save_transactions() return tx.txid() diff --git a/lib/transaction.py b/lib/transaction.py index 5688dd911367..b23cf9cf2b53 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -797,6 +797,14 @@ def get_preimage_script(self, txin): def serialize_outpoint(self, txin): return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4) + @classmethod + def get_outpoint_from_txin(cls, txin): + if txin['type'] == 'coinbase': + return None + prevout_hash = txin['prevout_hash'] + prevout_n = txin['prevout_n'] + return prevout_hash + ':%d' % prevout_n + @classmethod def serialize_input(self, txin, script): # Prev hash and index diff --git a/lib/wallet.py b/lib/wallet.py index 4d961dc56e94..83f90ab0eb30 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -188,7 +188,7 @@ def __init__(self, storage): self.load_keystore() self.load_addresses() self.load_transactions() - self.build_reverse_history() + self.build_spent_outpoints() # load requests self.receive_requests = self.storage.get('payment_requests', {}) @@ -204,8 +204,10 @@ def __init__(self, storage): # interface.is_up_to_date() returns true when all requests have been answered and processed # wallet.up_to_date is true when the wallet is synchronized (stronger requirement) 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.transaction_lock = threading.Lock() + self.transaction_lock = threading.RLock() self.check_history() @@ -238,7 +240,8 @@ def load_transactions(self): for tx_hash, raw in tx_list.items(): tx = Transaction(raw) self.transactions[tx_hash] = tx - if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None and (tx_hash not in self.pruned_txo.values()): + if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None \ + and (tx_hash not in self.pruned_txo.values()): self.print_error("removing unreferenced tx", tx_hash) self.transactions.pop(tx_hash) @@ -258,24 +261,25 @@ def save_transactions(self, write=False): self.storage.write() def clear_history(self): - with self.transaction_lock: - self.txi = {} - self.txo = {} - self.tx_fees = {} - self.pruned_txo = {} - self.save_transactions() with self.lock: - self.history = {} - self.tx_addr_hist = {} + with self.transaction_lock: + self.txi = {} + self.txo = {} + self.tx_fees = {} + self.pruned_txo = {} + self.spent_outpoints = {} + self.history = {} + self.save_transactions() @profiler - def build_reverse_history(self): - self.tx_addr_hist = {} - for addr, hist in self.history.items(): - for tx_hash, h in hist: - s = self.tx_addr_hist.get(tx_hash, set()) - s.add(addr) - self.tx_addr_hist[tx_hash] = s + 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 @profiler def check_history(self): @@ -415,7 +419,7 @@ def get_local_height(self): return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) def get_tx_height(self, tx_hash): - """ return the height and timestamp of a transaction. """ + """ Given a transaction, returns (height, conf, timestamp) """ with self.lock: if tx_hash in self.verified_tx: height, timestamp, pos = self.verified_tx[tx_hash] @@ -682,10 +686,69 @@ def find_pay_to_pubkey_address(self, prevout_hash, prevout_n): self.print_error("found pay-to-pubkey address:", addr) 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 + 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: + for txi in tx.inputs(): + ser = Transaction.get_outpoint_from_txin(txi) + if ser is None: + continue + spending_tx_hash = self.spent_outpoints.get(ser, None) + 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 + 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): is_coinbase = tx.inputs()[0]['type'] == 'coinbase' related = False with self.transaction_lock: + # Find all conflicting transactions. + # In case of a conflict, + # 1. confirmed > mempool > local + # 2. this new txn has priority over existing ones + # When this method exits, there must NOT be any conflict, so + # either keep this txn and remove all conflicting (along with dependencies) + # 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) + existing_confirmed_txn = any( + self.get_tx_height(tx_hash2)[0] > 0 + for tx_hash2 in conflicting_txns) + if existing_confirmed_txn and tx_height <= 0: + # this is a non-confirmed tx that conflicts with confirmed txns; drop. + return False + if existing_mempool_txn and tx_height == TX_HEIGHT_LOCAL: + # this is a local tx that conflicts with non-local txns; drop. + return False + # keep this txn and remove all conflicting + to_remove = set() + to_remove |= conflicting_txns + for conflicting_tx_hash in conflicting_txns: + 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(): @@ -694,6 +757,7 @@ def add_transaction(self, tx_hash, tx): 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 @@ -739,14 +803,27 @@ def add_transaction(self, tx_hash, tx): # 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: + 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) - for ser, hh in list(self.pruned_txo.items()): - if hh == tx_hash: - self.pruned_txo.pop(ser) + tx = self.transactions.get(tx_hash, None) + undo_spend(self.pruned_txo) + undo_spend(self.spent_outpoints) + # add tx to pruned_txo, and undo the txi addition for next_tx, dd in self.txi.items(): for addr, l in list(dd.items()): @@ -768,8 +845,8 @@ def remove_transaction(self, tx_hash): self.print_error("tx was not in history", tx_hash) def receive_tx_callback(self, tx_hash, tx, tx_height): - self.add_transaction(tx_hash, tx) self.add_unverified_tx(tx_hash, tx_height) + self.add_transaction(tx_hash, tx) def receive_history_callback(self, addr, hist, tx_fees): with self.lock: @@ -784,10 +861,6 @@ def receive_history_callback(self, addr, hist, tx_fees): for tx_hash, tx_height in hist: # add it in case it was previously unconfirmed self.add_unverified_tx(tx_hash, tx_height) - # add reference in tx_addr_hist - s = self.tx_addr_hist.get(tx_hash, set()) - s.add(addr) - self.tx_addr_hist[tx_hash] = s # if addr is new, we have to recompute txi and txo tx = self.transactions.get(tx_hash) if tx is not None and self.txi.get(tx_hash, {}).get(addr) is None and self.txo.get(tx_hash, {}).get(addr) is None: From aafd3b929b4403b73e4569f6bb058f83c9ce8a8d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 6 Feb 2018 11:09:42 +0100 Subject: [PATCH 079/270] config: sanitize fee_level and depth_level --- lib/simple_config.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/simple_config.py b/lib/simple_config.py index 33fca56d8b74..cdbe79562348 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -325,7 +325,7 @@ def eta_tooltip(self, x): def get_fee_status(self): dyn = self.is_dynfee() mempool = self.get('mempool_fees') - pos = self.get('fee_level', 2) if mempool else self.get('depth_level', 2) + 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) return target @@ -356,20 +356,28 @@ def get_fee_text(self, pos, dyn, mempool, fee_rate): tooltip = '' return text, tooltip + def get_depth_level(self): + maxp = len(FEE_DEPTH_TARGETS) - 1 + return min(maxp, self.get('depth_level', 2)) + + def get_fee_level(self): + maxp = len(FEE_ETA_TARGETS) - 1 + return min(maxp, self.get('fee_level', 2)) + def get_fee_slider(self, dyn, mempool): if dyn: if mempool: + pos = self.get_depth_level() maxp = len(FEE_DEPTH_TARGETS) - 1 - pos = min(maxp, self.get('depth_level', 2)) fee_rate = self.depth_to_fee(pos) else: + pos = self.get_fee_level() maxp = len(FEE_ETA_TARGETS) - 1 - pos = min(maxp, self.get('fee_level', 2)) fee_rate = self.eta_to_fee(pos) else: fee_rate = self.fee_per_kb() pos = self.static_fee_index(fee_rate) - maxp= 9 + maxp = 9 return maxp, pos, fee_rate @@ -398,9 +406,9 @@ def fee_per_kb(self): """ if self.is_dynfee(): if self.use_mempool_fees(): - fee_rate = self.depth_to_fee(self.get('depth_level', 2)) + fee_rate = self.depth_to_fee(self.get_depth_level()) else: - fee_rate = self.eta_to_fee(self.get('fee_level', 2)) + fee_rate = self.eta_to_fee(self.get_fee_level()) else: fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2) return fee_rate From f5d557cae9ec13daebfe1879ed0c1c01b7eaee7d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 6 Feb 2018 17:53:34 +0100 Subject: [PATCH 080/270] improve fee tooltips --- gui/kivy/uix/dialogs/fee_dialog.py | 19 +++++++++++++++---- gui/qt/fee_slider.py | 7 +++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/gui/kivy/uix/dialogs/fee_dialog.py b/gui/kivy/uix/dialogs/fee_dialog.py index 8417fd00152b..25e9926c43bc 100644 --- a/gui/kivy/uix/dialogs/fee_dialog.py +++ b/gui/kivy/uix/dialogs/fee_dialog.py @@ -18,7 +18,17 @@ orientation: 'horizontal' size_hint: 1, 0.5 Label: - id: fee_per_kb + text: (_('Target') if dynfees.active else _('Fixed rate')) + ':' + Label: + id: fee_target + text: '' + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + Label: + text: (_('Current rate') if dynfees.active else _('Estimate')) + ':' + Label: + id: fee_estimate text: '' Slider: id: slider @@ -77,7 +87,9 @@ def __init__(self, app, config, callback): def update_text(self): value = int(self.ids.slider.value) - self.ids.fee_per_kb.text = self.get_fee_text(value) + target, estimate = self.get_fee_text(value) + self.ids.fee_target.text = target + self.ids.fee_estimate.text = estimate def update_slider(self): slider = self.ids.slider @@ -93,8 +105,7 @@ def get_fee_text(self, pos): fee_rate = self.config.depth_to_fee(pos) if mempool else self.config.eta_to_fee(pos) else: fee_rate = self.config.static_fee(pos) - target, tooltip = self.config.get_fee_text(pos, dyn, mempool, fee_rate) - return target + return self.config.get_fee_text(pos, dyn, mempool, fee_rate) def on_ok(self): value = int(self.ids.slider.value) diff --git a/gui/qt/fee_slider.py b/gui/qt/fee_slider.py index 7e5231806303..209b0de7611f 100644 --- a/gui/qt/fee_slider.py +++ b/gui/qt/fee_slider.py @@ -31,8 +31,11 @@ def moved(self, pos): def get_tooltip(self, pos, fee_rate): mempool = self.config.get('mempool_fees') - text, tooltip = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate) - return text + '\n' + tooltip + target, estimate = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate) + if self.dyn: + return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate + else: + return _('Fixed rate') + ': ' + target + '\n' + _('Estimate') + ': ' + estimate def update(self): with self.lock: From 5095687d634cb512b88ffa4be94516e0b45cd689 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 6 Feb 2018 18:48:36 +0100 Subject: [PATCH 081/270] fee rounding: num sats displayed in dialog too, besides tooltip --- gui/qt/main_window.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index d7cf962c8868..0675929e35b6 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1012,6 +1012,10 @@ def update_receive_qr(self): if self.qr_window and self.qr_window.isVisible(): self.qr_window.set_content(addr, amount, message, uri) + def set_feerounding_text(self, num_satoshis_added): + self.feerounding_text = (_('Additional {} satoshis are going to be added.') + .format(num_satoshis_added)) + def create_send_tab(self): # A 4-column grid layout. All the stretch is in the last column. # The exchange rate plugin adds a fiat widget in column 2 @@ -1130,7 +1134,8 @@ def setAmount(self, byte_size): self.fee_e.editingFinished.connect(partial(on_fee_or_feerate, self.fee_e, True)) def feerounding_onclick(): - text = (_('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' + + 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' + _('Also, dust is not kept as change, but added to the fee.')) QMessageBox.information(self, 'Fee rounding', text) @@ -1330,12 +1335,9 @@ def do_update_fee(self): # show/hide fee rounding icon feerounding = (fee - displayed_fee) if fee else 0 - if feerounding: - self.feerounding_icon.setToolTip( - _('additional {} satoshis will be added').format(feerounding)) - self.feerounding_icon.setVisible(True) - else: - self.feerounding_icon.setVisible(False) + self.set_feerounding_text(feerounding) + self.feerounding_icon.setToolTip(self.feerounding_text) + self.feerounding_icon.setVisible(bool(feerounding)) if self.is_max: amount = tx.output_value() From d56dba80392bd5e7d679ab9483b2cbfd50d4c506 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 6 Feb 2018 19:10:56 +0100 Subject: [PATCH 082/270] watch-only wallets can't sign or decrypt messages --- gui/qt/main_window.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 0675929e35b6..e4f5085a7e8b 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2119,6 +2119,9 @@ def do_sign(self, address, message, signature, password): if not bitcoin.is_address(address): self.show_message(_('Invalid Bitcoin address.')) return + if self.wallet.is_watching_only(): + self.show_message(_('This is a watching-only wallet.')) + return if not self.wallet.is_mine(address): self.show_message(_('Address not in wallet.')) return @@ -2189,6 +2192,9 @@ def sign_verify_message(self, address=''): @protected def do_decrypt(self, message_e, pubkey_e, encrypted_e, password): + if self.wallet.is_watching_only(): + self.show_message(_('This is a watching-only wallet.')) + return 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'))) From 460e88ee536494d9cf9c66b4993d91ea4eccff8a Mon Sep 17 00:00:00 2001 From: slush Date: Tue, 6 Feb 2018 19:11:14 +0100 Subject: [PATCH 083/270] hw plugins: Allow custom enumerate functions trezor: Adding support for all supported transports (HID, WebUSB, UDP, Bridge) --- lib/plugins.py | 10 ++++++++++ plugins/trezor/plugin.py | 26 +++++--------------------- plugins/trezor/trezor.py | 17 +++++++++-------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/lib/plugins.py b/lib/plugins.py index e94013a24a92..b3611c167432 100644 --- a/lib/plugins.py +++ b/lib/plugins.py @@ -312,6 +312,8 @@ def __init__(self, config): # What we recognise. Each entry is a (vendor_id, product_id) # pair. self.recognised_hardware = set() + # Custom enumerate functions for devices we don't know about. + self.enumerate_func = set() # For synchronization self.lock = threading.RLock() self.hid_lock = threading.RLock() @@ -334,6 +336,9 @@ def register_devices(self, device_pairs): for pair in device_pairs: self.recognised_hardware.add(pair) + def register_enumerate_func(self, func): + self.enumerate_func.add(func) + def create_client(self, device, handler, plugin): # Get from cache first client = self.client_lookup(device.id_) @@ -509,6 +514,7 @@ def scan_devices(self): self.print_error("scanning devices...") with self.hid_lock: hid_list = hid.enumerate(0, 0) + # First see what's connected that we know about devices = [] for d in hid_list: @@ -524,6 +530,10 @@ def scan_devices(self): devices.append(Device(d['path'], interface_number, id_, product_key, usage_page)) + # Let plugin handlers enumerate devices we don't know about + for f in self.enumerate_func: + devices.extend(f()) + # Now find out what was disconnected pairs = [(dev.path, dev.id_) for dev in devices] disconnected_ids = [] diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py index 15fdb7526914..d828f8ae2f10 100644 --- a/plugins/trezor/plugin.py +++ b/plugins/trezor/plugin.py @@ -84,38 +84,22 @@ class TrezorCompatiblePlugin(HW_PluginBase): def __init__(self, parent, config, name): HW_PluginBase.__init__(self, parent, config, name) self.main_thread = threading.current_thread() - # FIXME: move to base class when Ledger is fixed if self.libraries_available: - self.device_manager().register_devices(self.DEVICE_IDS) + self.device_manager().register_enumerate_func(self.enumerate) - def _try_hid(self, device): - self.print_error("Trying to connect over USB...") + def create_client(self, device, handler): try: - return self.hid_transport(device) + self.print_error("Trying to connect to TREZOR...") + transport = self.transport(device) except BaseException as e: - # see fdb810ba622dc7dbe1259cbafb5b28e19d2ab114 - # raise self.print_error("cannot connect at", device.path, str(e)) return None - def _try_bridge(self, device): - self.print_error("Trying to connect over Trezor Bridge...") - try: - return self.bridge_transport({'path': hexlify(device.path)}) - except BaseException as e: - self.print_error("cannot connect to bridge", str(e)) - return None - - def create_client(self, device, handler): - # disable bridge because it seems to never returns if keepkey is plugged - #transport = self._try_bridge(device) or self._try_hid(device) - transport = self._try_hid(device) if not transport: - self.print_error("cannot connect to device") + self.print_error("cannot connect at", device.path) return self.print_error("connected to device at", device.path) - client = self.client_class(transport, handler, self) # Try a ping for device sanity diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py index 5ee31d390e96..258275dbdba7 100644 --- a/plugins/trezor/trezor.py +++ b/plugins/trezor/trezor.py @@ -16,21 +16,22 @@ def __init__(self, *args): from . import client import trezorlib import trezorlib.ckd_public - import trezorlib.transport_hid import trezorlib.messages + import trezorlib.device self.client_class = client.TrezorClient self.ckd_public = trezorlib.ckd_public self.types = trezorlib.messages - self.DEVICE_IDS = (trezorlib.transport_hid.DEV_TREZOR1, trezorlib.transport_hid.DEV_TREZOR2) + self.DEVICE_IDS = ('TREZOR',) self.libraries_available = True except ImportError: self.libraries_available = False TrezorCompatiblePlugin.__init__(self, *args) - def hid_transport(self, device): - from trezorlib.transport_hid import HidTransport - return HidTransport.find_by_path(device.path) + def enumerate(self): + from trezorlib.device import TrezorDevice + from electrum.plugins import Device + return [Device(str(d), -1, str(d), 'TREZOR', 0) for d in TrezorDevice.enumerate()] - def bridge_transport(self, d): - from trezorlib.transport_bridge import BridgeTransport - return BridgeTransport(d) + def transport(self, device): + from trezorlib.device import TrezorDevice + return TrezorDevice.find_by_path(device.path) From d3b94d935334cf9dc7deafecb15307773ebd6d44 Mon Sep 17 00:00:00 2001 From: slush Date: Tue, 6 Feb 2018 21:15:18 +0100 Subject: [PATCH 084/270] Small refactoring; removed one layer of unnecessary abstraction. --- plugins/trezor/plugin.py | 408 -------------------------------- plugins/trezor/qt_generic.py | 2 +- plugins/trezor/trezor.py | 434 +++++++++++++++++++++++++++++++++-- 3 files changed, 418 insertions(+), 426 deletions(-) delete mode 100644 plugins/trezor/plugin.py diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py deleted file mode 100644 index d828f8ae2f10..000000000000 --- a/plugins/trezor/plugin.py +++ /dev/null @@ -1,408 +0,0 @@ -import threading - -from binascii import hexlify, unhexlify - -from electrum.util import bfh, bh2u -from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, - TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants) -from electrum.i18n import _ -from electrum.plugins import BasePlugin -from electrum.transaction import deserialize -from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey - -from ..hw_wallet import HW_PluginBase - - -# TREZOR initialization methods -TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) - -# script "generation" -SCRIPT_GEN_LEGACY, SCRIPT_GEN_P2SH_SEGWIT, SCRIPT_GEN_NATIVE_SEGWIT = range(0, 3) - -class TrezorCompatibleKeyStore(Hardware_KeyStore): - - def get_derivation(self): - return self.derivation - - def get_script_gen(self): - def is_p2sh_segwit(): - return self.derivation.startswith("m/49'/") - - def is_native_segwit(): - return self.derivation.startswith("m/84'/") - - if is_native_segwit(): - return SCRIPT_GEN_NATIVE_SEGWIT - elif is_p2sh_segwit(): - return SCRIPT_GEN_P2SH_SEGWIT - else: - return SCRIPT_GEN_LEGACY - - def get_client(self, force_pair=True): - return self.plugin.get_client(self, force_pair) - - def decrypt_message(self, sequence, message, password): - raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) - - def sign_message(self, sequence, message, password): - client = self.get_client() - address_path = self.get_derivation() + "/%d/%d"%sequence - address_n = client.expand_path(address_path) - msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) - return msg_sig.signature - - def sign_transaction(self, tx, password): - if tx.is_complete(): - return - # previous transactions used as inputs - prev_tx = {} - # path of the xpubs that are involved - xpub_path = {} - for txin in tx.inputs(): - pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) - tx_hash = txin['prevout_hash'] - prev_tx[tx_hash] = txin['prev_tx'] - for x_pubkey in x_pubkeys: - if not is_xpubkey(x_pubkey): - continue - xpub, s = parse_xpubkey(x_pubkey) - if xpub == self.get_master_public_key(): - xpub_path[xpub] = self.get_derivation() - - self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) - - -class TrezorCompatiblePlugin(HW_PluginBase): - # Derived classes provide: - # - # class-static variables: client_class, firmware_URL, handler_class, - # libraries_available, libraries_URL, minimum_firmware, - # wallet_class, ckd_public, types, HidTransport - - MAX_LABEL_LEN = 32 - - def __init__(self, parent, config, name): - HW_PluginBase.__init__(self, parent, config, name) - self.main_thread = threading.current_thread() - if self.libraries_available: - self.device_manager().register_enumerate_func(self.enumerate) - - def create_client(self, device, handler): - try: - self.print_error("Trying to connect to TREZOR...") - transport = self.transport(device) - except BaseException as e: - self.print_error("cannot connect at", device.path, str(e)) - return None - - if not transport: - self.print_error("cannot connect at", device.path) - return - - self.print_error("connected to device at", device.path) - client = self.client_class(transport, handler, self) - - # Try a ping for device sanity - try: - client.ping('t') - except BaseException as e: - self.print_error("ping failed", str(e)) - return None - - if not client.atleast_version(*self.minimum_firmware): - msg = (_('Outdated {} firmware for device labelled {}. Please ' - 'download the updated firmware from {}') - .format(self.device, client.label(), self.firmware_URL)) - self.print_error(msg) - handler.show_error(msg) - return None - - return client - - def get_client(self, keystore, force_pair=True): - devmgr = self.device_manager() - handler = keystore.handler - with devmgr.hid_lock: - client = devmgr.client_for_keystore(self, handler, keystore, force_pair) - # returns the client for a given keystore. can use xpub - if client: - client.used() - return client - - def get_coin_name(self): - return "Testnet" if NetworkConstants.TESTNET else "Bitcoin" - - def initialize_device(self, device_id, wizard, handler): - # Initialization method - msg = _("Choose how you want to initialize your {}.\n\n" - "The first two methods are secure as no secret information " - "is entered into your computer.\n\n" - "For the last two methods you input secrets on your keyboard " - "and upload them to your {}, and so you should " - "only do those on a computer you know to be trustworthy " - "and free of malware." - ).format(self.device, self.device) - choices = [ - # Must be short as QT doesn't word-wrap radio button text - (TIM_NEW, _("Let the device generate a completely new seed randomly")), - (TIM_RECOVER, _("Recover from a seed you have previously written down")), - (TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")), - (TIM_PRIVKEY, _("Upload a master private key")) - ] - def f(method): - import threading - settings = self.request_trezor_init_settings(wizard, method, self.device) - t = threading.Thread(target = self._initialize_device, args=(settings, method, device_id, wizard, handler)) - t.setDaemon(True) - t.start() - wizard.loop.exec_() - wizard.choice_dialog(title=_('Initialize Device'), message=msg, choices=choices, run_next=f) - - def _initialize_device(self, settings, method, device_id, wizard, handler): - item, label, pin_protection, passphrase_protection = settings - - if method == TIM_RECOVER: - # FIXME the PIN prompt will appear over this message - # which makes this unreadable - handler.show_error(_( - "You will be asked to enter 24 words regardless of your " - "seed's actual length. If you enter a word incorrectly or " - "misspell it, you cannot change it or go back - you will need " - "to start again from the beginning.\n\nSo please enter " - "the words carefully!")) - - language = 'english' - devmgr = self.device_manager() - client = devmgr.client_by_id(device_id) - - if method == TIM_NEW: - strength = 64 * (item + 2) # 128, 192 or 256 - u2f_counter = 0 - skip_backup = False - client.reset_device(True, strength, passphrase_protection, - pin_protection, label, language, - u2f_counter, skip_backup) - elif method == TIM_RECOVER: - word_count = 6 * (item + 2) # 12, 18 or 24 - client.step = 0 - client.recovery_device(word_count, passphrase_protection, - pin_protection, label, language) - elif method == TIM_MNEMONIC: - pin = pin_protection # It's the pin, not a boolean - client.load_device_by_mnemonic(str(item), pin, - passphrase_protection, - label, language) - else: - pin = pin_protection # It's the pin, not a boolean - client.load_device_by_xprv(item, pin, passphrase_protection, - label, language) - wizard.loop.exit(0) - - def setup_device(self, device_info, wizard, purpose): - devmgr = self.device_manager() - device_id = device_info.device.id_ - client = devmgr.client_by_id(device_id) - # fixme: we should use: client.handler = wizard - client.handler = self.create_handler(wizard) - if not device_info.initialized: - self.initialize_device(device_id, wizard, client.handler) - client.get_xpub('m', 'standard') - client.used() - - def get_xpub(self, device_id, derivation, xtype, wizard): - devmgr = self.device_manager() - client = devmgr.client_by_id(device_id) - client.handler = wizard - xpub = client.get_xpub(derivation, xtype) - client.used() - return xpub - - def sign_transaction(self, keystore, tx, prev_tx, xpub_path): - self.prev_tx = prev_tx - self.xpub_path = xpub_path - client = self.get_client(keystore) - inputs = self.tx_inputs(tx, True, keystore.get_script_gen()) - outputs = self.tx_outputs(keystore.get_derivation(), tx, keystore.get_script_gen()) - signed_tx = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[1] - raw = bh2u(signed_tx) - tx.update_signatures(raw) - - def show_address(self, wallet, keystore, address): - client = self.get_client(keystore) - if not client.atleast_version(1, 3): - keystore.handler.show_error(_("Your device firmware is too old")) - return - change, index = wallet.get_address_index(address) - derivation = keystore.derivation - address_path = "%s/%d/%d"%(derivation, change, index) - address_n = client.expand_path(address_path) - xpubs = wallet.get_master_public_keys() - if len(xpubs) == 1: - script_gen = keystore.get_script_gen() - if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: - script_type = self.types.InputScriptType.SPENDWITNESS - elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: - script_type = self.types.InputScriptType.SPENDP2SHWITNESS - else: - script_type = self.types.InputScriptType.SPENDADDRESS - client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) - else: - def f(xpub): - node = self.ckd_public.deserialize(xpub) - return self.types.HDNodePathType(node=node, address_n=[change, index]) - pubkeys = wallet.get_public_keys(address) - # sort xpubs using the order of pubkeys - sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) - pubkeys = list(map(f, sorted_xpubs)) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=[b''] * wallet.n, - m=wallet.m, - ) - client.get_address(self.get_coin_name(), address_n, True, multisig=multisig) - - def tx_inputs(self, tx, for_sig=False, script_gen=SCRIPT_GEN_LEGACY): - inputs = [] - for txin in tx.inputs(): - txinputtype = self.types.TxInputType() - if txin['type'] == 'coinbase': - prev_hash = "\0"*32 - prev_index = 0xffffffff # signed int -1 - else: - if for_sig: - x_pubkeys = txin['x_pubkeys'] - if len(x_pubkeys) == 1: - x_pubkey = x_pubkeys[0] - xpub, s = parse_xpubkey(x_pubkey) - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype._extend_address_n(xpub_n + s) - if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: - txinputtype.script_type = self.types.InputScriptType.SPENDWITNESS - elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: - txinputtype.script_type = self.types.InputScriptType.SPENDP2SHWITNESS - else: - txinputtype.script_type = self.types.InputScriptType.SPENDADDRESS - else: - def f(x_pubkey): - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - else: - xpub = xpub_from_pubkey(0, bfh(x_pubkey)) - s = [] - node = self.ckd_public.deserialize(xpub) - return self.types.HDNodePathType(node=node, address_n=s) - pubkeys = list(map(f, x_pubkeys)) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))), - m=txin.get('num_sig'), - ) - if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: - script_type = self.types.InputScriptType.SPENDWITNESS - elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: - script_type = self.types.InputScriptType.SPENDP2SHWITNESS - else: - script_type = self.types.InputScriptType.SPENDMULTISIG - txinputtype = self.types.TxInputType( - script_type=script_type, - multisig=multisig - ) - # find which key is mine - for x_pubkey in x_pubkeys: - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - if xpub in self.xpub_path: - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype._extend_address_n(xpub_n + s) - break - - prev_hash = unhexlify(txin['prevout_hash']) - prev_index = txin['prevout_n'] - - if 'value' in txin: - txinputtype.amount = txin['value'] - txinputtype.prev_hash = prev_hash - txinputtype.prev_index = prev_index - - if 'scriptSig' in txin: - script_sig = bfh(txin['scriptSig']) - txinputtype.script_sig = script_sig - - txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) - - inputs.append(txinputtype) - - return inputs - - def tx_outputs(self, derivation, tx, script_gen=SCRIPT_GEN_LEGACY): - outputs = [] - has_change = False - - for _type, address, amount in tx.outputs(): - info = tx.output_info.get(address) - if info is not None and not has_change: - has_change = True # no more than one change address - index, xpubs, m = info - if len(xpubs) == 1: - if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: - script_type = self.types.OutputScriptType.PAYTOWITNESS - elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: - script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS - else: - script_type = self.types.OutputScriptType.PAYTOADDRESS - address_n = self.client_class.expand_path(derivation + "/%d/%d"%index) - txoutputtype = self.types.TxOutputType( - amount = amount, - script_type = script_type, - address_n = address_n, - ) - else: - if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: - script_type = self.types.OutputScriptType.PAYTOWITNESS - elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: - script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS - else: - script_type = self.types.OutputScriptType.PAYTOMULTISIG - address_n = self.client_class.expand_path("/%d/%d"%index) - nodes = map(self.ckd_public.deserialize, xpubs) - pubkeys = [ self.types.HDNodePathType(node=node, address_n=address_n) for node in nodes] - multisig = self.types.MultisigRedeemScriptType( - pubkeys = pubkeys, - signatures = [b''] * len(pubkeys), - m = m) - txoutputtype = self.types.TxOutputType( - multisig = multisig, - amount = amount, - address_n = self.client_class.expand_path(derivation + "/%d/%d"%index), - script_type = script_type) - else: - txoutputtype = self.types.TxOutputType() - txoutputtype.amount = amount - if _type == TYPE_SCRIPT: - txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN - txoutputtype.op_return_data = address[2:] - elif _type == TYPE_ADDRESS: - txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS - txoutputtype.address = address - - outputs.append(txoutputtype) - - return outputs - - def electrum_tx_to_txtype(self, tx): - t = self.types.TransactionType() - d = deserialize(tx.raw) - t.version = d['version'] - t.lock_time = d['lockTime'] - inputs = self.tx_inputs(tx) - t._extend_inputs(inputs) - for vout in d['outputs']: - o = t._add_bin_outputs() - o.amount = vout['value'] - o.script_pubkey = bfh(vout['scriptPubKey']) - return t - - # This function is called from the trezor libraries (via tx_api) - def get_tx(self, tx_hash): - tx = self.prev_tx[tx_hash] - return self.electrum_tx_to_txtype(tx) diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py index 68fbcf849a40..2440daee7672 100644 --- a/plugins/trezor/qt_generic.py +++ b/plugins/trezor/qt_generic.py @@ -5,7 +5,7 @@ from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton from PyQt5.Qt import QVBoxLayout, QLabel from electrum_gui.qt.util import * -from .plugin import TIM_NEW, TIM_RECOVER, TIM_MNEMONIC +from .trezor import TIM_NEW, TIM_RECOVER, TIM_MNEMONIC from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from electrum.i18n import _ diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py index 258275dbdba7..df3c96f88d39 100644 --- a/plugins/trezor/trezor.py +++ b/plugins/trezor/trezor.py @@ -1,37 +1,437 @@ -from .plugin import TrezorCompatiblePlugin, TrezorCompatibleKeyStore +import threading +from binascii import hexlify, unhexlify -class TrezorKeyStore(TrezorCompatibleKeyStore): +from electrum.util import bfh, bh2u +from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey, + TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants) +from electrum.i18n import _ +from electrum.plugins import BasePlugin, Device +from electrum.transaction import deserialize +from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey + +from ..hw_wallet import HW_PluginBase + + +# TREZOR initialization methods +TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) + +# script "generation" +SCRIPT_GEN_LEGACY, SCRIPT_GEN_P2SH_SEGWIT, SCRIPT_GEN_NATIVE_SEGWIT = range(0, 3) + + +class TrezorKeyStore(Hardware_KeyStore): hw_type = 'trezor' device = 'TREZOR' -class TrezorPlugin(TrezorCompatiblePlugin): + def get_derivation(self): + return self.derivation + + def get_script_gen(self): + def is_p2sh_segwit(): + return self.derivation.startswith("m/49'/") + + def is_native_segwit(): + return self.derivation.startswith("m/84'/") + + if is_native_segwit(): + return SCRIPT_GEN_NATIVE_SEGWIT + elif is_p2sh_segwit(): + return SCRIPT_GEN_P2SH_SEGWIT + else: + return SCRIPT_GEN_LEGACY + + def get_client(self, force_pair=True): + return self.plugin.get_client(self, force_pair) + + def decrypt_message(self, sequence, message, password): + raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device)) + + def sign_message(self, sequence, message, password): + client = self.get_client() + address_path = self.get_derivation() + "/%d/%d"%sequence + address_n = client.expand_path(address_path) + msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) + return msg_sig.signature + + def sign_transaction(self, tx, password): + if tx.is_complete(): + return + # previous transactions used as inputs + prev_tx = {} + # path of the xpubs that are involved + xpub_path = {} + for txin in tx.inputs(): + pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) + tx_hash = txin['prevout_hash'] + prev_tx[tx_hash] = txin['prev_tx'] + for x_pubkey in x_pubkeys: + if not is_xpubkey(x_pubkey): + continue + xpub, s = parse_xpubkey(x_pubkey) + if xpub == self.get_master_public_key(): + xpub_path[xpub] = self.get_derivation() + + self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) + + +class TrezorPlugin(HW_PluginBase): + # Derived classes provide: + # + # class-static variables: client_class, firmware_URL, handler_class, + # libraries_available, libraries_URL, minimum_firmware, + # wallet_class, ckd_public, types + firmware_URL = 'https://wallet.trezor.io' libraries_URL = 'https://github.com/trezor/python-trezor' minimum_firmware = (1, 5, 2) keystore_class = TrezorKeyStore - def __init__(self, *args): + MAX_LABEL_LEN = 32 + + def __init__(self, parent, config, name): + HW_PluginBase.__init__(self, parent, config, name) + self.main_thread = threading.current_thread() + try: - from . import client + # Minimal test if python-trezor is installed import trezorlib - import trezorlib.ckd_public - import trezorlib.messages - import trezorlib.device - self.client_class = client.TrezorClient - self.ckd_public = trezorlib.ckd_public - self.types = trezorlib.messages - self.DEVICE_IDS = ('TREZOR',) self.libraries_available = True except ImportError: self.libraries_available = False - TrezorCompatiblePlugin.__init__(self, *args) + return + + from . import client + import trezorlib.ckd_public + import trezorlib.messages + self.client_class = client.TrezorClient + self.ckd_public = trezorlib.ckd_public + self.types = trezorlib.messages + self.DEVICE_IDS = ('TREZOR',) + + self.device_manager().register_enumerate_func(self.enumerate) def enumerate(self): from trezorlib.device import TrezorDevice - from electrum.plugins import Device - return [Device(str(d), -1, str(d), 'TREZOR', 0) for d in TrezorDevice.enumerate()] + return [Device(d.get_path(), -1, d.get_path(), 'TREZOR', 0) for d in TrezorDevice.enumerate()] - def transport(self, device): + def create_client(self, device, handler): from trezorlib.device import TrezorDevice - return TrezorDevice.find_by_path(device.path) + try: + self.print_error("connecting to device at", device.path) + transport = TrezorDevice.find_by_path(device.path) + except BaseException as e: + self.print_error("cannot connect at", device.path, str(e)) + return None + + if not transport: + self.print_error("cannot connect at", device.path) + return + + self.print_error("connected to device at", device.path) + client = self.client_class(transport, handler, self) + + # Try a ping for device sanity + try: + client.ping('t') + except BaseException as e: + self.print_error("ping failed", str(e)) + return None + + if not client.atleast_version(*self.minimum_firmware): + msg = (_('Outdated {} firmware for device labelled {}. Please ' + 'download the updated firmware from {}') + .format(self.device, client.label(), self.firmware_URL)) + self.print_error(msg) + handler.show_error(msg) + return None + + return client + + def get_client(self, keystore, force_pair=True): + devmgr = self.device_manager() + handler = keystore.handler + with devmgr.hid_lock: + client = devmgr.client_for_keystore(self, handler, keystore, force_pair) + # returns the client for a given keystore. can use xpub + if client: + client.used() + return client + + def get_coin_name(self): + return "Testnet" if NetworkConstants.TESTNET else "Bitcoin" + + def initialize_device(self, device_id, wizard, handler): + # Initialization method + msg = _("Choose how you want to initialize your {}.\n\n" + "The first two methods are secure as no secret information " + "is entered into your computer.\n\n" + "For the last two methods you input secrets on your keyboard " + "and upload them to your {}, and so you should " + "only do those on a computer you know to be trustworthy " + "and free of malware." + ).format(self.device, self.device) + choices = [ + # Must be short as QT doesn't word-wrap radio button text + (TIM_NEW, _("Let the device generate a completely new seed randomly")), + (TIM_RECOVER, _("Recover from a seed you have previously written down")), + (TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")), + (TIM_PRIVKEY, _("Upload a master private key")) + ] + def f(method): + import threading + settings = self.request_trezor_init_settings(wizard, method, self.device) + t = threading.Thread(target = self._initialize_device, args=(settings, method, device_id, wizard, handler)) + t.setDaemon(True) + t.start() + wizard.loop.exec_() + wizard.choice_dialog(title=_('Initialize Device'), message=msg, choices=choices, run_next=f) + + def _initialize_device(self, settings, method, device_id, wizard, handler): + item, label, pin_protection, passphrase_protection = settings + + if method == TIM_RECOVER: + # FIXME the PIN prompt will appear over this message + # which makes this unreadable + handler.show_error(_( + "You will be asked to enter 24 words regardless of your " + "seed's actual length. If you enter a word incorrectly or " + "misspell it, you cannot change it or go back - you will need " + "to start again from the beginning.\n\nSo please enter " + "the words carefully!")) + + language = 'english' + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + + if method == TIM_NEW: + strength = 64 * (item + 2) # 128, 192 or 256 + u2f_counter = 0 + skip_backup = False + client.reset_device(True, strength, passphrase_protection, + pin_protection, label, language, + u2f_counter, skip_backup) + elif method == TIM_RECOVER: + word_count = 6 * (item + 2) # 12, 18 or 24 + client.step = 0 + client.recovery_device(word_count, passphrase_protection, + pin_protection, label, language) + elif method == TIM_MNEMONIC: + pin = pin_protection # It's the pin, not a boolean + client.load_device_by_mnemonic(str(item), pin, + passphrase_protection, + label, language) + else: + pin = pin_protection # It's the pin, not a boolean + client.load_device_by_xprv(item, pin, passphrase_protection, + label, language) + wizard.loop.exit(0) + + def setup_device(self, device_info, wizard, purpose): + devmgr = self.device_manager() + device_id = device_info.device.id_ + client = devmgr.client_by_id(device_id) + # fixme: we should use: client.handler = wizard + client.handler = self.create_handler(wizard) + if not device_info.initialized: + self.initialize_device(device_id, wizard, client.handler) + client.get_xpub('m', 'standard') + client.used() + + def get_xpub(self, device_id, derivation, xtype, wizard): + devmgr = self.device_manager() + client = devmgr.client_by_id(device_id) + client.handler = wizard + xpub = client.get_xpub(derivation, xtype) + client.used() + return xpub + + def sign_transaction(self, keystore, tx, prev_tx, xpub_path): + self.prev_tx = prev_tx + self.xpub_path = xpub_path + client = self.get_client(keystore) + inputs = self.tx_inputs(tx, True, keystore.get_script_gen()) + outputs = self.tx_outputs(keystore.get_derivation(), tx, keystore.get_script_gen()) + signed_tx = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[1] + raw = bh2u(signed_tx) + tx.update_signatures(raw) + + def show_address(self, wallet, keystore, address): + client = self.get_client(keystore) + if not client.atleast_version(1, 3): + keystore.handler.show_error(_("Your device firmware is too old")) + return + change, index = wallet.get_address_index(address) + derivation = keystore.derivation + address_path = "%s/%d/%d"%(derivation, change, index) + address_n = client.expand_path(address_path) + xpubs = wallet.get_master_public_keys() + if len(xpubs) == 1: + script_gen = keystore.get_script_gen() + if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: + script_type = self.types.InputScriptType.SPENDWITNESS + elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: + script_type = self.types.InputScriptType.SPENDP2SHWITNESS + else: + script_type = self.types.InputScriptType.SPENDADDRESS + client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) + else: + def f(xpub): + node = self.ckd_public.deserialize(xpub) + return self.types.HDNodePathType(node=node, address_n=[change, index]) + pubkeys = wallet.get_public_keys(address) + # sort xpubs using the order of pubkeys + sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) + pubkeys = list(map(f, sorted_xpubs)) + multisig = self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=[b''] * wallet.n, + m=wallet.m, + ) + client.get_address(self.get_coin_name(), address_n, True, multisig=multisig) + + def tx_inputs(self, tx, for_sig=False, script_gen=SCRIPT_GEN_LEGACY): + inputs = [] + for txin in tx.inputs(): + txinputtype = self.types.TxInputType() + if txin['type'] == 'coinbase': + prev_hash = "\0"*32 + prev_index = 0xffffffff # signed int -1 + else: + if for_sig: + x_pubkeys = txin['x_pubkeys'] + if len(x_pubkeys) == 1: + x_pubkey = x_pubkeys[0] + xpub, s = parse_xpubkey(x_pubkey) + xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) + txinputtype._extend_address_n(xpub_n + s) + if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: + txinputtype.script_type = self.types.InputScriptType.SPENDWITNESS + elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: + txinputtype.script_type = self.types.InputScriptType.SPENDP2SHWITNESS + else: + txinputtype.script_type = self.types.InputScriptType.SPENDADDRESS + else: + def f(x_pubkey): + if is_xpubkey(x_pubkey): + xpub, s = parse_xpubkey(x_pubkey) + else: + xpub = xpub_from_pubkey(0, bfh(x_pubkey)) + s = [] + node = self.ckd_public.deserialize(xpub) + return self.types.HDNodePathType(node=node, address_n=s) + pubkeys = list(map(f, x_pubkeys)) + multisig = self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))), + m=txin.get('num_sig'), + ) + if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: + script_type = self.types.InputScriptType.SPENDWITNESS + elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: + script_type = self.types.InputScriptType.SPENDP2SHWITNESS + else: + script_type = self.types.InputScriptType.SPENDMULTISIG + txinputtype = self.types.TxInputType( + script_type=script_type, + multisig=multisig + ) + # find which key is mine + for x_pubkey in x_pubkeys: + if is_xpubkey(x_pubkey): + xpub, s = parse_xpubkey(x_pubkey) + if xpub in self.xpub_path: + xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) + txinputtype._extend_address_n(xpub_n + s) + break + + prev_hash = unhexlify(txin['prevout_hash']) + prev_index = txin['prevout_n'] + + if 'value' in txin: + txinputtype.amount = txin['value'] + txinputtype.prev_hash = prev_hash + txinputtype.prev_index = prev_index + + if 'scriptSig' in txin: + script_sig = bfh(txin['scriptSig']) + txinputtype.script_sig = script_sig + + txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) + + inputs.append(txinputtype) + + return inputs + + def tx_outputs(self, derivation, tx, script_gen=SCRIPT_GEN_LEGACY): + outputs = [] + has_change = False + + for _type, address, amount in tx.outputs(): + info = tx.output_info.get(address) + if info is not None and not has_change: + has_change = True # no more than one change address + index, xpubs, m = info + if len(xpubs) == 1: + if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: + script_type = self.types.OutputScriptType.PAYTOWITNESS + elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: + script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS + else: + script_type = self.types.OutputScriptType.PAYTOADDRESS + address_n = self.client_class.expand_path(derivation + "/%d/%d"%index) + txoutputtype = self.types.TxOutputType( + amount = amount, + script_type = script_type, + address_n = address_n, + ) + else: + if script_gen == SCRIPT_GEN_NATIVE_SEGWIT: + script_type = self.types.OutputScriptType.PAYTOWITNESS + elif script_gen == SCRIPT_GEN_P2SH_SEGWIT: + script_type = self.types.OutputScriptType.PAYTOP2SHWITNESS + else: + script_type = self.types.OutputScriptType.PAYTOMULTISIG + address_n = self.client_class.expand_path("/%d/%d"%index) + nodes = map(self.ckd_public.deserialize, xpubs) + pubkeys = [ self.types.HDNodePathType(node=node, address_n=address_n) for node in nodes] + multisig = self.types.MultisigRedeemScriptType( + pubkeys = pubkeys, + signatures = [b''] * len(pubkeys), + m = m) + txoutputtype = self.types.TxOutputType( + multisig = multisig, + amount = amount, + address_n = self.client_class.expand_path(derivation + "/%d/%d"%index), + script_type = script_type) + else: + txoutputtype = self.types.TxOutputType() + txoutputtype.amount = amount + if _type == TYPE_SCRIPT: + txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN + txoutputtype.op_return_data = address[2:] + elif _type == TYPE_ADDRESS: + txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS + txoutputtype.address = address + + outputs.append(txoutputtype) + + return outputs + + def electrum_tx_to_txtype(self, tx): + t = self.types.TransactionType() + d = deserialize(tx.raw) + t.version = d['version'] + t.lock_time = d['lockTime'] + inputs = self.tx_inputs(tx) + t._extend_inputs(inputs) + for vout in d['outputs']: + o = t._add_bin_outputs() + o.amount = vout['value'] + o.script_pubkey = bfh(vout['scriptPubKey']) + return t + + # This function is called from the trezor libraries (via tx_api) + def get_tx(self, tx_hash): + tx = self.prev_tx[tx_hash] + return self.electrum_tx_to_txtype(tx) From 41e836a9f4cc25362e0a77e034fad61b2df3c851 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 6 Feb 2018 23:15:08 +0100 Subject: [PATCH 085/270] storage upgrade: don't run older upgrade methods on newer version storage --- lib/storage.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/storage.py b/lib/storage.py index ada874045bdf..d87a339a8344 100644 --- a/lib/storage.py +++ b/lib/storage.py @@ -339,6 +339,9 @@ def upgrade(self): self.write() def convert_wallet_type(self): + if not self._is_upgrade_method_needed(0, 13): + return + wallet_type = self.get('wallet_type') if wallet_type == 'btchip': wallet_type = 'ledger' if self.get('keystore') or self.get('x1/') or wallet_type=='imported': @@ -522,6 +525,9 @@ def remove_from_list(list_name): self.put('seed_version', 16) def convert_imported(self): + if not self._is_upgrade_method_needed(0, 13): + return + # '/x' is the internal ID for imported accounts d = self.get('accounts', {}).get('/x', {}).get('imported',{}) if not d: @@ -548,6 +554,9 @@ def convert_imported(self): raise BaseException('no addresses or privkeys') def convert_account(self): + if not self._is_upgrade_method_needed(0, 13): + return + self.put('accounts', None) def _is_upgrade_method_needed(self, min_version, max_version): From aaa0ee75b7603043dd96e352ee07d01e0f315b48 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 7 Feb 2018 16:54:03 +0100 Subject: [PATCH 086/270] make freeze_packages.sh cwd independent. update requirements-hw.txt. re-run freeze_packages.sh. --- contrib/deterministic-build/requirements-hw.txt | 7 +++++-- contrib/deterministic-build/requirements.txt | 2 +- contrib/freeze_packages.sh | 2 +- requirements-hw.txt | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index fc312796fa16..8e0ba52f0407 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -1,15 +1,18 @@ btchip-python==0.1.24 -certifi==2017.11.5 +certifi==2018.1.18 chardet==3.0.4 +click==6.7 Cython==0.27.3 ecdsa==0.13 hidapi==0.7.99.post21 idna==2.6 keepkey==4.0.2 +libusb1==1.6.4 mnemonic==0.18 pbkdf2==1.3 protobuf==3.5.1 +pyblake2==1.1.0 requests==2.18.4 six==1.11.0 -trezor==0.7.16 +trezor==0.9.0 urllib3==1.22 diff --git a/contrib/deterministic-build/requirements.txt b/contrib/deterministic-build/requirements.txt index 8594f0bccf59..e7d8925b80a0 100644 --- a/contrib/deterministic-build/requirements.txt +++ b/contrib/deterministic-build/requirements.txt @@ -1,4 +1,4 @@ -certifi==2017.11.5 +certifi==2018.1.18 chardet==3.0.4 dnspython==1.15.0 ecdsa==0.13 diff --git a/contrib/freeze_packages.sh b/contrib/freeze_packages.sh index 073df5f18173..2d6b037554c8 100755 --- a/contrib/freeze_packages.sh +++ b/contrib/freeze_packages.sh @@ -31,7 +31,7 @@ source $venv_dir/bin/activate echo "Installing hw wallet dependencies" -python -m pip install -r ../requirements-hw.txt --upgrade +python -m pip install -r $contrib/../requirements-hw.txt --upgrade pip freeze | sed '/^Electrum/ d' > $contrib/deterministic-build/requirements-hw.txt diff --git a/requirements-hw.txt b/requirements-hw.txt index e5f460aaf62a..5647f3cad08d 100644 --- a/requirements-hw.txt +++ b/requirements-hw.txt @@ -1,4 +1,4 @@ Cython>=0.27 -trezor +trezor>=0.9.0 keepkey btchip-python From 81bd8d8d679a0a6ece4b35da818cf8ce958016dd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 7 Feb 2018 17:30:08 +0100 Subject: [PATCH 087/270] fix: fee_to_depth --- lib/simple_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/simple_config.py b/lib/simple_config.py index cdbe79562348..b7a41ddcc44b 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -284,7 +284,7 @@ def fee_to_depth(self, target_fee): depth = 0 for fee, s in self.mempool_fees: depth += s - if fee < target_fee: + if fee <= target_fee: break else: return 0 From 6c4756dc3df877163b8b5e99377efd56bbae72bf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 7 Feb 2018 17:51:52 +0100 Subject: [PATCH 088/270] 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 b8e8ac8e50e3..a59f2a5a8b01 100644 --- a/lib/util.py +++ b/lib/util.py @@ -734,4 +734,8 @@ def run_with_except_hook(*args2, **kwargs2): 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 a9f7291c41f4..d2a3bb5f3664 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 @@ def load_wallet(self, wallet, window): 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 cf56ee973acc..9bab60347544 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -10,7 +10,7 @@ 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 @@ def label(self): 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 @@ def perform_hw1_preflight(self): 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 df3c96f88d39..f80346c71ac3 100644 --- a/plugins/trezor/trezor.py +++ b/plugins/trezor/trezor.py @@ -2,7 +2,7 @@ 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 @@ def __init__(self, parent, config, name): 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 f4363e7967d06a6b643449915b138cc75c2dbcea Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 8 Feb 2018 05:56:54 +0100 Subject: [PATCH 089/270] zbar for windows --- contrib/build-wine/deterministic.spec | 1 + contrib/build-wine/prepare-wine.sh | 8 ++++++-- lib/qrscanner.py | 10 +++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index 3dc5953b537b..5a6be0c54124 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -28,6 +28,7 @@ datas = [ (home+'lib/wordlist/english.txt', 'electrum/wordlist'), (home+'lib/locale', 'electrum/locale'), (home+'plugins', 'electrum_plugins'), + ('C:\\Program Files (x86)\\ZBar\\bin\\', '.') ] datas += collect_data_files('trezorlib') datas += collect_data_files('btchip') diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index ad26fca130c2..e9c1c5f48d50 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -3,6 +3,8 @@ # 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 +ZBAR_URL=https://sourceforge.net/projects/zbarw/files/zbarw-20121031-setup.exe/download +ZBAR_SHA256=177e32b272fa76528a3af486b74e9cb356707be1c5ace4ed3fcee9723e2c2c02 PYTHON_VERSION=3.5.4 ## These settings probably don't need change @@ -83,8 +85,10 @@ $PYTHON -m pip install PyQt5 # 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 +wget -q -O zbar.exe "$ZBAR_URL" +verify_hash zbar.exe $ZBAR_SHA256 +echo "Install zbar. Next-next-next; and skip the readme." +wine zbar.exe # install Cryptodome $PYTHON -m pip install pycryptodomex diff --git a/lib/qrscanner.py b/lib/qrscanner.py index 7516d55d5716..6212b4e68c03 100644 --- a/lib/qrscanner.py +++ b/lib/qrscanner.py @@ -29,8 +29,8 @@ if sys.platform == 'darwin': name = 'libzbar.dylib' -elif sys.platform == 'windows': - name = 'libzbar.dll' +elif sys.platform in ('windows', 'win32'): + name = 'libzbar-0.dll' else: name = 'libzbar.so.0' @@ -40,7 +40,7 @@ libzbar = None -def scan_barcode(device='', timeout=-1, display=True, threaded=False): +def scan_barcode(device='', timeout=-1, display=True, threaded=False, try_again=True): if libzbar is None: raise RuntimeError("Cannot start QR scanner; zbar not available.") libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p @@ -50,6 +50,10 @@ def scan_barcode(device='', timeout=-1, display=True, threaded=False): proc = libzbar.zbar_processor_create(threaded) libzbar.zbar_processor_request_size(proc, 640, 480) if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0: + if try_again: + # workaround for a bug in "ZBar for Windows" + # libzbar.zbar_processor_init always seem to fail the first time around + return scan_barcode(device, timeout, display, threaded, try_again=False) raise RuntimeError("Can not start QR scanner; initialization failed.") libzbar.zbar_processor_set_visible(proc) if libzbar.zbar_process_one(proc, timeout): From 30a43189fc5069c645635f3ea3c7e33602969dd8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 8 Feb 2018 06:08:04 +0100 Subject: [PATCH 090/270] make zbar install silent --- contrib/build-wine/prepare-wine.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index e9c1c5f48d50..fc6080ea4709 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -87,8 +87,7 @@ $PYTHON -m pip install PyQt5 # Install ZBar wget -q -O zbar.exe "$ZBAR_URL" verify_hash zbar.exe $ZBAR_SHA256 -echo "Install zbar. Next-next-next; and skip the readme." -wine zbar.exe +wine zbar.exe /S # install Cryptodome $PYTHON -m pip install pycryptodomex From c9d93d30c7380a302ab7cc2310121154ce786dbf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 8 Feb 2018 17:33:57 +0100 Subject: [PATCH 091/270] 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 16d07948f420..30270d17c1b1 100644 --- a/gui/qt/__init__.py +++ b/gui/qt/__init__.py @@ -94,6 +94,8 @@ def __init__(self, config, daemon, plugins): 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 092/270] Fix CoinDesk exchange rates and update currencies.json --- lib/currencies.json | 1371 +++++++++++++++++++++++------------------- lib/exchange_rate.py | 15 +- 2 files changed, 778 insertions(+), 608 deletions(-) diff --git a/lib/currencies.json b/lib/currencies.json index 81680d1047df..a4e85f1f6fc4 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" - ], + ], + "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" + ], "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", + "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" - ], - "Coinsecure": [ - "INR" - ], + ], "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 23de311ec5b9..e6a562474aa7 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -33,7 +33,7 @@ def __init__(self, on_quotes, on_history): 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 @@ def get_rates(self, ccy): 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 093/270] 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 e4f5085a7e8b..1c860edfe22a 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1136,7 +1136,8 @@ def setAmount(self, byte_size): 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 @@ def on_unconf(x): 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 472e3aa3820f..ffc5bfd8e901 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 @@ def trailing_zeroes(val): 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 @@ def trailing_zeroes(val): 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 094/270] 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 b7a41ddcc44b..c0bbb2162964 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -428,7 +428,12 @@ def estimate_fee(self, size): @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 095/270] 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 1c860edfe22a..62fb8f056ea1 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2494,32 +2494,13 @@ def plot_history_dialog(self): 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 @@ def do_export_history(self, wallet, fileName, is_csv): 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 c0bfb48ed51e..29bfd8b6bf80 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -440,46 +440,16 @@ def paytomany(self, outputs, fee=None, from_addr=None, change_addr=None, nocheck 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 @@ def help(self): '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 @@ def help(self): '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 6a682debfcf9..4c65501506b7 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -914,6 +914,57 @@ def get_history(self, domain=None): 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 096/270] 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 63a0b4b9b8df..4c8090313433 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -62,6 +62,7 @@ def refresh_headers(self): 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 @@ def on_update(self): 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 e6a562474aa7..5003721d029d 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -493,3 +493,8 @@ def history_rate(self, d_t): 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 d7b80a5c195a..96a3b7605943 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -962,6 +962,8 @@ def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx 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 @@ def get_depending_transactions(self, tx_hash): 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 097/270] 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 5003721d029d..fbd8658e5d7b 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -488,7 +488,7 @@ def history_rate(self, d_t): 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 96a3b7605943..6568aa9b7b11 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -38,6 +38,7 @@ from functools import partial from collections import defaultdict from numbers import Number +from decimal import Decimal import sys @@ -915,7 +916,6 @@ def get_history(self, domain=None): 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 @@ def capital_gain(self, txid, price_func): 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 098/270] cache historical exchange rates --- lib/exchange_rate.py | 52 ++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index fbd8658e5d7b..83bce0d48dc8 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -2,6 +2,8 @@ import inspect import requests import sys +import os +import json from threading import Thread import time import csv @@ -59,19 +61,34 @@ def update(self, ccy): 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(self, ccy): + 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, 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 @@ def history_ccys(self): '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 @@ def get_rates(self, ccy): 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 @@ def history_starts(self): 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 @@ def get_rates(self, ccy): 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 @@ def __init__(self, config, network): 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 @@ def run(self): # 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 099/270] 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 25495f79e389..0e109e8efee1 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 29bfd8b6bf80..f2230394995a 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -138,6 +138,8 @@ def restore(self, text): @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 7902c98a93d7..82192cfda0bf 100644 --- a/plugins/digitalbitbox/cmdline.py +++ b/plugins/digitalbitbox/cmdline.py @@ -9,3 +9,6 @@ def init_keystore(self, keystore): 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 c57e395dd4e2..2f63fda4d1cb 100644 --- a/plugins/digitalbitbox/digitalbitbox.py +++ b/plugins/digitalbitbox/digitalbitbox.py @@ -661,7 +661,8 @@ def get_dbb_device(self, device): 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 cd30bc0ccc03..4262b70198f5 100644 --- a/plugins/keepkey/cmdline.py +++ b/plugins/keepkey/cmdline.py @@ -9,3 +9,6 @@ def init_keystore(self, keystore): 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 b0b252ac8d62..5d8c9f46de08 100644 --- a/plugins/ledger/cmdline.py +++ b/plugins/ledger/cmdline.py @@ -9,3 +9,6 @@ def init_keystore(self, keystore): 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 9bab60347544..3d34bc9da497 100644 --- a/plugins/ledger/ledger.py +++ b/plugins/ledger/ledger.py @@ -516,7 +516,8 @@ def get_btchip_device(self, device): 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 9149eeee444f..630578acc255 100644 --- a/plugins/trezor/cmdline.py +++ b/plugins/trezor/cmdline.py @@ -9,3 +9,6 @@ def init_keystore(self, keystore): 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 100/270] 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 000000000000..b50bed1b16eb --- /dev/null +++ b/pubkeys/bauerj.asc @@ -0,0 +1,166 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +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 101/270] 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 48355183be3d..38a0acf8f2b4 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 d8af8c9d648d..ac265a7f5df6 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 ad26fca130c2..42828be7780a 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 000000000000..af90b89dd9bd --- /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 2d6b037554c8..3471e528bff5 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 000000000000..68181dd319e1 --- /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 80330b1cf19f..63581a614106 100755 --- a/setup.py +++ b/setup.py @@ -9,10 +9,10 @@ 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 @@ 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 102/270] 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 4c8090313433..12471c0e687f 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -63,6 +63,7 @@ def refresh_headers(self): 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 @@ def on_update(self): 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 @@ def on_update(self): 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 @@ def create_menu(self, position): 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 6568aa9b7b11..9e673e902958 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -185,6 +185,7 @@ def __init__(self, storage): 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 @@ def set_label(self, name, text = None): 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 @@ def txin_value(self, txin): 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 103/270] capital gains: update release notes --- RELEASE-NOTES | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 6413c9e22bca..ddadee13d6b4 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 104/270] 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 9e673e902958..f68de6465044 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -322,6 +322,9 @@ def load_addresses(self): 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 @@ def __init__(self, storage): 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 @@ def synchronize_sequence(self, for_change): 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 105/270] 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 ed1471d52530..0133f789e027 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -522,7 +522,12 @@ def get_card(self, addr, balance, is_used, label): 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 3d594c9c3d1b..d0247a3429e4 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 106/270] fix #3890 --- lib/wallet.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index f68de6465044..3a26b4131b18 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -208,7 +208,7 @@ def __init__(self, storage): 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 @@ def load_addresses(self): 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 107/270] 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 ac265a7f5df6..f0c7bc05b584 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 108/270] 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 f0c7bc05b584..0626e9fd676b 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 109/270] 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 a5c74cee79f3..1a6dc6228ef9 100644 --- a/gui/kivy/uix/dialogs/bump_fee_dialog.py +++ b/gui/kivy/uix/dialogs/bump_fee_dialog.py @@ -73,7 +73,7 @@ def __init__(self, app, fee, size, callback): 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 25e9926c43bc..1c61c6a21f83 100644 --- a/gui/kivy/uix/dialogs/fee_dialog.py +++ b/gui/kivy/uix/dialogs/fee_dialog.py @@ -78,8 +78,8 @@ def __init__(self, app, config, callback): 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 209b0de7611f..04911d878219 100644 --- a/gui/qt/fee_slider.py +++ b/gui/qt/fee_slider.py @@ -21,7 +21,7 @@ def __init__(self, window, config, callback): 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 @@ def moved(self, pos): 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 @@ def get_tooltip(self, pos, fee_rate): 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 62fb8f056ea1..df86e3e893b8 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1081,7 +1081,7 @@ def create_send_tab(self): 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 @@ def on_nz(): 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 c0bbb2162964..072edccd0e05 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -324,7 +324,7 @@ def eta_tooltip(self, x): 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 @@ def has_fee_mempool(self): 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 110/270] 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 9e673e902958..454e018c45f5 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -988,7 +988,7 @@ def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx 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 111/270] 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 12471c0e687f..dcb5661474b2 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -100,7 +100,7 @@ def on_update(self): 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 112/270] 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 0626e9fd676b..70793499d125 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 cfce7172f488..2efda9f14bb6 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 113/270] 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 84339755986f..8b9f7967239b 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 @@ def set_mainnet(cls): 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 @@ def set_testnet(cls): 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 114/270] 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 609006cdd000..e63fb6181126 100644 --- a/lib/tests/test_transaction.py +++ b/lib/tests/test_transaction.py @@ -235,6 +235,11 @@ def test_txid_input_p2wsh_p2sh_not_multisig(self): 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 b23cf9cf2b53..ddea8246c8cf 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -32,6 +32,8 @@ 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 115/270] 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 29bfd8b6bf80..d6c71a8a7879 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -449,6 +449,10 @@ def history(self, year=None, show_addresses=False, show_fiat=False): 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 38baeb158427..f8497144a4e9 100644 --- a/lib/daemon.py +++ b/lib/daemon.py @@ -121,13 +121,12 @@ def __init__(self, config, fd, is_gui): 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 83bce0d48dc8..26e114097f15 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -61,9 +61,10 @@ def update(self, ccy): 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 @@ def get_historical_rates_safe(self, ccy, cache_dir): 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 @@ def __init__(self, config, network): 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 @@ def set_exchange(self, name): # 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 @@ def historical_value_str(self, satoshis, d_t): 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 116/270] 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 dcb5661474b2..7e871aa249b7 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -61,7 +61,8 @@ def refresh_headers(self): 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 @@ def on_update(self): 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 @@ def on_update(self): 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 26e114097f15..ffd4c18a6a64 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -506,10 +506,14 @@ def get_fiat_status_text(self, btc_balance, base_unit, decimal_point): 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 3244d9775279..1967b6c9c3dd 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -943,10 +943,21 @@ def get_history(self, domain=None): 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 @@ def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx '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 @@ def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx 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 @@ def capital_gain(self, txid, price_func, ccy): 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 117/270] minor fixes --- lib/wallet.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 1967b6c9c3dd..862410065d35 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1011,9 +1011,11 @@ def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx 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 118/270] change names --- lib/wallet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 862410065d35..7f7876723813 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1022,16 +1022,16 @@ def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx 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 119/270] 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 609006cdd000..1af87b046e52 100644 --- a/lib/tests/test_transaction.py +++ b/lib/tests/test_transaction.py @@ -236,6 +236,380 @@ def test_txid_input_p2wsh_p2sh_not_multisig(self): 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 120/270] 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 ddea8246c8cf..4ee57d4e1d2a 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 121/270] 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 7e871aa249b7..04d27f6d1407 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -119,7 +119,7 @@ def on_update(self): 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 122/270] 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 30270d17c1b1..f7abac675341 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 @@ def start_new_window(self, path, uri): 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 @@ def main(self): 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 123/270] 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 7e8dda1eddda..a1794459355f 100644 --- a/gui/qt/contact_list.py +++ b/gui/qt/contact_list.py @@ -26,7 +26,7 @@ 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 @@ def import_contacts(self): 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 19cfea60aaec..a4a8374f7ed2 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 @@ def import_invoices(self): 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 3b5a3255dd48..5157adc41057 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 @@ def import_file(self, path): 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 8c9c6009cb23..c1e25441ac61 100644 --- a/lib/paymentrequest.py +++ b/lib/paymentrequest.py @@ -40,6 +40,7 @@ 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 @@ def import_file(self, path): 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 a59f2a5a8b01..60723810ac0c 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 124/270] 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 a603af5ef416..a15bbe25a540 100644 --- a/gui/qt/exception_window.py +++ b/gui/qt/exception_window.py @@ -34,7 +34,7 @@ from electrum.i18n import _ import sys -from electrum import ELECTRUM_VERSION +from electrum import ELECTRUM_VERSION, bitcoin issue_template = """

Traceback

@@ -105,6 +105,10 @@ def __init__(self, main_window, exctype, value, tb):
         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 125/270] 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 ffd4c18a6a64..cb43cd6e0b44 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -79,7 +79,7 @@ def read_historical_rates(self, ccy, cache_dir):
         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 @@ def get_historical_rates_safe(self, ccy, cache_dir):
             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 126/270] 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 7f7876723813..81091ac12db3 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -731,10 +731,7 @@ def get_conflicting_transactions(self, tx):
                 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 127/270] 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 2efda9f14bb6..bb48dddf8445 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 af90b89dd9bd..381b4378f79e 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 128/270] 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 f7abac675341..0879208f94ff 100644
--- a/gui/qt/__init__.py
+++ b/gui/qt/__init__.py
@@ -195,7 +195,8 @@ def start_new_window(self, path, uri):
                 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 @@ def start_new_window(self, path, uri):
                     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 f8497144a4e9..bebcf4046f57 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 @@ def init_gui(self, config, plugins):
             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 129/270] 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 70793499d125..a2f7b50f80c8 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 a4e39adf1a3f..8bf650626c9b 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 cf8a326cdf7f..000000000000
--- 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 42828be7780a..d62b4c63da7f 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 130/270] 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 cb43cd6e0b44..6931e3388a18 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -107,6 +107,8 @@ def history_ccys(self):
         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 @@ def format_fiat(self, value):
         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 131/270] 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 81091ac12db3..9a3c3346e5d6 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -271,6 +271,8 @@ def clear_history(self):
                 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 132/270] 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 9a3c3346e5d6..b1bad2ace9df 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -735,15 +735,11 @@ def get_conflicting_transactions(self, tx):
                 # 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 133/270] 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 b1bad2ace9df..3d59e293f7d4 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -720,8 +720,7 @@ def find_pay_to_pubkey_address(self, prevout_hash, prevout_n):
     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 134/270] 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 04d27f6d1407..142d98280416 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -64,7 +64,9 @@ def refresh_headers(self):
             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 135/270] 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 142d98280416..fcd4deb47cb7 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -58,15 +58,15 @@ def __init__(self, parent=None):
         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 5dbda84a6e65..c0bdf62e8300 100644
--- a/gui/qt/util.py
+++ b/gui/qt/util.py
@@ -389,7 +389,9 @@ def __init__(self, parent, create_menu, headers, stretch_column=None,
         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 136/270] 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 3d59e293f7d4..46b7de51898f 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -278,12 +278,10 @@ def clear_history(self):
     @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 @@ def get_address_history(self, addr):
                     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 @@ def find_pay_to_pubkey_address(self, prevout_hash, prevout_n):
                     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 @@ def get_conflicting_transactions(self, tx):
             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 @@ def add_transaction(self, tx_hash, tx):
             #     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 @@ def add_transaction(self, tx_hash, tx):
                     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 @@ def add_transaction(self, tx_hash, tx):
                             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 @@ def add_transaction(self, tx_hash, tx):
                     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 @@ def receive_tx_callback(self, tx_hash, tx, tx_height):
 
     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 @@ def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx
                 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 137/270] 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 46b7de51898f..7f9ec812371a 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -278,8 +278,8 @@ def clear_history(self):
     @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 @@ 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')
+        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 @@ def remove_transaction(self, tx_hash):
         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 138/270] 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 7f9ec812371a..921a1bb844e8 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -758,6 +758,7 @@ def add_transaction(self, tx_hash, tx):
             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 @@ def receive_history_callback(self, addr, hist, tx_fees):
             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 139/270] 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 ffc5bfd8e901..c4ca7a1518ad 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 @@ def sufficient_funds(buckets):
         tx.add_inputs([coin for b in buckets for coin in b.coins])
         tx_weight = get_tx_weight(buckets)
 
+        # change is sent back to sending address unless specified
+        if not change_addrs:
+            change_addrs = [tx.inputs()[0]['address']]
+            # note: this is not necessarily the final "first input address"
+            # because the inputs had not been sorted at this point
+            assert is_address(change_addrs[0])
+
         # This takes a count of change outputs and returns a tx fee
         output_weight = 4 * Transaction.estimated_output_size(change_addrs[0])
         fee = lambda count: fee_estimator_w(tx_weight + count * output_weight)
diff --git a/lib/wallet.py b/lib/wallet.py
index 3d59e293f7d4..7d7488e21e08 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1123,7 +1123,8 @@ def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None,
                 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 140/270] 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 fcd4deb47cb7..cfe412db9443 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.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 @@ def __init__(self, parent=None):
         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 @@ def get_domain(self):
         '''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 @@ def on_update(self):
             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 @@ def create_menu(self, position):
         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 @@ def create_menu(self, position):
     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 @@ def onFileAdded(self, fn):
                 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 df86e3e893b8..ad0dd77b83bb 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -52,10 +52,6 @@
 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 @@ def init_menubar(self):
         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 @@ def create_history_tab(self):
         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 @@ def do_export_labels(self):
         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 d6c71a8a7879..59ddbac90212 100644
--- a/lib/commands.py
+++ b/lib/commands.py
@@ -453,7 +453,7 @@ def history(self, year=None, show_addresses=False, show_fiat=False):
             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 06f8edd75697..82a83fe65ff6 100644
--- a/lib/plot.py
+++ b/lib/plot.py
@@ -14,17 +14,16 @@
 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 60723810ac0c..fd9bd0593a72 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 921a1bb844e8..ce14706f15a4 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -948,13 +948,14 @@ def balance_at_timestamp(self, domain, target_timestamp):
         # 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 @@ def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx
                 '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 @@ def export_history(self, domain=None, from_timestamp=None, to_timestamp=None, fx
                 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 @@ def price_at_timestamp(self, txid, price_func):
         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 @@ def capital_gain(self, txid, price_func, ccy):
             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 141/270] 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 cfe412db9443..49a01f9b4c50 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -163,10 +163,14 @@ def show_summary(self):
         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 @@ def on_update(self):
         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 ce14706f15a4..90e47c43643d 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -952,6 +952,9 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None,
         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 142/270] 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        | 42 +++++++++++++++++++++++++++++-------------
 lib/keystore.py       |  5 ++++-
 lib/wallet.py         | 36 +++++++++++++++++++-----------------
 5 files changed, 54 insertions(+), 35 deletions(-)

diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
index ad0dd77b83bb..231d189150cb 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -2094,8 +2094,6 @@ def show_private_key(self, address, password):
             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 @@ def export_privkeys_dialog(self, password):
                               _('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 c0bdf62e8300..7f0cb238f892 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 84339755986f..65d50bbf0624 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,25 +520,42 @@ def DecodeBase58Check(psz):
 }
 
 
-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 = 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]
-        assert len(vch) in [33, 34]
-        compressed = len(vch) == 34
-        return txin_type, vch[1:33], compressed
     else:
-        raise BaseException("cannot deserialize", key)
+        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
diff --git a/lib/keystore.py b/lib/keystore.py
index e3579958a285..011602ec4c93 100644
--- a/lib/keystore.py
+++ b/lib/keystore.py
@@ -139,7 +139,10 @@ def check_password(self, password):
     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 90e47c43643d..ec7d720b62c6 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -388,23 +388,21 @@ def is_change(self, address):
     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 @@ def import_private_key(self, sec, pw, redeem_script=None):
         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 @@ def load_keystore(self):
     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 @@ def __init__(self, storage):
     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 @@ def pubkeys_to_address(self, pubkeys):
     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 143/270] 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 dbae5005da4b..03f1d00f0a4f 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 @@ def test_is_valid_address(self):
     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 @@ def test_serialize_privkey(self):
         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 144/270] fix #3929

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

diff --git a/lib/wallet.py b/lib/wallet.py
index 90e47c43643d..4d513ea26d5e 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1720,7 +1720,8 @@ def coin_price(self, coin, price_func, ccy, txin_value):
             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 145/270] 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 6931e3388a18..51bb740229d4 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -68,40 +68,41 @@ def read_historical_rates(self, ccy, cache_dir):
             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 146/270] update release notes

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

diff --git a/RELEASE-NOTES b/RELEASE-NOTES
index ddadee13d6b4..0ed9b70f2166 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 147/270] 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 a15bbe25a540..091da186486c 100644
--- a/gui/qt/exception_window.py
+++ b/gui/qt/exception_window.py
@@ -107,14 +107,22 @@ def __init__(self, main_window, exctype, value, tb):
     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 148/270] 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 4d513ea26d5e..1d99f57fa891 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -733,7 +733,8 @@ def get_txout_address(self, txo):
     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 @@ def get_conflicting_transactions(self, tx):
                 # 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 @@ def add_transaction(self, tx_hash, tx):
                     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 @@ def add_transaction(self, tx_hash, tx):
                             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 149/270] 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 51bb740229d4..589ef6edc9a3 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -109,8 +109,8 @@ def history_ccys(self):
 
     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 @@ def on_history(self):
     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 @@ def historical_value_str(self, satoshis, d_t):
 
     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 1d99f57fa891..b14f583179eb 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -958,6 +958,7 @@ def balance_at_timestamp(self, domain, target_timestamp):
         # 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 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None,
                 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 @@ 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
+        return lp - ap
 
     def capital_gain(self, txid, price_func, ccy):
         """
@@ -1704,13 +1702,11 @@ def capital_gain(self, txid, price_func, ccy):
         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 @@ def coin_price(self, coin, price_func, ccy, txin_value):
                 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 150/270] 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 a8f743588f98..f0c346a4ea3f 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 151/270] 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 3dc5953b537b..ac5962ed1315 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 d62b4c63da7f..adbff4c06f3f 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 152/270] 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 ac5962ed1315..5292cbb7d22f 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 153/270] 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                    | 83 ++++++++++++++++---------
 lib/util.py                             |  1 -
 lib/wallet.py                           |  4 +-
 9 files changed, 90 insertions(+), 64 deletions(-)

diff --git a/gui/kivy/uix/dialogs/bump_fee_dialog.py b/gui/kivy/uix/dialogs/bump_fee_dialog.py
index 1a6dc6228ef9..e27c9e5439a4 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.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 @@
                 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 @@ def __init__(self, app, fee, size, callback):
         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 1c61c6a21f83..cf29f36b8d6f 100644
--- a/gui/kivy/uix/dialogs/fee_dialog.py
+++ b/gui/kivy/uix/dialogs/fee_dialog.py
@@ -3,7 +3,6 @@
 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 e73f33650466..dad215e8703b 100644
--- a/gui/kivy/uix/dialogs/settings.py
+++ b/gui/kivy/uix/dialogs/settings.py
@@ -8,7 +8,6 @@
 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 ad0dd77b83bb..36d087fe94f0 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -1512,7 +1512,7 @@ def do_send(self, preview = False):
             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 5097af28a047..b417b6b86fd8 100644
--- a/gui/qt/transaction_dialog.py
+++ b/gui/qt/transaction_dialog.py
@@ -33,6 +33,7 @@
 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 @@ def update(self):
         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 8b9f7967239b..b84f6a2c6f99 100644
--- a/lib/bitcoin.py
+++ b/lib/bitcoin.py
@@ -108,10 +108,6 @@ def set_testnet(cls):
 
 ################################## 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 072edccd0e05..bf57d5b53d98 100644
--- a/lib/simple_config.py
+++ b/lib/simple_config.py
@@ -5,14 +5,22 @@
 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 @@ def save_last_wallet(self, wallet):
             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 @@ def eta_to_fee(self, i):
             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 @@ def fee_to_depth(self, target_fee):
             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 @@ def depth_target(self, i):
         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 @@ def depth_tooltip(self, depth):
         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 @@ def get_fee_status(self):
         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 @@ def get_fee_text(self, pos, dyn, mempool, fee_rate):
             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 @@ def get_depth_level(self):
         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 @@ def get_fee_slider(self, dyn, mempool):
                 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 @@ def get_fee_slider(self, dyn, mempool):
             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 @@ def has_fee_etas(self):
     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 @@ def fee_per_kb(self):
             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 fd9bd0593a72..4368d12b66a2 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 b14f583179eb..b43d540c631d 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -78,9 +78,9 @@
 
 
 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 154/270] 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 49a01f9b4c50..92e6fc0c864a 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 @@ def remove_local_tx(self, delete_tx):
         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 ad0dd77b83bb..cd2d5f009de1 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -51,7 +51,7 @@
 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 @@ def on_rate(dyn, pos, fee_rate):
         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 5097af28a047..49ce31dc3235 100644
--- a/gui/qt/transaction_dialog.py
+++ b/gui/qt/transaction_dialog.py
@@ -35,7 +35,7 @@
 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 @@ def sign_done(success):
         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 b14f583179eb..49c0f972711d 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 @@ def add_transaction(self, tx_hash, tx):
             # 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 155/270] 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 92e6fc0c864a..b2029d370d61 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -203,8 +203,8 @@ def on_update(self):
         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 589ef6edc9a3..75979050b1ec 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -108,8 +108,6 @@ def history_ccys(self):
         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 @@ def format_fiat(self, value):
         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 fd9bd0593a72..9dc95b9129a8 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 df7b119ca2f4..c652902dfe57 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -969,11 +969,6 @@ def balance_at_timestamp(self, domain, target_timestamp):
     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 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None,
                 '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 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None,
                     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 156/270] 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 ccf82bec664f..6b4a1f9bcf37 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -2288,25 +2288,17 @@ def read_tx_from_file(self):
         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 49ce31dc3235..20f78a06275a 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.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 @@ def __init__(self, tx, parent, desc, prompt_if_unsaved):
         # 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 157/270] 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 a2f7b50f80c8..e5a656049459 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 bb48dddf8445..caef2519f5f7 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 158/270] compute capital gains using wallet.txi and txo

---
 lib/wallet.py | 61 ++++++++++++++++++++++++++++-----------------------
 1 file changed, 33 insertions(+), 28 deletions(-)

diff --git a/lib/wallet.py b/lib/wallet.py
index c652902dfe57..20a124909dc6 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -503,6 +503,17 @@ def get_tx_delta(self, tx_hash, address):
             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 @@ def unrealized_gains(self, domain, price_func, ccy):
         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 @@ def capital_gain(self, txid, price_func, ccy):
         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 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 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, 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 159/270] Test Windows build using Travis

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

diff --git a/.travis.yml b/.travis.yml
index 38a0acf8f2b4..9b129e33d942 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 160/270] 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 e0be39082c8a..1eb005f98607 100644
--- a/gui/kivy/i18n.py
+++ b/gui/kivy/i18n.py
@@ -15,7 +15,7 @@ def __new__(cls, s, *args, **kwargs):
 
     @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 f499618a8d51..db5f0ed289f4 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 161/270] 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 1eb005f98607..733249d3ea38 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 db5f0ed289f4..f499618a8d51 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 162/270] fix #3949

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

diff --git a/lib/wallet.py b/lib/wallet.py
index 20a124909dc6..3bfd3c08c0e3 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -718,7 +718,9 @@ def get_balance(self, domain=None):
 
     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 @@ def get_conflicting_transactions(self, tx):
             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 163/270] 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 a1794459355f..81b6ca869326 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 @@ def on_edited(self, item, column, prior):
         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 @@ def create_menu(self, position):
         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 a4a8374f7ed2..d36c4866be68 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 @@ def on_update(self):
         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 6b4a1f9bcf37..89c0ce1d75f8 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -61,7 +61,7 @@
 
 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 @@ def init_menubar(self):
         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 @@ def do_export_privkeys(self, fileName, pklist, is_csv):
                 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 7f0cb238f892..60a594bb02e6 100644
--- a/gui/qt/util.py
+++ b/gui/qt/util.py
@@ -11,6 +11,7 @@
 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 @@ def dropEvent(self, event):
     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 5157adc41057..df10e08631bd 100644
--- a/lib/contacts.py
+++ b/lib/contacts.py
@@ -25,10 +25,11 @@
 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 @@ def save(self):
         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 @@ def find_regex(self, haystack, needle):
         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 c1e25441ac61..878c541e76ef 100644
--- a/lib/paymentrequest.py
+++ b/lib/paymentrequest.py
@@ -40,7 +40,7 @@
 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 @@ def load(self, d):
                 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 9dc95b9129a8..ee66ff3f334d 100644
--- a/lib/util.py
+++ b/lib/util.py
@@ -60,16 +60,18 @@ def __str__(self):
 
 
 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 run_with_except_hook(*args2, **kwargs2):
 
 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 164/270] 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 75979050b1ec..1c8c8a959557 100644
--- a/lib/exchange_rate.py
+++ b/lib/exchange_rate.py
@@ -508,9 +508,11 @@ def get_fiat_status_text(self, btc_balance, base_unit, decimal_point):
         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 @@ def history_rate(self, d_t):
         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 165/270] 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 81b6ca869326..27c9efb59586 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 d36c4866be68..586dd71c553a 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 89c0ce1d75f8..8ab63b87154a 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -39,15 +39,14 @@
 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 .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 @@ def do_export_privkeys(self, fileName, pklist, is_csv):
 
     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 60a594bb02e6..916762162004 100644
--- a/gui/qt/util.py
+++ b/gui/qt/util.py
@@ -6,12 +6,15 @@
 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 @@
 
 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 @@ def dropEvent(self, event):
     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 df10e08631bd..0015a8610f7b 100644
--- a/lib/contacts.py
+++ b/lib/contacts.py
@@ -25,7 +25,6 @@
 import json
 import traceback
 import sys
-import os
 
 from . import bitcoin
 from . import dnssec
@@ -52,14 +51,14 @@ def save(self):
         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 @@ def find_regex(self, haystack, needle):
         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 878c541e76ef..4711867053ac 100644
--- a/lib/paymentrequest.py
+++ b/lib/paymentrequest.py
@@ -468,31 +468,29 @@ def load(self, d):
                 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 ee66ff3f334d..7099141b394f 100644
--- a/lib/util.py
+++ b/lib/util.py
@@ -66,12 +66,14 @@ def __init__(self, message=''):
     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 run_with_except_hook(*args2, **kwargs2):
 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 166/270] fix #3952

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

diff --git a/lib/wallet.py b/lib/wallet.py
index 3bfd3c08c0e3..e12b401f56c8 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1027,7 +1027,7 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None,
                     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 167/270] fix 3954

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

diff --git a/lib/wallet.py b/lib/wallet.py
index e12b401f56c8..67e1493b84eb 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1039,7 +1039,8 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None,
             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 168/270] 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 2c8a55f2f630..faf8e5672a44 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 169/270] 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 8a69276f3d1f..d592e584bf23 100644
--- a/lib/blockchain.py
+++ b/lib/blockchain.py
@@ -181,7 +181,8 @@ def save_chunk(self, index, chunk):
         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 @@ def connect_chunk(self, idx, hexdata):
             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 bf7d4eb415c1..08a68489142a 100644
--- a/lib/network.py
+++ b/lib/network.py
@@ -777,6 +777,7 @@ def on_get_chunk(self, interface, response):
         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 @@ def on_get_chunk(self, interface, response):
         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 20e83fd2e2e5..89694080c959 100644
--- a/lib/verifier.py
+++ b/lib/verifier.py
@@ -36,15 +36,19 @@ def __init__(self, network, wallet):
         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 170/270] 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 d592e584bf23..4de3e076ec51 100644
--- a/lib/blockchain.py
+++ b/lib/blockchain.py
@@ -226,6 +226,9 @@ def write(self, data, offset, truncate=True):
                 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 08a68489142a..28071e41b62f 100644
--- a/lib/network.py
+++ b/lib/network.py
@@ -549,7 +549,7 @@ def process_response(self, interface, response, callbacks):
                 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 @@ def on_get_chunk(self, interface, response):
         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 89694080c959..8784b98c9125 100644
--- a/lib/verifier.py
+++ b/lib/verifier.py
@@ -74,10 +74,18 @@ def verify_merkle(self, r):
         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 171/270] 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 4de3e076ec51..d592e584bf23 100644
--- a/lib/blockchain.py
+++ b/lib/blockchain.py
@@ -226,9 +226,6 @@ def write(self, data, offset, truncate=True):
                 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 172/270] 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 67e1493b84eb..888e69dd8730 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1704,7 +1704,7 @@ def txin_value(self, txin):
 
     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 173/270] 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 28071e41b62f..92edda9585fa 100644
--- a/lib/network.py
+++ b/lib/network.py
@@ -991,7 +991,7 @@ def on_notify_header(self, interface, header):
         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 174/270] fix #3858

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

diff --git a/lib/verifier.py b/lib/verifier.py
index 8784b98c9125..c2d0f1250907 100644
--- a/lib/verifier.py
+++ b/lib/verifier.py
@@ -36,11 +36,14 @@ def __init__(self, network, wallet):
         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 @@ def run(self):
                 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 175/270] 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 000000000000..8dabcca2fc80
--- /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 176/270] 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 8dabcca2fc80..ef05ebc2ab76 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 177/270] 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 42220edab67c..c05b2d1abaa9 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 "$@"
 

From 5e7c1330d4d3d0bad0745cd897c05f9ffafd70ee Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Fri, 23 Feb 2018 19:00:29 +0100
Subject: [PATCH 178/270] qt history_list: (UX) handle extremes of "Summary"
 and "Plot" buttons

---
 gui/qt/history_list.py | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
index b2029d370d61..afc1dc12b1ea 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -158,15 +158,16 @@ def on_date(date):
 
     def show_summary(self):
         h = self.summary
-        format_amount = lambda x: self.parent.format_amount(x) + ' '+ self.parent.base_unit()
+        start_date = h.get('start_date')
+        end_date = h.get('end_date')
+        if start_date is None or end_date is None:
+            self.parent.show_message(_("Nothing to summarize."))
+            return
+        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()
-        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(start_date.isoformat(' ')), 0, 1)
         grid.addWidget(QLabel(_("End")), 1, 0)
@@ -190,10 +191,15 @@ def show_summary(self):
 
     def plot_history_dialog(self):
         if plot_history is None:
+            self.parent.show_message(
+                _("Can't plot history.") + '\n' +
+                _("Perhaps some dependencies are missing...") + " (matplotlib?)")
             return
         if len(self.transactions) > 0:
             plt = plot_history(self.transactions)
             plt.show()
+        else:
+            self.parent.show_message(_("Nothing to plot."))
 
     @profiler
     def on_update(self):

From 55a0a6b7f032c99846c713f058d8dd4f550fed63 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Sat, 24 Feb 2018 00:14:34 +0100
Subject: [PATCH 179/270] fix #3962

---
 gui/qt/history_list.py |  8 ++++----
 lib/plot.py            | 20 +++++++++++++++++---
 2 files changed, 21 insertions(+), 7 deletions(-)

diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
index afc1dc12b1ea..332b95c4c122 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -33,7 +33,7 @@
 from electrum.util import timestamp_to_datetime, profiler
 
 try:
-    from electrum.plot import plot_history
+    from electrum.plot import plot_history, NothingToPlotException
 except:
     plot_history = None
 
@@ -195,11 +195,11 @@ def plot_history_dialog(self):
                 _("Can't plot history.") + '\n' +
                 _("Perhaps some dependencies are missing...") + " (matplotlib?)")
             return
-        if len(self.transactions) > 0:
+        try:
             plt = plot_history(self.transactions)
             plt.show()
-        else:
-            self.parent.show_message(_("Nothing to plot."))
+        except NothingToPlotException as e:
+            self.parent.show_message(str(e))
 
     @profiler
     def on_update(self):
diff --git a/lib/plot.py b/lib/plot.py
index 82a83fe65ff6..5bd6add64846 100644
--- a/lib/plot.py
+++ b/lib/plot.py
@@ -14,7 +14,14 @@
 from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker
 
 
+class NothingToPlotException(Exception):
+    def __str__(self):
+        return _("Nothing to plot.")
+
+
 def plot_history(history):
+    if len(history) == 0:
+        raise NothingToPlotException()
     hist_in = defaultdict(int)
     hist_out = defaultdict(int)
     for item in history:
@@ -42,12 +49,19 @@ def plot_history(history):
     xfmt = md.DateFormatter('%Y-%m')
     ax.xaxis.set_major_formatter(xfmt)
     width = 20
-    dates, values = zip(*sorted(hist_in.items()))
-    r1 = axarr[0].bar(dates, values, width, label='incoming')
-    axarr[0].legend(loc='upper left')
+
+    r1 = None
+    r2 = None
+    dates_values = list(zip(*sorted(hist_in.items())))
+    if dates_values and len(dates_values) == 2:
+        dates, values = dates_values
+        r1 = axarr[0].bar(dates, values, width, label='incoming')
+        axarr[0].legend(loc='upper left')
     dates_values = list(zip(*sorted(hist_out.items())))
     if dates_values and len(dates_values) == 2:
         dates, values = dates_values
         r2 = axarr[1].bar(dates, values, width, color='r', label='outgoing')
         axarr[1].legend(loc='upper left')
+    if r1 is None and r2 is None:
+        raise NothingToPlotException()
     return plt

From f1b757821838ec4496e9e978cca70cee222f98e9 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Sat, 24 Feb 2018 00:30:40 +0100
Subject: [PATCH 180/270] fix #3960

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

diff --git a/lib/wallet.py b/lib/wallet.py
index 5d39165108ff..a32b4e2b1c44 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -463,10 +463,10 @@ def get_tx_height(self, tx_hash):
                 return height, conf, timestamp
             elif tx_hash in self.unverified_tx:
                 height = self.unverified_tx[tx_hash]
-                return height, 0, False
+                return height, 0, None
             else:
                 # local transaction
-                return TX_HEIGHT_LOCAL, 0, False
+                return TX_HEIGHT_LOCAL, 0, None
 
     def get_txpos(self, tx_hash):
         "return position, even if the tx is unverified"

From b5badc385f3abdb49e94d833d21879aa407cbcb7 Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Sat, 24 Feb 2018 20:49:56 +0100
Subject: [PATCH 181/270] fix #3965

---
 gui/qt/main_window.py | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
index 480d67fa5823..ddaafc4b76c5 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -904,11 +904,17 @@ def save_payment_request(self):
         i = self.expires_combo.currentIndex()
         expiration = list(map(lambda x: x[1], expiration_values))[i]
         req = self.wallet.make_payment_request(addr, amount, message, expiration)
-        self.wallet.add_payment_request(req, self.config)
-        self.sign_payment_request(addr)
-        self.request_list.update()
-        self.address_list.update()
-        self.save_request_button.setEnabled(False)
+        try:
+            self.wallet.add_payment_request(req, self.config)
+        except Exception as e:
+            traceback.print_exc(file=sys.stderr)
+            self.show_error(_('Error adding payment request') + ':\n' + str(e))
+        else:
+            self.sign_payment_request(addr)
+            self.save_request_button.setEnabled(False)
+        finally:
+            self.request_list.update()
+            self.address_list.update()
 
     def view_and_paste(self, title, msg, data):
         dialog = WindowModalDialog(self, title)

From 649b5a729852883f371f8d52cbd3dfee3614531a Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Sat, 24 Feb 2018 22:37:03 +0100
Subject: [PATCH 182/270] fix #3964

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

diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
index 332b95c4c122..c099155a9449 100644
--- a/gui/qt/history_list.py
+++ b/gui/qt/history_list.py
@@ -29,8 +29,7 @@
 from electrum.wallet import AddTransactionException, TX_HEIGHT_LOCAL
 from .util import *
 from electrum.i18n import _
-from electrum.util import block_explorer_URL
-from electrum.util import timestamp_to_datetime, profiler
+from electrum.util import block_explorer_URL, profiler
 
 try:
     from electrum.plot import plot_history, NothingToPlotException
@@ -234,7 +233,6 @@ def on_update(self):
             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 = tx_item['fiat_value'].value
                 value_str = fx.format_fiat(fiat_value)
                 entry.append(value_str)
diff --git a/lib/util.py b/lib/util.py
index 9a99c9d4e848..18473a459614 100644
--- a/lib/util.py
+++ b/lib/util.py
@@ -419,6 +419,8 @@ def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespa
     return result
 
 def timestamp_to_datetime(timestamp):
+    if timestamp is None:
+        return None
     return datetime.fromtimestamp(timestamp)
 
 def format_time(timestamp):
diff --git a/lib/wallet.py b/lib/wallet.py
index a32b4e2b1c44..f99e65fba293 100644
--- a/lib/wallet.py
+++ b/lib/wallet.py
@@ -1000,7 +1000,7 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None,
                 'value': Satoshis(value),
                 'balance': Satoshis(balance)
             }
-            item['date'] = timestamp_to_datetime(timestamp) if timestamp is not None else None
+            item['date'] = timestamp_to_datetime(timestamp)
             item['label'] = self.get_label(tx_hash)
             if show_addresses:
                 tx = self.transactions.get(tx_hash)

From 44bf80d291f015c08f5d55489306d531fc9a5f84 Mon Sep 17 00:00:00 2001
From: Roman Zeyde 
Date: Sun, 25 Feb 2018 19:14:49 +0200
Subject: [PATCH 183/270] remove unneeded 'pass' statement (#3970)

---
 plugins/keepkey/clientbase.py | 1 -
 plugins/trezor/clientbase.py  | 1 -
 2 files changed, 2 deletions(-)

diff --git a/plugins/keepkey/clientbase.py b/plugins/keepkey/clientbase.py
index 4d5cf7c85f25..6b33c9d43b8a 100644
--- a/plugins/keepkey/clientbase.py
+++ b/plugins/keepkey/clientbase.py
@@ -188,7 +188,6 @@ def clear_session(self):
         except BaseException as e:
             # If the device was removed it has the same effect...
             self.print_error("clear_session: ignoring error", str(e))
-            pass
 
     def get_public_node(self, address_n, creating):
         self.creating_wallet = creating
diff --git a/plugins/trezor/clientbase.py b/plugins/trezor/clientbase.py
index f890c59b0840..687d31c4ddda 100644
--- a/plugins/trezor/clientbase.py
+++ b/plugins/trezor/clientbase.py
@@ -188,7 +188,6 @@ def clear_session(self):
         except BaseException as e:
             # If the device was removed it has the same effect...
             self.print_error("clear_session: ignoring error", str(e))
-            pass
 
     def get_public_node(self, address_n, creating):
         self.creating_wallet = creating

From a6fe73ad7280a5a1a6833b677022ce22d08d267f Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Sun, 25 Feb 2018 18:54:34 +0100
Subject: [PATCH 184/270] wine build: "exit 1" on hash/sig failure

---
 contrib/build-wine/prepare-wine.sh | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh
index 158e72d306a1..2d9c7e83ca5b 100755
--- a/contrib/build-wine/prepare-wine.sh
+++ b/contrib/build-wine/prepare-wine.sh
@@ -28,18 +28,18 @@ verify_signature() {
         return 0
     else
         echo "$out" >&2
-        exit 0
+        exit 1
     fi
 }
 
 verify_hash() {
-    local file=$1 expected_hash=$2 out=
+    local file=$1 expected_hash=$2
     actual_hash=$(sha256sum $file | awk '{print $1}')
     if [ "$actual_hash" == "$expected_hash" ]; then
         return 0
     else
         echo "$file $actual_hash (unexpected hash)" >&2
-        exit 0
+        exit 1
     fi
 }
 
@@ -117,4 +117,4 @@ cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/python$PYTHON_VERSION/
 # 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"
+echo "Wine is configured."

From 8951dc3120f08b8cdd4eda3a68360f3525f837bb Mon Sep 17 00:00:00 2001
From: SomberNight 
Date: Mon, 26 Feb 2018 01:33:02 +0100
Subject: [PATCH 185/270] restore cmd: stop wallet threads

while "restore" does not wait for verifier to finish, this will at least ensure that whatever has been verified, gets persisted
---
 electrum | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/electrum b/electrum
index 0e109e8efee1..4a0470b2388e 100755
--- a/electrum
+++ b/electrum
@@ -161,6 +161,8 @@ def run_non_RPC(config):
             print_msg("Recovering wallet...")
             wallet.synchronize()
             wallet.wait_until_synchronized()
+            wallet.stop_threads()
+            # note: we don't wait for SPV
             msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
         else:
             msg = "This wallet was restored offline. It may contain more addresses than displayed."

From 59e457d22c7a873c5c2fc8d9014b22bc201c1e96 Mon Sep 17 00:00:00 2001
From: Jason Bruderer 
Date: Mon, 26 Feb 2018 04:17:55 -0700
Subject: [PATCH 186/270] Update build-wine's README.md (#3972)

* Update build-wine's README.md
---
 contrib/build-wine/README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/contrib/build-wine/README.md b/contrib/build-wine/README.md
index b63485e87f8e..6c2892828b81 100644
--- a/contrib/build-wine/README.md
+++ b/contrib/build-wine/README.md
@@ -2,7 +2,7 @@ Windows Binary Builds
 =====================
 
 These scripts can be used for cross-compilation of Windows Electrum executables from Linux/Wine.
-Produced binaries are deterministic so you should be able to generate binaries that match the official releases.
+Produced binaries are deterministic, so you should be able to generate binaries that match the official releases. 
 
 
 Usage:
@@ -12,6 +12,7 @@ Usage:
 
  - dirmngr
  - gpg
+ - 7Zip
  - Wine (>= v2)
 
 
@@ -19,8 +20,7 @@ For example:
 
 
 ```
-$ sudo apt-get install wine-development dirmngr gnupg2
-$ sudo ln -sf /usr/bin/wine-development /usr/local/bin/wine
+$ sudo apt-get install wine-development dirmngr gnupg2 p7zip-full
 $ wine --version
  wine-2.0 (Debian 2.0-3+b2)
 ```

From 3838fdbdcc9b2baf2951248203bb0edcc123bea7 Mon Sep 17 00:00:00 2001
From: Johann Bauer 
Date: Mon, 26 Feb 2018 12:20:15 +0100
Subject: [PATCH 187/270] Change name of version field in crashhub request

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

diff --git a/gui/qt/exception_window.py b/gui/qt/exception_window.py
index 091da186486c..900663f4a13b 100644
--- a/gui/qt/exception_window.py
+++ b/gui/qt/exception_window.py
@@ -43,7 +43,7 @@
 
 

Additional information

    -
  • Electrum version: {electrum_version}
  • +
  • Electrum version: {app_version}
  • Operating system: {os}
  • Wallet type: {wallet_type}
  • Locale: {locale}
  • @@ -154,7 +154,7 @@ def get_traceback_info(self): def get_additional_info(self): args = { - "electrum_version": ELECTRUM_VERSION, + "app_version": ELECTRUM_VERSION, "os": platform.platform(), "wallet_type": "unknown", "locale": locale.getdefaultlocale()[0], From 9fe24e32c082c75e632eccb29cb6f04763f79643 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 26 Feb 2018 18:04:44 +0100 Subject: [PATCH 188/270] test for NaN in Fiat.__str__ --- lib/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.py b/lib/util.py index 18473a459614..925233603744 100644 --- a/lib/util.py +++ b/lib/util.py @@ -103,7 +103,7 @@ def __repr__(self): return 'Fiat(%s)'% self.__str__() def __str__(self): - if self.value is None: + if self.value.is_nan(): return _('No Data') else: return "{:.2f}".format(self.value) + ' ' + self.ccy From aad0e276c0fd5250d9f504a2471abbc143fe9796 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 26 Feb 2018 18:13:39 +0100 Subject: [PATCH 189/270] wallet history: compute income and expenditures, remove redundant calculation of acquisition price --- lib/wallet.py | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index f99e65fba293..d5c7b1b674c4 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -985,7 +985,10 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, from .util import timestamp_to_datetime, Satoshis, Fiat out = [] capital_gains = 0 + income = 0 + expenditures = 0 fiat_income = 0 + fiat_expenditures = 0 h = self.get_history(domain) for tx_hash, height, conf, timestamp, value, balance in h: if from_timestamp and timestamp < from_timestamp: @@ -1017,25 +1020,32 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, output_addresses.append(addr) item['input_addresses'] = input_addresses item['output_addresses'] = output_addresses + # value may be None if wallet is not fully synchronized + if value is None: + continue + # fixme: use in and out values + if value < 0: + expenditures += -value + else: + income += value + # fiat computations if fx is not None: date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp) fiat_value = self.get_fiat_value(tx_hash, fx.ccy) - if fiat_value is None: - fiat_value = fx.historical_value(value, date) - fiat_default = True - else: - fiat_default = False + fiat_default = fiat_value is None + fiat_value = - fiat_value if fiat_value is not None else value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) item['fiat_value'] = Fiat(fiat_value, fx.ccy) item['fiat_default'] = fiat_default - 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) + if value < 0: + acquisition_price = - value / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy) + liquidation_price = - fiat_value + item['acquisition_price'] = Fiat(aquisition_price, fx.ccy) + cg = liquidation_price - aquisition_price item['capital_gain'] = Fiat(cg, fx.ccy) capital_gains += cg + fiat_expenditures += fiat_value else: - if fiat_value is not None: - fiat_income += fiat_value + fiat_income += fiat_value out.append(item) # add summary if out: @@ -1048,17 +1058,19 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, 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) + 'end_balance': Satoshis(end_balance), + 'income': Satoshis(income), + 'expenditures': Satoshis(expenditures) } 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['fiat_expenditures'] = Fiat(fiat_expenditures, 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) @@ -1714,18 +1726,6 @@ def unrealized_gains(self, domain, price_func, ccy): lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN) return lp - ap - 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 - """ - out_value = - self.get_tx_value(txid)/Decimal(COIN) - fiat_value = self.get_fiat_value(txid, 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, txid, price_func, ccy): """ Average acquisition price of the inputs of a transaction """ input_value = 0 From 22daffbbea52bf8424d970d71c60afcc730b8c63 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 26 Feb 2018 18:16:33 +0100 Subject: [PATCH 190/270] fix typo --- lib/wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index d5c7b1b674c4..22dec42909a8 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1039,8 +1039,8 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, if value < 0: acquisition_price = - value / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy) liquidation_price = - fiat_value - item['acquisition_price'] = Fiat(aquisition_price, fx.ccy) - cg = liquidation_price - aquisition_price + item['acquisition_price'] = Fiat(acquisition_price, fx.ccy) + cg = liquidation_price - acquisition_price item['capital_gain'] = Fiat(cg, fx.ccy) capital_gains += cg fiat_expenditures += fiat_value From a32d27b9d7708cdc7ba2da267be9d11f1d7c130e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 26 Feb 2018 18:21:36 +0100 Subject: [PATCH 191/270] make 2fa wallets work on testnet --- lib/tests/test_wallet_vertical.py | 2 +- plugins/trustedcoin/trustedcoin.py | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/tests/test_wallet_vertical.py b/lib/tests/test_wallet_vertical.py index 4a95cb119937..a29690ea9a23 100644 --- a/lib/tests/test_wallet_vertical.py +++ b/lib/tests/test_wallet_vertical.py @@ -128,7 +128,7 @@ def test_electrum_seed_2fa(self, mock_write): long_user_id, short_id = trustedcoin.get_user_id( {'x1/': {'xpub': xpub1}, 'x2/': {'xpub': xpub2}}) - xpub3 = trustedcoin.make_xpub(trustedcoin.signing_xpub, long_user_id) + xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(), long_user_id) ks3 = keystore.from_xpub(xpub3) self._check_xpub_keystore_sanity(ks3) self.assertTrue(isinstance(ks3, keystore.BIP32_KeyStore)) diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py index dc4eeb11b079..a6a008c45485 100644 --- a/plugins/trustedcoin/trustedcoin.py +++ b/plugins/trustedcoin/trustedcoin.py @@ -43,8 +43,17 @@ from electrum.storage import STO_EV_USER_PW # signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server -signing_xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" -billing_xpub = "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU" +def get_signing_xpub(): + if NetworkConstants.TESTNET: + return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY" + else: + return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" + +def get_billing_xpub(): + if NetworkConstants.TESTNET: + return "tpubD6NzVbkrYhZ4X11EJFTJujsYbUmVASAYY7gXsEt4sL97AMBdypiH1E9ZVTpdXXEy3Kj9Eqd1UkxdGtvDt5z23DKsh6211CfNJo8bLLyem5r" + else: + return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU" SEED_PREFIX = version.SEED_PREFIX_2FA @@ -307,7 +316,7 @@ def make_xpub(xpub, s): def make_billing_address(wallet, num): long_id, short_id = wallet.get_user_id() - xpub = make_xpub(billing_xpub, long_id) + xpub = make_xpub(get_billing_xpub(), long_id) version, _, _, _, c, cK = deserialize_xpub(xpub) cK, c = bitcoin.CKD_pub(cK, c, num) return bitcoin.public_key_to_p2pkh(cK) @@ -484,7 +493,7 @@ def on_restore_pw(self, wizard, seed, passphrase, password, encrypt_storage): storage.put('x1/', k1.dump()) storage.put('x2/', k2.dump()) long_user_id, short_id = get_user_id(storage) - xpub3 = make_xpub(signing_xpub, long_user_id) + xpub3 = make_xpub(get_signing_xpub(), long_user_id) k3 = keystore.from_xpub(xpub3) storage.put('x3/', k3.dump()) @@ -501,7 +510,7 @@ def create_remote_key(self, wizard): xpub2 = wizard.storage.get('x2/')['xpub'] # Generate third key deterministically. long_user_id, short_id = get_user_id(wizard.storage) - xpub3 = make_xpub(signing_xpub, long_user_id) + xpub3 = make_xpub(get_signing_xpub(), long_user_id) # secret must be sent by the server try: r = server.create(xpub1, xpub2, email) From 3daa8b018fdfb4d4e3a1b6795c20169dfb27b246 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 26 Feb 2018 19:45:20 +0100 Subject: [PATCH 192/270] prepare self.unverified_tx regardless of network --- lib/wallet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index 22dec42909a8..5c190f48946c 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1242,8 +1242,9 @@ def prepare_for_verifier(self): def start_threads(self, network): self.network = network + # prepare self.unverified_tx regardless of network + self.prepare_for_verifier() if self.network is not None: - self.prepare_for_verifier() self.verifier = SPV(self.network, self) self.synchronizer = Synchronizer(self, network) network.add_jobs([self.verifier, self.synchronizer]) From 5893af5025a66121f95f9492f70c16cdf00b565b Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Mon, 26 Feb 2018 19:53:16 +0100 Subject: [PATCH 193/270] Try to use git hash in crash reports --- gui/qt/exception_window.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/gui/qt/exception_window.py b/gui/qt/exception_window.py index 900663f4a13b..1713af3ca1ad 100644 --- a/gui/qt/exception_window.py +++ b/gui/qt/exception_window.py @@ -25,6 +25,9 @@ import locale import platform import traceback +import os +import sys +import subprocess import requests from PyQt5.QtCore import QObject @@ -33,7 +36,6 @@ from PyQt5.QtWidgets import * from electrum.i18n import _ -import sys from electrum import ELECTRUM_VERSION, bitcoin issue_template = """

    Traceback

    @@ -165,6 +167,11 @@ def get_additional_info(self): except: # Maybe the wallet isn't loaded yet pass + try: + args["app_version"] = self.get_git_version() + except: + # This is probably not running from source + pass return args def get_report_string(self): @@ -172,6 +179,11 @@ def get_report_string(self): info["traceback"] = "".join(traceback.format_exception(*self.exc_args)) return issue_template.format(**info) + @staticmethod + def get_git_version(): + dir = os.path.dirname(os.path.realpath(sys.argv[0])) + return subprocess.check_output(['git', 'describe', '--always'], cwd=dir) + def _show_window(*args): if not Exception_Window._active_window: From 98cdbe37714b23b2826b18276a789cf98ca98510 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 26 Feb 2018 22:03:22 +0100 Subject: [PATCH 194/270] fix #3956 --- gui/qt/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/qt/util.py b/gui/qt/util.py index 369c05e87373..1a4c5f266ce5 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -218,13 +218,14 @@ def __init__(self, parent, title=None): class WaitingDialog(WindowModalDialog): - '''Shows a please wait dialog whilst runnning a task. It is not + '''Shows a please wait dialog whilst running a task. It is not necessary to maintain a reference to this dialog.''' def __init__(self, parent, message, task, on_success=None, on_error=None): assert parent if isinstance(parent, MessageBoxMixin): parent = parent.top_level_window() WindowModalDialog.__init__(self, parent, _("Please wait")) + self.setAttribute(Qt.WA_DeleteOnClose) # see #3956 vbox = QVBoxLayout(self) vbox.addWidget(QLabel(message)) self.accepted.connect(self.on_accepted) From 52d41a4339c70a367e8555f4d9ad6eff22561290 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 27 Feb 2018 03:06:49 +0100 Subject: [PATCH 195/270] follow-up 5893af5025a66121f95f9492f70c16cdf00b565b clean git version a bit (convert to str and remove trailing newline) --- gui/qt/exception_window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/qt/exception_window.py b/gui/qt/exception_window.py index 1713af3ca1ad..743a98351266 100644 --- a/gui/qt/exception_window.py +++ b/gui/qt/exception_window.py @@ -182,7 +182,8 @@ def get_report_string(self): @staticmethod def get_git_version(): dir = os.path.dirname(os.path.realpath(sys.argv[0])) - return subprocess.check_output(['git', 'describe', '--always'], cwd=dir) + version = subprocess.check_output(['git', 'describe', '--always'], cwd=dir) + return str(version, "utf8").strip() def _show_window(*args): From 6687c1b3360c21a9508b2f3419b1bc84eac195c8 Mon Sep 17 00:00:00 2001 From: Yuki Inoue Date: Tue, 27 Feb 2018 18:43:00 +0900 Subject: [PATCH 196/270] daemon returns true only if the wallet is loaded --- lib/daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/daemon.py b/lib/daemon.py index bebcf4046f57..b3242215ee5c 100644 --- a/lib/daemon.py +++ b/lib/daemon.py @@ -174,7 +174,7 @@ def run_daemon(self, config_options): path = config.get_wallet_path() wallet = self.load_wallet(path, config.get('password')) self.cmd_runner.wallet = wallet - response = True + response = wallet is not None elif sub == 'close_wallet': path = config.get_wallet_path() if path in self.wallets: From 26e966c3e0284b3a93faf617ccca6ceb945f3a6d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 24 Feb 2018 17:34:08 +0100 Subject: [PATCH 197/270] hide address and history toolbars, move functions back into menu. --- gui/qt/address_list.py | 2 +- gui/qt/history_list.py | 87 ++++++++++++++++++------------------------ gui/qt/main_window.py | 25 +++++++----- gui/qt/util.py | 18 +++++++++ 4 files changed, 73 insertions(+), 59 deletions(-) diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index 8377e6bf0c65..b7261d9d7ef3 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -49,7 +49,7 @@ def __init__(self, parent=None): for t in [_('All'), _('Unused'), _('Funded'), _('Used')]: self.used_button.addItem(t) - def get_list_header(self): + def create_toolbar_buttons(self): return QLabel(_("Filter:")), self.change_button, self.used_button def refresh_headers(self): diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index c099155a9449..d4122510c7e2 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -82,11 +82,14 @@ def get_domain(self): def on_combo(self, x): s = self.period_combo.itemText(x) + x = s == _('Custom') + self.start_button.setEnabled(x) + self.end_button.setEnabled(x) if s == _('All'): self.start_timestamp = None self.end_timestamp = None - elif s == _('Custom'): - start_date = self.select_date() + self.start_button.setText("-") + self.end_button.setText("-") else: try: year = int(s) @@ -96,64 +99,46 @@ def on_combo(self, x): end_date = datetime.datetime(year+1, 1, 1) self.start_timestamp = time.mktime(start_date.timetuple()) self.end_timestamp = time.mktime(end_date.timetuple()) + self.start_button.setText(_('From') + ' ' + str(start_date)) + self.end_button.setText(_('To') + ' ' + str(end_date)) self.update() - def get_list_header(self): + def create_toolbar_buttons(self): self.period_combo = QComboBox() + self.start_button = QPushButton('-') + self.start_button.pressed.connect(self.select_start_date) + self.start_button.setEnabled(False) + self.end_button = QPushButton('-') + self.end_button.pressed.connect(self.select_end_date) + self.end_button.setEnabled(False) 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 + return self.period_combo, self.start_button, self.end_button - def select_date(self): - h = self.summary - d = WindowModalDialog(self, _("Custom dates")) + def select_start_date(self): + self.start_timestamp = self.select_date(self.start_button) + self.update() + + def select_end_date(self): + self.end_timestamp = self.select_date(self.end_button) + self.update() + + def select_date(self, button): + d = WindowModalDialog(self, _("Select date")) d.setMinimumSize(600, 150) - d.b = True - d.start_date = None - d.end_date = None + d.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()) + d.date = date 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() + button.setText(d.date.toString()) + return time.mktime(d.date.toPyDate().timetuple()) def show_summary(self): h = self.summary @@ -162,7 +147,7 @@ def show_summary(self): if start_date is None or end_date is None: self.parent.show_message(_("Nothing to summarize.")) return - format_amount = lambda x: self.parent.format_amount(x) + ' ' + self.parent.base_unit() + format_amount = lambda x: self.parent.format_amount(x.value) + ' ' + self.parent.base_unit() d = WindowModalDialog(self, _("Summary")) d.setMinimumSize(600, 150) vbox = QVBoxLayout() @@ -172,13 +157,17 @@ def show_summary(self): grid.addWidget(QLabel(_("End")), 1, 0) 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(format_amount(h['start_balance'])), 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(format_amount(h['end_balance'])), 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(_("Income")), 5, 0) + grid.addWidget(QLabel(format_amount(h.get('income'))), 5, 1) + grid.addWidget(QLabel(str(h.get('fiat_income'))), 5, 2) + grid.addWidget(QLabel(_("Expenditures")), 6, 0) + grid.addWidget(QLabel(format_amount(h.get('expenditures'))), 6, 1) + grid.addWidget(QLabel(str(h.get('fiat_expenditures'))), 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) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index ddaafc4b76c5..85c2eec06416 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -475,9 +475,16 @@ def init_menubar(self): self.import_address_menu = wallet_menu.addAction(_("Import addresses"), self.import_addresses) wallet_menu.addSeparator() + history_menu = wallet_menu.addMenu(_("&Addresses")) + history_menu.addAction(_("&Filter"), lambda: self.address_list.show_toolbar(True)) labels_menu = wallet_menu.addMenu(_("&Labels")) labels_menu.addAction(_("&Import"), self.do_import_labels) labels_menu.addAction(_("&Export"), self.do_export_labels) + history_menu = wallet_menu.addMenu(_("&History")) + history_menu.addAction(_("&Filter"), lambda: self.history_list.show_toolbar(True)) + history_menu.addAction(_("&Summary"), self.history_list.show_summary) + history_menu.addAction(_("&Plot"), self.history_list.plot_history_dialog) + history_menu.addAction(_("&Export"), self.history_list.export_history_dialog) contacts_menu = wallet_menu.addMenu(_("Contacts")) contacts_menu.addAction(_("&New"), self.new_contact_dialog) contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts()) @@ -747,7 +754,7 @@ def create_history_tab(self): from .history_list import HistoryList self.history_list = l = HistoryList(self) l.searchable_list = l - return self.create_list_tab(l, l.get_list_header()) + return self.create_list_tab(l, l.create_toolbar()) def show_address(self, addr): from . import address_dialog @@ -1720,26 +1727,22 @@ def set_frozen_state(self, addrs, freeze): self.utxo_list.update() self.update_fee() - def create_list_tab(self, l, list_header=None): + def create_list_tab(self, l, toolbar=None): w = QWidget() w.searchable_list = l vbox = QVBoxLayout() w.setLayout(vbox) vbox.setContentsMargins(0, 0, 0, 0) vbox.setSpacing(0) - if list_header: - hbox = QHBoxLayout() - for b in list_header: - hbox.addWidget(b) - hbox.addStretch() - vbox.addLayout(hbox) + if toolbar: + vbox.addLayout(toolbar) vbox.addWidget(l) return w def create_addresses_tab(self): from .address_list import AddressList self.address_list = l = AddressList(self) - return self.create_list_tab(l, l.get_list_header()) + return self.create_list_tab(l, l.create_toolbar()) def create_utxo_tab(self): from .utxo_list import UTXOList @@ -1967,6 +1970,10 @@ def change_password_dialog(self): self.update_lock_icon() def toggle_search(self): + tab = self.tabs.currentWidget() + #if hasattr(tab, 'searchable_list'): + # tab.searchable_list.toggle_toolbar() + #return self.search_box.setHidden(not self.search_box.isHidden()) if not self.search_box.isHidden(): self.search_box.setFocus(1) diff --git a/gui/qt/util.py b/gui/qt/util.py index 1a4c5f266ce5..2010b51b1297 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -516,6 +516,24 @@ def filter(self, p): item.setHidden(all([item.text(column).lower().find(p) == -1 for column in columns])) + def create_toolbar(self): + hbox = QHBoxLayout() + buttons = self.create_toolbar_buttons() + for b in buttons: + b.setVisible(False) + hbox.addWidget(b) + hide_button = QPushButton('x') + hide_button.setVisible(False) + hide_button.pressed.connect(lambda: self.show_toolbar(False)) + self.toolbar_buttons = buttons + (hide_button,) + hbox.addStretch() + hbox.addWidget(hide_button) + return hbox + + def show_toolbar(self, x): + for b in self.toolbar_buttons: + b.setVisible(x) + class ButtonsWidget(QWidget): From 76734f6a884990312abc938c86d03c2cbd64ea82 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 27 Feb 2018 12:02:52 +0100 Subject: [PATCH 198/270] fix #3980 --- gui/qt/main_window.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 85c2eec06416..5c77a7511494 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1502,7 +1502,10 @@ def do_send(self, preview = False): tx.set_rbf(True) if fee < self.wallet.relayfee() * tx.estimated_size() / 1000: - self.show_error(_("This transaction requires a higher fee, or it will not be propagated by the network")) + self.show_error('\n'.join([ + _("This transaction requires a higher fee, or it will not be propagated by your current server"), + _("Try to raise your transaction fee, or use a server with a lower relay fee.") + ]) return if preview: From ac176db221fe6f80a76c78fe90da9354dce82f99 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 27 Feb 2018 12:03:35 +0100 Subject: [PATCH 199/270] follow-up previous commit --- gui/qt/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 5c77a7511494..944ca4df96fc 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1505,7 +1505,7 @@ def do_send(self, preview = False): self.show_error('\n'.join([ _("This transaction requires a higher fee, or it will not be propagated by your current server"), _("Try to raise your transaction fee, or use a server with a lower relay fee.") - ]) + ])) return if preview: From c50d758c3e037a3d6c2fcabc72d9ac4e5b104a3d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 27 Feb 2018 12:16:50 +0100 Subject: [PATCH 200/270] rename "Local only" as "Local" --- lib/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index 5c190f48946c..0288aadfb187 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -69,7 +69,7 @@ _('Unconfirmed parent'), _('Unconfirmed'), _('Not Verified'), - _('Local only'), + _('Local'), ] TX_HEIGHT_LOCAL = -2 From 6ab1ba52b05e0cd454ac0a513fc3cd44653a31f8 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 27 Feb 2018 12:35:06 +0100 Subject: [PATCH 201/270] Remove pyblake2 workaround --- contrib/build-wine/build-electrum-git.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index f0c346a4ea3f..a46be1463b6c 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -57,11 +57,6 @@ 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 355bd322df29b2cecd64a5e37fe294190aaa4661 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 27 Feb 2018 12:39:54 +0100 Subject: [PATCH 202/270] Replace icon for local tx --- icons/offline_tx.png | Bin 2451 -> 463 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/icons/offline_tx.png b/icons/offline_tx.png index 55d2b2a471ed3f96214b239dba9c6e18f1437d17..32fee54dd35cef4af942b07535f9cbbf5d4d32e8 100644 GIT binary patch delta 398 zcmV;90dfA56VC%7iBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj#ne*sKM zL_t(&fn#781*2eO14eWK`@pULsbTQJ_1+j}(*iKj!xCBo7F-eu;i_aAxM1aNvK>PX z81gYN>Vbg51)0|O04^T0rZQ4b6>81=wFgCXw$oW&@m09PlR z+F;1Xz>xOo!B6|4s%51_p+8>pd8*-%!D+cFm+VM%SnmtfPFpm(8Kt+yO~3 z5E8h9L0ISmgRsyChLE7`)N%twfFutvGW|oa=`#O97?3o$tEF|DAvAa^WCH^OBg3lI z-guJ`8|Qj*-1F=|14GuHWpg$XtAV6EaP68ZL*umq(w#*-kYZ$Dn8LuoK(q%2V<{v< siej2oWYjEa@PWY?1EXLRi~05SngUjCx_W&i*H07*qoM6N<$g3F_yj{pDw delta 2402 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 UKnMOKWdHyG07*qoM6N<$f*(VjY5)KL From 31edc419fabe408f1eb980736ec52916a3b39373 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 27 Feb 2018 15:13:44 +0100 Subject: [PATCH 203/270] improve get_tx_status --- gui/kivy/uix/screens.py | 3 +-- gui/qt/history_list.py | 3 +-- lib/wallet.py | 44 +++++++++++++++++++++++------------------ 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 0133f789e027..3db82c070bf7 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -89,10 +89,9 @@ def show_menu(self, obj): # note: this list needs to be kept in sync with another in qt TX_ICONS = [ - "close", - "close", "unconfirmed", "close", + "unconfirmed", "close", "clock1", "clock2", diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index d4122510c7e2..005f020b3584 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -38,9 +38,8 @@ # note: this list needs to be kept in sync with another in kivy TX_ICONS = [ - "warning.png", - "warning.png", "unconfirmed.png", + "warning.png", "unconfirmed.png", "offline_tx.png", "clock1.png", diff --git a/lib/wallet.py b/lib/wallet.py index 0288aadfb187..eb5f04dd7723 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -65,9 +65,8 @@ from .contacts import Contacts TX_STATUS = [ - _('Replaceable'), - _('Unconfirmed parent'), _('Unconfirmed'), + _('Unconfirmed parent'), _('Not Verified'), _('Local'), ] @@ -588,10 +587,10 @@ def get_tx_info(self, tx): status = _('Unconfirmed') if fee is None: fee = self.tx_fees.get(tx_hash) - if fee and self.network.config.has_fee_etas(): + if fee and self.network.config.has_fee_mempool(): size = tx.estimated_size() - fee_per_kb = fee * 1000 / size - exp_n = self.network.config.fee_to_eta(fee_per_kb) + fee_per_byte = fee / size + exp_n = self.network.config.fee_to_depth(fee_per_byte) can_bump = is_mine and not tx.is_final() else: status = _('Local') @@ -1102,33 +1101,40 @@ def get_default_label(self, tx_hash): def get_tx_status(self, tx_hash, height, conf, timestamp): from .util import format_time - exp_n = False + extra = [] if conf == 0: tx = self.transactions.get(tx_hash) if not tx: return 2, 'unknown' is_final = tx and tx.is_final() - fee = self.tx_fees.get(tx_hash) - if fee and self.network and self.network.config.has_fee_mempool(): + if not is_final: + extra.append('rbf') + fee = self.get_wallet_delta(tx)[3] + if fee is None: + fee = self.tx_fees.get(tx_hash) + if fee is not None: size = tx.estimated_size() - fee_per_kb = fee * 1000 / size - exp_n = self.network.config.fee_to_depth(fee_per_kb//1000) + fee_per_byte = fee / size + extra.append('%.1f sat/b'%(fee_per_byte)) + if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \ + and self.network and self.network.config.has_fee_mempool(): + exp_n = self.network.config.fee_to_depth(fee_per_byte) + if exp_n: + extra.append('%.2f MB'%(exp_n/1000000)) if height == TX_HEIGHT_LOCAL: - status = 4 + status = 3 elif height == TX_HEIGHT_UNCONF_PARENT: status = 1 - elif height == TX_HEIGHT_UNCONFIRMED and not is_final: - status = 0 elif height == TX_HEIGHT_UNCONFIRMED: - status = 2 + status = 0 else: - status = 3 + status = 2 else: - status = 4 + min(conf, 6) + status = 3 + min(conf, 6) time_str = format_time(timestamp) if timestamp else _("unknown") - status_str = TX_STATUS[status] if status < 5 else time_str - if exp_n: - status_str += ' [%d sat/b, %.2f MB]'%(fee_per_kb//1000, exp_n/1000000) + status_str = TX_STATUS[status] if status < 4 else time_str + if extra: + status_str += ' [%s]'%(', '.join(extra)) return status, status_str def relayfee(self): From 51630416fc2e1b7bdea2e008f3f8f42d40fdd615 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 27 Feb 2018 15:26:39 +0100 Subject: [PATCH 204/270] fix expenditures sign --- lib/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index eb5f04dd7723..54e446f52644 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1042,7 +1042,7 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, cg = liquidation_price - acquisition_price item['capital_gain'] = Fiat(cg, fx.ccy) capital_gains += cg - fiat_expenditures += fiat_value + fiat_expenditures += -fiat_value else: fiat_income += fiat_value out.append(item) From a3554dc2b7838b130becd004385b003a0fc79a63 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 27 Feb 2018 16:00:56 +0100 Subject: [PATCH 205/270] fix #3963 --- gui/qt/history_list.py | 8 +++----- lib/exchange_rate.py | 2 ++ lib/wallet.py | 10 ++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 005f020b3584..c5364cf2a0ca 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -143,18 +143,16 @@ def show_summary(self): h = self.summary start_date = h.get('start_date') end_date = h.get('end_date') - if start_date is None or end_date is None: - self.parent.show_message(_("Nothing to summarize.")) - return format_amount = lambda x: self.parent.format_amount(x.value) + ' ' + self.parent.base_unit() + format_date = lambda x: x.isoformat(' ')[:-3] if x else _("None") d = WindowModalDialog(self, _("Summary")) d.setMinimumSize(600, 150) vbox = QVBoxLayout() grid = QGridLayout() grid.addWidget(QLabel(_("Start")), 0, 0) - grid.addWidget(QLabel(start_date.isoformat(' ')), 0, 1) + grid.addWidget(QLabel(format_date(start_date)), 0, 1) grid.addWidget(QLabel(_("End")), 1, 0) - grid.addWidget(QLabel(end_date.isoformat(' ')), 1, 1) + grid.addWidget(QLabel(format_date(end_date)), 1, 1) grid.addWidget(QLabel(_("Initial balance")), 2, 0) grid.addWidget(QLabel(format_amount(h['start_balance'])), 2, 1) grid.addWidget(QLabel(str(h.get('start_fiat_balance'))), 2, 2) diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index 1c8c8a959557..5c0193153a73 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -520,6 +520,8 @@ def format_fiat(self, value): 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/wallet.py b/lib/wallet.py index 54e446f52644..ed11694d78f7 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1055,8 +1055,8 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=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'] + start_date = None + end_date = None summary = { 'start_date': start_date, 'end_date': end_date, @@ -1071,10 +1071,8 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, summary['fiat_income'] = Fiat(fiat_income, fx.ccy) summary['fiat_expenditures'] = Fiat(fiat_expenditures, 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) + summary['start_fiat_balance'] = Fiat(fx.historical_value(start_balance, start_date), fx.ccy) + summary['end_fiat_balance'] = Fiat(fx.historical_value(end_balance, end_date), fx.ccy) else: summary = {} return { From 3ce8bd4d8f8a9b7fb7aea4de03e27ec727e2e48f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 27 Feb 2018 16:40:11 +0100 Subject: [PATCH 206/270] fix #3983 --- gui/qt/history_list.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index c5364cf2a0ca..9edbd61d47a0 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -136,6 +136,8 @@ def on_date(date): vbox.addLayout(Buttons(OkButton(d), CancelButton(d))) d.setLayout(vbox) if d.exec_(): + if d.date is None: + return None button.setText(d.date.toString()) return time.mktime(d.date.toPyDate().timetuple()) From a821a3504e9172fafa39550f9fedada53e41fe0c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 27 Feb 2018 16:52:28 +0100 Subject: [PATCH 207/270] wizard: remove unused strings --- gui/qt/installwizard.py | 12 ++---------- lib/base_wizard.py | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py index d23631aca1a2..a6e3351252f3 100644 --- a/gui/qt/installwizard.py +++ b/gui/qt/installwizard.py @@ -22,21 +22,13 @@ class GoBack(Exception): pass -MSG_GENERATING_WAIT = _("Electrum is generating your addresses, please wait...") -MSG_ENTER_ANYTHING = _("Please enter a seed phrase, a master key, a list of " - "Bitcoin addresses, or a list of private keys") -MSG_ENTER_SEED_OR_MPK = _("Please enter a seed phrase or a master key (xpub or xprv):") -MSG_COSIGNER = _("Please enter the master public key of cosigner #{}:") + MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\ + _("Leave this field empty if you want to disable encryption.") MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\ + _("Your wallet file does not contain secrets, mostly just metadata. ") \ + _("It also contains your master public key that allows watching your addresses.") + '\n\n'\ + _("Note: If you enable this setting, you will need your hardware device to open your wallet.") -MSG_RESTORE_PASSPHRASE = \ - _("Please enter your seed derivation passphrase. " - "Note: this is NOT your encryption password. " - "Leave this field empty if you did not use one or are unsure.") class CosignWidget(QWidget): @@ -478,7 +470,7 @@ def terminate(self): self.accept_signal.emit() def waiting_dialog(self, task, msg): - self.please_wait.setText(MSG_GENERATING_WAIT) + self.please_wait.setText(msg) self.refresh_gui() t = threading.Thread(target = task) t.start() diff --git a/lib/base_wizard.py b/lib/base_wizard.py index ab18b7617a60..d4222b7f072e 100644 --- a/lib/base_wizard.py +++ b/lib/base_wizard.py @@ -516,5 +516,5 @@ def task(): self.wallet.synchronize() self.wallet.storage.write() self.terminate() - msg = _("Electrum is generating your addresses, please wait.") + msg = _("Electrum is generating your addresses, please wait...") self.waiting_dialog(task, msg) From 540b230c8a19a488b24f4b0707098a5ca748bf3d Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 27 Feb 2018 16:54:35 +0100 Subject: [PATCH 208/270] Use icon for tx saving success message --- gui/qt/main_window.py | 2 +- gui/qt/util.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 944ca4df96fc..65906dabf78b 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -3151,7 +3151,7 @@ def save_transaction_into_wallet(self, tx): 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")) + self.msg_box(QPixmap(":icons/offline_tx.png"), None, _('Success'), _("Transaction saved successfully")) return True diff --git a/gui/qt/util.py b/gui/qt/util.py index 2010b51b1297..3312afb7f51a 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -202,7 +202,11 @@ def show_message(self, msg, parent=None, title=None): def msg_box(self, icon, parent, title, text, buttons=QMessageBox.Ok, defaultButton=QMessageBox.NoButton): parent = parent or self.top_level_window() - d = QMessageBox(icon, title, str(text), buttons, parent) + if type(icon) is QPixmap: + d = QMessageBox(QMessageBox.Information, title, str(text), buttons, parent) + d.setIconPixmap(icon) + else: + d = QMessageBox(icon, title, str(text), buttons, parent) d.setWindowModality(Qt.WindowModal) d.setDefaultButton(defaultButton) return d.exec_() From 51368f6aa88dab2b2c576d4fd4260972b26ae644 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 27 Feb 2018 17:38:04 +0100 Subject: [PATCH 209/270] fix 3930: do not filter addresses by default --- gui/qt/address_list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index b7261d9d7ef3..12ed5c46d071 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -42,7 +42,7 @@ def __init__(self, parent=None): self.show_used = 0 self.change_button = QComboBox(self) self.change_button.currentIndexChanged.connect(self.toggle_change) - for t in [_('Receiving'), _('Change'), _('All')]: + for t in [_('All'), _('Receiving'), _('Change')]: self.change_button.addItem(t) self.used_button = QComboBox(self) self.used_button.currentIndexChanged.connect(self.toggle_used) @@ -76,9 +76,9 @@ def on_update(self): self.wallet = self.parent.wallet item = self.currentItem() current_address = item.data(0, Qt.UserRole) if item else None - if self.show_change == 0: + if self.show_change == 1: addr_list = self.wallet.get_receiving_addresses() - elif self.show_change == 1: + elif self.show_change == 2: addr_list = self.wallet.get_change_addresses() else: addr_list = self.wallet.get_addresses() From 02c0096b55b150625509ce1dd4dc994f48a78a28 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 27 Feb 2018 19:21:30 +0100 Subject: [PATCH 210/270] qt addresses tab: new column for type --- gui/qt/address_list.py | 38 ++++++++++++++++++++++-------------- gui/qt/transaction_dialog.py | 2 +- gui/qt/util.py | 3 +++ gui/qt/utxo_list.py | 3 ++- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index 12ed5c46d071..83d5b9106cf0 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -24,18 +24,19 @@ # SOFTWARE. import webbrowser -from .util import * from electrum.i18n import _ from electrum.util import block_explorer_URL from electrum.plugins import run_hook from electrum.bitcoin import is_address +from .util import * + class AddressList(MyTreeWidget): - filter_columns = [0, 1, 2] # Address, Label, Balance + filter_columns = [0, 1, 2, 3] # Type, Address, Label, Balance def __init__(self, parent=None): - MyTreeWidget.__init__(self, parent, self.create_menu, [], 1) + MyTreeWidget.__init__(self, parent, self.create_menu, [], 2) self.refresh_headers() self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.show_change = 0 @@ -53,7 +54,7 @@ def create_toolbar_buttons(self): return QLabel(_("Filter:")), self.change_button, self.used_button def refresh_headers(self): - headers = [ _('Address'), _('Label'), _('Balance')] + headers = [_('Type'), _('Address'), _('Label'), _('Balance')] fx = self.parent.fx if fx and fx.get_fiat_address_config(): headers.extend([_(fx.get_currency()+' Balance')]) @@ -95,23 +96,30 @@ def on_update(self): continue if self.show_used == 3 and not is_used: continue - balance_text = self.parent.format_amount(balance) + balance_text = self.parent.format_amount(balance, whitespaces=True) fx = self.parent.fx if fx and fx.get_fiat_address_config(): rate = fx.exchange_rate() fiat_balance = fx.value_str(balance, rate) - address_item = QTreeWidgetItem([address, label, balance_text, fiat_balance, "%d"%num]) - address_item.setTextAlignment(3, Qt.AlignRight) + address_item = QTreeWidgetItem(['', address, label, balance_text, fiat_balance, "%d"%num]) + address_item.setTextAlignment(4, Qt.AlignRight) + address_item.setFont(4, QFont(MONOSPACE_FONT)) + else: + address_item = QTreeWidgetItem(['', address, label, balance_text, "%d"%num]) + address_item.setFont(3, QFont(MONOSPACE_FONT)) + if self.wallet.is_change(address): + address_item.setText(0, _('change')) + address_item.setBackground(0, ColorScheme.YELLOW.as_color(True)) else: - address_item = QTreeWidgetItem([address, label, balance_text, "%d"%num]) - address_item.setTextAlignment(2, Qt.AlignRight) - address_item.setFont(0, QFont(MONOSPACE_FONT)) - address_item.setData(0, Qt.UserRole, address) - address_item.setData(0, Qt.UserRole+1, True) # label can be edited + address_item.setText(0, _('receiving')) + address_item.setBackground(0, ColorScheme.GREEN.as_color(True)) + address_item.setFont(1, QFont(MONOSPACE_FONT)) + address_item.setData(1, Qt.UserRole, address) + address_item.setData(1, Qt.UserRole+1, True) # label can be edited if self.wallet.is_frozen(address): - address_item.setBackground(0, ColorScheme.BLUE.as_color(True)) + address_item.setBackground(1, ColorScheme.BLUE.as_color(True)) if self.wallet.is_beyond_limit(address): - address_item.setBackground(0, ColorScheme.RED.as_color(True)) + address_item.setBackground(1, ColorScheme.RED.as_color(True)) self.addChild(address_item) if address == current_address: self.setCurrentItem(address_item) @@ -122,7 +130,7 @@ def create_menu(self, position): can_delete = self.wallet.can_delete_address() selected = self.selectedItems() multi_select = len(selected) > 1 - addrs = [item.text(0) for item in selected] + addrs = [item.text(1) for item in selected] if not addrs: return if not multi_select: diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index d918f02aa44a..4fc589dd13bf 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -263,7 +263,7 @@ def add_io(self, vbox): rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True))) rec.setToolTip(_("Wallet receive address")) chg = QTextCharFormat() - chg.setBackground(QBrush(QColor("yellow"))) + chg.setBackground(QBrush(ColorScheme.YELLOW.as_color(background=True))) chg.setToolTip(_("Wallet change address")) def text_format(addr): diff --git a/gui/qt/util.py b/gui/qt/util.py index 3312afb7f51a..bef83575b33d 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -405,6 +405,8 @@ def __init__(self, parent, create_menu, headers, stretch_column=None, self.update_headers(headers) self.current_filter = "" + self.setRootIsDecorated(False) # remove left margin + def update_headers(self, headers): self.setColumnCount(len(headers)) self.setHeaderLabels(headers) @@ -656,6 +658,7 @@ class ColorScheme: dark_scheme = False GREEN = ColorSchemeItem("#117c11", "#8af296") + YELLOW = ColorSchemeItem("#ffff00", "#ffff00") RED = ColorSchemeItem("#7c1111", "#f18c8c") BLUE = ColorSchemeItem("#123b7c", "#8cb3f2") DEFAULT = ColorSchemeItem("black", "white") diff --git a/gui/qt/utxo_list.py b/gui/qt/utxo_list.py index 40f9fa208e22..7e849df84b8f 100644 --- a/gui/qt/utxo_list.py +++ b/gui/qt/utxo_list.py @@ -46,9 +46,10 @@ def on_update(self): height = x.get('height') name = self.get_name(x) label = self.wallet.get_label(x.get('prevout_hash')) - amount = self.parent.format_amount(x['value']) + amount = self.parent.format_amount(x['value'], whitespaces=True) utxo_item = QTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]]) utxo_item.setFont(0, QFont(MONOSPACE_FONT)) + utxo_item.setFont(2, QFont(MONOSPACE_FONT)) utxo_item.setFont(4, QFont(MONOSPACE_FONT)) utxo_item.setData(0, Qt.UserRole, name) if self.wallet.is_frozen(address): From 2aaf250a4a4094ea52c980e682d666e2ffd6947e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 28 Feb 2018 01:26:05 +0100 Subject: [PATCH 211/270] fix #3956 --- gui/qt/util.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gui/qt/util.py b/gui/qt/util.py index 3312afb7f51a..e730daed0ded 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -229,12 +229,12 @@ def __init__(self, parent, message, task, on_success=None, on_error=None): if isinstance(parent, MessageBoxMixin): parent = parent.top_level_window() WindowModalDialog.__init__(self, parent, _("Please wait")) - self.setAttribute(Qt.WA_DeleteOnClose) # see #3956 vbox = QVBoxLayout(self) vbox.addWidget(QLabel(message)) self.accepted.connect(self.on_accepted) self.show() self.thread = TaskThread(self) + self.thread.finished.connect(self.deleteLater) # see #3956 self.thread.add(task, on_success, self.accept, on_error) def wait(self): @@ -624,12 +624,12 @@ def run(self): except BaseException: self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error) - def on_done(self, result, cb_done, cb): + def on_done(self, result, cb_done, cb_result): # This runs in the parent's thread. if cb_done: cb_done() - if cb: - cb(result) + if cb_result: + cb_result(result) def stop(self): self.tasks.put(None) From 98f99a34a3d5fb06cd917e933983c9edf3692123 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Feb 2018 09:29:58 +0100 Subject: [PATCH 212/270] follow up #3985 --- gui/qt/address_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index 83d5b9106cf0..2851ebbdb3e5 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -85,7 +85,7 @@ def on_update(self): addr_list = self.wallet.get_addresses() self.clear() for address in addr_list: - num = len(self.wallet.history.get(address,[])) + num = len(self.wallet.get_address_history(address)) is_used = self.wallet.is_used(address) label = self.wallet.labels.get(address, '') c, u, x = self.wallet.get_addr_balance(address) From 4236adc5529157e641bfb9af8c0bf98d95642ec2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Feb 2018 09:50:28 +0100 Subject: [PATCH 213/270] fix date filtering --- gui/qt/history_list.py | 12 ++++++------ lib/wallet.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 9edbd61d47a0..c87e92c6d013 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -195,12 +195,12 @@ def on_update(self): 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: - 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) + if not self.years and self.transactions: + from datetime import date + start_date = self.transactions[0].get('date') or date.today() + end_date = self.transactions[-1].get('date') or date.today() + self.years = [str(i) for i in range(start_date.year, end_date.year + 1)] + self.period_combo.insertItems(1, self.years) item = self.currentItem() current_tx = item.data(0, Qt.UserRole) if item else None self.clear() diff --git a/lib/wallet.py b/lib/wallet.py index ed11694d78f7..83533cde7a16 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -990,9 +990,9 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, fiat_expenditures = 0 h = self.get_history(domain) for tx_hash, height, conf, timestamp, value, balance in h: - if from_timestamp and timestamp < from_timestamp: + if from_timestamp and (timestamp or time.time()) < from_timestamp: continue - if to_timestamp and timestamp >= to_timestamp: + if to_timestamp and (timestamp or time.time()) >= to_timestamp: continue item = { 'txid':tx_hash, @@ -1029,7 +1029,7 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, income += value # fiat computations if fx is not None: - date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp) + date = timestamp_to_datetime(timestamp) fiat_value = self.get_fiat_value(tx_hash, fx.ccy) fiat_default = fiat_value is None fiat_value = - fiat_value if fiat_value is not None else value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) From 2345d5f47321e5e1e4fe959e63f7ae63289f8640 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Feb 2018 09:56:34 +0100 Subject: [PATCH 214/270] disable list filtering when toolbar is closed --- gui/qt/address_list.py | 5 +++++ gui/qt/history_list.py | 5 +++++ gui/qt/util.py | 2 ++ 3 files changed, 12 insertions(+) diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index 2851ebbdb3e5..3bdeacfe776e 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -53,6 +53,11 @@ def __init__(self, parent=None): def create_toolbar_buttons(self): return QLabel(_("Filter:")), self.change_button, self.used_button + def on_hide_toolbar(self): + self.show_change = 0 + self.show_used = 0 + self.update() + def refresh_headers(self): headers = [_('Type'), _('Address'), _('Label'), _('Balance')] fx = self.parent.fx diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index c87e92c6d013..a5f7b1e63bc2 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -114,6 +114,11 @@ def create_toolbar_buttons(self): self.period_combo.activated.connect(self.on_combo) return self.period_combo, self.start_button, self.end_button + def on_hide_toolbar(self): + self.start_timestamp = None + self.end_timestamp = None + self.update() + def select_start_date(self): self.start_timestamp = self.select_date(self.start_button) self.update() diff --git a/gui/qt/util.py b/gui/qt/util.py index 095d794078e3..26d4448cee3f 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -539,6 +539,8 @@ def create_toolbar(self): def show_toolbar(self, x): for b in self.toolbar_buttons: b.setVisible(x) + if not x: + self.on_hide_toolbar() class ButtonsWidget(QWidget): From df9935e6af3a8c65b2d5b5f55f77e985d5c4c384 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Feb 2018 11:07:39 +0100 Subject: [PATCH 215/270] unify date format in history tab --- gui/qt/history_list.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index a5f7b1e63bc2..99ccc82d208c 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -63,6 +63,9 @@ def __init__(self, parent=None): self.end_timestamp = None self.years = [] + def format_date(self, d): + return str(datetime.date(d.year, d.month, d.day)) if d else _('None') + def refresh_headers(self): headers = ['', '', _('Date'), _('Description'), _('Amount'), _('Balance')] fx = self.parent.fx @@ -98,8 +101,8 @@ def on_combo(self, x): end_date = datetime.datetime(year+1, 1, 1) self.start_timestamp = time.mktime(start_date.timetuple()) self.end_timestamp = time.mktime(end_date.timetuple()) - self.start_button.setText(_('From') + ' ' + str(start_date)) - self.end_button.setText(_('To') + ' ' + str(end_date)) + self.start_button.setText(_('From') + ' ' + self.format_date(start_date)) + self.end_button.setText(_('To') + ' ' + self.format_date(end_date)) self.update() def create_toolbar_buttons(self): @@ -143,23 +146,23 @@ def on_date(date): if d.exec_(): if d.date is None: return None - button.setText(d.date.toString()) - return time.mktime(d.date.toPyDate().timetuple()) + date = d.date.toPyDate() + button.setText(self.format_date(date)) + return time.mktime(date.timetuple()) def show_summary(self): h = self.summary start_date = h.get('start_date') end_date = h.get('end_date') format_amount = lambda x: self.parent.format_amount(x.value) + ' ' + self.parent.base_unit() - format_date = lambda x: x.isoformat(' ')[:-3] if x else _("None") d = WindowModalDialog(self, _("Summary")) d.setMinimumSize(600, 150) vbox = QVBoxLayout() grid = QGridLayout() grid.addWidget(QLabel(_("Start")), 0, 0) - grid.addWidget(QLabel(format_date(start_date)), 0, 1) + grid.addWidget(QLabel(self.format_date(start_date)), 0, 1) grid.addWidget(QLabel(_("End")), 1, 0) - grid.addWidget(QLabel(format_date(end_date)), 1, 1) + grid.addWidget(QLabel(self.format_date(end_date)), 1, 1) grid.addWidget(QLabel(_("Initial balance")), 2, 0) grid.addWidget(QLabel(format_amount(h['start_balance'])), 2, 1) grid.addWidget(QLabel(str(h.get('start_fiat_balance'))), 2, 2) From 6706a87e53837c5c9918ae547f6fa8275d0a34b4 Mon Sep 17 00:00:00 2001 From: Lastrellik Date: Wed, 28 Feb 2018 08:51:56 -0700 Subject: [PATCH 216/270] Create file caching system --- contrib/build-wine/prepare-wine.sh | 43 ++++++++++++++++++------------ 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 2d9c7e83ca5b..3413f20c45b9 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -1,13 +1,16 @@ #!/bin/bash # 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_FILENAME=nsis-3.02.1-setup.exe +NSIS_URL=https://prdownloads.sourceforge.net/nsis/$NSIS_FILENAME?download NSIS_SHA256=736c9062a02e297e335f82252e648a883171c98e0d5120439f538c81d429552e -ZBAR_URL=https://sourceforge.net/projects/zbarw/files/zbarw-20121031-setup.exe/download +ZBAR_FILENAME=zbarw-20121031-setup.exe +ZBAR_URL=https://sourceforge.net/projects/zbarw/files/$ZBAR_FILENAME/download ZBAR_SHA256=177e32b272fa76528a3af486b74e9cb356707be1c5ace4ed3fcee9723e2c2c02 -LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.21/libusb-1.0.21.7z?download +LIBUSB_FILENAME=libusb-1.0.21.7z +LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.21/$LIBUSB_FILENAME?download LIBUSB_SHA256=acdde63a40b1477898aee6153f9d91d1a2e8a5d93f832ca8ab876498f3a6d2b8 PYTHON_VERSION=3.5.4 @@ -35,7 +38,7 @@ verify_signature() { verify_hash() { local file=$1 expected_hash=$2 actual_hash=$(sha256sum $file | awk '{print $1}') - if [ "$actual_hash" == "$expected_hash" ]; then + if [ "$actual_hash" = "$expected_hash" ]; then return 0 else echo "$file $actual_hash (unexpected hash)" >&2 @@ -43,6 +46,13 @@ verify_hash() { fi } +download_if_not_exist() { + local file_name=$1 url=$2 + if [ ! -e $file_name ] ; then + wget -O $PWD/$file_name "$url" + fi +} + # Let's begin! cd `dirname $0` set -e @@ -55,7 +65,6 @@ echo "done" wine 'wineboot' echo "Cleaning tmp" -rm -rf tmp mkdir -p tmp echo "done" @@ -70,8 +79,8 @@ KEYSERVER_PYTHON_DEV="hkp://keys.gnupg.net" gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --keyserver $KEYSERVER_PYTHON_DEV --recv-keys $KEYLIST_PYTHON_DEV for msifile in core dev exe lib pip tools; do echo "Installing $msifile..." - wget "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi" - wget "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc" + wget -nc "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi" + wget -nc "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc" verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV wine msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/python$PYTHON_VERSION done @@ -91,22 +100,22 @@ $PYTHON -m pip install -r ../../deterministic-build/requirements-binaries.txt $PYTHON -m pip install https://github.com/ecdsa/pyinstaller/archive/fix_2952.zip # Install ZBar -wget -q -O zbar.exe "$ZBAR_URL" -verify_hash zbar.exe $ZBAR_SHA256 -wine zbar.exe /S - +download_if_not_exist $ZBAR_FILENAME "$ZBAR_URL" +verify_hash $ZBAR_FILENAME "$ZBAR_SHA256" +wine "$PWD/$ZBAR_FILENAME" /S # Upgrade setuptools (so Electrum can be installed later) $PYTHON -m pip install setuptools --upgrade # Install NSIS installer -wget -q -O nsis.exe "$NSIS_URL" -verify_hash nsis.exe $NSIS_SHA256 -wine nsis.exe /S +download_if_not_exist $NSIS_FILENAME "$NSIS_URL" +verify_hash $NSIS_FILENAME "$NSIS_SHA256" +wine "$PWD/$NSIS_FILENAME" /S + +download_if_not_exist $LIBUSB_FILENAME "$LIBUSB_URL" +verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256" +7z x -olibusb $LIBUSB_FILENAME -aos -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 From 2f4ee16fd41834607bff5b92aa5cc6729f38e99c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 1 Mar 2018 02:39:48 +0100 Subject: [PATCH 217/270] fix email_requests plugin --- gui/qt/util.py | 13 +++++++++++ plugins/email_requests/qt.py | 43 ++++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/gui/qt/util.py b/gui/qt/util.py index 26d4448cee3f..e186d6092b9e 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -738,6 +738,19 @@ def export_meta_gui(electrum_window, title, exporter): .format(title, str(filename))) +def get_parent_main_window(widget): + """Returns a reference to the ElectrumWindow this widget belongs to.""" + from .main_window import ElectrumWindow + for _ in range(100): + if widget is None: + return None + if not isinstance(widget, ElectrumWindow): + widget = widget.parentWidget() + else: + return widget + return None + + if __name__ == "__main__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done")) diff --git a/plugins/email_requests/qt.py b/plugins/email_requests/qt.py index 64061841a614..a4facccf3209 100644 --- a/plugins/email_requests/qt.py +++ b/plugins/email_requests/qt.py @@ -27,6 +27,8 @@ import threading import base64 from functools import partial +import traceback +import sys import smtplib import imaplib @@ -37,14 +39,14 @@ from PyQt5.QtGui import * from PyQt5.QtCore import * -import PyQt5.QtGui as QtGui -from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QLineEdit) +from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QLineEdit, + QInputDialog) from electrum.plugins import BasePlugin, hook from electrum.paymentrequest import PaymentRequest from electrum.i18n import _ -from electrum_gui.qt.util import EnterButton, Buttons, CloseButton -from electrum_gui.qt.util import OkButton, WindowModalDialog +from electrum_gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton, + WindowModalDialog, get_parent_main_window) class Processor(threading.Thread): @@ -64,9 +66,9 @@ def poll(self): except: return typ, data = self.M.search(None, 'ALL') - for num in data[0].split(): + for num in str(data[0], 'utf8').split(): typ, msg_data = self.M.fetch(num, '(RFC822)') - msg = email.message_from_string(msg_data[0][1]) + msg = email.message_from_string(str(msg_data[0][1], 'utf8')) p = msg.get_payload() if not msg.is_multipart(): p = [p] @@ -127,19 +129,29 @@ def __init__(self, parent, config, name): self.processor.start() self.obj = QEmailSignalObject() self.obj.email_new_invoice_signal.connect(self.new_invoice) + self.wallets = set() def on_receive(self, pr_str): self.print_error('received payment request') self.pr = PaymentRequest(pr_str) self.obj.email_new_invoice_signal.emit() + @hook + def load_wallet(self, wallet, main_window): + self.wallets |= {wallet} + + @hook + def close_wallet(self, wallet): + self.wallets -= {wallet} + def new_invoice(self): - self.parent.invoices.add(self.pr) - #window.update_invoices_list() + for wallet in self.wallets: + wallet.invoices.add(self.pr) + #main_window.invoice_list.update() @hook def receive_list_menu(self, menu, addr): - window = menu.parentWidget() + window = get_parent_main_window(menu) menu.addAction(_("Send via e-mail"), lambda: self.send(window, addr)) def send(self, window, addr): @@ -152,20 +164,20 @@ def send(self, window, addr): pr = paymentrequest.make_request(self.config, r) if not pr: return - recipient, ok = QtGui.QInputDialog.getText(window, 'Send request', 'Email invoice to:') + recipient, ok = QInputDialog.getText(window, 'Send request', 'Email invoice to:') if not ok: return recipient = str(recipient) payload = pr.SerializeToString() self.print_error('sending mail to', recipient) try: + # FIXME this runs in the GUI thread and blocks it... self.processor.send(recipient, message, payload) except BaseException as e: + traceback.print_exc(file=sys.stderr) window.show_message(str(e)) - return - - window.show_message(_('Request sent.')) - + else: + window.show_message(_('Request sent.')) def requires_settings(self): return True @@ -204,9 +216,12 @@ def settings_dialog(self, window): server = str(server_e.text()) self.config.set_key('email_server', server) + self.imap_server = server username = str(username_e.text()) self.config.set_key('email_username', username) + self.username = username password = str(password_e.text()) self.config.set_key('email_password', password) + self.password = password From a0023791e5d116ff06b2ae250e858f34d77c5a7d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 1 Mar 2018 04:32:34 +0100 Subject: [PATCH 218/270] fix kivy -- virtual keyboard for seed input: chars were not getting enabled kivy master broke this in kivy/kivy#5537 --- gui/kivy/uix/dialogs/installwizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py index 651810907cdc..da76ef75b55d 100644 --- a/gui/kivy/uix/dialogs/installwizard.py +++ b/gui/kivy/uix/dialogs/installwizard.py @@ -613,7 +613,7 @@ def on_text(self, dt): for c in line.children: if isinstance(c, Button): if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': - c.disabled = (c.text.lower() not in p) and last_word + c.disabled = (c.text.lower() not in p) and bool(last_word) elif c.text == ' ': c.disabled = not enable_space From 324da972977a344f0cdbd4dc8ed023cb042c3016 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 1 Mar 2018 12:46:42 +0100 Subject: [PATCH 219/270] Provide command-line instructions to install SDKs in Kivy/buildozer --- gui/kivy/Readme.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/gui/kivy/Readme.md b/gui/kivy/Readme.md index faf8e5672a44..4398283f729f 100644 --- a/gui/kivy/Readme.md +++ b/gui/kivy/Readme.md @@ -35,18 +35,29 @@ sudo python3 setup.py install Extract into `/opt/crystax-ndk-10.3.2` ## 3. Update the Android SDK build tools -3.1 Start the Android SDK manager: + +### Method 1: Using the GUI + + Start the Android SDK manager in GUI mode: ~/.buildozer/android/platform/android-sdk-20/tools/android - -3.2 Check the latest SDK available and install it. -3.3 Close the SDK manager. + Check the latest SDK available and install it. + Close the SDK manager. + Reopen the SDK manager, scroll to the bottom and install the latest build tools (probably v27) + Install "Android Support Library Repository" from the SDK manager. + +### Method 2: Using the command line: + + Repeat the following command until there is nothing to install: + + ~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t tools,platform-tools + + Install Build Tools, android API 19 and Android Support Library: + + ~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t build-tools-27.0.3,android-19,extra-android-m2repository -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. ## 5. Create the UI Atlas In the `gui/kivy` directory of Electrum, run `make theming`. From 2303f7b54036363067dddcae514f67584b6fff50 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 1 Mar 2018 12:50:48 +0100 Subject: [PATCH 220/270] use python 3.6 to build APK --- gui/kivy/tools/buildozer.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/kivy/tools/buildozer.spec b/gui/kivy/tools/buildozer.spec index b3679889db6c..1e910314b65c 100644 --- a/gui/kivy/tools/buildozer.spec +++ b/gui/kivy/tools/buildozer.spec @@ -31,7 +31,7 @@ version.filename = %(source.dir)s/contrib/versions.py #version = 1.9.8 # (list) Application requirements -requirements = python3crystax, android, openssl, plyer, kivy==master +requirements = python3crystax==3.6, android, openssl, plyer, kivy==master # (str) Presplash of the application #presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png From 2bfcf9f0b52c0a6f8eab99119ff101a4dff5197c Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 1 Mar 2018 13:41:02 +0100 Subject: [PATCH 221/270] Trezor model T does not provide bootloader hash --- plugins/trezor/qt_generic.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py index 2440daee7672..808f83a6a4cd 100644 --- a/plugins/trezor/qt_generic.py +++ b/plugins/trezor/qt_generic.py @@ -321,8 +321,11 @@ def task(): def update(features): self.features = features set_label_enabled() - bl_hash = bh2u(features.bootloader_hash) - bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) + if features.bootloader_hash: + bl_hash = bh2u(features.bootloader_hash) + bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) + else: + bl_hash = "N/A" noyes = [_("No"), _("Yes")] endis = [_("Enable Passphrases"), _("Disable Passphrases")] disen = [_("Disabled"), _("Enabled")] From e82838ecbb7b2a8ca3b776859100e680595e7efd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 1 Mar 2018 14:45:38 +0100 Subject: [PATCH 222/270] fix #3991 --- gui/kivy/main_window.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 020388fadcd1..09db416f28a1 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -684,9 +684,6 @@ def on_pause(self): def on_resume(self): if self.nfcscanner: self.nfcscanner.nfc_enable() - # workaround p4a bug: - # show an empty info bubble, to refresh the display - self.show_info_bubble('', duration=0.1, pos=(0,0), width=1, arrow_pos=None) def on_size(self, instance, value): width, height = value From f6e627798a575a5d028c61c22f09f008bd93d3f3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 1 Mar 2018 15:44:44 +0100 Subject: [PATCH 223/270] fix #3993 --- gui/qt/address_list.py | 2 +- gui/qt/history_list.py | 3 +++ gui/qt/util.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index 3bdeacfe776e..b30701836673 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -50,7 +50,7 @@ def __init__(self, parent=None): for t in [_('All'), _('Unused'), _('Funded'), _('Used')]: self.used_button.addItem(t) - def create_toolbar_buttons(self): + def get_toolbar_buttons(self): return QLabel(_("Filter:")), self.change_button, self.used_button def on_hide_toolbar(self): diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 99ccc82d208c..f84492fe086b 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -62,6 +62,7 @@ def __init__(self, parent=None): self.start_timestamp = None self.end_timestamp = None self.years = [] + self.create_toolbar_buttons() def format_date(self, d): return str(datetime.date(d.year, d.month, d.day)) if d else _('None') @@ -115,6 +116,8 @@ def create_toolbar_buttons(self): self.end_button.setEnabled(False) self.period_combo.addItems([_('All'), _('Custom')]) self.period_combo.activated.connect(self.on_combo) + + def get_toolbar_buttons(self): return self.period_combo, self.start_button, self.end_button def on_hide_toolbar(self): diff --git a/gui/qt/util.py b/gui/qt/util.py index 26d4448cee3f..c3993b12c926 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -524,7 +524,7 @@ def filter(self, p): def create_toolbar(self): hbox = QHBoxLayout() - buttons = self.create_toolbar_buttons() + buttons = self.get_toolbar_buttons() for b in buttons: b.setVisible(False) hbox.addWidget(b) From b963dc1d0da247cfa40bfd2c78e1c39fbad72cd5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 1 Mar 2018 16:33:06 +0100 Subject: [PATCH 224/270] persist history and addresses toolbars (qt) --- gui/qt/address_list.py | 3 +++ gui/qt/history_list.py | 3 +++ gui/qt/main_window.py | 16 +++++++++++----- gui/qt/util.py | 22 +++++++++++++++++----- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index b30701836673..5e4c966f4b4e 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -58,6 +58,9 @@ def on_hide_toolbar(self): self.show_used = 0 self.update() + def save_toolbar_state(self, state, config): + config.set_key('show_toolbar_addresses', state) + def refresh_headers(self): headers = [_('Type'), _('Address'), _('Label'), _('Balance')] fx = self.parent.fx diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index f84492fe086b..7a43d068cb8b 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -125,6 +125,9 @@ def on_hide_toolbar(self): self.end_timestamp = None self.update() + def save_toolbar_state(self, state, config): + config.set_key('show_toolbar_history', state) + def select_start_date(self): self.start_timestamp = self.select_date(self.start_button) self.update() diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 65906dabf78b..435134285d93 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -475,13 +475,13 @@ def init_menubar(self): self.import_address_menu = wallet_menu.addAction(_("Import addresses"), self.import_addresses) wallet_menu.addSeparator() - history_menu = wallet_menu.addMenu(_("&Addresses")) - history_menu.addAction(_("&Filter"), lambda: self.address_list.show_toolbar(True)) + addresses_menu = wallet_menu.addMenu(_("&Addresses")) + addresses_menu.addAction(_("&Filter"), lambda: self.address_list.toggle_toolbar(self.config)) labels_menu = wallet_menu.addMenu(_("&Labels")) labels_menu.addAction(_("&Import"), self.do_import_labels) labels_menu.addAction(_("&Export"), self.do_export_labels) history_menu = wallet_menu.addMenu(_("&History")) - history_menu.addAction(_("&Filter"), lambda: self.history_list.show_toolbar(True)) + history_menu.addAction(_("&Filter"), lambda: self.history_list.toggle_toolbar(self.config)) history_menu.addAction(_("&Summary"), self.history_list.show_summary) history_menu.addAction(_("&Plot"), self.history_list.plot_history_dialog) history_menu.addAction(_("&Export"), self.history_list.export_history_dialog) @@ -754,7 +754,10 @@ def create_history_tab(self): from .history_list import HistoryList self.history_list = l = HistoryList(self) l.searchable_list = l - return self.create_list_tab(l, l.create_toolbar()) + toolbar = l.create_toolbar(self.config) + toolbar_shown = self.config.get('show_toolbar_history', False) + l.show_toolbar(toolbar_shown) + return self.create_list_tab(l, toolbar) def show_address(self, addr): from . import address_dialog @@ -1745,7 +1748,10 @@ def create_list_tab(self, l, toolbar=None): def create_addresses_tab(self): from .address_list import AddressList self.address_list = l = AddressList(self) - return self.create_list_tab(l, l.create_toolbar()) + toolbar = l.create_toolbar(self.config) + toolbar_shown = self.config.get('show_toolbar_addresses', False) + l.show_toolbar(toolbar_shown) + return self.create_list_tab(l, toolbar) def create_utxo_tab(self): from .utxo_list import UTXOList diff --git a/gui/qt/util.py b/gui/qt/util.py index c3993b12c926..b2999dd0f4fa 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -406,6 +406,7 @@ def __init__(self, parent, create_menu, headers, stretch_column=None, self.current_filter = "" self.setRootIsDecorated(False) # remove left margin + self.toolbar_shown = False def update_headers(self, headers): self.setColumnCount(len(headers)) @@ -522,7 +523,7 @@ def filter(self, p): item.setHidden(all([item.text(column).lower().find(p) == -1 for column in columns])) - def create_toolbar(self): + def create_toolbar(self, config=None): hbox = QHBoxLayout() buttons = self.get_toolbar_buttons() for b in buttons: @@ -530,18 +531,29 @@ def create_toolbar(self): hbox.addWidget(b) hide_button = QPushButton('x') hide_button.setVisible(False) - hide_button.pressed.connect(lambda: self.show_toolbar(False)) + hide_button.pressed.connect(lambda: self.show_toolbar(False, config)) self.toolbar_buttons = buttons + (hide_button,) hbox.addStretch() hbox.addWidget(hide_button) return hbox - def show_toolbar(self, x): + def save_toolbar_state(self, state, config): + pass # implemented in subclasses + + def show_toolbar(self, state, config=None): + if state == self.toolbar_shown: + return + self.toolbar_shown = state + if config: + self.save_toolbar_state(state, config) for b in self.toolbar_buttons: - b.setVisible(x) - if not x: + b.setVisible(state) + if not state: self.on_hide_toolbar() + def toggle_toolbar(self, config=None): + self.show_toolbar(not self.toolbar_shown, config) + class ButtonsWidget(QWidget): From fb761b7c126b87c5397bb4128e7a9629a84c1f48 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 1 Mar 2018 18:28:54 +0100 Subject: [PATCH 225/270] request fee historgam every minute. show fee in the send tab of kivy gui --- gui/kivy/main_window.py | 16 ++++++++++++++++ gui/kivy/uix/dialogs/settings.py | 15 --------------- gui/kivy/uix/ui_screens/send.kv | 18 ++++++++++++++++++ lib/simple_config.py | 8 ++------ 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 09db416f28a1..c1df7b4129c3 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -82,7 +82,9 @@ class ElectrumWindow(App): server_port = StringProperty('') num_chains = NumericProperty(0) blockchain_name = StringProperty('') + fee_status = StringProperty('Fee') blockchain_checkpoint = NumericProperty(0) + _fee_dialog = None auto_connect = BooleanProperty(False) def on_auto_connect(self, instance, x): @@ -271,6 +273,7 @@ def __init__(self, **kwargs): # cached dialogs self._settings_dialog = None self._password_dialog = None + self.fee_status = self.electrum_config.get_fee_status() def wallet_name(self): return os.path.basename(self.wallet.storage.path) if self.wallet else ' ' @@ -457,6 +460,7 @@ def on_start(self): if self.network: interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces'] self.network.register_callback(self.on_network_event, interests) + self.network.register_callback(self.on_fee, ['fee']) self.network.register_callback(self.on_quotes, ['on_quotes']) self.network.register_callback(self.on_history, ['on_history']) # URI passed in config @@ -828,6 +832,18 @@ def cb(amount): popup = AmountDialog(show_max, amount, cb) popup.open() + def fee_dialog(self, label, dt): + if self._fee_dialog is None: + from .uix.dialogs.fee_dialog import FeeDialog + def cb(): + c = self.electrum_config + self.fee_status = c.get_fee_status() + self._fee_dialog = FeeDialog(self, self.electrum_config, cb) + self._fee_dialog.open() + + def on_fee(self, event, *arg): + self.fee_status = self.electrum_config.get_fee_status() + def protected(self, msg, f, args): if self.wallet.has_password(): self.password_dialog(msg, f, args) diff --git a/gui/kivy/uix/dialogs/settings.py b/gui/kivy/uix/dialogs/settings.py index dad215e8703b..a3b3a5788437 100644 --- a/gui/kivy/uix/dialogs/settings.py +++ b/gui/kivy/uix/dialogs/settings.py @@ -48,12 +48,6 @@ description: _("Base unit for Bitcoin amounts.") action: partial(root.unit_dialog, self) CardSeparator - SettingsItem: - status: root.fee_status() - title: _('Fees') + ': ' + self.status - description: _("Fees paid to the Bitcoin miners.") - action: partial(root.fee_dialog, self) - CardSeparator SettingsItem: status: root.fx_status() title: _('Fiat Currency') + ': ' + self.status @@ -112,7 +106,6 @@ def __init__(self, app): layout.bind(minimum_height=layout.setter('height')) # cached dialogs self._fx_dialog = None - self._fee_dialog = None self._proxy_dialog = None self._language_dialog = None self._unit_dialog = None @@ -205,14 +198,6 @@ def callback(status): def fee_status(self): return self.config.get_fee_status() - def fee_dialog(self, label, dt): - if self._fee_dialog is None: - from .fee_dialog import FeeDialog - def cb(): - label.status = self.fee_status() - self._fee_dialog = FeeDialog(self.app, self.config, cb) - self._fee_dialog.open() - def boolean_dialog(self, name, title, message, dt): from .checkbox_dialog import CheckBoxDialog CheckBoxDialog(title, message, getattr(self.app, name), lambda x: setattr(self.app, name, x)).open() diff --git a/gui/kivy/uix/ui_screens/send.kv b/gui/kivy/uix/ui_screens/send.kv index 8aa70c0709cb..f2a361e37aa7 100644 --- a/gui/kivy/uix/ui_screens/send.kv +++ b/gui/kivy/uix/ui_screens/send.kv @@ -71,6 +71,24 @@ SendScreen: text: s.message if s.message else (_('No Description') if root.is_pr else _('Description')) disabled: root.is_pr on_release: Clock.schedule_once(lambda dt: app.description_dialog(s)) + CardSeparator: + opacity: int(not root.is_pr) + color: blue_bottom.foreground_color + BoxLayout: + size_hint: 1, None + height: blue_bottom.item_height + spacing: '5dp' + Image: + source: 'atlas://gui/kivy/theming/light/star_big_inactive' + opacity: 0.7 + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + BlueButton: + id: fee_e + default_text: _('Fee') + text: app.fee_status + on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) BoxLayout: size_hint: 1, None height: '48dp' diff --git a/lib/simple_config.py b/lib/simple_config.py index bf57d5b53d98..d4e29eec73a3 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -348,7 +348,7 @@ def get_fee_status(self): 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) - return target + return target + ' [%s]'%tooltip def get_fee_text(self, pos, dyn, mempool, fee_rate): """Returns (text, tooltip) where @@ -469,11 +469,7 @@ def is_fee_estimates_update_required(self): Returns True if an update should be requested. """ now = time.time() - prev_updates = self.fee_estimates_last_updated.values() - oldest_fee_time = min(prev_updates) if prev_updates else 0 - stale_fees = now - oldest_fee_time > 7200 - old_request = now - self.last_time_fee_estimates_requested > 60 - return stale_fees and old_request + return = now - self.last_time_fee_estimates_requested > 60 def requested_fee_estimates(self): self.last_time_fee_estimates_requested = time.time() From 37e43d9c102d54951240a47f87a4fd9ffa3c7df0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 1 Mar 2018 18:31:16 +0100 Subject: [PATCH 226/270] follow up prev commit --- lib/simple_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/simple_config.py b/lib/simple_config.py index d4e29eec73a3..aaaa519697d2 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -469,7 +469,7 @@ def is_fee_estimates_update_required(self): Returns True if an update should be requested. """ now = time.time() - return = now - self.last_time_fee_estimates_requested > 60 + return now - self.last_time_fee_estimates_requested > 60 def requested_fee_estimates(self): self.last_time_fee_estimates_requested = time.time() From 724ed874aa21cc454149d3d4a92cb7d094e8cbc9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 1 Mar 2018 19:05:51 +0100 Subject: [PATCH 227/270] toggle capital gains columns in qt history tab --- gui/qt/history_list.py | 5 +++-- gui/qt/main_window.py | 15 +++++++++++++++ lib/exchange_rate.py | 6 ++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 7a43d068cb8b..f8e0b07ed183 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -72,9 +72,10 @@ def refresh_headers(self): 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')]) self.editable_columns |= {6} + if fx.get_history_capital_gains_config(): + headers.extend(['%s '%fx.ccy + _('Acquisition price')]) + headers.extend(['%s '%fx.ccy + _('Capital Gains')]) else: self.editable_columns -= {6} self.update_headers(headers) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 435134285d93..f01b007948d1 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2846,6 +2846,7 @@ def on_outrounding(x): # Fiat Currency hist_checkbox = QCheckBox() + hist_capgains_checkbox = QCheckBox() fiat_address_checkbox = QCheckBox() ccy_combo = QComboBox() ex_combo = QComboBox() @@ -2867,6 +2868,11 @@ def update_fiat_address_cb(): if not self.fx: return fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config()) + def update_history_capgains_cb(): + if not self.fx: return + hist_capgains_checkbox.setChecked(self.fx.get_history_capital_gains_config()) + hist_capgains_checkbox.setEnabled(hist_checkbox.isChecked()) + def update_exchanges(): if not self.fx: return b = self.fx.is_enabled() @@ -2905,6 +2911,12 @@ def on_history(checked): if self.fx.is_enabled() and checked: # reset timeout to get historical rates self.fx.timeout = 0 + update_history_capgains_cb() + + def on_history_capgains(checked): + if not self.fx: return + self.fx.set_history_capital_gains_config(checked) + self.history_list.refresh_headers() def on_fiat_address(checked): if not self.fx: return @@ -2914,16 +2926,19 @@ def on_fiat_address(checked): update_currencies() update_history_cb() + update_history_capgains_cb() update_fiat_address_cb() update_exchanges() ccy_combo.currentIndexChanged.connect(on_currency) hist_checkbox.stateChanged.connect(on_history) + hist_capgains_checkbox.stateChanged.connect(on_history_capgains) fiat_address_checkbox.stateChanged.connect(on_fiat_address) ex_combo.currentIndexChanged.connect(on_exchange) fiat_widgets = [] fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo)) fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox)) + fiat_widgets.append((QLabel(_('Show capital gains in history')), hist_capgains_checkbox)) fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')), fiat_address_checkbox)) fiat_widgets.append((QLabel(_('Source')), ex_combo)) diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index 5c0193153a73..7b9c86cc89a4 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -451,6 +451,12 @@ def get_history_config(self): def set_history_config(self, b): self.config.set_key('history_rates', bool(b)) + def get_history_capital_gains_config(self): + return bool(self.config.get('history_rates_capital_gains', False)) + + def set_history_capital_gains_config(self, b): + self.config.set_key('history_rates_capital_gains', bool(b)) + def get_fiat_address_config(self): return bool(self.config.get('fiat_address')) From 968862076ff3952a25ff95ff0f87c7dea6ab311e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 1 Mar 2018 19:09:24 +0100 Subject: [PATCH 228/270] fix sign --- lib/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index 83533cde7a16..8ccea4b6fbfa 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1032,7 +1032,7 @@ def get_full_history(self, domain=None, from_timestamp=None, to_timestamp=None, date = timestamp_to_datetime(timestamp) fiat_value = self.get_fiat_value(tx_hash, fx.ccy) fiat_default = fiat_value is None - fiat_value = - fiat_value if fiat_value is not None else value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) + fiat_value = fiat_value if fiat_value is not None else value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate) item['fiat_value'] = Fiat(fiat_value, fx.ccy) item['fiat_default'] = fiat_default if value < 0: From 6079105b2cd73fe3efedc7bfefde5ad6aa6584ae Mon Sep 17 00:00:00 2001 From: Lastrellik Date: Thu, 1 Mar 2018 11:36:36 -0700 Subject: [PATCH 229/270] Revert to using == instead of = --- contrib/build-wine/prepare-wine.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 3413f20c45b9..1bd9af08e571 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -38,7 +38,7 @@ verify_signature() { verify_hash() { local file=$1 expected_hash=$2 actual_hash=$(sha256sum $file | awk '{print $1}') - if [ "$actual_hash" = "$expected_hash" ]; then + if [ "$actual_hash" == "$expected_hash" ]; then return 0 else echo "$file $actual_hash (unexpected hash)" >&2 From 2845280c4bf967f9b5ab2ef5287ca048ee2e653a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Mar 2018 01:31:36 +0100 Subject: [PATCH 230/270] qt MessageBox: make text selectable --- gui/qt/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/qt/util.py b/gui/qt/util.py index 1346cfc84cc4..813e814951b5 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -209,6 +209,7 @@ def msg_box(self, icon, parent, title, text, buttons=QMessageBox.Ok, d = QMessageBox(icon, title, str(text), buttons, parent) d.setWindowModality(Qt.WindowModal) d.setDefaultButton(defaultButton) + d.setTextInteractionFlags(Qt.TextSelectableByMouse) return d.exec_() class WindowModalDialog(QDialog, MessageBoxMixin): From 7acbaa53825cb1dedb7d248037d0153d89331a4c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Mar 2018 05:15:29 +0100 Subject: [PATCH 231/270] trezorT: fix/implement passphrases --- plugins/trezor/clientbase.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/trezor/clientbase.py b/plugins/trezor/clientbase.py index 687d31c4ddda..6e10d4c49d19 100644 --- a/plugins/trezor/clientbase.py +++ b/plugins/trezor/clientbase.py @@ -55,6 +55,9 @@ def callback_PinMatrixRequest(self, msg): return self.proto.PinMatrixAck(pin=pin) def callback_PassphraseRequest(self, req): + if req and hasattr(req, 'on_device') and req.on_device is True: + return self.proto.PassphraseAck() + if self.creating_wallet: msg = _("Enter a passphrase to generate this wallet. Each time " "you use this wallet your {} will prompt you for the " @@ -68,6 +71,9 @@ def callback_PassphraseRequest(self, req): passphrase = bip39_normalize_passphrase(passphrase) return self.proto.PassphraseAck(passphrase=passphrase) + def callback_PassphraseStateRequest(self, msg): + return self.proto.PassphraseStateAck() + def callback_WordRequest(self, msg): self.step += 1 msg = _("Step {}/24. Enter seed word as explained on " From 02f2e5c59998562622f88f92e0f9239665236617 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Mar 2018 07:56:17 +0100 Subject: [PATCH 232/270] wallet.add_transaction: adding to spent_outpoints, out of order case --- lib/wallet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 8ccea4b6fbfa..003542d00adc 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -830,16 +830,16 @@ def add_transaction(self, tx_hash, tx): prevout_hash = txi['prevout_hash'] prevout_n = txi['prevout_n'] ser = prevout_hash + ':%d'%prevout_n - # find value from prev output if addr and self.is_mine(addr): + # we only track is_mine spends + self.spent_outpoints[ser] = tx_hash + # find value from prev output dd = self.txo.get(prevout_hash, {}) for n, v, is_cb in dd.get(addr, []): if n == prevout_n: 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 4321950f631a607a782b21f875610ab9d18413ac Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 2 Mar 2018 09:46:10 +0100 Subject: [PATCH 233/270] kivy: update fee dialog on new fee --- gui/kivy/main_window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index c1df7b4129c3..ef1d34c36828 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -843,6 +843,8 @@ def cb(): def on_fee(self, event, *arg): self.fee_status = self.electrum_config.get_fee_status() + if self._fee_dialog: + self._fee_dialog.update_text() def protected(self, msg, f, args): if self.wallet.has_password(): From b2c7d2d2ccee4e156d785dbff3d8e211fae0e86b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 2 Mar 2018 10:31:00 +0100 Subject: [PATCH 234/270] kivy: do not cache fee dialog --- gui/kivy/main_window.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index ef1d34c36828..d431e220dc7a 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -84,7 +84,6 @@ class ElectrumWindow(App): blockchain_name = StringProperty('') fee_status = StringProperty('Fee') blockchain_checkpoint = NumericProperty(0) - _fee_dialog = None auto_connect = BooleanProperty(False) def on_auto_connect(self, instance, x): @@ -833,18 +832,14 @@ def cb(amount): popup.open() def fee_dialog(self, label, dt): - if self._fee_dialog is None: - from .uix.dialogs.fee_dialog import FeeDialog - def cb(): - c = self.electrum_config - self.fee_status = c.get_fee_status() - self._fee_dialog = FeeDialog(self, self.electrum_config, cb) - self._fee_dialog.open() + from .uix.dialogs.fee_dialog import FeeDialog + def cb(): + self.fee_status = self.electrum_config.get_fee_status() + fee_dialog = FeeDialog(self, self.electrum_config, cb) + fee_dialog.open() def on_fee(self, event, *arg): self.fee_status = self.electrum_config.get_fee_status() - if self._fee_dialog: - self._fee_dialog.update_text() def protected(self, msg, f, args): if self.wallet.has_password(): From 3965176295364f0af444525206afb49ef983ab36 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Sun, 28 Jan 2018 13:07:59 +0100 Subject: [PATCH 235/270] Enable sorting of list widgets --- gui/qt/address_list.py | 5 +++-- gui/qt/history_list.py | 6 +++++- gui/qt/util.py | 15 +++++++++++++++ gui/qt/utxo_list.py | 3 ++- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index 5e4c966f4b4e..b348dfcc198c 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -39,6 +39,7 @@ def __init__(self, parent=None): MyTreeWidget.__init__(self, parent, self.create_menu, [], 2) self.refresh_headers() self.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.setSortingEnabled(True) self.show_change = 0 self.show_used = 0 self.change_button = QComboBox(self) @@ -109,11 +110,11 @@ def on_update(self): if fx and fx.get_fiat_address_config(): rate = fx.exchange_rate() fiat_balance = fx.value_str(balance, rate) - address_item = QTreeWidgetItem(['', address, label, balance_text, fiat_balance, "%d"%num]) + address_item = SortableTreeWidgetItem(['', address, label, balance_text, fiat_balance, "%d"%num]) address_item.setTextAlignment(4, Qt.AlignRight) address_item.setFont(4, QFont(MONOSPACE_FONT)) else: - address_item = QTreeWidgetItem(['', address, label, balance_text, "%d"%num]) + address_item = SortableTreeWidgetItem(['', address, label, balance_text, "%d"%num]) address_item.setFont(3, QFont(MONOSPACE_FONT)) if self.wallet.is_change(address): address_item.setText(0, _('change')) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index f8e0b07ed183..3140af001a0b 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -59,6 +59,8 @@ def __init__(self, parent=None): AcceptFileDragDrop.__init__(self, ".txn") self.refresh_headers() self.setColumnHidden(1, True) + self.setSortingEnabled(True) + self.sortByColumn(0, Qt.AscendingOrder) self.start_timestamp = None self.end_timestamp = None self.years = [] @@ -243,9 +245,10 @@ def on_update(self): if value < 0: entry.append(fx.format_fiat(tx_item['acquisition_price'].value)) entry.append(fx.format_fiat(tx_item['capital_gain'].value)) - item = QTreeWidgetItem(entry) + item = SortableTreeWidgetItem(entry) item.setIcon(0, icon) item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else "")) + item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf)) if has_invoice: item.setIcon(3, QIcon(":icons/seal")) for i in range(len(entry)): @@ -301,6 +304,7 @@ def update_item(self, tx_hash, height, conf, timestamp): if items: item = items[0] item.setIcon(0, icon) + item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf)) item.setText(2, status_str) def create_menu(self, position): diff --git a/gui/qt/util.py b/gui/qt/util.py index 813e814951b5..306c64a556ad 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -763,6 +763,21 @@ def get_parent_main_window(widget): return widget return None +class SortableTreeWidgetItem(QTreeWidgetItem): + DataRole = Qt.UserRole + 1 + + def __lt__(self, other): + column = self.treeWidget().sortColumn() + if None not in [x.data(column, self.DataRole) for x in [self, other]]: + # We have set custom data to sort by + return self.data(column, self.DataRole) < other.data(column, self.DataRole) + try: + # Is the value something numeric? + return float(self.text(column)) < float(other.text(column)) + except ValueError: + # If not, we will just do string comparison + return self.text(column) < other.text(column) + if __name__ == "__main__": app = QApplication([]) diff --git a/gui/qt/utxo_list.py b/gui/qt/utxo_list.py index 7e849df84b8f..78e865360697 100644 --- a/gui/qt/utxo_list.py +++ b/gui/qt/utxo_list.py @@ -32,6 +32,7 @@ class UTXOList(MyTreeWidget): def __init__(self, parent=None): MyTreeWidget.__init__(self, parent, self.create_menu, [ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')], 1) self.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.setSortingEnabled(True) def get_name(self, x): return x.get('prevout_hash') + ":%d"%x.get('prevout_n') @@ -47,7 +48,7 @@ def on_update(self): name = self.get_name(x) label = self.wallet.get_label(x.get('prevout_hash')) amount = self.parent.format_amount(x['value'], whitespaces=True) - utxo_item = QTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]]) + utxo_item = SortableTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]]) utxo_item.setFont(0, QFont(MONOSPACE_FONT)) utxo_item.setFont(2, QFont(MONOSPACE_FONT)) utxo_item.setFont(4, QFont(MONOSPACE_FONT)) From 2a51914c31b8696b51198663f02e269517fb710f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 2 Mar 2018 17:33:41 +0100 Subject: [PATCH 236/270] improve message --- gui/qt/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index f01b007948d1..ababe1f16bdf 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -3172,7 +3172,7 @@ def save_transaction_into_wallet(self, tx): self.wallet.save_transactions(write=True) # need to update at least: history_list, utxo_list, address_list self.need_update.set() - self.msg_box(QPixmap(":icons/offline_tx.png"), None, _('Success'), _("Transaction saved successfully")) + self.msg_box(QPixmap(":icons/offline_tx.png"), None, _('Success'), _("Transaction added to wallet history")) return True From 4b7cf297f5cf9513b32fcce2c5773af2ab1c7492 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 2 Mar 2018 18:00:05 +0100 Subject: [PATCH 237/270] fix #3998 --- lib/wallet.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/wallet.py b/lib/wallet.py index 003542d00adc..35979be25b07 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -866,16 +866,19 @@ def add_transaction(self, tx_hash, tx): return True def remove_transaction(self, tx_hash): - def undo_spend(outpoint_to_txid_map): - for addr, l in self.txi[tx_hash].items(): - for ser, v in l: - 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, None) - undo_spend(self.pruned_txo) - undo_spend(self.spent_outpoints) + # undo spent_outpoints that are in txi + for addr, l in self.txi[tx_hash].items(): + for ser, v in l: + self.spent_outpoints.pop(ser, None) + # undo spent_outpoints that are in pruned_txo + for ser, hh in list(self.pruned_txo.items()): + if hh == tx_hash: + self.spent_outpoints.pop(ser) + self.pruned_txo.pop(ser) # add tx to pruned_txo, and undo the txi addition for next_tx, dd in self.txi.items(): From ef9d1110d0bd11c6b01bcf246b06a96fa43925fe Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Mar 2018 19:13:59 +0100 Subject: [PATCH 238/270] qt address list: remove dead code This role was in collision with SortableTreeWidgetItem.DataRole; and was otherwise not used anywhere. --- gui/qt/address_list.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py index b348dfcc198c..104f367a87e2 100644 --- a/gui/qt/address_list.py +++ b/gui/qt/address_list.py @@ -124,7 +124,6 @@ def on_update(self): address_item.setBackground(0, ColorScheme.GREEN.as_color(True)) address_item.setFont(1, QFont(MONOSPACE_FONT)) address_item.setData(1, Qt.UserRole, address) - address_item.setData(1, Qt.UserRole+1, True) # label can be edited if self.wallet.is_frozen(address): address_item.setBackground(1, ColorScheme.BLUE.as_color(True)) if self.wallet.is_beyond_limit(address): From d5effe48d07b667313deede1dede3d6ef6bebf5f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 2 Mar 2018 19:16:08 +0100 Subject: [PATCH 239/270] kivy: do not show balance in every tab. fixes #3350 --- gui/kivy/main.kv | 2 +- gui/kivy/main_window.py | 15 ++++++++------- gui/kivy/uix/screens.py | 5 ----- gui/kivy/uix/ui_screens/history.kv | 30 ++++++++++++++++++++---------- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv index 853ddd94e792..445f5e51e054 100644 --- a/gui/kivy/main.kv +++ b/gui/kivy/main.kv @@ -397,7 +397,7 @@ slide: 1 CleanHeader: id: history_tab - text: _('History') + text: _('Balance') slide: 2 CleanHeader: id: receive_tab diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index d431e220dc7a..9989b6e92aa0 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -83,6 +83,7 @@ class ElectrumWindow(App): num_chains = NumericProperty(0) blockchain_name = StringProperty('') fee_status = StringProperty('Fee') + balance = StringProperty('') blockchain_checkpoint = NumericProperty(0) auto_connect = BooleanProperty(False) @@ -634,16 +635,16 @@ def update_status(self, *dt): if not self.wallet.up_to_date or server_height == 0: status = _("Synchronizing...") elif server_lag > 1: - status = _("Server lagging ({} blocks)").format(server_lag) + status = _("Server lagging") else: - c, u, x = self.wallet.get_balance() - text = self.format_amount(c+x+u) - status = str(text.strip() + ' ' + self.base_unit) + status = '' else: status = _("Disconnected") - - n = self.wallet.basename() - self.status = '[size=15dp]%s[/size]\n%s' %(n, status) + self.status = self.wallet.basename() + (' [size=15dp](%s)[/size]'%status if status else '') + # balance + c, u, x = self.wallet.get_balance() + text = self.format_amount(c+x+u) + self.balance = str(text.strip() + ' ' + self.base_unit) #fiat_balance = self.fx.format_amount_and_units(c+u+x) or '' def get_max_amount(self): diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 3db82c070bf7..07a00f359001 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -162,13 +162,8 @@ def update(self, see_all=False): count = 0 for item in history: ri = self.get_card(*item) - count += 1 history_card.add_widget(ri) - if count == 0: - msg = _('This screen shows your list of transactions. It is currently empty.') - history_card.add_widget(EmptyLabel(text=msg)) - class SendScreen(CScreen): diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv index ad06c5eafb52..b6d1d590e3e2 100644 --- a/gui/kivy/uix/ui_screens/history.kv +++ b/gui/kivy/uix/ui_screens/history.kv @@ -62,13 +62,23 @@ HistoryScreen: name: 'history' content: content - ScrollView: - id: content - do_scroll_x: False - GridLayout - id: history_container - cols: 1 - size_hint: 1, None - height: self.minimum_height - padding: '12dp' - spacing: '2dp' + BoxLayout: + orientation: 'vertical' + Label: + text: app.balance + color: .699, .699, .699, 1 + font_size: '22dp' + bold: True + size_hint: 1, 0.25 + ScrollView: + id: content + do_scroll_x: False + size_hint: 1, 0.75 + #height: self.minimum_height + GridLayout + id: history_container + cols: 1 + size_hint: 1, None + height: self.minimum_height + padding: '12dp' + spacing: '2dp' From f93c3d79d7f9d07db14728196728188086bb10de Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 2 Mar 2018 19:33:33 +0100 Subject: [PATCH 240/270] fix #3740 --- gui/kivy/Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/kivy/Readme.md b/gui/kivy/Readme.md index 4398283f729f..3e66d2078940 100644 --- a/gui/kivy/Readme.md +++ b/gui/kivy/Readme.md @@ -74,6 +74,7 @@ Update your Android build tools to version 27 like described above. ## Why do I get errors like `(use -source 7 or higher to enable multi-catch statement)` while compiling? Make sure that your p4a installation includes commit a3cc78a6d1a107cd3b6bd28db8b80f89e3ecddd2. +Also make sure you have recent SDK tools and platform-tools ## I changed something but I don't see any differences on the phone. What did I do wrong? You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}` From 7816edc3428f7148de1b2c4b567408b68c1d25cb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 3 Mar 2018 00:05:57 +0100 Subject: [PATCH 241/270] fix #4003 --- lib/plugins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/plugins.py b/lib/plugins.py index b3611c167432..9f58611f0224 100644 --- a/lib/plugins.py +++ b/lib/plugins.py @@ -502,7 +502,8 @@ def select_device(self, plugin, handler, keystore, devices=None): info = infos[c] # save new label keystore.set_label(info.label) - handler.win.wallet.save_keystore() + if handler.win.wallet is not None: + handler.win.wallet.save_keystore() return info def scan_devices(self): From 0df6d1ec9712d02f22339c2349336991904fae2e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 3 Mar 2018 02:39:49 +0100 Subject: [PATCH 242/270] trezor: segwit offline signing --- lib/network.py | 3 ++- lib/util.py | 10 ++++++++++ lib/wallet.py | 17 ++++++++++++----- plugins/trezor/trezor.py | 3 +++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/network.py b/lib/network.py index 92edda9585fa..7d00d9c64e98 100644 --- a/lib/network.py +++ b/lib/network.py @@ -40,6 +40,7 @@ from .interface import Connection, Interface from . import blockchain from .version import ELECTRUM_VERSION, PROTOCOL_VERSION +from .i18n import _ NODES_RETRY_INTERVAL = 60 @@ -1069,7 +1070,7 @@ def synchronous_get(self, request, timeout=30): try: r = q.get(True, timeout) except queue.Empty: - raise BaseException('Server did not answer') + raise util.TimeoutException(_('Server did not answer')) if r.get('error'): raise BaseException(r.get('error')) return r.get('result') diff --git a/lib/util.py b/lib/util.py index 925233603744..68d127f851a5 100644 --- a/lib/util.py +++ b/lib/util.py @@ -74,6 +74,16 @@ def __str__(self): return _("Failed to export to file.") + "\n" + self.message +class TimeoutException(Exception): + def __init__(self, message=''): + self.message = str(message) + + def __str__(self): + if not self.message: + return _("Operation timed out.") + return self.message + + # 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): diff --git a/lib/wallet.py b/lib/wallet.py index 35979be25b07..291d63db3010 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -44,7 +44,7 @@ from .i18n import _ from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, - format_satoshis, NoDynamicFeeEstimates) + format_satoshis, NoDynamicFeeEstimates, TimeoutException) from .bitcoin import * from .version import * @@ -1401,21 +1401,28 @@ def can_sign(self, tx): return True return False - def get_input_tx(self, tx_hash): + def get_input_tx(self, tx_hash, ignore_timeout=False): # First look up an input transaction in the wallet where it # will likely be. If co-signing a transaction it may not have # all the input txs, in which case we ask the network. - tx = self.transactions.get(tx_hash) + tx = self.transactions.get(tx_hash, None) if not tx and self.network: request = ('blockchain.transaction.get', [tx_hash]) - tx = Transaction(self.network.synchronous_get(request)) + try: + tx = Transaction(self.network.synchronous_get(request)) + except TimeoutException as e: + self.print_error('getting input txn from network timed out for {}'.format(tx_hash)) + if not ignore_timeout: + raise e return tx def add_hw_info(self, tx): # add previous tx for hw wallets for txin in tx.inputs(): tx_hash = txin['prevout_hash'] - txin['prev_tx'] = self.get_input_tx(tx_hash) + # segwit inputs might not be needed for some hw wallets + ignore_timeout = Transaction.is_segwit_input(txin) + txin['prev_tx'] = self.get_input_tx(tx_hash, ignore_timeout) # add output info for hw wallets info = {} xpubs = self.get_master_public_keys() diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py index f80346c71ac3..512168c18e8b 100644 --- a/plugins/trezor/trezor.py +++ b/plugins/trezor/trezor.py @@ -434,6 +434,9 @@ def tx_outputs(self, derivation, tx, script_gen=SCRIPT_GEN_LEGACY): def electrum_tx_to_txtype(self, tx): t = self.types.TransactionType() + if tx is None: + # probably for segwit input and we don't need this prev txn + return t d = deserialize(tx.raw) t.version = d['version'] t.lock_time = d['lockTime'] From 1e6d522137839067a0e7f74fe980e958cde258eb Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 3 Mar 2018 10:08:51 +0100 Subject: [PATCH 243/270] kivy: more usable amount dialog --- gui/kivy/uix/dialogs/amount_dialog.py | 48 +++++++++++++++------------ 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/gui/kivy/uix/dialogs/amount_dialog.py b/gui/kivy/uix/dialogs/amount_dialog.py index cdb8ae44f345..edccf0e4a7e7 100644 --- a/gui/kivy/uix/dialogs/amount_dialog.py +++ b/gui/kivy/uix/dialogs/amount_dialog.py @@ -13,21 +13,34 @@ anchor_x: 'center' BoxLayout: orientation: 'vertical' - size_hint: 0.8, 1 + size_hint: 0.9, 1 + Widget: + size_hint: 1, 0.2 BoxLayout: size_hint: 1, None height: '80dp' - Label: - id: a - btc_text: (kb.amount + ' ' + app.base_unit) if kb.amount else '' - fiat_text: (kb.fiat_amount + ' ' + app.fiat_unit) if kb.fiat_amount else '' - text1: ((self.fiat_text if kb.is_fiat else self.btc_text) if app.fiat_unit else self.btc_text) if self.btc_text else '' - text2: ((self.btc_text if kb.is_fiat else self.fiat_text) if app.fiat_unit else '') if self.btc_text else '' - text: self.text1 + "\\n" + "[color=#8888ff]" + self.text2 + "[/color]" + Button: + background_color: 0, 0, 0, 0 + id: btc + text: kb.amount + ' ' + app.base_unit + color: (0.7, 0.7, 1, 1) if kb.is_fiat else (1, 1, 1, 1) + halign: 'right' + size_hint: 1, None + font_size: '20dp' + height: '48dp' + on_release: + kb.is_fiat = False + Button: + background_color: 0, 0, 0, 0 + id: fiat + text: kb.fiat_amount + ' ' + app.fiat_unit + color: (1, 1, 1, 1) if kb.is_fiat else (0.7, 0.7, 1, 1) halign: 'right' size_hint: 1, None - font_size: '22dp' - height: '80dp' + font_size: '20dp' + height: '48dp' + on_release: + kb.is_fiat = True Widget: size_hint: 1, 0.2 GridLayout: @@ -65,6 +78,9 @@ text: '0' KButton: text: '<' + Widget: + size_hint: 1, None + height: '48dp' Button: id: but_max opacity: 1 if root.show_max else 0 @@ -75,13 +91,6 @@ on_release: kb.is_fiat = False kb.amount = app.get_max_amount() - Button: - id: button_fiat - size_hint: 1, None - height: '48dp' - text: (app.base_unit if not kb.is_fiat else app.fiat_unit) if app.fiat_unit else '' - on_release: - if app.fiat_unit: popup.toggle_fiat(kb) Button: size_hint: 1, None height: '48dp' @@ -102,7 +111,7 @@ height: '48dp' text: _('OK') on_release: - root.callback(a.btc_text) + root.callback(btc.text) popup.dismiss() ''') @@ -117,9 +126,6 @@ def __init__(self, show_max, amount, cb): if amount: self.ids.kb.amount = amount - def toggle_fiat(self, a): - a.is_fiat = not a.is_fiat - def update_amount(self, c): kb = self.ids.kb amount = kb.fiat_amount if kb.is_fiat else kb.amount From f863e7c7a8524c319339fd65ab6917628dbf3549 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 3 Mar 2018 10:36:56 +0100 Subject: [PATCH 244/270] follow-up previous commit --- gui/kivy/uix/dialogs/amount_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/kivy/uix/dialogs/amount_dialog.py b/gui/kivy/uix/dialogs/amount_dialog.py index edccf0e4a7e7..9a825d79952b 100644 --- a/gui/kivy/uix/dialogs/amount_dialog.py +++ b/gui/kivy/uix/dialogs/amount_dialog.py @@ -111,7 +111,7 @@ height: '48dp' text: _('OK') on_release: - root.callback(btc.text) + root.callback(btc.text if kb.amount else '') popup.dismiss() ''') From 1526768cd668b9b6da1ea902627b8f289395ad0a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 3 Mar 2018 11:32:38 +0100 Subject: [PATCH 245/270] kivy: simplify history screen, display either fiat or btc --- gui/kivy/main.kv | 2 +- gui/kivy/main_window.py | 6 ++-- gui/kivy/uix/screens.py | 13 ++++----- gui/kivy/uix/ui_screens/address.kv | 18 +----------- gui/kivy/uix/ui_screens/history.kv | 46 ++++++++++++------------------ lib/exchange_rate.py | 4 +++ 6 files changed, 34 insertions(+), 55 deletions(-) diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv index 445f5e51e054..95cfe8defbdd 100644 --- a/gui/kivy/main.kv +++ b/gui/kivy/main.kv @@ -239,7 +239,7 @@ self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu() canvas.before: Color: - rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.3, 0.3, 0.3, 1) + rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.15, 0.15, 0.17, 1) Rectangle: size: self.size pos: self.pos diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 9989b6e92aa0..bd23d2ae9a51 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -84,6 +84,8 @@ class ElectrumWindow(App): blockchain_name = StringProperty('') fee_status = StringProperty('Fee') balance = StringProperty('') + fiat_balance = StringProperty('') + is_fiat = BooleanProperty(False) blockchain_checkpoint = NumericProperty(0) auto_connect = BooleanProperty(False) @@ -644,8 +646,8 @@ def update_status(self, *dt): # balance c, u, x = self.wallet.get_balance() text = self.format_amount(c+x+u) - self.balance = str(text.strip() + ' ' + self.base_unit) - #fiat_balance = self.fx.format_amount_and_units(c+u+x) or '' + self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit + self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy def get_max_amount(self): inputs = self.wallet.get_spendable_coins(None, self.electrum_config) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 07a00f359001..eb9cf42110a8 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -143,14 +143,13 @@ def get_card(self, tx_hash, height, conf, timestamp, value, balance): ri.icon = icon ri.date = status_str ri.message = label - ri.value = value or 0 - ri.amount = self.app.format_amount(value, True) if value is not None else '--' ri.confirmations = conf - if self.app.fiat_unit and date: - rate = self.app.fx.history_rate(date) - if rate: - s = self.app.fx.value_str(value, rate) - ri.quote_text = '' if s is None else s + ' ' + self.app.fiat_unit + if value is not None: + ri.is_mine = value < 0 + if value < 0: value = - value + ri.amount = self.app.format_amount_and_units(value) + if self.app.fiat_unit and date: + ri.quote_text = self.app.fx.historical_value_str(value, date) + ' ' + self.app.fx.ccy return ri def update(self, see_all=False): diff --git a/gui/kivy/uix/ui_screens/address.kv b/gui/kivy/uix/ui_screens/address.kv index d0247a3429e4..07bb43677108 100644 --- a/gui/kivy/uix/ui_screens/address.kv +++ b/gui/kivy/uix/ui_screens/address.kv @@ -24,26 +24,11 @@ shorten: True Widget AddressLabel: - text: root.memo + text: (root.amount if root.status == 'Funded' else root.status) + ' ' + root.memo color: .699, .699, .699, 1 font_size: '13sp' shorten: True Widget - BoxLayout: - spacing: '8dp' - height: '32dp' - orientation: 'vertical' - Widget - AddressLabel: - text: root.amount - halign: 'right' - font_size: '15sp' - Widget - AddressLabel: - text: root.status - halign: 'right' - font_size: '13sp' - color: .699, .699, .699, 1 AddressScreen: id: addr_screen @@ -103,4 +88,3 @@ AddressScreen: id: search_container size_hint_y: None height: self.minimum_height - spacing: '2dp' diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv index b6d1d590e3e2..d6e371a2f2ae 100644 --- a/gui/kivy/uix/ui_screens/history.kv +++ b/gui/kivy/uix/ui_screens/history.kv @@ -19,44 +19,34 @@ icon: 'atlas://gui/kivy/theming/light/important' message: '' - value: 0 + is_mine: True amount: '--' - amount_color: '#FF6657' if self.value < 0 else '#2EA442' + action: _('Sent') if self.is_mine else _('Received') + amount_color: '#FF6657' if self.is_mine else '#2EA442' confirmations: 0 date: '' quote_text: '' - spacing: '9dp' Image: id: icon source: root.icon size_hint: None, 1 - width: self.height *.54 + width: self.height mipmap: True BoxLayout: orientation: 'vertical' Widget CardLabel: - text: root.date - font_size: '14sp' + text: + u'[color={color}]{s}[/color]'.format(s='<<' if root.is_mine else '>>', color=root.amount_color)\ + + ' ' + root.action + ' ' + (root.quote_text if app.is_fiat else root.amount) + font_size: '15sp' CardLabel: color: .699, .699, .699, 1 - font_size: '13sp' + font_size: '14sp' shorten: True - text: root.message + text: root.date + ' ' + root.message Widget - CardLabel: - halign: 'right' - font_size: '15sp' - size_hint: None, 1 - width: '110sp' - markup: True - font_name: font_light - text: - u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\ - u'[color=#B2B3B3][size=13sp]{qt}[/size]'\ - u'[/color]'.format(amount_color=root.amount_color,\ - amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\ - unit=app.base_unit) + HistoryScreen: @@ -64,21 +54,21 @@ HistoryScreen: content: content BoxLayout: orientation: 'vertical' - Label: - text: app.balance - color: .699, .699, .699, 1 - font_size: '22dp' + Button: + background_color: 0, 0, 0, 0 + text: app.fiat_balance if app.is_fiat else app.balance + markup: True + color: .9, .9, .9, 1 + font_size: '30dp' bold: True size_hint: 1, 0.25 + on_release: app.is_fiat = not app.is_fiat ScrollView: id: content do_scroll_x: False size_hint: 1, 0.75 - #height: self.minimum_height GridLayout id: history_container cols: 1 size_hint: 1, None height: self.minimum_height - padding: '12dp' - spacing: '2dp' diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index 7b9c86cc89a4..02ed91b9c347 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -505,6 +505,10 @@ def exchange_rate(self): return Decimal('NaN') return Decimal(rate) + def format_amount(self, btc_balance): + rate = self.exchange_rate() + return '' if rate.is_nan() else "%s" % self.value_str(btc_balance, rate) + def format_amount_and_units(self, btc_balance): rate = self.exchange_rate() return '' if rate.is_nan() else "%s %s" % (self.value_str(btc_balance, rate), self.ccy) From 721dc8cdb9147c3dcbb6b9e7d3034cbe51b77ea8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 3 Mar 2018 12:18:01 +0100 Subject: [PATCH 246/270] kivy: larger history icons --- gui/kivy/uix/ui_screens/history.kv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv index d6e371a2f2ae..524b53197940 100644 --- a/gui/kivy/uix/ui_screens/history.kv +++ b/gui/kivy/uix/ui_screens/history.kv @@ -30,7 +30,8 @@ id: icon source: root.icon size_hint: None, 1 - width: self.height + allow_stretch: True + width: self.height*1.5 mipmap: True BoxLayout: orientation: 'vertical' From 2e594d2d7a9731f5db1979fa855282dc9e46b186 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 3 Mar 2018 14:58:55 +0100 Subject: [PATCH 247/270] kivy: simplify fee dialog --- gui/kivy/uix/dialogs/fee_dialog.py | 74 +++++++++++++----------------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/gui/kivy/uix/dialogs/fee_dialog.py b/gui/kivy/uix/dialogs/fee_dialog.py index cf29f36b8d6f..7c270b89e04b 100644 --- a/gui/kivy/uix/dialogs/fee_dialog.py +++ b/gui/kivy/uix/dialogs/fee_dialog.py @@ -11,13 +11,26 @@ title: _('Transaction Fees') size_hint: 0.8, 0.8 pos_hint: {'top':0.9} + method: 0 BoxLayout: orientation: 'vertical' BoxLayout: orientation: 'horizontal' size_hint: 1, 0.5 Label: - text: (_('Target') if dynfees.active else _('Fixed rate')) + ':' + text: _('Method') + ':' + Button: + text: _('Mempool based') if root.method == 2 else _('ETA based') if root.method == 1 else _('Static') + background_color: (0,0,0,0) + on_release: + root.method = (root.method + 1) % 3 + root.update_slider() + root.update_text() + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + Label: + text: _('Target') + ':' Label: id: fee_target text: '' @@ -25,7 +38,7 @@ orientation: 'horizontal' size_hint: 1, 0.5 Label: - text: (_('Current rate') if dynfees.active else _('Estimate')) + ':' + text: (_('Current rate') if root.method > 0 else _('Estimate')) + ':' Label: id: fee_estimate text: '' @@ -34,22 +47,6 @@ range: 0, 4 step: 1 on_value: root.on_slider(self.value) - BoxLayout: - orientation: 'horizontal' - size_hint: 1, 0.5 - Label: - text: _('Dynamic Fees') - CheckBox: - id: dynfees - on_active: root.on_dynfees(self.active) - BoxLayout: - orientation: 'horizontal' - size_hint: 1, 0.5 - Label: - text: _('Use mempool') - CheckBox: - id: mempool - on_active: root.on_mempool(self.active) Widget: size_hint: 1, 1 BoxLayout: @@ -77,10 +74,9 @@ def __init__(self, app, config, callback): self.config = config self.fee_rate = self.config.fee_per_kb() self.callback = callback - 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 + mempool = self.config.use_mempool_fees() + dynfees = self.config.is_dynfee() + self.method = (2 if mempool else 1) if dynfees else 0 self.update_slider() self.update_text() @@ -90,28 +86,34 @@ def update_text(self): self.ids.fee_target.text = target self.ids.fee_estimate.text = estimate + def get_method(self): + dynfees = self.method > 0 + mempool = self.method == 2 + return dynfees, mempool + def update_slider(self): slider = self.ids.slider - maxp, pos, fee_rate = self.config.get_fee_slider(self.dynfees, self.mempool) + dynfees, mempool = self.get_method() + maxp, pos, fee_rate = self.config.get_fee_slider(dynfees, mempool) slider.range = (0, maxp) slider.step = 1 slider.value = pos def get_fee_text(self, pos): - dyn = self.dynfees - mempool = self.mempool - if dyn: + dynfees, mempool = self.get_method() + if dynfees: fee_rate = self.config.depth_to_fee(pos) if mempool else self.config.eta_to_fee(pos) else: fee_rate = self.config.static_fee(pos) - return self.config.get_fee_text(pos, dyn, mempool, fee_rate) + return self.config.get_fee_text(pos, dynfees, mempool, fee_rate) def on_ok(self): value = int(self.ids.slider.value) - self.config.set_key('dynamic_fees', self.dynfees, False) - self.config.set_key('mempool_fees', self.mempool, False) - if self.dynfees: - if self.mempool: + dynfees, mempool = self.get_method() + self.config.set_key('dynamic_fees', dynfees, False) + self.config.set_key('mempool_fees', mempool, False) + if dynfees: + if mempool: self.config.set_key('depth_level', value, True) else: self.config.set_key('fee_level', value, True) @@ -121,13 +123,3 @@ def on_ok(self): def on_slider(self, value): self.update_text() - - def on_dynfees(self, b): - self.dynfees = b - self.update_slider() - self.update_text() - - def on_mempool(self, b): - self.mempool = b - self.update_slider() - self.update_text() From 83f2dc44dedea15199e2ef8dad27831955e27e91 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Sat, 3 Mar 2018 19:36:05 +0100 Subject: [PATCH 248/270] Add websocket library to binary for Ledger 2FA support Closes: #2433 --- contrib/build-osx/osx.spec | 1 + contrib/build-wine/deterministic.spec | 1 + 2 files changed, 2 insertions(+) diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec index caef2519f5f7..a2c02f39b5a9 100644 --- a/contrib/build-osx/osx.spec +++ b/contrib/build-osx/osx.spec @@ -25,6 +25,7 @@ hiddenimports = [] hiddenimports += collect_submodules('trezorlib') hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('keepkeylib') +hiddenimports += collect_submodules('websocket') datas = [ (electrum+'lib/currencies.json', PYPKG), diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index f6888a03d03a..33dfd60bae27 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -18,6 +18,7 @@ hiddenimports = [] hiddenimports += collect_submodules('trezorlib') hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('keepkeylib') +hiddenimports += collect_submodules('websocket') # Add libusb binary binaries = [("c:/python3.5.4/libusb-1.0.dll", ".")] From 10057b18de218e7c6328324ccc09376f94441355 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Fri, 2 Mar 2018 20:08:30 +0100 Subject: [PATCH 249/270] Cache dependencies for Travis build Also: * Move temporary directory to /tmp * Remove files with wrong hash (so we don't have to remove them manually) --- .travis.yml | 5 ++++- contrib/build-wine/prepare-wine.sh | 11 +++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9b129e33d942..4646822a6a1b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,9 @@ python: install: - pip install -r contrib/requirements/requirements-travis.txt cache: - - pip + - pip: true + - directories: + - /tmp/electrum-build script: - tox after_success: @@ -24,5 +26,6 @@ jobs: - 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 + before_script: ls -lah /tmp/electrum-build script: ./contrib/build-wine/build.sh after_success: true diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 1bd9af08e571..c1bed9beb72a 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -42,6 +42,7 @@ verify_hash() { return 0 else echo "$file $actual_hash (unexpected hash)" >&2 + rm "$file" exit 1 fi } @@ -54,7 +55,7 @@ download_if_not_exist() { } # Let's begin! -cd `dirname $0` +here=$(dirname $(readlink -e $0)) set -e # Clean up Wine environment @@ -64,11 +65,9 @@ echo "done" wine 'wineboot' -echo "Cleaning tmp" -mkdir -p tmp -echo "done" +mkdir -p /tmp/electrum-build -cd tmp +cd /tmp/electrum-build # Install Python # note: you might need "sudo apt-get install dirmngr" for the following @@ -94,7 +93,7 @@ $PYTHON -m pip install pywin32-ctypes==0.1.2 # install PySocks $PYTHON -m pip install win_inet_pton==1.0.1 -$PYTHON -m pip install -r ../../deterministic-build/requirements-binaries.txt +$PYTHON -m pip install -r $here/../deterministic-build/requirements-binaries.txt # Install PyInstaller $PYTHON -m pip install https://github.com/ecdsa/pyinstaller/archive/fix_2952.zip From c2da8eb46df44f820ebe2715abe5d8d6dd83aeaa Mon Sep 17 00:00:00 2001 From: WakiyamaP Date: Sun, 4 Mar 2018 04:48:08 +0900 Subject: [PATCH 250/270] Add JPY exchange rate --- lib/currencies.json | 13 +++++++++++-- lib/exchange_rate.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/currencies.json b/lib/currencies.json index a4e85f1f6fc4..62efefdca106 100644 --- a/lib/currencies.json +++ b/lib/currencies.json @@ -798,5 +798,14 @@ "RUB", "USD" ], - "itBit": [] -} \ No newline at end of file + "itBit": [], + "Bitbank": [ + "JPY" + ], + "Bitflyer": [ + "JPY" + ], + "Zaif": [ + "JPY" + ] +} diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index 02ed91b9c347..526a71ff37ee 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -159,6 +159,19 @@ def request_history(self, ccy): return self.get_json('api.bitcoinvenezuela.com', "/historical/index.php?coin=BTC")[ccy +'_BTC'] +class Bitbank(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('public.bitbank.cc', '/btc_jpy/ticker') + return {'JPY': Decimal(json['data']['last'])} + + +class BitFlyer(ExchangeBase): + + def get_rates(self, ccy): + json = self.get_json('bitflyer.jp', '/api/echo/price') + return {'JPY': Decimal(json['mid'])} + class Bitmarket(ExchangeBase): @@ -350,6 +363,12 @@ def request_history(self, ccy): for h in history]) +class Zaif(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy') + return {'JPY': Decimal(json['last_price'])} + + def dictinvert(d): inv = {} for k, vlist in d.items(): From 56c94153ff60ddf917645ceaf7823c5994a2b630 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 4 Mar 2018 10:29:56 +0100 Subject: [PATCH 251/270] kivy: simplify fee dialog --- gui/kivy/uix/dialogs/fee_dialog.py | 51 +++++++++++++++++------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/gui/kivy/uix/dialogs/fee_dialog.py b/gui/kivy/uix/dialogs/fee_dialog.py index 7c270b89e04b..2694a6b85fc8 100644 --- a/gui/kivy/uix/dialogs/fee_dialog.py +++ b/gui/kivy/uix/dialogs/fee_dialog.py @@ -20,8 +20,9 @@ Label: text: _('Method') + ':' Button: - text: _('Mempool based') if root.method == 2 else _('ETA based') if root.method == 1 else _('Static') + text: _('Mempool') if root.method == 2 else _('ETA') if root.method == 1 else _('Static') background_color: (0,0,0,0) + bold: True on_release: root.method = (root.method + 1) % 3 root.update_slider() @@ -30,25 +31,26 @@ orientation: 'horizontal' size_hint: 1, 0.5 Label: - text: _('Target') + ':' + text: (_('Target') if root.method > 0 else _('Fee')) + ':' Label: id: fee_target text: '' - BoxLayout: - orientation: 'horizontal' - size_hint: 1, 0.5 - Label: - text: (_('Current rate') if root.method > 0 else _('Estimate')) + ':' - Label: - id: fee_estimate - text: '' Slider: id: slider range: 0, 4 step: 1 on_value: root.on_slider(self.value) Widget: - size_hint: 1, 1 + size_hint: 1, 0.5 + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + TopLabel: + id: fee_estimate + text: '' + font_size: '14dp' + Widget: + size_hint: 1, 0.5 BoxLayout: orientation: 'horizontal' size_hint: 1, 0.5 @@ -81,10 +83,23 @@ def __init__(self, app, config, callback): self.update_text() def update_text(self): - value = int(self.ids.slider.value) - target, estimate = self.get_fee_text(value) + pos = int(self.ids.slider.value) + dynfees, mempool = self.get_method() + if self.method == 2: + fee_rate = self.config.depth_to_fee(pos) + target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate) + msg = 'In the current network conditions, in order to be positioned %s, a transaction will require a fee of %s.' % (target, estimate) + elif self.method == 1: + fee_rate = self.config.eta_to_fee(pos) + target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate) + msg = 'In the last few days, transactions that confirmed %s usually paid a fee of at least %s.' % (target.lower(), estimate) + else: + fee_rate = self.config.static_fee(pos) + target, estimate = self.config.get_fee_text(pos, dynfees, True, fee_rate) + msg = 'In the current network conditions, a transaction paying %s would be positioned %s.' % (target, estimate) + self.ids.fee_target.text = target - self.ids.fee_estimate.text = estimate + self.ids.fee_estimate.text = msg def get_method(self): dynfees = self.method > 0 @@ -99,14 +114,6 @@ def update_slider(self): slider.step = 1 slider.value = pos - def get_fee_text(self, pos): - dynfees, mempool = self.get_method() - if dynfees: - fee_rate = self.config.depth_to_fee(pos) if mempool else self.config.eta_to_fee(pos) - else: - fee_rate = self.config.static_fee(pos) - return self.config.get_fee_text(pos, dynfees, mempool, fee_rate) - def on_ok(self): value = int(self.ids.slider.value) dynfees, mempool = self.get_method() From 1c0e23c8b90a99f7a2fe1b8e2dbbd128fa1c7d5f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 4 Mar 2018 10:35:14 +0100 Subject: [PATCH 252/270] Revert "Add JPY exchange rate" This reverts commit c2da8eb46df44f820ebe2715abe5d8d6dd83aeaa. --- lib/currencies.json | 13 ++----------- lib/exchange_rate.py | 19 ------------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/lib/currencies.json b/lib/currencies.json index 62efefdca106..a4e85f1f6fc4 100644 --- a/lib/currencies.json +++ b/lib/currencies.json @@ -798,14 +798,5 @@ "RUB", "USD" ], - "itBit": [], - "Bitbank": [ - "JPY" - ], - "Bitflyer": [ - "JPY" - ], - "Zaif": [ - "JPY" - ] -} + "itBit": [] +} \ No newline at end of file diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index 526a71ff37ee..02ed91b9c347 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -159,19 +159,6 @@ def request_history(self, ccy): return self.get_json('api.bitcoinvenezuela.com', "/historical/index.php?coin=BTC")[ccy +'_BTC'] -class Bitbank(ExchangeBase): - - def get_rates(self, ccy): - json = self.get_json('public.bitbank.cc', '/btc_jpy/ticker') - return {'JPY': Decimal(json['data']['last'])} - - -class BitFlyer(ExchangeBase): - - def get_rates(self, ccy): - json = self.get_json('bitflyer.jp', '/api/echo/price') - return {'JPY': Decimal(json['mid'])} - class Bitmarket(ExchangeBase): @@ -363,12 +350,6 @@ def request_history(self, ccy): for h in history]) -class Zaif(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy') - return {'JPY': Decimal(json['last_price'])} - - def dictinvert(d): inv = {} for k, vlist in d.items(): From 81b6d65764a718be99b4f8e309a644ac1dc78ed3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 4 Mar 2018 22:10:59 +0100 Subject: [PATCH 253/270] refactor network constants --- electrum | 3 +- gui/kivy/main_window.py | 4 +- gui/qt/exception_window.py | 4 +- gui/qt/main_window.py | 5 +- gui/qt/network_dialog.py | 6 +- lib/bitcoin.py | 131 +++++++------------------ lib/blockchain.py | 11 ++- lib/constants.py | 107 ++++++++++++++++++++ lib/keystore.py | 4 +- lib/network.py | 11 ++- lib/tests/test_bitcoin.py | 7 +- lib/util.py | 4 +- plugins/digitalbitbox/digitalbitbox.py | 7 +- plugins/keepkey/plugin.py | 9 +- plugins/trezor/trezor.py | 5 +- plugins/trustedcoin/trustedcoin.py | 5 +- 16 files changed, 191 insertions(+), 132 deletions(-) create mode 100644 lib/constants.py diff --git a/electrum b/electrum index 4a0470b2388e..78924631e21b 100755 --- a/electrum +++ b/electrum @@ -89,6 +89,7 @@ if is_local or is_android: from electrum import bitcoin, util +from electrum import constants from electrum import SimpleConfig, Network from electrum.wallet import Wallet, Imported_Wallet from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption @@ -411,7 +412,7 @@ if __name__ == '__main__': cmdname = config.get('cmd') if config.get('testnet'): - bitcoin.NetworkConstants.set_testnet() + constants.set_testnet() # run non-RPC commands separately if cmdname in ['create', 'restore']: diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index bd23d2ae9a51..9261e8b5e27b 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -99,8 +99,8 @@ def choose_server_dialog(self, popup): from .uix.dialogs.choice_dialog import ChoiceDialog protocol = 's' def cb2(host): - from electrum.bitcoin import NetworkConstants - pp = servers.get(host, NetworkConstants.DEFAULT_PORTS) + from electrum import constants + pp = servers.get(host, constants.net.DEFAULT_PORTS) port = pp.get(protocol, '') popup.ids.host.text = host popup.ids.port.text = port diff --git a/gui/qt/exception_window.py b/gui/qt/exception_window.py index 743a98351266..fa29128349fb 100644 --- a/gui/qt/exception_window.py +++ b/gui/qt/exception_window.py @@ -36,7 +36,7 @@ from PyQt5.QtWidgets import * from electrum.i18n import _ -from electrum import ELECTRUM_VERSION, bitcoin +from electrum import ELECTRUM_VERSION, bitcoin, constants issue_template = """

    Traceback

    @@ -107,7 +107,7 @@ def __init__(self, main_window, exctype, value, tb):
             self.show()
     
         def send_report(self):
    -        if bitcoin.NetworkConstants.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in report_server:
    +        if constants.net.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
    diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
    index ababe1f16bdf..f46d4ba71572 100644
    --- a/gui/qt/main_window.py
    +++ b/gui/qt/main_window.py
    @@ -40,7 +40,8 @@
     from PyQt5.QtWidgets import *
     
     from electrum import keystore, simple_config
    -from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS, NetworkConstants
    +from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS
    +from electrum import constants
     from electrum.plugins import run_hook
     from electrum.i18n import _
     from electrum.util import (format_time, format_satoshis, PrintError,
    @@ -371,7 +372,7 @@ def init_geometry(self):
                 self.setGeometry(100, 100, 840, 400)
     
         def watching_only_changed(self):
    -        name = "Electrum Testnet" if NetworkConstants.TESTNET else "Electrum"
    +        name = "Electrum Testnet" if constants.net.TESTNET else "Electrum"
             title = '%s %s  -  %s' % (name, self.wallet.electrum_version,
                                             self.wallet.basename())
             extra = [self.wallet.storage.get('wallet_type', '?')]
    diff --git a/gui/qt/network_dialog.py b/gui/qt/network_dialog.py
    index f1c578e36c29..e578bb43ae35 100644
    --- a/gui/qt/network_dialog.py
    +++ b/gui/qt/network_dialog.py
    @@ -31,7 +31,7 @@
     import PyQt5.QtCore as QtCore
     
     from electrum.i18n import _
    -from electrum.bitcoin import NetworkConstants
    +from electrum import constants
     from electrum.util import print_error
     from electrum.network import serialize_server, deserialize_server
     
    @@ -393,7 +393,7 @@ def set_protocol(self, protocol):
         def change_protocol(self, use_ssl):
             p = 's' if use_ssl else 't'
             host = self.server_host.text()
    -        pp = self.servers.get(host, NetworkConstants.DEFAULT_PORTS)
    +        pp = self.servers.get(host, constants.net.DEFAULT_PORTS)
             if p not in pp.keys():
                 p = list(pp.keys())[0]
             port = pp[p]
    @@ -418,7 +418,7 @@ def server_changed(self, x):
                 self.change_server(str(x.text(0)), self.protocol)
     
         def change_server(self, host, protocol):
    -        pp = self.servers.get(host, NetworkConstants.DEFAULT_PORTS)
    +        pp = self.servers.get(host, constants.net.DEFAULT_PORTS)
             if protocol and protocol not in protocol_letters:
                 protocol = None
             if protocol:
    diff --git a/lib/bitcoin.py b/lib/bitcoin.py
    index 2926cb732e24..18d2afd145af 100644
    --- a/lib/bitcoin.py
    +++ b/lib/bitcoin.py
    @@ -36,75 +36,8 @@
     from . import version
     from .util import print_error, InvalidPassword, assert_bytes, to_bytes, inv_dict
     from . import segwit_addr
    +from . import constants
     
    -def read_json(filename, default):
    -    path = os.path.join(os.path.dirname(__file__), filename)
    -    try:
    -        with open(path, 'r') as f:
    -            r = json.loads(f.read())
    -    except:
    -        r = default
    -    return r
    -
    -
    -class NetworkConstants:
    -
    -    @classmethod
    -    def set_mainnet(cls):
    -        cls.TESTNET = False
    -        cls.WIF_PREFIX = 0x80
    -        cls.ADDRTYPE_P2PKH = 0
    -        cls.ADDRTYPE_P2SH = 5
    -        cls.SEGWIT_HRP = "bc"
    -        cls.GENESIS = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
    -        cls.DEFAULT_PORTS = {'t': '50001', 's': '50002'}
    -        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
    -        cls.WIF_PREFIX = 0xef
    -        cls.ADDRTYPE_P2PKH = 111
    -        cls.ADDRTYPE_P2SH = 196
    -        cls.SEGWIT_HRP = "tb"
    -        cls.GENESIS = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
    -        cls.DEFAULT_PORTS = {'t':'51001', 's':'51002'}
    -        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()
     
     ################################## transactions
     
    @@ -341,16 +274,16 @@ def b58_address_to_hash160(addr):
     
     
     def hash160_to_p2pkh(h160):
    -    return hash160_to_b58_address(h160, NetworkConstants.ADDRTYPE_P2PKH)
    +    return hash160_to_b58_address(h160, constants.net.ADDRTYPE_P2PKH)
     
     def hash160_to_p2sh(h160):
    -    return hash160_to_b58_address(h160, NetworkConstants.ADDRTYPE_P2SH)
    +    return hash160_to_b58_address(h160, constants.net.ADDRTYPE_P2SH)
     
     def public_key_to_p2pkh(public_key):
         return hash160_to_p2pkh(hash_160(public_key))
     
     def hash_to_segwit_addr(h):
    -    return segwit_addr.encode(NetworkConstants.SEGWIT_HRP, 0, h)
    +    return segwit_addr.encode(constants.net.SEGWIT_HRP, 0, h)
     
     def public_key_to_p2wpkh(public_key):
         return hash_to_segwit_addr(hash_160(public_key))
    @@ -396,7 +329,7 @@ def script_to_address(script):
         return addr
     
     def address_to_script(addr):
    -    witver, witprog = segwit_addr.decode(NetworkConstants.SEGWIT_HRP, addr)
    +    witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr)
         if witprog is not None:
             assert (0 <= witver <= 16)
             OP_n = witver + 0x50 if witver > 0 else 0
    @@ -404,11 +337,11 @@ def address_to_script(addr):
             script += push_script(bh2u(bytes(witprog)))
             return script
         addrtype, hash_160 = b58_address_to_hash160(addr)
    -    if addrtype == NetworkConstants.ADDRTYPE_P2PKH:
    +    if addrtype == constants.net.ADDRTYPE_P2PKH:
             script = '76a9'                                      # op_dup, op_hash_160
             script += push_script(bh2u(hash_160))
             script += '88ac'                                     # op_equalverify, op_checksig
    -    elif addrtype == NetworkConstants.ADDRTYPE_P2SH:
    +    elif addrtype == constants.net.ADDRTYPE_P2SH:
             script = 'a9'                                        # op_hash_160
             script += push_script(bh2u(hash_160))
             script += '87'                                       # op_equal
    @@ -526,9 +459,9 @@ def DecodeBase58Check(psz):
     
     def serialize_privkey(secret, compressed, txin_type, internal_use=False):
         if internal_use:
    -        prefix = bytes([(SCRIPT_TYPES[txin_type] + NetworkConstants.WIF_PREFIX) & 255])
    +        prefix = bytes([(SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255])
         else:
    -        prefix = bytes([NetworkConstants.WIF_PREFIX])
    +        prefix = bytes([constants.net.WIF_PREFIX])
         suffix = b'\01' if compressed else b''
         vchIn = prefix + secret + suffix
         base58_wif = EncodeBase58Check(vchIn)
    @@ -552,9 +485,9 @@ def deserialize_privkey(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]
    +        txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - constants.net.WIF_PREFIX]
         else:
    -        assert vch[0] == NetworkConstants.WIF_PREFIX
    +        assert vch[0] == constants.net.WIF_PREFIX
     
         assert len(vch) in [33, 34]
         compressed = len(vch) == 34
    @@ -590,7 +523,7 @@ def address_from_private_key(sec):
     
     def is_segwit_address(addr):
         try:
    -        witver, witprog = segwit_addr.decode(NetworkConstants.SEGWIT_HRP, addr)
    +        witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr)
         except Exception as e:
             return False
         return witprog is not None
    @@ -600,7 +533,7 @@ def is_b58_address(addr):
             addrtype, h = b58_address_to_hash160(addr)
         except Exception as e:
             return False
    -    if addrtype not in [NetworkConstants.ADDRTYPE_P2PKH, NetworkConstants.ADDRTYPE_P2SH]:
    +    if addrtype not in [constants.net.ADDRTYPE_P2PKH, constants.net.ADDRTYPE_P2SH]:
             return False
         return addr == hash160_to_b58_address(h, addrtype)
     
    @@ -912,25 +845,35 @@ def _CKD_pub(cK, c, s):
         return cK_n, c_n
     
     
    -def xprv_header(xtype):
    -    return bfh("%08x" % NetworkConstants.XPRV_HEADERS[xtype])
    +def xprv_header(xtype, *, net=None):
    +    if net is None:
    +        net = constants.net
    +    return bfh("%08x" % net.XPRV_HEADERS[xtype])
     
     
    -def xpub_header(xtype):
    -    return bfh("%08x" % NetworkConstants.XPUB_HEADERS[xtype])
    +def xpub_header(xtype, *, net=None):
    +    if net is None:
    +        net = constants.net
    +    return bfh("%08x" % net.XPUB_HEADERS[xtype])
     
     
    -def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4):
    -    xprv = xprv_header(xtype) + bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
    +def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4,
    +                   child_number=b'\x00'*4, *, net=None):
    +    xprv = xprv_header(xtype, net=net) \
    +           + bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
         return EncodeBase58Check(xprv)
     
     
    -def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4):
    -    xpub = xpub_header(xtype) + bytes([depth]) + fingerprint + child_number + c + cK
    +def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4,
    +                   child_number=b'\x00'*4, *, net=None):
    +    xpub = xpub_header(xtype, net=net) \
    +           + bytes([depth]) + fingerprint + child_number + c + cK
         return EncodeBase58Check(xpub)
     
     
    -def deserialize_xkey(xkey, prv):
    +def deserialize_xkey(xkey, prv, *, net=None):
    +    if net is None:
    +        net = constants.net
         xkey = DecodeBase58Check(xkey)
         if len(xkey) != 78:
             raise BaseException('Invalid length')
    @@ -939,7 +882,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 = NetworkConstants.XPRV_HEADERS if prv else NetworkConstants.XPUB_HEADERS
    +    headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS
         if header not in headers.values():
             raise BaseException('Invalid xpub format', hex(header))
         xtype = list(headers.keys())[list(headers.values()).index(header)]
    @@ -948,11 +891,11 @@ def deserialize_xkey(xkey, prv):
         return xtype, depth, fingerprint, child_number, c, K_or_k
     
     
    -def deserialize_xpub(xkey):
    -    return deserialize_xkey(xkey, False)
    +def deserialize_xpub(xkey, *, net=None):
    +    return deserialize_xkey(xkey, False, net=net)
     
    -def deserialize_xprv(xkey):
    -    return deserialize_xkey(xkey, True)
    +def deserialize_xprv(xkey, *, net=None):
    +    return deserialize_xkey(xkey, True, net=net)
     
     def xpub_type(x):
         return deserialize_xpub(x)[0]
    diff --git a/lib/blockchain.py b/lib/blockchain.py
    index d592e584bf23..f6fe31dcf033 100644
    --- a/lib/blockchain.py
    +++ b/lib/blockchain.py
    @@ -25,6 +25,7 @@
     
     from . import util
     from . import bitcoin
    +from . import constants
     from .bitcoin import *
     
     MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
    @@ -102,7 +103,7 @@ def __init__(self, config, checkpoint, parent_id):
             self.config = config
             self.catch_up = None # interface catching up
             self.checkpoint = checkpoint
    -        self.checkpoints = bitcoin.NetworkConstants.CHECKPOINTS
    +        self.checkpoints = constants.net.CHECKPOINTS
             self.parent_id = parent_id
             self.lock = threading.Lock()
             with self.lock:
    @@ -152,7 +153,7 @@ def verify_header(self, header, prev_hash, target):
             _hash = hash_header(header)
             if prev_hash != header.get('prev_block_hash'):
                 raise BaseException("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
    -        if bitcoin.NetworkConstants.TESTNET:
    +        if constants.net.TESTNET:
                 return
             bits = self.target_to_bits(target)
             if bits != header.get('bits'):
    @@ -262,7 +263,7 @@ def get_hash(self, height):
             if height == -1:
                 return '0000000000000000000000000000000000000000000000000000000000000000'
             elif height == 0:
    -            return bitcoin.NetworkConstants.GENESIS
    +            return constants.net.GENESIS
             elif height < len(self.checkpoints) * 2016:
                 assert (height+1) % 2016 == 0, height
                 index = height // 2016
    @@ -273,7 +274,7 @@ def get_hash(self, height):
     
         def get_target(self, index):
             # compute target from chunk x, used in chunk x+1
    -        if bitcoin.NetworkConstants.TESTNET:
    +        if constants.net.TESTNET:
                 return 0
             if index == -1:
                 return MAX_TARGET
    @@ -317,7 +318,7 @@ def can_connect(self, header, check_height=True):
                 #self.print_error("cannot connect at height", height)
                 return False
             if height == 0:
    -            return hash_header(header) == bitcoin.NetworkConstants.GENESIS
    +            return hash_header(header) == constants.net.GENESIS
             try:
                 prev_hash = self.get_hash(height - 1)
             except:
    diff --git a/lib/constants.py b/lib/constants.py
    new file mode 100644
    index 000000000000..ec35cbe3e9b9
    --- /dev/null
    +++ b/lib/constants.py
    @@ -0,0 +1,107 @@
    +# -*- coding: utf-8 -*-
    +#
    +# Electrum - lightweight Bitcoin client
    +# Copyright (C) 2018 The Electrum developers
    +#
    +# Permission is hereby granted, free of charge, to any person
    +# obtaining a copy of this software and associated documentation files
    +# (the "Software"), to deal in the Software without restriction,
    +# including without limitation the rights to use, copy, modify, merge,
    +# publish, distribute, sublicense, and/or sell copies of the Software,
    +# and to permit persons to whom the Software is furnished to do so,
    +# subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be
    +# included in all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
    +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
    +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
    +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +# SOFTWARE.
    +
    +import os
    +import json
    +
    +
    +def read_json(filename, default):
    +    path = os.path.join(os.path.dirname(__file__), filename)
    +    try:
    +        with open(path, 'r') as f:
    +            r = json.loads(f.read())
    +    except:
    +        r = default
    +    return r
    +
    +
    +class BitcoinMainnet:
    +
    +    TESTNET = False
    +    WIF_PREFIX = 0x80
    +    ADDRTYPE_P2PKH = 0
    +    ADDRTYPE_P2SH = 5
    +    SEGWIT_HRP = "bc"
    +    GENESIS = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
    +    DEFAULT_PORTS = {'t': '50001', 's': '50002'}
    +    DEFAULT_SERVERS = read_json('servers.json', {})
    +    CHECKPOINTS = read_json('checkpoints.json', [])
    +
    +    XPRV_HEADERS = {
    +        'standard':    0x0488ade4,  # xprv
    +        'p2wpkh-p2sh': 0x049d7878,  # yprv
    +        'p2wsh-p2sh':  0x0295b005,  # Yprv
    +        'p2wpkh':      0x04b2430c,  # zprv
    +        'p2wsh':       0x02aa7a99,  # Zprv
    +    }
    +    XPUB_HEADERS = {
    +        'standard':    0x0488b21e,  # xpub
    +        'p2wpkh-p2sh': 0x049d7cb2,  # ypub
    +        'p2wsh-p2sh':  0x0295b43f,  # Ypub
    +        'p2wpkh':      0x04b24746,  # zpub
    +        'p2wsh':       0x02aa7ed3,  # Zpub
    +    }
    +
    +
    +class BitcoinTestnet:
    +
    +    TESTNET = True
    +    WIF_PREFIX = 0xef
    +    ADDRTYPE_P2PKH = 111
    +    ADDRTYPE_P2SH = 196
    +    SEGWIT_HRP = "tb"
    +    GENESIS = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
    +    DEFAULT_PORTS = {'t': '51001', 's': '51002'}
    +    DEFAULT_SERVERS = read_json('servers_testnet.json', {})
    +    CHECKPOINTS = read_json('checkpoints_testnet.json', [])
    +
    +    XPRV_HEADERS = {
    +        'standard':    0x04358394,  # tprv
    +        'p2wpkh-p2sh': 0x044a4e28,  # uprv
    +        'p2wsh-p2sh':  0x024285b5,  # Uprv
    +        'p2wpkh':      0x045f18bc,  # vprv
    +        'p2wsh':       0x02575048,  # Vprv
    +    }
    +    XPUB_HEADERS = {
    +        'standard':    0x043587cf,  # tpub
    +        'p2wpkh-p2sh': 0x044a5262,  # upub
    +        'p2wsh-p2sh':  0x024285ef,  # Upub
    +        'p2wpkh':      0x045f1cf6,  # vpub
    +        'p2wsh':       0x02575483,  # Vpub
    +    }
    +
    +
    +# don't import net directly, import the module instead (so that net is singleton)
    +net = BitcoinMainnet
    +
    +
    +def set_mainnet():
    +    global net
    +    net = BitcoinMainnet
    +
    +
    +def set_testnet():
    +    global net
    +    net = BitcoinTestnet
    diff --git a/lib/keystore.py b/lib/keystore.py
    index 011602ec4c93..676db247d25c 100644
    --- a/lib/keystore.py
    +++ b/lib/keystore.py
    @@ -28,7 +28,7 @@
     
     from . import bitcoin
     from .bitcoin import *
    -
    +from . import constants
     from .util import PrintError, InvalidPassword, hfu
     from .mnemonic import Mnemonic, load_wordlist
     from .plugins import run_hook
    @@ -688,7 +688,7 @@ def is_private_key_list(text):
     
     
     def bip44_derivation(account_id, bip43_purpose=44):
    -    coin = 1 if bitcoin.NetworkConstants.TESTNET else 0
    +    coin = 1 if constants.net.TESTNET else 0
         return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
     
     def from_seed(seed, passphrase, is_p2sh):
    diff --git a/lib/network.py b/lib/network.py
    index 7d00d9c64e98..b803a7820e28 100644
    --- a/lib/network.py
    +++ b/lib/network.py
    @@ -37,6 +37,7 @@
     from . import util
     from . import bitcoin
     from .bitcoin import *
    +from . import constants
     from .interface import Connection, Interface
     from . import blockchain
     from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
    @@ -60,7 +61,7 @@ def parse_servers(result):
                 for v in item[2]:
                     if re.match("[st]\d*", v):
                         protocol, port = v[0], v[1:]
    -                    if port == '': port = bitcoin.NetworkConstants.DEFAULT_PORTS[protocol]
    +                    if port == '': port = constants.net.DEFAULT_PORTS[protocol]
                         out[protocol] = port
                     elif re.match("v(.?)+", v):
                         version = v[1:]
    @@ -94,7 +95,7 @@ def filter_protocol(hostmap, protocol = 's'):
     
     def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()):
         if hostmap is None:
    -        hostmap = bitcoin.NetworkConstants.DEFAULT_SERVERS
    +        hostmap = constants.net.DEFAULT_SERVERS
         eligible = list(set(filter_protocol(hostmap, protocol)) - exclude_set)
         return random.choice(eligible) if eligible else None
     
    @@ -364,7 +365,7 @@ def get_interfaces(self):
             return list(self.interfaces.keys())
     
         def get_servers(self):
    -        out = bitcoin.NetworkConstants.DEFAULT_SERVERS
    +        out = constants.net.DEFAULT_SERVERS
             if self.irc_servers:
                 out.update(filter_version(self.irc_servers.copy()))
             else:
    @@ -967,7 +968,7 @@ def wait_on_sockets(self):
         def init_headers_file(self):
             b = self.blockchains[0]
             filename = b.path()
    -        length = 80 * len(bitcoin.NetworkConstants.CHECKPOINTS) * 2016
    +        length = 80 * len(constants.net.CHECKPOINTS) * 2016
             if not os.path.exists(filename) or os.path.getsize(filename) < length:
                 with open(filename, 'wb') as f:
                     if length>0:
    @@ -1092,4 +1093,4 @@ def export_checkpoints(self, path):
                 f.write(json.dumps(cp, indent=4))
     
         def max_checkpoint(self):
    -        return max(0, len(bitcoin.NetworkConstants.CHECKPOINTS) * 2016 - 1)
    +        return max(0, len(constants.net.CHECKPOINTS) * 2016 - 1)
    diff --git a/lib/tests/test_bitcoin.py b/lib/tests/test_bitcoin.py
    index 03f1d00f0a4f..a5f734357f6e 100644
    --- a/lib/tests/test_bitcoin.py
    +++ b/lib/tests/test_bitcoin.py
    @@ -11,8 +11,9 @@
         var_int, op_push, address_to_script, regenerate_key,
         verify_message, deserialize_privkey, serialize_privkey, is_segwit_address,
         is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub,
    -    xpub_type, is_xprv, is_bip32_derivation, seed_type, NetworkConstants)
    +    xpub_type, is_xprv, is_bip32_derivation, seed_type)
     from lib.util import bfh
    +from lib import constants
     
     try:
         import ecdsa
    @@ -168,12 +169,12 @@ class Test_bitcoin_testnet(unittest.TestCase):
         @classmethod
         def setUpClass(cls):
             super().setUpClass()
    -        NetworkConstants.set_testnet()
    +        constants.set_testnet()
     
         @classmethod
         def tearDownClass(cls):
             super().tearDownClass()
    -        NetworkConstants.set_mainnet()
    +        constants.set_mainnet()
     
         def test_address_to_script(self):
             # bech32 native segwit
    diff --git a/lib/util.py b/lib/util.py
    index 68d127f851a5..1714c78d4406 100644
    --- a/lib/util.py
    +++ b/lib/util.py
    @@ -530,8 +530,8 @@ def time_difference(distance_in_time, include_seconds):
     }
     
     def block_explorer_info():
    -    from . import bitcoin
    -    return testnet_block_explorers if bitcoin.NetworkConstants.TESTNET else mainnet_block_explorers
    +    from . import constants
    +    return testnet_block_explorers if constants.net.TESTNET else mainnet_block_explorers
     
     def block_explorer(config):
         return config.get('block_explorer', 'Blocktrail.com')
    diff --git a/plugins/digitalbitbox/digitalbitbox.py b/plugins/digitalbitbox/digitalbitbox.py
    index 2f63fda4d1cb..623290543a80 100644
    --- a/plugins/digitalbitbox/digitalbitbox.py
    +++ b/plugins/digitalbitbox/digitalbitbox.py
    @@ -7,6 +7,7 @@
         import electrum
         from electrum.bitcoin import TYPE_ADDRESS, push_script, var_int, msg_magic, Hash, verify_message, pubkey_from_signature, point_to_ser, public_key_to_p2pkh, EncodeAES, DecodeAES, MyVerifyingKey
         from electrum.bitcoin import serialize_xpub, deserialize_xpub
    +    from electrum import constants
         from electrum.transaction import Transaction
         from electrum.i18n import _
         from electrum.keystore import Hardware_KeyStore
    @@ -92,10 +93,10 @@ def get_xpub(self, bip32_path, xtype):
             if reply:
                 xpub = reply['xpub']
                 # Change type of xpub to the requested type. The firmware
    -            # only ever returns the standard type, but it is agnostic
    +            # only ever returns the mainnet standard type, but it is agnostic
                 # to the type when signing.
    -            if xtype != 'standard':
    -                _, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
    +            if xtype != 'standard' or constants.net.TESTNET:
    +                _, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub, net=constants.BitcoinMainnet)
                     xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
                 return xpub
             else:
    diff --git a/plugins/keepkey/plugin.py b/plugins/keepkey/plugin.py
    index acbad49c092b..a81a328bd5cc 100644
    --- a/plugins/keepkey/plugin.py
    +++ b/plugins/keepkey/plugin.py
    @@ -4,8 +4,9 @@
     
     from electrum.util import bfh, bh2u
     from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey,
    -                              TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants,
    +                              TYPE_ADDRESS, TYPE_SCRIPT,
                                   is_segwit_address)
    +from electrum import constants
     from electrum.i18n import _
     from electrum.plugins import BasePlugin
     from electrum.transaction import deserialize
    @@ -139,7 +140,7 @@ def get_client(self, keystore, force_pair=True):
             return client
     
         def get_coin_name(self):
    -        return "Testnet" if NetworkConstants.TESTNET else "Bitcoin"
    +        return "Testnet" if constants.net.TESTNET else "Bitcoin"
     
         def initialize_device(self, device_id, wizard, handler):
             # Initialization method
    @@ -344,9 +345,9 @@ def tx_outputs(self, derivation, tx, segwit=False):
                             txoutputtype.script_type = self.types.PAYTOWITNESS
                         else:
                             addrtype, hash_160 = b58_address_to_hash160(address)
    -                        if addrtype == NetworkConstants.ADDRTYPE_P2PKH:
    +                        if addrtype == constants.net.ADDRTYPE_P2PKH:
                                 txoutputtype.script_type = self.types.PAYTOADDRESS
    -                        elif addrtype == NetworkConstants.ADDRTYPE_P2SH:
    +                        elif addrtype == constants.net.ADDRTYPE_P2SH:
                                 txoutputtype.script_type = self.types.PAYTOSCRIPTHASH
                             else:
                                 raise BaseException('addrtype: ' + str(addrtype))
    diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py
    index 512168c18e8b..322c2da31f44 100644
    --- a/plugins/trezor/trezor.py
    +++ b/plugins/trezor/trezor.py
    @@ -4,7 +4,8 @@
     
     from electrum.util import bfh, bh2u, versiontuple
     from electrum.bitcoin import (b58_address_to_hash160, xpub_from_pubkey,
    -                              TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants)
    +                              TYPE_ADDRESS, TYPE_SCRIPT)
    +from electrum import constants
     from electrum.i18n import _
     from electrum.plugins import BasePlugin, Device
     from electrum.transaction import deserialize
    @@ -173,7 +174,7 @@ def get_client(self, keystore, force_pair=True):
             return client
     
         def get_coin_name(self):
    -        return "Testnet" if NetworkConstants.TESTNET else "Bitcoin"
    +        return "Testnet" if constants.net.TESTNET else "Bitcoin"
     
         def initialize_device(self, device_id, wizard, handler):
             # Initialization method
    diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py
    index a6a008c45485..b8e832e668ad 100644
    --- a/plugins/trustedcoin/trustedcoin.py
    +++ b/plugins/trustedcoin/trustedcoin.py
    @@ -32,6 +32,7 @@
     
     import electrum
     from electrum import bitcoin
    +from electrum import constants
     from electrum import keystore
     from electrum.bitcoin import *
     from electrum.mnemonic import Mnemonic
    @@ -44,13 +45,13 @@
     
     # signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server
     def get_signing_xpub():
    -    if NetworkConstants.TESTNET:
    +    if constants.net.TESTNET:
             return "tpubD6NzVbkrYhZ4XdmyJQcCPjQfg6RXVUzGFhPjZ7uvRC8JLcS7Hw1i7UTpyhp9grHpak4TyK2hzBJrujDVLXQ6qB5tNpVx9rC6ixijUXadnmY"
         else:
             return "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL"
     
     def get_billing_xpub():
    -    if NetworkConstants.TESTNET:
    +    if constants.net.TESTNET:
             return "tpubD6NzVbkrYhZ4X11EJFTJujsYbUmVASAYY7gXsEt4sL97AMBdypiH1E9ZVTpdXXEy3Kj9Eqd1UkxdGtvDt5z23DKsh6211CfNJo8bLLyem5r"
         else:
             return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU"
    
    From c9b0840c7879db73a5fed5fb4ab0b4b086d905d4 Mon Sep 17 00:00:00 2001
    From: ThomasV 
    Date: Sun, 4 Mar 2018 23:58:38 +0100
    Subject: [PATCH 254/270] kivy: minor fixes related to exchange rates
    
    ---
     gui/kivy/main_window.py               | 6 ++++--
     gui/kivy/uix/dialogs/amount_dialog.py | 1 +
     gui/kivy/uix/dialogs/fx_dialog.py     | 2 ++
     gui/kivy/uix/screens.py               | 2 ++
     gui/kivy/uix/ui_screens/history.kv    | 2 +-
     5 files changed, 10 insertions(+), 3 deletions(-)
    
    diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py
    index 9261e8b5e27b..7ca40bf8d0c4 100644
    --- a/gui/kivy/main_window.py
    +++ b/gui/kivy/main_window.py
    @@ -179,8 +179,10 @@ def decimal_point(self):
         def btc_to_fiat(self, amount_str):
             if not amount_str:
                 return ''
    +        if not self.fx.is_enabled():
    +            return ''
             rate = self.fx.exchange_rate()
    -        if not rate:
    +        if rate.is_nan():
                 return ''
             fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8)
             return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')
    @@ -189,7 +191,7 @@ def fiat_to_btc(self, fiat_amount):
             if not fiat_amount:
                 return ''
             rate = self.fx.exchange_rate()
    -        if not rate:
    +        if rate.is_nan():
                 return ''
             satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate))
             return format_satoshis_plain(satoshis, self.decimal_point())
    diff --git a/gui/kivy/uix/dialogs/amount_dialog.py b/gui/kivy/uix/dialogs/amount_dialog.py
    index 9a825d79952b..244f8e61df03 100644
    --- a/gui/kivy/uix/dialogs/amount_dialog.py
    +++ b/gui/kivy/uix/dialogs/amount_dialog.py
    @@ -39,6 +39,7 @@
                         size_hint: 1, None
                         font_size: '20dp'
                         height: '48dp'
    +                    disabled: not app.fx.is_enabled()
                         on_release:
                             kb.is_fiat = True
                 Widget:
    diff --git a/gui/kivy/uix/dialogs/fx_dialog.py b/gui/kivy/uix/dialogs/fx_dialog.py
    index 5184679206c1..35a34be95c87 100644
    --- a/gui/kivy/uix/dialogs/fx_dialog.py
    +++ b/gui/kivy/uix/dialogs/fx_dialog.py
    @@ -106,4 +106,6 @@ def on_currency(self, ccy):
                 if ccy != self.fx.get_currency():
                     self.fx.set_currency(ccy)
                 self.app.fiat_unit = ccy
    +        else:
    +            self.app.is_fiat = False
             Clock.schedule_once(lambda dt: self.add_exchanges())
    diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py
    index eb9cf42110a8..5664d439d6f5 100644
    --- a/gui/kivy/uix/screens.py
    +++ b/gui/kivy/uix/screens.py
    @@ -373,6 +373,8 @@ def do_copy(self):
     
         def save_request(self):
             addr = self.screen.address
    +        if not addr:
    +            return
             amount = self.screen.amount
             message = self.screen.message
             amount = self.app.get_amount(amount) if amount else 0
    diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv
    index 524b53197940..3b45408032ef 100644
    --- a/gui/kivy/uix/ui_screens/history.kv
    +++ b/gui/kivy/uix/ui_screens/history.kv
    @@ -63,7 +63,7 @@ HistoryScreen:
                 font_size: '30dp'
                 bold: True
                 size_hint: 1, 0.25
    -            on_release: app.is_fiat = not app.is_fiat
    +            on_release: app.is_fiat = not app.is_fiat if app.fx.is_enabled() else False
             ScrollView:
                 id: content
                 do_scroll_x: False
    
    From 219ea6588bb20e24d88c9406c892d57293afb951 Mon Sep 17 00:00:00 2001
    From: SomberNight 
    Date: Mon, 5 Mar 2018 06:00:24 +0100
    Subject: [PATCH 255/270] update default server list
    
    ---
     lib/servers.json         | 234 ++++++++++++++++++++++++++-------------
     lib/servers_testnet.json |  35 +++++-
     2 files changed, 185 insertions(+), 84 deletions(-)
    
    diff --git a/lib/servers.json b/lib/servers.json
    index 1f6398584f18..5f43bce146ec 100644
    --- a/lib/servers.json
    +++ b/lib/servers.json
    @@ -1,231 +1,309 @@
     {
    -    "E-X.not.fyi": {
    +    "207.154.223.80": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "ELECTRUMX.not.fyi": {
    +    "4cii7ryno5j3axe4.onion": {
    +        "pruning": "-",
    +        "t": "50001",
    +        "version": "1.2"
    +    },
    +    "74.222.1.20": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "ELEX01.blackpole.online": {
    +    "88.198.43.231": {
    +        "pruning": "-",
    +        "s": "50002",
    +        "t": "50001",
    +        "version": "1.2"
    +    },
    +    "E-X.not.fyi": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
         "VPS.hsmiths.com": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "bitcoin.freedomnode.com": {
    +    "arihancckjge66iv.onion": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
    +    },
    +    "aspinall.io": {
    +        "pruning": "-",
    +        "s": "50002",
    +        "version": "1.2"
    +    },
    +    "bauerjda5hnedjam.onion": {
    +        "pruning": "-",
    +        "s": "50002",
    +        "t": "50001",
    +        "version": "1.2"
    +    },
    +    "bauerjhejlv6di7s.onion": {
    +        "pruning": "-",
    +        "s": "50002",
    +        "t": "50001",
    +        "version": "1.2"
    +    },
    +    "btc.asis.io": {
    +        "pruning": "-",
    +        "s": "50002",
    +        "t": "50001",
    +        "version": "1.2"
    +    },
    +    "btc.cihar.com": {
    +        "pruning": "-",
    +        "s": "50002",
    +        "t": "50001",
    +        "version": "1.2"
         },
         "btc.smsys.me": {
             "pruning": "-",
             "s": "995",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "currentlane.lovebitco.in": {
    +    "cryptohead.de": {
             "pruning": "-",
    -        "t": "50001",
    -        "version": "1.1"
    +        "s": "50002",
    +        "version": "1.2"
         },
         "daedalus.bauerj.eu": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "de01.hamster.science": {
    +    "de.hamster.science": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "ecdsa.net": {
    +    "e.keff.org": {
             "pruning": "-",
    -        "s": "110",
    +        "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
         "elec.luggs.co": {
             "pruning": "-",
             "s": "443",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "electrum.akinbo.org": {
    +    "electrum-server.ninja": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "electrum.antumbra.se": {
    +    "electrum.achow101.com": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "electrum.be": {
    +    "electrum.cutie.ga": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "electrum.coinucopia.io": {
    +    "electrum.hsmiths.com": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "electrum.cutie.ga": {
    +    "electrum.leblancnet.us": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "electrum.festivaldelhumor.org": {
    +    "electrum.meltingice.net": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "electrum.hsmiths.com": {
    +    "electrum.nute.net": {
    +        "pruning": "-",
    +        "s": "50002",
    +        "version": "1.2"
    +    },
    +    "electrum.poorcoding.com": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
         "electrum.qtornado.com": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
         "electrum.vom-stausee.de": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "electrum3.hachre.de": {
    +    "electrum0.snel.it": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
    +    },
    +    "electrumx-core.1209k.com": {
    +        "pruning": "-",
    +        "s": "50002",
    +        "t": "50001",
    +        "version": "1.2"
         },
         "electrumx.bot.nu": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "electrumx.westeurope.cloudapp.azure.com": {
    +    "electrumx.nmdps.net": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "elx01.knas.systems": {
    +    "electrumx.westeurope.cloudapp.azure.com": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "ex-btc.server-on.net": {
    +    "electrumxhqdsmlu.onion": {
    +        "pruning": "-",
    +        "t": "50001",
    +        "version": "1.2"
    +    },
    +    "elx2018.mooo.com": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
         "helicarrier.bauerj.eu": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "mooo.not.fyi": {
    +    "hsmiths4fyqlw5xw.onion": {
             "pruning": "-",
    -        "s": "50012",
    -        "t": "50011",
    -        "version": "1.1"
    +        "s": "50002",
    +        "t": "50001",
    +        "version": "1.2"
         },
    -    "ndnd.selfhost.eu": {
    +    "hsmiths5mjk6uijs.onion": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "node.arihanc.com": {
    +    "j5jfrdthqt5g25xz.onion": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "node.xbt.eu": {
    +    "kirsche.emzy.de": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "node1.volatilevictory.com": {
    +    "luggscoqbymhvnkp.onion": {
    +        "pruning": "-",
    +        "t": "80",
    +        "version": "1.2"
    +    },
    +    "ndnd.selfhost.eu": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "noserver4u.de": {
    +    "ndndword5lpb7eex.onion": {
    +        "pruning": "-",
    +        "t": "50001",
    +        "version": "1.2"
    +    },
    +    "node.arihanc.com": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "qmebr.spdns.org": {
    +    "node.erratic.space": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "raspi.hsmiths.com": {
    +    "ozahtqwp25chjdjd.onion": {
             "pruning": "-",
    -        "s": "51002",
    -        "t": "51001",
    -        "version": "1.1"
    +        "s": "50002",
    +        "t": "50001",
    +        "version": "1.2"
         },
    -    "s2.noip.pl": {
    +    "qtornadoklbgdyww.onion": {
             "pruning": "-",
    -        "s": "50102",
    -        "version": "1.1"
    +        "s": "50002",
    +        "t": "50001",
    +        "version": "1.2"
         },
    -    "s5.noip.pl": {
    +    "rbx.curalle.ovh": {
             "pruning": "-",
    -        "s": "50105",
    -        "version": "1.1"
    +        "s": "50002",
    +        "version": "1.2"
    +    },
    +    "ruuxwv74pjxms3ws.onion": {
    +        "pruning": "-",
    +        "s": "10042",
    +        "t": "50001",
    +        "version": "1.2"
    +    },
    +    "s7clinmo4cazmhul.onion": {
    +        "pruning": "-",
    +        "t": "50001",
    +        "version": "1.2"
         },
         "songbird.bauerj.eu": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         },
    -    "us.electrum.be": {
    +    "spv.48.org": {
             "pruning": "-",
             "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    +        "t": "50003",
    +        "version": "1.2"
         },
    -    "us01.hamster.science": {
    +    "tardis.bauerj.eu": {
             "pruning": "-",
             "s": "50002",
             "t": "50001",
    -        "version": "1.1"
    +        "version": "1.2"
         }
     }
    diff --git a/lib/servers_testnet.json b/lib/servers_testnet.json
    index 4ec92007f2fe..4ff6d2302b68 100644
    --- a/lib/servers_testnet.json
    +++ b/lib/servers_testnet.json
    @@ -1,8 +1,31 @@
     {
    -        "testnetnode.arihanc.com": {"t":"51001", "s":"51002"},
    -        "testnet1.bauerj.eu": {"t":"51001", "s":"51002"},
    -        "14.3.140.101": {"t":"51001", "s":"51002"},
    -        "testnet.hsmiths.com": {"t":"53011", "s":"53012"},
    -        "electrum.akinbo.org": {"t":"51001", "s":"51002"},
    -        "ELEX05.blackpole.online": {"t":"52011", "s":"52002"}
    +    "electrumx.kekku.li": {
    +        "pruning": "-",
    +        "s": "51002",
    +        "version": "1.2"
    +    },
    +    "hsmithsxurybd7uh.onion": {
    +        "pruning": "-",
    +        "s": "53012",
    +        "t": "53011",
    +        "version": "1.2"
    +    },
    +    "testnet.hsmiths.com": {
    +        "pruning": "-",
    +        "s": "53012",
    +        "t": "53011",
    +        "version": "1.2"
    +    },
    +    "testnet.qtornado.com": {
    +        "pruning": "-",
    +        "s": "51002",
    +        "t": "51001",
    +        "version": "1.2"
    +    },
    +    "testnet1.bauerj.eu": {
    +        "pruning": "-",
    +        "s": "50002",
    +        "t": "50001",
    +        "version": "1.2"
    +    }
     }
    
    From 2b5117ab3dfa8213f303f80771ca19afe7249ca9 Mon Sep 17 00:00:00 2001
    From: ThomasV 
    Date: Mon, 5 Mar 2018 09:37:25 +0100
    Subject: [PATCH 256/270] kivy: fix tx dialog message
    
    ---
     gui/kivy/uix/dialogs/tx_dialog.py | 8 ++++++--
     1 file changed, 6 insertions(+), 2 deletions(-)
    
    diff --git a/gui/kivy/uix/dialogs/tx_dialog.py b/gui/kivy/uix/dialogs/tx_dialog.py
    index 5339b0d39c01..fbbdf0a276ad 100644
    --- a/gui/kivy/uix/dialogs/tx_dialog.py
    +++ b/gui/kivy/uix/dialogs/tx_dialog.py
    @@ -20,6 +20,7 @@
         can_rbf: False
         fee_str: ''
         date_str: ''
    +    date_label:''
         amount_str: ''
         tx_hash: ''
         status_str: ''
    @@ -46,7 +47,7 @@
                             text: _('Description') if root.description else ''
                             value: root.description
                         BoxLabel:
    -                        text: _('Date') if root.date_str else ''
    +                        text: root.date_label
                             value: root.date_str
                         BoxLabel:
                             text: _('Amount sent') if root.is_mine else _('Amount received')
    @@ -110,10 +111,13 @@ def update(self):
             tx_hash, self.status_str, self.description, self.can_broadcast, self.can_rbf, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx)
             self.tx_hash = tx_hash or ''
             if timestamp:
    +            self.date_label = _('Date')
                 self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
             elif exp_n:
    -            self.date_str = _('Within {} blocks').format(exp_n) if exp_n > 0 else _('unknown (low fee)')
    +            self.date_label = _('Mempool depth')
    +            self.date_str = _('{} from tip').format('%.2f MB'%(exp_n/1000000))
             else:
    +            self.date_label = ''
                 self.date_str = ''
     
             if amount is None:
    
    From e537b58d109155cb7eaa8ae2137e1f4ca08dfa15 Mon Sep 17 00:00:00 2001
    From: SomberNight 
    Date: Mon, 5 Mar 2018 09:39:55 +0100
    Subject: [PATCH 257/270] release notes: 3.1 - more stuff
    
    ---
     RELEASE-NOTES | 24 ++++++++++++++++++++++--
     1 file changed, 22 insertions(+), 2 deletions(-)
    
    diff --git a/RELEASE-NOTES b/RELEASE-NOTES
    index 0ed9b70f2166..f20ce14e0455 100644
    --- a/RELEASE-NOTES
    +++ b/RELEASE-NOTES
    @@ -23,7 +23,7 @@
        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
    +   retargeting period. The headers for a retargeting 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
    @@ -43,13 +43,33 @@
        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
    +   values 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.
    + * A new version of the Electrum protocol is required by the client
    +   (version 1.2). Servers using older versions of the protocol will
    +   not be displayed in the GUI.
    + * The Trezor T hardware wallet is now supported.
    + * BIP84: native segwit p2wpkh scripts for bip39 seeds and hardware
    +   wallets can now be created when specifying a BIP84 derivation
    +   path. This is usable with Trezor and Ledger.
    + * Windows: the binaries now include ZBar, and QR code scanning should work.
    + * The Wallet Import Format (WIF) for private keys that was extended in 3.0
    +   is changed. Keys in the previous format can be imported, compatibility
    +   is maintained. Newly exported keys will be serialized as
    +   "script_type:original_wif_format_key".
    + * BIP32 master keys for testnet once again have different version bytes than
    +   on mainnet. For the mainnet prefixes {x,y,Y,z,Z}|{pub|prv}, the
    +   corresponding testnet prefixes are   {t,u,U,v,V}|{pub|prv}.
    +   More details and exact version bytes are specified at:
    +   https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst
    +   Note that due to this change, testnet wallet files created with previous
    +   versions of Electrum must be considered broken, and they need to be
    +   recreated from seed words.
     
     
     # Release 3.0.6 :
    
    From fd4142703c22ad08e849f2e40f8f17fd5a855488 Mon Sep 17 00:00:00 2001
    From: SomberNight 
    Date: Mon, 5 Mar 2018 09:43:45 +0100
    Subject: [PATCH 258/270] typo in prev
    
    ---
     RELEASE-NOTES | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/RELEASE-NOTES b/RELEASE-NOTES
    index f20ce14e0455..25b94d5f7d83 100644
    --- a/RELEASE-NOTES
    +++ b/RELEASE-NOTES
    @@ -63,8 +63,8 @@
        is maintained. Newly exported keys will be serialized as
        "script_type:original_wif_format_key".
      * BIP32 master keys for testnet once again have different version bytes than
    -   on mainnet. For the mainnet prefixes {x,y,Y,z,Z}|{pub|prv}, the
    -   corresponding testnet prefixes are   {t,u,U,v,V}|{pub|prv}.
    +   on mainnet. For the mainnet prefixes {x,y,Y,z,Z}|{pub,prv}, the
    +   corresponding testnet prefixes are   {t,u,U,v,V}|{pub,prv}.
        More details and exact version bytes are specified at:
        https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst
        Note that due to this change, testnet wallet files created with previous
    
    From 0bca97dd2f8913f70c3762ae52b9469a9edbfba5 Mon Sep 17 00:00:00 2001
    From: ThomasV 
    Date: Mon, 5 Mar 2018 09:49:17 +0100
    Subject: [PATCH 259/270] improve fee status message
    
    ---
     lib/simple_config.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/lib/simple_config.py b/lib/simple_config.py
    index aaaa519697d2..23bad22ad0e3 100644
    --- a/lib/simple_config.py
    +++ b/lib/simple_config.py
    @@ -348,7 +348,7 @@ def get_fee_status(self):
             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)
    -        return target + '  [%s]'%tooltip
    +        return tooltip + '  [%s]'%target if dyn else target + '  [Static]'
     
         def get_fee_text(self, pos, dyn, mempool, fee_rate):
             """Returns (text, tooltip) where
    
    From 89599cf0d53f1540243be69d30fe473c24f236e1 Mon Sep 17 00:00:00 2001
    From: ThomasV 
    Date: Mon, 5 Mar 2018 10:12:44 +0100
    Subject: [PATCH 260/270] simplify fee preferences dialog
    
    ---
     gui/qt/main_window.py | 14 +++-----------
     1 file changed, 3 insertions(+), 11 deletions(-)
    
    diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
    index f46d4ba71572..d2d318426f9a 100644
    --- a/gui/qt/main_window.py
    +++ b/gui/qt/main_window.py
    @@ -2626,23 +2626,15 @@ def on_nz():
             )
             fee_type_label = HelpLabel(_('Fee estimation') + ':', msg)
             fee_type_combo = QComboBox()
    -        fee_type_combo.addItems([_('Time based'), _('Mempool based')])
    +        fee_type_combo.addItems([_('Static'), _('ETA'), _('Mempool')])
             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.config.set_key('mempool_fees', x==2)
    +            self.config.set_key('dynamic_fees', x>0)
                 self.fee_slider.update()
             fee_type_combo.currentIndexChanged.connect(on_fee_type)
             fee_widgets.append((fee_type_label, fee_type_combo))
     
    -        def on_dynfee(x):
    -            self.config.set_key('dynamic_fees', x == Qt.Checked)
    -            self.fee_slider.update()
    -        dynfee_cb = QCheckBox(_('Use dynamic fees'))
    -        dynfee_cb.setChecked(self.config.is_dynfee())
    -        dynfee_cb.setToolTip(_("Use fees recommended by the server."))
    -        fee_widgets.append((dynfee_cb, None))
    -        dynfee_cb.stateChanged.connect(on_dynfee)
    -
             feebox_cb = QCheckBox(_('Edit fees manually'))
             feebox_cb.setChecked(self.config.get('show_fee', False))
             feebox_cb.setToolTip(_("Show fee edit box in send tab."))
    
    From e971bd849868122135012b836ece86aeb591efb7 Mon Sep 17 00:00:00 2001
    From: ThomasV 
    Date: Mon, 5 Mar 2018 10:39:29 +0100
    Subject: [PATCH 261/270] update release notes, add date
    
    ---
     RELEASE-NOTES | 58 +++++++++++++++++++++++++--------------------------
     1 file changed, 29 insertions(+), 29 deletions(-)
    
    diff --git a/RELEASE-NOTES b/RELEASE-NOTES
    index 25b94d5f7d83..258efb5d2ce7 100644
    --- a/RELEASE-NOTES
    +++ b/RELEASE-NOTES
    @@ -1,19 +1,30 @@
     
    -# Release 3.1 - (to be released)
    -
    - * 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.
    +# Release 3.1 - (March 5, 2018)
    +
    + * Memory-pool based fee estimation. Dynamic fees can target a desired
    +   depth in the memory pool. This feature is optional, and ETA-based
    +   estimates from Bitcoin Core are still available. 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. In addition, the client uses a hard threshold to
    +   protect itself from servers sending excessive fee estimates. 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.
    + * 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
    +   values 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 presence of other addresses in the wallet.
      * 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
    @@ -39,20 +50,6 @@
      * 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
    -   values 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.
    - * A new version of the Electrum protocol is required by the client
    -   (version 1.2). Servers using older versions of the protocol will
    -   not be displayed in the GUI.
      * The Trezor T hardware wallet is now supported.
      * BIP84: native segwit p2wpkh scripts for bip39 seeds and hardware
        wallets can now be created when specifying a BIP84 derivation
    @@ -70,6 +67,9 @@
        Note that due to this change, testnet wallet files created with previous
        versions of Electrum must be considered broken, and they need to be
        recreated from seed words.
    + * A new version of the Electrum protocol is required by the client
    +   (version 1.2). Servers using older versions of the protocol will
    +   not be displayed in the GUI.
     
     
     # Release 3.0.6 :
    
    From 81666bf9ac8b42d0ac25415ee3815d899c8adda6 Mon Sep 17 00:00:00 2001
    From: SomberNight 
    Date: Mon, 5 Mar 2018 11:44:03 +0100
    Subject: [PATCH 262/270] fix #4026
    
    ---
     lib/synchronizer.py | 6 ++++++
     lib/verifier.py     | 5 ++++-
     lib/wallet.py       | 3 ++-
     3 files changed, 12 insertions(+), 2 deletions(-)
    
    diff --git a/lib/synchronizer.py b/lib/synchronizer.py
    index 70e13e529934..4b81810db14f 100644
    --- a/lib/synchronizer.py
    +++ b/lib/synchronizer.py
    @@ -84,6 +84,8 @@ def get_status(self, h):
             return bh2u(hashlib.sha256(status.encode('ascii')).digest())
     
         def on_address_status(self, response):
    +        if self.wallet.synchronizer is None:
    +            return  # we have been killed, this was just an orphan callback
             params, result = self.parse_response(response)
             if not params:
                 return
    @@ -98,6 +100,8 @@ def on_address_status(self, response):
                 self.requested_addrs.remove(addr)
     
         def on_address_history(self, response):
    +        if self.wallet.synchronizer is None:
    +            return  # we have been killed, this was just an orphan callback
             params, result = self.parse_response(response)
             if not params:
                 return
    @@ -127,6 +131,8 @@ def on_address_history(self, response):
             self.requested_histories.pop(addr)
     
         def tx_response(self, response):
    +        if self.wallet.synchronizer is None:
    +            return  # we have been killed, this was just an orphan callback
             params, result = self.parse_response(response)
             if not params:
                 return
    diff --git a/lib/verifier.py b/lib/verifier.py
    index c2d0f1250907..236ffbfe6597 100644
    --- a/lib/verifier.py
    +++ b/lib/verifier.py
    @@ -65,6 +65,8 @@ def run(self):
                 self.undo_verifications()
     
         def verify_merkle(self, r):
    +        if self.wallet.verifier is None:
    +            return  # we have been killed, this was just an orphan callback
             if r.get('error'):
                 self.print_error('received an error:', r)
                 return
    @@ -95,7 +97,8 @@ def verify_merkle(self, r):
             self.print_error("verified %s" % tx_hash)
             self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos))
     
    -    def hash_merkle_root(self, merkle_s, target_hash, pos):
    +    @classmethod
    +    def hash_merkle_root(cls, merkle_s, target_hash, pos):
             h = hash_decode(target_hash)
             for i in range(len(merkle_s)):
                 item = merkle_s[i]
    diff --git a/lib/wallet.py b/lib/wallet.py
    index 291d63db3010..28597b8519ac 100644
    --- a/lib/wallet.py
    +++ b/lib/wallet.py
    @@ -912,7 +912,8 @@ def receive_history_callback(self, addr, hist, tx_fees):
                         # 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)
    +                    if self.verifier:
    +                        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
    
    From 122cb08dc6f9c7b649283725f871a7d2a14ce418 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=E8=84=87=E5=B1=B1P?= 
    Date: Mon, 5 Mar 2018 20:08:10 +0900
    Subject: [PATCH 263/270] Add JPY exchange rate (#4016)
    
    ---
     lib/currencies.json  | 13 +++++++++++--
     lib/exchange_rate.py | 20 ++++++++++++++++++++
     2 files changed, 31 insertions(+), 2 deletions(-)
    
    diff --git a/lib/currencies.json b/lib/currencies.json
    index a4e85f1f6fc4..beb4951f5848 100644
    --- a/lib/currencies.json
    +++ b/lib/currencies.json
    @@ -798,5 +798,14 @@
             "RUB",
             "USD"
         ],
    -    "itBit": []
    -}
    \ No newline at end of file
    +     "itBit": [],
    +     "Bitbank": [
    +         "JPY"
    +     ],
    +     "BitFlyer": [
    +         "JPY"
    +     ],
    +     "Zaif": [
    +         "JPY"
    +     ]
    + }
    diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py
    index 02ed91b9c347..b6a965fe7910 100644
    --- a/lib/exchange_rate.py
    +++ b/lib/exchange_rate.py
    @@ -160,6 +160,20 @@ def request_history(self, ccy):
                                  "/historical/index.php?coin=BTC")[ccy +'_BTC']
     
     
    +class Bitbank(ExchangeBase):
    +
    +    def get_rates(self, ccy):
    +        json = self.get_json('public.bitbank.cc', '/btc_jpy/ticker')
    +        return {'JPY': Decimal(json['data']['last'])}
    +
    +
    +class BitFlyer(ExchangeBase):
    +
    +    def get_rates(self, ccy):
    +        json = self.get_json('bitflyer.jp', '/api/echo/price')
    +        return {'JPY': Decimal(json['mid'])}
    +
    +
     class Bitmarket(ExchangeBase):
     
         def get_rates(self, ccy):
    @@ -350,6 +364,12 @@ def request_history(self, ccy):
                          for h in history])
     
     
    +class Zaif(ExchangeBase):
    +    def get_rates(self, ccy):
    +        json = self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy')
    +        return {'JPY': Decimal(json['last_price'])}
    +
    +
     def dictinvert(d):
         inv = {}
         for k, vlist in d.items():
    
    From 2fdbfa6b8161a55a791fc24cc881f52915bccc4f Mon Sep 17 00:00:00 2001
    From: SomberNight 
    Date: Mon, 5 Mar 2018 12:58:03 +0100
    Subject: [PATCH 264/270] wizard: rename option to "use a master key"
    
    ---
     lib/base_wizard.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/lib/base_wizard.py b/lib/base_wizard.py
    index d4222b7f072e..793a96fb71d8 100644
    --- a/lib/base_wizard.py
    +++ b/lib/base_wizard.py
    @@ -131,7 +131,7 @@ def choose_keystore(self):
                 choices = [
                     ('choose_seed_type', _('Create a new seed')),
                     ('restore_from_seed', _('I already have a seed')),
    -                ('restore_from_key', _('Use public or private keys')),
    +                ('restore_from_key', _('Use a master key')),
                 ]
                 if not self.is_kivy:
                     choices.append(('choose_hw_device',  _('Use a hardware device')))
    
    From 2bde6867526f5c41b553d3aeff561edf3c826d4e Mon Sep 17 00:00:00 2001
    From: Johann Bauer 
    Date: Mon, 5 Mar 2018 17:15:38 +0100
    Subject: [PATCH 265/270] Windows Build: Create tmp dir
    
    ---
     contrib/build-wine/build-electrum-git.sh | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh
    index a46be1463b6c..2fd6ea007e27 100755
    --- a/contrib/build-wine/build-electrum-git.sh
    +++ b/contrib/build-wine/build-electrum-git.sh
    @@ -16,6 +16,7 @@ PYTHON="wine $PYHOME/python.exe -OO -B"
     cd `dirname $0`
     set -e
     
    +mkdir -p tmp
     cd tmp
     
     for repo in electrum electrum-locale electrum-icons; do
    
    From 1451c66a8f65e87555fce8a7111ddef898381598 Mon Sep 17 00:00:00 2001
    From: Johann Bauer 
    Date: Mon, 5 Mar 2018 23:07:52 +0100
    Subject: [PATCH 266/270] Check if value has 'is_nan' before calling it
    
    Fixes: #4034
    ---
     lib/util.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/lib/util.py b/lib/util.py
    index 1714c78d4406..9dbf21c858a3 100644
    --- a/lib/util.py
    +++ b/lib/util.py
    @@ -113,7 +113,7 @@ def __repr__(self):
             return 'Fiat(%s)'% self.__str__()
     
         def __str__(self):
    -        if self.value.is_nan():
    +        if hasattr(self.value, 'is_nan') and self.value.is_nan():
                 return _('No Data')
             else:
                 return "{:.2f}".format(self.value) + ' ' + self.ccy
    
    From 4c81a77cccbd4886ac70ff136ee3ef7d4c37ea92 Mon Sep 17 00:00:00 2001
    From: Johann Bauer 
    Date: Mon, 5 Mar 2018 23:24:12 +0100
    Subject: [PATCH 267/270] Revert "Check if value has 'is_nan' before calling
     it"
    
    This reverts commit 1451c66a8f65e87555fce8a7111ddef898381598.
    ---
     lib/util.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/lib/util.py b/lib/util.py
    index 9dbf21c858a3..1714c78d4406 100644
    --- a/lib/util.py
    +++ b/lib/util.py
    @@ -113,7 +113,7 @@ def __repr__(self):
             return 'Fiat(%s)'% self.__str__()
     
         def __str__(self):
    -        if hasattr(self.value, 'is_nan') and self.value.is_nan():
    +        if self.value.is_nan():
                 return _('No Data')
             else:
                 return "{:.2f}".format(self.value) + ' ' + self.ccy
    
    From 75c1b176816128603fe8c872d8b80b6185e36f20 Mon Sep 17 00:00:00 2001
    From: Jon Layton 
    Date: Mon, 5 Mar 2018 20:04:28 -0600
    Subject: [PATCH 268/270] Post-merge fixups, runs, doesnt connect
    
    ---
     lib/blockchain.py |  5 ++++-
     lib/constants.py  | 28 +++++++++++++++++++++-------
     lib/interface.py  |  4 +---
     lib/servers.json  |  4 ++--
     lib/x509.py       |  2 +-
     5 files changed, 29 insertions(+), 14 deletions(-)
    
    diff --git a/lib/blockchain.py b/lib/blockchain.py
    index cea971e675ec..784b56dd22b2 100644
    --- a/lib/blockchain.py
    +++ b/lib/blockchain.py
    @@ -84,7 +84,10 @@ def hash_header(header):
         if header is None:
             return '0' * 64
         if header.get('prev_block_hash') is None:
    -        header['prev_block_hash'] = '00'*64 ''' TODO 32? '''
    +        header['prev_block_hash'] = '00'*64
    +        '''
    +        TODO 32?
    +        '''
         return hash_encode(Hash(bfh(serialize_header(header))))
     
     
    diff --git a/lib/constants.py b/lib/constants.py
    index ec35cbe3e9b9..6b93e04b7ac1 100644
    --- a/lib/constants.py
    +++ b/lib/constants.py
    @@ -37,18 +37,26 @@ def read_json(filename, default):
         return r
     
     
    +
    +
     class BitcoinMainnet:
     
         TESTNET = False
         WIF_PREFIX = 0x80
    -    ADDRTYPE_P2PKH = 0
    -    ADDRTYPE_P2SH = 5
    -    SEGWIT_HRP = "bc"
    -    GENESIS = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
    +    ADDRTYPE_P2PKH = [0x1C, 0xB8] 
    +    ADDRTYPE_P2SH = [0x1C, 0xBD]
    +    ADDRTYPE_SHIELDED = [0x16, 0x9A]
    +    SEGWIT_HRP = "bc" # (No ZCL Segwit)
    +    GENESIS = "0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602"
         DEFAULT_PORTS = {'t': '50001', 's': '50002'}
         DEFAULT_SERVERS = read_json('servers.json', {})
         CHECKPOINTS = read_json('checkpoints.json', [])
     
    +    EQUIHASH_N = 200
    +    EQUIHASH_K = 9
    +
    +    CHUNK_SIZE = 200
    +
         XPRV_HEADERS = {
             'standard':    0x0488ade4,  # xprv
             'p2wpkh-p2sh': 0x049d7878,  # yprv
    @@ -69,14 +77,20 @@ class BitcoinTestnet:
     
         TESTNET = True
         WIF_PREFIX = 0xef
    -    ADDRTYPE_P2PKH = 111
    -    ADDRTYPE_P2SH = 196
    -    SEGWIT_HRP = "tb"
    +    ADDRTYPE_P2PKH = [0x1D, 0x25] 
    +    ADDRTYPE_P2SH = [0x1C, 0xBA] 
    +    ADDTYPE_SHIELDED = [0x16, 0xB6]
    +    SEGWIT_HRP = "tb" # (ZCL has no Segwit)
         GENESIS = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
         DEFAULT_PORTS = {'t': '51001', 's': '51002'}
         DEFAULT_SERVERS = read_json('servers_testnet.json', {})
         CHECKPOINTS = read_json('checkpoints_testnet.json', [])
     
    +    EQUIHASH_N = 200
    +    EQUIHASH_K = 9
    +
    +    CHUNK_SIZE = 200
    +
         XPRV_HEADERS = {
             'standard':    0x04358394,  # tprv
             'p2wpkh-p2sh': 0x044a4e28,  # uprv
    diff --git a/lib/interface.py b/lib/interface.py
    index 6a741bff77e9..813828266484 100644
    --- a/lib/interface.py
    +++ b/lib/interface.py
    @@ -31,9 +31,7 @@
     import time
     import traceback
     
    -from .util import print_error, get_cert_path
    -
    -ca_path = get_cert_path()
    +from .util import print_error
     
     from . import util
     from . import x509
    diff --git a/lib/servers.json b/lib/servers.json
    index c6b5588b55c0..bf03998a63a7 100644
    --- a/lib/servers.json
    +++ b/lib/servers.json
    @@ -1,5 +1,5 @@
     {
    -    "electrum.zclassic.org": {"s":"5222"},
    -    "zcl-electrum.com": {"s":"50002"}
    +    "electrum.zclassic.org": {"s":"5222", "pruning": "-", "version": "1.1"},
    +    "zcl-electrum.com": {"s":"50002", "pruning": "-", "version": "1.1"}
     }
     
    diff --git a/lib/x509.py b/lib/x509.py
    index 46933251b042..122aa7306f46 100644
    --- a/lib/x509.py
    +++ b/lib/x509.py
    @@ -23,7 +23,7 @@
     # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     # SOFTWARE.
     from . import util
    -from .util import profiler, bh2u, get_cert_path
    +from .util import profiler, bh2u
     import ecdsa
     import hashlib
     
    
    From 757c2b93b04d6504cc3025df44f13afaa9b48294 Mon Sep 17 00:00:00 2001
    From: Jon Layton 
    Date: Mon, 5 Mar 2018 20:51:39 -0600
    Subject: [PATCH 269/270] Removed leftovers, added back HEADERS_URL
    
    ---
     lib/bitcoin.py        |  66 ------------
     lib/blockchain.py     |   2 +-
     lib/constants.py      |   4 +-
     lib/servers-orig.json | 231 ------------------------------------------
     4 files changed, 3 insertions(+), 300 deletions(-)
     delete mode 100644 lib/servers-orig.json
    
    diff --git a/lib/bitcoin.py b/lib/bitcoin.py
    index d8c3672872fa..aee777915a90 100644
    --- a/lib/bitcoin.py
    +++ b/lib/bitcoin.py
    @@ -40,72 +40,6 @@
     from . import constants
     
     
    -
    -
    -''' TODO
    -# 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:
    -
    -    # https://github.com/z-classic/zclassic/blob/master/src/chainparams.cpp#L103
    -    @classmethod
    -    def set_mainnet(cls):
    -        cls.TESTNET = False
    -        cls.WIF_PREFIX = 0x80
    -        cls.ADDRTYPE_P2PKH = [0x1C, 0xB8]
    -        cls.ADDRTYPE_P2SH = [0x1C, 0xBD]
    -        cls.ADDRTYPE_SHIELDED = [0x16, 0x9A]
    -        cls.SEGWIT_HRP = "bc" #TODO zcl has no segwit
    -        cls.GENESIS = "0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602"
    -        cls.DEFAULT_PORTS = {'t': '50001', 's': '50002'}
    -        cls.DEFAULT_SERVERS = read_json('servers.json', {})
    -        cls.CHECKPOINTS = read_json('checkpoints.json', [])
    -        cls.EQUIHASH_N = 200
    -        cls.EQUIHASH_K = 9
    -        cls.HEADERS_URL = "http://headers.zcl-electrum.com/blockchain_headers"
    -
    -        cls.CHUNK_SIZE = 200
    -
    -    # https://github.com/z-classic/zclassic/blob/master/src/chainparams.cpp#L234
    -    @classmethod
    -    def set_testnet(cls):
    -        cls.TESTNET = True
    -        cls.WIF_PREFIX = 0xef
    -        cls.ADDRTYPE_P2PKH = [0x1D, 0x25]
    -        cls.ADDRTYPE_P2SH = [0x1C, 0xBA]
    -        cls.ADDRTYPE_SHIELDED = [0x16, 0xB6]
    -        cls.SEGWIT_HRP = "tb" #TODO zcl has no segwit
    -        cls.GENESIS = "03e1c4bb705c871bf9bfda3e74b7f8f86bff267993c215a89d5795e3708e5e1f"
    -        cls.DEFAULT_PORTS = {'t': '51001', 's': '51002'}
    -        cls.DEFAULT_SERVERS = read_json('servers_testnet.json', {})
    -        cls.CHECKPOINTS = read_json('checkpoints_testnet.json', [])
    -        cls.EQUIHASH_N = 200
    -        cls.EQUIHASH_K = 9
    -
    -        #cls.HEADERS_URL = "http://35.224.186.7/blockchain_headers"
    -
    -        cls.CHUNK_SIZE = 200
    -
    -NetworkConstants.set_mainnet()
    -'''
     ################################## transactions
     
     COINBASE_MATURITY = 100
    diff --git a/lib/blockchain.py b/lib/blockchain.py
    index 784b56dd22b2..81809c48a75f 100644
    --- a/lib/blockchain.py
    +++ b/lib/blockchain.py
    @@ -204,7 +204,7 @@ def verify_chunk(self, index, data):
             target = self.get_target(index-1)
             for i in range(num):
                 raw_header = data[i*bitcoin.HEADER_SIZE:(i+1) * bitcoin.HEADER_SIZE]
    -            header = deserialize_header(raw_header, index*NetworkConstants.CHUNK_SIZE + i)
    +            header = deserialize_header(raw_header, index*constants.net.CHUNK_SIZE + i)
                 self.verify_header(header, prev_hash, target)
                 prev_hash = hash_header(header)
     
    diff --git a/lib/constants.py b/lib/constants.py
    index 6b93e04b7ac1..1d90ae3f6e10 100644
    --- a/lib/constants.py
    +++ b/lib/constants.py
    @@ -37,8 +37,6 @@ def read_json(filename, default):
         return r
     
     
    -
    -
     class BitcoinMainnet:
     
         TESTNET = False
    @@ -57,6 +55,8 @@ class BitcoinMainnet:
     
         CHUNK_SIZE = 200
     
    +    HEADERS_URL = "http://headers.zcl-electrum.com/blockchain_headers"
    +
         XPRV_HEADERS = {
             'standard':    0x0488ade4,  # xprv
             'p2wpkh-p2sh': 0x049d7878,  # yprv
    diff --git a/lib/servers-orig.json b/lib/servers-orig.json
    deleted file mode 100644
    index 1f6398584f18..000000000000
    --- a/lib/servers-orig.json
    +++ /dev/null
    @@ -1,231 +0,0 @@
    -{
    -    "E-X.not.fyi": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "ELECTRUMX.not.fyi": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "ELEX01.blackpole.online": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "VPS.hsmiths.com": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "bitcoin.freedomnode.com": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "btc.smsys.me": {
    -        "pruning": "-",
    -        "s": "995",
    -        "version": "1.1"
    -    },
    -    "currentlane.lovebitco.in": {
    -        "pruning": "-",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "daedalus.bauerj.eu": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "de01.hamster.science": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "ecdsa.net": {
    -        "pruning": "-",
    -        "s": "110",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "elec.luggs.co": {
    -        "pruning": "-",
    -        "s": "443",
    -        "version": "1.1"
    -    },
    -    "electrum.akinbo.org": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrum.antumbra.se": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrum.be": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrum.coinucopia.io": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrum.cutie.ga": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrum.festivaldelhumor.org": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrum.hsmiths.com": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrum.qtornado.com": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrum.vom-stausee.de": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrum3.hachre.de": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrumx.bot.nu": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "electrumx.westeurope.cloudapp.azure.com": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "elx01.knas.systems": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "ex-btc.server-on.net": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "helicarrier.bauerj.eu": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "mooo.not.fyi": {
    -        "pruning": "-",
    -        "s": "50012",
    -        "t": "50011",
    -        "version": "1.1"
    -    },
    -    "ndnd.selfhost.eu": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "node.arihanc.com": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "node.xbt.eu": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "node1.volatilevictory.com": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "noserver4u.de": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "qmebr.spdns.org": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "raspi.hsmiths.com": {
    -        "pruning": "-",
    -        "s": "51002",
    -        "t": "51001",
    -        "version": "1.1"
    -    },
    -    "s2.noip.pl": {
    -        "pruning": "-",
    -        "s": "50102",
    -        "version": "1.1"
    -    },
    -    "s5.noip.pl": {
    -        "pruning": "-",
    -        "s": "50105",
    -        "version": "1.1"
    -    },
    -    "songbird.bauerj.eu": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "us.electrum.be": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    },
    -    "us01.hamster.science": {
    -        "pruning": "-",
    -        "s": "50002",
    -        "t": "50001",
    -        "version": "1.1"
    -    }
    -}
    
    From 6401c83068df3dfab615aec83ed2ec3b34df5fda Mon Sep 17 00:00:00 2001
    From: nimbosa 
    Date: Fri, 14 Sep 2018 19:27:30 +0800
    Subject: [PATCH 270/270] Update links and version 1.0.7 / 3.1
    
    Electrum-ZCL v1.0.7
    based on Electrum v3.1
    ---
     Dockerfile                 | 4 ++--
     contrib/build-osx/make_osx | 6 +++---
     contrib/build-osx/osx.spec | 2 +-
     3 files changed, 6 insertions(+), 6 deletions(-)
    
    diff --git a/Dockerfile b/Dockerfile
    index e763fb17a58f..cb7286abffe3 100644
    --- a/Dockerfile
    +++ b/Dockerfile
    @@ -1,12 +1,12 @@
     FROM ubuntu:18.04
     
    -ENV VERSION 1.0.2
    +ENV VERSION 1.0.7
     
     RUN set -x \
         && apt-get update \
         && apt-get install -y curl \
         && curl -sL https://github.com/z-classic/electrum-zcl/archive/${VERSION}.tar.gz |tar xzv \
    -    && mv electrum-zcl-Z-${VERSION} electrum-zcl \
    +    && mv electrum-zcl-${VERSION} electrum-zcl \
         && cd electrum-zcl \
         && apt-get install -y $(grep -vE "^\s*#" packages.txt  | tr "\n" " ") \
         && pip3 install -r requirements.txt \
    diff --git a/contrib/build-osx/make_osx b/contrib/build-osx/make_osx
    index e5a656049459..96c4b385f57a 100755
    --- a/contrib/build-osx/make_osx
    +++ b/contrib/build-osx/make_osx
    @@ -20,8 +20,8 @@ VERSION=`git describe --tags`
     # Paramterize
     PYTHON_VERSION=3.6.4
     BUILDDIR=/tmp/electrum-build
    -PACKAGE=Electrum
    -GIT_REPO=https://github.com/spesmilo/electrum
    +PACKAGE=Electrum-ZCL
    +GIT_REPO=https://github.com/z-classic/electrum-zcl
     
     
     info "Installing Python $PYTHON_VERSION"
    @@ -81,4 +81,4 @@ 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 $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG"
    +hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/Electrum-ZCL-$VERSION.dmg || fail "Could not create .DMG"
    diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec
    index a2c02f39b5a9..2977b6858ea2 100644
    --- a/contrib/build-osx/osx.spec
    +++ b/contrib/build-osx/osx.spec
    @@ -5,7 +5,7 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules, coll
     import sys
     import os
     
    -PACKAGE='Electrum'
    +PACKAGE='Electrum-ZCL'
     PYPKG='electrum'
     MAIN_SCRIPT='electrum'
     ICONS_FILE='electrum.icns'