plugins: separate GUIs using child classes

This commit is contained in:
ThomasV 2015-11-23 14:15:25 +01:00
parent 175fdbcac6
commit 2c0489c809
14 changed files with 937 additions and 903 deletions

View file

@ -107,7 +107,6 @@ expiration_values = [
class ElectrumWindow(QMainWindow, PrintError): class ElectrumWindow(QMainWindow, PrintError):
labelsChanged = pyqtSignal()
def __init__(self, config, network, gui_object): def __init__(self, config, network, gui_object):
QMainWindow.__init__(self) 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_ok'), self.payment_request_ok)
self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error) self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
self.labelsChanged.connect(self.update_tabs)
self.history_list.setFocus(True) self.history_list.setFocus(True)
# network callbacks # network callbacks

View file

@ -40,6 +40,7 @@ class Plugins(PrintError):
self.plugins = {} self.plugins = {}
self.network = None self.network = None
self.gui_name = gui_name
self.descriptions = plugins.descriptions self.descriptions = plugins.descriptions
for item in self.descriptions: for item in self.descriptions:
name = item['name'] name = item['name']
@ -66,7 +67,15 @@ class Plugins(PrintError):
p = imp.load_source(full_name, path) p = imp.load_source(full_name, path)
else: else:
p = __import__(full_name, fromlist=['electrum_plugins']) 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: if self.network:
self.network.add_jobs(plugin.thread_jobs()) self.network.add_jobs(plugin.thread_jobs())
self.plugins[name] = plugin self.plugins[name] = plugin

View file

@ -25,7 +25,7 @@ except ImportError:
print_error('Audio MODEM is not found.') print_error('Audio MODEM is not found.')
class Plugin(BasePlugin): class QtPlugin(BasePlugin):
def __init__(self, parent, config, name): def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name) BasePlugin.__init__(self, parent, config, name)

View file

@ -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 unhexlify
from binascii import hexlify from binascii import hexlify
from struct import pack,unpack from struct import pack,unpack
@ -7,7 +5,6 @@ from sys import stderr
from time import sleep from time import sleep
import electrum import electrum
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog
from electrum.account import BIP32_Account from electrum.account import BIP32_Account
from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160 from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160
from electrum.i18n import _ from electrum.i18n import _
@ -32,96 +29,6 @@ try:
except ImportError: except ImportError:
BTCHIP = False 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): class BTChipWallet(BIP32_HD_Wallet):
wallet_type = 'btchip' wallet_type = 'btchip'
@ -517,6 +424,98 @@ class BTChipWallet(BIP32_HD_Wallet):
return False, None, None return False, None, None
return True, response, response 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: class BTChipQTHandler:
def __init__(self, win): def __init__(self, win):
@ -563,6 +562,15 @@ class BTChipQTHandler:
self.d.hide() self.d.hide()
self.d = None 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: class BTChipCmdLineHandler:
def stop(self): def stop(self):

View file

@ -79,7 +79,7 @@ class Listener(util.DaemonThread):
time.sleep(30) time.sleep(30)
class Plugin(BasePlugin): class QtPlugin(BasePlugin):
def __init__(self, parent, config, name): def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name) BasePlugin.__init__(self, parent, config, name)

View file

@ -101,7 +101,7 @@ class Processor(threading.Thread):
s.quit() s.quit()
class Plugin(BasePlugin): class QtPlugin(BasePlugin):
def fullname(self): def fullname(self):
return 'Email' return 'Email'

View file

