1
0

Better install wizard

Break out the workflow logic of the install wizard
into a base class.  This means reimplementing with
full support in a new GUI is now easy; you just provide
ways to request passwords, show messages etc.  The API
is fully documented in the base class.

There are a couple of minor outstanding issues, including that
the old messages shown when recovering a wallet are missing.
I will come back to that.  Ledger wallet might be broken.

Other improvements:

The install wizard code is now easy to follow and understand.
Hardware wallets can now be restored without any need for their
accompanying libraries.
Various bits of trustedcoin were broken and have been fixed.
Many plugin hooks can be removed.  I have only started on this.
This commit is contained in:
Neil Booth
2015-12-31 11:36:33 +09:00
parent e6dbe621c6
commit 11d135b32d
20 changed files with 750 additions and 647 deletions

View File

@@ -11,3 +11,4 @@ import transaction
from transaction import Transaction
from plugins import BasePlugin
from commands import Commands, known_commands
import wizard as wizard

View File

@@ -26,6 +26,7 @@ import time
from util import *
from i18n import _
from util import profiler, PrintError, DaemonThread
import wallet
class Plugins(DaemonThread):
@@ -38,18 +39,21 @@ class Plugins(DaemonThread):
else:
plugins = __import__('electrum_plugins')
self.pkgpath = os.path.dirname(plugins.__file__)
self.config = config
self.hw_wallets = {}
self.plugins = {}
self.gui_name = gui_name
self.descriptions = []
for loader, name, ispkg in pkgutil.iter_modules([self.pkgpath]):
m = loader.find_module(name).load_module(name)
d = m.__dict__
if gui_name not in d.get('available_for', []):
gui_good = gui_name in d.get('available_for', [])
details = d.get('registers_wallet_type')
if details:
self.register_plugin_wallet(name, gui_good, details)
if not gui_good:
continue
self.descriptions.append(d)
x = d.get('registers_wallet_type')
if x:
self.register_wallet_type(config, name, x)
if not d.get('requires_wallet_type') and config.get('use_' + name):
self.load_plugin(config, name)
@@ -100,15 +104,28 @@ class Plugins(DaemonThread):
return False
return w.wallet_type in d.get('requires_wallet_type', [])
def wallet_plugin_loader(self, config, name):
if self.plugins.get(name) is None:
self.load_plugin(config, name)
return self.plugins[name]
def hardware_wallets(self, action):
result = []
for name, (gui_good, details) in self.hw_wallets.items():
if gui_good:
try:
p = self.wallet_plugin_loader(name)
if action == 'restore' or p.is_enabled():
result.append((details[1], details[2]))
except:
self.print_error("cannot load plugin for:", name)
return result
def register_wallet_type(self, config, name, x):
import wallet
x += (lambda: self.wallet_plugin_loader(config, name),)
wallet.wallet_types.append(x)
def register_plugin_wallet(self, name, gui_good, details):
if details[0] == 'hardware':
self.hw_wallets[name] = (gui_good, details)
register = details + (lambda: self.wallet_plugin_loader(name),)
wallet.wallet_types.append(register)
def wallet_plugin_loader(self, name):
if not name in self.plugins:
self.load_plugin(self.config, name)
return self.plugins[name]
def run(self):
jobs = [job for plugin in self.plugins.values()

View File

@@ -162,6 +162,11 @@ class SimpleConfig(object):
return new_path
def remove_from_recently_open(self, filename):
recent = self.get('recently_open', [])
if filename in recent:
recent.remove(filename)
self.set_key('recently_open', recent)
def read_system_config(path=SYSTEM_CONFIG_PATH):

View File

@@ -205,6 +205,9 @@ class Abstract_Wallet(PrintError):
def diagnostic_name(self):
return self.basename()
def set_use_encryption(self, use_encryption):
self.use_encryption = use_encryption
self.storage.put('use_encryption', use_encryption)
@profiler
def load_transactions(self):
@@ -310,6 +313,9 @@ class Abstract_Wallet(PrintError):
if removed:
self.save_accounts()
def create_main_account(self, password):
pass
def synchronize(self):
pass
@@ -1049,8 +1055,7 @@ class Abstract_Wallet(PrintError):
self.master_private_keys[k] = c
self.storage.put('master_private_keys', self.master_private_keys)
self.use_encryption = (new_password != None)
self.storage.put('use_encryption', self.use_encryption)
self.set_use_encryption(new_password is not None)
def is_frozen(self, addr):
return addr in self.frozen_addresses
@@ -1112,15 +1117,16 @@ class Abstract_Wallet(PrintError):
_("Please wait..."),
_("Addresses generated:"),
len(self.addresses(True)))
apply(callback, (msg,))
callback(msg)
time.sleep(0.1)
def wait_for_network():
while not self.network.is_connected():
if callback:
msg = "%s \n" % (_("Connecting..."))
apply(callback, (msg,))
callback(msg)
time.sleep(0.1)
# wait until we are connected, because the user might have selected another server
# wait until we are connected, because the user
# might have selected another server
if self.network:
wait_for_network()
wait_for_wallet()
@@ -1428,14 +1434,10 @@ class Deterministic_Wallet(Abstract_Wallet):
self.seed_version, self.seed = self.format_seed(seed)
if password:
self.seed = pw_encode( self.seed, password)
self.use_encryption = True
else:
self.use_encryption = False
self.seed = pw_encode(self.seed, password)
self.storage.put('seed', self.seed)
self.storage.put('seed_version', self.seed_version)
self.storage.put('use_encryption', self.use_encryption)
self.set_use_encryption(password is not None)
def get_seed(self, password):
return pw_decode(self.seed, password)
@@ -1527,8 +1529,6 @@ class Deterministic_Wallet(Abstract_Wallet):
def get_action(self):
if not self.get_master_public_key():
return 'create_seed'
if not self.accounts:
return 'create_accounts'
def get_master_public_keys(self):
out = {}
@@ -1632,8 +1632,7 @@ class BIP32_Simple_Wallet(BIP32_Wallet):
self.add_master_private_key(self.root_name, xprv, password)
self.add_master_public_key(self.root_name, xpub)
self.add_account('0', account)
self.use_encryption = (password != None)
self.storage.put('use_encryption', self.use_encryption)
self.set_use_encryption(password is not None)
def create_xpub_wallet(self, xpub):
account = BIP32_Account({'xpub':xpub})
@@ -1797,10 +1796,6 @@ class Multisig_Wallet(BIP32_Wallet, Mnemonic):
for i in range(self.n):
if self.master_public_keys.get("x%d/"%(i+1)) is None:
return 'create_seed' if i == 0 else 'add_cosigners'
if not self.accounts:
return 'create_accounts'
class OldWallet(Deterministic_Wallet):
@@ -1997,6 +1992,15 @@ class Wallet(object):
return (Wallet.is_seed(text) or Wallet.is_xprv(text)
or Wallet.is_private_key(text))
@staticmethod
def multisig_type(wallet_type):
'''If wallet_type is mofn multi-sig, return [m, n],
otherwise return None.'''
match = re.match('(\d+)of(\d+)', wallet_type)
if match:
match = [int(x) for x in match.group(1, 2)]
return match
@staticmethod
def from_seed(seed, password, storage):
if is_old_seed(seed):
@@ -2048,10 +2052,9 @@ class Wallet(object):
def from_multisig(key_list, password, storage, wallet_type):
storage.put('wallet_type', wallet_type)
wallet = Multisig_Wallet(storage)
key_list = sorted(key_list, key = lambda x: Wallet.is_xpub(x))
key_list = sorted(key_list, key = Wallet.is_xpub)
for i, text in enumerate(key_list):
assert Wallet.is_seed(text) or Wallet.is_xprv(text) or Wallet.is_xpub(text)
name = "x%d/"%(i+1)
name = "x%d/" % (i+1)
if Wallet.is_xprv(text):
xpub = bitcoin.xpub_from_xprv(text)
wallet.add_master_public_key(name, xpub)
@@ -2064,8 +2067,9 @@ class Wallet(object):
wallet.create_master_keys(password)
else:
wallet.add_cosigner_seed(text, name, password)
wallet.use_encryption = (password != None)
wallet.storage.put('use_encryption', wallet.use_encryption)
else:
raise RunTimeError("Cannot handle text for multisig")
wallet.set_use_encryption(password is not None)
wallet.create_main_account(password)
return wallet

