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