@ -1,6 +1,3 @@
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from datetime import datetime from datetime import datetime
import inspect import inspect
import requests import requests
@ -17,8 +14,7 @@ from electrum.plugins import BasePlugin, hook
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import PrintError, ThreadJob, timestamp_to_datetime from electrum.util import PrintError, ThreadJob, timestamp_to_datetime
from electrum.util import format_satoshis 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 # See https://en.wikipedia.org/wiki/ISO_4217
CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, 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 @hook
def on_new_window(self, window): def format_amount_and_units(self, btc_balance):
# Additional send and receive edit boxes rate = self.exchange_rate()
send_e = AmountEdit(self.config_ccy) return '' if rate is None else " (%s %s)" % (self.value_str(btc_balance, rate), self.ccy)
window.send_grid.addWidget(send_e, 4, 2, Qt.AlignLeft)
window.amount_e.frozen.connect( @hook
lambda: send_e.setFrozen(window.amount_e.isReadOnly())) def get_fiat_status_text(self, btc_balance):
receive_e = AmountEdit(self.config_ccy) rate = self.exchange_rate()
window.receive_grid.addWidget(receive_e, 2, 2, Qt.AlignLeft) return _(" (No FX rate available)") if rate is None else "1 BTC~%s %s" % (self.value_str(COIN, rate), self.ccy)
window.fiat_send_e = send_e
window.fiat_receive_e = receive_e def get_historical_rates(self):
self.connect_fields(window, window.amount_e, send_e, window.fee_e) if self.show_history():
self.connect_fields(window, window.receive_amount_e, receive_e, None) self.exchange.get_historical_rates(self.ccy)
window.history_list.refresh_headers()
window.update_status() def requires_settings(self):
window.connect(window.app, SIGNAL('new_fx_quotes'), lambda: self.on_fx_quotes(window)) return True
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)) def value_str(self, satoshis, rate):
window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers) 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): def connect_fields(self, window, btc_e, fiat_e, fee_e):
@ -412,68 +462,25 @@ class Plugin(BasePlugin, ThreadJob):
combo.blockSignals(False) combo.blockSignals(False)
combo.setCurrentIndex(combo.findText(self.ccy)) 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 @hook
def format_amount_and_units(self, btc_balance): def on_new_window(self, window):
rate = self.exchange_rate() # Additional send and receive edit boxes
return '' if rate is None else " (%s %s)" % (self.value_str(btc_balance, rate), self.ccy) send_e = AmountEdit(self.config_ccy)
window.send_grid.addWidget(send_e, 4, 2, Qt.AlignLeft)
@hook window.amount_e.frozen.connect(
def get_fiat_status_text(self, btc_balance): lambda: send_e.setFrozen(window.amount_e.isReadOnly()))
rate = self.exchange_rate() receive_e = AmountEdit(self.config_ccy)
return _(" (No FX rate available)") if rate is None else "1 BTC~%s %s" % (self.value_str(COIN, rate), self.ccy) window.receive_grid.addWidget(receive_e, 2, 2, Qt.AlignLeft)
window.fiat_send_e = send_e
def get_historical_rates(self): window.fiat_receive_e = receive_e
if self.show_history(): self.connect_fields(window, window.amount_e, send_e, window.fee_e)
self.exchange.get_historical_rates(self.ccy) self.connect_fields(window, window.receive_amount_e, receive_e, None)
window.history_list.refresh_headers()
def requires_settings(self): window.update_status()
return True 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))
def value_str(self, satoshis, rate): window.connect(window.app, SIGNAL('close_fx_plugin'), lambda: self.restore_window(window))
if satoshis is None: # Can happen with incomplete history window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers)
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 settings_widget(self, window): def settings_widget(self, window):
return EnterButton(_('Settings'), self.settings_dialog) return EnterButton(_('Settings'), self.settings_dialog)

View file

@ -28,7 +28,7 @@ from electrum.i18n import _
class Plugin(BasePlugin): class QtPlugin(BasePlugin):
button_label = _("Verify GA instant") button_label = _("Verify GA instant")

View file

