tinstallwizard.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tinstallwizard.py (34286B)
---
1
2 from functools import partial
3 import threading
4 import os
5 from typing import TYPE_CHECKING
6
7 from kivy.app import App
8 from kivy.clock import Clock
9 from kivy.lang import Builder
10 from kivy.properties import ObjectProperty, StringProperty, OptionProperty
11 from kivy.core.window import Window
12 from kivy.uix.button import Button
13 from kivy.uix.togglebutton import ToggleButton
14 from kivy.utils import platform
15 from kivy.uix.widget import Widget
16 from kivy.core.window import Window
17 from kivy.clock import Clock
18 from kivy.utils import platform
19
20 from electrum.base_wizard import BaseWizard
21 from electrum.util import is_valid_email
22
23
24 from . import EventsDialog
25 from ...i18n import _
26 from .password_dialog import PasswordDialog
27
28 if TYPE_CHECKING:
29 from electrum.gui.kivy.main_window import ElectrumWindow
30
31
32 # global Variables
33
34 Builder.load_string('''
35 #:import Window kivy.core.window.Window
36 #:import _ electrum.gui.kivy.i18n._
37 #:import KIVY_GUI_PATH electrum.gui.kivy.KIVY_GUI_PATH
38
39
40 <WizardTextInput@TextInput>
41 border: 4, 4, 4, 4
42 font_size: '15sp'
43 padding: '15dp', '15dp'
44 background_color: (1, 1, 1, 1) if self.focus else (0.454, 0.698, 0.909, 1)
45 foreground_color: (0.31, 0.31, 0.31, 1) if self.focus else (0.835, 0.909, 0.972, 1)
46 hint_text_color: self.foreground_color
47 background_active: f'atlas://{KIVY_GUI_PATH}/theming/light/create_act_text_active'
48 background_normal: f'atlas://{KIVY_GUI_PATH}/theming/light/create_act_text_active'
49 size_hint_y: None
50 height: '48sp'
51
52 <WizardButton@Button>:
53 root: None
54 size_hint: 1, None
55 height: '48sp'
56 on_press: if self.root: self.root.dispatch('on_press', self)
57 on_release: if self.root: self.root.dispatch('on_release', self)
58
59 <BigLabel@Label>
60 color: .854, .925, .984, 1
61 size_hint: 1, None
62 text_size: self.width, None
63 height: self.texture_size[1]
64 bold: True
65
66 <-WizardDialog>
67 text_color: .854, .925, .984, 1
68 value: ''
69 #auto_dismiss: False
70 size_hint: None, None
71 canvas.before:
72 Color:
73 rgba: .239, .588, .882, 1
74 Rectangle:
75 size: Window.size
76
77 crcontent: crcontent
78 # add electrum icon
79 BoxLayout:
80 orientation: 'vertical' if self.width < self.height else 'horizontal'
81 padding:
82 min(dp(27), self.width/32), min(dp(27), self.height/32),\
83 min(dp(27), self.width/32), min(dp(27), self.height/32)
84 spacing: '10dp'
85 GridLayout:
86 id: grid_logo
87 cols: 1
88 pos_hint: {'center_y': .5}
89 size_hint: 1, None
90 height: self.minimum_height
91 Label:
92 color: root.text_color
93 text: 'ELECTRUM'
94 size_hint: 1, None
95 height: self.texture_size[1] if self.opacity else 0
96 font_size: '33sp'
97 font_name: f'{KIVY_GUI_PATH}/data/fonts/tron/Tr2n.ttf'
98 GridLayout:
99 cols: 1
100 id: crcontent
101 spacing: '1dp'
102 Widget:
103 size_hint: 1, 0.3
104 GridLayout:
105 rows: 1
106 spacing: '12dp'
107 size_hint: 1, None
108 height: self.minimum_height
109 WizardButton:
110 id: back
111 text: _('Back')
112 root: root
113 WizardButton:
114 id: next
115 text: _('Next')
116 root: root
117 disabled: root.value == ''
118
119
120 <WizardMultisigDialog>
121 value: 'next'
122 Widget
123 size_hint: 1, 1
124 Label:
125 color: root.text_color
126 size_hint: 1, None
127 text_size: self.width, None
128 height: self.texture_size[1]
129 text: _("Choose the number of signatures needed to unlock funds in your wallet")
130 Widget
131 size_hint: 1, 1
132 GridLayout:
133 cols: 2
134 spacing: '14dp'
135 size_hint: 1, 1
136 height: self.minimum_height
137 Label:
138 color: root.text_color
139 text: _('From {} cosigners').format(n.value)
140 Slider:
141 id: n
142 range: 2, 5
143 step: 1
144 value: 2
145 Label:
146 color: root.text_color
147 text: _('Require {} signatures').format(m.value)
148 Slider:
149 id: m
150 range: 1, n.value
151 step: 1
152 value: 2
153 Widget
154 size_hint: 1, 1
155 Label:
156 id: backup_warning_label
157 color: root.text_color
158 size_hint: 1, None
159 text_size: self.width, None
160 height: self.texture_size[1]
161 opacity: int(m.value != n.value)
162 text: _("Warning: to be able to restore a multisig wallet, " \
163 "you should include the master public key for each cosigner " \
164 "in all of your backups.")
165
166
167 <WizardChoiceDialog>
168 message : ''
169 Widget:
170 size_hint: 1, 1
171 Label:
172 color: root.text_color
173 size_hint: 1, None
174 text_size: self.width, None
175 height: self.texture_size[1]
176 text: root.message
177 Widget
178 size_hint: 1, 1
179 GridLayout:
180 row_default_height: '48dp'
181 id: choices
182 cols: 1
183 spacing: '14dp'
184 size_hint: 1, None
185
186 <WizardConfirmDialog>
187 message : ''
188 Widget:
189 size_hint: 1, 1
190 Label:
191 color: root.text_color
192 size_hint: 1, None
193 text_size: self.width, None
194 height: self.texture_size[1]
195 text: root.message
196 Widget
197 size_hint: 1, 1
198
199 <WizardTOSDialog>
200 message : ''
201 size_hint: 1, 1
202 ScrollView:
203 size_hint: 1, 1
204 TextInput:
205 color: root.text_color
206 size_hint: 1, None
207 text_size: self.width, None
208 height: self.minimum_height
209 text: root.message
210 disabled: True
211
212 <WizardEmailDialog>
213 Label:
214 color: root.text_color
215 size_hint: 1, None
216 text_size: self.width, None
217 height: self.texture_size[1]
218 text: 'Please enter your email address'
219 WizardTextInput:
220 id: email
221 on_text: Clock.schedule_once(root.on_text)
222 multiline: False
223 on_text_validate: Clock.schedule_once(root.on_enter)
224
225 <WizardKnownOTPDialog>
226 message : ''
227 message2: ''
228 Widget:
229 size_hint: 1, 1
230 Label:
231 color: root.text_color
232 size_hint: 1, None
233 text_size: self.width, None
234 height: self.texture_size[1]
235 text: root.message
236 Widget
237 size_hint: 1, 1
238 WizardTextInput:
239 id: otp
240 on_text: Clock.schedule_once(root.on_text)
241 multiline: False
242 on_text_validate: Clock.schedule_once(root.on_enter)
243 Widget
244 size_hint: 1, 1
245 Label:
246 color: root.text_color
247 size_hint: 1, None
248 text_size: self.width, None
249 height: self.texture_size[1]
250 text: root.message2
251 Widget
252 size_hint: 1, 1
253 height: '48sp'
254 BoxLayout:
255 orientation: 'horizontal'
256 WizardButton:
257 id: cb
258 text: _('Request new secret')
259 on_release: root.request_new_secret()
260 size_hint: 1, None
261 WizardButton:
262 id: abort
263 text: _('Abort creation')
264 on_release: root.abort_wallet_creation()
265 size_hint: 1, None
266
267
268 <WizardNewOTPDialog>
269 message : ''
270 message2 : ''
271 Label:
272 color: root.text_color
273 size_hint: 1, None
274 text_size: self.width, None
275 height: self.texture_size[1]
276 text: root.message
277 QRCodeWidget:
278 id: qr
279 size_hint: 1, 1
280 Label:
281 color: root.text_color
282 size_hint: 1, None
283 text_size: self.width, None
284 height: self.texture_size[1]
285 text: root.message2
286 WizardTextInput:
287 id: otp
288 on_text: Clock.schedule_once(root.on_text)
289 multiline: False
290 on_text_validate: Clock.schedule_once(root.on_enter)
291
292 <MButton@Button>:
293 size_hint: 1, None
294 height: '33dp'
295 on_release:
296 self.parent.update_amount(self.text)
297
298 <WordButton@Button>:
299 size_hint: None, None
300 padding: '5dp', '5dp'
301 text_size: None, self.height
302 width: self.texture_size[0]
303 height: '30dp'
304 on_release:
305 if self.parent: self.parent.new_word(self.text)
306
307
308 <SeedButton@Button>:
309 height: dp(100)
310 border: 4, 4, 4, 4
311 halign: 'justify'
312 valign: 'top'
313 font_size: '18dp'
314 text_size: self.width - dp(24), self.height - dp(12)
315 color: .1, .1, .1, 1
316 background_normal: f'atlas://{KIVY_GUI_PATH}/theming/light/white_bg_round_top'
317 background_down: self.background_normal
318 size_hint_y: None
319
320
321 <SeedLabel@Label>:
322 font_size: '12sp'
323 text_size: self.width, None
324 size_hint: 1, None
325 height: self.texture_size[1]
326 halign: 'justify'
327 valign: 'middle'
328 border: 4, 4, 4, 4
329
330 <SeedDialogHeader@GridLayout>
331 text: ''
332 options_dialog: None
333 rows: 1
334 size_hint: 1, None
335 height: self.minimum_height
336 BigLabel:
337 size_hint: 9, None
338 text: root.text
339 IconButton:
340 id: options_button
341 height: '30dp'
342 width: '30dp'
343 size_hint: 1, None
344 icon: f'atlas://{KIVY_GUI_PATH}/theming/light/gear'
345 on_release:
346 root.options_dialog() if root.options_dialog else None
347
348 <RestoreSeedDialog>
349 message: ''
350 word: ''
351 SeedDialogHeader:
352 id: seed_dialog_header
353 text: 'ENTER YOUR SEED PHRASE'
354 options_dialog: root.options_dialog
355 GridLayout:
356 cols: 1
357 padding: 0, '12dp'
358 spacing: '12dp'
359 size_hint: 1, None
360 height: self.minimum_height
361 SeedButton:
362 id: text_input_seed
363 text: ''
364 on_text: Clock.schedule_once(root.on_text)
365 SeedLabel:
366 text: root.message
367 BoxLayout:
368 id: suggestions
369 height: '35dp'
370 size_hint: 1, None
371 new_word: root.on_word
372 BoxLayout:
373 id: line1
374 update_amount: root.update_text
375 size_hint: 1, None
376 height: '30dp'
377 MButton:
378 text: 'Q'
379 MButton:
380 text: 'W'
381 MButton:
382 text: 'E'
383 MButton:
384 text: 'R'
385 MButton:
386 text: 'T'
387 MButton:
388 text: 'Y'
389 MButton:
390 text: 'U'
391 MButton:
392 text: 'I'
393 MButton:
394 text: 'O'
395 MButton:
396 text: 'P'
397 BoxLayout:
398 id: line2
399 update_amount: root.update_text
400 size_hint: 1, None
401 height: '30dp'
402 Widget:
403 size_hint: 0.5, None
404 height: '33dp'
405 MButton:
406 text: 'A'
407 MButton:
408 text: 'S'
409 MButton:
410 text: 'D'
411 MButton:
412 text: 'F'
413 MButton:
414 text: 'G'
415 MButton:
416 text: 'H'
417 MButton:
418 text: 'J'
419 MButton:
420 text: 'K'
421 MButton:
422 text: 'L'
423 Widget:
424 size_hint: 0.5, None
425 height: '33dp'
426 BoxLayout:
427 id: line3
428 update_amount: root.update_text
429 size_hint: 1, None
430 height: '30dp'
431 Widget:
432 size_hint: 1, None
433 MButton:
434 text: 'Z'
435 MButton:
436 text: 'X'
437 MButton:
438 text: 'C'
439 MButton:
440 text: 'V'
441 MButton:
442 text: 'B'
443 MButton:
444 text: 'N'
445 MButton:
446 text: 'M'
447 MButton:
448 text: ' '
449 MButton:
450 text: '<'
451
452 <AddXpubDialog>
453 title: ''
454 message: ''
455 BigLabel:
456 text: root.title
457 GridLayout
458 cols: 1
459 padding: 0, '12dp'
460 spacing: '12dp'
461 size_hint: 1, None
462 height: self.minimum_height
463 SeedButton:
464 id: text_input
465 text: ''
466 on_text: Clock.schedule_once(root.check_text)
467 SeedLabel:
468 text: root.message
469 GridLayout
470 rows: 1
471 spacing: '12dp'
472 size_hint: 1, None
473 height: self.minimum_height
474 IconButton:
475 id: scan
476 height: '48sp'
477 on_release: root.scan_xpub()
478 icon: f'atlas://{KIVY_GUI_PATH}/theming/light/camera'
479 size_hint: 1, None
480 WizardButton:
481 text: _('Paste')
482 on_release: root.do_paste()
483 WizardButton:
484 text: _('Clear')
485 on_release: root.do_clear()
486
487
488 <ShowXpubDialog>
489 xpub: ''
490 message: _('Here is your master public key. Share it with your cosigners.')
491 BigLabel:
492 text: "MASTER PUBLIC KEY"
493 GridLayout
494 cols: 1
495 padding: 0, '12dp'
496 spacing: '12dp'
497 size_hint: 1, None
498 height: self.minimum_height
499 SeedButton:
500 id: text_input
501 text: root.xpub
502 SeedLabel:
503 text: root.message
504 GridLayout
505 rows: 1
506 spacing: '12dp'
507 size_hint: 1, None
508 height: self.minimum_height
509 WizardButton:
510 text: _('QR code')
511 on_release: root.do_qr()
512 WizardButton:
513 text: _('Copy')
514 on_release: root.do_copy()
515 WizardButton:
516 text: _('Share')
517 on_release: root.do_share()
518
519 <ShowSeedDialog>
520 spacing: '12dp'
521 value: 'next'
522 SeedDialogHeader:
523 text: "PLEASE WRITE DOWN YOUR SEED PHRASE"
524 options_dialog: root.options_dialog
525 GridLayout:
526 id: grid
527 cols: 1
528 pos_hint: {'center_y': .5}
529 size_hint_y: None
530 height: self.minimum_height
531 spacing: '12dp'
532 SeedButton:
533 text: root.seed_text
534 SeedLabel:
535 text: root.message
536
537 <LineDialog>
538 BigLabel:
539 text: root.title
540 SeedLabel:
541 text: root.message
542 TextInput:
543 id: passphrase_input
544 multiline: False
545 size_hint: 1, None
546 height: '48dp'
547 on_text: Clock.schedule_once(root.on_text)
548 SeedLabel:
549 text: root.warning
550
551 <ChoiceLineDialog>
552 BigLabel:
553 text: root.title
554 SeedLabel:
555 text: root.message1
556 GridLayout:
557 row_default_height: '48dp'
558 id: choices
559 cols: 1
560 spacing: '14dp'
561 size_hint: 1, None
562 SeedLabel:
563 text: root.message2
564 TextInput:
565 id: text_input
566 multiline: False
567 size_hint: 1, None
568 height: '48dp'
569
570 ''')
571
572
573
574 class WizardDialog(EventsDialog):
575 ''' Abstract dialog to be used as the base for all Create Account Dialogs
576 '''
577 crcontent = ObjectProperty(None)
578
579 def __init__(self, wizard, **kwargs):
580 self.auto_dismiss = False
581 super(WizardDialog, self).__init__()
582 self.wizard = wizard
583 self.ids.back.disabled = not wizard.can_go_back()
584 self.app = App.get_running_app()
585 self.run_next = kwargs['run_next']
586
587 self._trigger_size_dialog = Clock.create_trigger(self._size_dialog, -1)
588 # note: everything bound here needs to be unbound as otherwise the
589 # objects will be kept around and keep receiving the callbacks
590 Window.bind(size=self._trigger_size_dialog,
591 rotation=self._trigger_size_dialog,
592 on_keyboard=self.on_keyboard)
593 self._trigger_size_dialog()
594 self._on_release = False
595
596 def _size_dialog(self, dt):
597 if self.app.ui_mode[0] == 'p':
598 self.size = Window.size
599 else:
600 #tablet
601 if self.app.orientation[0] == 'p':
602 #portrait
603 self.size = Window.size[0]/1.67, Window.size[1]/1.4
604 else:
605 self.size = Window.size[0]/2.5, Window.size[1]
606
607 def add_widget(self, widget, index=0):
608 if not self.crcontent:
609 super(WizardDialog, self).add_widget(widget)
610 else:
611 self.crcontent.add_widget(widget, index=index)
612
613 def on_keyboard(self, instance, key, keycode, codepoint, modifier):
614 if key == 27:
615 if self.wizard.can_go_back():
616 self.dismiss()
617 self.wizard.go_back()
618 else:
619 if not self.app.is_exit:
620 self.app.is_exit = True
621 self.app.show_info(_('Press again to exit'))
622 else:
623 self._on_release = False
624 self.dismiss()
625 return True
626
627 def on_dismiss(self):
628 Window.unbind(size=self._trigger_size_dialog,
629 rotation=self._trigger_size_dialog,
630 on_keyboard=self.on_keyboard)
631 if self.app.wallet is None and not self._on_release:
632 self.app.stop()
633
634 def get_params(self, button):
635 return (None,)
636
637 def on_release(self, button):
638 if self._on_release is True:
639 return
640 self._on_release = True
641 self.dismiss()
642 if not button:
643 self.wizard.terminate(aborted=True)
644 return
645 if button is self.ids.back:
646 self.wizard.go_back()
647 return
648 params = self.get_params(button)
649 self.run_next(*params)
650
651
652 class WizardMultisigDialog(WizardDialog):
653
654 def get_params(self, button):
655 m = self.ids.m.value
656 n = self.ids.n.value
657 return m, n
658
659
660 class WizardOTPDialogBase(WizardDialog):
661
662 def get_otp(self):
663 otp = self.ids.otp.text
664 if len(otp) != 6:
665 return
666 try:
667 return int(otp)
668 except:
669 return
670
671 def on_text(self, dt):
672 self.ids.next.disabled = self.get_otp() is None
673
674 def on_enter(self, dt):
675 # press next
676 next = self.ids.next
677 if not next.disabled:
678 next.dispatch('on_release')
679
680
681 class WizardKnownOTPDialog(WizardOTPDialogBase):
682
683 def __init__(self, wizard, **kwargs):
684 WizardOTPDialogBase.__init__(self, wizard, **kwargs)
685 self.message = _("This wallet is already registered with TrustedCoin. To finalize wallet creation, please enter your Google Authenticator Code.")
686 self.message2 =_("If you have lost your Google Authenticator account, you can request a new secret. You will need to retype your seed.")
687 self.request_new = False
688
689 def get_params(self, button):
690 return (self.get_otp(), self.request_new)
691
692 def request_new_secret(self):
693 self.request_new = True
694 self.on_release(True)
695
696 def abort_wallet_creation(self):
697 self._on_release = True
698 self.wizard.terminate(aborted=True)
699 self.dismiss()
700
701
702 class WizardNewOTPDialog(WizardOTPDialogBase):
703
704 def __init__(self, wizard, **kwargs):
705 WizardOTPDialogBase.__init__(self, wizard, **kwargs)
706 otp_secret = kwargs['otp_secret']
707 uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret)
708 self.message = "Please scan the following QR code in Google Authenticator. You may also use the secret key: %s"%otp_secret
709 self.message2 = _('Then, enter your Google Authenticator code:')
710 self.ids.qr.set_data(uri)
711
712 def get_params(self, button):
713 return (self.get_otp(), False)
714
715 class WizardTOSDialog(WizardDialog):
716
717 def __init__(self, wizard, **kwargs):
718 WizardDialog.__init__(self, wizard, **kwargs)
719 self.ids.next.text = 'Accept'
720 self.ids.next.disabled = False
721 self.message = kwargs['tos']
722
723 class WizardEmailDialog(WizardDialog):
724
725 def get_params(self, button):
726 return (self.ids.email.text,)
727
728 def on_text(self, dt):
729 self.ids.next.disabled = not is_valid_email(self.ids.email.text)
730
731 def on_enter(self, dt):
732 # press next
733 next = self.ids.next
734 if not next.disabled:
735 next.dispatch('on_release')
736
737 class WizardConfirmDialog(WizardDialog):
738
739 def __init__(self, wizard, **kwargs):
740 super(WizardConfirmDialog, self).__init__(wizard, **kwargs)
741 self.message = kwargs.get('message', '')
742 self.value = 'ok'
743
744 def on_parent(self, instance, value):
745 if value:
746 self._back = _back = partial(self.app.dispatch, 'on_back')
747
748 def get_params(self, button):
749 return (True,)
750
751
752 class WizardChoiceDialog(WizardDialog):
753
754 def __init__(self, wizard, **kwargs):
755 super(WizardChoiceDialog, self).__init__(wizard, **kwargs)
756 self.title = kwargs.get('message', '')
757 self.message = kwargs.get('message', '')
758 choices = kwargs.get('choices', [])
759 self.init_choices(choices)
760
761 def init_choices(self, choices):
762 layout = self.ids.choices
763 layout.bind(minimum_height=layout.setter('height'))
764 for action, text in choices:
765 l = WizardButton(text=text)
766 l.action = action
767 l.height = '48dp'
768 l.root = self
769 layout.add_widget(l)
770
771 def on_parent(self, instance, value):
772 if value:
773 self._back = _back = partial(self.app.dispatch, 'on_back')
774
775 def get_params(self, button):
776 return (button.action,)
777
778
779 class LineDialog(WizardDialog):
780 title = StringProperty('')
781 message = StringProperty('')
782 warning = StringProperty('')
783
784 def __init__(self, wizard, **kwargs):
785 WizardDialog.__init__(self, wizard, **kwargs)
786 self.title = kwargs.get('title', '')
787 self.message = kwargs.get('message', '')
788 self.ids.next.disabled = True
789 self.test = kwargs['test']
790
791 def get_text(self):
792 return self.ids.passphrase_input.text
793
794 def on_text(self, dt):
795 self.ids.next.disabled = not self.test(self.get_text())
796
797 def get_params(self, b):
798 return (self.get_text(),)
799
800 class CLButton(ToggleButton):
801 def on_release(self):
802 self.root.script_type = self.script_type
803 self.root.set_text(self.value)
804
805 class ChoiceLineDialog(WizardChoiceDialog):
806 title = StringProperty('')
807 message1 = StringProperty('')
808 message2 = StringProperty('')
809
810 def __init__(self, wizard, **kwargs):
811 WizardDialog.__init__(self, wizard, **kwargs)
812 self.title = kwargs.get('title', '')
813 self.message1 = kwargs.get('message1', '')
814 self.message2 = kwargs.get('message2', '')
815 self.choices = kwargs.get('choices', [])
816 default_choice_idx = kwargs.get('default_choice_idx', 0)
817 self.ids.next.disabled = False
818 layout = self.ids.choices
819 layout.bind(minimum_height=layout.setter('height'))
820 for idx, (script_type, title, text) in enumerate(self.choices):
821 b = CLButton(text=title, height='30dp', group=self.title, allow_no_selection=False)
822 b.script_type = script_type
823 b.root = self
824 b.value = text
825 layout.add_widget(b)
826 if idx == default_choice_idx:
827 b.trigger_action(duration=0)
828
829 def set_text(self, value):
830 self.ids.text_input.text = value
831
832 def get_params(self, b):
833 return (self.ids.text_input.text, self.script_type)
834
835 class ShowSeedDialog(WizardDialog):
836 seed_text = StringProperty('')
837 message = _("If you forget your PIN or lose your device, your seed phrase will be the only way to recover your funds.")
838
839 def __init__(self, wizard, **kwargs):
840 super(ShowSeedDialog, self).__init__(wizard, **kwargs)
841 self.seed_text = kwargs['seed_text']
842 self.opt_ext = True
843 self.is_ext = False
844
845 def on_parent(self, instance, value):
846 if value:
847 self._back = _back = partial(self.ids.back.dispatch, 'on_release')
848
849 def options_dialog(self):
850 from .seed_options import SeedOptionsDialog
851 def callback(ext, _):
852 self.is_ext = ext
853 d = SeedOptionsDialog(self.opt_ext, False, self.is_ext, False, callback)
854 d.open()
855
856 def get_params(self, b):
857 return (self.is_ext,)
858
859
860 class WordButton(Button):
861 pass
862
863 class WizardButton(Button):
864 pass
865
866
867 class RestoreSeedDialog(WizardDialog):
868
869 def __init__(self, wizard, **kwargs):
870 super(RestoreSeedDialog, self).__init__(wizard, **kwargs)
871 self._test = kwargs['test']
872 from electrum.mnemonic import Mnemonic
873 from electrum.old_mnemonic import wordlist as old_wordlist
874 self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist))
875 self.ids.text_input_seed.text = ''
876 self.message = _('Please type your seed phrase using the virtual keyboard.')
877 self.title = _('Enter Seed')
878 self.opt_ext = kwargs['opt_ext']
879 self.opt_bip39 = kwargs['opt_bip39']
880 self.is_ext = False
881 self.is_bip39 = False
882
883 def options_dialog(self):
884 from .seed_options import SeedOptionsDialog
885 def callback(ext, bip39):
886 self.is_ext = ext
887 self.is_bip39 = bip39
888 self.update_next_button()
889 d = SeedOptionsDialog(self.opt_ext, self.opt_bip39, self.is_ext, self.is_bip39, callback)
890 d.open()
891
892 def get_suggestions(self, prefix):
893 for w in self.words:
894 if w.startswith(prefix):
895 yield w
896
897 def update_next_button(self):
898 from electrum.keystore import bip39_is_checksum_valid
899 text = self.get_text()
900 if self.is_bip39:
901 is_seed, is_wordlist = bip39_is_checksum_valid(text)
902 else:
903 is_seed = bool(self._test(text))
904 self.ids.next.disabled = not is_seed
905
906 def on_text(self, dt):
907 self.update_next_button()
908
909 text = self.ids.text_input_seed.text
910 if not text:
911 last_word = ''
912 elif text[-1] == ' ':
913 last_word = ''
914 else:
915 last_word = text.split(' ')[-1]
916
917 enable_space = False
918 self.ids.suggestions.clear_widgets()
919 suggestions = [x for x in self.get_suggestions(last_word)]
920
921 if last_word in suggestions:
922 b = WordButton(text=last_word)
923 self.ids.suggestions.add_widget(b)
924 enable_space = True
925
926 for w in suggestions:
927 if w != last_word and len(suggestions) < 10:
928 b = WordButton(text=w)
929 self.ids.suggestions.add_widget(b)
930
931 i = len(last_word)
932 p = set()
933 for x in suggestions:
934 if len(x)>i: p.add(x[i])
935
936 for line in [self.ids.line1, self.ids.line2, self.ids.line3]:
937 for c in line.children:
938 if isinstance(c, Button):
939 if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
940 c.disabled = (c.text.lower() not in p) and bool(last_word)
941 elif c.text == ' ':
942 c.disabled = not enable_space
943
944 def on_word(self, w):
945 text = self.get_text()
946 words = text.split(' ')
947 words[-1] = w
948 text = ' '.join(words)
949 self.ids.text_input_seed.text = text + ' '
950 self.ids.suggestions.clear_widgets()
951
952 def get_text(self):
953 ti = self.ids.text_input_seed
954 return ' '.join(ti.text.strip().split())
955
956 def update_text(self, c):
957 c = c.lower()
958 text = self.ids.text_input_seed.text
959 if c == '<':
960 text = text[:-1]
961 else:
962 text += c
963 self.ids.text_input_seed.text = text
964
965 def on_parent(self, instance, value):
966 if value:
967 tis = self.ids.text_input_seed
968 tis.focus = True
969 #tis._keyboard.bind(on_key_down=self.on_key_down)
970 self._back = _back = partial(self.ids.back.dispatch,
971 'on_release')
972
973 def on_key_down(self, keyboard, keycode, key, modifiers):
974 if keycode[0] in (13, 271):
975 self.on_enter()
976 return True
977
978 def on_enter(self):
979 #self._remove_keyboard()
980 # press next
981 next = self.ids.next
982 if not next.disabled:
983 next.dispatch('on_release')
984
985 def _remove_keyboard(self):
986 tis = self.ids.text_input_seed
987 if tis._keyboard:
988 tis._keyboard.unbind(on_key_down=self.on_key_down)
989 tis.focus = False
990
991 def get_params(self, b):
992 return (self.get_text(), self.is_bip39, self.is_ext)
993
994
995 class ConfirmSeedDialog(RestoreSeedDialog):
996
997 def __init__(self, *args, **kwargs):
998 RestoreSeedDialog.__init__(self, *args, **kwargs)
999 self.ids.seed_dialog_header.ids.options_button.disabled = True
1000 self.ids.text_input_seed.text = kwargs['seed']
1001
1002 def get_params(self, b):
1003 return (self.get_text(),)
1004 def options_dialog(self):
1005 pass
1006
1007
1008 class ShowXpubDialog(WizardDialog):
1009
1010 def __init__(self, wizard, **kwargs):
1011 WizardDialog.__init__(self, wizard, **kwargs)
1012 self.xpub = kwargs['xpub']
1013 self.ids.next.disabled = False
1014
1015 def do_copy(self):
1016 self.app._clipboard.copy(self.xpub)
1017
1018 def do_share(self):
1019 self.app.do_share(self.xpub, _("Master Public Key"))
1020
1021 def do_qr(self):
1022 from .qr_dialog import QRDialog
1023 popup = QRDialog(_("Master Public Key"), self.xpub, True)
1024 popup.open()
1025
1026
1027 class AddXpubDialog(WizardDialog):
1028
1029 def __init__(self, wizard, **kwargs):
1030 WizardDialog.__init__(self, wizard, **kwargs)
1031 def is_valid(x):
1032 try:
1033 return kwargs['is_valid'](x)
1034 except:
1035 return False
1036 self.is_valid = is_valid
1037 self.title = kwargs['title']
1038 self.message = kwargs['message']
1039 self.allow_multi = kwargs.get('allow_multi', False)
1040
1041 def check_text(self, dt):
1042 self.ids.next.disabled = not bool(self.is_valid(self.get_text()))
1043
1044 def get_text(self):
1045 ti = self.ids.text_input
1046 return ti.text.strip()
1047
1048 def get_params(self, button):
1049 return (self.get_text(),)
1050
1051 def scan_xpub(self):
1052 def on_complete(text):
1053 if self.allow_multi:
1054 self.ids.text_input.text += text + '\n'
1055 else:
1056 self.ids.text_input.text = text
1057 self.app.scan_qr(on_complete)
1058
1059 def do_paste(self):
1060 self.ids.text_input.text = self.app._clipboard.paste()
1061
1062 def do_clear(self):
1063 self.ids.text_input.text = ''
1064
1065
1066
1067
1068 class InstallWizard(BaseWizard, Widget):
1069
1070 def __init__(self, *args, **kwargs):
1071 BaseWizard.__init__(self, *args, **kwargs)
1072 self.app = App.get_running_app()
1073
1074 def terminate(self, *, storage=None, db=None, aborted=False):
1075 # storage must be None because manual upgrades are disabled on Kivy
1076 assert storage is None
1077 if not aborted:
1078 password = self.pw_args.password
1079 storage, db = self.create_storage(self.path)
1080 self.app.on_wizard_success(storage, db, password)
1081 else:
1082 try: os.unlink(self.path)
1083 except FileNotFoundError: pass
1084 self.reset_stack()
1085 self.confirm_dialog(message=_('Wallet creation failed'), run_next=lambda x: self.app.on_wizard_aborted())
1086
1087 def choice_dialog(self, **kwargs):
1088 choices = kwargs['choices']
1089 if len(choices) > 1:
1090 WizardChoiceDialog(self, **kwargs).open()
1091 else:
1092 f = kwargs['run_next']
1093 f(choices[0][0])
1094
1095 def multisig_dialog(self, **kwargs): WizardMultisigDialog(self, **kwargs).open()
1096 def show_seed_dialog(self, **kwargs): ShowSeedDialog(self, **kwargs).open()
1097 def line_dialog(self, **kwargs): LineDialog(self, **kwargs).open()
1098 def derivation_and_script_type_gui_specific_dialog(self, **kwargs): ChoiceLineDialog(self, **kwargs).open()
1099
1100 def confirm_seed_dialog(self, **kwargs):
1101 kwargs['title'] = _('Confirm Seed')
1102 kwargs['message'] = _('Please retype your seed phrase, to confirm that you properly saved it')
1103 kwargs['opt_bip39'] = self.opt_bip39
1104 kwargs['opt_ext'] = self.opt_ext
1105 ConfirmSeedDialog(self, **kwargs).open()
1106
1107 def restore_seed_dialog(self, **kwargs):
1108 kwargs['opt_bip39'] = self.opt_bip39
1109 kwargs['opt_ext'] = self.opt_ext
1110 RestoreSeedDialog(self, **kwargs).open()
1111
1112 def confirm_dialog(self, **kwargs):
1113 WizardConfirmDialog(self, **kwargs).open()
1114
1115 def tos_dialog(self, **kwargs):
1116 WizardTOSDialog(self, **kwargs).open()
1117
1118 def email_dialog(self, **kwargs):
1119 WizardEmailDialog(self, **kwargs).open()
1120
1121 def otp_dialog(self, **kwargs):
1122 if kwargs['otp_secret']:
1123 WizardNewOTPDialog(self, **kwargs).open()
1124 else:
1125 WizardKnownOTPDialog(self, **kwargs).open()
1126
1127 def add_xpub_dialog(self, **kwargs):
1128 kwargs['message'] += ' ' + _('Use the camera button to scan a QR code.')
1129 AddXpubDialog(self, **kwargs).open()
1130
1131 def add_cosigner_dialog(self, **kwargs):
1132 kwargs['title'] = _("Add Cosigner") + " %d"%kwargs['index']
1133 kwargs['message'] = _('Please paste your cosigners master public key, or scan it using the camera button.')
1134 AddXpubDialog(self, **kwargs).open()
1135
1136 def show_xpub_dialog(self, **kwargs): ShowXpubDialog(self, **kwargs).open()
1137
1138 def show_message(self, msg): self.show_error(msg)
1139
1140 def show_error(self, msg):
1141 Clock.schedule_once(lambda dt: self.app.show_error(msg))
1142
1143 def request_password(self, run_next, force_disable_encrypt_cb=False):
1144 if self.app.password is not None:
1145 run_next(self.app.password, True)
1146 return
1147 def on_success(old_pw, pw):
1148 assert old_pw is None
1149 run_next(pw, True)
1150 def on_failure():
1151 self.show_error(_('Password mismatch'))
1152 self.request_password(run_next)
1153 popup = PasswordDialog(
1154 self.app,
1155 check_password=lambda x:True,
1156 on_success=on_success,
1157 on_failure=on_failure,
1158 is_change=True,
1159 is_password=True,
1160 message=_('Choose a password'))
1161 popup.open()
1162
1163 def action_dialog(self, action, run_next):
1164 f = getattr(self, action)
1165 f()