tallow several hardware cosigners in the same wallet - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 80675121ce4882aa59ab98e662a2c3e8239602c6 DIR parent d16fb3ee482d79bf9700c48c9f11d1db024457a6 HTML Author: ThomasV <thomasv@electrum.org> Date: Mon, 22 Aug 2016 12:50:24 +0200 allow several hardware cosigners in the same wallet Diffstat: M lib/base_wizard.py | 89 ++++++++++++++++++------------- M lib/wallet.py | 15 +++++++++------ M plugins/trezor/plugin.py | 5 +++-- M plugins/trezor/qt_generic.py | 30 +++++++++++++++--------------- 4 files changed, 78 insertions(+), 61 deletions(-) --- DIR diff --git a/lib/base_wizard.py b/lib/base_wizard.py t@@ -78,6 +78,7 @@ class BaseWizard(object): ('standard', _("Standard wallet")), ('2fa', _("Wallet with two-factor authentication")), ('multisig', _("Multi-signature wallet")), + ('imported', _("Watch Bitcoin addresses")), ] choices = [pair for pair in wallet_kinds if pair[0] in wallet_types] self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type) t@@ -93,26 +94,37 @@ class BaseWizard(object): self.storage.put('use_trustedcoin', True) self.plugin = self.plugins.load_plugin('trustedcoin') action = self.storage.get_action() - + elif choice == 'imported': + action = 'import_addresses' self.run(action) def choose_multisig(self): def on_multisig(m, n): self.multisig_type = "%dof%d"%(m, n) + self.storage.put('wallet_type', self.multisig_type) self.n = n + self.keystores = [] self.run('choose_keystore') self.multisig_dialog(run_next=on_multisig) def choose_keystore(self): assert self.wallet_type in ['standard', 'multisig'] - title = _('Keystore') + c = self.wallet_type == 'multisig' and len(self.keystores)>0 + title = _('Add cosigner') + ' %d'%len(self.keystores) if c else _('Keystore') message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') - choices = [ - ('create_seed', _('Create a new seed')), - ('restore_seed', _('I already have a seed')), - ('restore_from_key', _('Import keys or addresses')), - ('choose_hw', _('Use hardware keystore')), - ] + if not c: + choices = [ + ('create_seed', _('Create a new seed')), + ('restore_seed', _('I already have a seed')), + ('restore_from_key', _('Import keys')), + ('choose_hw', _('Use hardware device')), + ] + else: + choices = [ + ('restore_from_key', _('Import cosigner key')), + ('choose_hw', _('Cosign with hardware device')), + ] + self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) def restore_seed(self): t@@ -130,12 +142,18 @@ class BaseWizard(object): else: self.create_keystore(text, None) + def import_addresses(self): + v = keystore.is_address_list + title = _("Import Bitcoin Addresses") + message = _("Enter a list of Bitcoin addresses. This will create a watching-only wallet.") + self.restore_keys_dialog(title=title, message=message, run_next=self.on_restore, is_valid=v) + def restore_from_key(self): if self.wallet_type == 'standard': v = keystore.is_any_key title = _("Import keys") message = ' '.join([ - _("To create a watching-only wallet, please enter your master public key (xpub), or a list of Bitcoin addresses."), + _("To create a watching-only wallet, please enter your master public key (xpub)."), _("To create a spending wallet, please enter a master private key (xprv), or a list of Bitcoin private keys.") ]) else: t@@ -179,7 +197,7 @@ class BaseWizard(object): derivation = bip44_derivation(int(account_id)) plugin = self.plugins.get_plugin(self.hw_type) k = plugin.create_keystore(self.hw_type, derivation, self) - self.create_wallet(k, None) + self.on_keystore(k, None) def on_hardware_seed(self): self.storage.put('key_type', 'hw_seed') t@@ -207,46 +225,41 @@ class BaseWizard(object): derivation = "m/44'/0'/%d'"%account_id self.storage.put('account_id', account_id) k.add_xprv_from_seed(bip32_seed, derivation, password) - self.storage.put('keystore', k.dump()) - self.wallet = Standard_Wallet(self.storage) - self.run('create_addresses') + self.on_keystore(k, password) - def create_wallet(self, k, password): + def on_keystore(self, k, password): if self.wallet_type == 'standard': self.storage.put('keystore', k.dump()) self.wallet = Standard_Wallet(self.storage) self.run('create_addresses') elif self.wallet_type == 'multisig': - self.storage.put('wallet_type', self.multisig_type) - self.add_cosigner(k, 0) - xpub = k.get_master_public_key() - self.stack = [] - self.run('show_xpub_and_add_cosigners', password, xpub) - def show_xpub_and_add_cosigners(self, password, xpub): - self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('add_cosigners', password, 1)) - - def add_cosigner(self, keystore, i): - d = self.storage.get('master_public_keys', {}) - if keystore.xpub in d.values(): - raise BaseException('duplicate key') - self.storage.put('x%d/'%(i+1), keystore.dump()) + if k.xpub in map(lambda x: x.xpub, self.keystores): + raise BaseException('duplicate key') + self.keystores.append(k) + + if len(self.keystores) == 1: + xpub = k.get_master_public_key() + self.stack = [] + self.run('show_xpub_and_add_cosigners', xpub) + elif len(self.keystores) < self.n: + self.run('choose_keystore') + else: + for i, k in enumerate(self.keystores): + self.storage.put('x%d/'%(i+1), k.dump()) + self.storage.write() + self.wallet = Multisig_Wallet(self.storage) + self.run('create_addresses') + + def show_xpub_and_add_cosigners(self, xpub): + self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore')) def add_cosigners(self, password, i): self.add_cosigner_dialog(run_next=lambda x: self.on_cosigner(x, password, i), index=i, is_valid=keystore.is_xpub) def on_cosigner(self, text, password, i): k = keystore.from_text(text, password) - try: - self.add_cosigner(k, i) - except BaseException as e: - self.show_message("error:" + str(e)) - return - if i < self.n - 1: - self.run('add_cosigners', password, i+1) - else: - self.wallet = Multisig_Wallet(self.storage) - self.create_addresses() + self.on_keystore(k) def create_seed(self): from electrum.mnemonic import Mnemonic t@@ -262,7 +275,7 @@ class BaseWizard(object): def create_keystore(self, text, password): k = keystore.from_text(text, password) - self.create_wallet(k, password) + self.on_keystore(k, password) def create_addresses(self): def task(): DIR diff --git a/lib/wallet.py b/lib/wallet.py t@@ -990,9 +990,9 @@ class Abstract_Wallet(PrintError): return False if tx.is_complete(): return False - # add input info. (should be done already) - for txin in tx.inputs(): - self.add_input_info(txin) + ## add input info. (should be done already) + #for txin in tx.inputs(): + # self.add_input_info(txin) can_sign = any([txin['can_sign'] for txin in tx.inputs()]) return can_sign t@@ -1027,7 +1027,10 @@ class Abstract_Wallet(PrintError): # sign for keystore in self.get_keystores(): if not keystore.is_watching_only(): - keystore.sign_transaction(tx, password) + try: + keystore.sign_transaction(tx, password) + except: + print "keystore cannot sign", keystore def get_unused_addresses(self): # fixme: use slots from expired requests t@@ -1519,7 +1522,7 @@ class Multisig_Wallet(Deterministic_Wallet): def add_input_sig_info(self, txin, address): txin['derivation'] = derivation = self.get_address_index(address) pubkeys = self.get_pubkeys(*derivation) - x_pubkeys = self.get_xpubkeys(*derivation) + x_pubkeys = [k.get_xpubkey(*derivation) for k in self.get_keystores()] # sort pubkeys and x_pubkeys, using the order of pubkeys pubkeys, x_pubkeys = zip( *sorted(zip(pubkeys, x_pubkeys))) txin['pubkeys'] = list(pubkeys) t@@ -1527,7 +1530,7 @@ class Multisig_Wallet(Deterministic_Wallet): txin['signatures'] = [None] * len(pubkeys) txin['redeemScript'] = self.redeem_script(*derivation) txin['num_sig'] = self.m - + txin['can_sign'] = any([x is None for x in txin['signatures']]) DIR diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py t@@ -217,7 +217,6 @@ class TrezorCompatiblePlugin(HW_PluginBase): devmgr = self.device_manager() device_info = devmgr.select_device(handler, self) device_id = device_info.device.id_ - #devmgr.pair_wallet(keystore, device_info.device.id_) if device_info.initialized: task = lambda: self.init_xpub(derivation, device_id, handler) else: t@@ -239,7 +238,9 @@ class TrezorCompatiblePlugin(HW_PluginBase): if not client.atleast_version(1, 3): wallet.handler.show_error(_("Your device firmware is too old")) return - address_path = wallet.address_id(address) + change, index = wallet.get_address_index(address) + derivation = wallet.keystore.derivation + address_path = "%s/%d/%d"%(derivation, change, index) address_n = client.expand_path(address_path) client.get_address('Bitcoin', address_n, True) DIR diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py t@@ -273,16 +273,16 @@ def qt_plugin_class(base_plugin_class): @hook def load_wallet(self, wallet, window): - keystore = wallet.get_keystore() - if type(keystore) != self.keystore_class: - return - window.tzb = StatusBarButton(QIcon(self.icon_file), self.device, - partial(self.settings_dialog, window)) - window.statusBar().addPermanentWidget(window.tzb) - keystore.handler = self.create_handler(window) - keystore.thread = TaskThread(window, window.on_error) - # Trigger a pairing - keystore.thread.add(partial(self.get_client, keystore)) + for keystore in wallet.get_keystores(): + if type(keystore) != self.keystore_class: + continue + window.tzb = StatusBarButton(QIcon(self.icon_file), self.device, + partial(self.settings_dialog, window)) + window.statusBar().addPermanentWidget(window.tzb) + keystore.handler = self.create_handler(window) + keystore.thread = TaskThread(window, window.on_error) + # Trigger a pairing + keystore.thread.add(partial(self.get_client, keystore)) def create_keystore(self, hw_type, derivation, wizard): from electrum.keystore import hardware_keystore t@@ -310,11 +310,11 @@ def qt_plugin_class(base_plugin_class): @hook def receive_menu(self, menu, addrs, wallet): - keystore = wallet.get_keystore() - if type(keystore) == self.keystore_class and len(addrs) == 1: - def show_address(): - keystore.thread.add(partial(self.show_address, wallet, addrs[0])) - menu.addAction(_("Show on %s") % self.device, show_address) + for keystore in wallet.get_keystores(): + if type(keystore) == self.keystore_class and len(addrs) == 1: + def show_address(): + keystore.thread.add(partial(self.show_address, wallet, addrs[0])) + menu.addAction(_("Show on %s") % self.device, show_address) def settings_dialog(self, window): device_id = self.choose_device(window)