e697d3aacd33a5c6feff9eb6b887a0ea000be786
[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_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             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_mpk(text):
480                     wallet = Wallet.from_mpk(text, self.storage)
481                 elif Wallet.is_address(text):
482                     wallet = Wallet.from_address(text, self.storage)
483                 elif Wallet.is_private_key(text):
484                     wallet = Wallet.from_private_key(text, self.storage)
485                 else:
486                     raise
487
488             elif t in ['2fa', '2of2']:
489                 r = self.multi_seed_dialog(1)
490                 if not r: 
491                     return
492                 text1, text2 = r
493                 password = self.password_dialog()
494                 if t == '2of2':
495                     wallet = Wallet_2of2(self.storage)
496                 elif t == '2of3':
497                     wallet = Wallet_2of3(self.storage)
498                 elif t == '2fa':
499                     wallet = Wallet_2of3(self.storage)
500
501                 if Wallet.is_seed(text1):
502                     wallet.add_seed(text1, password)
503                     if Wallet.is_seed(text2):
504                         wallet.add_cold_seed(text2, password)
505                     else:
506                         wallet.add_master_public_key("cold/", text2)
507
508                 elif Wallet.is_mpk(text1):
509                     if Wallet.is_seed(text2):
510                         wallet.add_seed(text2, password)
511                         wallet.add_master_public_key("cold/", text1)
512                     else:
513                         wallet.add_master_public_key("m/", text1)
514                         wallet.add_master_public_key("cold/", text2)
515
516                 if t == '2fa':
517                     run_hook('restore_third_key', wallet, self)
518
519                 wallet.create_account()
520
521             elif t in ['2of3']:
522                 r = self.multi_seed_dialog(2)
523                 if not r: 
524                     return
525                 text1, text2, text3 = r
526                 password = self.password_dialog()
527                 wallet = Wallet_2of3(self.storage)
528
529                 if Wallet.is_seed(text1):
530                     wallet.add_seed(text1, password)
531                     if Wallet.is_seed(text2):
532                         wallet.add_cold_seed(text2, password)
533                     else:
534                         wallet.add_master_public_key("cold/", text2)
535
536                 elif Wallet.is_mpk(text1):
537                     if Wallet.is_seed(text2):
538                         wallet.add_seed(text2, password)
539                         wallet.add_master_public_key("cold/", text1)
540                     else:
541                         wallet.add_master_public_key("m/", text1)
542                         wallet.add_master_public_key("cold/", text2)
543
544                 wallet.create_account()
545
546             else:
547                 raise
548
549
550                 
551         #if not self.config.get('server'):
552         if self.network:
553             if self.network.interfaces:
554                 self.network_dialog()
555             else:
556                 QMessageBox.information(None, _('Warning'), _('You are offline'), _('OK'))
557                 self.network.stop()
558                 self.network = None
559
560         # start wallet threads
561         wallet.start_threads(self.network)
562
563         if action == 'restore':
564
565             self.waiting_dialog(lambda: wallet.restore(self.waiting_label.setText))
566
567             if self.network:
568                 if wallet.is_found():
569                     QMessageBox.information(None, _('Information'), _("Recovery successful"), _('OK'))
570                 else:
571                     QMessageBox.information(None, _('Information'), _("No transactions found for this seed"), _('OK'))
572             else:
573                 QMessageBox.information(None, _('Information'), _("This wallet was restored offline. It may contain more addresses than displayed."), _('OK'))
574
575         return wallet