fix: wizard 2of2
[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 cosigning 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         vbox.addLayout(ok_cancel_buttons(self, _('Next')))
136
137         self.set_layout(vbox)
138
139         self.show()
140         self.raise_()
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_mpk(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_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_mpk(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             xpub = self.multi_mpk_dialog(xpub_hot, 1)
440             if not xpub:
441                 return
442             wallet.add_master_public_key("cold/", xpub)
443             wallet.create_account()
444             self.waiting_dialog(wallet.synchronize)
445
446
447         if action == 'create_2of3_2':
448             xpub_hot = wallet.master_public_keys.get("m/")
449             r = self.multi_mpk_dialog(xpub_hot, 2)
450             if not r:
451                 return
452             xpub1, xpub2 = r
453             wallet.add_master_public_key("cold/", xpub1)
454             wallet.add_master_public_key("remote/", xpub2)
455             wallet.create_account()
456             self.waiting_dialog(wallet.synchronize)
457
458
459         if action == 'create_2fa_3':
460             run_hook('create_remote_key', wallet, self)
461             if not wallet.master_public_keys.get("remote/"):
462                 return
463             wallet.create_account()
464             self.waiting_dialog(wallet.synchronize)
465
466
467         if action == 'restore':
468
469             if t == 'standard':
470                 text = self.enter_seed_dialog(MSG_ENTER_ANYTHING, None)
471                 if not text:
472                     return
473                 if Wallet.is_seed(text):
474                     password = self.password_dialog()
475                     wallet = Wallet.from_seed(text, self.storage)
476                     wallet.add_seed(text, password)
477                     wallet.create_accounts(password)
478                 elif Wallet.is_mpk(text):
479                     wallet = Wallet.from_mpk(text, self.storage)
480                 elif Wallet.is_address(text):
481                     wallet = Wallet.from_address(text, self.storage)
482                 elif Wallet.is_private_key(text):
483                     wallet = Wallet.from_private_key(text, self.storage)
484                 else:
485                     raise
486
487             elif t in ['2fa', '2of2']:
488                 r = self.multi_seed_dialog(1)
489                 if not r: 
490                     return
491                 text1, text2 = r
492                 password = self.password_dialog()
493                 if t == '2of2':
494                     wallet = Wallet_2of2(self.storage)
495                 elif t == '2of3':
496                     wallet = Wallet_2of3(self.storage)
497                 elif t == '2fa':
498                     wallet = Wallet_2of3(self.storage)
499
500                 if Wallet.is_seed(text1):
501                     wallet.add_seed(text1, password)
502                     if Wallet.is_seed(text2):
503                         wallet.add_cold_seed(text2, password)
504                     else:
505                         wallet.add_master_public_key("cold/", text2)
506
507                 elif Wallet.is_mpk(text1):
508                     if Wallet.is_seed(text2):
509                         wallet.add_seed(text2, password)
510                         wallet.add_master_public_key("cold/", text1)
511                     else:
512                         wallet.add_master_public_key("m/", text1)
513                         wallet.add_master_public_key("cold/", text2)
514
515                 if t == '2fa':
516                     run_hook('restore_third_key', wallet, self)
517
518                 wallet.create_account()
519
520             elif t in ['2of3']:
521                 r = self.multi_seed_dialog(2)
522                 if not r: 
523                     return
524                 text1, text2, text3 = r
525                 password = self.password_dialog()
526                 wallet = Wallet_2of3(self.storage)
527
528                 if Wallet.is_seed(text1):
529                     wallet.add_seed(text1, password)
530                     if Wallet.is_seed(text2):
531                         wallet.add_cold_seed(text2, password)
532                     else:
533                         wallet.add_master_public_key("cold/", text2)
534
535                 elif Wallet.is_mpk(text1):
536                     if Wallet.is_seed(text2):
537                         wallet.add_seed(text2, password)
538                         wallet.add_master_public_key("cold/", text1)
539                     else:
540                         wallet.add_master_public_key("m/", text1)
541                         wallet.add_master_public_key("cold/", text2)
542
543                 wallet.create_account()
544
545             else:
546                 raise
547
548
549                 
550         #if not self.config.get('server'):
551         if self.network:
552             if self.network.interfaces:
553                 self.network_dialog()
554             else:
555                 QMessageBox.information(None, _('Warning'), _('You are offline'), _('OK'))
556                 self.network.stop()
557                 self.network = None
558
559         # start wallet threads
560         wallet.start_threads(self.network)
561
562         if action == 'restore':
563
564             self.waiting_dialog(lambda: wallet.restore(self.waiting_label.setText))
565
566             if self.network:
567                 if wallet.is_found():
568                     QMessageBox.information(None, _('Information'), _("Recovery successful"), _('OK'))
569                 else:
570                     QMessageBox.information(None, _('Information'), _("No transactions found for this seed"), _('OK'))
571             else:
572                 QMessageBox.information(None, _('Information'), _("This wallet was restored offline. It may contain more addresses than displayed."), _('OK'))
573
574         return wallet