electrum

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

commit f3329988b28ba89ff2e27b03a98a88e45d9bddd7
parent 33e57fe5a79c1b83aa0f21087eab9d69f36b09e7
Author: Neil Booth <kyuupichan@gmail.com>
Date:   Sun, 27 Dec 2015 13:56:50 +0900

More keepkey / trezor commonizing and cleanup

Diffstat:
Mplugins/keepkey/keepkey.py | 50+++++++-------------------------------------------
Aplugins/trezor/client.py | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/trezor/gui_mixin.py | 57---------------------------------------------------------
Aplugins/trezor/plugin.py | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/trezor/plugin_generic.py | 206-------------------------------------------------------------------------------
Mplugins/trezor/trezor.py | 54++++++++++--------------------------------------------
6 files changed, 313 insertions(+), 350 deletions(-)

diff --git a/plugins/keepkey/keepkey.py b/plugins/keepkey/keepkey.py @@ -5,8 +5,6 @@ from plugins.trezor.plugin_generic import TrezorCompatiblePlugin try: from keepkeylib.client import proto, BaseClient, ProtocolMixin - from keepkeylib.transport import ConnectionError - from keepkeylib.transport_hid import HidTransport KEEPKEY = True except ImportError: KEEPKEY = False @@ -19,46 +17,12 @@ class KeepKeyWallet(BIP32_Hardware_Wallet): class KeepKeyPlugin(TrezorCompatiblePlugin): - wallet_type = 'keepkey' + client_class = trezor_client_class(ProtocolMixin, BaseClient, proto) + firmware_URL = 'https://www.keepkey.com' + libraries_URL = 'https://github.com/keepkey/python-keepkey' + libraries_available = KEEPKEY + minimum_firmware = (1, 0, 0) + wallet_class = KeepKeyWallet import keepkeylib.ckd_public as ckd_public from keepkeylib.client import types - - @staticmethod - def libraries_available(): - return KEEPKEY - - def constructor(self, s): - return KeepKeyWallet(s) - - 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 - - -if KEEPKEY: - class QtGuiKeepKeyClient(ProtocolMixin, GuiMixin, BaseClient): - protocol = proto - device = 'KeepKey' - - def call_raw(self, msg): - try: - resp = BaseClient.call_raw(self, msg) - except ConnectionError: - self.bad = True - raise - - return resp + from keepkeylib.transport_hid import HidTransport diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py @@ -0,0 +1,74 @@ +from sys import stderr + +from electrum.i18n import _ + +class GuiMixin(object): + # Requires: self.proto, self.device + + def callback_ButtonRequest(self, msg): + if msg.code == 3: + message = _("Confirm transaction outputs on %s device to continue") + elif msg.code == 8: + message = _("Confirm transaction fee on %s device to continue") + elif msg.code == 7: + message = _("Confirm message to sign on %s device to continue") + elif msg.code == 10: + message = _("Confirm address on %s device to continue") + else: + message = _("Check %s device to continue") + + if msg.code in [3, 8] and hasattr(self, 'cancel'): + cancel_callback = self.cancel + else: + cancel_callback = None + + self.handler.show_message(message % self.device, cancel_callback) + return self.proto.ButtonAck() + + def callback_PinMatrixRequest(self, msg): + if msg.type == 1: + msg = _("Please enter %s current PIN") + elif msg.type == 2: + msg = _("Please enter %s new PIN") + elif msg.type == 3: + msg = _("Please enter %s new PIN again") + else: + msg = _("Please enter %s PIN") + pin = self.handler.get_pin(msg % self.device) + if not pin: + return self.proto.Cancel() + return self.proto.PinMatrixAck(pin=pin) + + def callback_PassphraseRequest(self, req): + msg = _("Please enter your %s passphrase") + passphrase = self.handler.get_passphrase(msg % self.device) + if passphrase is None: + return self.proto.Cancel() + return self.proto.PassphraseAck(passphrase=passphrase) + + def callback_WordRequest(self, msg): + #TODO + stderr.write("Enter one word of mnemonic:\n") + stderr.flush() + word = raw_input() + return self.proto.WordAck(word=word) + +def trezor_client_class(protocol_mixin, base_client, proto): + '''Returns a class dynamically.''' + + class TrezorClient(protocol_mixin, GuiMixin, base_client): + + def __init__(self, transport, device): + base_client.__init__(self, transport) + protocol_mixin.__init__(self, transport) + self.proto = proto + self.device = device + + def call_raw(self, msg): + try: + return base_client.call_raw(self, msg) + except: + self.bad = True + raise + + return TrezorClient diff --git a/plugins/trezor/gui_mixin.py b/plugins/trezor/gui_mixin.py @@ -1,57 +0,0 @@ -from sys import stderr - -from electrum.i18n import _ - -class GuiMixin(object): - # Requires: self.protcol, self.device - - def __init__(self, *args, **kwargs): - super(GuiMixin, self).__init__(*args, **kwargs) - - def callback_ButtonRequest(self, msg): - if msg.code == 3: - message = _("Confirm transaction outputs on %s device to continue") - elif msg.code == 8: - message = _("Confirm transaction fee on %s device to continue") - elif msg.code == 7: - message = _("Confirm message to sign on %s device to continue") - elif msg.code == 10: - message = _("Confirm address on %s device to continue") - else: - message = _("Check %s device to continue") - - if msg.code in [3, 8] and hasattr(self, 'cancel'): - cancel_callback = self.cancel - else: - cancel_callback = None - - self.handler.show_message(message % self.device, cancel_callback) - return self.protocol.ButtonAck() - - def callback_PinMatrixRequest(self, msg): - if msg.type == 1: - msg = _("Please enter %s current PIN") - elif msg.type == 2: - msg = _("Please enter %s new PIN") - elif msg.type == 3: - msg = _("Please enter %s new PIN again") - else: - msg = _("Please enter %s PIN") - pin = self.handler.get_pin(msg % self.device) - if not pin: - return self.protocol.Cancel() - return self.protocol.PinMatrixAck(pin=pin) - - def callback_PassphraseRequest(self, req): - msg = _("Please enter your %s passphrase") - passphrase = self.handler.get_passphrase(msg % self.device) - if passphrase is None: - return self.protocol.Cancel() - return self.protocol.PassphraseAck(passphrase=passphrase) - - def callback_WordRequest(self, msg): - #TODO - stderr.write("Enter one word of mnemonic:\n") - stderr.flush() - word = raw_input() - return self.protocol.WordAck(word=word) diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py @@ -0,0 +1,222 @@ +from binascii import unhexlify + +from electrum.account import BIP32_Account +from electrum.bitcoin import bc_address_to_hash_160, xpub_from_pubkey +from electrum.i18n import _ +from electrum.plugins import BasePlugin, hook +from electrum.transaction import deserialize, is_extended_pubkey + +class TrezorCompatiblePlugin(BasePlugin): + # Derived classes provide: + # + # class-static variables: client_class, firmware_URL, + # libraries_available, libraries_URL, minimum_firmware, + # wallet_class, ckd_public, types, HidTransport + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.device = self.wallet_class.device + self.wallet = None + self.handler = None + self.client = None + + def constructor(self, s): + return self.wallet_class(s) + + def give_error(self, message): + self.print_error(message) + raise Exception(message) + + def is_available(self): + if not self.libraries_available: + return False + if not self.wallet: + return False + wallet_type = self.wallet.storage.get('wallet_type') + return wallet_type == self.wallet_class.wallet_type + + 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 get_client(self): + if not self.libraries_available: + self.give_error(_('please install the %s libraries from %s') + % (self.device, self.libraries_URL)) + + if not self.client or self.client.bad: + d = self.HidTransport.enumerate() + if not d: + self.give_error(_('Could not connect to your %s. Please ' + 'verify the cable is connected and that no ' + 'other app is using it.' % self.device)) + transport = self.HidTransport(d[0]) + self.client = self.client_class(transport, self.device) + self.client.handler = self.handler + self.client.set_tx_api(self) + self.client.bad = False + if not self.atleast_version(*self.minimum_firmware): + self.client = None + self.give_error(_('Outdated %s firmware. Please update the ' + 'firmware from %s') % (self.device, + self.firmware_URL)) + return self.client + + def compare_version(self, major, minor=0, patch=0): + f = self.get_client().features + v = [f.major_version, f.minor_version, f.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 + + @hook + def close_wallet(self): + self.print_error("clear session") + if self.client: + self.client.clear_session() + self.client.transport.close() + self.client = None + self.wallet = None + + 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 as e: + self.give_error(e) + finally: + self.handler.stop() + raw = signed_tx.encode('hex') + tx.update_signatures(raw) + + def show_address(self, address): + client = self.get_client() + self.wallet.check_proper_device() + try: + address_path = self.wallet.address_id(address) + address_n = client.expand_path(address_path) + except Exception as e: + self.give_error(e) + try: + client.get_address('Bitcoin', address_n, True) + except Exception as e: + self.give_error(e) + finally: + self.handler.stop() + + def tx_inputs(self, tx, for_sig=False): + client = self.get_client() + inputs = [] + for txin in tx.inputs: + txinputtype = self.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 = 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 self.types.HDNodePathType(node=node, address_n=s) + pubkeys = map(f, x_pubkeys) + multisig = self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), + m=txin.get('num_sig'), + ) + txinputtype = self.types.TxInputType( + script_type=self.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 = 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): + client = self.get_client() + outputs = [] + + for type, address, amount in tx.outputs: + assert type == 'address' + txoutputtype = self.types.TxOutputType() + if self.wallet.is_change(address): + address_path = self.wallet.address_id(address) + address_n = 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 = self.types.PAYTOADDRESS + elif addrtype == 5: + txoutputtype.script_type = self.types.PAYTOSCRIPTHASH + else: + raise BaseException('addrtype') + outputs.append(txoutputtype) + + return outputs + + def electrum_tx_to_txtype(self, tx): + t = self.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) diff --git a/plugins/trezor/plugin_generic.py b/plugins/trezor/plugin_generic.py @@ -1,206 +0,0 @@ -from binascii import unhexlify - -from electrum.account import BIP32_Account -from electrum.bitcoin import bc_address_to_hash_160, xpub_from_pubkey -from electrum.i18n import _ -from electrum.plugins import BasePlugin, hook -from electrum.transaction import deserialize, is_extended_pubkey - -class TrezorCompatiblePlugin(BasePlugin): - # Derived classes provide: - - # libraries_available() - # constructor() - # ckd_public - # types - # wallet_type - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.wallet = None - self.handler = None - self.client = None - self.transport = None - - def constructor(self, s): - raise NotImplementedError - - @staticmethod - def libraries_available(): - raise NotImplementedError - - def give_error(self, message): - self.print_error(message) - raise Exception(message) - - def is_available(self): - if not self.libraries_available(): - return False - if not self.wallet: - return False - if self.wallet.storage.get('wallet_type') != self.wallet_type: - 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 - - @hook - def close_wallet(self): - self.print_error("clear session") - if self.client: - self.client.clear_session() - self.client.transport.close() - self.client = None - self.wallet = None - - 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 as e: - self.give_error(e) - finally: - self.handler.stop() - raw = signed_tx.encode('hex') - tx.update_signatures(raw) - - def show_address(self, address): - client = self.get_client() - self.wallet.check_proper_device() - try: - address_path = self.wallet.address_id(address) - address_n = client.expand_path(address_path) - except Exception as e: - self.give_error(e) - try: - client.get_address('Bitcoin', address_n, True) - except Exception as e: - self.give_error(e) - finally: - self.handler.stop() - - def tx_inputs(self, tx, for_sig=False): - client = self.get_client() - inputs = [] - for txin in tx.inputs: - txinputtype = self.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 = 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 self.types.HDNodePathType(node=node, address_n=s) - pubkeys = map(f, x_pubkeys) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), - m=txin.get('num_sig'), - ) - txinputtype = self.types.TxInputType( - script_type=self.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 = 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): - client = self.get_client() - outputs = [] - - for type, address, amount in tx.outputs: - assert type == 'address' - txoutputtype = self.types.TxOutputType() - if self.wallet.is_change(address): - address_path = self.wallet.address_id(address) - address_n = 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 = self.types.PAYTOADDRESS - elif addrtype == 5: - txoutputtype.script_type = self.types.PAYTOSCRIPTHASH - else: - raise BaseException('addrtype') - outputs.append(txoutputtype) - - return outputs - - def electrum_tx_to_txtype(self, tx): - t = self.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) diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py @@ -1,12 +1,10 @@ from electrum.wallet import BIP32_Hardware_Wallet -from plugins.trezor.gui_mixin import GuiMixin -from plugins.trezor.plugin_generic import TrezorCompatiblePlugin +from plugins.trezor.client import trezor_client_class +from plugins.trezor.plugin import TrezorCompatiblePlugin try: from trezorlib.client import proto, BaseClient, ProtocolMixin - from trezorlib.transport import ConnectionError - from trezorlib.transport_hid import HidTransport TREZOR = True except ImportError: TREZOR = False @@ -17,46 +15,14 @@ class TrezorWallet(BIP32_Hardware_Wallet): root_derivation = "m/44'/0'" device = 'Trezor' + class TrezorPlugin(TrezorCompatiblePlugin): - wallet_type = 'trezor' + client_class = trezor_client_class(ProtocolMixin, BaseClient, proto) + firmware_URL = 'https://www.mytrezor.com' + libraries_URL = 'https://github.com/trezor/python-trezor' + libraries_available = TREZOR + minimum_firmware = (1, 2, 1) + wallet_class = TrezorWallet import trezorlib.ckd_public as ckd_public from trezorlib.client import types - - @staticmethod - def libraries_available(): - return TREZOR - - def constructor(self, s): - return TrezorWallet(s) - - def get_client(self): - if not TREZOR: - self.give_error('please install github.com/trezor/python-trezor') - - if not self.client or self.client.bad: - d = HidTransport.enumerate() - if not d: - self.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 - self.give_error('Outdated Trezor firmware. Please update the firmware from https://www.mytrezor.com') - return self.client - -if TREZOR: - class QtGuiTrezorClient(ProtocolMixin, GuiMixin, BaseClient): - protocol = proto - device = 'Trezor' - - def call_raw(self, msg): - try: - resp = BaseClient.call_raw(self, msg) - except ConnectionError: - self.bad = True - raise - - return resp + from trezorlib.transport_hid import HidTransport