diff --git a/.gitignore b/.gitignore index 87d614076..7918ea80b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ gui/qt/icons_rc.py locale/ .devlocaltmp/ *_trial_temp +packages diff --git a/MANIFEST.in b/MANIFEST.in index 515844d0b..a96077dcf 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,7 @@ recursive-include lib *.py recursive-include gui *.py recursive-include plugins *.py recursive-include packages *.py +recursive-include packages cacert.pem include app.fil include icons.qrc recursive-include icons * diff --git a/RELEASE-NOTES b/RELEASE-NOTES index ca3440a66..9780db805 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,28 +1,32 @@ # Release 2.0 - * New address derivation (BIP32 + BIP44). + * New address derivation (BIP32). - * 8 bits of the seed phrase are used to store a version number. The - current version number (0x01) refers to the current wallet - structure (BIP44). The version number also serves as a checksum for - the seed, and it will prevent the import of seeds from incompatible - wallets. + * New seed phrase format: 8 bits of the seed phrase are used to store + a version number. The current version number (0x01) refers to the + default wallet structure. The version number also serves as a + checksum for the seed, and it will prevent the import of seeds from + incompatible wallets. - * New serialization format for unsigned or partially signed + * Compact serialization format for unsigned or partially signed transactions, that includes the master public key and derivation - needed to sign inputs. This new format is compact enough to - send transactions to cold storage using QR codes + needed to sign inputs. This allows to send partially signed + transactions using QR codes * Deterministic Multisig wallets using parallel BIP32 derivations and P2SH addresses (2 of 2, 2 of 3). - * New plugins: + * New plugins: + - TrustedCoin: two-factor authentication using 2 of 3 multisig and + Google Authenticator - Trezor: support for the Trezor hardware wallet by SatoshiLabs - - Cosigner Pool: shared memory pool for partially signed transactions + - Cosigner Pool: encrypted communication channel for multisig + wallets, to send and receive partially signed transactions + - Audio Modem: send and receive transactions by sound - * BIP70: verification of signed payment requests - - Verification is pure python, using tlslite. - - In the GUI, payment requests are in the 'Invoices' tab. + * Support for BIP70: payment requests + - Verification of the signature chain uses tlslite. + - In the GUI, payment requests are shown in the 'Invoices' tab. * New 'Receive' tab: - create and manage payment requests, with QR Codes @@ -31,7 +35,7 @@ window that pops up if you click on the QR code * The 'Send' tab in the Qt GUI supports transactions with multiple - outputs, and with OP_RETURN "message" + outputs, and raw hexadecimal scripts. * The GUI can use the daemon: "electrum -d". The daemon can serve several clients. It times out if no client uses if for more than 5 @@ -42,8 +46,15 @@ * Wallet files are saved as JSON instead of Python. + * Client supports servers with SSL certificates signed by a CA. + * Documentation is now hosted on a wiki: http://electrum.orain.org + * ECIES encrypt/decrypt methods, availabe in the GUI and using the + command line: + encrypt + decrypt + # Release 1.9.8 diff --git a/make_packages b/contrib/make_android similarity index 50% rename from make_packages rename to contrib/make_android index 22e01a30c..859300fb0 100755 --- a/make_packages +++ b/contrib/make_android @@ -1,23 +1,20 @@ #!/usr/bin/python -from lib.version import ELECTRUM_VERSION as version if __name__ == '__main__': import sys, re, shutil, os, hashlib + import imp + os.chdir(os.path.dirname(os.path.realpath(__file__))) + os.chdir('..') + + imp.load_module('electrum', *imp.find_module('../lib')) + from electrum.version import ELECTRUM_VERSION as version if not ( os.path.exists('packages')): print "The packages directory is missing." sys.exit() - # os.system("python mki18n.py") - os.system("pyrcc4 icons.qrc -o gui/qt/icons_rc.py") - os.system("python setup.py sdist --format=zip,gztar") - - _tgz="Electrum-%s.tar.gz"%version - _zip="Electrum-%s.zip"%version - - # android os.system('rm -rf dist/e4a-%s'%version) os.mkdir('dist/e4a-%s'%version) shutil.copyfile("electrum",'dist/e4a-%s/e4a.py'%version) @@ -37,21 +34,6 @@ if __name__ == '__main__': e4a_name = "e4a-%s.zip"%version e4a_name2 = e4a_name.replace(".","") os.system( "mv %s %s"%(e4a_name, e4a_name2) ) + print "dist/%s "%e4a_name2 - import getpass - password = getpass.getpass("Password:") - for f in os.listdir("."): - os.system( "gpg --sign --armor --detach --passphrase \"%s\" %s"%(password, f) ) - - md5_tgz = hashlib.md5(file(_tgz, 'r').read()).digest().encode('hex') - md5_zip = hashlib.md5(file(_zip, 'r').read()).digest().encode('hex') - md5_android = hashlib.md5(file(e4a_name2, 'r').read()).digest().encode('hex') - os.chdir("..") - - print "" - print "Packages are ready:" - print "dist/%s "%_tgz, md5_tgz - print "dist/%s "%_zip, md5_zip - print "dist/%s "%e4a_name2, md5_android - print "To make a release, upload the files to the server, and update the webpages in branch gh-pages" diff --git a/contrib/make_packages b/contrib/make_packages new file mode 100755 index 000000000..13ef14c53 --- /dev/null +++ b/contrib/make_packages @@ -0,0 +1,40 @@ +#!/usr/bin/python + +import sys, re, shutil, os, hashlib +import imp +import getpass + +if __name__ == '__main__': + + os.chdir(os.path.dirname(os.path.realpath(__file__))) + os.chdir('..') + + imp.load_module('electrum', *imp.find_module('../lib')) + from electrum.version import ELECTRUM_VERSION as version + + if not ( os.path.exists('packages')): + print "The packages directory is missing." + sys.exit() + + # os.system("python mki18n.py") + os.system("pyrcc4 icons.qrc -o gui/qt/icons_rc.py") + os.system("python setup.py sdist --format=zip,gztar") + + _tgz="Electrum-%s.tar.gz"%version + _zip="Electrum-%s.zip"%version + + os.chdir("dist") + password = getpass.getpass("Password:") + for f in [_tgz,_zip]: + os.system( "gpg --sign --armor --detach --passphrase \"%s\" %s"%(password, f) ) + + md5_tgz = hashlib.md5(file(_tgz, 'r').read()).digest().encode('hex') + md5_zip = hashlib.md5(file(_zip, 'r').read()).digest().encode('hex') + os.chdir("..") + + print "" + print "Packages are ready:" + print "dist/%s "%_tgz, md5_tgz + print "dist/%s "%_zip, md5_zip + print "To make a release, upload the files to the server, and update the webpages in branch gh-pages" + diff --git a/electrum b/electrum index 82bd509fa..0d5d7635e 100755 --- a/electrum +++ b/electrum @@ -26,18 +26,54 @@ import sys import time import traceback - -is_local = os.path.dirname(os.path.realpath(__file__)) == os.getcwd() +is_bundle = getattr(sys, 'frozen', False) +is_local = not is_bundle and os.path.dirname(os.path.realpath(__file__)) == os.getcwd() is_android = 'ANDROID_DATA' in os.environ if is_local: - sys.path.append('packages') + sys.path.insert(0, 'packages') +elif is_bundle and sys.platform=='darwin': + sys.path.insert(0, os.getcwd() + "/lib/python2.7/packages") import __builtin__ __builtin__.use_local_modules = is_local or is_android +# pure-python dependencies need to be imported here for pyinstaller +try: + import aes + import ecdsa + import socks + import requests + import six + import qrcode + import pyasn1 + import pyasn1_modules + import tlslite + import pbkdf2 + import google.protobuf +except ImportError as e: + sys.exit("Error: %s. Try 'sudo pip install '"%e.message) + +# the following imports are for pyinstaller +import pyasn1.codec +import pyasn1.codec.der +from pyasn1.codec.der import encoder, decoder +from pyasn1_modules import rfc2459 +from google.protobuf import descriptor +from google.protobuf import message +from google.protobuf import reflection +from google.protobuf import descriptor_pb2 + + +# check that we have the correct version of ecdsa +try: + from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1 +except Exception: + sys.exit("cannot import ecdsa.curve_secp256k1. You probably need to upgrade ecdsa.\nTry: sudo pip install --upgrade ecdsa") + + # load local module as electrum -if __builtin__.use_local_modules: +if is_bundle or is_local or is_android: import imp imp.load_module('electrum', *imp.find_module('lib')) imp.load_module('electrum_gui', *imp.find_module('gui')) @@ -45,12 +81,11 @@ if __builtin__.use_local_modules: from electrum import util from electrum import SimpleConfig, Network, Wallet, WalletStorage, NetworkProxy, Commands, known_commands, pick_random_server -from electrum.util import print_msg, print_stderr, print_json, set_verbosity, InvalidPassword +from electrum.util import print_msg, print_error, print_stderr, print_json, set_verbosity, InvalidPassword from electrum.daemon import get_daemon from electrum.plugins import init_plugins - # get password routine def prompt_password(prompt, confirm=True): import getpass @@ -170,10 +205,12 @@ if __name__ == '__main__': for k, v in config_options.items(): if v is None: config_options.pop(k) + if config_options.get('server'): + config_options['auto_cycle'] = False set_verbosity(config_options.get('verbose')) - config = SimpleConfig(config_options) + print_error("CA bundle:", requests.utils.DEFAULT_CA_BUNDLE_PATH, "found" if os.path.exists(requests.utils.DEFAULT_CA_BUNDLE_PATH) else "not found") if len(args) == 0: url = None @@ -185,7 +222,6 @@ if __name__ == '__main__': cmd = args[0] if cmd == 'gui': - init_plugins(config) gui_name = config.get('gui', 'classic') if gui_name in ['lite', 'classic']: gui_name = 'qt' @@ -196,6 +232,9 @@ if __name__ == '__main__': sys.exit() #sys.exit("Error: Unknown GUI: " + gui_name ) + if gui_name=='qt': + init_plugins(config, is_bundle or is_local or is_android) + # network interface if not options.offline: s = get_daemon(config, start_daemon=options.daemon) diff --git a/electrum-env b/electrum-env new file mode 100755 index 000000000..0e2cc70f1 --- /dev/null +++ b/electrum-env @@ -0,0 +1,24 @@ +#!/bin/bash +# +# This script creates a virtualenv named 'env' and installs all +# python dependencies before activating the env and running Electrum. +# If 'env' already exists, it is activated and Electrum is started +# without any installations. Additionally, the PYTHONPATH environment +# variable is set properly before running Electrum. +# +# python-qt and its dependencies will still need to be installed with +# your package manager. + +if [ -e ./env/bin/activate ]; then + source ./env/bin/activate +else + virtualenv env + source ./env/bin/activate + python setup.py install +fi + +export PYTHONPATH=/usr/local/lib/python2.7/site-packages:$PYTHONPATH + +./electrum + +deactivate diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py index 0b625d75f..2d8291780 100644 --- a/gui/qt/installwizard.py +++ b/gui/qt/installwizard.py @@ -35,7 +35,7 @@ class InstallWizard(QDialog): self.storage = storage self.setMinimumSize(575, 400) self.setMaximumSize(575, 400) - self.setWindowTitle('Electrum') + self.setWindowTitle('Electrum' + ' - ' + os.path.basename(self.storage.path)) self.connect(self, QtCore.SIGNAL('accept'), self.accept) self.stack = QStackedLayout() self.setLayout(self.stack) @@ -142,10 +142,8 @@ class InstallWizard(QDialog): def multi_mpk_dialog(self, xpub_hot, n): vbox = QVBoxLayout() - vbox0, seed_e0 = seed_dialog.enter_seed_box(MSG_SHOW_MPK, self, 'hot') + vbox0 = seed_dialog.show_seed_box(MSG_SHOW_MPK, xpub_hot, 'hot') vbox.addLayout(vbox0) - seed_e0.setText(xpub_hot) - seed_e0.setReadOnly(True) entries = [] for i in range(n): vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_COLD_MPK, self, 'cold') @@ -308,7 +306,7 @@ class InstallWizard(QDialog): def show_seed(self, seed, sid): - vbox = seed_dialog.show_seed_box(seed, sid) + vbox = seed_dialog.show_seed_box_msg(seed, sid) vbox.addLayout(ok_cancel_buttons(self, _("Next"))) self.set_layout(vbox) return self.exec_() diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 7e6b0e115..2ea39a154 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -203,6 +203,7 @@ class ElectrumWindow(QMainWindow): def close_wallet(self): self.wallet.stop_threads() + self.hide() run_hook('close_wallet') def load_wallet(self, wallet): @@ -210,13 +211,17 @@ class ElectrumWindow(QMainWindow): self.wallet = wallet self.update_wallet_format() # address used to create a dummy transaction and estimate transaction fee - self.dummy_address = self.wallet.addresses(False)[0] + a = self.wallet.addresses(False) + self.dummy_address = a[0] if a else None + self.invoices = self.wallet.storage.get('invoices', {}) self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{}) self.current_account = self.wallet.storage.get("current_account", None) title = 'Electrum ' + self.wallet.electrum_version + ' - ' + os.path.basename(self.wallet.storage.path) if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only')) self.setWindowTitle( title ) + self.update_history_tab() + self.show() self.update_wallet() # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized self.notify_transactions() @@ -254,17 +259,42 @@ class ElectrumWindow(QMainWindow): filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) ) if not filename: return - - storage = WalletStorage({'wallet_path': filename}) + try: + storage = WalletStorage({'wallet_path': filename}) + except Exception as e: + self.show_message(str(e)) + return if not storage.file_exists: - self.show_message("file not found "+ filename) + self.show_message(_("File not found") + ' ' + filename) return - + # read wizard action + try: + wallet = Wallet(storage) + except BaseException as e: + QMessageBox.warning(None, _('Warning'), str(e), _('OK')) + return + action = wallet.get_action() + # ask for confirmation + if action is not None: + if not self.question(_("This file contains an incompletely created wallet.\nDo you want to complete its creation now?")): + return # close current wallet self.close_wallet() - # load new wallet - wallet = Wallet(storage) - wallet.start_threads(self.network) + # run wizard + if action is not None: + import installwizard + wizard = installwizard.InstallWizard(self.config, self.network, storage) + try: + wallet = wizard.run(action) + except BaseException as e: + traceback.print_exc(file=sys.stdout) + QMessageBox.information(None, _('Error'), str(e), _('OK')) + return + if not wallet: + return + else: + wallet.start_threads(self.network) + # load new wallet in gui self.load_wallet(wallet) @@ -308,6 +338,8 @@ class ElectrumWindow(QMainWindow): QMessageBox.critical(None, "Error", _("File exists")) return + if self.wallet: + self.close_wallet() wizard = installwizard.InstallWizard(self.config, self.network, storage) wallet = wizard.run('new') if wallet: @@ -371,7 +403,7 @@ class ElectrumWindow(QMainWindow): help_menu.addAction(_("&About"), self.show_about) help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org")) help_menu.addSeparator() - help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents) + help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.orain.org/")).setShortcut(QKeySequence.HelpContents) help_menu.addAction(_("&Report Bug"), self.show_report_bug) self.setMenuBar(menubar) @@ -1278,7 +1310,7 @@ class ElectrumWindow(QMainWindow): if not request_url: if label: if self.wallet.labels.get(address) != label: - if self.question(_('Save label "%s" for address %s ?'%(label,address))): + if self.question(_('Save label "%(label)s" for address %(address)s ?'%{'label':label,'address':address})): if address not in self.wallet.addressbook and not self.wallet.is_mine(address): self.wallet.addressbook.append(address) self.wallet.set_label(address, label) @@ -2507,7 +2539,7 @@ class ElectrumWindow(QMainWindow): with open(fileName, "w+") as f: if is_csv: - transaction = csv.writer(f) + transaction = csv.writer(f, lineterminator='\n') transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"]) for line in lines: transaction.writerow(line) diff --git a/gui/qt/network_dialog.py b/gui/qt/network_dialog.py index b777db29c..a52cb1c23 100644 --- a/gui/qt/network_dialog.py +++ b/gui/qt/network_dialog.py @@ -122,14 +122,15 @@ class NetworkDialog(QDialog): lambda x,y: self.server_changed(x)) grid.addWidget(self.servers_list_widget, 1, 1, 1, 3) - if not config.is_modifiable('server'): - for w in [self.server_host, self.server_port, self.server_protocol, self.servers_list_widget]: w.setEnabled(False) - def enable_set_server(): - enabled = not self.autocycle_cb.isChecked() - self.server_host.setEnabled(enabled) - self.server_port.setEnabled(enabled) - self.servers_list_widget.setEnabled(enabled) + if config.is_modifiable('server'): + enabled = not self.autocycle_cb.isChecked() + self.server_host.setEnabled(enabled) + self.server_port.setEnabled(enabled) + self.servers_list_widget.setEnabled(enabled) + else: + for w in [self.autocycle_cb, self.server_host, self.server_port, self.server_protocol, self.servers_list_widget]: + w.setEnabled(False) self.autocycle_cb.clicked.connect(enable_set_server) enable_set_server() @@ -143,19 +144,18 @@ class NetworkDialog(QDialog): self.proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP']) def check_for_disable(index = False): - if self.proxy_mode.currentText() != 'NONE': - self.proxy_host.setEnabled(True) - self.proxy_port.setEnabled(True) + if self.config.is_modifiable('proxy'): + if self.proxy_mode.currentText() != 'NONE': + self.proxy_host.setEnabled(True) + self.proxy_port.setEnabled(True) + else: + self.proxy_host.setEnabled(False) + self.proxy_port.setEnabled(False) else: - self.proxy_host.setEnabled(False) - self.proxy_port.setEnabled(False) + for w in [self.proxy_host, self.proxy_port, self.proxy_mode]: w.setEnabled(False) check_for_disable() self.proxy_mode.connect(self.proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable) - - if not self.config.is_modifiable('proxy'): - for w in [self.proxy_host, self.proxy_port, self.proxy_mode]: w.setEnabled(False) - self.proxy_mode.setCurrentIndex(self.proxy_mode.findText(str(proxy_config.get("mode").upper()))) self.proxy_host.setText(proxy_config.get("host")) self.proxy_port.setText(proxy_config.get("port")) diff --git a/gui/qt/paytoedit.py b/gui/qt/paytoedit.py index 924ffcb8f..b9e3c88f3 100644 --- a/gui/qt/paytoedit.py +++ b/gui/qt/paytoedit.py @@ -96,20 +96,21 @@ class PayToEdit(ScanQRTextEdit): self.errors = [] if self.is_pr: return - # filter out empty lines lines = filter( lambda x: x, self.lines()) outputs = [] total = 0 - self.payto_address = None if len(lines) == 1: + data = lines[0] + if data.startswith("bitcoin:"): + self.scan_f(data) + return try: - self.payto_address = self.parse_address(lines[0]) + self.payto_address = self.parse_address(data) except: pass - if self.payto_address: self.unlock_amount() return diff --git a/gui/qt/seed_dialog.py b/gui/qt/seed_dialog.py index c27f77050..87497f4a7 100644 --- a/gui/qt/seed_dialog.py +++ b/gui/qt/seed_dialog.py @@ -31,7 +31,7 @@ class SeedDialog(QDialog): self.setModal(1) self.setMinimumWidth(400) self.setWindowTitle('Electrum' + ' - ' + _('Seed')) - vbox = show_seed_box(seed) + vbox = show_seed_box_msg(seed) if imported_keys: vbox.addWidget(QLabel(""+_("WARNING")+": " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "

