electrum

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

commit e7d25faf028543760f7ae114c395a4400c507636
parent 97dc130e26b36af84f5fb54b468d867423243c97
Author: ThomasV <thomasv@electrum.org>
Date:   Mon, 20 Jun 2016 16:25:11 +0200

Finish wizard unification

Diffstat:
Mgui/kivy/__init__.py | 1+
Mgui/kivy/main_window.py | 41+++++++++++++++++++----------------------
Mgui/kivy/uix/dialogs/installwizard.py | 97++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mgui/qt/__init__.py | 15++++++++-------
Mgui/qt/installwizard.py | 276++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mlib/base_wizard.py | 236++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mlib/daemon.py | 36+++++++++++++++++++-----------------
Dlib/wizard.py | 328-------------------------------------------------------------------------------
Mplugins/hw_wallet/plugin.py | 18++++++++++++------
Mplugins/trustedcoin/qt.py | 11+++--------
Mplugins/trustedcoin/trustedcoin.py | 39+++++++++++++++++++++++++++------------
11 files changed, 450 insertions(+), 648 deletions(-)

diff --git a/gui/kivy/__init__.py b/gui/kivy/__init__.py @@ -45,6 +45,7 @@ class ElectrumGui: def __init__(self, config, daemon, plugins): Logger.debug('ElectrumGUI: initialising') + self.daemon = daemon self.network = daemon.network self.config = config self.plugins = plugins diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py @@ -199,8 +199,8 @@ class ElectrumWindow(App): self.plugins = kwargs.get('plugins', []) self.gui_object = kwargs.get('gui_object', None) + self.daemon = self.gui_object.daemon - #self.config = self.gui_object.config self.contacts = Contacts(self.electrum_config) self.invoices = InvoiceStore(self.electrum_config) @@ -408,36 +408,32 @@ class ElectrumWindow(App): else: return '' - def load_wallet_by_name(self, wallet_path): - if not wallet_path: - return - config = self.electrum_config - try: - storage = WalletStorage(wallet_path) - except IOError: - self.show_error("Cannot read wallet file") + def on_wizard_complete(self, instance, wallet): + if wallet: + self.daemon.add_wallet(wallet) + self.load_wallet(wallet) + self.on_resume() + + def load_wallet_by_name(self, path): + if not path: return - if storage.file_exists: - wallet = Wallet(storage) - action = wallet.get_action() + wallet = self.daemon.load_wallet(path) + if wallet: + self.load_wallet(wallet) + self.on_resume() else: - action = 'new' - if action is not None: - # start installation wizard Logger.debug('Electrum: Wallet not found. Launching install wizard') - wizard = Factory.InstallWizard(config, self.network, storage) - wizard.bind(on_wizard_complete=lambda instance, wallet: self.load_wallet(wallet)) + wizard = Factory.InstallWizard(self.electrum_config, self.network, path) + wizard.bind(on_wizard_complete=self.on_wizard_complete) + action = wizard.get_action() wizard.run(action) - else: - self.load_wallet(wallet) - self.on_resume() def on_stop(self): self.stop_wallet() def stop_wallet(self): if self.wallet: - self.wallet.stop_threads() + self.daemon.stop_wallet(self.wallet.storage.path) self.wallet = None def on_key_down(self, instance, key, keycode, codepoint, modifiers): @@ -539,9 +535,10 @@ class ElectrumWindow(App): @profiler def load_wallet(self, wallet): + print "load wallet", wallet.storage.path + self.stop_wallet() self.wallet = wallet - self.wallet.start_threads(self.network) self.current_account = self.wallet.storage.get('current_account', None) self.update_wallet() # Once GUI has been initialized check if we want to announce something diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py @@ -24,7 +24,9 @@ from password_dialog import PasswordDialog from electrum.base_wizard import BaseWizard - +is_test = True +test_seed = "time taxi field recycle tiny license olive virus report rare steel portion achieve" +test_xpub = "xpub661MyMwAqRbcEbvVtRRSjqxVnaWVUMewVzMiURAKyYratih4TtBpMypzzefmv8zUNebmNVzB3PojdC5sV2P9bDgMoo9B3SARw1MXUUfU1GL" Builder.load_string(''' #:import Window kivy.core.window.Window @@ -152,7 +154,7 @@ Builder.load_string(''' <WizardChoiceDialog> - msg : '' + message : '' Widget: size_hint: 1, 1 Label: @@ -160,7 +162,7 @@ Builder.load_string(''' size_hint: 1, None text_size: self.width, None height: self.texture_size[1] - text: root.msg + text: root.message Widget size_hint: 1, 1 GridLayout: @@ -408,11 +410,12 @@ class WizardDialog(EventsDialog): ''' crcontent = ObjectProperty(None) - def __init__(self, **kwargs): + def __init__(self, wizard, **kwargs): super(WizardDialog, self).__init__(**kwargs) - #self.action = kwargs.get('action') + self.wizard = wizard + self.ids.back.disabled = not wizard.can_go_back() + self.app = App.get_running_app() self.run_next = kwargs['run_next'] - self.run_prev = kwargs['run_prev'] _trigger_size_dialog = Clock.create_trigger(self._size_dialog) Window.bind(size=_trigger_size_dialog, rotation=_trigger_size_dialog) @@ -443,7 +446,7 @@ class WizardDialog(EventsDialog): app.stop() def get_params(self, button): - return () + return (None,) def on_release(self, button): self._on_release = True @@ -452,7 +455,7 @@ class WizardDialog(EventsDialog): self.parent.dispatch('on_wizard_complete', None) return if button is self.ids.back: - self.run_prev() + self.wizard.go_back() return params = self.get_params(button) self.run_next(*params) @@ -467,13 +470,13 @@ class WizardMultisigDialog(WizardDialog): class WizardChoiceDialog(WizardDialog): - def __init__(self, **kwargs): - super(WizardChoiceDialog, self).__init__(**kwargs) - self.msg = kwargs.get('msg', '') + def __init__(self, wizard, **kwargs): + super(WizardChoiceDialog, self).__init__(wizard, **kwargs) + self.message = kwargs.get('message', '') choices = kwargs.get('choices', []) layout = self.ids.choices layout.bind(minimum_height=layout.setter('height')) - for text, action in choices: + for action, text in choices: l = WizardButton(text=text) l.action = action l.height = '48dp' @@ -508,17 +511,18 @@ class WordButton(Button): class WizardButton(Button): pass + class RestoreSeedDialog(WizardDialog): message = StringProperty('') - def __init__(self, **kwargs): - super(RestoreSeedDialog, self).__init__(**kwargs) - self._test = kwargs['test'] + def __init__(self, wizard, **kwargs): + super(RestoreSeedDialog, self).__init__(wizard, **kwargs) + self._test = kwargs['is_valid'] from electrum.mnemonic import Mnemonic from electrum.old_mnemonic import words as old_wordlist self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist)) - self.ids.text_input_seed.text = '' + self.ids.text_input_seed.text = test_seed if is_test else '' def get_suggestions(self, prefix): for w in self.words: @@ -616,9 +620,8 @@ class RestoreSeedDialog(WizardDialog): class ShowXpubDialog(WizardDialog): - def __init__(self, **kwargs): - WizardDialog.__init__(self, **kwargs) - self.app = App.get_running_app() + def __init__(self, wizard, **kwargs): + WizardDialog.__init__(self, wizard, **kwargs) self.xpub = kwargs['xpub'] self.ids.next.disabled = False @@ -636,15 +639,14 @@ class ShowXpubDialog(WizardDialog): class AddXpubDialog(WizardDialog): - def __init__(self, **kwargs): - WizardDialog.__init__(self, **kwargs) - self.app = App.get_running_app() - self._test = kwargs['test'] + def __init__(self, wizard, **kwargs): + WizardDialog.__init__(self, wizard, **kwargs) + self.is_valid = kwargs['is_valid'] self.title = kwargs['title'] self.message = kwargs['message'] def check_text(self, dt): - self.ids.next.disabled = not bool(self._test(self.get_text())) + self.ids.next.disabled = not bool(self.is_valid(self.get_text())) def get_text(self): ti = self.ids.text_input @@ -659,7 +661,7 @@ class AddXpubDialog(WizardDialog): self.app.scan_qr(on_complete) def do_paste(self): - self.ids.text_input.text = unicode(self.app._clipboard.paste()) + self.ids.text_input.text = test_xpub if is_test else unicode(self.app._clipboard.paste()) def do_clear(self): self.ids.text_input.text = '' @@ -681,7 +683,7 @@ class InstallWizard(BaseWizard, Widget): """overriden by main_window""" pass - def waiting_dialog(self, task, msg, on_complete=None): + def waiting_dialog(self, task, msg): '''Perform a blocking task in the background by running the passed method in a thread. ''' @@ -693,8 +695,6 @@ class InstallWizard(BaseWizard, Widget): Clock.schedule_once(lambda dt: app.show_error(str(err))) # on completion hide message Clock.schedule_once(lambda dt: app.info_bubble.hide(now=True), -1) - if on_complete: - on_complete() app.show_info_bubble( text=msg, icon='atlas://gui/kivy/theming/light/important', @@ -702,17 +702,42 @@ class InstallWizard(BaseWizard, Widget): t = threading.Thread(target = target) t.start() - def choice_dialog(self, **kwargs): WizardChoiceDialog(**kwargs).open() - def multisig_dialog(self, **kwargs): WizardMultisigDialog(**kwargs).open() - def show_seed_dialog(self, **kwargs): ShowSeedDialog(**kwargs).open() - def restore_seed_dialog(self, **kwargs): RestoreSeedDialog(**kwargs).open() - def add_xpub_dialog(self, **kwargs): AddXpubDialog(**kwargs).open() - def show_xpub_dialog(self, **kwargs): ShowXpubDialog(**kwargs).open() + def terminate(self, **kwargs): + self.wallet.start_threads(self.network) + self.dispatch('on_wizard_complete', self.wallet) + + def choice_dialog(self, **kwargs): WizardChoiceDialog(self, **kwargs).open() + def multisig_dialog(self, **kwargs): WizardMultisigDialog(self, **kwargs).open() + def show_seed_dialog(self, **kwargs): ShowSeedDialog(self, **kwargs).open() + def enter_seed_dialog(self, **kwargs): RestoreSeedDialog(self, **kwargs).open() + def add_xpub_dialog(self, **kwargs): AddXpubDialog(self, **kwargs).open() + def show_xpub_dialog(self, **kwargs): ShowXpubDialog(self, **kwargs).open() + + def show_error(self, msg): + app.show_error(msg, duration=0.5) def password_dialog(self, message, callback): popup = PasswordDialog() popup.init(message, callback) popup.open() - def show_error(self, msg): - app.show_error(msg, duration=0.5) + def request_password(self, run_next): + def callback(pin): + if pin: + self.run('confirm_password', (pin, run_next)) + else: + run_next(None) + self.password_dialog('Choose a PIN code', callback) + + def confirm_password(self, pin, run_next): + def callback(conf): + if conf == pin: + run_next(pin) + else: + self.show_error(_('PIN mismatch')) + self.run('request_password', (run_next,)) + self.password_dialog('Confirm your PIN code', callback) + + def action_dialog(self, action, run_next): + f = getattr(self, action) + f() diff --git a/gui/qt/__init__.py b/gui/qt/__init__.py @@ -149,9 +149,6 @@ class ElectrumGui: run_hook('on_new_window', w) return w - def get_wizard(self): - return InstallWizard(self.config, self.app, self.plugins) - def start_new_window(self, path, uri): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it.''' @@ -160,14 +157,18 @@ class ElectrumGui: w.bring_to_top() break else: - wallet = self.daemon.load_wallet(path, self.get_wizard) + wallet = self.daemon.load_wallet(path) if not wallet: - return + wizard = InstallWizard(self.config, self.app, self.plugins, self.daemon.network, path) + wallet = wizard.run_and_get_wallet() + if not wallet: + return + if wallet.get_action(): + return + self.daemon.add_wallet(wallet) w = self.create_window_for_wallet(wallet) - if uri: w.pay_to_URI(uri) - return w def close_window(self, window): diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py @@ -5,6 +5,10 @@ from PyQt4.QtCore import * import PyQt4.QtCore as QtCore import electrum +from electrum.wallet import Wallet +from electrum.mnemonic import prepare_seed +from electrum.util import UserCancelled +from electrum.base_wizard import BaseWizard from electrum.i18n import _ from seed_dialog import SeedDisplayLayout, SeedWarningLayout, SeedInputLayout @@ -12,14 +16,23 @@ from network_dialog import NetworkChoiceLayout from util import * from password_dialog import PasswordLayout, PW_NEW, PW_PASSPHRASE -from electrum.wallet import Wallet -from electrum.mnemonic import prepare_seed -from electrum.util import UserCancelled -from electrum.wizard import (WizardBase, - MSG_ENTER_PASSWORD, MSG_RESTORE_PASSPHRASE, - MSG_COSIGNER, MSG_ENTER_SEED_OR_MPK, - MSG_SHOW_MPK, MSG_VERIFY_SEED, - MSG_GENERATING_WAIT) + +class GoBack(Exception): + pass + +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.") def clean_text(seed_e): text = unicode(seed_e.toPlainText()).strip() @@ -63,14 +76,42 @@ class CosignWidget(QWidget): qp.end() + +def wizard_dialog(func): + def func_wrapper(*args, **kwargs): + run_next = kwargs['run_next'] + wizard = args[0] + wizard.back_button.setText(_('Back') if wizard.can_go_back() else _('Cancel')) + try: + out = func(*args, **kwargs) + except GoBack: + print "go back" + wizard.go_back() + return + except UserCancelled: + print "usercancelled" + return + #if out is None: + # out = () + if type(out) is not tuple: + out = (out,) + apply(run_next, out) + return func_wrapper + + + # WindowModalDialog must come first as it overrides show_error -class InstallWizard(QDialog, MessageBoxMixin, WizardBase): +class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): + + def __init__(self, config, app, plugins, network, storage): - def __init__(self, config, app, plugins): + BaseWizard.__init__(self, config, network, storage) QDialog.__init__(self, None) + self.setWindowTitle('Electrum - ' + _('Install Wizard')) self.app = app self.config = config + # Set for base base class self.plugins = plugins self.language_for_seed = config.get('language') @@ -79,7 +120,7 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): self.connect(self, QtCore.SIGNAL('accept'), self.accept) self.title = WWLabel() self.main_widget = QWidget() - self.cancel_button = QPushButton(_("Cancel"), self) + self.back_button = QPushButton(_("Back"), self) self.next_button = QPushButton(_("Next"), self) self.next_button.setDefault(True) self.logo = QLabel() @@ -87,9 +128,9 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): self.please_wait.setAlignment(Qt.AlignCenter) self.icon_filename = None self.loop = QEventLoop() - self.rejected.connect(lambda: self.loop.exit(False)) - self.cancel_button.clicked.connect(lambda: self.loop.exit(False)) - self.next_button.clicked.connect(lambda: self.loop.exit(True)) + self.rejected.connect(lambda: self.loop.exit(0)) + self.back_button.clicked.connect(lambda: self.loop.exit(1)) + self.next_button.clicked.connect(lambda: self.loop.exit(2)) outer_vbox = QVBoxLayout(self) inner_vbox = QVBoxLayout() inner_vbox = QVBoxLayout() @@ -107,12 +148,35 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): hbox.addLayout(inner_vbox) hbox.setStretchFactor(inner_vbox, 1) outer_vbox.addLayout(hbox) - outer_vbox.addLayout(Buttons(self.cancel_button, self.next_button)) + outer_vbox.addLayout(Buttons(self.back_button, self.next_button)) self.set_icon(':icons/electrum.png') self.show() self.raise_() self.refresh_gui() # Need for QT on MacOSX. Lame. + def run_and_get_wallet(self): + # Show network dialog if config does not exist + if self.network: + if self.config.get('auto_connect') is None: + self.choose_server(self.network) + + action = self.get_action() + if action != 'new': + self.hide() + path = self.storage.path + msg = _("The file '%s' contains an incompletely created wallet.\n" + "Do you want to complete its creation now?") % path + if not self.question(msg): + if self.question(_("Do you want to delete '%s'?") % path): + import os + os.remove(path) + self.show_warning(_('The file was removed')) + return + return + self.show() + self.run(action) + return self.wallet + def finished(self): '''Ensure the dialog is closed.''' self.accept() @@ -137,15 +201,17 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): if prior_layout: QWidget().setLayout(prior_layout) self.main_widget.setLayout(layout) - self.cancel_button.setEnabled(True) + self.back_button.setEnabled(True) self.next_button.setEnabled(next_enabled) self.main_widget.setVisible(True) self.please_wait.setVisible(False) result = self.loop.exec_() if not result and raise_on_cancel: raise UserCancelled + if result == 1: + raise GoBack self.title.setVisible(False) - self.cancel_button.setEnabled(False) + self.back_button.setEnabled(False) self.next_button.setEnabled(False) self.main_widget.setVisible(False) self.please_wait.setVisible(True) @@ -157,58 +223,42 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): self.app.processEvents() self.app.processEvents() - def run(self, *args): - '''Wrap the base wizard implementation with try/except blocks - to give a sensible error message to the user.''' - wallet = None - try: - wallet = WizardBase.run(self, *args) - except UserCancelled: - self.print_error("wallet creation cancelled by user") - self.accept() # For when called from menu - except BaseException as e: - self.on_error(sys.exc_info()) - raise - return wallet - def remove_from_recently_open(self, filename): self.config.remove_from_recently_open(filename) - def request_seed(self, title, is_valid=None): - is_valid = is_valid or Wallet.is_any - slayout = SeedInputLayout() + def text_input(self, title, message, is_valid): + slayout = SeedInputLayout(title=message) def sanitized_seed(): return clean_text(slayout.seed_edit()) def set_enabled(): self.next_button.setEnabled(is_valid(sanitized_seed())) slayout.seed_edit().textChanged.connect(set_enabled) self.set_main_layout(slayout.layout(), title, next_enabled=False) - return sanitized_seed() + seed = sanitized_seed() + return seed - def show_seed(self, seed): - slayout = SeedWarningLayout(seed) - self.set_main_layout(slayout.layout()) - - def verify_seed(self, seed, is_valid=None): - while True: - r = self.request_seed(MSG_VERIFY_SEED, is_valid) - if prepare_seed(r) == prepare_seed(seed): - return - self.show_error(_('Incorrect seed')) + @wizard_dialog + def add_xpub_dialog(self, title, message, is_valid, run_next): + return self.text_input(title, message, is_valid) - def show_and_verify_seed(self, seed, is_valid=None): - """Show the user their seed. Ask them to re-enter it. Return - True on success.""" - self.show_seed(seed) + @wizard_dialog + def enter_seed_dialog(self, run_next, title, message, is_valid): self.app.clipboard().clear() - self.verify_seed(seed, is_valid) + return self.text_input(title, message, is_valid) + + @wizard_dialog + def show_seed_dialog(self, run_next, message, seed_text): + slayout = SeedWarningLayout(seed_text) + self.set_main_layout(slayout.layout()) + return seed_text def pw_layout(self, msg, kind): playout = PasswordLayout(None, msg, kind, self.next_button) self.set_main_layout(playout.layout()) return playout.new_password() - def request_passphrase(self, device_text): + @wizard_dialog + def request_passphrase(self, device_text, run_next): """When restoring a wallet, request the passphrase that was used for the wallet on the given device and confirm it. Should return a unicode string.""" @@ -218,10 +268,11 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): raise UserCancelled return phrase - def request_password(self, msg=None): + @wizard_dialog + def request_password(self, run_next): """Request the user enter a new password and confirm it. Return the password or None for no password.""" - return self.pw_layout(msg or MSG_ENTER_PASSWORD, PW_NEW) + return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW) def show_restore(self, wallet, network): # FIXME: these messages are shown after the install wizard is @@ -244,85 +295,43 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): "contain more addresses than displayed.") self.show_message(msg) - def create_addresses(self, wallet): - def task(): - wallet.synchronize() - self.emit(QtCore.SIGNAL('accept')) - t = threading.Thread(target = task) - t.start() - self.please_wait.setText(MSG_GENERATING_WAIT) - self.refresh_gui() - - def query_create_or_restore(self, wallet_kinds): - """Ask the user what they want to do, and which wallet kind. - wallet_kinds is an array of translated wallet descriptions. - Return a a tuple (action, kind_index). Action is 'create' or - 'restore', and kind the index of the wallet kind chosen.""" + def confirm(self, msg): + vbox = QVBoxLayout() + vbox.addWidget(WWLabel(msg)) + self.set_main_layout(vbox) - actions = [_("Create a new wallet"), - _("Restore a wallet from seed words or from keys")] - title = _("Electrum could not find an existing wallet.") - actions_clayout = ChoicesLayout(_("What do you want to do?"), actions) - wallet_clayout = ChoicesLayout(_("Wallet kind:"), wallet_kinds) + @wizard_dialog + def action_dialog(self, action, run_next): + self.run(action) - vbox = QVBoxLayout() - vbox.addLayout(actions_clayout.layout()) - vbox.addLayout(wallet_clayout.layout()) - self.set_main_layout(vbox, title) + def terminate(self): + self.wallet.start_threads(self.network) + self.emit(QtCore.SIGNAL('accept')) - action = ['create', 'restore'][actions_clayout.selected_index()] - return action, wallet_clayout.selected_index() + def waiting_dialog(self, task, msg): + self.please_wait.setText(MSG_GENERATING_WAIT) + self.refresh_gui() + t = threading.Thread(target = task) + t.start() - def query_hw_wallet_choice(self, msg, choices): + @wizard_dialog + def choice_dialog(self, title, message, choices, run_next): + c_values = map(lambda x: x[0], choices) + c_titles = map(lambda x: x[1], choices) + clayout = ChoicesLayout(message, c_titles) vbox = QVBoxLayout() - if choices: - wallet_clayout = ChoicesLayout(msg, choices) - vbox.addLayout(wallet_clayout.layout()) - else: - vbox.addWidget(QLabel(msg, wordWrap=True)) - self.set_main_layout(vbox, next_enabled=len(choices) != 0) - return wallet_clayout.selected_index() if choices else 0 + vbox.addLayout(clayout.layout()) + self.set_main_layout(vbox, title) + action = c_values[clayout.selected_index()] + return action - def request_many(self, n, xpub_hot=None): + @wizard_dialog + def show_xpub_dialog(self, xpub, run_next): vbox = QVBoxLayout() - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setFrameShape(QFrame.NoFrame) - vbox.addWidget(scroll) - - w = QWidget() - innerVbox = QVBoxLayout(w) - scroll.setWidget(w) - - entries = [] - - if xpub_hot: - layout = SeedDisplayLayout(xpub_hot, title=MSG_SHOW_MPK, sid='hot') - else: - layout = SeedInputLayout(title=MSG_ENTER_SEED_OR_MPK, sid='hot') - entries.append(layout.seed_edit()) - innerVbox.addLayout(layout.layout()) - - for i in range(n): - msg = MSG_COSIGNER % (i + 1) if xpub_hot else MSG_ENTER_SEED_OR_MPK - layout = SeedInputLayout(title=msg, sid='cold') - innerVbox.addLayout(layout.layout()) - entries.append(layout.seed_edit()) - - def get_texts(): - return [clean_text(entry) for entry in entries] - def set_enabled(): - texts = get_texts() - is_valid = Wallet.is_xpub if xpub_hot else Wallet.is_any - all_valid = all(is_valid(text) for text in texts) - if xpub_hot: - texts.append(xpub_hot) - has_dups = len(set(texts)) < len(texts) - self.next_button.setEnabled(all_valid and not has_dups) - for e in entries: - e.textChanged.connect(set_enabled) - self.set_main_layout(vbox, next_enabled=False) - return get_texts() + layout = SeedDisplayLayout(xpub, title=MSG_SHOW_MPK, sid='hot') + vbox.addLayout(layout.layout()) + self.set_main_layout(vbox, MSG_SHOW_MPK) + return None def choose_server(self, network): title = _("Electrum communicates with remote servers to get " @@ -335,7 +344,6 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): choices_title = _("How do you want to connect to a server? ") clayout = ChoicesLayout(choices_title, choices) self.set_main_layout(clayout.layout(), title) - auto_connect = True if clayout.selected_index() == 1: nlayout = NetworkChoiceLayout(network, self.config, wizard=True) @@ -345,12 +353,8 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): self.config.set_key('auto_connect', auto_connect, True) network.auto_connect = auto_connect - def query_choice(self, msg, choices): - clayout = ChoicesLayout(msg, choices) - self.set_main_layout(clayout.layout(), next_enabled=bool(choices)) - return clayout.selected_index() - - def query_multisig(self, action): + @wizard_dialog + def multisig_dialog(self, run_next): cw = CosignWidget(2, 2) m_edit = QSlider(Qt.Horizontal, self) n_edit = QSlider(Qt.Horizontal, self) @@ -360,7 +364,6 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): m_edit.setMaximum(2) n_edit.setValue(2) m_edit.setValue(2) - n_label = QLabel() m_label = QLabel() grid = QGridLayout() @@ -379,14 +382,11 @@ class InstallWizard(QDialog, MessageBoxMixin, WizardBase): m_edit.valueChanged.connect(on_m) on_n(2) on_m(2) - vbox = QVBoxLayout() vbox.addWidget(cw) - vbox.addWidget(WWLabel(_("Choose the number of signatures needed " - "to unlock funds in your wallet:"))) + vbox.addWidget(WWLabel(_("Choose the number of signatures needed to unlock funds in your wallet:"))) vbox.addLayout(grid) self.set_main_layout(vbox, _("Multi-Signature Wallet")) m = int(m_edit.value()) n = int(n_edit.value()) - wallet_type = '%dof%d'%(m,n) - return wallet_type + return (m, n) diff --git a/lib/base_wizard.py b/lib/base_wizard.py @@ -24,144 +24,232 @@ # SOFTWARE. import os -from electrum.wallet import Wallet, Multisig_Wallet +from electrum.wallet import Wallet, Multisig_Wallet, WalletStorage from i18n import _ class BaseWizard(object): - def __init__(self, config, network, storage): + def __init__(self, config, network, path): super(BaseWizard, self).__init__() self.config = config self.network = network - self.storage = storage + self.storage = WalletStorage(path) self.wallet = None + self.stack = [] def run(self, action, *args): - '''Entry point of our Installation wizard''' + self.stack.append((action, args)) if not action: return - if hasattr(self, action): + if hasattr(self.wallet, 'plugin'): + if hasattr(self.wallet.plugin, action): + f = getattr(self.wallet.plugin, action) + apply(f, (self.wallet, self) + args) + elif hasattr(self, action): f = getattr(self, action) apply(f, *args) else: raise BaseException("unknown action", action) + def get_action(self): + if self.storage.file_exists: + self.wallet = Wallet(self.storage) + action = self.wallet.get_action() + else: + action = 'new' + return action + + def get_wallet(self): + if self.wallet and self.wallet.get_action() is None: + return self.wallet + + def can_go_back(self): + return len(self.stack)>1 + + def go_back(self): + if not self.can_go_back(): + return + self.stack.pop() + action, args = self.stack.pop() + self.run(action, *args) + + def run_wallet(self): + self.stack = [] + action = self.wallet.get_action() + if action: + self.action_dialog(action=action, run_next=lambda x: self.run_wallet()) + def new(self): name = os.path.basename(self.storage.path) - msg = "\n".join([ - _("Welcome to the Electrum installation wizard."), + title = _("Welcome to the Electrum installation wizard.") + message = '\n'.join([ _("The wallet '%s' does not exist.") % name, _("What kind of wallet do you want to create?") ]) - choices = [ - (_('Standard wallet'), 'create_standard'), - (_('Multi-signature wallet'), 'create_multisig'), + wallet_kinds = [ + ('standard', _("Standard wallet")), + ('twofactor', _("Wallet with two-factor authentication")), + ('multisig', _("Multi-signature wallet")), + ('hardware', _("Hardware wallet")), ] - self.choice_dialog(msg=msg, choices=choices, run_prev=self.cancel, run_next=self.run) + registered_kinds = Wallet.categories() + choices = [pair for pair in wallet_kinds if pair[0] in registered_kinds] + self.choice_dialog(title = title, message=message, choices=choices, run_next=self.on_wallet_type) + + def on_wallet_type(self, choice): + self.wallet_type = choice + if choice == 'standard': + action = 'choose_seed' + elif choice == 'multisig': + action = 'choose_multisig' + elif choice == 'hardware': + action = 'choose_hw' + elif choice == 'twofactor': + action = 'choose_seed' + self.run(action) + + def choose_multisig(self): + def on_multisig(m, n): + self.multisig_type = "%dof%d"%(m, n) + self.run('choose_seed') + self.multisig_dialog(run_next=on_multisig) def choose_seed(self): - msg = ' '.join([ - _("Do you want to create a new seed, or to restore a wallet using an existing seed?") - ]) - choices = [ - (_('Create a new seed'), 'create_seed'), - (_('I already have a seed'), 'restore_seed'), - (_('Watching-only wallet'), 'restore_xpub') - ] - self.choice_dialog(msg=msg, choices=choices, run_prev=self.new, run_next=self.run) - - def create_multisig(self): - def f(m, n): - self.wallet_type = "%dof%d"%(m, n) - self.run('choose_seed') - name = os.path.basename(self.storage.path) - self.multisig_dialog(run_prev=self.new, run_next=f) + title = _('Private Keys') + message = _("Do you want to create a new seed, or to restore a wallet using an existing seed?") + if self.wallet_type == 'standard': + choices = [ + ('create_seed', _('Create a new seed')), + ('restore_seed', _('I already have a seed')), + ('restore_xpub', _('Watching-only wallet')), + ] + elif self.wallet_type == 'twofactor': + choices = [ + ('create_2fa', _('Create a new seed')), + ('restore_2fa', _('I already have a seed')), + ] + elif self.wallet_type == 'multisig': + choices = [ + ('create_seed', _('Create a new seed')), + ('restore_seed', _('I already have a seed')), + ('restore_xpub', _('Watching-only wallet')), + ('choose_hw', _('Cosign with hardware wallet')), + ] + self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) + + def create_2fa(self): + print 'create 2fa' + self.storage.put('wallet_type', '2fa') + self.wallet = Wallet(self.storage) + self.run_wallet() def restore_seed(self): msg = _('Please type your seed phrase using the virtual keyboard.') - self.restore_seed_dialog(run_prev=self.new, run_next=self.enter_pin, test=Wallet.is_seed, message=msg) + title = _('Enter Seed') + self.enter_seed_dialog(run_next=self.add_password, title=title, message=msg, is_valid=Wallet.is_seed) def restore_xpub(self): title = "MASTER PUBLIC KEY" - message = _('To create a watching-only wallet, paste your master public key, or scan it using the camera button.') - self.add_xpub_dialog(run_prev=self.new, run_next=lambda xpub: self.create_wallet(xpub, None), title=title, message=message, test=Wallet.is_mpk) - - def create_standard(self): - self.wallet_type = 'standard' - self.run('choose_seed') + message = _('To create a watching-only wallet, paste your master public key, or scan it using the camera button.') + self.add_xpub_dialog(run_next=lambda xpub: self.create_wallet(xpub, None), title=title, message=message, is_valid=Wallet.is_mpk) + + def restore_2fa(self): + self.storage.put('wallet_type', '2fa') + self.wallet = Wallet(self.storage) + self.wallet.plugin.on_restore_wallet(self.wallet, self) + + def choose_hw(self): + hw_wallet_types, choices = self.plugins.hardware_wallets('create') + choices = zip(hw_wallet_types, choices) + title = _('Hardware wallet') + if choices: + msg = _('Select the type of hardware wallet: ') + else: + msg = ' '.join([ + _('No hardware wallet support found on your system.'), + _('Please install the relevant libraries (eg python-trezor for Trezor).'), + ]) + self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_hardware) + + def on_hardware(self, hw_type): + self.hw_type = hw_type + if self.wallet_type == 'multisig': + self.create_hardware_multisig() + else: + title = _('Hardware wallet') + ' [%s]' % hw_type + message = _('Do you have a device, or do you want to restore a wallet using an existing seed?') + choices = [ + ('create_hardware_wallet', _('I have a device')), + ('restore_hardware_wallet', _('Use hardware wallet seed')), + ] + self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) + + def create_hardware_multisig(self): + self.storage.put('wallet_type', self.multisig_type) + self.wallet = Multisig_Wallet(self.storage) + # todo: get the xpub from the plugin + self.run('create_wallet', xpub, None) + + def create_hardware_wallet(self): + self.storage.put('wallet_type', self.hw_type) + self.wallet = Wallet(self.storage) + self.wallet.plugin.on_create_wallet(self.wallet, self) + self.terminate() + + def restore_hardware_wallet(self): + self.storage.put('wallet_type', self.wallet_type) + self.wallet = Wallet(self.storage) + self.wallet.plugin.on_restore_wallet(self.wallet, self) + self.terminate() def create_wallet(self, text, password): if self.wallet_type == 'standard': self.wallet = Wallet.from_text(text, password, self.storage) self.run('create_addresses') - else: - self.storage.put('wallet_type', self.wallet_type) + elif self.wallet_type == 'multisig': + self.storage.put('wallet_type', self.multisig_type) self.wallet = Multisig_Wallet(self.storage) self.wallet.add_seed(text, password) self.wallet.create_master_keys(password) - action = self.wallet.get_action() - self.run(action) + self.run_wallet() def add_cosigners(self): xpub = self.wallet.master_public_keys.get('x1/') - self.show_xpub_dialog(run_prev=self.create_multisig, run_next=self.add_cosigner, xpub=xpub, test=Wallet.is_xpub) + self.show_xpub_dialog(run_next=lambda x: self.add_cosigner(), xpub=xpub) def add_cosigner(self): def on_xpub(xpub): self.wallet.add_cosigner(xpub) i = self.wallet.get_missing_cosigner() - action = 'add_cosigner' if i else 'create_main_account' + action = 'add_cosigner' if i else 'create_addresses' self.run(action) - title = "ADD COSIGNER" + i = self.wallet.get_missing_cosigner() + title = _("Add Cosigner") + " %d"%(i-1) message = _('Please paste your cosigners master public key, or scan it using the camera button.') - self.add_xpub_dialog(run_prev=self.add_cosigners, run_next=on_xpub, title=title, message=message, test=Wallet.is_xpub) - - def create_main_account(self): - self.wallet.create_main_account() - self.run('create_addresses') + self.add_xpub_dialog(run_next=on_xpub, title=title, message=message, is_valid=Wallet.is_any) def create_addresses(self): def task(): self.wallet.create_main_account() self.wallet.synchronize() + self.terminate() msg= _("Electrum is generating your addresses, please wait.") - self.waiting_dialog(task, msg, on_complete=self.terminate) + self.waiting_dialog(task, msg) def create_seed(self): from electrum.wallet import BIP32_Wallet seed = BIP32_Wallet.make_seed() msg = _("If you forget your PIN or lose your device, your seed phrase will be the " "only way to recover your funds.") - self.show_seed_dialog(run_prev=self.new, run_next=self.confirm_seed, message=msg, seed_text=seed) + self.show_seed_dialog(run_next=self.confirm_seed, message=msg, seed_text=seed) def confirm_seed(self, seed): assert Wallet.is_seed(seed) + title = _('Confirm Seed') msg = _('Please retype your seed phrase, to confirm that you properly saved it') - self.restore_seed_dialog(run_prev=self.create_seed, run_next=self.enter_pin, test=lambda x: x==seed, message=msg) - - def enter_pin(self, seed): - def callback(pin): - action = 'confirm_pin' if pin else 'create_wallet' - self.run(action, (seed, pin)) - self.password_dialog('Choose a PIN code', callback) - - def confirm_pin(self, seed, pin): - def callback(conf): - if conf == pin: - self.run('create_wallet', (seed, pin)) - else: - self.show_error(_('PIN mismatch')) - self.run('enter_pin', (seed,)) - self.password_dialog('Confirm your PIN code', callback) - - def terminate(self): - self.wallet.start_threads(self.network) - self.dispatch('on_wizard_complete', self.wallet) - - def cancel(self): - self.dispatch('on_wizard_complete', None) - return True - - + self.enter_seed_dialog(run_next=self.add_password, title=title, message=msg, is_valid=lambda x: x==seed) + def add_password(self, seed): + f = lambda x: self.create_wallet(seed, x) + self.request_password(run_next=f) diff --git a/lib/daemon.py b/lib/daemon.py @@ -171,27 +171,29 @@ class Daemon(DaemonThread): response = "Error: Electrum is running in daemon mode. Please stop the daemon first." return response - def load_wallet(self, path, get_wizard=None): + def load_wallet(self, path): if path in self.wallets: wallet = self.wallets[path] - else: - storage = WalletStorage(path) - if storage.file_exists: - wallet = Wallet(storage) - action = wallet.get_action() - else: - action = 'new' - if action: - if get_wizard is None: - return None - wizard = get_wizard() - wallet = wizard.run(self.network, storage) - else: - wallet.start_threads(self.network) - if wallet: - self.wallets[path] = wallet + return wallet + storage = WalletStorage(path) + if not storage.file_exists: + return + wallet = Wallet(storage) + action = wallet.get_action() + if action: + return + wallet.start_threads(self.network) + self.wallets[path] = wallet return wallet + def add_wallet(self, wallet): + path = wallet.storage.path + self.wallets[path] = wallet + + def stop_wallet(self, path): + wallet = self.wallets.pop(path) + wallet.stop_threads() + def run_cmdline(self, config_options): config = SimpleConfig(config_options) cmdname = config.get('cmd') diff --git a/lib/wizard.py b/lib/wizard.py @@ -1,328 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - lightweight Bitcoin client -# Copyright (C) 2015 thomasv@gitorious, kyuupichan@gmail -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -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 WizardBase(PrintError): - '''Base class for gui-specific install wizards.''' - user_actions = ('create', 'restore') - wallet_kinds = [ - ('standard', _("Standard wallet")), - ('twofactor', _("Wallet with two-factor authentication")), - ('multisig', _("Multi-signature wallet")), - ('hardware', _("Hardware wallet")), - ] - - # 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, wallet_kinds): - """Ask the user what they want to do, and which wallet kind. - wallet_kinds is an array of translated wallet descriptions. - Return a a tuple (action, kind_index). Action is 'create' or - 'restore', and kind the index of the wallet kind chosen.""" - 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_choice(self, msg, choices): - """Asks the user which of several choices they would like. - Return the index of the choice.""" - raise NotImplementedError - - def query_hw_wallet_choice(self, msg, action, choices): - """Asks the user which hardware wallet kind they are using. Action is - 'create' or 'restore' from the initial screen. As this is - confusing for hardware wallets ask a new question with the - three possibilities Initialize ('create'), Use ('create') or - Restore a software-only wallet ('restore'). Return a pair - (action, choice).""" - 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): - """When restoring a wallet, request the passphrase that was used for - the wallet on the given device and confirm it. 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 show_restore(self, wallet, network): - """Show restore result""" - pass - - def finished(self): - """Called when the wizard is done.""" - pass - - def run(self, network, storage): - '''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, created by calling create_wizard().''' - need_sync = False - is_restore = False - - if storage.file_exists: - wallet = Wallet(storage) - if wallet.imported_keys: - self.update_wallet_format(wallet) - action = wallet.get_action() - if action != 'new': - self.hide() - path = storage.path - msg = _("The file '%s' contains an incompletely created wallet.\n" - "Do you want to complete its creation now?") % path - if not self.question(msg): - if self.question(_("Do you want to delete '%s'?") % path): - import os - os.remove(path) - self.show_warning(_('The file was removed')) - return - return - self.show() - else: - cr, wallet = self.create_or_restore(storage) - if not wallet: - return - need_sync = True - is_restore = (cr == 'restore') - - while True: - action = wallet.get_action() - if not action: - break - need_sync = True - self.run_wallet_action(wallet, action) - # Save the wallet after each action - wallet.storage.write() - - if network: - # Show network dialog if config does not exist - if self.config.get('auto_connect') is None: - self.choose_server(network) - else: - self.show_warning(_('You are offline')) - - if need_sync: - self.create_addresses(wallet) - - # start wallet threads - if network: - wallet.start_threads(network) - - if is_restore: - self.show_restore(wallet, network) - - self.finished() - - return wallet - - 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 = [] - if hasattr(wallet, 'plugin'): - calls.append((wallet.plugin, (wallet, self))) - calls.extend([(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) - - # Filter out any unregistered wallet kinds - registered_kinds = Wallet.categories() - kinds, descriptions = zip(*[pair for pair in WizardBase.wallet_kinds - if pair[0] in registered_kinds]) - action, kind_index = self.query_create_or_restore(descriptions) - assert action in WizardBase.user_actions - kind = kinds[kind_index] - if kind == 'multisig': - wallet_type = self.query_multisig(action) - elif kind == 'hardware': - hw_wallet_types, choices = self.plugins.hardware_wallets(action) - if choices: - msg = _('Select the type of hardware wallet: ') - else: - msg = ' '.join([ - _('No hardware wallet support found on your system.'), - _('Please install the relevant libraries (eg python-trezor for Trezor).'), - ]) - choice = self.query_hw_wallet_choice(msg, choices) - wallet_type = hw_wallet_types[choice] - 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) - - 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 generates - master keys.''' - 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) - - def create_main_account(self, wallet): - # FIXME: BIP44 restore requires password - wallet.create_main_account() - - def create_addresses(self, wallet): - wallet.synchronize() - - 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 - 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() diff --git a/plugins/hw_wallet/plugin.py b/plugins/hw_wallet/plugin.py @@ -53,21 +53,27 @@ class HW_PluginBase(BasePlugin): def on_restore_wallet(self, wallet, wizard): assert isinstance(wallet, self.wallet_class) - msg = _("Enter the seed for your %s wallet:" % self.device) - seed = wizard.request_seed(msg, is_valid = self.is_valid_seed) + f = lambda x: wizard.run('on_restore_seed', x) + wizard.enter_seed_dialog(run_next=f, title=_('Restore hardware wallet'), message=msg, is_valid=self.is_valid_seed) + + def on_restore_seed(self, wallet, wizard, seed): + f = lambda x: wizard.run('on_restore_passphrase', seed, x) + wizard.request_passphrase(self.device, run_next=f) + def on_restore_passphrase(self, wallet, wizard, seed, passphrase): + f = lambda x: wizard.run('on_restore_password', seed, passphrase, x) + wizard.request_password(run_next=f) + + def on_restore_password(self, wallet, wizard, seed, passphrase, password): # Restored wallets are not hardware wallets wallet_class = self.wallet_class.restore_wallet_class wallet.storage.put('wallet_type', wallet_class.wallet_type) wallet = wallet_class(wallet.storage) - - passphrase = wizard.request_passphrase(self.device) - password = wizard.request_password() wallet.add_seed(seed, password) wallet.add_xprv_from_seed(seed, 'x/', password, passphrase) wallet.create_hd_account(password) - return wallet + wizard.create_addresses() @staticmethod def is_valid_seed(seed): diff --git a/plugins/trustedcoin/qt.py b/plugins/trustedcoin/qt.py @@ -110,14 +110,9 @@ class Plugin(TrustedCoinPlugin): return WaitingDialog(window, 'Getting billing information...', task, on_finished) - def confirm(self, window, msg): - vbox = QVBoxLayout() - vbox.addWidget(WWLabel(msg)) - window.set_main_layout(vbox) - - def show_disclaimer(self, wallet, window): - window.set_icon(':icons/trustedcoin.png') - self.confirm(window, '\n\n'.join(DISCLAIMER)) + def show_disclaimer(self, wallet, wizard): + wizard.set_icon(':icons/trustedcoin.png') + wizard.confirm('\n\n'.join(DISCLAIMER)) self.set_enabled(wallet, True) @hook diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py @@ -346,21 +346,32 @@ class TrustedCoinPlugin(BasePlugin): wallet.price_per_tx = dict(billing_info['price_per_tx']) return True - def create_extended_seed(self, wallet, window): + def create_extended_seed(self, wallet, wizard): + self.wallet = wallet + self.wizard = wizard seed = wallet.make_seed() - window.show_and_verify_seed(seed, is_valid=self.is_valid_seed) + f = lambda x: wizard.run('confirm_seed', x) + self.wizard.show_seed_dialog(run_next=f, message="z", seed_text=seed) - password = window.request_password() + def confirm_seed(self, wallet, wizard, seed): + title = _('Confirm Seed') + msg = _('Please retype your seed phrase, to confirm that you properly saved it') + f = lambda x: wizard.run('add_password', x) + self.wizard.enter_seed_dialog(run_next=f, title=title, message=msg, is_valid=lambda x: x==seed) + + def add_password(self, wallet, wizard, seed): + f = lambda x: self.create_wallet(seed, x) + self.wizard.request_password(run_next=f) + + def create_wallet(self, seed, password): + wallet = self.wallet wallet.storage.put('seed_version', wallet.seed_version) wallet.storage.put('use_encryption', password is not None) - words = seed.split() n = len(words)/2 wallet.add_xprv_from_seed(' '.join(words[0:n]), 'x1/', password) wallet.add_xpub_from_seed(' '.join(words[n:]), 'x2/') - wallet.storage.write() - msg = [ _("Your wallet file is: %s.")%os.path.abspath(wallet.storage.path), _("You need to be online in order to complete the creation of " @@ -371,7 +382,8 @@ class TrustedCoinPlugin(BasePlugin): _('If you are online, click on "%s" to continue.') % _('Next') ] msg = '\n\n'.join(msg) - self.confirm(window, msg) + self.wizard.confirm(msg) + return wallet @hook def do_clear(self, window): @@ -379,19 +391,22 @@ class TrustedCoinPlugin(BasePlugin): def on_restore_wallet(self, wallet, wizard): assert isinstance(wallet, self.wallet_class) + title = _("Restore two-factor Wallet") + f = lambda x: wizard.run('on_restore_seed', x) + wizard.enter_seed_dialog(run_next=f, title=title, message=RESTORE_MSG, is_valid=self.is_valid_seed) - seed = wizard.request_seed(RESTORE_MSG, is_valid=self.is_valid_seed) - password = wizard.request_password() + def on_restore_seed(self, wallet, wizard, seed): + f = lambda x: wizard.run('on_restore_pw', seed, x) + wizard.request_password(run_next=f) + def on_restore_pw(self, wallet, wizard, seed, password): wallet.add_seed(seed, password) words = seed.split() n = len(words)/2 wallet.add_xprv_from_seed(' '.join(words[0:n]), 'x1/', password) wallet.add_xprv_from_seed(' '.join(words[n:]), 'x2/', password) - restore_third_key(wallet) - wallet.create_main_account() - return wallet + wizard.create_addresses() def create_remote_key(self, wallet, window): email = self.accept_terms_of_use(window)