Finish wizard unification
This commit is contained in:
parent
97dc130e26
commit
e7d25faf02
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
return
|
||||
if storage.file_exists:
|
||||
wallet = Wallet(storage)
|
||||
action = wallet.get_action()
|
||||
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.run(action)
|
||||
else:
|
||||
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
|
||||
wallet = self.daemon.load_wallet(path)
|
||||
if wallet:
|
||||
self.load_wallet(wallet)
|
||||
self.on_resume()
|
||||
else:
|
||||
Logger.debug('Electrum: Wallet not found. Launching install wizard')
|
||||
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)
|
||||
|
||||
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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
# WindowModalDialog must come first as it overrides show_error
|
||||
class InstallWizard(QDialog, MessageBoxMixin, WizardBase):
|
||||
|
||||
def __init__(self, config, app, plugins):
|
||||
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, BaseWizard):
|
||||
|
||||
def __init__(self, config, app, plugins, network, storage):
|
||||
|
||||
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())
|
||||
@wizard_dialog
|
||||
def add_xpub_dialog(self, title, message, is_valid, run_next):
|
||||
return self.text_input(title, message, is_valid)
|
||||
|
||||
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'))
|
||||
|
||||
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()
|
||||
def confirm(self, msg):
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget(WWLabel(msg))
|
||||
self.set_main_layout(vbox)
|
||||
|
||||
@wizard_dialog
|
||||
def action_dialog(self, action, run_next):
|
||||
self.run(action)
|
||||
|
||||
def terminate(self):
|
||||
self.wallet.start_threads(self.network)
|
||||
self.emit(QtCore.SIGNAL('accept'))
|
||||
|
||||
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_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."""
|
||||
|
||||
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 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()
|
||||
vbox.addLayout(actions_clayout.layout())
|
||||
vbox.addLayout(wallet_clayout.layout())
|
||||
vbox.addLayout(clayout.layout())
|
||||
self.set_main_layout(vbox, title)
|
||||
action = c_values[clayout.selected_index()]
|
||||
return action
|
||||
|
||||
action = ['create', 'restore'][actions_clayout.selected_index()]
|
||||
return action, wallet_clayout.selected_index()
|
||||
|
||||
def query_hw_wallet_choice(self, msg, choices):
|
||||
@wizard_dialog
|
||||
def show_xpub_dialog(self, xpub, run_next):
|
||||
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
|
||||
|
||||
def request_many(self, n, xpub_hot=None):
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
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_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)
|
||||
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)
|
||||
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 create_standard(self):
|
||||
self.wallet_type = 'standard'
|
||||
self.run('choose_seed')
|
||||
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)
|
||||
|
|
|
@ -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')
|
||||
|
|
328
lib/wizard.py
328
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()
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue