installwizard: multisig wallets
[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_2of3
7
8 import seed_dialog
9 from network_dialog import NetworkDialog
10 from util import *
11 from amountedit import AmountEdit
12
13 import sys
14 import threading
15 from electrum.plugins import run_hook
16
17 class InstallWizard(QDialog):
18
19     def __init__(self, config, network, storage):
20         QDialog.__init__(self)
21         self.config = config
22         self.network = network
23         self.storage = storage
24         self.setMinimumSize(575, 400)
25         self.setWindowTitle('Electrum')
26         self.connect(self, QtCore.SIGNAL('accept'), self.accept)
27
28         self.stack = QStackedLayout()
29         self.setLayout(self.stack)
30
31
32     def set_layout(self, layout):
33         w = QWidget()
34         w.setLayout(layout)
35         self.stack.setCurrentIndex(self.stack.addWidget(w))
36
37
38     def restore_or_create(self):
39
40         grid = QGridLayout()
41         grid.setSpacing(5)
42
43         msg = _("Electrum could not find an existing wallet.") + "\n\n" \
44             + _("What do you want to do?") + "\n"
45         label = QLabel(msg)
46         label.setWordWrap(True)
47         grid.addWidget(label, 0, 0)
48
49         gb = QGroupBox()
50
51         b1 = QRadioButton(gb)
52         b1.setText(_("Create new wallet"))
53         b1.setChecked(True)
54
55         b2 = QRadioButton(gb)
56         b2.setText(_("Restore an existing wallet"))
57
58         grid.addWidget(b1,1,0)
59         grid.addWidget(b2,2,0)
60
61         vbox = QVBoxLayout()
62         self.set_layout(vbox)
63
64         vbox.addLayout(grid)
65         vbox.addStretch(1)
66         vbox.addLayout(ok_cancel_buttons(self, _('Next')))
67
68         if not self.exec_():
69             return
70         
71         return 'create' if b1.isChecked() else 'restore'
72
73
74
75     def verify_seed(self, seed, sid):
76         r = self.enter_seed_dialog(False, sid)
77         if not r:
78             return
79
80         if r != seed:
81             QMessageBox.warning(None, _('Error'), _('Incorrect seed'), _('OK'))
82             return False
83         else:
84             return True
85
86
87     def get_seed_text(self, seed_e):
88         return unicode(seed_e.toPlainText())
89
90
91     def is_seed(self, seed_e):
92         text = self.get_seed_text(seed_e)
93         return Wallet.is_seed(text) or Wallet.is_mpk(text)
94
95
96     def enter_seed_dialog(self, is_restore, sid):
97         vbox, seed_e = seed_dialog.enter_seed_box(is_restore, sid)
98         vbox.addStretch(1)
99         hbox, button = ok_cancel_buttons2(self, _('Next'))
100         vbox.addLayout(hbox)
101         button.setEnabled(False)
102         seed_e.textChanged.connect(lambda: button.setEnabled(self.is_seed(seed_e)))
103         self.set_layout(vbox)
104         if not self.exec_():
105             return
106         return self.get_seed_text(seed_e)
107
108
109     def double_seed_dialog(self):
110         vbox = QVBoxLayout()
111         vbox1, seed_e1 = seed_dialog.enter_seed_box(True, 'hot')
112         vbox2, seed_e2 = seed_dialog.enter_seed_box(True, 'cold')
113         vbox.addLayout(vbox1)
114         vbox.addLayout(vbox2)
115         vbox.addStretch(1)
116         hbox, button = ok_cancel_buttons2(self, _('Next'))
117         vbox.addLayout(hbox)
118         button.setEnabled(False)
119         f = lambda: button.setEnabled(self.is_seed(seed_e1) and self.is_seed(seed_e2))
120         seed_e1.textChanged.connect(f)
121         seed_e2.textChanged.connect(f)
122         self.set_layout(vbox)
123         if not self.exec_():
124             return 
125         return self.get_seed_text(seed_e1), self.get_seed_text(seed_e2)
126
127
128
129
130     def waiting_dialog(self, task, msg= _("Electrum is generating your addresses, please wait.")):
131         def target():
132             task()
133             self.emit(QtCore.SIGNAL('accept'))
134
135         vbox = QVBoxLayout()
136         self.waiting_label = QLabel(msg)
137         vbox.addWidget(self.waiting_label)
138         self.set_layout(vbox)
139         t = threading.Thread(target = target)
140         t.start()
141         self.exec_()
142
143
144
145
146     def network_dialog(self):
147         
148         grid = QGridLayout()
149         grid.setSpacing(5)
150
151         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" \
152                       + _("How do you want to connect to a server:")+" ")
153         label.setWordWrap(True)
154         grid.addWidget(label, 0, 0)
155
156         gb = QGroupBox()
157
158         b1 = QRadioButton(gb)
159         b1.setText(_("Auto connect"))
160         b1.setChecked(True)
161
162         b2 = QRadioButton(gb)
163         b2.setText(_("Select server manually"))
164
165         #b3 = QRadioButton(gb)
166         #b3.setText(_("Stay offline"))
167
168         grid.addWidget(b1,1,0)
169         grid.addWidget(b2,2,0)
170         #grid.addWidget(b3,3,0)
171
172         vbox = QVBoxLayout()
173         vbox.addLayout(grid)
174
175         vbox.addStretch(1)
176         vbox.addLayout(ok_cancel_buttons(self, _('Next')))
177
178         self.set_layout(vbox)
179         if not self.exec_():
180             return
181         
182         if b2.isChecked():
183             return NetworkDialog(self.network, self.config, None).do_exec()
184
185         elif b1.isChecked():
186             self.config.set_key('auto_cycle', True, True)
187             return
188
189         else:
190             self.config.set_key("server", None, True)
191             self.config.set_key('auto_cycle', False, True)
192             return
193         
194
195     def show_message(self, msg):
196         vbox = QVBoxLayout()
197         vbox.addWidget(QLabel(msg))
198         vbox.addStretch(1)
199         vbox.addLayout(close_button(self, _('Next')))
200         self.set_layout(vbox)
201         if not self.exec_(): 
202             return None
203
204     def question(self, msg):
205         vbox = QVBoxLayout()
206         vbox.addWidget(QLabel(msg))
207         vbox.addStretch(1)
208         vbox.addLayout(ok_cancel_buttons(self, _('OK')))
209         self.set_layout(vbox)
210         if not self.exec_(): 
211             return None
212         return True
213
214
215     def show_seed(self, seed, sid):
216         vbox = seed_dialog.show_seed_box(seed, sid)
217         vbox.addLayout(ok_cancel_buttons(self, _("Next")))
218         self.set_layout(vbox)
219         return self.exec_()
220
221
222     def password_dialog(self, wallet):
223         msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
224               +_("Leave these fields empty if you want to disable encryption.")
225         from password_dialog import make_password_dialog, run_password_dialog
226         self.set_layout( make_password_dialog(self, wallet, msg) )
227         return run_password_dialog(self, wallet, self)
228
229
230     def choose_wallet_type(self):
231         grid = QGridLayout()
232         grid.setSpacing(5)
233
234         msg = _("Choose your wallet.")
235         label = QLabel(msg)
236         label.setWordWrap(True)
237         grid.addWidget(label, 0, 0)
238
239         gb = QGroupBox()
240
241         b1 = QRadioButton(gb)
242         b1.setText(_("Standard wallet (protected by password)"))
243         b1.setChecked(True)
244
245         b2 = QRadioButton(gb)
246         b2.setText(_("Multi-signature wallet (two-factor authentication)"))
247
248         grid.addWidget(b1,1,0)
249         grid.addWidget(b2,2,0)
250
251         vbox = QVBoxLayout()
252
253         vbox.addLayout(grid)
254         vbox.addStretch(1)
255         vbox.addLayout(ok_cancel_buttons(self, _('Next')))
256
257         self.set_layout(vbox)
258         if not self.exec_():
259             return
260         
261         if b1.isChecked():
262             return 'standard'
263         elif b2.isChecked():
264             return '2of3'
265
266
267     def run(self, action = None):
268
269         if action is None:
270             action = self.restore_or_create()
271
272         if action is None: 
273             return
274
275         if action == 'create':            
276             t = self.choose_wallet_type()
277             if not t:
278                 return 
279
280             if t == '2of3':
281                 run_hook('create_cold_seed', self.storage, self)
282                 return
283
284
285         if action in ['create', 'create2of3']:
286
287             wallet = Wallet(self.storage)
288
289             wallet.init_seed(None)
290             seed = wallet.get_mnemonic(None)
291             sid = 'hot' if action == 'create2of3' else None
292             if not self.show_seed(seed, sid):
293                 return
294             if not self.verify_seed(seed, sid):
295                 return
296             ok, old_password, password = self.password_dialog(wallet)
297             wallet.save_seed(password)
298
299             if action == 'create2of3':
300                 run_hook('create_third_key', wallet, self)
301                 if not wallet.master_public_keys.get("remote/"):
302                     return
303
304             wallet.create_accounts(password)
305             # generate first addresses offline
306             self.waiting_dialog(wallet.synchronize)
307
308         elif action == 'restore':
309             # dialog box will accept either seed or xpub. 
310             # use two boxes for 2of3
311             t = self.choose_wallet_type()
312             if not t: 
313                 return
314
315             if t == 'standard':
316                 text = self.enter_seed_dialog(True, None)
317                 if Wallet.is_seed(text):
318                     wallet = Wallet.from_seed(text, self.storage)
319                     ok, old_password, password = self.password_dialog(wallet)
320                     wallet.save_seed(password)
321                     wallet.create_accounts(password)
322                 elif Wallet.is_mpk(text):
323                     wallet = Wallet.from_mpk(text, self.storage)
324                 else:
325                     return
326
327             elif t in ['2of2', '2of3']:
328                 r = self.double_seed_dialog()
329                 if not r: 
330                     return
331                 text1, text2 = r
332                 wallet = Wallet_2of3(self.storage)
333
334                 if Wallet.is_seed(text1):
335                     xpriv, xpub = bip32_root(text1)
336                 elif Wallet.is_mpk(text1):
337                     xpub = text1
338                 wallet.add_master_public_key("m/", xpub)
339
340                 if Wallet.is_seed(text2):
341                     xpriv2, xpub2 = bip32_root(text2)
342                 elif Wallet.is_mpk(text2):
343                     xpub2 = text2
344                 wallet.add_master_public_key("cold/", xpub2)
345
346                 run_hook('restore_third_key', wallet, self)
347
348                 wallet.create_accounts(None)
349
350
351
352
353         else: raise
354                 
355         #if not self.config.get('server'):
356         if self.network:
357             if self.network.interfaces:
358                 self.network_dialog()
359             else:
360                 QMessageBox.information(None, _('Warning'), _('You are offline'), _('OK'))
361                 self.network.stop()
362                 self.network = None
363
364         # start wallet threads
365         wallet.start_threads(self.network)
366
367         if action == 'restore':
368
369             self.waiting_dialog(lambda: wallet.restore(self.waiting_label.setText))
370
371             if self.network:
372                 if wallet.is_found():
373                     QMessageBox.information(None, _('Information'), _("Recovery successful"), _('OK'))
374                 else:
375                     QMessageBox.information(None, _('Information'), _("No transactions found for this seed"), _('OK'))
376             else:
377                 QMessageBox.information(None, _('Information'), _("This wallet was restored offline. It may contain more addresses than displayed."), _('OK'))
378
379         return wallet