tkivy: separate base wizard class from gui - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 9182392b5519d0246c5da712236e9e6a5cb5e681 DIR parent 362e4be6f0b00bfaf6da8f5d5db6ea0b72377163 HTML Author: ThomasV <thomasv@electrum.org> Date: Thu, 16 Jun 2016 19:25:44 +0200 kivy: separate base wizard class from gui Diffstat: M gui/kivy/uix/dialogs/__init__.py | 2 -- D gui/kivy/uix/dialogs/create_restor… | 623 ------------------------------- M gui/kivy/uix/dialogs/installwizard… | 874 ++++++++++++++++++++++++------- A lib/base_wizard.py | 167 +++++++++++++++++++++++++++++++ M lib/wallet.py | 13 +++++++++++-- 5 files changed, 850 insertions(+), 829 deletions(-) --- DIR diff --git a/gui/kivy/uix/dialogs/__init__.py b/gui/kivy/uix/dialogs/__init__.py t@@ -67,7 +67,6 @@ class EventsDialog(Factory.Popup): def __init__(self, **kwargs): super(EventsDialog, self).__init__(**kwargs) - self._on_release = kwargs.get('on_release') def on_release(self, instance): pass t@@ -76,7 +75,6 @@ class EventsDialog(Factory.Popup): pass def close(self): - self._on_release = None self.dismiss() DIR diff --git a/gui/kivy/uix/dialogs/create_restore.py b/gui/kivy/uix/dialogs/create_restore.py t@@ -1,623 +0,0 @@ - -from functools import partial - -from kivy.app import App -from kivy.clock import Clock -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 -#:import _ electrum_gui.kivy.i18n._ - - -<WizardTextInput@TextInput> - border: 4, 4, 4, 4 - font_size: '15sp' - padding: '15dp', '15dp' - background_color: (1, 1, 1, 1) if self.focus else (0.454, 0.698, 0.909, 1) - foreground_color: (0.31, 0.31, 0.31, 1) if self.focus else (0.835, 0.909, 0.972, 1) - hint_text_color: self.foreground_color - background_active: 'atlas://gui/kivy/theming/light/create_act_text_active' - background_normal: 'atlas://gui/kivy/theming/light/create_act_text_active' - size_hint_y: None - height: '48sp' - -<WizardButton@Button>: - root: None - size_hint: 1, None - height: '48sp' - on_press: if self.root: self.root.dispatch('on_press', self) - on_release: if self.root: self.root.dispatch('on_release', self) - -<BigLabel@Label> - color: .854, .925, .984, 1 - size_hint: 1, None - text_size: self.width, None - height: self.texture_size[1] - bold: True - -<-WizardDialog> - text_color: .854, .925, .984, 1 - value: '' - #auto_dismiss: False - size_hint: None, None - canvas.before: - Color: - rgba: 0, 0, 0, .9 - Rectangle: - size: Window.size - Color: - rgba: .239, .588, .882, 1 - Rectangle: - size: Window.size - - crcontent: crcontent - # add electrum icon - BoxLayout: - orientation: 'vertical' if self.width < self.height else 'horizontal' - padding: - min(dp(27), self.width/32), min(dp(27), self.height/32),\ - min(dp(27), self.width/32), min(dp(27), self.height/32) - spacing: '10dp' - GridLayout: - id: grid_logo - cols: 1 - pos_hint: {'center_y': .5} - size_hint: 1, None - height: self.minimum_height - Label: - color: root.text_color - text: 'ELECTRUM' - size_hint: 1, None - height: self.texture_size[1] if self.opacity else 0 - font_size: '33sp' - font_name: 'gui/kivy/data/fonts/tron/Tr2n.ttf' - GridLayout: - cols: 1 - id: crcontent - 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 == '' - - -<WizardMultisigDialog> - value: 'next' - Widget - size_hint: 1, 1 - Label: - color: root.text_color - size_hint: 1, None - 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: - color: root.text_color - size_hint: 1, None - text_size: self.width, None - height: self.texture_size[1] - text: root.msg - Widget - size_hint: 1, 1 - GridLayout: - row_default_height: '48dp' - orientation: 'vertical' - id: choices - cols: 1 - spacing: '14dp' - size_hint: 1, None - -<MButton@Button>: - size_hint: 1, None - height: '33dp' - on_release: - self.parent.update_amount(self.text) - -<WordButton@Button>: - size_hint: None, None - padding: '5dp', '5dp' - text_size: None, self.height - width: self.texture_size[0] - height: '30dp' - on_release: - self.parent.new_word(self.text) - - -<SeedButton@Button>: - height: dp(100) - border: 4, 4, 4, 4 - halign: 'justify' - valign: 'top' - font_size: '18dp' - text_size: self.width - dp(24), self.height - dp(12) - color: .1, .1, .1, 1 - background_normal: 'atlas://gui/kivy/theming/light/white_bg_round_top' - background_down: self.background_normal - size_hint_y: None - - -<SeedLabel@Label>: - font_size: '12sp' - text_size: self.width, None - size_hint: 1, None - height: self.texture_size[1] - halign: 'justify' - valign: 'middle' - border: 4, 4, 4, 4 - - -<RestoreSeedDialog> - word: '' - BigLabel: - text: "ENTER YOUR SEED PHRASE" - GridLayout - cols: 1 - padding: 0, '12dp' - orientation: 'vertical' - spacing: '12dp' - size_hint: 1, None - height: self.minimum_height - SeedButton: - id: text_input_seed - text: '' - on_text: Clock.schedule_once(root.on_text) - SeedLabel: - text: root.message - BoxLayout: - id: suggestions - height: '35dp' - size_hint: 1, None - new_word: root.on_word - BoxLayout: - id: line1 - update_amount: root.update_text - size_hint: 1, None - height: '30dp' - MButton: - text: 'Q' - MButton: - text: 'W' - MButton: - text: 'E' - MButton: - text: 'R' - MButton: - text: 'T' - MButton: - text: 'Y' - MButton: - text: 'U' - MButton: - text: 'I' - MButton: - text: 'O' - MButton: - text: 'P' - BoxLayout: - id: line2 - update_amount: root.update_text - size_hint: 1, None - height: '30dp' - Widget: - size_hint: 0.5, None - height: '33dp' - MButton: - text: 'A' - MButton: - text: 'S' - MButton: - text: 'D' - MButton: - text: 'F' - MButton: - text: 'G' - MButton: - text: 'H' - MButton: - text: 'J' - MButton: - text: 'K' - MButton: - text: 'L' - Widget: - size_hint: 0.5, None - height: '33dp' - BoxLayout: - id: line3 - update_amount: root.update_text - size_hint: 1, None - height: '30dp' - Widget: - size_hint: 1, None - MButton: - text: 'Z' - MButton: - text: 'X' - MButton: - text: 'C' - MButton: - text: 'V' - MButton: - text: 'B' - MButton: - text: 'N' - MButton: - text: 'M' - MButton: - text: ' ' - MButton: - text: '<' - -<AddXpubDialog> - title: '' - message: '' - BigLabel: - text: root.title - GridLayout - cols: 1 - padding: 0, '12dp' - orientation: 'vertical' - spacing: '12dp' - size_hint: 1, None - height: self.minimum_height - SeedButton: - id: text_input - text: '' - on_text: Clock.schedule_once(root.check_text) - SeedLabel: - text: root.message - GridLayout - rows: 1 - spacing: '12dp' - size_hint: 1, None - height: self.minimum_height - IconButton: - id: scan - height: '48sp' - on_release: root.scan_xpub() - icon: 'atlas://gui/kivy/theming/light/camera' - size_hint: 1, None - WizardButton: - text: _('Paste') - on_release: root.do_paste() - WizardButton: - text: _('Clear') - on_release: root.do_clear() - - -<ShowXpubDialog> - xpub: '' - message: _('Here is your master public key. Share it with your cosigners.') - BigLabel: - text: "MASTER PUBLIC KEY" - GridLayout - cols: 1 - padding: 0, '12dp' - orientation: 'vertical' - spacing: '12dp' - size_hint: 1, None - height: self.minimum_height - SeedButton: - id: text_input - text: root.xpub - SeedLabel: - text: root.message - GridLayout - rows: 1 - spacing: '12dp' - size_hint: 1, None - height: self.minimum_height - WizardButton: - text: _('QR code') - on_release: root.do_qr() - WizardButton: - text: _('Copy') - on_release: root.do_copy() - WizardButton: - text: _('Share') - on_release: root.do_share() - - -<ShowSeedDialog> - spacing: '12dp' - value: 'next' - BigLabel: - text: "PLEASE WRITE DOWN YOUR SEED PHRASE" - GridLayout: - id: grid - cols: 1 - pos_hint: {'center_y': .5} - size_hint_y: None - height: self.minimum_height - orientation: 'vertical' - spacing: '12dp' - SeedButton: - text: root.seed_text - SeedLabel: - text: root.message -''') - - - -class WizardDialog(EventsDialog): - ''' Abstract dialog to be used as the base for all Create Account Dialogs - ''' - crcontent = ObjectProperty(None) - - def __init__(self, **kwargs): - super(WizardDialog, self).__init__(**kwargs) - self.action = kwargs.get('action') - _trigger_size_dialog = Clock.create_trigger(self._size_dialog) - Window.bind(size=_trigger_size_dialog, - rotation=_trigger_size_dialog) - _trigger_size_dialog() - - def _size_dialog(self, dt): - app = App.get_running_app() - if app.ui_mode[0] == 'p': - self.size = Window.size - else: - #tablet - if app.orientation[0] == 'p': - #portrait - self.size = Window.size[0]/1.67, Window.size[1]/1.4 - else: - self.size = Window.size[0]/2.5, Window.size[1] - - def add_widget(self, widget, index=0): - if not self.crcontent: - super(WizardDialog, self).add_widget(widget) - else: - self.crcontent.add_widget(widget, index=index) - - def on_dismiss(self): - app = App.get_running_app() - if app.wallet is None and self._on_release is not None: - app.stop() - - -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: - app = App.get_running_app() - self._back = _back = partial(app.dispatch, 'on_back') - - -class ShowSeedDialog(WizardDialog): - - seed_text = StringProperty('') - message = StringProperty('') - - def on_parent(self, instance, value): - if value: - app = App.get_running_app() - self._back = _back = partial(self.ids.back.dispatch, 'on_release') - - -class WordButton(Button): - pass - -class WizardButton(Button): - pass - -class RestoreSeedDialog(WizardDialog): - - message = StringProperty('') - - def __init__(self, **kwargs): - super(RestoreSeedDialog, self).__init__(**kwargs) - self._test = kwargs['test'] - 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: - if w.startswith(prefix): - yield w - - def on_text(self, dt): - self.ids.next.disabled = not bool(self._test(self.get_text())) - - text = self.ids.text_input_seed.text - if not text: - last_word = '' - elif text[-1] == ' ': - last_word = '' - else: - last_word = text.split(' ')[-1] - - enable_space = False - self.ids.suggestions.clear_widgets() - suggestions = [x for x in self.get_suggestions(last_word)] - if suggestions and len(suggestions) < 10: - for w in suggestions: - if w == last_word: - enable_space = True - else: - b = WordButton(text=w) - self.ids.suggestions.add_widget(b) - - i = len(last_word) - p = set() - for x in suggestions: - if len(x)>i: p.add(x[i]) - - for line in [self.ids.line1, self.ids.line2, self.ids.line3]: - for c in line.children: - if isinstance(c, Button): - if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': - c.disabled = (c.text.lower() not in p) and last_word - elif c.text == ' ': - c.disabled = not enable_space - - def on_word(self, w): - text = self.get_text() - words = text.split(' ') - words[-1] = w - text = ' '.join(words) - self.ids.text_input_seed.text = text + ' ' - self.ids.suggestions.clear_widgets() - - def get_text(self): - ti = self.ids.text_input_seed - text = unicode(ti.text).strip() - text = ' '.join(text.split()) - return text - - def update_text(self, c): - c = c.lower() - text = self.ids.text_input_seed.text - if c == '<': - text = text[:-1] - else: - text += c - self.ids.text_input_seed.text = text - - def on_parent(self, instance, value): - if value: - tis = self.ids.text_input_seed - tis.focus = True - #tis._keyboard.bind(on_key_down=self.on_key_down) - self._back = _back = partial(self.ids.back.dispatch, - 'on_release') - app = App.get_running_app() - - def on_key_down(self, keyboard, keycode, key, modifiers): - if keycode[0] in (13, 271): - self.on_enter() - return True - - def on_enter(self): - #self._remove_keyboard() - # press next - next = self.ids.next - if not next.disabled: - next.dispatch('on_release') - - def _remove_keyboard(self): - tis = self.ids.text_input_seed - if tis._keyboard: - tis._keyboard.unbind(on_key_down=self.on_key_down) - tis.focus = False - - -class ShowXpubDialog(WizardDialog): - - def __init__(self, **kwargs): - WizardDialog.__init__(self, **kwargs) - self.app = App.get_running_app() - self.xpub = kwargs['xpub'] - self.ids.next.disabled = False - - def do_copy(self): - self.app._clipboard.copy(self.xpub) - - def do_share(self): - self.app.do_share(self.xpub, _("Master Public Key")) - - def do_qr(self): - from qr_dialog import QRDialog - popup = QRDialog(_("Master Public Key"), self.xpub, True) - popup.open() - - -class AddXpubDialog(WizardDialog): - - def __init__(self, **kwargs): - WizardDialog.__init__(self, **kwargs) - self.app = App.get_running_app() - self._test = kwargs['test'] - self.title = kwargs['title'] - self.message = kwargs['message'] - - def check_text(self, dt): - self.ids.next.disabled = not bool(self._test(self.get_text())) - - def get_text(self): - ti = self.ids.text_input - return unicode(ti.text).strip() - - def scan_xpub(self): - def on_complete(text): - self.ids.text_input.text = text - self.app.scan_qr(on_complete) - - def do_paste(self): - 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 = '' DIR diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py t@@ -1,30 +1,675 @@ -import os -from electrum.wallet import Wallet, Multisig_Wallet -from electrum_gui.kivy.i18n import _ + +from functools import partial +import threading from kivy.app import App +from kivy.clock import Clock +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 kivy.uix.widget import Widget from kivy.core.window import Window from kivy.clock import Clock -from kivy.factory import Factory -import sys -import threading -from functools import partial -import weakref - -from create_restore import WizardChoiceDialog, ShowSeedDialog, RestoreSeedDialog, AddXpubDialog, ShowXpubDialog, WizardMultisigDialog -from password_dialog import PasswordDialog +from electrum_gui.kivy.uix.dialogs import EventsDialog +from electrum_gui.kivy.i18n import _ # global Variables app = App.get_running_app() +from password_dialog import PasswordDialog + +from electrum.base_wizard import BaseWizard + + + +Builder.load_string(''' +#:import Window kivy.core.window.Window +#:import _ electrum_gui.kivy.i18n._ + + +<WizardTextInput@TextInput> + border: 4, 4, 4, 4 + font_size: '15sp' + padding: '15dp', '15dp' + background_color: (1, 1, 1, 1) if self.focus else (0.454, 0.698, 0.909, 1) + foreground_color: (0.31, 0.31, 0.31, 1) if self.focus else (0.835, 0.909, 0.972, 1) + hint_text_color: self.foreground_color + background_active: 'atlas://gui/kivy/theming/light/create_act_text_active' + background_normal: 'atlas://gui/kivy/theming/light/create_act_text_active' + size_hint_y: None + height: '48sp' + +<WizardButton@Button>: + root: None + size_hint: 1, None + height: '48sp' + on_press: if self.root: self.root.dispatch('on_press', self) + on_release: if self.root: self.root.dispatch('on_release', self) + +<BigLabel@Label> + color: .854, .925, .984, 1 + size_hint: 1, None + text_size: self.width, None + height: self.texture_size[1] + bold: True + +<-WizardDialog> + text_color: .854, .925, .984, 1 + value: '' + #auto_dismiss: False + size_hint: None, None + canvas.before: + Color: + rgba: 0, 0, 0, .9 + Rectangle: + size: Window.size + Color: + rgba: .239, .588, .882, 1 + Rectangle: + size: Window.size + + crcontent: crcontent + # add electrum icon + BoxLayout: + orientation: 'vertical' if self.width < self.height else 'horizontal' + padding: + min(dp(27), self.width/32), min(dp(27), self.height/32),\ + min(dp(27), self.width/32), min(dp(27), self.height/32) + spacing: '10dp' + GridLayout: + id: grid_logo + cols: 1 + pos_hint: {'center_y': .5} + size_hint: 1, None + height: self.minimum_height + Label: + color: root.text_color + text: 'ELECTRUM' + size_hint: 1, None + height: self.texture_size[1] if self.opacity else 0 + font_size: '33sp' + font_name: 'gui/kivy/data/fonts/tron/Tr2n.ttf' + GridLayout: + cols: 1 + id: crcontent + 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 == '' + + +<WizardMultisigDialog> + value: 'next' + Widget + size_hint: 1, 1 + Label: + color: root.text_color + size_hint: 1, None + 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: + color: root.text_color + size_hint: 1, None + text_size: self.width, None + height: self.texture_size[1] + text: root.msg + Widget + size_hint: 1, 1 + GridLayout: + row_default_height: '48dp' + orientation: 'vertical' + id: choices + cols: 1 + spacing: '14dp' + size_hint: 1, None + +<MButton@Button>: + size_hint: 1, None + height: '33dp' + on_release: + self.parent.update_amount(self.text) + +<WordButton@Button>: + size_hint: None, None + padding: '5dp', '5dp' + text_size: None, self.height + width: self.texture_size[0] + height: '30dp' + on_release: + self.parent.new_word(self.text) + + +<SeedButton@Button>: + height: dp(100) + border: 4, 4, 4, 4 + halign: 'justify' + valign: 'top' + font_size: '18dp' + text_size: self.width - dp(24), self.height - dp(12) + color: .1, .1, .1, 1 + background_normal: 'atlas://gui/kivy/theming/light/white_bg_round_top' + background_down: self.background_normal + size_hint_y: None + + +<SeedLabel@Label>: + font_size: '12sp' + text_size: self.width, None + size_hint: 1, None + height: self.texture_size[1] + halign: 'justify' + valign: 'middle' + border: 4, 4, 4, 4 + + +<RestoreSeedDialog> + word: '' + BigLabel: + text: "ENTER YOUR SEED PHRASE" + GridLayout + cols: 1 + padding: 0, '12dp' + orientation: 'vertical' + spacing: '12dp' + size_hint: 1, None + height: self.minimum_height + SeedButton: + id: text_input_seed + text: '' + on_text: Clock.schedule_once(root.on_text) + SeedLabel: + text: root.message + BoxLayout: + id: suggestions + height: '35dp' + size_hint: 1, None + new_word: root.on_word + BoxLayout: + id: line1 + update_amount: root.update_text + size_hint: 1, None + height: '30dp' + MButton: + text: 'Q' + MButton: + text: 'W' + MButton: + text: 'E' + MButton: + text: 'R' + MButton: + text: 'T' + MButton: + text: 'Y' + MButton: + text: 'U' + MButton: + text: 'I' + MButton: + text: 'O' + MButton: + text: 'P' + BoxLayout: + id: line2 + update_amount: root.update_text + size_hint: 1, None + height: '30dp' + Widget: + size_hint: 0.5, None + height: '33dp' + MButton: + text: 'A' + MButton: + text: 'S' + MButton: + text: 'D' + MButton: + text: 'F' + MButton: + text: 'G' + MButton: + text: 'H' + MButton: + text: 'J' + MButton: + text: 'K' + MButton: + text: 'L' + Widget: + size_hint: 0.5, None + height: '33dp' + BoxLayout: + id: line3 + update_amount: root.update_text + size_hint: 1, None + height: '30dp' + Widget: + size_hint: 1, None + MButton: + text: 'Z' + MButton: + text: 'X' + MButton: + text: 'C' + MButton: + text: 'V' + MButton: + text: 'B' + MButton: + text: 'N' + MButton: + text: 'M' + MButton: + text: ' ' + MButton: + text: '<' + +<AddXpubDialog> + title: '' + message: '' + BigLabel: + text: root.title + GridLayout + cols: 1 + padding: 0, '12dp' + orientation: 'vertical' + spacing: '12dp' + size_hint: 1, None + height: self.minimum_height + SeedButton: + id: text_input + text: '' + on_text: Clock.schedule_once(root.check_text) + SeedLabel: + text: root.message + GridLayout + rows: 1 + spacing: '12dp' + size_hint: 1, None + height: self.minimum_height + IconButton: + id: scan + height: '48sp' + on_release: root.scan_xpub() + icon: 'atlas://gui/kivy/theming/light/camera' + size_hint: 1, None + WizardButton: + text: _('Paste') + on_release: root.do_paste() + WizardButton: + text: _('Clear') + on_release: root.do_clear() + + +<ShowXpubDialog> + xpub: '' + message: _('Here is your master public key. Share it with your cosigners.') + BigLabel: + text: "MASTER PUBLIC KEY" + GridLayout + cols: 1 + padding: 0, '12dp' + orientation: 'vertical' + spacing: '12dp' + size_hint: 1, None + height: self.minimum_height + SeedButton: + id: text_input + text: root.xpub + SeedLabel: + text: root.message + GridLayout + rows: 1 + spacing: '12dp' + size_hint: 1, None + height: self.minimum_height + WizardButton: + text: _('QR code') + on_release: root.do_qr() + WizardButton: + text: _('Copy') + on_release: root.do_copy() + WizardButton: + text: _('Share') + on_release: root.do_share() + + +<ShowSeedDialog> + spacing: '12dp' + value: 'next' + BigLabel: + text: "PLEASE WRITE DOWN YOUR SEED PHRASE" + GridLayout: + id: grid + cols: 1 + pos_hint: {'center_y': .5} + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + spacing: '12dp' + SeedButton: + text: root.seed_text + SeedLabel: + text: root.message +''') + + + +class WizardDialog(EventsDialog): + ''' Abstract dialog to be used as the base for all Create Account Dialogs + ''' + crcontent = ObjectProperty(None) + + def __init__(self, **kwargs): + super(WizardDialog, self).__init__(**kwargs) + #self.action = kwargs.get('action') + 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) + _trigger_size_dialog() + self._on_release = False + + def _size_dialog(self, dt): + app = App.get_running_app() + if app.ui_mode[0] == 'p': + self.size = Window.size + else: + #tablet + if app.orientation[0] == 'p': + #portrait + self.size = Window.size[0]/1.67, Window.size[1]/1.4 + else: + self.size = Window.size[0]/2.5, Window.size[1] + + def add_widget(self, widget, index=0): + if not self.crcontent: + super(WizardDialog, self).add_widget(widget) + else: + self.crcontent.add_widget(widget, index=index) + + def on_dismiss(self): + app = App.get_running_app() + if app.wallet is None and not self._on_release: + app.stop() + + def get_params(self, button): + return () + + def on_release(self, button): + self._on_release = True + self.close() + if not button: + self.parent.dispatch('on_wizard_complete', None) + return + if button is self.ids.back: + self.run_prev() + return + params = self.get_params(button) + self.run_next(*params) + + +class WizardMultisigDialog(WizardDialog): + + def get_params(self, button): + m = self.ids.m.value + n = self.ids.n.value + return m, n + +class WizardChoiceDialog(WizardDialog): + + 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: + app = App.get_running_app() + self._back = _back = partial(app.dispatch, 'on_back') + + def get_params(self, button): + return (button.action,) + +class ShowSeedDialog(WizardDialog): + + seed_text = StringProperty('') + message = StringProperty('') + + def on_parent(self, instance, value): + if value: + app = App.get_running_app() + self._back = _back = partial(self.ids.back.dispatch, 'on_release') + + def get_params(self, b): + return(self.seed_text,) + + +class WordButton(Button): + pass + +class WizardButton(Button): + pass -class InstallWizard(Widget): - '''Installation Wizard. Responsible for instantiating the - creation/restoration of wallets. +class RestoreSeedDialog(WizardDialog): + message = StringProperty('') + + def __init__(self, **kwargs): + super(RestoreSeedDialog, self).__init__(**kwargs) + self._test = kwargs['test'] + 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 = '' + + def get_suggestions(self, prefix): + for w in self.words: + if w.startswith(prefix): + yield w + + def on_text(self, dt): + self.ids.next.disabled = not bool(self._test(self.get_text())) + + text = self.ids.text_input_seed.text + if not text: + last_word = '' + elif text[-1] == ' ': + last_word = '' + else: + last_word = text.split(' ')[-1] + + enable_space = False + self.ids.suggestions.clear_widgets() + suggestions = [x for x in self.get_suggestions(last_word)] + if suggestions and len(suggestions) < 10: + for w in suggestions: + if w == last_word: + enable_space = True + else: + b = WordButton(text=w) + self.ids.suggestions.add_widget(b) + + i = len(last_word) + p = set() + for x in suggestions: + if len(x)>i: p.add(x[i]) + + for line in [self.ids.line1, self.ids.line2, self.ids.line3]: + for c in line.children: + if isinstance(c, Button): + if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': + c.disabled = (c.text.lower() not in p) and last_word + elif c.text == ' ': + c.disabled = not enable_space + + def on_word(self, w): + text = self.get_text() + words = text.split(' ') + words[-1] = w + text = ' '.join(words) + self.ids.text_input_seed.text = text + ' ' + self.ids.suggestions.clear_widgets() + + def get_text(self): + ti = self.ids.text_input_seed + text = unicode(ti.text).strip() + text = ' '.join(text.split()) + return text + + def get_params(self, b): + return (self.get_text(),) + + def update_text(self, c): + c = c.lower() + text = self.ids.text_input_seed.text + if c == '<': + text = text[:-1] + else: + text += c + self.ids.text_input_seed.text = text + + def on_parent(self, instance, value): + if value: + tis = self.ids.text_input_seed + tis.focus = True + #tis._keyboard.bind(on_key_down=self.on_key_down) + self._back = _back = partial(self.ids.back.dispatch, + 'on_release') + app = App.get_running_app() + + def on_key_down(self, keyboard, keycode, key, modifiers): + if keycode[0] in (13, 271): + self.on_enter() + return True + + def on_enter(self): + #self._remove_keyboard() + # press next + next = self.ids.next + if not next.disabled: + next.dispatch('on_release') + + def _remove_keyboard(self): + tis = self.ids.text_input_seed + if tis._keyboard: + tis._keyboard.unbind(on_key_down=self.on_key_down) + tis.focus = False + + +class ShowXpubDialog(WizardDialog): + + def __init__(self, **kwargs): + WizardDialog.__init__(self, **kwargs) + self.app = App.get_running_app() + self.xpub = kwargs['xpub'] + self.ids.next.disabled = False + + def do_copy(self): + self.app._clipboard.copy(self.xpub) + + def do_share(self): + self.app.do_share(self.xpub, _("Master Public Key")) + + def do_qr(self): + from qr_dialog import QRDialog + popup = QRDialog(_("Master Public Key"), self.xpub, True) + popup.open() + + +class AddXpubDialog(WizardDialog): + + def __init__(self, **kwargs): + WizardDialog.__init__(self, **kwargs) + self.app = App.get_running_app() + self._test = kwargs['test'] + self.title = kwargs['title'] + self.message = kwargs['message'] + + def check_text(self, dt): + self.ids.next.disabled = not bool(self._test(self.get_text())) + + def get_text(self): + ti = self.ids.text_input + return unicode(ti.text).strip() + + def get_params(self, button): + return (self.get_text(),) + + def scan_xpub(self): + def on_complete(text): + self.ids.text_input.text = text + self.app.scan_qr(on_complete) + + def do_paste(self): + self.ids.text_input.text = unicode(self.app._clipboard.paste()) + + def do_clear(self): + self.ids.text_input.text = '' + + + + + +class InstallWizard(BaseWizard, Widget): + ''' events:: `on_wizard_complete` Fired when the wizard is done creating/ restoring wallet/s. t@@ -32,12 +677,9 @@ class InstallWizard(Widget): __events__ = ('on_wizard_complete', ) - def __init__(self, config, network, storage): - super(InstallWizard, self).__init__() - self.config = config - self.network = network - self.storage = storage - self.wallet = None + def on_wizard_complete(self, wallet): + """overriden by main_window""" + pass def waiting_dialog(self, task, msg, on_complete=None): '''Perform a blocking task in the background by running the passed t@@ -60,189 +702,17 @@ class InstallWizard(Widget): t = threading.Thread(target = target) t.start() - def run(self, action, *args): - '''Entry point of our Installation wizard''' - if not action: - return - if hasattr(self, action): - f = getattr(self, action) - apply(f, *args) - 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() - 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): - _dlg.close() - if btn is _dlg.ids.back: - self.run('new') - return - text = _dlg.get_text() - self.run('enter_pin', (text,)) - msg = _('Please type your seed phrase using the virtual keyboard.') - RestoreSeedDialog(test=Wallet.is_seed, message=msg, on_release=on_seed).open() - - def restore_xpub(self): - def on_xpub(_dlg, btn): - _dlg.close() - if btn is _dlg.ids.back: - self.run('new') - return - text = _dlg.get_text() - self.run('create_wallet', (text, None)) - title = "MASTER PUBLIC KEY" - message = _('To create a watching-only wallet, paste your master public key, or scan it using the camera button.') - AddXpubDialog(title=title, message=message, test=Wallet.is_mpk, on_release=on_xpub).open() - - 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_release(_dlg, btn): - _dlg.close() - self.run('add_cosigner') - xpub = self.wallet.master_public_keys.get('x1/') - ShowXpubDialog(xpub=xpub, test=Wallet.is_xpub, on_release=on_release).open() - - def add_cosigner(self): - def on_xpub(_dlg, btn): - xpub = _dlg.get_text() - _dlg.close() - if btn is _dlg.ids.back: - self.run('add_cosigners') - return - if xpub: - self.wallet.add_master_public_key("x%d/" % 2, xpub) - action = self.wallet.get_action() - if action == 'add_cosigners': action = 'add_cosigner' - self.run(action) - - title = "ADD COSIGNERS" - message = _('Please paste your cosigners master public key, or scan it using the camera button.') - AddXpubDialog(title=title, message=message, test=Wallet.is_xpub, 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, on_complete=self.terminate) - - 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.next: - self.run('confirm_seed', (seed,)) - else: - self.run('new') - ShowSeedDialog(message=msg, seed_text=seed, on_release=on_ok).open() - - def confirm_seed(self, seed): - assert Wallet.is_seed(seed) - def on_seed(_dlg, btn): - if btn is _dlg.ids.back: - _dlg.close() - self.run('create_seed') - return - _dlg.close() - self.run('enter_pin', (seed,)) - msg = _('Please retype your seed phrase, to confirm that you properly saved it') - RestoreSeedDialog(test=lambda x: x==seed, message=msg, on_release=on_seed).open() - - def enter_pin(self, seed): - def callback(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() + 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 confirm_pin(self, seed, pin): - def callback(conf): - if conf == pin: - self.run('create_wallet', (seed, pin)) - else: - app.show_error(_('PIN mismatch'), duration=.5) - self.run('enter_pin', (seed,)) + def password_dialog(self, message, callback): popup = PasswordDialog() - popup.init('Confirm your PIN code', callback) + popup.init(message, callback) popup.open() - def terminate(self): - self.wallet.start_threads(self.network) - self.dispatch('on_wizard_complete', self.wallet) - - def on_wizard_complete(self, wallet): - """overriden by main_window""" - pass + def show_error(self, msg): + app.show_error(msg, duration=0.5) DIR diff --git a/lib/base_wizard.py b/lib/base_wizard.py t@@ -0,0 +1,167 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2016 Thomas Voegtlin +# +# 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. + +import os +from electrum.wallet import Wallet, Multisig_Wallet +from electrum_gui.kivy.i18n import _ + + +class BaseWizard(object): + + def __init__(self, config, network, storage): + super(BaseWizard, self).__init__() + self.config = config + self.network = network + self.storage = storage + self.wallet = None + + def run(self, action, *args): + '''Entry point of our Installation wizard''' + if not action: + return + if hasattr(self, action): + f = getattr(self, action) + apply(f, *args) + else: + raise BaseException("unknown action", action) + + 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'), + ] + self.choice_dialog(msg=msg, choices=choices, run_prev=self.cancel, run_next=self.run) + + 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) + + 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) + + 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') + + 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): + 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) + + 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' + self.run(action) + title = "ADD COSIGNER" + 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') + + 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, on_complete=self.terminate) + + 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) + + def confirm_seed(self, seed): + assert Wallet.is_seed(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 + + + DIR diff --git a/lib/wallet.py b/lib/wallet.py t@@ -1915,10 +1915,19 @@ class Multisig_Wallet(BIP32_RD_Wallet, Mnemonic): def get_master_public_keys(self): return self.master_public_keys - def get_action(self): + def get_missing_cosigner(self): for i in range(self.n): if self.master_public_keys.get("x%d/"%(i+1)) is None: - return 'create_seed' if i == 0 else 'add_cosigners' + return i+1 + + def add_cosigner(self, xpub): + i = self.get_missing_cosigner() + self.add_master_public_key("x%d/" % i, xpub) + + def get_action(self): + i = self.get_missing_cosigner() + if i is not None: + return 'create_seed' if i == 1 else 'add_cosigners' if not self.accounts: return 'create_main_account'