restore from xprv
[electrum-nvc.git] / gui / qt / installwizard.py
1 from PyQt4.QtGui import *
2 from PyQt4.QtCore import *
3 import PyQt4.QtCore as QtCore
4
5 from electrum.i18n import _
6 from electrum import Wallet, Wallet_2of2, Wallet_2of3
7 import electrum.bitcoin as bitcoin
8
9 import seed_dialog
10 from network_dialog import NetworkDialog
11 from util import *
12 from amountedit import AmountEdit
13
14 import sys
15 import threading
16 from electrum.plugins import run_hook
17
18
19 MSG_ENTER_ANYTHING    = _("Please enter a wallet seed, a master public key, a list of Bitcoin addresses, or a list of private keys")
20 MSG_SHOW_MPK          = _("This is your master public key")
21 MSG_ENTER_MPK         = _("Please enter your master public key")
22 MSG_ENTER_COLD_MPK    = _("Please enter the master public key of your cosigner wallet")
23 MSG_ENTER_SEED_OR_MPK = _("Please enter a wallet seed, or master public key")
24 MSG_VERIFY_SEED       = _("Your seed is important!") + "\n" + _("To make sure that you have properly saved your seed, please retype it here.")
25
26
27 class InstallWizard(QDialog):
28
29     def __init__(self, config, network, storage):
30         QDialog.__init__(self)
31         self.config = config
32         self.network = network
33         self.storage = storage
34         self.setMinimumSize(575, 400)
35         self.setWindowTitle('Electrum')
36         self.connect(self, QtCore.SIGNAL('accept'), self.accept)
37
38         self.stack = QStackedLayout()
39         self.setLayout(self.stack)
40
41
42     def set_layout(self, layout):
43         w = QWidget()
44         w.setLayout(layout)
45         self.stack.setCurrentIndex(self.stack.addWidget(w))
46
47
48     def restore_or_create(self):
49
50         vbox = QVBoxLayout()
51
52         main_label = QLabel(_("Electrum could not find an existing wallet."))
53         vbox.addWidget(main_label)
54
55         grid = QGridLayout()
56         grid.setSpacing(5)
57
58         label = QLabel(_("What do you want to do?"))
59         label.setWordWrap(True)
60         grid.addWidget(label, 0, 0)
61
62         gb1 = QGroupBox()
63         grid.addWidget(gb1, 0, 0)
64
65         group1 = QButtonGroup()
66
67         b1 = QRadioButton(gb1)
68         b1.setText(_("Create new wallet"))
69         b1.setChecked(True)
70
71         b2 = QRadioButton(gb1)
72         b2.setText(_("Restore an existing wallet"))
73
74         group1.addButton(b1)
75         group1.addButton(b2)
76
77         grid.addWidget(b1, 1, 0)
78         grid.addWidget(b2, 2, 0)
79         vbox.addLayout(grid)
80
81         grid2 = QGridLayout()
82         grid2.setSpacing(5)
83
84         class ClickableLabel(QLabel):
85             def mouseReleaseEvent(self, ev):
86                 self.emit(SIGNAL('clicked()'))
87
88         label2 = ClickableLabel(_("Wallet type:") + " [+]")
89         hbox = QHBoxLayout()
90         hbox.addWidget(label2)
91         grid2.addLayout(hbox, 3, 0)
92         
93         gb2 = QGroupBox()
94         grid.addWidget(gb2, 3, 0)
95
96         group2 = QButtonGroup()
97
98         bb1 = QRadioButton(gb2)
99         bb1.setText(_("Standard wallet"))
100         bb1.setChecked(True)
101
102         bb2 = QRadioButton(gb2)
103         bb2.setText(_("Wallet with two-factor authentication (plugin)"))
104
105         bb3 = QRadioButton(gb2)
106         bb3.setText(_("Multisig wallet (2 of 2)"))
107         bb3.setHidden(True)
108
109         bb4 = QRadioButton(gb2)
110         bb4.setText(_("Multisig wallet (2 of 3)"))
111         bb4.setHidden(True)
112
113         grid2.addWidget(bb1, 4, 0)
114         grid2.addWidget(bb2, 5, 0)
115         grid2.addWidget(bb3, 6, 0)
116         grid2.addWidget(bb4, 7, 0)
117
118         def toggle():
119             x = not bb3.isHidden()
120             label2.setText(_("Wallet type:") + (' [+]' if x else ' [-]'))
121             bb3.setHidden(x)
122             bb4.setHidden(x)
123  
124         self.connect(label2, SIGNAL('clicked()'), toggle)
125
126         grid2.addWidget(label2)
127
128         group2.addButton(bb1)
129         group2.addButton(bb2)
130         group2.addButton(bb3)
131         group2.addButton(bb4)
132  
133         vbox.addLayout(grid2)
134         vbox.addStretch(1)
135         hbox, button = ok_cancel_buttons2(self, _('Next'))
136         vbox.addLayout(hbox)
137         self.set_layout(vbox)
138         self.show()
139         self.raise_()
140         button.setDefault(True)
141
142         if not self.exec_():
143             return None, None
144         
145         action = 'create' if b1.isChecked() else 'restore'
146
147         if bb1.isChecked():
148             t = 'standard'
149         elif bb2.isChecked():
150             t = '2fa'
151         elif bb3.isChecked():
152             t = '2of2'
153         elif bb4.isChecked():
154             t = '2of3'
155
156         return action, t
157
158
159     def verify_seed(self, seed, sid):
160         r = self.enter_seed_dialog(MSG_VERIFY_SEED, sid)
161         if not r:
162             return
163
164         if r != seed:
165             QMessageBox.warning(None, _('Error'), _('Incorrect seed'), _('OK'))
166             return False
167         else:
168             return True
169
170
171     def get_seed_text(self, seed_e):
172         text = unicode(seed_e.toPlainText()).strip()
173         text = ' '.join(text.split())
174         return text
175
176
177     def is_any(self, seed_e):
178         text = self.get_seed_text(seed_e)
179         return Wallet.is_seed(text) or Wallet.is_old_mpk(text) or Wallet.is_xpub(text) or Wallet.is_xprv(text) or Wallet.is_address(text) or Wallet.is_private_key(text)
180
181     def is_mpk(self, seed_e):
182         text = self.get_seed_text(seed_e)
183         return Wallet.is_xpub(text) or Wallet.is_old_mpk(text)
184
185
186     def enter_seed_dialog(self, msg, sid):
187         vbox, seed_e = seed_dialog.enter_seed_box(msg, sid)
188         vbox.addStretch(1)
189         hbox, button = ok_cancel_buttons2(self, _('Next'))
190         vbox.addLayout(hbox)
191         button.setEnabled(False)
192         seed_e.textChanged.connect(lambda: button.setEnabled(self.is_any(seed_e)))
193         self.set_layout(vbox)
194         if not self.exec_():
195             return
196         return self.get_seed_text(seed_e)
197
198
199     def multi_mpk_dialog(self, xpub_hot, n):
200         vbox = QVBoxLayout()
201         vbox0, seed_e0 = seed_dialog.enter_seed_box(MSG_SHOW_MPK, 'hot')
202         vbox.addLayout(vbox0)
203         seed_e0.setText(xpub_hot)
204         seed_e0.setReadOnly(True)
205         entries = []
206         for i in range(n):
207             vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_COLD_MPK, 'cold')
208             vbox.addLayout(vbox2)
209             entries.append(seed_e2)
210         vbox.addStretch(1)
211         hbox, button = ok_cancel_buttons2(self, _('Next'))
212         vbox.addLayout(hbox)
213         button.setEnabled(False)
214         f = lambda: button.setEnabled( map(lambda e: self.is_xpub(e), entries) == [True]*len(entries))
215         for e in entries:
216             e.textChanged.connect(f)
217         self.set_layout(vbox)
218         if not self.exec_():
219             return
220         return map(lambda e: self.get_seed_text(e), entries)
221
222
223     def multi_seed_dialog(self, n):
224         vbox = QVBoxLayout()
225         vbox1, seed_e1 = seed_dialog.enter_seed_box(MSG_ENTER_SEED_OR_MPK, 'hot')
226         vbox.addLayout(vbox1)
227         entries = [seed_e1]
228         for i in range(n):
229             vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_SEED_OR_MPK, 'cold')
230             vbox.addLayout(vbox2)
231             entries.append(seed_e2)
232         vbox.addStretch(1)
233         hbox, button = ok_cancel_buttons2(self, _('Next'))
234         vbox.addLayout(hbox)
235         button.setEnabled(False)
236
237         f = lambda: button.setEnabled( map(lambda e: self.is_any(e), entries) == [True]*len(entries))
238         for e in entries:
239             e.textChanged.connect(f)
240
241         self.set_layout(vbox)
242         if not self.exec_():
243             return 
244         return map(lambda e: self.get_seed_text(e), entries)
245
246
247
248
249
250     def waiting_dialog(self, task, msg= _("Electrum is generating your addresses, please wait.")):
251         def target():
252             task()
253             self.emit(QtCore.SIGNAL('accept'))
254
255         vbox = QVBoxLayout()
256         self.waiting_label = QLabel(msg)
257         vbox.addWidget(self.waiting_label)
258         self.set_layout(vbox)
259         t = threading.Thread(target = target)
260         t.start()
261         self.exec_()
262
263
264
265
266     def network_dialog(self):
267         
268         grid = QGridLayout()
269         grid.setSpacing(5)
270
271         label = QLabel(_("Electrum communicates with remote servers to get information about your transactions and addresses. The servers all fulfil the same purpose only differing in hardware. In most cases you simply want to let Electrum pick one at random if you have a preference though feel free to select a server manually.") + "\n\n" \
272                       + _("How do you want to connect to a server:")+" ")
273         label.setWordWrap(True)
274         grid.addWidget(label, 0, 0)
275
276         gb = QGroupBox()
277
278         b1 = QRadioButton(gb)
279         b1.setText(_("Auto connect"))
280         b1.setChecked(True)
281
282         b2 = QRadioButton(gb)
283         b2.setText(_("Select server manually"))
284
285         #b3 = QRadioButton(gb)
286         #b3.setText(_("Stay offline"))
287
288         grid.addWidget(b1,1,0)
289         grid.addWidget(b2,2,0)
290         #grid.addWidget(b3,3,0)
291
292         vbox = QVBoxLayout()
293         vbox.addLayout(grid)
294
295         vbox.addStretch(1)
296         vbox.addLayout(ok_cancel_buttons(self, _('Next')))
297
298         self.set_layout(vbox)
299         if not self.exec_():
300             return
301         
302         if b2.isChecked():
303             return NetworkDialog(self.network, self.config, None).do_exec()
304
305         elif b1.isChecked():
306             self.config.set_key('auto_cycle', True, True)
307             return
308
309         else:
310             self.config.set_key("server", None, True)
311             self.config.set_key('auto_cycle', False, True)
312             return
313         
314
315     def show_message(self, msg, icon=None):
316         vbox = QVBoxLayout()
317         self.set_layout(vbox)
318         if icon:
319             logo = QLabel()
320             logo.setPixmap(icon)
321             vbox.addWidget(logo)
322         vbox.addWidget(QLabel(msg))
323         vbox.addStretch(1)
324         vbox.addLayout(close_button(self, _('Next')))
325         if not self.exec_(): 
326             return None
327
328
329     def question(self, msg, icon=None):
330         vbox = QVBoxLayout()
331         self.set_layout(vbox)
332         if icon:
333             logo = QLabel()
334             logo.setPixmap(icon)
335             vbox.addWidget(logo)
336         vbox.addWidget(QLabel(msg))
337         vbox.addStretch(1)
338         vbox.addLayout(ok_cancel_buttons(self, _('OK')))
339         if not self.exec_(): 
340             return None
341         return True
342
343
344     def show_seed(self, seed, sid):
345         vbox = seed_dialog.show_seed_box(seed, sid)
346         vbox.addLayout(ok_cancel_buttons(self, _("Next")))
347         self.set_layout(vbox)
348         return self.exec_()
349
350
351     def password_dialog(self):
352         msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
353               +_("Leave these fields empty if you want to disable encryption.")
354         from password_dialog import make_password_dialog, run_password_dialog
355         self.set_layout( make_password_dialog(self, None, msg) )
356         return run_password_dialog(self, None, self)[2]
357
358
359     def create_cold_seed(self, wallet):
360         from electrum.bitcoin import mnemonic_to_seed, bip32_root
361         msg = _('You are about to generate the cold storage seed of your wallet.') + '\n' \
362               + _('For safety, you should do this on an offline computer.')
363         icon = QPixmap( ':icons/cold_seed.png').scaledToWidth(56)
364         if not self.question(msg, icon):
365             return
366
367         cold_seed = wallet.make_seed()
368         if not self.show_seed(cold_seed, 'cold'):
369             return
370         if not self.verify_seed(cold_seed, 'cold'):
371             return
372
373         hex_seed = mnemonic_to_seed(cold_seed,'').encode('hex')
374         xpriv, xpub = bip32_root(hex_seed)
375         wallet.add_master_public_key('cold/', xpub)
376
377         msg = _('Your master public key was saved in your wallet file.') + '\n'\
378               + _('Your cold seed must be stored on paper; it is not in the wallet file.')+ '\n\n' \
379               + _('This program is about to close itself.') + '\n'\
380               + _('You will need to reopen your wallet on an online computer, in order to complete the creation of your wallet')
381         self.show_message(msg)
382
383
384
385     def run(self, action):
386
387         if action == 'new':
388             action, t = self.restore_or_create()
389
390         if action is None: 
391             return
392             
393         if action == 'create':
394             if t == 'standard':
395                 wallet = Wallet(self.storage)
396
397             elif t == '2fa':
398                 wallet = Wallet_2of3(self.storage)
399                 run_hook('create_cold_seed', wallet, self)
400                 self.create_cold_seed(wallet)
401                 return
402
403             elif t == '2of2':
404                 wallet = Wallet_2of2(self.storage)
405                 action = 'create_2of2_1'
406
407             elif t == '2of3':
408                 wallet = Wallet_2of3(self.storage)
409                 action = 'create_2of3_1'
410
411
412         if action in ['create_2fa_2', 'create_2of3_2']:
413             wallet = Wallet_2of3(self.storage)
414
415         if action in ['create_2of2_2']:
416             wallet = Wallet_2of2(self.storage)
417
418         if action in ['create', 'create_2of2_1', 'create_2fa_2', 'create_2of3_1']:
419             seed = wallet.make_seed()
420             sid = None if action == 'create' else 'hot'
421             if not self.show_seed(seed, sid):
422                 return
423             if not self.verify_seed(seed, sid):
424                 return
425             password = self.password_dialog()
426             wallet.add_seed(seed, password)
427             if action == 'create':
428                 wallet.create_accounts(password)
429                 self.waiting_dialog(wallet.synchronize)
430             elif action == 'create_2of2_1':
431                 action = 'create_2of2_2'
432             elif action == 'create_2of3_1':
433                 action = 'create_2of3_2'
434             elif action == 'create_2fa_2':
435                 action = 'create_2fa_3'
436
437         if action == 'create_2of2_2':
438             xpub_hot = wallet.master_public_keys.get("m/")
439             r = self.multi_mpk_dialog(xpub_hot, 1)
440             if not r:
441                 return
442             xpub_cold = r[0]
443             wallet.add_master_public_key("cold/", xpub_cold)
444             wallet.create_account()
445             self.waiting_dialog(wallet.synchronize)
446
447
448         if action == 'create_2of3_2':
449             xpub_hot = wallet.master_public_keys.get("m/")
450             r = self.multi_mpk_dialog(xpub_hot, 2)
451             if not r:
452                 return
453             xpub1, xpub2 = r
454             wallet.add_master_public_key("cold/", xpub1)
455             wallet.add_master_public_key("remote/", xpub2)
456             wallet.create_account()
457             self.waiting_dialog(wallet.synchronize)
458
459
460         if action == 'create_2fa_3':
461             run_hook('create_remote_key', wallet, self)
462             if not wallet.master_public_keys.get("remote/"):
463                 return
464             wallet.create_account()
465             self.waiting_dialog(wallet.synchronize)
466
467
468         if action == 'restore':
469
470             if t == 'standard':
471                 text = self.enter_seed_dialog(MSG_ENTER_ANYTHING, None)
472                 if not text:
473                     return
474                 if Wallet.is_seed(text):
475                     password = self.password_dialog()
476                     wallet = Wallet.from_seed(text, self.storage)
477                     wallet.add_seed(text, password)
478                     wallet.create_accounts(password)
479                 elif Wallet.is_xprv(text):
480                     password = self.password_dialog()
481                     wallet = Wallet.from_xprv(text, password, self.storage)
482                 elif Wallet.is_old_mpk(text):
483                     wallet = Wallet.from_old_mpk(text, self.storage)
484                 elif Wallet.is_xpub(text):
485                     wallet = Wallet.from_xpub(text, self.storage)
486                 elif Wallet.is_address(text):
487                     wallet = Wallet.from_address(text, self.storage)
488                 elif Wallet.is_private_key(text):
489                     wallet = Wallet.from_private_key(text, self.storage)
490                 else:
491                     raise
492
493             elif t in ['2fa', '2of2']:
494                 r = self.multi_seed_dialog(1)
495                 if not r: 
496                     return
497                 text1, text2 = r
498                 password = self.password_dialog()
499                 if t == '2of2':
500                     wallet = Wallet_2of2(self.storage)
501                 elif t == '2of3':
502                     wallet = Wallet_2of3(self.storage)
503                 elif t == '2fa':
504                     wallet = Wallet_2of3(self.storage)
505
506                 if Wallet.is_seed(text1):
507                     wallet.add_seed(text1, password)
508                     if Wallet.is_seed(text2):
509                         wallet.add_cold_seed(text2, password)
510                     else:
511                         wallet.add_master_public_key("cold/", text2)
512
513                 elif Wallet.is_mpk(text1):
514                     if Wallet.is_seed(text2):
515                         wallet.add_seed(text2, password)
516                         wallet.add_master_public_key("cold/", text1)
517                     else:
518                         wallet.add_master_public_key("m/", text1)
519                         wallet.add_master_public_key("cold/", text2)
520
521                 if t == '2fa':
522                     run_hook('restore_third_key', wallet, self)
523
524                 wallet.create_account()
525
526             elif t in ['2of3']:
527                 r = self.multi_seed_dialog(2)
528                 if not r: 
529                     return
530                 text1, text2, text3 = r
531                 password = self.password_dialog()
532                 wallet = Wallet_2of3(self.storage)
533
534                 if Wallet.is_seed(text1):
535                     wallet.add_seed(text1, password)
536                     if Wallet.is_seed(text2):
537                         wallet.add_cold_seed(text2, password)
538                     else:
539                         wallet.add_master_public_key("cold/", text2)
540
541                 elif Wallet.is_mpk(text1):
542                     if Wallet.is_seed(text2):
543                         wallet.add_seed(text2, password)
544                         wallet.add_master_public_key("cold/", text1)
545                     else:
546                         wallet.add_master_public_key("m/", text1)
547                         wallet.add_master_public_key("cold/", text2)
548
549                 wallet.create_account()
550
551             else:
552                 raise
553
554
555                 
556         #if not self.config.get('server'):
557         if self.network:
558             if self.network.interfaces:
559                 self.network_dialog()
560             else:
561                 QMessageBox.information(None, _('Warning'), _('You are offline'), _('OK'))
562                 self.network.stop()
563                 self.network = None
564
565         # start wallet threads
566         wallet.start_threads(self.network)
567
568         if action == 'restore':
569
570             self.waiting_dialog(lambda: wallet.restore(self.waiting_label.setText))
571
572             if self.network:
573                 if wallet.is_found():
574                     QMessageBox.information(None, _('Information'), _("Recovery successful"), _('OK'))
575                 else:
576                     QMessageBox.information(None, _('Information'), _("No transactions found for this seed"), _('OK'))
577             else:
578                 QMessageBox.information(None, _('Information'), _("This wallet was restored offline. It may contain more addresses than displayed."), _('OK'))
579
580         return wallet