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