Browse Source
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.283
Neil Booth
9 years ago
20 changed files with 750 additions and 647 deletions
@ -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() |
@ -1,6 +1,9 @@ |
|||
from plugins.trezor.qt_generic import QtPlugin |
|||
from keepkeylib.qt.pinmatrix import PinMatrixWidget |
|||
|
|||
|
|||
class Plugin(QtPlugin): |
|||
pin_matrix_widget_class = PinMatrixWidget |
|||
icon_file = ":icons/keepkey.png" |
|||
|
|||
def pin_matrix_widget_class(): |
|||
from keepkeylib.qt.pinmatrix import PinMatrixWidget |
|||
return PinMatrixWidget |
|||
|
@ -1,7 +1,10 @@ |
|||
from plugins.trezor.qt_generic import QtPlugin |
|||
from trezorlib.qt.pinmatrix import PinMatrixWidget |
|||
|
|||
|
|||
class Plugin(QtPlugin): |
|||
pin_matrix_widget_class = PinMatrixWidget |
|||
icon_file = ":icons/trezor.png" |
|||
|
|||
@staticmethod |
|||
def pin_matrix_widget_class(): |
|||
from trezorlib.qt.pinmatrix import PinMatrixWidget |
|||
return PinMatrixWidget |
|||
|
Loading…
Reference in new issue