electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 2c0489c8094b2ba9409ae0e36bfe674ce327d883
parent 175fdbcac6fc1bd5ed54ceb709200b746877935d
Author: ThomasV <thomasv@electrum.org>
Date:   Mon, 23 Nov 2015 14:15:25 +0100

plugins: separate GUIs using child classes

Diffstat:
Mgui/qt/main_window.py | 2--
Mlib/plugins.py | 11++++++++++-
Mplugins/audio_modem.py | 2+-
Mplugins/btchipwallet.py | 194+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mplugins/cosigner_pool.py | 2+-
Mplugins/email_requests.py | 2+-
Mplugins/exchange_rate.py | 175+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mplugins/greenaddress_instant.py | 2+-
Mplugins/keepkey.py | 498++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mplugins/labels.py | 171++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mplugins/plot.py | 2+-
Mplugins/trezor.py | 500++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mplugins/trustedcoin.py | 19++++++++++++-------
Mplugins/virtualkeyboard.py | 2+-
14 files changed, 808 insertions(+), 774 deletions(-)

diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -107,7 +107,6 @@ expiration_values = [ class ElectrumWindow(QMainWindow, PrintError): - labelsChanged = pyqtSignal() def __init__(self, config, network, gui_object): QMainWindow.__init__(self) @@ -157,7 +156,6 @@ class ElectrumWindow(QMainWindow, PrintError): self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok) self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error) - self.labelsChanged.connect(self.update_tabs) self.history_list.setFocus(True) # network callbacks diff --git a/lib/plugins.py b/lib/plugins.py @@ -40,6 +40,7 @@ class Plugins(PrintError): self.plugins = {} self.network = None + self.gui_name = gui_name self.descriptions = plugins.descriptions for item in self.descriptions: name = item['name'] @@ -66,7 +67,15 @@ class Plugins(PrintError): p = imp.load_source(full_name, path) else: p = __import__(full_name, fromlist=['electrum_plugins']) - plugin = p.Plugin(self, config, name) + + if self.gui_name == 'qt': + klass = p.QtPlugin + elif self.gui_name == 'cmdline': + klass = p.CmdlinePlugin + else: + return + + plugin = klass(self, config, name) if self.network: self.network.add_jobs(plugin.thread_jobs()) self.plugins[name] = plugin diff --git a/plugins/audio_modem.py b/plugins/audio_modem.py @@ -25,7 +25,7 @@ except ImportError: print_error('Audio MODEM is not found.') -class Plugin(BasePlugin): +class QtPlugin(BasePlugin): def __init__(self, parent, config, name): BasePlugin.__init__(self, parent, config, name) diff --git a/plugins/btchipwallet.py b/plugins/btchipwallet.py @@ -1,5 +1,3 @@ -from PyQt4.Qt import QApplication, QMessageBox, QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, QThread, SIGNAL -import PyQt4.QtCore as QtCore from binascii import unhexlify from binascii import hexlify from struct import pack,unpack @@ -7,7 +5,6 @@ from sys import stderr from time import sleep import electrum -from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog from electrum.account import BIP32_Account from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160 from electrum.i18n import _ @@ -32,96 +29,6 @@ try: except ImportError: BTCHIP = False -class Plugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self._is_available = self._init() - self.wallet = None - self.handler = None - - def constructor(self, s): - return BTChipWallet(s) - - def _init(self): - return BTCHIP - - 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) - - def is_enabled(self): - if not self.is_available(): - return False - if self.wallet.has_seed(): - return False - return True - - def btchip_is_connected(self): - try: - self.wallet.get_client().getFirmwareVersion() - except: - return False - return True - - @hook - def cmdline_load_wallet(self, wallet): - self.wallet = wallet - self.wallet.plugin = self - if self.handler is None: - self.handler = BTChipCmdLineHandler() - - @hook - def load_wallet(self, wallet, window): - self.wallet = wallet - self.wallet.plugin = self - if self.handler is None: - self.handler = BTChipQTHandler(window) - if self.btchip_is_connected(): - if not self.wallet.check_proper_device(): - QMessageBox.information(window, _('Error'), _("This wallet does not match your Ledger device"), _('OK')) - self.wallet.force_watching_only = True - else: - QMessageBox.information(window, _('Error'), _("Ledger device not detected.\nContinuing in watching-only mode."), _('OK')) - self.wallet.force_watching_only = True - - @hook - def close_wallet(self): - self.wallet = None - - @hook - def installwizard_load_wallet(self, wallet, window): - if type(wallet) != BTChipWallet: - return - self.load_wallet(wallet, window) - - @hook - def installwizard_restore(self, wizard, storage): - if storage.get('wallet_type') != 'btchip': - return - wallet = BTChipWallet(storage) - try: - wallet.create_main_account(None) - except BaseException as e: - QMessageBox.information(None, _('Error'), str(e), _('OK')) - return - return wallet - - @hook - def sign_tx(self, window, tx): - tx.error = None - try: - self.wallet.sign_transaction(tx, None) - except Exception as e: - tx.error = str(e) class BTChipWallet(BIP32_HD_Wallet): wallet_type = 'btchip' @@ -517,6 +424,98 @@ class BTChipWallet(BIP32_HD_Wallet): return False, None, None return True, response, response + +class Plugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self._is_available = self._init() + self.wallet = None + self.handler = None + + def constructor(self, s): + return BTChipWallet(s) + + def _init(self): + return BTCHIP + + 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) + + def is_enabled(self): + if not self.is_available(): + return False + if self.wallet.has_seed(): + return False + return True + + def btchip_is_connected(self): + try: + self.wallet.get_client().getFirmwareVersion() + except: + return False + return True + + @hook + def close_wallet(self): + self.wallet = None + + @hook + def installwizard_load_wallet(self, wallet, window): + if type(wallet) != BTChipWallet: + return + self.load_wallet(wallet, window) + + @hook + def installwizard_restore(self, wizard, storage): + if storage.get('wallet_type') != 'btchip': + return + wallet = BTChipWallet(storage) + try: + wallet.create_main_account(None) + except BaseException as e: + QMessageBox.information(None, _('Error'), str(e), _('OK')) + return + return wallet + + @hook + def sign_tx(self, window, tx): + tx.error = None + try: + self.wallet.sign_transaction(tx, None) + except Exception as e: + tx.error = str(e) + +from PyQt4.Qt import QApplication, QMessageBox, QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, QThread, SIGNAL +import PyQt4.QtCore as QtCore +from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog + +class QtPlugin(Plugin): + + @hook + def load_wallet(self, wallet, window): + self.wallet = wallet + self.wallet.plugin = self + if self.handler is None: + self.handler = BTChipQTHandler(window) + if self.btchip_is_connected(): + if not self.wallet.check_proper_device(): + QMessageBox.information(window, _('Error'), _("This wallet does not match your Ledger device"), _('OK')) + self.wallet.force_watching_only = True + else: + QMessageBox.information(window, _('Error'), _("Ledger device not detected.\nContinuing in watching-only mode."), _('OK')) + self.wallet.force_watching_only = True + + class BTChipQTHandler: def __init__(self, win): @@ -563,6 +562,15 @@ class BTChipQTHandler: self.d.hide() self.d = None +class CmdlinePlugin(Plugin): + @hook + def cmdline_load_wallet(self, wallet): + self.wallet = wallet + self.wallet.plugin = self + if self.handler is None: + self.handler = BTChipCmdLineHandler() + + class BTChipCmdLineHandler: def stop(self): diff --git a/plugins/cosigner_pool.py b/plugins/cosigner_pool.py @@ -79,7 +79,7 @@ class Listener(util.DaemonThread): time.sleep(30) -class Plugin(BasePlugin): +class QtPlugin(BasePlugin): def __init__(self, parent, config, name): BasePlugin.__init__(self, parent, config, name) diff --git a/plugins/email_requests.py b/plugins/email_requests.py @@ -101,7 +101,7 @@ class Processor(threading.Thread): s.quit() -class Plugin(BasePlugin): +class QtPlugin(BasePlugin): def fullname(self): return 'Email' diff --git a/plugins/exchange_rate.py b/plugins/exchange_rate.py @@ -1,6 +1,3 @@ -from PyQt4.QtGui import * -from PyQt4.QtCore import * - from datetime import datetime import inspect import requests @@ -17,8 +14,7 @@ from electrum.plugins import BasePlugin, hook from electrum.i18n import _ from electrum.util import PrintError, ThreadJob, timestamp_to_datetime from electrum.util import format_satoshis -from electrum_gui.qt.util import * -from electrum_gui.qt.amountedit import AmountEdit + # See https://en.wikipedia.org/wiki/ISO_4217 CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, @@ -301,25 +297,79 @@ class Plugin(BasePlugin, ThreadJob): + def exchange_rate(self): + '''Returns None, or the exchange rate as a Decimal''' + rate = self.exchange.quotes.get(self.ccy) + if rate: + return Decimal(rate) + @hook - def on_new_window(self, window): - # Additional send and receive edit boxes - send_e = AmountEdit(self.config_ccy) - window.send_grid.addWidget(send_e, 4, 2, Qt.AlignLeft) - window.amount_e.frozen.connect( - lambda: send_e.setFrozen(window.amount_e.isReadOnly())) - receive_e = AmountEdit(self.config_ccy) - window.receive_grid.addWidget(receive_e, 2, 2, Qt.AlignLeft) - window.fiat_send_e = send_e - window.fiat_receive_e = receive_e - self.connect_fields(window, window.amount_e, send_e, window.fee_e) - self.connect_fields(window, window.receive_amount_e, receive_e, None) - window.history_list.refresh_headers() - window.update_status() - window.connect(window.app, SIGNAL('new_fx_quotes'), lambda: self.on_fx_quotes(window)) - window.connect(window.app, SIGNAL('new_fx_history'), lambda: self.on_fx_history(window)) - window.connect(window.app, SIGNAL('close_fx_plugin'), lambda: self.restore_window(window)) - window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers) + 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) + + @hook + def get_fiat_status_text(self, btc_balance): + rate = self.exchange_rate() + return _(" (No FX rate available)") if rate is None else "1 BTC~%s %s" % (self.value_str(COIN, rate), self.ccy) + + def get_historical_rates(self): + if self.show_history(): + self.exchange.get_historical_rates(self.ccy) + + def requires_settings(self): + return True + + def value_str(self, satoshis, rate): + if satoshis is None: # Can happen with incomplete history + return _("Unknown") + if rate: + value = Decimal(satoshis) / COIN * Decimal(rate) + return "%s" % (self.ccy_amount_str(value, True)) + return _("No data") + + @hook + def historical_value_str(self, satoshis, d_t): + 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) + self.history_used_spot = True + return self.value_str(satoshis, rate) + + @hook + def history_tab_headers(self, headers): + if self.show_history(): + headers.extend(['%s '%self.ccy + _('Amount'), '%s '%self.ccy + _('Balance')]) + + @hook + def history_tab_update_begin(self): + self.history_used_spot = False + + @hook + def history_tab_update(self, tx, entry): + if not self.show_history(): + return + tx_hash, conf, value, timestamp, balance = tx + if conf <= 0: + date = datetime.today() + else: + date = timestamp_to_datetime(timestamp) + for amount in [value, balance]: + text = self.historical_value_str(amount, date) + entry.append(text) + + + + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from electrum_gui.qt.util import * +from electrum_gui.qt.amountedit import AmountEdit + + +class QtPlugin(Plugin): def connect_fields(self, window, btc_e, fiat_e, fee_e): @@ -412,68 +462,25 @@ class Plugin(BasePlugin, ThreadJob): combo.blockSignals(False) combo.setCurrentIndex(combo.findText(self.ccy)) - 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) - - @hook - 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) - - @hook - def get_fiat_status_text(self, btc_balance): - rate = self.exchange_rate() - return _(" (No FX rate available)") if rate is None else "1 BTC~%s %s" % (self.value_str(COIN, rate), self.ccy) - - def get_historical_rates(self): - if self.show_history(): - self.exchange.get_historical_rates(self.ccy) - - def requires_settings(self): - return True - - def value_str(self, satoshis, rate): - if satoshis is None: # Can happen with incomplete history - return _("Unknown") - if rate: - value = Decimal(satoshis) / COIN * Decimal(rate) - return "%s" % (self.ccy_amount_str(value, True)) - return _("No data") - @hook - def historical_value_str(self, satoshis, d_t): - 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) - self.history_used_spot = True - return self.value_str(satoshis, rate) - - @hook - def history_tab_headers(self, headers): - if self.show_history(): - headers.extend(['%s '%self.ccy + _('Amount'), '%s '%self.ccy + _('Balance')]) - - @hook - def history_tab_update_begin(self): - self.history_used_spot = False - - @hook - def history_tab_update(self, tx, entry): - if not self.show_history(): - return - tx_hash, conf, value, timestamp, balance = tx - if conf <= 0: - date = datetime.today() - else: - date = timestamp_to_datetime(timestamp) - for amount in [value, balance]: - text = self.historical_value_str(amount, date) - entry.append(text) + def on_new_window(self, window): + # Additional send and receive edit boxes + send_e = AmountEdit(self.config_ccy) + window.send_grid.addWidget(send_e, 4, 2, Qt.AlignLeft) + window.amount_e.frozen.connect( + lambda: send_e.setFrozen(window.amount_e.isReadOnly())) + receive_e = AmountEdit(self.config_ccy) + window.receive_grid.addWidget(receive_e, 2, 2, Qt.AlignLeft) + window.fiat_send_e = send_e + window.fiat_receive_e = receive_e + self.connect_fields(window, window.amount_e, send_e, window.fee_e) + self.connect_fields(window, window.receive_amount_e, receive_e, None) + window.history_list.refresh_headers() + window.update_status() + window.connect(window.app, SIGNAL('new_fx_quotes'), lambda: self.on_fx_quotes(window)) + window.connect(window.app, SIGNAL('new_fx_history'), lambda: self.on_fx_history(window)) + window.connect(window.app, SIGNAL('close_fx_plugin'), lambda: self.restore_window(window)) + window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers) def settings_widget(self, window): return EnterButton(_('Settings'), self.settings_dialog) diff --git a/plugins/greenaddress_instant.py b/plugins/greenaddress_instant.py @@ -28,7 +28,7 @@ from electrum.i18n import _ -class Plugin(BasePlugin): +class QtPlugin(BasePlugin): button_label = _("Verify GA instant") diff --git a/plugins/keepkey.py b/plugins/keepkey.py @@ -7,8 +7,6 @@ import threading import re from functools import partial -from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton -import PyQt4.QtCore as QtCore import electrum from electrum import bitcoin @@ -22,14 +20,10 @@ from electrum.wallet import BIP32_HD_Wallet from electrum.util import print_error, print_msg from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root -from electrum_gui.qt.util import * -from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow -from electrum_gui.qt.installwizard import InstallWizard try: from keepkeylib.client import types from keepkeylib.client import proto, BaseClient, ProtocolMixin - from keepkeylib.qt.pinmatrix import PinMatrixWidget from keepkeylib.transport import ConnectionError from keepkeylib.transport_hid import HidTransport KEEPKEY = True @@ -47,6 +41,172 @@ def give_error(message): raise Exception(message) + + +class KeepKeyWallet(BIP32_HD_Wallet): + wallet_type = 'keepkey' + root_derivation = "m/44'/0'" + + def __init__(self, storage): + BIP32_HD_Wallet.__init__(self, storage) + self.mpk = None + self.device_checked = False + self.proper_device = False + self.force_watching_only = False + + def get_action(self): + if not self.accounts: + return 'create_accounts' + + def can_import(self): + return False + + def can_sign_xpubkey(self, x_pubkey): + xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) + return xpub in self.master_public_keys.values() + + def can_export(self): + return False + + def can_create_accounts(self): + return True + + def can_change_password(self): + return False + + def is_watching_only(self): + return self.force_watching_only + + def get_client(self): + return self.plugin.get_client() + + def address_id(self, address): + account_id, (change, address_index) = self.get_address_index(address) + 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 + + def mnemonic_to_seed(self, mnemonic, passphrase): + # keepkey uses bip39 + import pbkdf2, hashlib, hmac + PBKDF2_ROUNDS = 2048 + mnemonic = unicodedata.normalize('NFKD', ' '.join(mnemonic.split())) + passphrase = unicodedata.normalize('NFKD', passphrase) + return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) + + def derive_xkeys(self, root, derivation, password): + 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.plugin.get_client().expand_path(bip32_path) + node = self.plugin.get_client().get_public_node(address_n).node + xpub = "0488B21E".decode('hex') + chr(node.depth) + self.i4b(node.fingerprint) + self.i4b(node.child_num) + node.chain_code + node.public_key + return EncodeBase58Check(xpub) + + def get_master_public_key(self): + if not self.mpk: + self.mpk = self.get_public_key("44'/0'") + return self.mpk + + def i4b(self, x): + return pack('>I', x) + + def add_keypairs(self, tx, keypairs, password): + #do nothing - no priv keys available + pass + + def decrypt_message(self, pubkey, message, password): + raise BaseException( _('Decrypt method is not implemented in KeepKey') ) + #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: + # give_error(e) + #finally: + # twd.stop() + #return str(decrypted_msg) + + def sign_message(self, address, message, password): + if self.has_seed(): + return BIP32_HD_Wallet.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.plugin.get_client().expand_path(address_path) + except Exception, e: + give_error(e) + try: + msg_sig = self.plugin.get_client().sign_message('Bitcoin', address_n, message) + except Exception, e: + give_error(e) + finally: + self.plugin.handler.stop() + return msg_sig.signature + + def sign_transaction(self, tx, password): + if self.has_seed(): + return BIP32_HD_Wallet.sign_transaction(self, tx, password) + if tx.is_complete(): + return + if not self.check_proper_device(): + give_error('Wrong device or password') + # previous transactions used as inputs + prev_tx = {} + # path of the xpubs that are involved + xpub_path = {} + for txin in tx.inputs: + tx_hash = txin['prevout_hash'] + + ptx = self.transactions.get(tx_hash) + if ptx is None: + ptx = self.network.synchronous_get(('blockchain.transaction.get', [tx_hash])) + ptx = Transaction(ptx) + prev_tx[tx_hash] = ptx + + for x_pubkey in txin['x_pubkeys']: + account_derivation = None + if not is_extended_pubkey(x_pubkey): + continue + xpub = x_to_xpub(x_pubkey) + for k, v in self.master_public_keys.items(): + if v == xpub: + account_id = re.match("x/(\d+)'", k).group(1) + account_derivation = "44'/0'/%s'"%account_id + if account_derivation is not None: + xpub_path[xpub] = account_derivation + + self.plugin.sign_transaction(tx, prev_tx, xpub_path) + + def check_proper_device(self): + self.get_client().ping('t') + if not self.device_checked: + address = self.addresses(False)[0] + address_id = self.address_id(address) + n = self.get_client().expand_path(address_id) + device_address = self.get_client().get_address('Bitcoin', n) + self.device_checked = True + + if device_address != address: + self.proper_device = False + else: + self.proper_device = True + + return self.proper_device + + + class Plugin(BasePlugin): def __init__(self, parent, config, name): @@ -118,64 +278,6 @@ class Plugin(BasePlugin): self.client = None self.wallet = None - @hook - def cmdline_load_wallet(self, wallet): - self.wallet = wallet - self.wallet.plugin = self - if self.handler is None: - self.handler = KeepKeyCmdLineHandler() - - @hook - def load_wallet(self, wallet, window): - self.print_error("load_wallet") - self.wallet = wallet - self.wallet.plugin = self - self.keepkey_button = StatusBarButton(QIcon(":icons/keepkey.png"), _("KeepKey"), partial(self.settings_dialog, window)) - if type(window) is ElectrumWindow: - window.statusBar().addPermanentWidget(self.keepkey_button) - if self.handler is None: - self.handler = KeepKeyQtHandler(window) - try: - self.get_client().ping('t') - except BaseException as e: - QMessageBox.information(window, _('Error'), _("KeepKey device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) - self.wallet.force_watching_only = True - return - if self.wallet.addresses() and not self.wallet.check_proper_device(): - QMessageBox.information(window, _('Error'), _("This wallet does not match your KeepKey device"), _('OK')) - self.wallet.force_watching_only = True - - @hook - def installwizard_load_wallet(self, wallet, window): - if type(wallet) != KeepKeyWallet: - return - self.load_wallet(wallet, window) - - @hook - def installwizard_restore(self, wizard, storage): - if storage.get('wallet_type') != 'keepkey': - return - seed = wizard.enter_seed_dialog("Enter your KeepKey seed", None, func=lambda x:True) - if not seed: - return - wallet = KeepKeyWallet(storage) - self.wallet = wallet - handler = KeepKeyQtHandler(wizard) - passphrase = handler.get_passphrase(_("Please enter your KeepKey passphrase.") + '\n' + _("Press OK if you do not use one.")) - if passphrase is None: - return - password = wizard.password_dialog() - wallet.add_seed(seed, password) - wallet.add_cosigner_seed(seed, 'x/', password, passphrase) - wallet.create_main_account(password) - # disable keepkey plugin - self.set_enabled(False) - return wallet - - @hook - def receive_menu(self, menu, addrs): - if not self.wallet.is_watching_only() and self.atleast_version(1, 3) and len(addrs) == 1: - menu.addAction(_("Show on TREZOR"), lambda: self.show_address(addrs[0])) def show_address(self, address): if not self.wallet.check_proper_device(): @@ -193,39 +295,6 @@ class Plugin(BasePlugin): self.handler.stop() - def settings_dialog(self, window): - try: - device_id = self.get_client().get_device_id() - except BaseException as e: - window.show_message(str(e)) - return - get_label = lambda: self.get_client().features.label - update_label = lambda: current_label_label.setText("Label: %s" % get_label()) - d = QDialog() - layout = QGridLayout(d) - layout.addWidget(QLabel("KeepKey Options"),0,0) - layout.addWidget(QLabel("ID:"),1,0) - layout.addWidget(QLabel(" %s" % device_id),1,1) - - def modify_label(): - response = QInputDialog().getText(None, "Set New KeepKey Label", "New KeepKey Label: (upon submission confirm on KeepKey)") - if not response[1]: - return - new_label = str(response[0]) - self.handler.show_message("Please confirm label change on KeepKey") - status = self.get_client().apply_settings(label=new_label) - self.handler.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) - d.exec_() - - def sign_transaction(self, tx, prev_tx, xpub_path): self.prev_tx = prev_tx self.xpub_path = xpub_path @@ -350,167 +419,110 @@ class Plugin(BasePlugin): -class KeepKeyWallet(BIP32_HD_Wallet): - wallet_type = 'keepkey' - root_derivation = "m/44'/0'" - - def __init__(self, storage): - BIP32_HD_Wallet.__init__(self, storage) - self.mpk = None - self.device_checked = False - self.proper_device = False - self.force_watching_only = False - - def get_action(self): - if not self.accounts: - return 'create_accounts' - - def can_import(self): - return False - - def can_sign_xpubkey(self, x_pubkey): - xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) - return xpub in self.master_public_keys.values() - - def can_export(self): - return False - - def can_create_accounts(self): - return True - - def can_change_password(self): - return False - - def is_watching_only(self): - return self.force_watching_only - - def get_client(self): - return self.plugin.get_client() - - def address_id(self, address): - account_id, (change, address_index) = self.get_address_index(address) - 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 - - def mnemonic_to_seed(self, mnemonic, passphrase): - # keepkey uses bip39 - import pbkdf2, hashlib, hmac - PBKDF2_ROUNDS = 2048 - mnemonic = unicodedata.normalize('NFKD', ' '.join(mnemonic.split())) - passphrase = unicodedata.normalize('NFKD', passphrase) - return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) +from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton +import PyQt4.QtCore as QtCore +from electrum_gui.qt.util import * +from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow +from electrum_gui.qt.installwizard import InstallWizard +from keepkeylib.qt.pinmatrix import PinMatrixWidget - def derive_xkeys(self, root, derivation, password): - 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.plugin.get_client().expand_path(bip32_path) - node = self.plugin.get_client().get_public_node(address_n).node - xpub = "0488B21E".decode('hex') + chr(node.depth) + self.i4b(node.fingerprint) + self.i4b(node.child_num) + node.chain_code + node.public_key - return EncodeBase58Check(xpub) +class QtPlugin(Plugin): - def get_master_public_key(self): - if not self.mpk: - self.mpk = self.get_public_key("44'/0'") - return self.mpk + @hook + def load_wallet(self, wallet, window): + self.print_error("load_wallet") + self.wallet = wallet + self.wallet.plugin = self + self.keepkey_button = StatusBarButton(QIcon(":icons/keepkey.png"), _("KeepKey"), partial(self.settings_dialog, window)) + if type(window) is ElectrumWindow: + window.statusBar().addPermanentWidget(self.keepkey_button) + if self.handler is None: + self.handler = KeepKeyQtHandler(window) + try: + self.get_client().ping('t') + except BaseException as e: + QMessageBox.information(window, _('Error'), _("KeepKey device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) + self.wallet.force_watching_only = True + return + if self.wallet.addresses() and not self.wallet.check_proper_device(): + QMessageBox.information(window, _('Error'), _("This wallet does not match your KeepKey device"), _('OK')) + self.wallet.force_watching_only = True - def i4b(self, x): - return pack('>I', x) + @hook + def installwizard_load_wallet(self, wallet, window): + if type(wallet) != KeepKeyWallet: + return + self.load_wallet(wallet, window) - def add_keypairs(self, tx, keypairs, password): - #do nothing - no priv keys available - pass + @hook + def installwizard_restore(self, wizard, storage): + if storage.get('wallet_type') != 'keepkey': + return + seed = wizard.enter_seed_dialog("Enter your KeepKey seed", None, func=lambda x:True) + if not seed: + return + wallet = KeepKeyWallet(storage) + self.wallet = wallet + handler = KeepKeyQtHandler(wizard) + passphrase = handler.get_passphrase(_("Please enter your KeepKey passphrase.") + '\n' + _("Press OK if you do not use one.")) + if passphrase is None: + return + password = wizard.password_dialog() + wallet.add_seed(seed, password) + wallet.add_cosigner_seed(seed, 'x/', password, passphrase) + wallet.create_main_account(password) + # disable keepkey plugin + self.set_enabled(False) + return wallet - def decrypt_message(self, pubkey, message, password): - raise BaseException( _('Decrypt method is not implemented in KeepKey') ) - #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: - # give_error(e) - #finally: - # twd.stop() - #return str(decrypted_msg) + @hook + def receive_menu(self, menu, addrs): + if not self.wallet.is_watching_only() and self.atleast_version(1, 3) and len(addrs) == 1: + menu.addAction(_("Show on TREZOR"), lambda: self.show_address(addrs[0])) - def sign_message(self, address, message, password): - if self.has_seed(): - return BIP32_HD_Wallet.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.plugin.get_client().expand_path(address_path) - except Exception, e: - give_error(e) + def settings_dialog(self, window): try: - msg_sig = self.plugin.get_client().sign_message('Bitcoin', address_n, message) - except Exception, e: - give_error(e) - finally: - self.plugin.handler.stop() - return msg_sig.signature - - def sign_transaction(self, tx, password): - if self.has_seed(): - return BIP32_HD_Wallet.sign_transaction(self, tx, password) - if tx.is_complete(): + device_id = self.get_client().get_device_id() + except BaseException as e: + window.show_message(str(e)) return - if not self.check_proper_device(): - give_error('Wrong device or password') - # previous transactions used as inputs - prev_tx = {} - # path of the xpubs that are involved - xpub_path = {} - for txin in tx.inputs: - tx_hash = txin['prevout_hash'] + get_label = lambda: self.get_client().features.label + update_label = lambda: current_label_label.setText("Label: %s" % get_label()) + d = QDialog() + layout = QGridLayout(d) + layout.addWidget(QLabel("KeepKey Options"),0,0) + layout.addWidget(QLabel("ID:"),1,0) + layout.addWidget(QLabel(" %s" % device_id),1,1) - ptx = self.transactions.get(tx_hash) - if ptx is None: - ptx = self.network.synchronous_get(('blockchain.transaction.get', [tx_hash])) - ptx = Transaction(ptx) - prev_tx[tx_hash] = ptx + def modify_label(): + response = QInputDialog().getText(None, "Set New KeepKey Label", "New KeepKey Label: (upon submission confirm on KeepKey)") + if not response[1]: + return + new_label = str(response[0]) + self.handler.show_message("Please confirm label change on KeepKey") + status = self.get_client().apply_settings(label=new_label) + self.handler.stop() + update_label() - for x_pubkey in txin['x_pubkeys']: - account_derivation = None - if not is_extended_pubkey(x_pubkey): - continue - xpub = x_to_xpub(x_pubkey) - for k, v in self.master_public_keys.items(): - if v == xpub: - account_id = re.match("x/(\d+)'", k).group(1) - account_derivation = "44'/0'/%s'"%account_id - if account_derivation is not None: - xpub_path[xpub] = account_derivation + 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) + d.exec_() - self.plugin.sign_transaction(tx, prev_tx, xpub_path) - def check_proper_device(self): - self.get_client().ping('t') - if not self.device_checked: - address = self.addresses(False)[0] - address_id = self.address_id(address) - n = self.get_client().expand_path(address_id) - device_address = self.get_client().get_address('Bitcoin', n) - self.device_checked = True +class CmdlinePlugin(Plugin): - if device_address != address: - self.proper_device = False - else: - self.proper_device = True + @hook + def cmdline_load_wallet(self, wallet): + self.wallet = wallet + self.wallet.plugin = self + if self.handler is None: + self.handler = KeepKeyCmdLineHandler() - return self.proper_device class KeepKeyGuiMixin(object): diff --git a/plugins/labels.py b/plugins/labels.py @@ -7,15 +7,6 @@ import sys import traceback from functools import partial -try: - import PyQt4 -except Exception: - sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'") - -from PyQt4.QtGui import * -from PyQt4.QtCore import * -import PyQt4.QtCore as QtCore -import PyQt4.QtGui as QtGui import aes import base64 @@ -23,8 +14,6 @@ import electrum from electrum.plugins import BasePlugin, hook from electrum.i18n import _ -from electrum_gui.qt import HelpButton, EnterButton -from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton class Plugin(BasePlugin): @@ -32,34 +21,6 @@ class Plugin(BasePlugin): BasePlugin.__init__(self, parent, config, name) self.target_host = 'sync.bytesized-hosting.com:9090' self.wallets = {} - self.obj = QObject() - self.obj.connect(self.obj, SIGNAL('labels:pulled'), self.on_pulled) - - @hook - def on_new_window(self, window): - wallet = window.wallet - nonce = self.get_nonce(wallet) - self.print_error("wallet", wallet.basename(), "nonce is", nonce) - mpk = ''.join(sorted(wallet.get_master_public_keys().values())) - if not mpk: - return - - password = hashlib.sha1(mpk).digest().encode('hex')[:32] - iv = hashlib.sha256(password).digest()[:16] - wallet_id = hashlib.sha256(mpk).digest().encode('hex') - self.wallets[wallet] = (password, iv, wallet_id) - - # If there is an auth token we can try to actually start syncing - t = threading.Thread(target=self.pull_thread, args=(window, False)) - t.setDaemon(True) - t.start() - - @hook - def on_close_window(self, window): - self.wallets.pop(window.wallet) - - def version(self): - return "0.0.1" def encode(self, wallet, msg): password, iv, wallet_id = self.wallets[wallet] @@ -85,9 +46,6 @@ class Plugin(BasePlugin): self.print_error("set", wallet.basename(), "nonce to", nonce) wallet.storage.put("wallet_nonce", nonce, force_write) - def requires_settings(self): - return True - @hook def set_label(self, wallet, item, label): if not wallet in self.wallets: @@ -105,47 +63,6 @@ class Plugin(BasePlugin): # Caller will write the wallet self.set_nonce(wallet, nonce + 1, force_write=False) - def settings_widget(self, window): - return EnterButton(_('Settings'), - partial(self.settings_dialog, window)) - - def settings_dialog(self, window): - print "window:", window - d = QDialog(window) - vbox = QVBoxLayout(d) - layout = QGridLayout() - vbox.addLayout(layout) - - layout.addWidget(QLabel("Label sync options: "),2,0) - - self.upload = ThreadedButton("Force upload", - partial(self.push_thread, window), - self.done_processing) - layout.addWidget(self.upload, 2, 1) - - self.download = ThreadedButton("Force download", - partial(self.pull_thread, window, True), - self.done_processing) - layout.addWidget(self.download, 2, 2) - - self.accept = OkButton(d, _("Done")) - vbox.addLayout(Buttons(CancelButton(d), self.accept)) - - if d.exec_(): - return True - else: - return False - - def on_pulled(self, window, nonce): - wallet = window.wallet - wallet.storage.put('labels', wallet.labels, False) - self.set_nonce(wallet, nonce) - window.labelsChanged.emit() - - def done_processing(self): - QMessageBox.information(None, _("Labels synchronised"), - _("Your labels have been synchronised.")) - def do_request(self, method, url = "/labels", is_batch=False, data=None): url = 'https://' + self.target_host + url kwargs = {'headers': {}} @@ -162,8 +79,7 @@ class Plugin(BasePlugin): raise BaseException(response["error"]) return response - def push_thread(self, window): - wallet = window.wallet + def push_thread(self, wallet): wallet_id = self.wallets[wallet][2] bundle = {"labels": [], "walletId": wallet_id, @@ -179,14 +95,14 @@ class Plugin(BasePlugin): 'externalId': encoded_key}) self.do_request("POST", "/labels", True, bundle) - def pull_thread(self, window, force): - wallet = window.wallet + def pull_thread(self, wallet, force): wallet_id = self.wallets[wallet][2] nonce = 1 if force else self.get_nonce(wallet) - 1 self.print_error("asking for labels since nonce", nonce) try: response = self.do_request("GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id) )) if response["labels"] is None: + self.print_error('no new labels') return result = {} for label in response["labels"]: @@ -208,9 +124,86 @@ class Plugin(BasePlugin): wallet.labels[key] = value self.print_error("received %d labels" % len(response)) - self.obj.emit(SIGNAL('labels:pulled'), window, - response["nonce"] + 1) + # do not write to disk because we're in a daemon thread + wallet.storage.put('labels', wallet.labels, False) + self.set_nonce(wallet, response["nonce"] + 1, False) + self.on_pulled(wallet) except Exception as e: traceback.print_exc(file=sys.stderr) self.print_error("could not retrieve labels") + + + + + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +import PyQt4.QtCore as QtCore +import PyQt4.QtGui as QtGui +from electrum_gui.qt import HelpButton, EnterButton +from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton + +class QtPlugin(Plugin): + + def __init__(self, *args): + Plugin.__init__(self, *args) + self.obj = QObject() + + def requires_settings(self): + return True + + def settings_widget(self, window): + return EnterButton(_('Settings'), + partial(self.settings_dialog, window)) + + def settings_dialog(self, window): + d = QDialog(window) + vbox = QVBoxLayout(d) + layout = QGridLayout() + vbox.addLayout(layout) + layout.addWidget(QLabel("Label sync options: "), 2, 0) + self.upload = ThreadedButton("Force upload", + partial(self.push_thread, window.wallet), + self.done_processing) + layout.addWidget(self.upload, 2, 1) + self.download = ThreadedButton("Force download", + partial(self.pull_thread, window.wallet, True), + self.done_processing) + layout.addWidget(self.download, 2, 2) + self.accept = OkButton(d, _("Done")) + vbox.addLayout(Buttons(CancelButton(d), self.accept)) + if d.exec_(): + return True + else: + return False + + def on_pulled(self, wallet): + self.obj.emit(SIGNAL('labels_changed'), wallet) + + def done_processing(self): + QMessageBox.information(None, _("Labels synchronised"), + _("Your labels have been synchronised.")) + + @hook + def on_new_window(self, window): + window.connect(window.app, SIGNAL('labels_changed'), window.update_tabs) + wallet = window.wallet + nonce = self.get_nonce(wallet) + self.print_error("wallet", wallet.basename(), "nonce is", nonce) + mpk = ''.join(sorted(wallet.get_master_public_keys().values())) + if not mpk: + return + password = hashlib.sha1(mpk).digest().encode('hex')[:32] + iv = hashlib.sha256(password).digest()[:16] + wallet_id = hashlib.sha256(mpk).digest().encode('hex') + self.wallets[wallet] = (password, iv, wallet_id) + # If there is an auth token we can try to actually start syncing + t = threading.Thread(target=self.pull_thread, args=(wallet, False)) + t.setDaemon(True) + t.start() + + @hook + def on_close_window(self, window): + self.wallets.pop(window.wallet) + diff --git a/plugins/plot.py b/plugins/plot.py @@ -17,7 +17,7 @@ except: flag_matlib=False -class Plugin(BasePlugin): +class QtPlugin(BasePlugin): def is_available(self): if flag_matlib: diff --git a/plugins/trezor.py b/plugins/trezor.py @@ -7,8 +7,6 @@ import threading import re from functools import partial -from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton -import PyQt4.QtCore as QtCore import electrum from electrum import bitcoin @@ -22,14 +20,9 @@ from electrum.wallet import BIP32_HD_Wallet from electrum.util import print_error, print_msg from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root -from electrum_gui.qt.util import * -from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow -from electrum_gui.qt.installwizard import InstallWizard - try: from trezorlib.client import types from trezorlib.client import proto, BaseClient, ProtocolMixin - from trezorlib.qt.pinmatrix import PinMatrixWidget from trezorlib.transport import ConnectionError from trezorlib.transport_hid import HidTransport TREZOR = True @@ -47,6 +40,166 @@ def give_error(message): raise Exception(message) +class TrezorWallet(BIP32_HD_Wallet): + wallet_type = 'trezor' + root_derivation = "m/44'/0'" + + def __init__(self, storage): + BIP32_HD_Wallet.__init__(self, storage) + self.mpk = None + self.device_checked = False + self.proper_device = False + self.force_watching_only = False + + def get_action(self): + if not self.accounts: + return 'create_accounts' + + def can_import(self): + return False + + def can_sign_xpubkey(self, x_pubkey): + xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) + return xpub in self.master_public_keys.values() + + def can_export(self): + return False + + def can_create_accounts(self): + return True + + def can_change_password(self): + return False + + def is_watching_only(self): + return self.force_watching_only + + def get_client(self): + return self.plugin.get_client() + + def address_id(self, address): + account_id, (change, address_index) = self.get_address_index(address) + 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 + + def mnemonic_to_seed(self, mnemonic, passphrase): + # trezor uses bip39 + import pbkdf2, hashlib, hmac + PBKDF2_ROUNDS = 2048 + mnemonic = unicodedata.normalize('NFKD', ' '.join(mnemonic.split())) + passphrase = unicodedata.normalize('NFKD', passphrase) + return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) + + def derive_xkeys(self, root, derivation, password): + 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.plugin.get_client().expand_path(bip32_path) + node = self.plugin.get_client().get_public_node(address_n).node + xpub = "0488B21E".decode('hex') + chr(node.depth) + self.i4b(node.fingerprint) + self.i4b(node.child_num) + node.chain_code + node.public_key + return EncodeBase58Check(xpub) + + def get_master_public_key(self): + if not self.mpk: + self.mpk = self.get_public_key("44'/0'") + return self.mpk + + def i4b(self, x): + return pack('>I', x) + + def add_keypairs(self, tx, keypairs, password): + #do nothing - no priv keys available + pass + + def decrypt_message(self, pubkey, message, password): + raise BaseException( _('Decrypt method is not implemented in Trezor') ) + #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: + # give_error(e) + #finally: + # twd.stop() + #return str(decrypted_msg) + + def sign_message(self, address, message, password): + if self.has_seed(): + return BIP32_HD_Wallet.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.plugin.get_client().expand_path(address_path) + except Exception, e: + give_error(e) + try: + msg_sig = self.plugin.get_client().sign_message('Bitcoin', address_n, message) + except Exception, e: + give_error(e) + finally: + self.plugin.handler.stop() + return msg_sig.signature + + def sign_transaction(self, tx, password): + if self.has_seed(): + return BIP32_HD_Wallet.sign_transaction(self, tx, password) + if tx.is_complete(): + return + if not self.check_proper_device(): + give_error('Wrong device or password') + # previous transactions used as inputs + prev_tx = {} + # path of the xpubs that are involved + xpub_path = {} + for txin in tx.inputs: + tx_hash = txin['prevout_hash'] + + ptx = self.transactions.get(tx_hash) + if ptx is None: + ptx = self.network.synchronous_get(('blockchain.transaction.get', [tx_hash])) + ptx = Transaction(ptx) + prev_tx[tx_hash] = ptx + + for x_pubkey in txin['x_pubkeys']: + account_derivation = None + if not is_extended_pubkey(x_pubkey): + continue + xpub = x_to_xpub(x_pubkey) + for k, v in self.master_public_keys.items(): + if v == xpub: + account_id = re.match("x/(\d+)'", k).group(1) + account_derivation = "44'/0'/%s'"%account_id + if account_derivation is not None: + xpub_path[xpub] = account_derivation + + self.plugin.sign_transaction(tx, prev_tx, xpub_path) + + def check_proper_device(self): + self.get_client().ping('t') + if not self.device_checked: + address = self.addresses(False)[0] + address_id = self.address_id(address) + n = self.get_client().expand_path(address_id) + device_address = self.get_client().get_address('Bitcoin', n) + self.device_checked = True + self.proper_device = (device_address == address) + + return self.proper_device + + + class Plugin(BasePlugin): def __init__(self, parent, config, name): @@ -125,107 +278,6 @@ class Plugin(BasePlugin): if self.handler is None: self.handler = TrezorCmdLineHandler() - @hook - def load_wallet(self, wallet, window): - self.print_error("load_wallet") - self.wallet = wallet - self.wallet.plugin = self - self.trezor_button = StatusBarButton(QIcon(":icons/trezor.png"), _("Trezor"), partial(self.settings_dialog, window)) - if type(window) is ElectrumWindow: - window.statusBar().addPermanentWidget(self.trezor_button) - if self.handler is None: - self.handler = TrezorQtHandler(window) - try: - self.get_client().ping('t') - except BaseException as e: - QMessageBox.information(window, _('Error'), _("Trezor device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) - self.wallet.force_watching_only = True - return - if self.wallet.addresses() and not self.wallet.check_proper_device(): - QMessageBox.information(window, _('Error'), _("This wallet does not match your Trezor device"), _('OK')) - self.wallet.force_watching_only = True - - @hook - def installwizard_load_wallet(self, wallet, window): - if type(wallet) != TrezorWallet: - return - self.load_wallet(wallet, window) - - @hook - def installwizard_restore(self, wizard, storage): - if storage.get('wallet_type') != 'trezor': - return - seed = wizard.enter_seed_dialog("Enter your Trezor seed", None, func=lambda x:True) - if not seed: - return - wallet = TrezorWallet(storage) - self.wallet = wallet - handler = TrezorQtHandler(wizard) - passphrase = handler.get_passphrase(_("Please enter your Trezor passphrase.") + '\n' + _("Press OK if you do not use one.")) - if passphrase is None: - return - password = wizard.password_dialog() - wallet.add_seed(seed, password) - wallet.add_cosigner_seed(seed, 'x/', password, passphrase) - wallet.create_main_account(password) - # disable trezor plugin - self.set_enabled(False) - return wallet - - @hook - def receive_menu(self, menu, addrs): - if not self.wallet.is_watching_only() and self.atleast_version(1, 3) and len(addrs) == 1: - menu.addAction(_("Show on TREZOR"), lambda: self.show_address(addrs[0])) - - def show_address(self, address): - if not self.wallet.check_proper_device(): - give_error('Wrong device or password') - try: - address_path = self.wallet.address_id(address) - address_n = self.get_client().expand_path(address_path) - except Exception, e: - give_error(e) - try: - self.get_client().get_address('Bitcoin', address_n, True) - except Exception, e: - give_error(e) - finally: - self.handler.stop() - - - def settings_dialog(self, window): - try: - device_id = self.get_client().get_device_id() - except BaseException as e: - window.show_message(str(e)) - return - get_label = lambda: self.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" % 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]) - self.handler.show_message("Please confirm label change on Trezor") - status = self.get_client().apply_settings(label=new_label) - self.handler.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) - d.exec_() - - def sign_transaction(self, tx, prev_tx, xpub_path): self.prev_tx = prev_tx self.xpub_path = xpub_path @@ -347,169 +399,119 @@ class Plugin(BasePlugin): return self.electrum_tx_to_txtype(tx) +from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton +import PyQt4.QtCore as QtCore +from electrum_gui.qt.util import * +from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow +from electrum_gui.qt.installwizard import InstallWizard +from trezorlib.qt.pinmatrix import PinMatrixWidget +class QtPlugin(Plugin): -class TrezorWallet(BIP32_HD_Wallet): - wallet_type = 'trezor' - root_derivation = "m/44'/0'" - - def __init__(self, storage): - BIP32_HD_Wallet.__init__(self, storage) - self.mpk = None - self.device_checked = False - self.proper_device = False - self.force_watching_only = False - - def get_action(self): - if not self.accounts: - return 'create_accounts' - - def can_import(self): - return False - - def can_sign_xpubkey(self, x_pubkey): - xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) - return xpub in self.master_public_keys.values() - - def can_export(self): - return False - - def can_create_accounts(self): - return True - - def can_change_password(self): - return False - - def is_watching_only(self): - return self.force_watching_only - - def get_client(self): - return self.plugin.get_client() - - def address_id(self, address): - account_id, (change, address_index) = self.get_address_index(address) - 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 - - def mnemonic_to_seed(self, mnemonic, passphrase): - # trezor uses bip39 - import pbkdf2, hashlib, hmac - PBKDF2_ROUNDS = 2048 - mnemonic = unicodedata.normalize('NFKD', ' '.join(mnemonic.split())) - passphrase = unicodedata.normalize('NFKD', passphrase) - return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) - - def derive_xkeys(self, root, derivation, password): - 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.plugin.get_client().expand_path(bip32_path) - node = self.plugin.get_client().get_public_node(address_n).node - xpub = "0488B21E".decode('hex') + chr(node.depth) + self.i4b(node.fingerprint) + self.i4b(node.child_num) + node.chain_code + node.public_key - return EncodeBase58Check(xpub) - - def get_master_public_key(self): - if not self.mpk: - self.mpk = self.get_public_key("44'/0'") - return self.mpk + @hook + def load_wallet(self, wallet, window): + self.print_error("load_wallet") + self.wallet = wallet + self.wallet.plugin = self + self.trezor_button = StatusBarButton(QIcon(":icons/trezor.png"), _("Trezor"), partial(self.settings_dialog, window)) + if type(window) is ElectrumWindow: + window.statusBar().addPermanentWidget(self.trezor_button) + if self.handler is None: + self.handler = TrezorQtHandler(window) + try: + self.get_client().ping('t') + except BaseException as e: + QMessageBox.information(window, _('Error'), _("Trezor device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) + self.wallet.force_watching_only = True + return + if self.wallet.addresses() and not self.wallet.check_proper_device(): + QMessageBox.information(window, _('Error'), _("This wallet does not match your Trezor device"), _('OK')) + self.wallet.force_watching_only = True - def i4b(self, x): - return pack('>I', x) + @hook + def installwizard_load_wallet(self, wallet, window): + if type(wallet) != TrezorWallet: + return + self.load_wallet(wallet, window) - def add_keypairs(self, tx, keypairs, password): - #do nothing - no priv keys available - pass + @hook + def installwizard_restore(self, wizard, storage): + if storage.get('wallet_type') != 'trezor': + return + seed = wizard.enter_seed_dialog("Enter your Trezor seed", None, func=lambda x:True) + if not seed: + return + wallet = TrezorWallet(storage) + self.wallet = wallet + handler = TrezorQtHandler(wizard) + passphrase = handler.get_passphrase(_("Please enter your Trezor passphrase.") + '\n' + _("Press OK if you do not use one.")) + if passphrase is None: + return + password = wizard.password_dialog() + wallet.add_seed(seed, password) + wallet.add_cosigner_seed(seed, 'x/', password, passphrase) + wallet.create_main_account(password) + # disable trezor plugin + self.set_enabled(False) + return wallet - def decrypt_message(self, pubkey, message, password): - raise BaseException( _('Decrypt method is not implemented in Trezor') ) - #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: - # give_error(e) - #finally: - # twd.stop() - #return str(decrypted_msg) + @hook + def receive_menu(self, menu, addrs): + if not self.wallet.is_watching_only() and self.atleast_version(1, 3) and len(addrs) == 1: + menu.addAction(_("Show on TREZOR"), lambda: self.show_address(addrs[0])) - def sign_message(self, address, message, password): - if self.has_seed(): - return BIP32_HD_Wallet.sign_message(self, address, message, password) - if not self.check_proper_device(): + def show_address(self, address): + if not self.wallet.check_proper_device(): give_error('Wrong device or password') try: - address_path = self.address_id(address) - address_n = self.plugin.get_client().expand_path(address_path) + address_path = self.wallet.address_id(address) + address_n = self.get_client().expand_path(address_path) except Exception, e: give_error(e) try: - msg_sig = self.plugin.get_client().sign_message('Bitcoin', address_n, message) + self.get_client().get_address('Bitcoin', address_n, True) except Exception, e: give_error(e) finally: - self.plugin.handler.stop() - return msg_sig.signature + self.handler.stop() - def sign_transaction(self, tx, password): - if self.has_seed(): - return BIP32_HD_Wallet.sign_transaction(self, tx, password) - if tx.is_complete(): + + def settings_dialog(self, window): + try: + device_id = self.get_client().get_device_id() + except BaseException as e: + window.show_message(str(e)) return - if not self.check_proper_device(): - give_error('Wrong device or password') - # previous transactions used as inputs - prev_tx = {} - # path of the xpubs that are involved - xpub_path = {} - for txin in tx.inputs: - tx_hash = txin['prevout_hash'] + get_label = lambda: self.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" % device_id),1,1) - ptx = self.transactions.get(tx_hash) - if ptx is None: - ptx = self.network.synchronous_get(('blockchain.transaction.get', [tx_hash])) - ptx = Transaction(ptx) - prev_tx[tx_hash] = ptx + 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]) + self.handler.show_message("Please confirm label change on Trezor") + status = self.get_client().apply_settings(label=new_label) + self.handler.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) + d.exec_() - for x_pubkey in txin['x_pubkeys']: - account_derivation = None - if not is_extended_pubkey(x_pubkey): - continue - xpub = x_to_xpub(x_pubkey) - for k, v in self.master_public_keys.items(): - if v == xpub: - account_id = re.match("x/(\d+)'", k).group(1) - account_derivation = "44'/0'/%s'"%account_id - if account_derivation is not None: - xpub_path[xpub] = account_derivation - self.plugin.sign_transaction(tx, prev_tx, xpub_path) - def check_proper_device(self): - self.get_client().ping('t') - if not self.device_checked: - address = self.addresses(False)[0] - address_id = self.address_id(address) - n = self.get_client().expand_path(address_id) - device_address = self.get_client().get_address('Bitcoin', n) - self.device_checked = True - if device_address != address: - self.proper_device = False - else: - self.proper_device = True - return self.proper_device class TrezorGuiMixin(object): diff --git a/plugins/trustedcoin.py b/plugins/trustedcoin.py @@ -27,9 +27,6 @@ from urlparse import urljoin from urllib import quote from functools import partial -from PyQt4.QtGui import * -from PyQt4.QtCore import * - import electrum from electrum import bitcoin from electrum.bitcoin import * @@ -39,10 +36,6 @@ from electrum.wallet import Multisig_Wallet, BIP32_Wallet from electrum.i18n import _ from electrum.plugins import BasePlugin, run_hook, hook -from electrum_gui.qt.util import * -from electrum_gui.qt.qrcodewidget import QRCodeWidget -from electrum_gui.qt.amountedit import AmountEdit -from electrum_gui.qt.main_window import StatusBarButton from decimal import Decimal @@ -460,6 +453,16 @@ class Plugin(BasePlugin): wallet.add_master_public_key('x3/', xpub3) return True + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from electrum_gui.qt.util import * +from electrum_gui.qt.qrcodewidget import QRCodeWidget +from electrum_gui.qt.amountedit import AmountEdit +from electrum_gui.qt.main_window import StatusBarButton + +class QtPlugin(Plugin): + def auth_dialog(self, window): d = QDialog(window) d.setModal(1) @@ -674,3 +677,5 @@ class Plugin(BasePlugin): except: QMessageBox.information(window, _('Message'), _('Incorrect password'), _('OK')) pw.setText('') + + diff --git a/plugins/virtualkeyboard.py b/plugins/virtualkeyboard.py @@ -3,7 +3,7 @@ from electrum.plugins import BasePlugin, hook from electrum.i18n import _ import random -class Plugin(BasePlugin): +class QtPlugin(BasePlugin): vkb = None vkb_index = 0