From 2c0489c8094b2ba9409ae0e36bfe674ce327d883 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 23 Nov 2015 14:15:25 +0100 Subject: [PATCH] plugins: separate GUIs using child classes --- gui/qt/main_window.py | 2 - lib/plugins.py | 11 +- plugins/audio_modem.py | 2 +- plugins/btchipwallet.py | 194 +++++----- plugins/cosigner_pool.py | 2 +- plugins/email_requests.py | 2 +- plugins/exchange_rate.py | 175 ++++----- plugins/greenaddress_instant.py | 2 +- plugins/keepkey.py | 626 +++++++++++++++---------------- plugins/labels.py | 171 +++++---- plugins/plot.py | 2 +- plugins/trezor.py | 630 ++++++++++++++++---------------- plugins/trustedcoin.py | 19 +- plugins/virtualkeyboard.py | 2 +- 14 files changed, 937 insertions(+), 903 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 54de605b..e5b1f721 100644 --- 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 index fee56404..a8077e56 100644 --- 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 index e797f12c..27904705 100644 --- 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 index 430cf3f5..d7c77f5b 100644 --- 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 index 5f00098b..b2e9a651 100644 --- 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 index f76099e4..d9740937 100644 --- 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 index 83408831..559044b8 100644 --- 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 index 0dddf23e..f6874884 100644 --- 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 index 0b14bfab..6376c162 100644 --- 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,307 +41,6 @@ def give_error(message): raise Exception(message) -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 - self.client = None - self.transport = None - - def constructor(self, s): - return KeepKeyWallet(s) - - def _init(self): - return KEEPKEY - - def is_available(self): - if not self._is_available: - return False - if not self.wallet: - return False - if self.wallet.storage.get('wallet_type') != 'keepkey': - 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 compare_version(self, major, minor=0, patch=0): - features = self.get_client().features - v = [features.major_version, features.minor_version, features.patch_version] - self.print_error('firmware version', v) - return cmp(v, [major, minor, patch]) - - def atleast_version(self, major, minor=0, patch=0): - return self.compare_version(major, minor, patch) >= 0 - - def get_client(self): - if not KEEPKEY: - give_error('please install github.com/keepkey/python-keepkey') - - if not self.client or self.client.bad: - d = HidTransport.enumerate() - if not d: - give_error('Could not connect to your KeepKey. Please verify the cable is connected and that no other app is using it.') - self.transport = HidTransport(d[0]) - self.client = QtGuiKeepKeyClient(self.transport) - self.client.handler = self.handler - self.client.set_tx_api(self) - self.client.bad = False - if not self.atleast_version(1, 0, 0): - self.client = None - give_error('Outdated KeepKey firmware. Please update the firmware from https://www.keepkey.com') - return self.client - - @hook - def close_wallet(self): - print_error("keepkey: clear session") - if self.client: - self.client.clear_session() - self.client.transport.close() - 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(): - 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("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 - client = self.get_client() - inputs = self.tx_inputs(tx, True) - outputs = self.tx_outputs(tx) - try: - signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] - except Exception, e: - self.handler.stop() - give_error(e) - - self.handler.stop() - - raw = signed_tx.encode('hex') - tx.update_signatures(raw) - - - def tx_inputs(self, tx, for_sig=False): - inputs = [] - for txin in tx.inputs: - txinputtype = types.TxInputType() - if txin.get('is_coinbase'): - prev_hash = "\0"*32 - prev_index = 0xffffffff # signed int -1 - else: - if for_sig: - x_pubkeys = txin['x_pubkeys'] - if len(x_pubkeys) == 1: - x_pubkey = x_pubkeys[0] - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - else: - def f(x_pubkey): - if is_extended_pubkey(x_pubkey): - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - else: - xpub = xpub_from_pubkey(x_pubkey.decode('hex')) - s = [] - node = ckd_public.deserialize(xpub) - return types.HDNodePathType(node=node, address_n=s) - pubkeys = map(f, x_pubkeys) - multisig = types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), - m=txin.get('num_sig'), - ) - txinputtype = types.TxInputType( - script_type=types.SPENDMULTISIG, - multisig= multisig - ) - # find which key is mine - for x_pubkey in x_pubkeys: - if is_extended_pubkey(x_pubkey): - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - if xpub in self.xpub_path: - xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - break - - prev_hash = unhexlify(txin['prevout_hash']) - prev_index = txin['prevout_n'] - - txinputtype.prev_hash = prev_hash - txinputtype.prev_index = prev_index - - if 'scriptSig' in txin: - script_sig = txin['scriptSig'].decode('hex') - txinputtype.script_sig = script_sig - - if 'sequence' in txin: - sequence = txin['sequence'] - txinputtype.sequence = sequence - - inputs.append(txinputtype) - - return inputs - - def tx_outputs(self, tx): - outputs = [] - - for type, address, amount in tx.outputs: - assert type == 'address' - txoutputtype = types.TxOutputType() - if self.wallet.is_change(address): - address_path = self.wallet.address_id(address) - address_n = self.get_client().expand_path(address_path) - txoutputtype.address_n.extend(address_n) - else: - txoutputtype.address = address - txoutputtype.amount = amount - addrtype, hash_160 = bc_address_to_hash_160(address) - if addrtype == 0: - txoutputtype.script_type = types.PAYTOADDRESS - elif addrtype == 5: - txoutputtype.script_type = types.PAYTOSCRIPTHASH - else: - raise BaseException('addrtype') - outputs.append(txoutputtype) - - return outputs - - def electrum_tx_to_txtype(self, tx): - t = types.TransactionType() - d = deserialize(tx.raw) - t.version = d['version'] - t.lock_time = d['lockTime'] - inputs = self.tx_inputs(tx) - t.inputs.extend(inputs) - for vout in d['outputs']: - o = t.bin_outputs.add() - o.amount = vout['value'] - o.script_pubkey = vout['scriptPubKey'].decode('hex') - return t - - def get_tx(self, tx_hash): - tx = self.prev_tx[tx_hash] - tx.deserialize() - return self.electrum_tx_to_txtype(tx) - - class KeepKeyWallet(BIP32_HD_Wallet): @@ -513,6 +206,325 @@ class KeepKeyWallet(BIP32_HD_Wallet): return self.proper_device + +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 + self.client = None + self.transport = None + + def constructor(self, s): + return KeepKeyWallet(s) + + def _init(self): + return KEEPKEY + + def is_available(self): + if not self._is_available: + return False + if not self.wallet: + return False + if self.wallet.storage.get('wallet_type') != 'keepkey': + 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 compare_version(self, major, minor=0, patch=0): + features = self.get_client().features + v = [features.major_version, features.minor_version, features.patch_version] + self.print_error('firmware version', v) + return cmp(v, [major, minor, patch]) + + def atleast_version(self, major, minor=0, patch=0): + return self.compare_version(major, minor, patch) >= 0 + + def get_client(self): + if not KEEPKEY: + give_error('please install github.com/keepkey/python-keepkey') + + if not self.client or self.client.bad: + d = HidTransport.enumerate() + if not d: + give_error('Could not connect to your KeepKey. Please verify the cable is connected and that no other app is using it.') + self.transport = HidTransport(d[0]) + self.client = QtGuiKeepKeyClient(self.transport) + self.client.handler = self.handler + self.client.set_tx_api(self) + self.client.bad = False + if not self.atleast_version(1, 0, 0): + self.client = None + give_error('Outdated KeepKey firmware. Please update the firmware from https://www.keepkey.com') + return self.client + + @hook + def close_wallet(self): + print_error("keepkey: clear session") + if self.client: + self.client.clear_session() + self.client.transport.close() + self.client = None + self.wallet = None + + + 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 sign_transaction(self, tx, prev_tx, xpub_path): + self.prev_tx = prev_tx + self.xpub_path = xpub_path + client = self.get_client() + inputs = self.tx_inputs(tx, True) + outputs = self.tx_outputs(tx) + try: + signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] + except Exception, e: + self.handler.stop() + give_error(e) + + self.handler.stop() + + raw = signed_tx.encode('hex') + tx.update_signatures(raw) + + + def tx_inputs(self, tx, for_sig=False): + inputs = [] + for txin in tx.inputs: + txinputtype = types.TxInputType() + if txin.get('is_coinbase'): + prev_hash = "\0"*32 + prev_index = 0xffffffff # signed int -1 + else: + if for_sig: + x_pubkeys = txin['x_pubkeys'] + if len(x_pubkeys) == 1: + x_pubkey = x_pubkeys[0] + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + else: + def f(x_pubkey): + if is_extended_pubkey(x_pubkey): + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + else: + xpub = xpub_from_pubkey(x_pubkey.decode('hex')) + s = [] + node = ckd_public.deserialize(xpub) + return types.HDNodePathType(node=node, address_n=s) + pubkeys = map(f, x_pubkeys) + multisig = types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), + m=txin.get('num_sig'), + ) + txinputtype = types.TxInputType( + script_type=types.SPENDMULTISIG, + multisig= multisig + ) + # find which key is mine + for x_pubkey in x_pubkeys: + if is_extended_pubkey(x_pubkey): + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + if xpub in self.xpub_path: + xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + break + + prev_hash = unhexlify(txin['prevout_hash']) + prev_index = txin['prevout_n'] + + txinputtype.prev_hash = prev_hash + txinputtype.prev_index = prev_index + + if 'scriptSig' in txin: + script_sig = txin['scriptSig'].decode('hex') + txinputtype.script_sig = script_sig + + if 'sequence' in txin: + sequence = txin['sequence'] + txinputtype.sequence = sequence + + inputs.append(txinputtype) + + return inputs + + def tx_outputs(self, tx): + outputs = [] + + for type, address, amount in tx.outputs: + assert type == 'address' + txoutputtype = types.TxOutputType() + if self.wallet.is_change(address): + address_path = self.wallet.address_id(address) + address_n = self.get_client().expand_path(address_path) + txoutputtype.address_n.extend(address_n) + else: + txoutputtype.address = address + txoutputtype.amount = amount + addrtype, hash_160 = bc_address_to_hash_160(address) + if addrtype == 0: + txoutputtype.script_type = types.PAYTOADDRESS + elif addrtype == 5: + txoutputtype.script_type = types.PAYTOSCRIPTHASH + else: + raise BaseException('addrtype') + outputs.append(txoutputtype) + + return outputs + + def electrum_tx_to_txtype(self, tx): + t = types.TransactionType() + d = deserialize(tx.raw) + t.version = d['version'] + t.lock_time = d['lockTime'] + inputs = self.tx_inputs(tx) + t.inputs.extend(inputs) + for vout in d['outputs']: + o = t.bin_outputs.add() + o.amount = vout['value'] + o.script_pubkey = vout['scriptPubKey'].decode('hex') + return t + + def get_tx(self, tx_hash): + tx = self.prev_tx[tx_hash] + tx.deserialize() + 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 keepkeylib.qt.pinmatrix import PinMatrixWidget + + +class QtPlugin(Plugin): + + @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 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_() + + +class CmdlinePlugin(Plugin): + + @hook + def cmdline_load_wallet(self, wallet): + self.wallet = wallet + self.wallet.plugin = self + if self.handler is None: + self.handler = KeepKeyCmdLineHandler() + + + class KeepKeyGuiMixin(object): def __init__(self, *args, **kwargs): diff --git a/plugins/labels.py b/plugins/labels.py index e0074b36..d691592b 100644 --- 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 index 6aae31f0..0ff8d98a 100644 --- 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 index 3f94b1e8..3f75ccb7 100644 --- 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,308 +40,6 @@ def give_error(message): raise Exception(message) -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 - self.client = None - self.transport = None - - def constructor(self, s): - return TrezorWallet(s) - - def _init(self): - return TREZOR - - def is_available(self): - 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 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 compare_version(self, major, minor=0, patch=0): - features = self.get_client().features - v = [features.major_version, features.minor_version, features.patch_version] - self.print_error('firmware version', v) - return cmp(v, [major, minor, patch]) - - def atleast_version(self, major, minor=0, patch=0): - return self.compare_version(major, minor, patch) >= 0 - - def get_client(self): - if not TREZOR: - give_error('please install github.com/trezor/python-trezor') - - if not self.client or self.client.bad: - d = HidTransport.enumerate() - if not d: - give_error('Could not connect to your Trezor. Please verify the cable is connected and that no other app is using it.') - self.transport = HidTransport(d[0]) - self.client = QtGuiTrezorClient(self.transport) - self.client.handler = self.handler - self.client.set_tx_api(self) - self.client.bad = False - if not self.atleast_version(1, 2, 1): - self.client = None - give_error('Outdated Trezor firmware. Please update the firmware from https://www.mytrezor.com') - return self.client - - @hook - def close_wallet(self): - print_error("trezor: clear session") - if self.client: - self.client.clear_session() - self.client.transport.close() - 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 = 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 - client = self.get_client() - inputs = self.tx_inputs(tx, True) - outputs = self.tx_outputs(tx) - #try: - signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] - #except Exception, e: - # give_error(e) - #finally: - self.handler.stop() - - raw = signed_tx.encode('hex') - tx.update_signatures(raw) - - - def tx_inputs(self, tx, for_sig=False): - inputs = [] - for txin in tx.inputs: - txinputtype = types.TxInputType() - if txin.get('is_coinbase'): - prev_hash = "\0"*32 - prev_index = 0xffffffff # signed int -1 - else: - if for_sig: - x_pubkeys = txin['x_pubkeys'] - if len(x_pubkeys) == 1: - x_pubkey = x_pubkeys[0] - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - else: - def f(x_pubkey): - if is_extended_pubkey(x_pubkey): - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - else: - xpub = xpub_from_pubkey(x_pubkey.decode('hex')) - s = [] - node = ckd_public.deserialize(xpub) - return types.HDNodePathType(node=node, address_n=s) - pubkeys = map(f, x_pubkeys) - multisig = types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), - m=txin.get('num_sig'), - ) - txinputtype = types.TxInputType( - script_type=types.SPENDMULTISIG, - multisig= multisig - ) - # find which key is mine - for x_pubkey in x_pubkeys: - if is_extended_pubkey(x_pubkey): - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - if xpub in self.xpub_path: - xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - break - - prev_hash = unhexlify(txin['prevout_hash']) - prev_index = txin['prevout_n'] - - txinputtype.prev_hash = prev_hash - txinputtype.prev_index = prev_index - - if 'scriptSig' in txin: - script_sig = txin['scriptSig'].decode('hex') - txinputtype.script_sig = script_sig - - if 'sequence' in txin: - sequence = txin['sequence'] - txinputtype.sequence = sequence - - inputs.append(txinputtype) - - return inputs - - def tx_outputs(self, tx): - outputs = [] - - for type, address, amount in tx.outputs: - assert type == 'address' - txoutputtype = types.TxOutputType() - if self.wallet.is_change(address): - address_path = self.wallet.address_id(address) - address_n = self.get_client().expand_path(address_path) - txoutputtype.address_n.extend(address_n) - else: - txoutputtype.address = address - txoutputtype.amount = amount - addrtype, hash_160 = bc_address_to_hash_160(address) - if addrtype == 0: - txoutputtype.script_type = types.PAYTOADDRESS - elif addrtype == 5: - txoutputtype.script_type = types.PAYTOSCRIPTHASH - else: - raise BaseException('addrtype') - outputs.append(txoutputtype) - - return outputs - - def electrum_tx_to_txtype(self, tx): - t = types.TransactionType() - d = deserialize(tx.raw) - t.version = d['version'] - t.lock_time = d['lockTime'] - inputs = self.tx_inputs(tx) - t.inputs.extend(inputs) - for vout in d['outputs']: - o = t.bin_outputs.add() - o.amount = vout['value'] - o.script_pubkey = vout['scriptPubKey'].decode('hex') - return t - - def get_tx(self, tx_hash): - tx = self.prev_tx[tx_hash] - tx.deserialize() - return self.electrum_tx_to_txtype(tx) - - - - class TrezorWallet(BIP32_HD_Wallet): wallet_type = 'trezor' root_derivation = "m/44'/0'" @@ -503,15 +194,326 @@ class TrezorWallet(BIP32_HD_Wallet): 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 + self.proper_device = (device_address == address) return self.proper_device + +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 + self.client = None + self.transport = None + + def constructor(self, s): + return TrezorWallet(s) + + def _init(self): + return TREZOR + + def is_available(self): + 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 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 compare_version(self, major, minor=0, patch=0): + features = self.get_client().features + v = [features.major_version, features.minor_version, features.patch_version] + self.print_error('firmware version', v) + return cmp(v, [major, minor, patch]) + + def atleast_version(self, major, minor=0, patch=0): + return self.compare_version(major, minor, patch) >= 0 + + def get_client(self): + if not TREZOR: + give_error('please install github.com/trezor/python-trezor') + + if not self.client or self.client.bad: + d = HidTransport.enumerate() + if not d: + give_error('Could not connect to your Trezor. Please verify the cable is connected and that no other app is using it.') + self.transport = HidTransport(d[0]) + self.client = QtGuiTrezorClient(self.transport) + self.client.handler = self.handler + self.client.set_tx_api(self) + self.client.bad = False + if not self.atleast_version(1, 2, 1): + self.client = None + give_error('Outdated Trezor firmware. Please update the firmware from https://www.mytrezor.com') + return self.client + + @hook + def close_wallet(self): + print_error("trezor: clear session") + if self.client: + self.client.clear_session() + self.client.transport.close() + 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 = TrezorCmdLineHandler() + + def sign_transaction(self, tx, prev_tx, xpub_path): + self.prev_tx = prev_tx + self.xpub_path = xpub_path + client = self.get_client() + inputs = self.tx_inputs(tx, True) + outputs = self.tx_outputs(tx) + #try: + signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] + #except Exception, e: + # give_error(e) + #finally: + self.handler.stop() + + raw = signed_tx.encode('hex') + tx.update_signatures(raw) + + + def tx_inputs(self, tx, for_sig=False): + inputs = [] + for txin in tx.inputs: + txinputtype = types.TxInputType() + if txin.get('is_coinbase'): + prev_hash = "\0"*32 + prev_index = 0xffffffff # signed int -1 + else: + if for_sig: + x_pubkeys = txin['x_pubkeys'] + if len(x_pubkeys) == 1: + x_pubkey = x_pubkeys[0] + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + else: + def f(x_pubkey): + if is_extended_pubkey(x_pubkey): + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + else: + xpub = xpub_from_pubkey(x_pubkey.decode('hex')) + s = [] + node = ckd_public.deserialize(xpub) + return types.HDNodePathType(node=node, address_n=s) + pubkeys = map(f, x_pubkeys) + multisig = types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), + m=txin.get('num_sig'), + ) + txinputtype = types.TxInputType( + script_type=types.SPENDMULTISIG, + multisig= multisig + ) + # find which key is mine + for x_pubkey in x_pubkeys: + if is_extended_pubkey(x_pubkey): + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + if xpub in self.xpub_path: + xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + break + + prev_hash = unhexlify(txin['prevout_hash']) + prev_index = txin['prevout_n'] + + txinputtype.prev_hash = prev_hash + txinputtype.prev_index = prev_index + + if 'scriptSig' in txin: + script_sig = txin['scriptSig'].decode('hex') + txinputtype.script_sig = script_sig + + if 'sequence' in txin: + sequence = txin['sequence'] + txinputtype.sequence = sequence + + inputs.append(txinputtype) + + return inputs + + def tx_outputs(self, tx): + outputs = [] + + for type, address, amount in tx.outputs: + assert type == 'address' + txoutputtype = types.TxOutputType() + if self.wallet.is_change(address): + address_path = self.wallet.address_id(address) + address_n = self.get_client().expand_path(address_path) + txoutputtype.address_n.extend(address_n) + else: + txoutputtype.address = address + txoutputtype.amount = amount + addrtype, hash_160 = bc_address_to_hash_160(address) + if addrtype == 0: + txoutputtype.script_type = types.PAYTOADDRESS + elif addrtype == 5: + txoutputtype.script_type = types.PAYTOSCRIPTHASH + else: + raise BaseException('addrtype') + outputs.append(txoutputtype) + + return outputs + + def electrum_tx_to_txtype(self, tx): + t = types.TransactionType() + d = deserialize(tx.raw) + t.version = d['version'] + t.lock_time = d['lockTime'] + inputs = self.tx_inputs(tx) + t.inputs.extend(inputs) + for vout in d['outputs']: + o = t.bin_outputs.add() + o.amount = vout['value'] + o.script_pubkey = vout['scriptPubKey'].decode('hex') + return t + + def get_tx(self, tx_hash): + tx = self.prev_tx[tx_hash] + tx.deserialize() + 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): + + @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_() + + + + + + + class TrezorGuiMixin(object): def __init__(self, *args, **kwargs): diff --git a/plugins/trustedcoin.py b/plugins/trustedcoin.py index 3ae2959d..3e33f105 100644 --- 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 index 4ad2cc32..cd75faf0 100644 --- 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