")) vbox.addLayout(close_button(self)) @@ -47,71 +47,39 @@ def icon_filename(sid): return ":icons/seed.png" - - -def show_seed_box(seed, sid=None): - - save_msg = _("Please save these %d words on paper (order is important).")%len(seed.split()) + " " - qr_msg = _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "

" - warning_msg = ""+_("WARNING")+": " + _("Never disclose your seed. Never type it on a website.") + "

" - - if sid is None: - msg = _("Your wallet generation seed is") - msg2 = save_msg + " " \ - + _("This seed will allow you to recover your wallet in case of computer failure.") + "
" \ - + warning_msg - - elif sid == 'cold': - msg = _("Your cold storage seed is") - msg2 = save_msg + " " \ - + _("This seed will be permanently deleted from your wallet file. Make sure you have saved it before you press 'next'") + " " \ - - elif sid == 'hot': - msg = _("Your hot seed is") - msg2 = save_msg + " " \ - + _("If you ever need to recover your wallet from seed, you will need both this seed and your cold seed.") + " " \ - - label1 = QLabel(msg+ ":") - seed_text = ShowQRTextEdit(text=seed) - seed_text.setMaximumHeight(130) - +def show_seed_box_msg(seedphrase, sid=None): + msg = _("Your wallet generation seed is") + ":" + vbox = show_seed_box(msg, seedphrase, sid) + save_msg = _("Please save these %d words on paper (order is important).")%len(seedphrase.split()) + " " + msg2 = save_msg + " " \ + + _("This seed will allow you to recover your wallet in case of computer failure.") + "
" \ + + ""+_("WARNING")+": " + _("Never disclose your seed. Never type it on a website.") + "