285
lib/wizard.py Normal file
View File

@@ -0,0 +1,285 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2015 thomasv@gitorious, kyuupichan@gmail
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from electrum import WalletStorage
from electrum.plugins import run_hook
from util import PrintError
from wallet import Wallet
from i18n import _
MSG_GENERATING_WAIT = _("Electrum is generating your addresses, please wait...")
MSG_ENTER_ANYTHING = _("Please enter a seed phrase, a master key, a list of "
"Bitcoin addresses, or a list of private keys")
MSG_ENTER_SEED_OR_MPK = _("Please enter a seed phrase or a master key (xpub or xprv):")
MSG_VERIFY_SEED = _("Your seed is important!\nTo make sure that you have properly saved your seed, please retype it here.")
MSG_COSIGNER = _("Please enter the master public key of cosigner #%d:")
MSG_SHOW_MPK = _("Here is your master public key:")
MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys. "
"Enter nothing if you want to disable encryption.")
MSG_RESTORE_PASSPHRASE = \
_("Please enter the passphrase you used when creating your %s wallet. "
"Note this is NOT a password. Enter nothing if you did not use "
"one or are unsure.")
class UserCancelled(Exception):
pass
class WizardBase(PrintError):
'''Base class for gui-specific install wizards.'''
user_actions = ('create', 'restore')
wallet_kinds = ('standard', 'hardware', 'multisig', 'twofactor')
# Derived classes must set:
# self.language_for_seed
# self.plugins
def show_error(self, msg):
raise NotImplementedError
def show_warning(self, msg):
raise NotImplementedError
def remove_from_recently_open(self, filename):
"""Remove filename from the recently used list."""
raise NotImplementedError
def query_create_or_restore(self):
"""Returns a tuple (action, kind). Action is one of user_actions,
kind is one of wallet_kinds."""
raise NotImplementedError
def query_multisig(self, action):
"""Asks the user what kind of multisig wallet they want. Returns a
string like "2of3". Action is 'create' or 'restore'."""
raise NotImplementedError
def query_hardware(self, choices, action):
"""Asks the user what kind of hardware wallet they want from the given
choices. choices is a list of (wallet_type, translated
description) tuples. Action is 'create' or 'restore'. Return
the wallet type chosen."""
raise NotImplementedError
def show_and_verify_seed(self, seed):
"""Show the user their seed. Ask them to re-enter it. Return
True on success."""
raise NotImplementedError
def request_passphrase(self, device_text, restore=True):
"""Request a passphrase for a wallet from the given device and
confirm it. restore is True if restoring a wallet. Should return
a unicode string."""
raise NotImplementedError
def request_password(self, msg=None):
"""Request the user enter a new password and confirm it. Return
the password or None for no password."""
raise NotImplementedError
def request_seed(self, msg, is_valid=None):
"""Request the user enter a seed. Returns the seed the user entered.
is_valid is a function that returns True if a seed is valid, for
dynamic feedback. If not provided, Wallet.is_any is used."""
raise NotImplementedError
def request_many(self, n, xpub_hot=None):
"""If xpub_hot is provided, a new wallet is being created. Request N
master public keys for cosigners; xpub_hot is the master xpub
key for the wallet.
If xpub_hot is None, request N cosigning master xpub keys,
xprv keys, or seeds in order to perform wallet restore."""
raise NotImplementedError
def choose_server(self, network):
"""Choose a server if one is not set in the config anyway."""
raise NotImplementedError
def open_existing_wallet(self, storage, network):
wallet = Wallet(storage)
self.update_wallet_format(wallet)
self.run_wallet_actions(wallet)
wallet.start_threads(network)
return wallet, None
def create_new_wallet(self, storage, network):
action, wallet = self.create_or_restore(storage)
self.run_wallet_actions(wallet)
if network:
self.choose_server(network)
else:
self.show_warning(_('You are offline'))
def task():
# Synchronize before starting the threads
wallet.synchronize()
wallet.start_threads(network)
# FIXME
# if action == 'create':
# msg = _('Wallet addresses generated.')
# else:
# wallet.wait_until_synchronized()
# if network:
# if wallet.is_found():
# msg = _("Recovery successful")
# else:
# msg = _("No transactions found for this seed")
# else:
# msg = _("This wallet was restored offline. It may "
# "contain more addresses than displayed.")
# self.show_message(msg)
return wallet, (MSG_GENERATING_WAIT, task)
def open_wallet(self, network, filename):
'''The main entry point of the wizard. Open a wallet from the given
filename. If the file doesn't exist launch the GUI-specific
install wizard proper.'''
storage = WalletStorage(filename)
if storage.file_exists:
return self.open_existing_wallet(storage, network)
else:
return self.create_new_wallet(storage, network)
def run_wallet_actions(self, wallet):
if not wallet:
return
action = orig_action = wallet.get_action()
while action:
self.run_wallet_action(wallet, action)
action = wallet.get_action()
# Save the wallet after successful completion of actions.
# It will be saved again once synchronized.
if orig_action:
wallet.storage.write()
def run_wallet_action(self, wallet, action):
self.print_error("action %s on %s" % (action, wallet.basename()))
# Run the action on the wallet plugin, if any, then the
# wallet and finally ourselves
calls = [(wallet.plugin, (wallet, self)),
(wallet, (wallet, )),
(self, (wallet, ))]
calls = [(getattr(actor, action), args) for (actor, args) in calls
if hasattr(actor, action)]
if not calls:
raise RuntimeError("No handler found for %s action" % action)
for method, args in calls:
method(*args)
def create_or_restore(self, storage):
'''After querying the user what they wish to do, create or restore
a wallet and return it.'''
self.remove_from_recently_open(storage.path)
action, kind = self.query_create_or_restore()
assert action in self.user_actions
assert kind in self.wallet_kinds
if kind == 'multisig':
wallet_type = self.query_multisig(action)
elif kind == 'hardware':
choices = self.plugins.hardware_wallets(action)
wallet_type = self.query_hardware(choices, action)
elif kind == 'twofactor':
wallet_type = '2fa'
else:
wallet_type = 'standard'
if action == 'create':
wallet = self.create_wallet(storage, wallet_type, kind)
else:
wallet = self.restore_wallet(storage, wallet_type, kind)
return action, wallet
def construct_wallet(self, storage, wallet_type):
storage.put('wallet_type', wallet_type)
return Wallet(storage)
def create_wallet(self, storage, wallet_type, kind):
wallet = self.construct_wallet(storage, wallet_type)
if kind == 'hardware':
wallet.plugin.on_create_wallet(wallet, self)
return wallet
def restore_wallet(self, storage, wallet_type, kind):
if wallet_type == 'standard':
return self.restore_standard_wallet(storage)
# Multisig?
if kind == 'multisig':
return self.restore_multisig_wallet(storage, wallet_type)
# Plugin (two-factor or hardware)
wallet = self.construct_wallet(storage, wallet_type)
return wallet.plugin.on_restore_wallet(wallet, self)
def restore_standard_wallet(self, storage):
text = self.request_seed(MSG_ENTER_ANYTHING)
need_password = Wallet.should_encrypt(text)
password = self.request_password() if need_password else None
return Wallet.from_text(text, password, storage)
def restore_multisig_wallet(self, storage, wallet_type):
# FIXME: better handling of duplicate keys
m, n = Wallet.multisig_type(wallet_type)
key_list = self.request_many(n - 1)
need_password = any(Wallet.should_encrypt(text) for text in key_list)
password = self.request_password() if need_password else None
return Wallet.from_multisig(key_list, password, storage, wallet_type)
def create_seed(self, wallet):
'''The create_seed action creates a seed and then generates
wallet account(s).'''
seed = wallet.make_seed(self.language_for_seed)
self.show_and_verify_seed(seed)
password = self.request_password()
wallet.add_seed(seed, password)
wallet.create_master_keys(password)
wallet.create_main_account(password)
def add_cosigners(self, wallet):
# FIXME: better handling of duplicate keys
m, n = Wallet.multisig_type(wallet.wallet_type)
xpub1 = wallet.master_public_keys.get("x1/")
xpubs = self.request_many(n - 1, xpub1)
for i, xpub in enumerate(xpubs):
wallet.add_master_public_key("x%d/" % (i + 2), xpub)
def update_wallet_format(self, wallet):
# Backwards compatibility: convert old-format imported keys
if wallet.imported_keys:
msg = _("Please enter your password in order to update "
"imported keys")
if wallet.use_encryption:
password = self.request_password(msg)
else:
password = None
try:
wallet.convert_imported_keys(password)
except Exception as e:
self.show_error(str(e))
# Call synchronize to regenerate addresses in case we're offline
if wallet.get_master_public_keys() and not wallet.addresses():
wallet.synchronize()