kivy: add multisig wallets to install wizard

This commit is contained in:
ThomasV 2016-06-15 11:16:29 +02:00
parent dabeae9f95
commit 0ccc812f86
2 changed files with 178 additions and 112 deletions

View File

@ -1,8 +1,3 @@
''' Dialogs and widgets Responsible for creation, restoration of accounts are
defined here.
Namely: CreateAccountDialog, CreateRestoreDialog, RestoreSeedDialog
'''
from functools import partial from functools import partial
@ -12,10 +7,14 @@ from kivy.lang import Builder
from kivy.properties import ObjectProperty, StringProperty, OptionProperty from kivy.properties import ObjectProperty, StringProperty, OptionProperty
from kivy.core.window import Window from kivy.core.window import Window
from kivy.uix.button import Button from kivy.uix.button import Button
from kivy.utils import platform
from electrum_gui.kivy.uix.dialogs import EventsDialog from electrum_gui.kivy.uix.dialogs import EventsDialog
from electrum_gui.kivy.i18n import _ from electrum_gui.kivy.i18n import _
test_xpub = "xpub661MyMwAqRbcFpV2JqonBDKdgJiExpxiSAtvphtpviunv42FNVJNNRA3Zdy5kQXoK7NpwUC2QQPXVMLKLoHxaekNfemFs5zkfrNnk91dobZ"
test_seed = "powder idea leader task pretty harsh resemble alert quit athlete clerk almost able"
is_test = platform != 'android'
Builder.load_string(''' Builder.load_string('''
#:import Window kivy.core.window.Window #:import Window kivy.core.window.Window
@ -44,6 +43,7 @@ Builder.load_string('''
<-WizardDialog> <-WizardDialog>
text_color: .854, .925, .984, 1 text_color: .854, .925, .984, 1
value: ''
#auto_dismiss: False #auto_dismiss: False
size_hint: None, None size_hint: None, None
canvas.before: canvas.before:
@ -81,16 +81,62 @@ Builder.load_string('''
cols: 1 cols: 1
id: crcontent id: crcontent
spacing: '1dp' spacing: '1dp'
Widget:
size_hint: 1, 0.3
GridLayout:
rows: 1
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
WizardButton:
id: back
text: _('Back')
root: root
WizardButton:
id: next
text: _('Next')
root: root
disabled: root.value == ''
<CreateRestoreDialog> <WizardMultisigDialog>
Image: value: 'next'
id: logo_img Widget
mipmap: True size_hint: 1, 1
allow_stretch: True Label:
color: root.text_color
size_hint: 1, None size_hint: 1, None
height: '110dp' text_size: self.width, None
source: 'atlas://gui/kivy/theming/light/electrum_icon640' height: self.texture_size[1]
text: _("Choose the number of signatures needed to unlock funds in your wallet")
Widget
size_hint: 1, 1
GridLayout:
orientation: 'vertical'
cols: 2
spacing: '14dp'
size_hint: 1, 1
height: self.minimum_height
Label:
color: root.text_color
text: _('From %d cosigners')%n.value
Slider:
id: n
range: 2, 5
step: 1
value: 2
Label:
color: root.text_color
text: _('Require %d signatures')%m.value
Slider:
id: m
range: 1, n.value
step: 1
value: 2
<WizardChoiceDialog>
msg : ''
Widget: Widget:
size_hint: 1, 1 size_hint: 1, 1
Label: Label:
@ -98,31 +144,16 @@ Builder.load_string('''
size_hint: 1, None size_hint: 1, None
text_size: self.width, None text_size: self.width, None
height: self.texture_size[1] height: self.texture_size[1]
text: text: root.msg
_("Creating a new wallet.")+" " +\
_("Do you want to create a new seed, or to restore a wallet using an existing seed?")
Widget Widget
size_hint: 1, 1 size_hint: 1, 1
GridLayout: GridLayout:
id: grid row_default_height: '48dp'
orientation: 'vertical' orientation: 'vertical'
id: choices
cols: 1 cols: 1
spacing: '14dp' spacing: '14dp'
size_hint: 1, None size_hint: 1, None
height: self.minimum_height
WizardButton:
id: create
text: _('Create a new seed')
root: root
WizardButton:
id: restore_seed
text: _('I already have a seed')
root: root
WizardButton:
id: restore_xpub
text: _('Watching-only wallet')
root: root
<MButton@Button>: <MButton@Button>:
size_hint: 1, None size_hint: 1, None
@ -268,27 +299,8 @@ Builder.load_string('''
text: ' ' text: ' '
MButton: MButton:
text: '<' text: '<'
Widget:
size_hint: 1, 1
GridLayout: <AddXpubDialog>
rows: 1
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
WizardButton:
id: back
text: _('Back')
root: root
WizardButton:
id: next
text: _('Next')
root: root
disabled: True
<RestoreXpubDialog>
word: ''
Label: Label:
color: root.text_color color: root.text_color
size_hint: 1, None size_hint: 1, None
@ -309,7 +321,7 @@ Builder.load_string('''
SeedLabel: SeedLabel:
text: root.message text: root.message
GridLayout: GridLayout
rows: 1 rows: 1
spacing: '12dp' spacing: '12dp'
size_hint: 1, None size_hint: 1, None
@ -327,27 +339,10 @@ Builder.load_string('''
text: _('Clear') text: _('Clear')
on_release: root.do_clear() on_release: root.do_clear()
Widget:
size_hint: 1, 1
GridLayout:
rows: 1
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
WizardButton:
id: back
text: _('Back')
root: root
WizardButton:
id: next
text: _('Next')
root: root
disabled: True
<ShowSeedDialog> <ShowSeedDialog>
spacing: '12dp' spacing: '12dp'
value: 'next'
Label: Label:
color: root.text_color color: root.text_color
size_hint: 1, None size_hint: 1, None
@ -366,24 +361,10 @@ Builder.load_string('''
text: root.seed_text text: root.seed_text
SeedLabel: SeedLabel:
text: root.message text: root.message
Widget:
size_hint: 1, 1
GridLayout:
rows: 1
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
WizardButton:
id: back
text: _('Back')
root: root
WizardButton:
id: confirm
text: _('Confirm')
root: root
''') ''')
class WizardDialog(EventsDialog): class WizardDialog(EventsDialog):
''' Abstract dialog to be used as the base for all Create Account Dialogs ''' Abstract dialog to be used as the base for all Create Account Dialogs
''' '''
@ -421,8 +402,24 @@ class WizardDialog(EventsDialog):
app.stop() app.stop()
class CreateRestoreDialog(WizardDialog): class WizardMultisigDialog(WizardDialog):
''' Initial Dialog for creating or restoring seed''' pass
class WizardChoiceDialog(WizardDialog):
''' Multiple choices dialog '''
def __init__(self, **kwargs):
super(WizardChoiceDialog, self).__init__(**kwargs)
self.msg = kwargs.get('msg', '')
choices = kwargs.get('choices', [])
layout = self.ids.choices
layout.bind(minimum_height=layout.setter('height'))
for text, action in choices:
l = WizardButton(text=text)
l.action = action
l.height = '48dp'
l.root = self
layout.add_widget(l)
def on_parent(self, instance, value): def on_parent(self, instance, value):
if value: if value:
@ -444,6 +441,9 @@ class ShowSeedDialog(WizardDialog):
class WordButton(Button): class WordButton(Button):
pass pass
class WizardButton(Button):
pass
class RestoreSeedDialog(WizardDialog): class RestoreSeedDialog(WizardDialog):
message = StringProperty('') message = StringProperty('')
@ -454,6 +454,7 @@ class RestoreSeedDialog(WizardDialog):
from electrum.mnemonic import Mnemonic from electrum.mnemonic import Mnemonic
from electrum.old_mnemonic import words as old_wordlist from electrum.old_mnemonic import words as old_wordlist
self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist)) self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist))
self.ids.text_input_seed.text = test_seed if is_test else ''
def get_suggestions(self, prefix): def get_suggestions(self, prefix):
for w in self.words: for w in self.words:
@ -545,12 +546,12 @@ class RestoreSeedDialog(WizardDialog):
tis._keyboard.unbind(on_key_down=self.on_key_down) tis._keyboard.unbind(on_key_down=self.on_key_down)
tis.focus = False tis.focus = False
class RestoreXpubDialog(WizardDialog): class AddXpubDialog(WizardDialog):
message = StringProperty('') message = StringProperty('')
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(RestoreXpubDialog, self).__init__(**kwargs) super(AddXpubDialog, self).__init__(**kwargs)
self._test = kwargs['test'] self._test = kwargs['test']
self.app = App.get_running_app() self.app = App.get_running_app()
@ -567,7 +568,8 @@ class RestoreXpubDialog(WizardDialog):
self.app.scan_qr(on_complete) self.app.scan_qr(on_complete)
def do_paste(self): def do_paste(self):
self.ids.text_input_seed.text = unicode(self.app._clipboard.paste()) self.ids.text_input_seed.text = test_xpub if is_test else unicode(self.app._clipboard.paste())
def do_clear(self): def do_clear(self):
self.ids.text_input_seed.text = '' self.ids.text_input_seed.text = ''

View File

@ -1,4 +1,5 @@
from electrum import Wallet import os
from electrum.wallet import Wallet, Multisig_Wallet
from electrum_gui.kivy.i18n import _ from electrum_gui.kivy.i18n import _
from kivy.app import App from kivy.app import App
@ -12,7 +13,7 @@ import threading
from functools import partial from functools import partial
import weakref import weakref
from create_restore import CreateRestoreDialog, ShowSeedDialog, RestoreSeedDialog, RestoreXpubDialog from create_restore import WizardChoiceDialog, ShowSeedDialog, RestoreSeedDialog, AddXpubDialog, WizardMultisigDialog
from password_dialog import PasswordDialog from password_dialog import PasswordDialog
@ -36,6 +37,7 @@ class InstallWizard(Widget):
self.config = config self.config = config
self.network = network self.network = network
self.storage = storage self.storage = storage
self.wallet = None
def waiting_dialog(self, task, msg, on_complete=None): def waiting_dialog(self, task, msg, on_complete=None):
'''Perform a blocking task in the background by running the passed '''Perform a blocking task in the background by running the passed
@ -68,22 +70,51 @@ class InstallWizard(Widget):
else: else:
raise BaseException("unknown action", action) raise BaseException("unknown action", action)
def on_release(self, dialog, button):
if not button or button.action == 'cancel':
# soft back or escape button pressed
return self.dispatch('on_wizard_complete', None)
action = button.action if self.wallet is None else self.wallet.get_action()
print "action", action
dialog.close()
self.run(action)
def add_seed_or_xpub(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')
]
WizardChoiceDialog(msg=msg, choices=choices, on_release=self.on_release).open()
def new(self): def new(self):
name = os.path.basename(self.storage.path)
msg = "\n".join([
_("Welcome to the Electrum installation wizard."),
_("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'),
]
WizardChoiceDialog(msg=msg, choices=choices, on_release=self.on_release).open()
def choose_cosigners(self):
def on_release(dialog, button): def on_release(dialog, button):
if not button: if not button:
# soft back or escape button pressed # soft back or escape button pressed
return self.dispatch('on_wizard_complete', None) return self.dispatch('on_wizard_complete', None)
m = dialog.ids.m.value
n = dialog.ids.n.value
dialog.close() dialog.close()
action = dialog.action self.wallet_type = "%dof%d"%(m, n)
if button == dialog.ids.create: self.run('add_seed_or_xpub')
self.run('create') name = os.path.basename(self.storage.path)
elif button == dialog.ids.restore_seed: WizardMultisigDialog(on_release=on_release).open()
self.run('restore_seed')
elif button == dialog.ids.restore_xpub:
self.run('restore_xpub')
else:
self.dispatch('on_wizard_complete', None)
CreateRestoreDialog(on_release=on_release).open()
def restore_seed(self): def restore_seed(self):
def on_seed(_dlg, btn): def on_seed(_dlg, btn):
@ -103,27 +134,59 @@ class InstallWizard(Widget):
self.run('new') self.run('new')
return return
text = _dlg.get_text() text = _dlg.get_text()
self.run('add_seed', (text, None)) self.run('create_wallet', (text, None))
msg = _('To create a watching-only wallet, paste your master public key, or scan it using the camera button.') msg = _('To create a watching-only wallet, paste your master public key, or scan it using the camera button.')
RestoreXpubDialog(test=Wallet.is_mpk, message=msg, on_release=on_xpub).open() AddXpubDialog(test=Wallet.is_mpk, message=msg, on_release=on_xpub).open()
def add_seed(self, text, password): def create_standard(self):
def task(): self.wallet_type = 'standard'
self.run('add_seed_or_xpub')
def create_multisig(self):
self.wallet_type = 'multisig'
self.run('choose_cosigners')
def create_wallet(self, text, password):
if self.wallet_type == 'standard':
self.wallet = Wallet.from_text(text, password, self.storage) self.wallet = Wallet.from_text(text, password, self.storage)
self.run('create_addresses')
else:
self.storage.put('wallet_type', self.wallet_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)
def add_cosigners(self):
def on_xpub(_dlg, btn):
xpub = _dlg.get_text()
_dlg.close()
self.wallet.add_master_public_key("x%d/" % 2, xpub)
action = self.wallet.get_action()
self.run(action)
msg = _('Paste your cosigner xpub, or scan it using the camera button.')
AddXpubDialog(test=Wallet.is_xpub, message=msg, on_release=on_xpub).open()
def create_main_account(self):
self.wallet.create_main_account()
self.run('create_addresses')
def create_addresses(self):
def task():
self.wallet.create_main_account() self.wallet.create_main_account()
self.wallet.synchronize() self.wallet.synchronize()
msg= _("Electrum is generating your addresses, please wait.") msg= _("Electrum is generating your addresses, please wait.")
self.waiting_dialog(task, msg, self.terminate) self.waiting_dialog(task, msg, on_complete=self.terminate)
def create(self): def create_seed(self):
from electrum.wallet import BIP32_Wallet from electrum.wallet import BIP32_Wallet
seed = BIP32_Wallet.make_seed() seed = BIP32_Wallet.make_seed()
msg = _("If you forget your PIN or lose your device, your seed phrase will be the " msg = _("If you forget your PIN or lose your device, your seed phrase will be the "
"only way to recover your funds.") "only way to recover your funds.")
def on_ok(_dlg, _btn): def on_ok(_dlg, _btn):
_dlg.close() _dlg.close()
if _btn == _dlg.ids.confirm: if _btn == _dlg.ids.next:
self.run('confirm_seed', (seed,)) self.run('confirm_seed', (seed,))
else: else:
self.run('new') self.run('new')
@ -134,7 +197,7 @@ class InstallWizard(Widget):
def on_seed(_dlg, btn): def on_seed(_dlg, btn):
if btn is _dlg.ids.back: if btn is _dlg.ids.back:
_dlg.close() _dlg.close()
self.run('create') self.run('create_seed')
return return
_dlg.close() _dlg.close()
self.run('enter_pin', (seed,)) self.run('enter_pin', (seed,))
@ -143,7 +206,8 @@ class InstallWizard(Widget):
def enter_pin(self, seed): def enter_pin(self, seed):
def callback(pin): def callback(pin):
self.run('confirm_pin', (seed, pin)) action = 'confirm_pin' if pin else 'create_wallet'
self.run(action, (seed, pin))
popup = PasswordDialog() popup = PasswordDialog()
popup.init('Choose a PIN code', callback) popup.init('Choose a PIN code', callback)
popup.open() popup.open()
@ -151,7 +215,7 @@ class InstallWizard(Widget):
def confirm_pin(self, seed, pin): def confirm_pin(self, seed, pin):
def callback(conf): def callback(conf):
if conf == pin: if conf == pin:
self.run('add_seed', (seed, pin)) self.run('create_wallet', (seed, pin))
else: else:
app.show_error(_('PIN mismatch'), duration=.5) app.show_error(_('PIN mismatch'), duration=.5)
self.run('enter_pin', (seed,)) self.run('enter_pin', (seed,))