" label2 = QLabel(msg2) label2.setWordWrap(True) - - logo = QLabel() - logo.setPixmap(QPixmap(icon_filename(sid)).scaledToWidth(56)) - logo.setMaximumWidth(60) - - grid = QGridLayout() - grid.addWidget(logo, 0, 0) - grid.addWidget(label1, 0, 1) - grid.addWidget(seed_text, 1, 0, 1, 2) - vbox = QVBoxLayout() - vbox.addLayout(grid) vbox.addWidget(label2) vbox.addStretch(1) - return vbox +def show_seed_box(msg, seed, sid): + vbox, seed_e = enter_seed_box(msg, None, sid=sid, text=seed) + return vbox -def enter_seed_box(msg, window, sid=None): +def enter_seed_box(msg, window, sid=None, text=None): vbox = QVBoxLayout() logo = QLabel() logo.setPixmap(QPixmap(icon_filename(sid)).scaledToWidth(56)) logo.setMaximumWidth(60) - label = QLabel(msg) label.setWordWrap(True) - - seed_e = ScanQRTextEdit(win=window) - seed_e.setMaximumHeight(100) - seed_e.setTabChangesFocus(True) - + if not text: + seed_e = ScanQRTextEdit(win=window) + seed_e.setTabChangesFocus(True) + else: + seed_e = ShowQRTextEdit(text=text) + seed_e.setMaximumHeight(130) vbox.addWidget(label) - grid = QGridLayout() grid.addWidget(logo, 0, 0) grid.addWidget(seed_e, 0, 1) - vbox.addLayout(grid) return vbox, seed_e diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 0754dbdec..866057f95 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -156,7 +156,7 @@ class TxDialog(QDialog): self.broadcast_button.show() else: s, r = self.tx.signature_count() - status = _("Unsigned") if s == 0 else _('Partially signed (%d/%d)'%(s,r)) + status = _("Unsigned") if s == 0 else _('Partially signed') + ' (%d/%d)'%(s,r) time_str = None self.broadcast_button.hide() tx_hash = 'unknown' diff --git a/gui/qt/version_getter.py b/gui/qt/version_getter.py index f60a60919..d37bda317 100644 --- a/gui/qt/version_getter.py +++ b/gui/qt/version_getter.py @@ -33,7 +33,7 @@ class VersionGetter(threading.Thread): def run(self): try: - con = httplib.HTTPConnection('electrum.org', 80, timeout=5) + con = httplib.HTTPSConnection('electrum.org', timeout=5) con.request("GET", "/version") res = con.getresponse() except socket.error as msg: @@ -75,7 +75,10 @@ class UpdateLabel(QLabel): def compare_versions(self, version1, version2): def normalize(v): return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")] - return cmp(normalize(version1), normalize(version2)) + try: + return cmp(normalize(version1), normalize(version2)) + except: + return 0 def ignore_this_version(self): self.setText("") diff --git a/icons/expired.png b/icons/expired.png index e0f3639df..6400b8ba6 100644 Binary files a/icons/expired.png and b/icons/expired.png differ diff --git a/icons/unpaid.png b/icons/unpaid.png index 6400b8ba6..e0f3639df 100644 Binary files a/icons/unpaid.png and b/icons/unpaid.png differ diff --git a/lib/account.py b/lib/account.py index 3e765d08d..9d90c1455 100644 --- a/lib/account.py +++ b/lib/account.py @@ -76,7 +76,7 @@ class Account(object): return None def synchronize_sequence(self, wallet, for_change): - limit = self.gap_limit_for_change if for_change else self.gap_limit + limit = wallet.gap_limit_for_change if for_change else wallet.gap_limit while True: addresses = self.get_addresses(for_change) if len(addresses) < limit: @@ -175,14 +175,11 @@ class ImportedAccount(Account): class OldAccount(Account): """ Privatekey(type,n) = Master_private_key + H(n|S|type) """ - gap_limit = 5 - gap_limit_for_change = 3 def __init__(self, v): Account.__init__(self, v) self.mpk = v['mpk'].decode('hex') - @classmethod def mpk_from_seed(klass, seed): curve = SECP256k1 @@ -274,8 +271,6 @@ class OldAccount(Account): class BIP32_Account(Account): - gap_limit = 20 - gap_limit_for_change = 3 def __init__(self, v): Account.__init__(self, v) diff --git a/lib/bitcoin.py b/lib/bitcoin.py index d4a265b3a..65771b8e8 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -26,15 +26,8 @@ import hmac import version from util import print_error, InvalidPassword -try: - import ecdsa -except ImportError: - sys.exit("Error: python-ecdsa does not seem to be installed. Try 'sudo pip install ecdsa'") - -try: - import aes -except ImportError: - sys.exit("Error: AES does not seem to be installed. Try 'sudo pip install slowaes'") +import ecdsa +import aes ################################## transactions @@ -58,6 +51,8 @@ def strip_PKCS7_padding(s): raise ValueError("Invalid PKCS7 padding") return s[:-numpads] +# backport padding fix to AES module +aes.strip_PKCS7_padding = strip_PKCS7_padding def aes_encrypt_with_iv(key, iv, data): mode = aes.AESModeOfOperation.modeOfOperation["CBC"] @@ -401,12 +396,7 @@ def is_private_key(key): ########### end pywallet functions ####################### -try: - from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1 -except Exception: - print "cannot import ecdsa.curve_secp256k1. You probably need to upgrade ecdsa.\nTry: sudo pip install --upgrade ecdsa" - exit() - +from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1 from ecdsa.curves import SECP256k1 from ecdsa.ellipticcurve import Point from ecdsa.util import string_to_number, number_to_string diff --git a/lib/interface.py b/lib/interface.py index 0831dd555..71b4f6884 100644 --- a/lib/interface.py +++ b/lib/interface.py @@ -38,6 +38,30 @@ proxy_modes = ['socks4', 'socks5', 'http'] import util +def serialize_proxy(p): + if type(p) != dict: + return None + return ':'.join([p.get('mode'),p.get('host'), p.get('port')]) + +def deserialize_proxy(s): + if type(s) != str: + return None + if s.lower() == 'none': + return None + proxy = { "mode":"socks5", "host":"localhost" } + args = s.split(':') + n = 0 + if proxy_modes.count(args[n]) == 1: + proxy["mode"] = args[n] + n += 1 + if len(args) > n: + proxy["host"] = args[n] + n += 1 + if len(args) > n: + proxy["port"] = args[n] + else: + proxy["port"] = "8080" if proxy["mode"] == "http" else "1080" + return proxy def Interface(server, config = None): @@ -68,7 +92,7 @@ class TcpInterface(threading.Thread): self.host, self.port, self.protocol = self.server.split(':') self.port = int(self.port) self.use_ssl = (self.protocol == 's') - self.proxy = self.parse_proxy_options(self.config.get('proxy')) + self.proxy = deserialize_proxy(self.config.get('proxy')) if self.proxy: self.proxy_mode = proxy_modes.index(self.proxy["mode"]) + 1 socks.setdefaultproxy(self.proxy_mode, self.proxy["host"], int(self.proxy["port"])) @@ -271,25 +295,6 @@ class TcpInterface(threading.Thread): self.unanswered_requests[self.message_id] = method, params, _id, queue self.message_id += 1 - def parse_proxy_options(self, s): - if type(s) == type({}): return s # fixme: type should be fixed - if type(s) != type(""): return None - if s.lower() == 'none': return None - proxy = { "mode":"socks5", "host":"localhost" } - args = s.split(':') - n = 0 - if proxy_modes.count(args[n]) == 1: - proxy["mode"] = args[n] - n += 1 - if len(args) > n: - proxy["host"] = args[n] - n += 1 - if len(args) > n: - proxy["port"] = args[n] - else: - proxy["port"] = "8080" if proxy["mode"] == "http" else "1080" - return proxy - def stop(self): if self.is_connected and self.protocol in 'st' and self.s: self.s.shutdown(socket.SHUT_RDWR) diff --git a/lib/network.py b/lib/network.py index c1b88ba9b..de6ac30a7 100644 --- a/lib/network.py +++ b/lib/network.py @@ -1,4 +1,12 @@ -import threading, time, Queue, os, sys, shutil, random +import threading +import time +import Queue +import os +import sys +import random +import traceback + + from util import user_dir, appdata_dir, print_error, print_msg from bitcoin import * import interface @@ -178,7 +186,7 @@ class Network(threading.Thread): def get_parameters(self): host, port, protocol = self.default_server.split(':') - proxy = self.proxy + proxy = interface.deserialize_proxy(self.proxy) auto_connect = self.config.get('auto_cycle', True) return host, port, protocol, proxy, auto_connect @@ -225,14 +233,16 @@ class Network(threading.Thread): threading.Thread.start(self) def set_parameters(self, host, port, protocol, proxy, auto_connect): + proxy_str = interface.serialize_proxy(proxy) + server_str = ':'.join([ host, port, protocol ]) self.config.set_key('auto_cycle', auto_connect, True) - self.config.set_key("proxy", proxy, True) + self.config.set_key("proxy", proxy_str, True) self.config.set_key("protocol", protocol, True) - server = ':'.join([ host, port, protocol ]) - self.config.set_key("server", server, True) + self.config.set_key("server", server_str, True) - if self.proxy != proxy or self.protocol != protocol: - self.proxy = proxy + if self.proxy != proxy_str or self.protocol != protocol: + print_error('restarting network') + self.proxy = proxy_str self.protocol = protocol for i in self.interfaces.values(): i.stop() if auto_connect: @@ -246,7 +256,7 @@ class Network(threading.Thread): if self.server_is_lagging(): self.stop_interface() else: - self.set_server(server) + self.set_server(server_str) def switch_to_random_interface(self): @@ -358,6 +368,7 @@ class Network(threading.Thread): out['result'] = f(*params) except BaseException as e: out['error'] = str(e) + traceback.print_exc(file=sys.stout) print_error("network error", str(e)) self.response_queue.put(out) diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py index 6d2c09c04..0c9630268 100644 --- a/lib/paymentrequest.py +++ b/lib/paymentrequest.py @@ -27,18 +27,12 @@ import time import traceback import urllib2 import urlparse - +import requests try: import paymentrequest_pb2 -except: - sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto'") - -try: - import requests except ImportError: - sys.exit("Error: requests does not seem to be installed. Try 'sudo pip install requests'") - + sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto'") import bitcoin import util @@ -116,7 +110,7 @@ class PaymentRequest: self.id = bitcoin.sha256(r)[0:16].encode('hex') filename = os.path.join(self.dir_path, self.id) - with open(filename,'w') as f: + with open(filename,'wb') as f: f.write(r) return self.parse(r) @@ -131,7 +125,7 @@ class PaymentRequest: def read_file(self, key): filename = os.path.join(self.dir_path, key) - with open(filename,'r') as f: + with open(filename,'rb') as f: r = f.read() assert key == bitcoin.sha256(r)[0:16].encode('hex') diff --git a/lib/plugins.py b/lib/plugins.py index d3dba3500..956caffa4 100644 --- a/lib/plugins.py +++ b/lib/plugins.py @@ -6,11 +6,11 @@ from i18n import _ plugins = [] -def init_plugins(config): +def init_plugins(config, local): import imp, pkgutil, __builtin__, os global plugins - if __builtin__.use_local_modules: + if local: fp, pathname, description = imp.find_module('plugins') plugin_names = [name for a, name, b in pkgutil.iter_modules([pathname])] plugin_names = filter( lambda name: os.path.exists(os.path.join(pathname,name+'.py')), plugin_names) @@ -40,21 +40,26 @@ def hook(func): def run_hook(name, *args): + SPECIAL_HOOKS = ['get_wizard_action'] results = [] f_list = hooks.get(name,[]) for p, f in f_list: if name == 'load_wallet': p.wallet = args[0] - if not p.is_enabled(): - continue - try: - r = f(*args) - except Exception: - print_error("Plugin error") - traceback.print_exc(file=sys.stdout) - r = False - if r: - results.append(r) + if name == 'init_qt': + gui = args[0] + p.window = gui.main_window + if name in SPECIAL_HOOKS or p.is_enabled(): + try: + r = f(*args) + except Exception: + print_error("Plugin error") + traceback.print_exc(file=sys.stdout) + r = False + if r: + results.append(r) + if name == 'close_wallet': + p.wallet = None if results: assert len(results) == 1, results @@ -92,8 +97,12 @@ class BasePlugin: def init_qt(self, gui): pass + @hook def load_wallet(self, wallet): pass + @hook + def close_wallet(self): pass + #def init(self): pass def close(self): pass diff --git a/lib/qrscanner.py b/lib/qrscanner.py index 479dc8e8c..38cc1f3d0 100644 --- a/lib/qrscanner.py +++ b/lib/qrscanner.py @@ -12,7 +12,7 @@ proc = None def scan_qr(config): global proc if not zbar: - raise BaseException("\n".join([_("Cannot start QR scanner."),_("The zbar package is not available."),_("On Linux, try 'sudo apt-get install python-zbar'")])) + raise BaseException("\n".join([_("Cannot start QR scanner."),_("The zbar package is not available."),_("On Linux, try 'sudo pip install zbar'")])) if proc is None: device = config.get("video_device", "default") if device == 'default': diff --git a/lib/simple_config.py b/lib/simple_config.py index 0824c04e4..b9ad42f4d 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -110,7 +110,7 @@ class SimpleConfig(object): out = None with self.lock: out = self.read_only_options.get(key) - if not out: + if out is None: out = self.user_config.get(key, default) return out diff --git a/lib/util.py b/lib/util.py index 27e9228b4..175c29045 100644 --- a/lib/util.py +++ b/lib/util.py @@ -66,7 +66,16 @@ def data_dir(): if __builtin__.use_local_modules: return local_data_dir() else: - return appdata_dir() + is_frozen = getattr(sys, 'frozen', False) + if is_frozen: + if is_frozen == "macosx_app": + basedir = os.path.abspath(".") + else: + basedir = sys._MEIPASS + + return os.path.join(basedir, 'data') + else: + return appdata_dir() def usr_share_dir(): return os.path.join(sys.prefix, "share") diff --git a/lib/version.py b/lib/version.py index 5b98ef598..beaeb8d3f 100644 --- a/lib/version.py +++ b/lib/version.py @@ -1,4 +1,4 @@ -ELECTRUM_VERSION = "2.0" # version of the client package +ELECTRUM_VERSION = "2.0b2" # version of the client package PROTOCOL_VERSION = '0.9' # protocol version requested NEW_SEED_VERSION = 11 # electrum versions >= 2.0 OLD_SEED_VERSION = 4 # electrum versions < 2.0 diff --git a/lib/wallet.py b/lib/wallet.py index ef8a62cb5..c9c10aee7 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1048,9 +1048,11 @@ class Abstract_Wallet(object): addr = bitcoin.public_key_to_bc_address(x_pubkey.decode('hex')) return self.is_mine(addr) elif x_pubkey[0:2] == 'ff': + if not isinstance(self, BIP32_Wallet): return False xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) return xpub in [ self.master_public_keys[k] for k in self.master_private_keys.keys() ] elif x_pubkey[0:2] == 'fe': + if not isinstance(self, OldWallet): return False xpub, sequence = OldAccount.parse_xpubkey(x_pubkey) return xpub == self.get_master_public_key() elif x_pubkey[0:2] == 'fd': @@ -1140,17 +1142,15 @@ class Deterministic_Wallet(Abstract_Wallet): if value >= self.gap_limit: self.gap_limit = value self.storage.put('gap_limit', self.gap_limit, True) - #self.interface.poke('synchronizer') return True elif value >= self.min_acceptable_gap(): for key, account in self.accounts.items(): - addresses = account[0] + addresses = account.get_addresses(False) k = self.num_unused_trailing_addresses(addresses) n = len(addresses) - k + value - addresses = addresses[0:n] - self.accounts[key][0] = addresses - + account.receiving_pubkeys = account.receiving_pubkeys[0:n] + account.receiving_addresses = account.receiving_addresses[0:n] self.gap_limit = value self.storage.put('gap_limit', self.gap_limit, True) self.save_accounts() @@ -1264,12 +1264,12 @@ class Deterministic_Wallet(Abstract_Wallet): class BIP32_Wallet(Deterministic_Wallet): # abstract class, bip32 logic root_name = 'x/' - gap_limit = 20 def __init__(self, storage): Deterministic_Wallet.__init__(self, storage) self.master_public_keys = storage.get('master_public_keys', {}) self.master_private_keys = storage.get('master_private_keys', {}) + self.gap_limit = storage.get('gap_limit', 20) def is_watching_only(self): return not bool(self.master_private_keys) @@ -1539,7 +1539,6 @@ class Wallet_2of3(Wallet_2of2): class OldWallet(Deterministic_Wallet): wallet_type = 'old' - gap_limit = 5 def __init__(self, storage): Deterministic_Wallet.__init__(self, storage) @@ -1632,12 +1631,21 @@ class Wallet(object): seed_version = OLD_SEED_VERSION if len(storage.get('master_public_key','')) == 128 else NEW_SEED_VERSION if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]: - msg = "This wallet seed is not supported anymore." + msg = "Your wallet has an unsupported seed version." + msg += '\n\nWallet file: %s' % os.path.abspath(storage.path) if seed_version in [5, 7, 8, 9, 10]: - msg += "\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version + msg += "\n\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version + if seed_version == 6: + # version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog + msg += '\n\nThis file was created because of a bug in version 1.9.8.' + if storage.get('master_public_keys') is None and storage.get('master_private_keys') is None and storage.get('imported_keys') is None: + # pbkdf2 was not included with the binaries, and wallet creation aborted. + msg += "\nIt does not contain any keys, and can safely be removed." + else: + # creation was complete if electrum was run from source + msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet." raise BaseException(msg) - run_hook('add_wallet_types', wallet_types) wallet_type = storage.get('wallet_type') if wallet_type: for cat, t, name, c in wallet_types: diff --git a/lib/x509.py b/lib/x509.py index bebe2d72e..41da17a96 100644 --- a/lib/x509.py +++ b/lib/x509.py @@ -20,21 +20,9 @@ from datetime import datetime import sys -try: - import pyasn1 -except ImportError: - sys.exit("Error: pyasn1 does not seem to be installed. Try 'sudo pip install pyasn1'") - -try: - import pyasn1_modules -except ImportError: - sys.exit("Error: pyasn1 does not seem to be installed. Try 'sudo pip install pyasn1-modules'") - -try: - import tlslite -except ImportError: - sys.exit("Error: tlslite does not seem to be installed. Try 'sudo pip install tlslite'") - +import pyasn1 +import pyasn1_modules +import tlslite # workaround https://github.com/trevp/tlslite/issues/15 tlslite.utils.cryptomath.pycryptoLoaded = False diff --git a/plugins/audio_modem.py b/plugins/audio_modem.py index 2244edbbb..efe35a28b 100644 --- a/plugins/audio_modem.py +++ b/plugins/audio_modem.py @@ -106,10 +106,8 @@ class Plugin(BasePlugin): button.clicked.connect(handler) def _audio_interface(self): - return amodem.audio.Interface( - config=self.modem_config, - name=self.library_name - ) + interface = amodem.audio.Interface(config=self.modem_config) + return interface.load(self.library_name) def _send(self, parent, blob): def sender_thread(): diff --git a/plugins/btchipwallet.py b/plugins/btchipwallet.py index 7852b3997..727ddc951 100644 --- a/plugins/btchipwallet.py +++ b/plugins/btchipwallet.py @@ -15,7 +15,7 @@ from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, public_key_to from electrum.i18n import _ from electrum.plugins import BasePlugin, hook from electrum.transaction import deserialize -from electrum.wallet import NewWallet +from electrum.wallet import BIP32_HD_Wallet from electrum.util import format_satoshis import hashlib @@ -45,19 +45,21 @@ class Plugin(BasePlugin): def __init__(self, gui, name): BasePlugin.__init__(self, gui, name) self._is_available = self._init() - self.wallet = None - electrum.wallet.wallet_types.append(('hardware', 'btchip', _("BTChip wallet"), BTChipWallet)) - + self.wallet = None + if self._is_available: + electrum.wallet.wallet_types.append(('hardware', 'btchip', _("BTChip wallet"), BTChipWallet)) def _init(self): return BTCHIP - def is_available(self): - if self.wallet is None: - return self._is_available - if self.wallet.storage.get('wallet_type') == 'btchip': - return True - return False + def is_available(self): + if not self._is_available: + return False + if not self.wallet: + return False + if self.wallet.storage.get('wallet_type') != 'btchip': + return False + return True def set_enabled(self, enabled): self.wallet.storage.put('use_' + self.name, enabled) @@ -65,19 +67,30 @@ class Plugin(BasePlugin): def is_enabled(self): if not self.is_available(): return False - - if not self.wallet or self.wallet.storage.get('wallet_type') == 'btchip': - return True - - return self.wallet.storage.get('use_' + self.name) is True + if self.wallet.has_seed(): + return False + return True def enable(self): return BasePlugin.enable(self) + def btchip_is_connected(self): + try: + self.wallet.get_client().getFirmwareVersion() + except: + return False + return True + @hook def load_wallet(self, wallet): - self.wallet = wallet - + if self.btchip_is_connected(): + if not self.wallet.check_proper_device(): + QMessageBox.information(self.window, _('Error'), _("This wallet does not match your BTChip device"), _('OK')) + self.wallet.force_watching_only = True + else: + QMessageBox.information(self.window, _('Error'), _("BTChip device not detected.\nContinuing in watching-only mode."), _('OK')) + self.wallet.force_watching_only = True + @hook def installwizard_restore(self, wizard, storage): if storage.get('wallet_type') != 'btchip': @@ -98,16 +111,18 @@ class Plugin(BasePlugin): except Exception as e: tx.error = str(e) -class BTChipWallet(NewWallet): +class BTChipWallet(BIP32_HD_Wallet): wallet_type = 'btchip' + root_derivation = "m/44'/0'" def __init__(self, storage): - NewWallet.__init__(self, storage) + BIP32_HD_Wallet.__init__(self, storage) self.transport = None self.client = None self.mpk = None self.device_checked = False self.signing = False + self.force_watching_only = False def give_error(self, message, clear_client = False): if not self.signing: @@ -129,11 +144,8 @@ class BTChipWallet(NewWallet): def can_change_password(self): return False - def has_seed(self): - return False - def is_watching_only(self): - return False + return self.force_watching_only def get_client(self, noPin=False): if not BTCHIP: @@ -258,9 +270,6 @@ class BTChipWallet(NewWallet): def get_master_public_key(self): try: if not self.mpk: - self.get_client() # prompt for the PIN if necessary - if not self.check_proper_device(): - self.give_error('Wrong device or password') self.mpk = self.get_public_key("44'/0'") return self.mpk except Exception, e: @@ -278,6 +287,7 @@ class BTChipWallet(NewWallet): def sign_message(self, address, message, password): use2FA = False + self.signing = True self.get_client() # prompt for the PIN before displaying the dialog if necessary if not self.check_proper_device(): self.give_error('Wrong device or password') @@ -298,11 +308,15 @@ class BTChipWallet(NewWallet): self.get_client(True) signature = self.get_client().signMessageSign(pin) except Exception, e: - self.give_error(e, True) + if e.sw == 0x6a80: + self.give_error("Unfortunately, this message cannot be signed by BTChip. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.") + else: + self.give_error(e, True) finally: if waitDialog.waiting: waitDialog.emit(SIGNAL('dongle_done')) self.client.bad = use2FA + self.signing = False # Parse the ASN.1 signature diff --git a/plugins/cosigner_pool.py b/plugins/cosigner_pool.py index 63216b222..dd851acd0 100644 --- a/plugins/cosigner_pool.py +++ b/plugins/cosigner_pool.py @@ -93,14 +93,9 @@ class Plugin(BasePlugin): def init_qt(self, gui): self.win = gui.main_window self.win.connect(self.win, SIGNAL('cosigner:receive'), self.on_receive) - if self.listener is None: - self.listener = Listener(self) - self.listener.start() def enable(self): self.set_enabled(True) - if self.win.wallet: - self.load_wallet(self.win.wallet) return True def is_available(self): @@ -113,6 +108,9 @@ class Plugin(BasePlugin): self.wallet = wallet if not self.is_available(): return + if self.listener is None: + self.listener = Listener(self) + self.listener.start() mpk = self.wallet.get_master_public_keys() self.cosigner_list = [] for key, xpub in mpk.items(): diff --git a/plugins/trezor.py b/plugins/trezor.py index 66a0e1bdd..1048b6e02 100644 --- a/plugins/trezor.py +++ b/plugins/trezor.py @@ -49,17 +49,20 @@ class Plugin(BasePlugin): self._is_available = self._init() self._requires_settings = True self.wallet = None - electrum.wallet.wallet_types.append(('hardware', 'trezor', _("Trezor wallet"), TrezorWallet)) + if self._is_available: + electrum.wallet.wallet_types.append(('hardware', 'trezor', _("Trezor wallet"), TrezorWallet)) def _init(self): return TREZOR def is_available(self): - if self.wallet is None: - return self._is_available - if self.wallet.storage.get('wallet_type') == 'trezor': - return True - return False + if not self._is_available: + return False + if not self.wallet: + return False + if self.wallet.storage.get('wallet_type') != 'trezor': + return False + return True def requires_settings(self): return self._requires_settings @@ -70,11 +73,9 @@ class Plugin(BasePlugin): def is_enabled(self): if not self.is_available(): return False - - if not self.wallet or self.wallet.storage.get('wallet_type') == 'trezor': - return True - - return self.wallet.storage.get('use_' + self.name) is True + if self.wallet.has_seed(): + return False + return True def enable(self): return BasePlugin.enable(self) @@ -93,28 +94,34 @@ class Plugin(BasePlugin): @hook def close_wallet(self): print_error("trezor: clear session") - if self.wallet.client: + if self.wallet and self.wallet.client: self.wallet.client.clear_session() @hook def load_wallet(self, wallet): - self.wallet = wallet if self.trezor_is_connected(): if not self.wallet.check_proper_device(): QMessageBox.information(self.window, _('Error'), _("This wallet does not match your Trezor device"), _('OK')) + self.wallet.force_watching_only = True else: QMessageBox.information(self.window, _('Error'), _("Trezor device not detected.\nContinuing in watching-only mode."), _('OK')) + self.wallet.force_watching_only = True @hook def installwizard_restore(self, wizard, storage): if storage.get('wallet_type') != 'trezor': return - wallet = TrezorWallet(storage) - try: - wallet.create_main_account(None) - except BaseException as e: - QMessageBox.information(None, _('Error'), str(e), _('OK')) + seed = wizard.enter_seed_dialog("Enter your Trezor seed", None, func=lambda x:True) + if not seed: return + wallet = TrezorWallet(storage) + self.wallet = wallet + password = wizard.password_dialog() + wallet.add_seed(seed, password) + wallet.add_cosigner_seed(' '.join(seed.split()), 'x/', password) + wallet.create_main_account(password) + # disable trezor plugin + self.set_enabled(False) return wallet @hook @@ -161,6 +168,8 @@ class Plugin(BasePlugin): return False +from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root + class TrezorWallet(BIP32_HD_Wallet): wallet_type = 'trezor' root_derivation = "m/44'/0'" @@ -171,6 +180,7 @@ class TrezorWallet(BIP32_HD_Wallet): self.client = None self.mpk = None self.device_checked = False + self.force_watching_only = False def get_action(self): if not self.accounts: @@ -188,11 +198,8 @@ class TrezorWallet(BIP32_HD_Wallet): def can_change_password(self): return False - def has_seed(self): - return False - def is_watching_only(self): - return False + return self.force_watching_only def get_client(self): if not TREZOR: @@ -221,10 +228,23 @@ class TrezorWallet(BIP32_HD_Wallet): def create_main_account(self, password): self.create_account('Main account', None) #name, empty password + def mnemonic_to_seed(self, mnemonic, passphrase): + # trezor uses bip39 + import pbkdf2, hashlib, hmac + PBKDF2_ROUNDS = 2048 + mnemonic = ' '.join(mnemonic.split()) + return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) + def derive_xkeys(self, root, derivation, password): - derivation = derivation.replace(self.root_name,"44'/0'/") - xpub = self.get_public_key(derivation) - return xpub, None + x = self.master_private_keys.get(root) + if x: + root_xprv = pw_decode(x, password) + xprv, xpub = bip32_private_derivation(root_xprv, root, derivation) + return xpub, xprv + else: + derivation = derivation.replace(self.root_name,"44'/0'/") + xpub = self.get_public_key(derivation) + return xpub, None def get_public_key(self, bip32_path): address_n = self.get_client().expand_path(bip32_path) diff --git a/plugins/trustedcoin.py b/plugins/trustedcoin.py index 32a040342..2bfc132dd 100644 --- a/plugins/trustedcoin.py +++ b/plugins/trustedcoin.py @@ -223,8 +223,8 @@ class Plugin(BasePlugin): + _("For more information, visit") + " https://api.trustedcoin.com/#/electrum-help" def is_available(self): - if self.wallet is None: - return True + if not self.wallet: + return False if self.wallet.storage.get('wallet_type') == '2fa': return True return False @@ -238,10 +238,6 @@ class Plugin(BasePlugin): def is_enabled(self): if not self.is_available(): return False - if not self.wallet: - return True - if self.wallet.storage.get('wallet_type') != '2fa': - return False if self.wallet.master_private_keys.get('x2/'): return False return True @@ -344,15 +340,13 @@ class Plugin(BasePlugin): @hook def load_wallet(self, wallet): - self.wallet = wallet - if self.is_enabled(): - self.trustedcoin_button = StatusBarButton( QIcon(":icons/trustedcoin.png"), _("Network"), self.settings_dialog) - self.window.statusBar().addPermanentWidget(self.trustedcoin_button) - self.xpub = self.wallet.master_public_keys.get('x1/') - self.user_id = self.get_user_id()[1] - t = threading.Thread(target=self.request_billing_info) - t.setDaemon(True) - t.start() + self.trustedcoin_button = StatusBarButton( QIcon(":icons/trustedcoin.png"), _("Network"), self.settings_dialog) + self.window.statusBar().addPermanentWidget(self.trustedcoin_button) + self.xpub = self.wallet.master_public_keys.get('x1/') + self.user_id = self.get_user_id()[1] + t = threading.Thread(target=self.request_billing_info) + t.setDaemon(True) + t.start() @hook def close_wallet(self): @@ -481,6 +475,7 @@ class Plugin(BasePlugin): return 0 # trustedcoin won't charge if the total inputs is lower than their fee price = int(self.price_per_tx.get(1)) + assert price <= 100000 if tx.input_value() < price: print_error("not charging for this tx") return 0 diff --git a/setup-release.py b/setup-release.py index 62f9a8708..1ec18dd8c 100644 --- a/setup-release.py +++ b/setup-release.py @@ -37,7 +37,7 @@ if sys.platform == 'darwin': app=[mainscript], options=dict(py2app=dict(argv_emulation=True, includes=['PyQt4.QtCore', 'PyQt4.QtGui', 'PyQt4.QtWebKit', 'PyQt4.QtNetwork', 'sip'], - packages=['lib', 'gui', 'plugins'], + packages=['lib', 'gui', 'plugins', 'packages'], iconfile='electrum.icns', plist=plist, resources=["data", "icons"])), diff --git a/setup.py b/setup.py index 1d6eca546..264743e07 100644 --- a/setup.py +++ b/setup.py @@ -65,14 +65,15 @@ setup( name="Electrum", version=version.ELECTRUM_VERSION, install_requires=[ - 'slowaes', + 'slowaes>=0.1a1', 'ecdsa>=0.9', 'pbkdf2', 'requests', - 'pyasn1', 'pyasn1-modules', + 'pyasn1', 'qrcode', 'SocksiPy-branch', + 'protobuf', 'tlslite', 'dnspython' ],