fix spacing for indent
[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 from electrum import bitcoin
8 from electrum import util
9
10 import seed_dialog
11 from network_dialog import NetworkDialog
12 from util import *
13 from amountedit import AmountEdit
14
15 import sys
16 import threading
17 from electrum.plugins import run_hook
18
19
20 MSG_ENTER_ANYTHING    = _("Please enter a wallet seed, a master public key, a list of Bitcoin addresses, or a list of private keys")
21 MSG_SHOW_MPK          = _("This is your master public key")
22 MSG_ENTER_MPK         = _("Please enter your master public key")
23 MSG_ENTER_COLD_MPK    = _("Please enter the master public key of your cosigner wallet")
24 MSG_ENTER_SEED_OR_MPK = _("Please enter a wallet seed, or master public key")
25 MSG_VERIFY_SEED       = _("Your seed is important!") + "\n" + _("To make sure that you have properly saved your seed, please retype it here.")
26
27
28 class InstallWizard(QDialog):
29
30     def __init__(self, config, network, storage):
31         QDialog.__init__(self)
32         self.config = config
33         self.network = network
34         self.storage = storage
35         self.setMinimumSize(575, 400)
36         self.setWindowTitle('Electrum')
37         self.connect(self, QtCore.SIGNAL('accept'), self.accept)
38
39         self.stack = QStackedLayout()
40         self.setLayout(self.stack)
41
42
43     def set_layout(self, layout):
44         w = QWidget()
45         w.setLayout(layout)
46         self.stack.setCurrentIndex(self.stack.addWidget(w))
47
48
49     def restore_or_create(self):
50
51         vbox = QVBoxLayout()
52
53         main_label = QLabel(_("Electrum could not find an existing wallet."))
54         vbox.addWidget(main_label)
55
56         grid = QGridLayout()
57         grid.setSpacing(5)
58
59         label = QLabel(_("What do you want to do?"))
60         label.setWordWrap(True)
61         grid.addWidget(label, 0, 0)
62
63         gb1 = QGroupBox()
64         grid.addWidget(gb1, 0, 0)
65
66         group1 = QButtonGroup()
67
68         b1 = QRadioButton(gb1)
69         b1.setText(_("Create new wallet"))
70         b1.setChecked(True)
71
72         b2 = QRadioButton(gb1)
73         b2.setText(_("Restore an existing wallet"))
74
75         group1.addButton(b1)
76         group1.addButton(b2)
77
78         grid.addWidget(b1, 1, 0)
79         grid.addWidget(b2, 2, 0)
80         vbox.addLayout(grid)
81
82         grid2 = QGridLayout()
83         grid2.setSpacing(5)
84
85         class ClickableLabel(QLabel):
86             def mouseReleaseEvent(self, ev):
87                 self.emit(SIGNAL('clicked()'))
88
89         label2 = ClickableLabel(_("Wallet type:") + " [+]")
90         hbox = QHBoxLayout()
91         hbox.addWidget(label2)
92         grid2.addLayout(hbox, 0, 0)
93         
94         gb2 = QGroupBox()
95         grid.addWidget(gb2, 3, 0)
96         group2 = QButtonGroup()
97
98         self.wallet_types = [ 
99             ('standard', _("Standard wallet"),          Wallet), 
100             ('2of2',     _("Multisig wallet (2 of 2)"), Wallet_2of2),
101             ('2of3',     _("Multisig wallet (2 of 3)"), Wallet_2of3)
102         ]
103         run_hook('add_wallet_types', self.wallet_types)
104
105         for i, (t,l,c) in enumerate(self.wallet_types):
106             button = QRadioButton(gb2)
107             button.setText(l)
108             grid2.addWidget(button, i+1, 0)
109             group2.addButton(button)
110             group2.setId(button, i)
111             if i==0:
112                 button.setChecked(True)
113             #else:
114             #    button.setHidden(True)
115
116
117         def toggle():
118             buttons = group2.buttons()
119             x = buttons[1].isHidden()
120             label2.setText(_("Wallet type:") + (' [+]' if x else ' [-]'))
121             for b in buttons[1:]:
122                 b.setHidden(not x)
123
124         self.connect(label2, SIGNAL('clicked()'), toggle)
125         grid2.addWidget(label2)
126  
127         vbox.addLayout(grid2)
128         vbox.addStretch(1)
129         hbox, button = ok_cancel_buttons2(self, _('Next'))
130         vbox.addLayout(hbox)
131         self.set_layout(vbox)
132         self.show()
133         self.raise_()
134         button.setDefault(True)
135
136         if not self.exec_():
137             return None, None
138         
139         action = 'create' if b1.isChecked() else 'restore'
140         wallet_type = self.wallet_types[group2.checkedId()][0]
141         return action, wallet_type
142
143
144     def verify_seed(self, seed, sid):
145         r = self.enter_seed_dialog(MSG_VERIFY_SEED, sid)
146         if not r:
147             return
148
149         if r != seed:
150             QMessageBox.warning(None, _('Error'), _('Incorrect seed'), _('OK'))
151             return False
152         else:
153             return True
154
155
156     def get_seed_text(self, seed_e):
157         text = unicode(seed_e.toPlainText()).strip()
158         text = ' '.join(text.split())
159         return text
160
161     def is_any(self, seed_e):
162         text = self.get_seed_text(seed_e)
163         return Wallet.is_seed(text) or Wallet.is_old_mpk(text) or Wallet.is_xpub(text) or Wallet.is_xprv(text) or Wallet.is_address(text) or Wallet.is_private_key(text)
164
165     def is_mpk(self, seed_e):
166         text = self.get_seed_text(seed_e)
167         return Wallet.is_xpub(text) or Wallet.is_old_mpk(text)
168
169     def is_xpub(self, seed_e):
170         text = self.get_seed_text(seed_e)
171         return Wallet.is_xpub(text)
172
173     def enter_seed_dialog(self, msg, sid):
174         vbox, seed_e = seed_dialog.enter_seed_box(msg, sid)
175         vbox.addStretch(1)
176         hbox, button = ok_cancel_buttons2(self, _('Next'))
177         vbox.addLayout(hbox)
178         button.setEnabled(False)
179         seed_e.textChanged.connect(lambda: button.setEnabled(self.is_any(seed_e)))
180         self.set_layout(vbox)
181         if not self.exec_():
182             return
183         return self.get_seed_text(seed_e)
184
185
186     def multi_mpk_dialog(self, xpub_hot, n):
187         vbox = QVBoxLayout()
188         vbox0, seed_e0 = seed_dialog.enter_seed_box(MSG_SHOW_MPK, 'hot')
189         vbox.addLayout(vbox0)
190         seed_e0.setText(xpub_hot)
191         seed_e0.setReadOnly(True)
192         entries = []
193         for i in range(n):
194             vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_COLD_MPK, 'cold')
195             vbox.addLayout(vbox2)
196             entries.append(seed_e2)
197         vbox.addStretch(1)
198         hbox, button = ok_cancel_buttons2(self, _('Next'))
199         vbox.addLayout(hbox)
200         button.setEnabled(False)
201         f = lambda: button.setEnabled( map(lambda e: self.is_xpub(e), entries) == [True]*len(entries))
202         for e in entries:
203             e.textChanged.connect(f)
204         self.set_layout(vbox)
205         if not self.exec_():
206             return
207         return map(lambda e: self.get_seed_text(e), entries)
208
209
210     def multi_seed_dialog(self, n):
211         vbox = QVBoxLayout()
212         vbox1, seed_e1 = seed_dialog.enter_seed_box(MSG_ENTER_SEED_OR_MPK, 'hot')
213         vbox.addLayout(vbox1)
214         entries = [seed_e1]
215         for i in range(n):
216             vbox2, seed_e2 = seed_dialog.enter_seed_box(MSG_ENTER_SEED_OR_MPK, 'cold')
217             vbox.addLayout(vbox2)
218             entries.append(seed_e2)
219         vbox.addStretch(1)
220         hbox, button = ok_cancel_buttons2(self, _('Next'))
221         vbox.addLayout(hbox)
222         button.setEnabled(False)
223
224         f = lambda: button.setEnabled( map(lambda e: self.is_any(e), entries) == [True]*len(entries))
225         for e in entries:
226             e.textChanged.connect(f)
227
228         self.set_layout(vbox)
229         if not self.exec_():
230             return 
231         return map(lambda e: self.get_seed_text(e), entries)
232
233
234
235
236
237     def waiting_dialog(self, task, msg= _("Electrum is generating your addresses, please wait.")):
238         def target():
239             task()
240             self.emit(QtCore.SIGNAL('accept'))
241
242         vbox = QVBoxLayout()
243         self.waiting_label = QLabel(msg)
244         vbox.addWidget(self.waiting_label)
245         self.set_layout(vbox)
246         t = threading.Thread(target = target)
247         t.start()
248         self.exec_()
249
250
251
252
253     def network_dialog(self):
254         
255         grid = QGridLayout()
256         grid.setSpacing(5)
257
258         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" \
259                       + _("How do you want to connect to a server:")+" ")
260         label.setWordWrap(True)
261         grid.addWidget(label, 0, 0)
262
263         gb = QGroupBox()
264
265         b1 = QRadioButton(gb)
266         b1.setText(_("Auto connect"))
267         b1.setChecked(True)
268
269         b2 = QRadioButton(gb)
270         b2.setText(_("Select server manually"))
271
272         #b3 = QRadioButton(gb)
273         #b3.setText(_("Stay offline"))
274
275         grid.addWidget(b1,1,0)
276         grid.addWidget(b2,2,0)
277         #grid.addWidget(b3,3,0)
278
279         vbox = QVBoxLayout()
280         vbox.addLayout(grid)
281
282         vbox.addStretch(1)
283         vbox.addLayout(ok_cancel_buttons(self, _('Next')))
284
285         self.set_layout(vbox)
286         if not self.exec_():
287             return
288         
289         if b2.isChecked():
290             return NetworkDialog(self.network, self.config, None).do_exec()
291
292         elif b1.isChecked():
293             self.config.set_key('auto_cycle', True, True)
294             return
295
296         else:
297             self.config.set_key("server", None, True)
298             self.config.set_key('auto_cycle', False, True)
299             return
300         
301
302     def show_message(self, msg, icon=None):
303         vbox = QVBoxLayout()
304         self.set_layout(vbox)
305         if icon:
306             logo = QLabel()
307             logo.setPixmap(icon)
308             vbox.addWidget(logo)
309         vbox.addWidget(QLabel(msg))
310         vbox.addStretch(1)
311         vbox.addLayout(close_button(self, _('Next')))
312         if not self.exec_(): 
313             return None
314
315
316     def question(self, msg, icon=None):
317         vbox = QVBoxLayout()
318         self.set_layout(vbox)
319         if icon:
320             logo = QLabel()
321             logo.setPixmap(icon)
322             vbox.addWidget(logo)
323         vbox.addWidget(QLabel(msg))
324         vbox.addStretch(1)
325         vbox.addLayout(ok_cancel_buttons(self, _('OK')))
326         if not self.exec_(): 
327             return None
328         return True
329
330
331     def show_seed(self, seed, sid):
332         vbox = seed_dialog.show_seed_box(seed, sid)
333         vbox.addLayout(ok_cancel_buttons(self, _("Next")))
334         self.set_layout(vbox)
335         return self.exec_()
336
337
338     def password_dialog(self):
339         msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
340               +_("Leave these fields empty if you want to disable encryption.")
341         from password_dialog import make_password_dialog, run_password_dialog
342         self.set_layout( make_password_dialog(self, None, msg) )
343         return run_password_dialog(self, None, self)[2]
344
345
346     def create_cold_seed(self, wallet):
347         from electrum.bitcoin import mnemonic_to_seed, bip32_root
348         msg = _('You are about to generate the cold storage seed of your wallet.') + '\n' \
349               + _('For safety, you should do this on an offline computer.')
350         icon = QPixmap( ':icons/cold_seed.png').scaledToWidth(56)
351         if not self.question(msg, icon):
352             return
353
354         cold_seed = wallet.make_seed()
355         if not self.show_seed(cold_seed, 'cold'):
356             return
357         if not self.verify_seed(cold_seed, 'cold'):
358             return
359
360         hex_seed = mnemonic_to_seed(cold_seed,'').encode('hex')
361         xpriv, xpub = bip32_root(hex_seed)
362         wallet.add_master_public_key('cold/', xpub)
363
364         msg = _('Your master public key was saved in your wallet file.') + '\n'\
365               + _('Your cold seed must be stored on paper; it is not in the wallet file.')+ '\n\n' \
366               + _('This program is about to close itself.') + '\n'\
367               + _('You will need to reopen your wallet on an online computer, in order to complete the creation of your wallet')
368         self.show_message(msg)
369
370
371
372     def run(self, action):
373
374         if action == 'new':
375             action, wallet_type = self.restore_or_create()
376             self.storage.put('wallet_type', wallet_type, False)
377
378         if action is None:
379             return
380
381         if action == 'restore':
382             wallet = self.restore(wallet_type)
383             if not wallet:
384                 return
385             action = None
386
387         else:
388             wallet = Wallet(self.storage)
389             action = wallet.get_action()
390             # fixme: password is only needed for multiple accounts
391             password = None
392
393         while action is not None:
394
395             util.print_error("installwizard:", wallet, action)
396
397             if action == 'create_seed':
398                 seed = wallet.make_seed()
399                 if not self.show_seed(seed, None):
400                     return
401                 if not self.verify_seed(seed, None):
402                     return
403                 password = self.password_dialog()
404                 wallet.add_seed(seed, password)
405
406             elif action == 'add_cosigner':
407                 xpub_hot = wallet.master_public_keys.get("m/")
408                 r = self.multi_mpk_dialog(xpub_hot, 1)
409                 if not r:
410                     return
411                 xpub_cold = r[0]
412                 wallet.add_master_public_key("cold/", xpub_cold)
413
414             elif action == 'add_two_cosigners':
415                 xpub_hot = wallet.master_public_keys.get("m/")
416                 r = self.multi_mpk_dialog(xpub_hot, 2)
417                 if not r:
418                     return
419                 xpub1, xpub2 = r
420                 wallet.add_master_public_key("cold/", xpub1)
421                 wallet.add_master_public_key("remote/", xpub2)
422
423             elif action == 'create_accounts':
424                 wallet.create_accounts(password)
425                 self.waiting_dialog(wallet.synchronize)
426
427             elif action == 'create_cold_seed':
428                 self.create_cold_seed(wallet)
429                 return
430
431             else:
432                  r = run_hook('install_wizard_action', self, wallet, action)
433                  if not r: 
434                      raise BaseException('unknown wizard action', action)
435
436             # next action
437             action = wallet.get_action()
438
439
440         if self.network:
441             if self.network.interfaces:
442                 self.network_dialog()
443             else:
444                 QMessageBox.information(None, _('Warning'), _('You are offline'), _('OK'))
445                 self.network.stop()
446                 self.network = None
447
448         # start wallet threads
449         wallet.start_threads(self.network)
450
451         if action == 'restore':
452             self.waiting_dialog(lambda: wallet.restore(self.waiting_label.setText))
453             if self.network:
454                 if wallet.is_found():
455                     QMessageBox.information(None, _('Information'), _("Recovery successful"), _('OK'))
456                 else:
457                     QMessageBox.information(None, _('Information'), _("No transactions found for this seed"), _('OK'))
458             else:
459                 QMessageBox.information(None, _('Information'), _("This wallet was restored offline. It may contain more addresses than displayed."), _('OK'))
460
461         return wallet
462
463
464
465     def restore(self, t):
466
467             if t == 'standard':
468                 text = self.enter_seed_dialog(MSG_ENTER_ANYTHING, None)
469                 if not text:
470                     return
471                 if Wallet.is_seed(text):
472                     password = self.password_dialog()
473                     wallet = Wallet.from_seed(text, self.storage)
474                     wallet.add_seed(text, password)
475                     wallet.create_accounts(password)
476                 elif Wallet.is_xprv(text):
477                     password = self.password_dialog()
478                     wallet = Wallet.from_xprv(text, password, self.storage)
479                 elif Wallet.is_old_mpk(text):
480                     wallet = Wallet.from_old_mpk(text, self.storage)
481                 elif Wallet.is_xpub(text):
482                     wallet = Wallet.from_xpub(text, self.storage)
483                 elif Wallet.is_address(text):
484                     wallet = Wallet.from_address(text, self.storage)
485                 elif Wallet.is_private_key(text):
486                     wallet = Wallet.from_private_key(text, self.storage)
487                 else:
488                     raise
489
490             elif t in ['2of2']:
491                 r = self.multi_seed_dialog(1)
492                 if not r: 
493                     return
494                 text1, text2 = r
495                 wallet = Wallet_2of2(self.storage)
496                 if Wallet.is_seed(text1) or Wallet.is_seed(text2):
497                     password = self.password_dialog()
498                 else:
499                     password = None
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                 else:
508                     assert Wallet.is_xpub(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                 wallet.create_accounts(password)
517
518
519             elif t in ['2of3']:
520                 r = self.multi_seed_dialog(2)
521                 if not r: 
522                     return
523                 text1, text2, text3 = r
524                 wallet = Wallet_2of3(self.storage)
525                 if Wallet.is_seed(text1) or Wallet.is_seed(text2) or Wallet.is_seed(text3):
526                     password = self.password_dialog()
527                 else:
528                     password = None
529
530                 if Wallet.is_seed(text1):
531                     wallet.add_seed(text1, password)
532                     if Wallet.is_seed(text2):
533                         wallet.add_cold_seed(text2, password)
534                     else:
535                         wallet.add_master_public_key("cold/", text2)
536
537                 elif Wallet.is_xpub(text1):
538                     if Wallet.is_seed(text2):
539                         wallet.add_seed(text2, password)
540                         wallet.add_master_public_key("cold/", text1)
541                     else:
542                         wallet.add_master_public_key("m/", text1)
543                         wallet.add_master_public_key("cold/", text2)
544
545                 wallet.create_accounts(password)
546
547             else:
548                 wallet = run_hook('installwizard_restore', self, self.storage)
549
550             # create first keys offline
551             self.waiting_dialog(wallet.synchronize)
552                 
553             return wallet