electrum

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

commit 0ccc812f869571f52b68b5b2efb7eed1792216cd
parent dabeae9f9508a1a59b86c2e8fef81327b0ee688d
Author: ThomasV <thomasv@electrum.org>
Date:   Wed, 15 Jun 2016 11:16:29 +0200

kivy: add multisig wallets to install wizard

Diffstat:
Mgui/kivy/uix/dialogs/create_restore.py | 184++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mgui/kivy/uix/dialogs/installwizard.py | 110++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
2 files changed, 180 insertions(+), 114 deletions(-)

diff --git a/gui/kivy/uix/dialogs/create_restore.py b/gui/kivy/uix/dialogs/create_restore.py @@ -1,8 +1,3 @@ -''' Dialogs and widgets Responsible for creation, restoration of accounts are -defined here. - -Namely: CreateAccountDialog, CreateRestoreDialog, RestoreSeedDialog -''' from functools import partial @@ -12,10 +7,14 @@ from kivy.lang import Builder from kivy.properties import ObjectProperty, StringProperty, OptionProperty from kivy.core.window import Window from kivy.uix.button import Button +from kivy.utils import platform from electrum_gui.kivy.uix.dialogs import EventsDialog 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(''' #:import Window kivy.core.window.Window @@ -44,6 +43,7 @@ Builder.load_string(''' <-WizardDialog> text_color: .854, .925, .984, 1 + value: '' #auto_dismiss: False size_hint: None, None canvas.before: @@ -81,16 +81,62 @@ Builder.load_string(''' cols: 1 id: crcontent spacing: '1dp' - - -<CreateRestoreDialog> - Image: - id: logo_img - mipmap: True - allow_stretch: True + 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 == '' + + +<WizardMultisigDialog> + value: 'next' + Widget + size_hint: 1, 1 + Label: + color: root.text_color size_hint: 1, None - height: '110dp' - source: 'atlas://gui/kivy/theming/light/electrum_icon640' + text_size: self.width, None + 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: size_hint: 1, 1 Label: @@ -98,31 +144,16 @@ Builder.load_string(''' size_hint: 1, None text_size: self.width, None height: self.texture_size[1] - text: - _("Creating a new wallet.")+" " +\ - _("Do you want to create a new seed, or to restore a wallet using an existing seed?") + text: root.msg Widget size_hint: 1, 1 GridLayout: - id: grid + row_default_height: '48dp' orientation: 'vertical' + id: choices cols: 1 spacing: '14dp' 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>: size_hint: 1, None @@ -268,27 +299,8 @@ Builder.load_string(''' text: ' ' MButton: text: '<' - 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 - - -<RestoreXpubDialog> - word: '' +<AddXpubDialog> Label: color: root.text_color size_hint: 1, None @@ -309,7 +321,7 @@ Builder.load_string(''' SeedLabel: text: root.message - GridLayout: + GridLayout rows: 1 spacing: '12dp' size_hint: 1, None @@ -327,27 +339,10 @@ Builder.load_string(''' text: _('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> spacing: '12dp' + value: 'next' Label: color: root.text_color size_hint: 1, None @@ -366,24 +361,10 @@ Builder.load_string(''' text: root.seed_text SeedLabel: 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): ''' Abstract dialog to be used as the base for all Create Account Dialogs ''' @@ -421,8 +402,24 @@ class WizardDialog(EventsDialog): app.stop() -class CreateRestoreDialog(WizardDialog): - ''' Initial Dialog for creating or restoring seed''' +class WizardMultisigDialog(WizardDialog): + 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): if value: @@ -444,6 +441,9 @@ class ShowSeedDialog(WizardDialog): class WordButton(Button): pass +class WizardButton(Button): + pass + class RestoreSeedDialog(WizardDialog): message = StringProperty('') @@ -454,6 +454,7 @@ class RestoreSeedDialog(WizardDialog): 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 = test_seed if is_test else '' def get_suggestions(self, prefix): for w in self.words: @@ -545,12 +546,12 @@ class RestoreSeedDialog(WizardDialog): tis._keyboard.unbind(on_key_down=self.on_key_down) tis.focus = False -class RestoreXpubDialog(WizardDialog): +class AddXpubDialog(WizardDialog): message = StringProperty('') def __init__(self, **kwargs): - super(RestoreXpubDialog, self).__init__(**kwargs) + super(AddXpubDialog, self).__init__(**kwargs) self._test = kwargs['test'] self.app = App.get_running_app() @@ -567,7 +568,8 @@ class RestoreXpubDialog(WizardDialog): self.app.scan_qr(on_complete) 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): self.ids.text_input_seed.text = '' + diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py @@ -1,4 +1,5 @@ -from electrum import Wallet +import os +from electrum.wallet import Wallet, Multisig_Wallet from electrum_gui.kivy.i18n import _ from kivy.app import App @@ -12,7 +13,7 @@ import threading from functools import partial import weakref -from create_restore import CreateRestoreDialog, ShowSeedDialog, RestoreSeedDialog, RestoreXpubDialog +from create_restore import WizardChoiceDialog, ShowSeedDialog, RestoreSeedDialog, AddXpubDialog, WizardMultisigDialog from password_dialog import PasswordDialog @@ -36,6 +37,7 @@ class InstallWizard(Widget): self.config = config self.network = network self.storage = storage + self.wallet = None def waiting_dialog(self, task, msg, on_complete=None): '''Perform a blocking task in the background by running the passed @@ -68,22 +70,51 @@ class InstallWizard(Widget): else: 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): + 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): if not button: # soft back or escape button pressed return self.dispatch('on_wizard_complete', None) + m = dialog.ids.m.value + n = dialog.ids.n.value dialog.close() - action = dialog.action - if button == dialog.ids.create: - self.run('create') - elif button == dialog.ids.restore_seed: - 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() + self.wallet_type = "%dof%d"%(m, n) + self.run('add_seed_or_xpub') + name = os.path.basename(self.storage.path) + WizardMultisigDialog(on_release=on_release).open() def restore_seed(self): def on_seed(_dlg, btn): @@ -103,27 +134,59 @@ class InstallWizard(Widget): self.run('new') return 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.') - 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 task(): + def create_standard(self): + 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.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.synchronize() 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 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.") def on_ok(_dlg, _btn): _dlg.close() - if _btn == _dlg.ids.confirm: + if _btn == _dlg.ids.next: self.run('confirm_seed', (seed,)) else: self.run('new') @@ -134,7 +197,7 @@ class InstallWizard(Widget): def on_seed(_dlg, btn): if btn is _dlg.ids.back: _dlg.close() - self.run('create') + self.run('create_seed') return _dlg.close() self.run('enter_pin', (seed,)) @@ -143,7 +206,8 @@ class InstallWizard(Widget): def enter_pin(self, seed): 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.init('Choose a PIN code', callback) popup.open() @@ -151,7 +215,7 @@ class InstallWizard(Widget): def confirm_pin(self, seed, pin): def callback(conf): if conf == pin: - self.run('add_seed', (seed, pin)) + self.run('create_wallet', (seed, pin)) else: app.show_error(_('PIN mismatch'), duration=.5) self.run('enter_pin', (seed,))