@ -7,8 +7,6 @@ import threading
import re import re
from functools import partial from functools import partial
from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton
import PyQt4.QtCore as QtCore
import electrum import electrum
from electrum import bitcoin 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.util import print_error, print_msg
from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root 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: try:
from keepkeylib.client import types from keepkeylib.client import types
from keepkeylib.client import proto, BaseClient, ProtocolMixin from keepkeylib.client import proto, BaseClient, ProtocolMixin
from keepkeylib.qt.pinmatrix import PinMatrixWidget
from keepkeylib.transport import ConnectionError from keepkeylib.transport import ConnectionError
from keepkeylib.transport_hid import HidTransport from keepkeylib.transport_hid import HidTransport
KEEPKEY = True KEEPKEY = True
@ -47,307 +41,6 @@ def give_error(message):
raise Exception(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): class KeepKeyWallet(BIP32_HD_Wallet):
@ -513,6 +206,325 @@ class KeepKeyWallet(BIP32_HD_Wallet):
return self.proper_device 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): class KeepKeyGuiMixin(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View file

@ -7,15 +7,6 @@ import sys
import traceback import traceback
from functools import partial 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 aes
import base64 import base64
@ -23,8 +14,6 @@ import electrum
from electrum.plugins import BasePlugin, hook from electrum.plugins import BasePlugin, hook
from electrum.i18n import _ from electrum.i18n import _
from electrum_gui.qt import HelpButton, EnterButton
from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton
class Plugin(BasePlugin): class Plugin(BasePlugin):
@ -32,34 +21,6 @@ class Plugin(BasePlugin):
BasePlugin.__init__(self, parent, config, name) BasePlugin.__init__(self, parent, config, name)
self.target_host = 'sync.bytesized-hosting.com:9090' self.target_host = 'sync.bytesized-hosting.com:9090'
self.wallets = {} 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): def encode(self, wallet, msg):
password, iv, wallet_id = self.wallets[wallet] password, iv, wallet_id = self.wallets[wallet]
@ -85,9 +46,6 @@ class Plugin(BasePlugin):
self.print_error("set", wallet.basename(), "nonce to", nonce) self.print_error("set", wallet.basename(), "nonce to", nonce)
wallet.storage.put("wallet_nonce", nonce, force_write) wallet.storage.put("wallet_nonce", nonce, force_write)
def requires_settings(self):
return True
@hook @hook
def set_label(self, wallet, item, label): def set_label(self, wallet, item, label):
if not wallet in self.wallets: if not wallet in self.wallets:
@ -105,47 +63,6 @@ class Plugin(BasePlugin):
# Caller will write the wallet # Caller will write the wallet
self.set_nonce(wallet, nonce + 1, force_write=False) 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): def do_request(self, method, url = "/labels", is_batch=False, data=None):
url = 'https://' + self.target_host + url url = 'https://' + self.target_host + url
kwargs = {'headers': {}} kwargs = {'headers': {}}
@ -162,8 +79,7 @@ class Plugin(BasePlugin):
raise BaseException(response["error"]) raise BaseException(response["error"])
return response return response
def push_thread(self, window): def push_thread(self, wallet):
wallet = window.wallet
wallet_id = self.wallets[wallet][2] wallet_id = self.wallets[wallet][2]
bundle = {"labels": [], bundle = {"labels": [],
"walletId": wallet_id, "walletId": wallet_id,
@ -179,14 +95,14 @@ class Plugin(BasePlugin):
'externalId': encoded_key}) 'externalId': encoded_key})
self.do_request("POST", "/labels", True, bundle) self.do_request("POST", "/labels", True, bundle)
def pull_thread(self, window, force): def pull_thread(self, wallet, force):
wallet = window.wallet
wallet_id = self.wallets[wallet][2] wallet_id = self.wallets[wallet][2]
nonce = 1 if force else self.get_nonce(wallet) - 1 nonce = 1 if force else self.get_nonce(wallet) - 1
self.print_error("asking for labels since nonce", nonce) self.print_error("asking for labels since nonce", nonce)
try: try:
response = self.do_request("GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id) )) response = self.do_request("GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id) ))
if response["labels"] is None: if response["labels"] is None:
self.print_error('no new labels')
return return
result = {} result = {}
for label in response["labels"]: for label in response["labels"]:
@ -208,9 +124,86 @@ class Plugin(BasePlugin):
wallet.labels[key] = value wallet.labels[key] = value
self.print_error("received %d labels" % len(response)) self.print_error("received %d labels" % len(response))
self.obj.emit(SIGNAL('labels:pulled'), window, # do not write to disk because we're in a daemon thread
response["nonce"] + 1) wallet.storage.put('labels', wallet.labels, False)
self.set_nonce(wallet, response["nonce"] + 1, False)
self.on_pulled(wallet)
except Exception as e: except Exception as e:
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
self.print_error("could not retrieve labels") 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)

