installwizard.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()