25cbad4474eef31a98dc94a467e731aee5027ab6
[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         if not self.exec_():
139             return None, None
140         
141         action = 'create' if b1.isChecked() else 'restore'
142
143         if bb1.isChecked():
144             t = 'standard'
145         elif bb2.isChecked():
146             t = '2fa'
147         elif bb3.isChecked():
148             t = '2of2'
149         elif bb4.isChecked():
150             t = '2of3'
151
152         return action, t
153
154
155     def verify_seed(self, seed, sid):
156         r = self.enter_seed_dialog(MSG_VERIFY_SEED, sid)
157         if not r:
158             return
159
160         if r != seed:
161             QMessageBox.warning(None, _('Error'), _('Incorrect seed'), _('OK'))
162             return False
163         else:
164             return True
165
166
167     def get_seed_text(self, seed_e):
168         text = unicode(seed_e.toPlainText()).strip()
169         text = ' '.join(text.split())
170         return text
171
172
173     def is_any(self, seed_e):
174         text = self.get_seed_text(seed_e)
175         return Wallet.is_seed(text) or Wallet.is_mpk(text) or Wallet.is_address(text) or Wallet.is_private_key(text)
176
177     def is_mpk(self, seed_e):
178         text = self.get_seed_text(seed_e)
179         return Wallet.is_mpk(text)
180
181
182     def enter_seed_dialog(self, msg, sid):
183         vbox, seed_e = seed_dialog.enter_seed_box(msg, sid)
184         vbox.addStretch(1)
185         hbox, button = ok_cancel_buttons2(self, _('Next'))
186         vbox.addLayout(hbox)
187         button.setEnabled(False)
188         seed_e.textChanged.connect(lambda: button.setEnabled(self.is_any(seed_e)))
189         self.set_layout(vbox)
190         if not self.exec_():
191             return
192         return self.get_seed_text(seed_e)
193
194
195     def multi_mpk_dialog(self, xpub_hot, n):
196         vbox = QVBoxLayout()
197         vbox0, seed_e0 = seed_dialog.enter_seed_box(MSG_SHOW_MPK, 'hot')
198         vbox.addLayout(vbox0)
199         seed_e0.setText(xpub_hot)
200         seed_e0.setReadOnly(True)
201         entries = []
202         for i in range(n):
203             vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_COLD_MPK, 'cold')
204             vbox.addLayout(vbox2)
205             entries.append(seed_e2)
206         vbox.addStretch(1)
207         hbox, button = ok_cancel_buttons2(self, _('Next'))
208         vbox.addLayout(hbox)
209         button.setEnabled(False)
210         f = lambda: button.setEnabled( map(lambda e: self.is_mpk(e), entries) == [True]*len(entries))
211         for e in entries:
212             e.textChanged.connect(f)
213         self.set_layout(vbox)
214         if not self.exec_():
215             return
216         return map(lambda e: self.get_seed_text(e), entries)
217
218
219     def multi_seed_dialog(self, n):
220         vbox = QVBoxLayout()
221         vbox1, seed_e1 = seed_dialog.enter_seed_box(MSG_ENTER_SEED_OR_MPK, 'hot')
222         vbox.addLayout(vbox1)
223         entries = [seed_e1]
224         for i in range(n):
225             vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_SEED_OR_MPK, 'cold')
226             vbox.addLayout(vbox2)
227             entries.append(seed_e2)
228         vbox.addStretch(1)
229         hbox, button = ok_cancel_buttons2(self, _('Next'))
230         vbox.addLayout(hbox)
231         button.setEnabled(False)
232
233         f = lambda: button.setEnabled( map(lambda e: self.is_any(e), entries) == [True]*len(entries))
234         for e in entries:
235             e.textChanged.connect(f)
236
237         self.set_layout(vbox)
238         if not self.exec_():
239             return 
240         return map(lambda e: self.get_seed_text(e), entries)
241
242
243
244
245
246     def waiting_dialog(self, task, msg= _("Electrum is generating your addresses, please wait.")):
247         def target():
248             task()
249             self.emit(QtCore.SIGNAL('accept'))
250
251         vbox = QVBoxLayout()
252         self.waiting_label = QLabel(msg)
253         vbox.addWidget(self.waiting_label)
254         self.set_layout(vbox)
255         t = threading.Thread(target = target)
256         t.start()
257         self.exec_()
258
259
260
261
262     def network_dialog(self):
263         
264         grid = QGridLayout()
265         grid.setSpacing(5)
266
267         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" \
268                       + _("How do you want to connect to a server:")+" ")
269         label.setWordWrap(True)
270         grid.addWidget(label, 0, 0)
271
272         gb = QGroupBox()
273
274         b1 = QRadioButton(gb)
275         b1.setText(_("Auto connect"))
276         b1.setChecked(True)
277
278         b2 = QRadioButton(gb)
279         b2.setText(_("Select server manually"))
280
281         #b3 = QRadioButton(gb)
282         #b3.setText(_("Stay offline"))
283
284         grid.addWidget(b1,1,0)
285         grid.addWidget(b2,2,0)
286         #grid.addWidget(b3,3,0)
287
288         vbox = QVBoxLayout()
289         vbox.addLayout(grid)
290
291         vbox.addStretch(1)
292         vbox.addLayout(ok_cancel_buttons(self, _('Next')))
293
294         self.set_layout(vbox)
295         if not self.exec_():
296             return
297         
298         if b2.isChecked():
299             return NetworkDialog(self.network, self.config, None).do_exec()
300
301         elif b1.isChecked():
302             self.config.set_key('auto_cycle', True, True)
303             return
304
305         else:
306             self.config.set_key("server", None, True)
307             self.config.set_key('auto_cycle', False, True)
308             return
309         
310
311     def show_message(self, msg, icon=None):
312         vbox = QVBoxLayout()
313         self.set_layout(vbox)
314         if icon:
315             logo = QLabel()
316             logo.setPixmap(icon)
317             vbox.addWidget(logo)
318         vbox.addWidget(QLabel(msg))
319         vbox.addStretch(1)
320         vbox.addLayout(close_button(self, _('Next')))
321         if not self.exec_(): 
322             return None
323
324
325     def question(self, msg, icon=None):
326         vbox = QVBoxLayout()
327         self.set_layout(vbox)
328         if icon:
329             logo = QLabel()
330             logo.setPixmap(icon)
331             vbox.addWidget(logo)
332         vbox.addWidget(QLabel(msg))
333         vbox.addStretch(1)
334         vbox.addLayout(ok_cancel_buttons(self, _('OK')))
335         if not self.exec_(): 
336             return None
337         return True
338
339
340     def show_seed(self, seed, sid):
341         vbox = seed_dialog.show_seed_box(seed, sid)
342         vbox.addLayout(ok_cancel_buttons(self, _("Next")))
343         self.set_layout(vbox)
344         return self.exec_()
345
346
347     def password_dialog(self):
348         msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
349               +_("Leave these fields empty if you want to disable encryption.")
350         from password_dialog import make_password_dialog, run_password_dialog
351         self.set_layout( make_password_dialog(self, None, msg) )
352         return run_password_dialog(self, None, self)[2]
353
354
355     def create_cold_seed(self, wallet):
356         from electrum.bitcoin import mnemonic_to_seed, bip32_root
357         msg = _('You are about to generate the cold storage seed of your wallet.') + '\n' \
358               + _('For safety, you should do this on an offline computer.')
359         icon = QPixmap( ':icons/cold_seed.png').scaledToWidth(56)
360         if not self.question(msg, icon):
361             return
362
363         cold_seed = wallet.make_seed()
364         if not self.show_seed(cold_seed, 'cold'):
365             return
366         if not self.verify_seed(cold_seed, 'cold'):
367             return
368
369         hex_seed = mnemonic_to_seed(cold_seed,'').encode('hex')
370         xpriv, xpub = bip32_root(hex_seed)
371         wallet.add_master_public_key('cold/', xpub)
372
373         msg = _('Your master public key was saved in your wallet file.') + '\n'\
374               + _('Your cold seed must be stored on paper; it is not in the wallet file.')+ '\n\n' \
375               + _('This program is about to close itself.') + '\n'\
376               + _('You will need to reopen your wallet on an online computer, in order to complete the creation of your wallet')
377         self.show_message(msg)
378
379
380
381     def run(self, action):
382
383         if action == 'new':
384             action, t = self.restore_or_create()
385
386         if action is None: 
387             return
388             
389         if action == 'create':
390             if t == 'standard':
391                 wallet = Wallet(self.storage)
392
393             elif t == '2fa':
394                 wallet = Wallet_2of3(self.storage)
395                 run_hook('create_cold_seed', wallet, self)
396                 self.create_cold_seed(wallet)
397                 return
398
399             elif t == '2of2':
400                 wallet = Wallet_2of2(self.storage)
401                 action = 'create_2of2_1'
402
403             elif t == '2of3':
404                 wallet = Wallet_2of3(self.storage)
405                 action = 'create_2of3_1'
406
407
408         if action in ['create_2fa_2', 'create_2of3_2']:
409             wallet = Wallet_2of3(self.storage)
410
411         if action in ['create', 'create_2of2_1', 'create_2fa_2', 'create_2of3_1']:
412             seed = wallet.make_seed()
413             sid = None if action == 'create' else 'hot'
414             if not self.show_seed(seed, sid):
415                 return
416             if not self.verify_seed(seed, sid):
417                 return
418             password = self.password_dialog()
419             wallet.add_seed(seed, password)
420             if action == 'create':
421                 wallet.create_accounts(password)
422                 self.waiting_dialog(wallet.synchronize)
423             elif action == 'create_2of2_1':
424                 action = 'create_2of2_2'
425             elif action == 'create_2of3_1':
426                 action = 'create_2of3_2'
427             elif action == 'create_2fa_2':
428                 action = 'create_2fa_3'
429
430         if action == 'create_2of2_2':
431             xpub_hot = wallet.master_public_keys.get("m/")
432             xpub = self.multi_mpk_dialog(xpub_hot, 1)
433             if not xpub:
434                 return
435             wallet.add_master_public_key("cold/", xpub)
436             wallet.create_account()
437             self.waiting_dialog(wallet.synchronize)
438
439
440         if action == 'create_2of3_2':
441             xpub_hot = wallet.master_public_keys.get("m/")
442             r = self.multi_mpk_dialog(xpub_hot, 2)
443             if not r:
444                 return
445             xpub1, xpub2 = r
446             wallet.add_master_public_key("cold/", xpub1)
447             wallet.add_master_public_key("remote/", xpub2)
448             wallet.create_account()
449             self.waiting_dialog(wallet.synchronize)
450
451
452         if action == 'create_2fa_3':
453             run_hook('create_remote_key', wallet, self)
454             if not wallet.master_public_keys.get("remote/"):
455                 return
456             wallet.create_account()
457             self.waiting_dialog(wallet.synchronize)
458
459
460         if action == 'restore':
461
462             if t == 'standard':
463                 text = self.enter_seed_dialog(MSG_ENTER_ANYTHING, None)
464                 if not text:
465                     return
466                 if Wallet.is_seed(text):
467                     password = self.password_dialog()
468                     wallet = Wallet.from_seed(text, self.storage)
469                     wallet.add_seed(text, password)
470                     wallet.create_accounts(password)
471                 elif Wallet.is_mpk(text):
472                     wallet = Wallet.from_mpk(text, self.storage)
473                 elif Wallet.is_address(text):
474                     wallet = Wallet.from_address(text, self.storage)
475                 elif Wallet.is_private_key(text):
476                     wallet = Wallet.from_private_key(text, self.storage)
477                 else:
478                     raise
479
480             elif t in ['2fa', '2of2']:
481                 r = self.multi_seed_dialog(1)
482                 if not r: 
483                     return
484                 text1, text2 = r
485                 password = self.password_dialog()
486                 if t == '2of2':
487                     wallet = Wallet_2of2(self.storage)
488                 elif t == '2of3':
489                     wallet = Wallet_2of3(self.storage)
490                 elif t == '2fa':
491                     wallet = Wallet_2of3(self.storage)
492
493                 if Wallet.is_seed(text1):
494                     wallet.add_seed(text1, password)
495                     if Wallet.is_seed(text2):
496                         wallet.add_cold_seed(text2, password)
497                     else:
498                         wallet.add_master_public_key("cold/", text2)
499
500                 elif Wallet.is_mpk(text1):
501                     if Wallet.is_seed(text2):
502                         wallet.add_seed(text2, password)
503                         wallet.add_master_public_key("cold/", text1)
504                     else:
505                         wallet.add_master_public_key("m/", text1)
506                         wallet.add_master_public_key("cold/", text2)
507
508                 if t == '2fa':
509                     run_hook('restore_third_key', wallet, self)
510
511                 wallet.create_account()
512
513             elif t in ['2of3']:
514                 r = self.multi_seed_dialog(2)
515                 if not r: 
516                     return
517                 text1, text2, text3 = r
518                 password = self.password_dialog()
519                 wallet = Wallet_2of3(self.storage)
520
521                 if Wallet.is_seed(text1):
522                     wallet.add_seed(text1, password)
523                     if Wallet.is_seed(text2):
524                         wallet.add_cold_seed(text2, password)
525                     else:
526                         wallet.add_master_public_key("cold/", text2)
527
528                 elif Wallet.is_mpk(text1):
529                     if Wallet.is_seed(text2):
530                         wallet.add_seed(text2, password)
531                         wallet.add_master_public_key("cold/", text1)
532                     else:
533                         wallet.add_master_public_key("m/", text1)
534                         wallet.add_master_public_key("cold/", text2)
535
536                 wallet.create_account()
537
538             else:
539                 raise
540
541
542                 
543         #if not self.config.get('server'):
544         if self.network:
545             if self.network.interfaces:
546                 self.network_dialog()
547             else:
548                 QMessageBox.information(None, _('Warning'), _('You are offline'), _('OK'))
549                 self.network.stop()
550                 self.network = None
551
552         # start wallet threads
553         wallet.start_threads(self.network)
554
555         if action == 'restore':
556
557             self.waiting_dialog(lambda: wallet.restore(self.waiting_label.setText))
558
559             if self.network:
560                 if wallet.is_found():
561                     QMessageBox.information(None, _('Information'), _("Recovery successful"), _('OK'))
562                 else:
563                     QMessageBox.information(None, _('Information'), _("No transactions found for this seed"), _('OK'))
564             else:
565                 QMessageBox.information(None, _('Information'), _("This wallet was restored offline. It may contain more addresses than displayed."), _('OK'))
566
567         return wallet