better errors
[electrum-nvc.git] / client / gui_qt.py
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 import sys, time, datetime, re
20
21 # todo: see PySide
22
23 from PyQt4.QtGui import *
24 from PyQt4.QtCore import *
25 import PyQt4.QtCore as QtCore
26 import PyQt4.QtGui as QtGui
27
28 try:
29     import icons_rc
30 except:
31     print "Could not import icons_rp.py"
32     print "Please generate it with: 'pyrcc4 icons.qrc -o icons_rc.py'"
33     sys.exit(1)
34
35 from wallet import format_satoshis
36 from decimal import Decimal
37
38
39 def numbify(entry, is_int = False):
40     text = unicode(entry.text()).strip()
41     chars = '0123456789'
42     if not is_int: chars +='.'
43     s = ''.join([i for i in text if i in chars])
44     if not is_int:
45         if '.' in s:
46             p = s.find('.')
47             s = s.replace('.','')
48             s = s[:p] + '.' + s[p:p+8]
49         try:
50             amount = int( Decimal(s) * 100000000 )
51         except:
52             amount = None
53     else:
54         try:
55             amount = int( s )
56         except:
57             amount = None
58     entry.setText(s)
59     return amount
60
61
62 class Timer(QtCore.QThread):
63     def run(self):
64         while True:
65             self.emit(QtCore.SIGNAL('timersignal'))
66             time.sleep(0.5)
67
68 class EnterButton(QPushButton):
69     def __init__(self, text, func):
70         QPushButton.__init__(self, text)
71         self.func = func
72         self.clicked.connect(func)
73
74     def keyPressEvent(self, e):
75         if e.key() == QtCore.Qt.Key_Return:
76             apply(self.func,())
77
78 class StatusBarButton(QPushButton):
79     def __init__(self, icon, tooltip, func):
80         QPushButton.__init__(self, icon, '')
81         self.setToolTip(tooltip)
82         self.setFlat(True)
83         self.setMaximumWidth(25)
84         self.clicked.connect(func)
85         self.func = func
86
87     def keyPressEvent(self, e):
88         if e.key() == QtCore.Qt.Key_Return:
89             apply(self.func,())
90
91
92 class QRCodeWidget(QWidget):
93
94     def __init__(self, addr):
95         import pyqrnative
96         super(QRCodeWidget, self).__init__()
97         self.addr = addr
98         self.setGeometry(300, 300, 350, 350)
99         self.setWindowTitle('Colors')
100         self.show()
101         self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
102         self.qr.addData(addr)
103         self.qr.make()
104         
105     def paintEvent(self, e):
106         qp = QtGui.QPainter()
107         qp.begin(self)
108         boxsize = 7
109         size = self.qr.getModuleCount()*boxsize
110         k = self.qr.getModuleCount()
111         black = QColor(0, 0, 0, 255)
112         white = QColor(255, 255, 255, 255)
113         for r in range(k):
114             for c in range(k):
115                 if self.qr.isDark(r, c):
116                     qp.setBrush(black)
117                     qp.setPen(black)
118                 else:
119                     qp.setBrush(white)
120                     qp.setPen(white)
121                 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
122         qp.end()
123         
124
125
126 def ok_cancel_buttons(dialog):
127     hbox = QHBoxLayout()
128     hbox.addStretch(1)
129     b = QPushButton("OK")
130     hbox.addWidget(b)
131     b.clicked.connect(dialog.accept)
132     b = QPushButton("Cancel")
133     hbox.addWidget(b)
134     b.clicked.connect(dialog.reject)
135     return hbox
136
137
138 class ElectrumWindow(QMainWindow):
139
140     def __init__(self, wallet):
141         QMainWindow.__init__(self)
142         self.wallet = wallet
143         self.funds_error = False
144
145         self.tabs = tabs = QTabWidget(self)
146         tabs.addTab(self.create_history_tab(), 'History')
147         tabs.addTab(self.create_send_tab(),    'Send')
148         tabs.addTab(self.create_receive_tab(), 'Receive')
149         tabs.addTab(self.create_contacts_tab(),'Contacts')
150         tabs.addTab(self.create_wall_tab(),    'Wall')
151         tabs.setMinimumSize(600, 400)
152         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
153         self.setCentralWidget(tabs)
154         self.create_status_bar()
155         self.setGeometry(100,100,840,400)
156         self.setWindowTitle( 'Electrum ' + self.wallet.electrum_version )
157         self.show()
158
159         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
160         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
161         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
162         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
163
164
165
166     def connect_slots(self, sender):
167         self.connect(sender, QtCore.SIGNAL('timersignal'), self.update_wallet)
168         self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
169         self.previous_payto_e=''
170
171     def check_recipient(self):
172         if self.payto_e.hasFocus():
173             return
174         r = unicode( self.payto_e.text() )
175         if r != self.previous_payto_e:
176             self.previous_payto_e = r
177             r = r.strip()
178             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
179                 try:
180                     to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
181                 except:
182                     return
183                 if to_address:
184                     s = r + ' <' + to_address + '>'
185                     self.payto_e.setText(s)
186
187
188     def update_wallet(self):
189         if self.wallet.interface.is_connected:
190             if self.wallet.blocks == 0:
191                 text = "Server not ready"
192                 icon = QIcon(":icons/status_disconnected.png")
193             elif not self.wallet.up_to_date:
194                 text = "Synchronizing..."
195                 icon = QIcon(":icons/status_waiting.png")
196             else:
197                 c, u = self.wallet.get_balance()
198                 text =  "Balance: %s "%( format_satoshis(c) )
199                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True) )
200                 icon = QIcon(":icons/status_connected.png")
201         else:
202             text = "Not connected"
203             icon = QIcon(":icons/status_disconnected.png")
204
205         if self.funds_error:
206             text = "Not enough funds"
207
208         self.statusBar().showMessage(text)
209         self.status_button.setIcon( icon )
210
211         if self.wallet.was_updated and self.wallet.up_to_date:
212             self.wallet.was_updated = False
213             self.textbox.setText( self.wallet.banner )
214             self.update_history_tab()
215             self.update_receive_tab()
216             self.update_contacts_tab()
217
218
219     def create_history_tab(self):
220         self.history_list = w = QTreeWidget(self)
221         #print w.getContentsMargins()
222         w.setColumnCount(5)
223         w.setColumnWidth(0, 40) 
224         w.setColumnWidth(1, 140) 
225         w.setColumnWidth(2, 350) 
226         w.setColumnWidth(3, 140) 
227         w.setColumnWidth(4, 140) 
228         w.setHeaderLabels( [ '', 'Date', 'Description', 'Amount', 'Balance'] )
229         self.connect(w, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.tx_details)
230         self.connect(w, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
231         self.connect(w, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
232         return w
233
234     def tx_details(self, item, column):
235         tx_hash = str(item.toolTip(0))
236         tx = self.wallet.tx_history.get(tx_hash)
237
238         if tx['height']:
239             conf = self.wallet.blocks - tx['height'] + 1
240             time_str = datetime.datetime.fromtimestamp( tx['nTime']).isoformat(' ')[:-3]
241         else:
242             conf = 0
243             time_str = 'pending'
244
245         tx_details = "Transaction Details:\n\n" \
246             + "Transaction ID:\n" + tx_hash + "\n\n" \
247             + "Status: %d confirmations\n\n"%conf  \
248             + "Date: %s\n\n"%time_str \
249             + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
250             + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
251
252         r = self.wallet.receipts.get(tx_hash)
253         if r:
254             tx_details += "\n_______________________________________" \
255                 + '\n\nSigned URI: ' + r[2] \
256                 + "\n\nSigned by: " + r[0] \
257                 + '\n\nSignature: ' + r[1]
258
259         QMessageBox.information(self, 'Details', tx_details, 'OK')
260
261
262     def tx_label_clicked(self, item, column):
263         if column==2 and item.isSelected():
264             tx_hash = str(item.toolTip(0))
265             self.is_edit=True
266             #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
267             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
268             self.history_list.editItem( item, column )
269             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
270             self.is_edit=False
271
272     def tx_label_changed(self, item, column):
273         if self.is_edit: 
274             return
275         self.is_edit=True
276         tx_hash = str(item.toolTip(0))
277         tx = self.wallet.tx_history.get(tx_hash)
278         s = self.wallet.labels.get(tx_hash)
279         text = unicode( item.text(2) )
280         if text: 
281             self.wallet.labels[tx_hash] = text
282             item.setForeground(2, QBrush(QColor('black')))
283         else:
284             if s: self.wallet.labels.pop(tx_hash)
285             text = tx['default_label']
286             item.setText(2, text)
287             item.setForeground(2, QBrush(QColor('gray')))
288         self.is_edit=False
289
290     def address_label_clicked(self, item, column, l):
291         if column==1 and item.isSelected():
292             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
293             l.editItem( item, column )
294             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
295
296     def address_label_changed(self, item, column, l):
297         addr = unicode( item.text(0) )
298         text = unicode( item.text(1) )
299         if text:
300             self.wallet.labels[addr] = text
301         else:
302             s = self.wallet.labels.get(addr)
303             if s: self.wallet.labels.pop(addr)
304         self.update_history_tab()
305
306     def update_history_tab(self):
307         self.history_list.clear()
308         balance = 0
309         for tx in self.wallet.get_tx_history():
310             tx_hash = tx['tx_hash']
311             if tx['height']:
312                 conf = self.wallet.blocks - tx['height'] + 1
313                 time_str = datetime.datetime.fromtimestamp( tx['nTime']).isoformat(' ')[:-3]
314                 icon = QIcon(":icons/confirmed.png")
315             else:
316                 conf = 0
317                 time_str = 'pending'
318                 icon = QIcon(":icons/unconfirmed.png")
319             v = tx['value']
320             balance += v 
321             label = self.wallet.labels.get(tx_hash)
322             is_default_label = (label == '') or (label is None)
323             if is_default_label: label = tx['default_label']
324
325             item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True), format_satoshis(balance)] )
326             item.setFont(2, QFont('monospace'))
327             item.setFont(3, QFont('monospace'))
328             item.setFont(4, QFont('monospace'))
329             item.setToolTip(0, tx_hash)
330             if is_default_label:
331                 item.setForeground(2, QBrush(QColor('grey')))
332
333             item.setIcon(0, icon)
334             self.history_list.insertTopLevelItem(0,item)
335
336
337     def create_send_tab(self):
338         w = QWidget()
339
340         grid = QGridLayout()
341         grid.setSpacing(8)
342         grid.setColumnMinimumWidth(3,300)
343         grid.setColumnStretch(4,1)
344
345         self.payto_e = QLineEdit()
346         grid.addWidget(QLabel('Pay to'), 1, 0)
347         grid.addWidget(self.payto_e, 1, 1, 1, 3)
348
349         self.message_e = QLineEdit()
350         grid.addWidget(QLabel('Description'), 2, 0)
351         grid.addWidget(self.message_e, 2, 1, 1, 3)
352
353         self.amount_e = QLineEdit()
354         grid.addWidget(QLabel('Amount'), 3, 0)
355         grid.addWidget(self.amount_e, 3, 1, 1, 2)
356         
357         self.fee_e = QLineEdit()
358         grid.addWidget(QLabel('Fee'), 4, 0)
359         grid.addWidget(self.fee_e, 4, 1, 1, 2)
360         
361         b = EnterButton("Send", self.do_send)
362         grid.addWidget(b, 5, 1)
363
364         b = EnterButton("Clear",self.do_clear)
365         grid.addWidget(b, 5, 2)
366
367         self.payto_sig = QLabel('')
368         grid.addWidget(self.payto_sig, 6, 0, 1, 4)
369
370         w.setLayout(grid) 
371         w.show()
372
373         w2 = QWidget()
374         vbox = QVBoxLayout()
375         vbox.addWidget(w)
376         vbox.addStretch(1)
377         w2.setLayout(vbox)
378
379         def entry_changed( is_fee ):
380             self.funds_error = False
381             amount = numbify(self.amount_e)
382             fee = numbify(self.fee_e)
383             if not is_fee: fee = None
384             if amount is None:
385                 return
386             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
387             if not is_fee:
388                 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
389             if inputs:
390                 palette = QPalette()
391                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
392             else:
393                 palette = QPalette()
394                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
395                 self.funds_error = True
396             self.amount_e.setPalette(palette)
397             self.fee_e.setPalette(palette)
398
399         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
400         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
401
402         return w2
403
404     def do_send(self):
405
406         label = unicode( self.message_e.text() )
407         r = unicode( self.payto_e.text() )
408         r = r.strip()
409
410         m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
411         m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
412         
413         if m1:
414             to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
415             if not to_address:
416                 return
417         elif m2:
418             to_address = m2.group(5)
419         else:
420             to_address = r
421
422         if not self.wallet.is_valid(to_address):
423             QMessageBox.warning(self, 'Error', 'Invalid Bitcoin Address:\n'+to_address, 'OK')
424             return
425
426         try:
427             amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
428         except:
429             QMessageBox.warning(self, 'Error', 'Invalid Amount', 'OK')
430             return
431         try:
432             fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
433         except:
434             QMessageBox.warning(self, 'Error', 'Invalid Fee', 'OK')
435             return
436
437         if self.wallet.use_encryption:
438             password = self.password_dialog()
439             if not password:
440                 return
441         else:
442             password = None
443
444         try:
445             tx = self.wallet.mktx( to_address, amount, label, password, fee )
446         except BaseException, e:
447             self.show_message(e.message)
448             return
449             
450         status, msg = self.wallet.sendtx( tx )
451         if status:
452             QMessageBox.information(self, '', 'Payment sent.\n'+msg, 'OK')
453             self.do_clear()
454             self.update_contacts_tab()
455         else:
456             QMessageBox.warning(self, 'Error', msg, 'OK')
457
458
459     def set_url(self, url):
460         payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
461         self.tabs.setCurrentIndex(1)
462         self.payto_e.setText(payto)
463         self.message_e.setText(message)
464         self.amount_e.setText(amount)
465         if identity:
466             self.set_frozen(self.payto_e,True)
467             self.set_frozen(self.amount_e,True)
468             self.set_frozen(self.message_e,True)
469             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
470         else:
471             self.payto_sig.setVisible(False)
472
473     def do_clear(self):
474         self.payto_sig.setVisible(False)
475         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
476             e.setText('')
477             self.set_frozen(e,False)
478
479     def set_frozen(self,entry,frozen):
480         if frozen:
481             entry.setReadOnly(True)
482             entry.setFrame(False)
483             palette = QPalette()
484             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
485             entry.setPalette(palette)
486         else:
487             entry.setReadOnly(False)
488             entry.setFrame(True)
489             palette = QPalette()
490             palette.setColor(entry.backgroundRole(), QColor('white'))
491             entry.setPalette(palette)
492
493
494     def make_address_list(self, is_recv):
495
496         l = QTreeWidget(self)
497         l.setColumnCount(3)
498         l.setColumnWidth(0, 350) 
499         l.setColumnWidth(1, 330)
500         l.setColumnWidth(2, 20) 
501         l.setHeaderLabels( ['Address', 'Label','Tx'])
502
503         vbox = QVBoxLayout()
504         vbox.setMargin(0)
505         vbox.setSpacing(0)
506         vbox.addWidget(l)
507
508         hbox = QHBoxLayout()
509         hbox.setMargin(0)
510         hbox.setSpacing(0)
511
512         def get_addr(l):
513             i = l.currentItem()
514             if not i: return
515             addr = unicode( i.text(0) )
516             return addr
517
518         def showqrcode(address):
519             if not address: return
520             d = QDialog(self)
521             d.setModal(1)
522             d.setMinimumSize(270, 300)
523             vbox = QVBoxLayout()
524             vbox.addWidget(QRCodeWidget(address))
525             vbox.addLayout(ok_cancel_buttons(d))
526             d.setLayout(vbox)
527             d.exec_()
528         qrButton = EnterButton("QR",lambda: showqrcode(get_addr(l)))
529
530         def copy2clipboard(addr):
531             self.app.clipboard().setText(addr)
532         copyButton = EnterButton("Copy to Clipboard", lambda: copy2clipboard(get_addr(l)))
533         hbox.addWidget(qrButton)
534         hbox.addWidget(copyButton)
535         if not is_recv:
536             addButton = EnterButton("New", self.newaddress_dialog)
537             hbox.addWidget(addButton)
538             def payto(addr):
539                 if not addr:return
540                 self.tabs.setCurrentIndex(1)
541                 self.payto_e.setText(addr)
542                 self.amount_e.setFocus()
543             paytoButton = EnterButton('Pay to', lambda: payto(get_addr(l)))
544             hbox.addWidget(paytoButton)
545         hbox.addStretch(1)
546         buttons = QWidget()
547         buttons.setLayout(hbox)
548         vbox.addWidget(buttons)
549
550         w = QWidget()
551         w.setLayout(vbox)
552         return w, l
553
554     def create_receive_tab(self):
555         w, l = self.make_address_list(True)
556         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l))
557         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l))
558         self.receive_list = l
559         return w
560
561     def create_contacts_tab(self):
562         w, l = self.make_address_list(False)
563         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l))
564         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l))
565         self.connect(l, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.show_contact_details)
566         self.contacts_list = l
567         return w
568
569     def update_receive_tab(self):
570         self.receive_list.clear()
571         for address in self.wallet.all_addresses():
572             if self.wallet.is_change(address):continue
573             label = self.wallet.labels.get(address,'')
574             n = 0 
575             h = self.wallet.history.get(address,[])
576             for item in h:
577                 if not item['is_in'] : n=n+1
578             tx = "None" if n==0 else "%d"%n
579             item = QTreeWidgetItem( [ address, label, tx] )
580             item.setFont(0, QFont('monospace'))
581             self.receive_list.addTopLevelItem(item)
582
583     def show_contact_details(self, item, column):
584         m = unicode(item.text(0))
585         a = self.wallet.aliases.get(m)
586         if a:
587             if a[0] in self.wallet.authorities.keys():
588                 s = self.wallet.authorities.get(a[0])
589             else:
590                 s = "self-signed"
591             msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
592             QMessageBox.information(self, 'Alias', msg, 'OK')
593
594     def update_contacts_tab(self):
595         self.contacts_list.clear()
596         for alias, v in self.wallet.aliases.items():
597             s, target = v
598             label = self.wallet.labels.get(alias,'')
599             item = QTreeWidgetItem( [ alias, label, '-'] )
600             self.contacts_list.addTopLevelItem(item)
601             
602         for address in self.wallet.addressbook:
603             label = self.wallet.labels.get(address,'')
604             n = 0 
605             for item in self.wallet.tx_history.values():
606                 if address in item['outputs'] : n=n+1
607             tx = "None" if n==0 else "%d"%n
608             item = QTreeWidgetItem( [ address, label, tx] )
609             item.setFont(0, QFont('monospace'))
610             self.contacts_list.addTopLevelItem(item)
611
612
613     def create_wall_tab(self):
614         self.textbox = textbox = QTextEdit(self)
615         textbox.setFont(QFont('monospace'))
616         textbox.setReadOnly(True)
617         return textbox
618
619     def create_status_bar(self):
620         sb = QStatusBar()
621         sb.setFixedHeight(35)
622         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
623         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
624         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
625         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) ) 
626         sb.addPermanentWidget( self.status_button )
627         self.setStatusBar(sb)
628
629     def newaddress_dialog(self):
630         text, ok = QInputDialog.getText(self, 'New Contact', 'Address:')
631         address = unicode(text)
632         if ok:
633             if self.wallet.is_valid(address):
634                 self.wallet.addressbook.append(address)
635                 self.wallet.save()
636                 self.update_contacts_tab()
637             else:
638                 QMessageBox.warning(self, 'Error', 'Invalid Address', 'OK')
639
640     @staticmethod
641     def show_seed_dialog(wallet, parent=None):
642         import mnemonic
643         if wallet.use_encryption:
644             password = parent.password_dialog()
645             if not password: return
646         else:
647             password = None
648             
649         try:
650             seed = wallet.pw_decode( wallet.seed, password)
651         except:
652             QMessageBox.warning(parent, 'Error', 'Invalid Password', 'OK')
653             return
654
655         msg = "Your wallet generation seed is:\n\n" + seed \
656               + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
657               + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" \
658               + ' '.join(mnemonic.mn_encode(seed)) + "\""
659
660         QMessageBox.information(parent, 'Seed', msg, 'OK')
661
662     def question(self, msg):
663         return QMessageBox.question(self, 'Message', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
664
665     def show_message(self, msg):
666         QMessageBox.information(self, 'Message', msg, 'OK')
667
668     def password_dialog(self ):
669         d = QDialog(self)
670         d.setModal(1)
671
672         pw = QLineEdit()
673         pw.setEchoMode(2)
674
675         vbox = QVBoxLayout()
676         msg = 'Please enter your password'
677         vbox.addWidget(QLabel(msg))
678
679         grid = QGridLayout()
680         grid.setSpacing(8)
681         grid.addWidget(QLabel('Password'), 1, 0)
682         grid.addWidget(pw, 1, 1)
683         vbox.addLayout(grid)
684
685         vbox.addLayout(ok_cancel_buttons(d))
686         d.setLayout(vbox) 
687
688         if not d.exec_(): return
689         return unicode(pw.text())
690
691     @staticmethod
692     def change_password_dialog( wallet, parent=None ):
693         d = QDialog(parent)
694         d.setModal(1)
695
696         pw = QLineEdit()
697         pw.setEchoMode(2)
698         new_pw = QLineEdit()
699         new_pw.setEchoMode(2)
700         conf_pw = QLineEdit()
701         conf_pw.setEchoMode(2)
702
703         vbox = QVBoxLayout()
704         if parent:
705             msg = 'Your wallet is encrypted. Use this dialog to change your password.\nTo disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted'
706         else:
707             msg = "Please choose a password to encrypt your wallet keys.\nLeave these fields empty if you want to disable encryption."
708         vbox.addWidget(QLabel(msg))
709
710         grid = QGridLayout()
711         grid.setSpacing(8)
712
713         if wallet.use_encryption:
714             grid.addWidget(QLabel('Password'), 1, 0)
715             grid.addWidget(pw, 1, 1)
716
717         grid.addWidget(QLabel('New Password'), 2, 0)
718         grid.addWidget(new_pw, 2, 1)
719
720         grid.addWidget(QLabel('Confirm Password'), 3, 0)
721         grid.addWidget(conf_pw, 3, 1)
722         vbox.addLayout(grid)
723
724         vbox.addLayout(ok_cancel_buttons(d))
725         d.setLayout(vbox) 
726
727         if not d.exec_(): return
728
729         password = unicode(pw.text()) if wallet.use_encryption else None
730         new_password = unicode(new_pw.text())
731         new_password2 = unicode(conf_pw.text())
732
733         try:
734             seed = wallet.pw_decode( wallet.seed, password)
735         except:
736             QMessageBox.warning(parent, 'Error', 'Incorrect Password', 'OK')
737             return
738
739         if new_password != new_password2:
740             QMessageBox.warning(parent, 'Error', 'Passwords do not match', 'OK')
741             return
742
743         wallet.update_password(seed, new_password)
744
745     @staticmethod
746     def seed_dialog(wallet, parent=None):
747         d = QDialog(parent)
748         d.setModal(1)
749
750         vbox = QVBoxLayout()
751         msg = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
752         vbox.addWidget(QLabel(msg))
753
754         grid = QGridLayout()
755         grid.setSpacing(8)
756
757         seed_e = QLineEdit()
758         grid.addWidget(QLabel('Seed or mnemonic'), 1, 0)
759         grid.addWidget(seed_e, 1, 1)
760
761         gap_e = QLineEdit()
762         gap_e.setText("5")
763         grid.addWidget(QLabel('Gap limit'), 2, 0)
764         grid.addWidget(gap_e, 2, 1)
765         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
766         vbox.addLayout(grid)
767
768         vbox.addLayout(ok_cancel_buttons(d))
769         d.setLayout(vbox) 
770
771         if not d.exec_(): return
772
773         try:
774             gap = int(unicode(gap_e.text()))
775         except:
776             QMessageBox.warning(None, 'Error', 'error', 'OK')
777             sys.exit(0)
778
779         try:
780             seed = unicode(seed_e.text())
781             seed.decode('hex')
782         except:
783             import mnemonic
784             print "not hex, trying decode"
785             try:
786                 seed = mnemonic.mn_decode( seed.split(' ') )
787             except:
788                 QMessageBox.warning(None, 'Error', 'I cannot decode this', 'OK')
789                 sys.exit(0)
790         if not seed:
791             QMessageBox.warning(None, 'Error', 'no seed', 'OK')
792             sys.exit(0)
793         
794         wallet.seed = str(seed)
795         #print repr(wallet.seed)
796         wallet.gap_limit = gap
797         return True
798
799
800     def settings_dialog(self):
801         d = QDialog(self)
802         d.setModal(1)
803
804         vbox = QVBoxLayout()
805
806         msg = 'Here are the settings of your wallet.'
807         vbox.addWidget(QLabel(msg))
808
809         grid = QGridLayout()
810         grid.setSpacing(8)
811
812         fee_e = QLineEdit()
813         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
814         grid.addWidget(QLabel('Fee per tx. input'), 2, 0)
815         grid.addWidget(fee_e, 2, 1)
816         vbox.addLayout(grid)
817         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
818
819         vbox.addLayout(ok_cancel_buttons(d))
820         d.setLayout(vbox) 
821
822         if not d.exec_(): return
823
824         fee = unicode(fee_e.text())
825         try:
826             fee = int( 100000000 * Decimal(fee) )
827         except:
828             QMessageBox.warning(self, 'Error', 'Invalid value:%s'%fee, 'OK')
829             return
830
831         self.wallet.fee = fee
832         self.wallet.save()
833
834     @staticmethod 
835     def network_dialog(wallet, parent=None):
836         interface = wallet.interface
837         if parent:
838             if interface.is_connected:
839                 status = "Connected to %s:%d\n%d blocks\nresponse time: %f"%(interface.host, interface.port, wallet.blocks, interface.rtime)
840             else:
841                 status = "Not connected"
842             host = wallet.host
843             port = wallet.port
844         else:
845             import random
846             status = "Please choose a server."
847             host = random.choice( interface.servers )
848             port = wallet.port
849
850         d = QDialog(parent)
851         d.setModal(1)
852         d.setWindowTitle('Server')
853         d.setMinimumSize(375, 20)
854
855         vbox = QVBoxLayout()
856         vbox.setSpacing(20)
857
858         hbox = QHBoxLayout()
859         l = QLabel()
860         l.setPixmap(QPixmap(":icons/network.png"))
861         hbox.addWidget(l)        
862         hbox.addWidget(QLabel(status))
863
864         vbox.addLayout(hbox)
865
866         hbox = QHBoxLayout()
867         host_line = QLineEdit()
868         host_line.setText("%s:%d"% (host,port) )
869         hbox.addWidget(QLabel('Connect to:'))
870         hbox.addWidget(host_line)
871         vbox.addLayout(hbox)
872
873         if wallet.interface.servers:
874             servers_list = QTreeWidget(parent)
875             servers_list.setHeaderLabels( [ 'Active servers'] )
876             servers_list.setMaximumHeight(150)
877             for item in wallet.interface.servers:
878                 servers_list.addTopLevelItem(QTreeWidgetItem( [ item ] ))
879             servers_list.connect(servers_list, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x:host_line.setText( x.text(0) + ':%d'%wallet.port ))
880             vbox.addWidget(servers_list)
881         else:
882             hbox = QHBoxLayout()
883             hbox.addWidget(QLabel('No nodes available'))
884             b = EnterButton("Find nodes", lambda: wallet.interface.get_servers(wallet) )
885             hbox.addWidget(b)
886             vbox.addLayout(hbox)
887
888         vbox.addLayout(ok_cancel_buttons(d))
889         d.setLayout(vbox) 
890
891         if not d.exec_(): return
892         hh = unicode( host_line.text() )
893
894         try:
895             if ':' in hh:
896                 host, port = hh.split(':')
897                 port = int(port)
898             else:
899                 host = hh
900                 port = wallet.port
901         except:
902             QMessageBox.information(None, 'Error', 'error', 'OK')
903             if parent == None:
904                 sys.exit(1)
905             else:
906                 return
907
908         wallet.set_server(host, port) 
909         return True
910
911
912
913 class ElectrumGui():
914
915     def __init__(self, wallet):
916         self.wallet = wallet
917         self.app = QApplication(sys.argv)
918
919     def restore_or_create(self):
920
921         msg = "Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?"
922         r = QMessageBox.question(None, 'Message', msg, 'create', 'restore', 'cancel', 0, 2)
923         if r==2: return False
924         
925         is_recovery = (r==1)
926         wallet = self.wallet
927         # ask for the server.
928         if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
929
930         if not is_recovery:
931
932             wallet.new_seed(None)
933             wallet.init_mpk( wallet.seed )
934             wallet.up_to_date_event.clear()
935             wallet.update()
936             # run a dialog indicating the seed, ask the user to remember it
937             ElectrumWindow.show_seed_dialog(wallet)
938             #ask for password
939             ElectrumWindow.change_password_dialog(wallet)
940         else:
941             # ask for seed and gap.
942             if not ElectrumWindow.seed_dialog( wallet ): return False
943             wallet.init_mpk( wallet.seed )
944             wallet.up_to_date_event.clear()
945             wallet.update()
946
947             if wallet.is_found():
948                 # history and addressbook
949                 wallet.update_tx_history()
950                 wallet.fill_addressbook()
951                 print "recovery successful"
952                 wallet.save()
953             else:
954                 QMessageBox.information(None, 'Message', "No transactions found for this seed", 'OK')
955
956         wallet.save()
957         return True
958
959     def main(self,url):
960         s = Timer()
961         s.start()
962         w = ElectrumWindow(self.wallet)
963         if url: w.set_url(url)
964         w.app = self.app
965         w.connect_slots(s)
966         self.app.exec_()