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.utils import platform from electrum_gui.kivy.uix.dialogs import EventsDialog from electrum_gui.kivy.i18n import _ from electrum.base_wizard import BaseWizard from password_dialog import PasswordDialog # global Variables app = App.get_running_app() is_test = (platform == "linux") 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 #:import _ electrum_gui.kivy.i18n._ 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' : 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) 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 == '' 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 message : '' 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.message Widget size_hint: 1, 1 GridLayout: row_default_height: '48dp' orientation: 'vertical' id: choices cols: 1 spacing: '14dp' size_hint: 1, None : size_hint: 1, None height: '33dp' on_release: self.parent.update_amount(self.text) : 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) : 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 : 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 message: '' 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) on_release: root.options_dialog() 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: '<' 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() 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() 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 on_release: root.options_dialog() SeedLabel: text: root.message BigLabel: text: root.title SeedLabel: text: root.message TextInput: id: passphrase_input multiline: False size_hint: 1, None height: '27dp' SeedLabel: text: root.warning ''') class WizardDialog(EventsDialog): ''' Abstract dialog to be used as the base for all Create Account Dialogs ''' crcontent = ObjectProperty(None) def __init__(self, wizard, **kwargs): super(WizardDialog, self).__init__(**kwargs) self.wizard = wizard self.ids.back.disabled = not wizard.can_go_back() self.app = App.get_running_app() self.run_next = kwargs['run_next'] _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 (None,) 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.wizard.go_back() 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, 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 action, text 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 LineDialog(WizardDialog): title = StringProperty('') message = StringProperty('') warning = StringProperty('') def __init__(self, wizard, **kwargs): WizardDialog.__init__(self, wizard, **kwargs) self.ids.next.disabled = False def get_params(self, b): return (self.ids.passphrase_input.text,) class ShowSeedDialog(WizardDialog): seed_text = StringProperty('') message = _("If you forget your PIN or lose your device, your seed phrase will be the only way to recover your funds.") ext = False def on_parent(self, instance, value): if value: app = App.get_running_app() self._back = _back = partial(self.ids.back.dispatch, 'on_release') def options_dialog(self): from seed_options import SeedOptionsDialog def callback(status): self.ext = status d = SeedOptionsDialog(self.ext, callback) d.open() def get_params(self, b): return (self.ext,) class WordButton(Button): pass class WizardButton(Button): pass class RestoreSeedDialog(WizardDialog): def __init__(self, wizard, **kwargs): super(RestoreSeedDialog, self).__init__(wizard, **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 '' self.message = _('Please type your seed phrase using the virtual keyboard.') self.title = _('Enter Seed') self.ext = False def options_dialog(self): from seed_options import SeedOptionsDialog def callback(status): self.ext = status d = SeedOptionsDialog(self.ext, callback) d.open() 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 last_word in suggestions: b = WordButton(text=last_word) self.ids.suggestions.add_widget(b) enable_space = True for w in suggestions: if w != last_word and len(suggestions) < 10: 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 def get_params(self, b): return (self.get_text(), False, self.ext) class ConfirmSeedDialog(RestoreSeedDialog): def get_params(self, b): return (self.get_text(),) def options_dialog(self): pass class ShowXpubDialog(WizardDialog): def __init__(self, wizard, **kwargs): WizardDialog.__init__(self, wizard, **kwargs) 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, 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.is_valid(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 = test_xpub if is_test else 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. ''' __events__ = ('on_wizard_complete', ) def on_wizard_complete(self, wallet): """overriden by main_window""" pass def waiting_dialog(self, task, msg): '''Perform a blocking task in the background by running the passed method in a thread. ''' def target(): # run your threaded function try: task() except Exception as err: self.show_error(str(err)) # on completion hide message Clock.schedule_once(lambda dt: app.info_bubble.hide(now=True), -1) app.show_info_bubble( text=msg, icon='atlas://gui/kivy/theming/light/important', pos=Window.center, width='200sp', arrow_pos=None, modal=True) t = threading.Thread(target = target) t.start() def terminate(self, **kwargs): self.dispatch('on_wizard_complete', self.wallet) def choice_dialog(self, **kwargs): choices = kwargs['choices'] if len(choices) > 1: WizardChoiceDialog(self, **kwargs).open() else: f = kwargs['run_next'] apply(f, (choices[0][0],)) def multisig_dialog(self, **kwargs): WizardMultisigDialog(self, **kwargs).open() def show_seed_dialog(self, **kwargs): ShowSeedDialog(self, **kwargs).open() def line_dialog(self, **kwargs): LineDialog(self, **kwargs).open() def confirm_seed_dialog(self, **kwargs): kwargs['title'] = _('Confirm Seed') kwargs['message'] = _('Please retype your seed phrase, to confirm that you properly saved it') ConfirmSeedDialog(self, **kwargs).open() def restore_seed_dialog(self, **kwargs): RestoreSeedDialog(self, **kwargs).open() def add_xpub_dialog(self, **kwargs): kwargs['message'] += ' ' + _('Use the camera button to scan a QR code.') AddXpubDialog(self, **kwargs).open() def add_cosigner_dialog(self, **kwargs): kwargs['title'] = _("Add Cosigner") + " %d"%kwargs['index'] kwargs['message'] = _('Please paste your cosigners master public key, or scan it using the camera button.') AddXpubDialog(self, **kwargs).open() def show_xpub_dialog(self, **kwargs): ShowXpubDialog(self, **kwargs).open() def show_error(self, msg): Clock.schedule_once(lambda dt: app.show_error(msg)) def password_dialog(self, message, callback): popup = PasswordDialog() popup.init(message, callback) popup.open() 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, False) 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()