electrum

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

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