View file

@ -17,7 +17,7 @@ except:
flag_matlib=False flag_matlib=False
class Plugin(BasePlugin): class QtPlugin(BasePlugin):
def is_available(self): def is_available(self):
if flag_matlib: if flag_matlib:

View file

@ -7,8 +7,6 @@ import threading
import re import re
from functools import partial from functools import partial
from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton
import PyQt4.QtCore as QtCore
import electrum import electrum
from electrum import bitcoin 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.util import print_error, print_msg
from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root 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: try:
from trezorlib.client import types from trezorlib.client import types
from trezorlib.client import proto, BaseClient, ProtocolMixin from trezorlib.client import proto, BaseClient, ProtocolMixin
from trezorlib.qt.pinmatrix import PinMatrixWidget
from trezorlib.transport import ConnectionError from trezorlib.transport import ConnectionError
from trezorlib.transport_hid import HidTransport from trezorlib.transport_hid import HidTransport
TREZOR = True TREZOR = True
@ -47,308 +40,6 @@ def give_error(message):
raise Exception(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): class TrezorWallet(BIP32_HD_Wallet):
wallet_type = 'trezor' wallet_type = 'trezor'
root_derivation = "m/44'/0'" root_derivation = "m/44'/0'"
@ -503,15 +194,326 @@ class TrezorWallet(BIP32_HD_Wallet):
n = self.get_client().expand_path(address_id) n = self.get_client().expand_path(address_id)
device_address = self.get_client().get_address('Bitcoin', n) device_address = self.get_client().get_address('Bitcoin', n)
self.device_checked = True self.device_checked = True
self.proper_device = (device_address == address)
if device_address != address:
self.proper_device = False
else:
self.proper_device = True
return self.proper_device 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): class TrezorGuiMixin(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View file

@ -27,9 +27,6 @@ from urlparse import urljoin
from urllib import quote from urllib import quote
from functools import partial from functools import partial
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import electrum import electrum
from electrum import bitcoin from electrum import bitcoin
from electrum.bitcoin import * from electrum.bitcoin import *
@ -39,10 +36,6 @@ from electrum.wallet import Multisig_Wallet, BIP32_Wallet
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugins import BasePlugin, run_hook, hook 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 from decimal import Decimal
@ -460,6 +453,16 @@ class Plugin(BasePlugin):
wallet.add_master_public_key('x3/', xpub3) wallet.add_master_public_key('x3/', xpub3)
return True 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): def auth_dialog(self, window):
d = QDialog(window) d = QDialog(window)
d.setModal(1) d.setModal(1)
@ -674,3 +677,5 @@ class Plugin(BasePlugin):
except: except:
QMessageBox.information(window, _('Message'), _('Incorrect password'), _('OK')) QMessageBox.information(window, _('Message'), _('Incorrect password'), _('OK'))
pw.setText('') pw.setText('')

View file

@ -3,7 +3,7 @@ from electrum.plugins import BasePlugin, hook
from electrum.i18n import _ from electrum.i18n import _
import random import random
class Plugin(BasePlugin): class QtPlugin(BasePlugin):
vkb = None vkb = None
vkb_index = 0 vkb_index = 0