diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py index 039000a1..250552ef 100644 --- a/gui/qt/installwizard.py +++ b/gui/qt/installwizard.py @@ -33,6 +33,7 @@ class InstallWizard(QDialog): self.network = network self.storage = storage self.setMinimumSize(575, 400) + self.setMaximumSize(575, 400) self.setWindowTitle('Electrum') self.connect(self, QtCore.SIGNAL('accept'), self.accept) @@ -313,16 +314,19 @@ class InstallWizard(QDialog): return None - def question(self, msg, icon=None): + def question(self, msg, yes_label=_('OK'), no_label=_('Cancel'), icon=None): vbox = QVBoxLayout() self.set_layout(vbox) if icon: logo = QLabel() logo.setPixmap(icon) vbox.addWidget(logo) - vbox.addWidget(QLabel(msg)) + + label = QLabel(msg) + label.setWordWrap(True) + vbox.addWidget(label) vbox.addStretch(1) - vbox.addLayout(ok_cancel_buttons(self, _('OK'))) + vbox.addLayout(ok_cancel_buttons(self, yes_label, no_label)) if not self.exec_(): return None return True @@ -343,29 +347,6 @@ class InstallWizard(QDialog): return run_password_dialog(self, None, self)[2] - def create_cold_seed(self, wallet): - from electrum.bitcoin import mnemonic_to_seed, bip32_root - msg = _('You are about to generate the cold storage seed of your wallet.') + '\n' \ - + _('For safety, you should do this on an offline computer.') - icon = QPixmap( ':icons/cold_seed.png').scaledToWidth(56) - if not self.question(msg, icon): - return - - cold_seed = wallet.make_seed() - if not self.show_seed(cold_seed, 'cold'): - return - if not self.verify_seed(cold_seed, 'cold'): - return - - hex_seed = mnemonic_to_seed(cold_seed,'').encode('hex') - xpriv, xpub = bip32_root(hex_seed) - wallet.add_master_public_key('cold/', xpub) - - msg = _('Your master public key was saved in your wallet file.') + '\n'\ - + _('Your cold seed must be stored on paper; it is not in the wallet file.')+ '\n\n' \ - + _('This program is about to close itself.') + '\n'\ - + _('You will need to reopen your wallet on an online computer, in order to complete the creation of your wallet') - self.show_message(msg) @@ -429,14 +410,13 @@ class InstallWizard(QDialog): return self.waiting_dialog(wallet.synchronize) - elif action == 'create_cold_seed': - self.create_cold_seed(wallet) - return - else: - r = run_hook('install_wizard_action', self, wallet, action) - if not r: - raise BaseException('unknown wizard action', action) + f = run_hook('get_wizard_action', self, wallet, action) + if not f: + raise BaseException('unknown wizard action', action) + r = f(wallet, self) + if not r: + return # next action action = wallet.get_action() @@ -558,6 +538,7 @@ class InstallWizard(QDialog): wallet.create_main_account(password) else: + self.storage.put('wallet_type', t) wallet = run_hook('installwizard_restore', self, self.storage) if not wallet: return diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 57fb1283..a3795997 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1988,6 +1988,7 @@ class ElectrumWindow(QMainWindow): decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password) message_e.setText(decrypted) except Exception as e: + traceback.print_exc(file=sys.stdout) self.show_message(str(e)) @@ -1998,6 +1999,7 @@ class ElectrumWindow(QMainWindow): encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text())) encrypted_e.setText(encrypted) except Exception as e: + traceback.print_exc(file=sys.stdout) self.show_message(str(e)) diff --git a/gui/qt/util.py b/gui/qt/util.py index c021e8b3..4998498d 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -94,10 +94,10 @@ def close_button(dialog, label=_("Close") ): b.setDefault(True) return hbox -def ok_cancel_buttons2(dialog, ok_label=_("OK") ): +def ok_cancel_buttons2(dialog, ok_label=_("OK"), cancel_label=_('Cancel')): hbox = QHBoxLayout() hbox.addStretch(1) - b = QPushButton(_("Cancel")) + b = QPushButton(cancel_label) hbox.addWidget(b) b.clicked.connect(dialog.reject) b = QPushButton(ok_label) @@ -106,8 +106,8 @@ def ok_cancel_buttons2(dialog, ok_label=_("OK") ): b.setDefault(True) return hbox, b -def ok_cancel_buttons(dialog, ok_label=_("OK") ): - hbox, b = ok_cancel_buttons2(dialog, ok_label) +def ok_cancel_buttons(dialog, ok_label=_("OK"), cancel_label=_('Cancel')): + hbox, b = ok_cancel_buttons2(dialog, ok_label, cancel_label) return hbox def line_dialog(parent, title, label, ok_label, default=None): diff --git a/icons.qrc b/icons.qrc index 5dce17bc..97bae3b2 100644 --- a/icons.qrc +++ b/icons.qrc @@ -25,5 +25,6 @@ icons/network.png icons/dark_background.png icons/qrcode.png + icons/trustedcoin.png diff --git a/icons/trustedcoin.png b/icons/trustedcoin.png new file mode 100644 index 00000000..2153866a Binary files /dev/null and b/icons/trustedcoin.png differ diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 39b33996..002dcdc2 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -706,10 +706,9 @@ def xpub_from_xprv(xprv, testnet=False): return EncodeBase58Check(xpub) -def bip32_root(mnemonic_seed, testnet=False): +def bip32_root(seed, testnet=False): import hmac header_pub, header_priv = _get_headers(testnet) - seed = mnemonic_to_seed(mnemonic_seed,'') I = hmac.new("Bitcoin seed", seed, hashlib.sha512).digest() master_k = I[0:32] master_c = I[32:] diff --git a/lib/commands.py b/lib/commands.py index 4f9df0fc..fc6dc855 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -320,7 +320,7 @@ class Commands: label, is_default_label = self.wallet.get_label(tx_hash) - out.append({'txid':tx_hash, 'date':"%16s"%time_str, 'label':label, 'value':format_satoshis(value)}) + out.append({'txid':tx_hash, 'date':"%16s"%time_str, 'label':label, 'value':format_satoshis(value), 'confirmations':conf}) return out def setlabel(self, key, label): diff --git a/lib/plugins.py b/lib/plugins.py index feea2fcf..46c1c396 100644 --- a/lib/plugins.py +++ b/lib/plugins.py @@ -50,6 +50,7 @@ def run_hook(name, *args): except Exception: print_error("Plugin error") traceback.print_exc(file=sys.stdout) + r = False if r: results.append(r) diff --git a/lib/tests/test_bitcoin.py b/lib/tests/test_bitcoin.py index e2227170..c61df592 100644 --- a/lib/tests/test_bitcoin.py +++ b/lib/tests/test_bitcoin.py @@ -68,7 +68,7 @@ class Test_bitcoin(unittest.TestCase): assert xprv == "tprv8jTo9vZtZTSiTo6BBDjeQDgLEaipWPsrhYsQpZYoqqJNPKbCyDewkHJZhkoSHiWYCUf1Gm4TFzQxcG4D6s1J9Hsn4whDK7QYyHHokJeUuac" def _do_test_bip32(self, seed, sequence, testnet): - xprv, xpub = bip32_root(seed, testnet) + xprv, xpub = bip32_root(seed.decode('hex'), testnet) assert sequence[0:2] == "m/" path = 'm' sequence = sequence[2:] diff --git a/lib/tests/test_wallet.py b/lib/tests/test_wallet.py index 49d22caf..e537c9a0 100644 --- a/lib/tests/test_wallet.py +++ b/lib/tests/test_wallet.py @@ -3,6 +3,7 @@ import tempfile import sys import unittest import os +import json from StringIO import StringIO from lib.wallet import WalletStorage, NewWallet @@ -97,20 +98,15 @@ class TestWalletStorage(WalletTestCase): contents = "" with open(path, "r") as f: contents = f.read() - self.assertEqual(repr(some_dict), contents) + self.assertEqual(some_dict, json.loads(contents)) class TestNewWallet(WalletTestCase): - seed_text = "The seed will sprout and grow up tall." + seed_text = "travel nowhere air position hill peace suffer parent beautiful rise blood power home crumble teach" password = "secret" - master_xpub = "xpub661MyMwAqRbcGEop5Rnp68oX1ikeFNVMtx1utwXZGRKMmeXVxwBM5UzkwU9nGB1EofZekLDRfi1w5F9P7Vac3PEuWdWHr2gHLW8vp5YyKJ1" - master_xpriv = "xprv9s21ZrQH143K3kjLyQFoizrnTgv9qumWXj6K6Z7wi5nNtrCMRPs6XggH6Bbgz9CUgPJnZnV74yUdRSr8qWVELr9QQTgU5aNL33ViMyD9nhs" - first_account_name = "account1" - first_account_first_address = "1Jv9pLCJ4Sqr7aDYLGX5QhET4ps5qRcB9V" - first_account_second_address = "14n9EsZsgTTc4eC4TxeP1ccP8bXgwxPMmL" import_private_key = "L52XzL2cMkHxqxBXRyEpnPQZGUs3uKiL3R11XbAdHigRzDozKZeW" import_key_address = "15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma" @@ -131,7 +127,8 @@ class TestNewWallet(WalletTestCase): # in setUp() new_dir = tempfile.mkdtemp() config = FakeConfig(new_dir) - wallet = NewWallet(config) + storage = WalletStorage(config) + wallet = NewWallet(storage) self.assertTrue(wallet.is_watching_only()) shutil.rmtree(new_dir) # Don't leave useless stuff in /tmp @@ -142,22 +139,6 @@ class TestNewWallet(WalletTestCase): self.assertEqual(self.wallet.get_seed(self.password), self.seed_text) self.assertEqual(0, len(self.wallet.addresses())) - def test_add_account(self): - self.wallet.create_account(self.first_account_name, self.password) - self.assertEqual(1, len(self.wallet.addresses())) - self.assertIn(self.first_account_first_address, - self.wallet.addresses()) - - def test_add_account_add_address(self): - self.wallet.create_account(self.first_account_name, self.password) - self.wallet.synchronizer = FakeSynchronizer() - - self.wallet.create_new_address() - self.assertEqual(2, len(self.wallet.addresses())) - self.assertIn(self.first_account_first_address, - self.wallet.addresses()) - self.assertIn(self.first_account_second_address, - self.wallet.addresses()) def test_key_import(self): # Wallets have no imported keys by default. diff --git a/lib/wallet.py b/lib/wallet.py index 09830457..5bb75839 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1385,7 +1385,7 @@ class BIP39_Wallet(BIP32_Wallet): def create_master_keys(self, password): seed = self.get_seed(password) - xprv, xpub = bip32_root(seed) + xprv, xpub = bip32_root(mnemonic_to_seed(seed,'')) xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation) self.add_master_public_key(self.root_name, xpub) self.add_master_private_key(self.root_name, xprv, password) @@ -1459,11 +1459,17 @@ class Wallet_2of2(BIP39_Wallet): def add_cosigner_seed(self, seed, name, password): # we don't store the seed, only the master xpriv - xprv, xpub = bip32_root(seed) + xprv, xpub = bip32_root(mnemonic_to_seed(seed,'')) xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation) self.add_master_public_key(name, xpub) self.add_master_private_key(name, xprv, password) + def add_cosigner_xpub(self, seed, name): + # store only master xpub + xprv, xpub = bip32_root(mnemonic_to_seed(seed,'')) + xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation) + self.add_master_public_key(name, xpub) + class Wallet_2of3(Wallet_2of2): diff --git a/plugins/cosigner_pool.py b/plugins/cosigner_pool.py index a8e09764..69f1b0a4 100644 --- a/plugins/cosigner_pool.py +++ b/plugins/cosigner_pool.py @@ -45,42 +45,36 @@ class Listener(threading.Thread): threading.Thread.__init__(self) self.daemon = True self.parent = parent - self.key = None + self.keyname = None + self.keyhash = None self.is_running = False self.message = None - self.delete = False - - def set_key(self, key): - self.key = key + + def set_key(self, keyname, keyhash): + self.keyname = keyname + self.keyhash = keyhash def clear(self): - self.delete = True + server.delete(self.keyhash) + self.message = None + def run(self): self.is_running = True while self.is_running: - - if not self.key: + if not self.keyhash: time.sleep(2) continue - if not self.message: try: - self.message = server.get(self.key) + self.message = server.get(self.keyhash) except Exception as e: util.print_error("cannot contact cosigner pool") time.sleep(30) continue - if self.message: self.parent.win.emit(SIGNAL("cosigner:receive")) - else: - if self.delete: - # save it to disk - server.delete(self.key) - self.message = None - self.delete = False - + # poll every 30 seconds time.sleep(30) @@ -109,21 +103,25 @@ class Plugin(BasePlugin): self.load_wallet(self.win.wallet) return True + def is_available(self): + if self.wallet is None: + return True + return self.wallet.wallet_type in ['2of2', '2of3'] + def load_wallet(self, wallet): self.wallet = wallet + if not self.is_available(): + return mpk = self.wallet.get_master_public_keys() - - self.cold = mpk.get('x2') - if self.cold: - self.cold_K = bitcoin.deserialize_xkey(self.cold)[-1].encode('hex') - self.cold_hash = bitcoin.Hash(self.cold_K).encode('hex') - - self.hot = mpk.get('x1') - if self.hot: - self.hot_K = bitcoin.deserialize_xkey(self.hot)[-1].encode('hex') - self.hot_hash = bitcoin.Hash(self.hot_K).encode('hex') - self.listener.set_key(self.hot_hash) - + self.cosigner_list = [] + for key, xpub in mpk.items(): + keyname = key + '/' # fixme + K = bitcoin.deserialize_xkey(xpub)[-1].encode('hex') + _hash = bitcoin.Hash(K).encode('hex') + if self.wallet.master_private_keys.get(keyname): + self.listener.set_key(keyname, _hash) + else: + self.cosigner_list.append((xpub, K, _hash)) def transaction_dialog(self, d): self.send_button = b = QPushButton(_("Send to cosigner")) @@ -131,18 +129,18 @@ class Plugin(BasePlugin): d.buttons.insertWidget(2, b) self.transaction_dialog_update(d) - def transaction_dialog_update(self, d): if d.tx.is_complete(): self.send_button.hide() return - if self.cosigner_can_sign(d.tx): - self.send_button.show() + for xpub, K, _hash in self.cosigner_list: + if self.cosigner_can_sign(d.tx, xpub): + self.send_button.show() + break else: self.send_button.hide() - - def cosigner_can_sign(self, tx): + def cosigner_can_sign(self, tx, cosigner_xpub): from electrum.transaction import x_to_xpub xpub_set = set([]) for txin in tx.inputs: @@ -151,24 +149,21 @@ class Plugin(BasePlugin): if xpub: xpub_set.add(xpub) - return self.cold in xpub_set - + return cosigner_xpub in xpub_set def do_send(self, tx): - if not self.cosigner_can_sign(tx): - return - - message = bitcoin.encrypt_message(tx.raw, self.cold_K) - - try: - server.put(self.cold_hash, message) - self.win.show_message("Your transaction was sent to the cosigning pool.\nOpen your cosigner wallet to retrieve it.") - except Exception as e: - self.win.show_message(str(e)) - + for xpub, K, _hash in self.cosigner_list: + if not self.cosigner_can_sign(tx, xpub): + continue + message = bitcoin.encrypt_message(tx.raw, K) + try: + server.put(_hash, message) + except Exception as e: + self.win.show_message(str(e)) + return + self.win.show_message("Your transaction was sent to the cosigning pool.\nOpen your cosigner wallet to retrieve it.") def on_receive(self): - if self.wallet.use_encryption: password = self.win.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease enter your password to decrypt it.') if not password: @@ -177,11 +172,12 @@ class Plugin(BasePlugin): password = None message = self.listener.message - xpriv = self.wallet.get_master_private_key('x1/', password) - if not xpriv: + key = self.listener.keyname + xprv = self.wallet.get_master_private_key(key, password) + if not xprv: return try: - k = bitcoin.deserialize_xkey(xpriv)[-1].encode('hex') + k = bitcoin.deserialize_xkey(xprv)[-1].encode('hex') EC = bitcoin.EC_KEY(k.decode('hex')) message = EC.decrypt_message(message) except Exception as e: @@ -190,7 +186,6 @@ class Plugin(BasePlugin): return self.listener.clear() - tx = transaction.Transaction.deserialize(message) self.win.show_transaction(tx) diff --git a/plugins/trezor.py b/plugins/trezor.py index 0ebfb6dc..79bb85e0 100644 --- a/plugins/trezor.py +++ b/plugins/trezor.py @@ -1,4 +1,4 @@ -from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL +from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton import PyQt4.QtCore as QtCore from binascii import unhexlify from struct import pack @@ -7,7 +7,7 @@ from time import sleep from base64 import b64encode, b64decode from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog -from electrum_gui.qt.util import ok_cancel_buttons +from electrum_gui.qt.util import ok_cancel_buttons, EnterButton from electrum.account import BIP32_Account from electrum.bitcoin import EncodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160 from electrum.i18n import _ @@ -43,6 +43,7 @@ class Plugin(BasePlugin): def __init__(self, gui, name): BasePlugin.__init__(self, gui, name) self._is_available = self._init() + self._requires_settings = True self.wallet = None def _init(self): @@ -55,6 +56,9 @@ class Plugin(BasePlugin): return True return False + def requires_settings(self): + return self._requires_settings + def set_enabled(self, enabled): self.wallet.storage.put('use_' + self.name, enabled) @@ -77,6 +81,8 @@ class Plugin(BasePlugin): wallet_types.append(('trezor', _("Trezor wallet"), TrezorWallet)) def installwizard_restore(self, wizard, storage): + if storage.get('wallet_type') != 'trezor': + return wallet = TrezorWallet(storage) try: wallet.create_main_account(None) @@ -91,6 +97,41 @@ class Plugin(BasePlugin): except Exception as e: tx.error = str(e) + def settings_widget(self, window): + return EnterButton(_('Settings'), self.settings_dialog) + + def settings_dialog(self): + get_label = lambda: self.wallet.get_client().features.label + update_label = lambda: current_label_label.setText("Label: %s" % get_label()) + + d = QDialog() + layout = QGridLayout(d) + layout.addWidget(QLabel("Trezor Options"),0,0) + layout.addWidget(QLabel("ID:"),1,0) + layout.addWidget(QLabel(" %s" % self.wallet.get_client().get_device_id()),1,1) + + def modify_label(): + response = QInputDialog().getText(None, "Set New Trezor Label", "New Trezor Label: (upon submission confirm on Trezor)") + if not response[1]: + return + new_label = str(response[0]) + twd.start("Please confirm label change on Trezor") + status = self.wallet.get_client().apply_settings(label=new_label) + twd.stop() + update_label() + + current_label_label = QLabel() + update_label() + change_label_button = QPushButton("Modify") + change_label_button.clicked.connect(modify_label) + layout.addWidget(current_label_label,3,0) + layout.addWidget(change_label_button,3,1) + + if d.exec_(): + return True + else: + return False + class TrezorWallet(NewWallet): wallet_type = 'trezor' @@ -138,7 +179,7 @@ class TrezorWallet(NewWallet): def address_id(self, address): account_id, (change, address_index) = self.get_address_index(address) - return "%s/%d/%d" % (account_id, change, address_index) + return "44'/0'/%s'/%d/%d" % (account_id, change, address_index) def create_main_account(self, password): self.create_account('Main account', None) #name, empty password @@ -167,12 +208,9 @@ class TrezorWallet(NewWallet): pass def decrypt_message(self, pubkey, message, password): - try: - address = public_key_to_bc_address(pubkey.decode('hex')) - address_path = self.address_id(address) - address_n = self.get_client().expand_path(address_path) - except Exception, e: - raise e + address = public_key_to_bc_address(pubkey.decode('hex')) + address_path = self.address_id(address) + address_n = self.get_client().expand_path(address_path) try: decrypted_msg = self.get_client().decrypt_message(address_n, b64decode(message)) except Exception, e: @@ -182,6 +220,8 @@ class TrezorWallet(NewWallet): return str(decrypted_msg) def sign_message(self, address, message, password): + if not self.check_proper_device(): + give_error('Wrong device or password') try: address_path = self.address_id(address) address_n = self.get_client().expand_path(address_path)