do not use deprecated BaseException.message
[electrum-nvc.git] / lib / 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 try:
22     import PyQt4
23 except:
24     print "could not import PyQt4"
25     print "on Linux systems, you may try 'sudo apt-get install python-qt4'"
26     sys.exit(1)
27
28
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
32 import PyQt4.QtGui as QtGui
33 from interface import DEFAULT_SERVERS
34
35 try:
36     import icons_rc
37 except:
38     print "Could not import icons_rp.py"
39     print "Please generate it with: 'pyrcc4 icons.qrc -o icons_rc.py'"
40     sys.exit(1)
41
42 from wallet import format_satoshis
43 from decimal import Decimal
44
45 import platform
46 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
47     
48
49 def numbify(entry, is_int = False):
50     text = unicode(entry.text()).strip()
51     chars = '0123456789'
52     if not is_int: chars +='.'
53     s = ''.join([i for i in text if i in chars])
54     if not is_int:
55         if '.' in s:
56             p = s.find('.')
57             s = s.replace('.','')
58             s = s[:p] + '.' + s[p:p+8]
59         try:
60             amount = int( Decimal(s) * 100000000 )
61         except:
62             amount = None
63     else:
64         try:
65             amount = int( s )
66         except:
67             amount = None
68     entry.setText(s)
69     return amount
70
71
72 class Timer(QtCore.QThread):
73     def run(self):
74         while True:
75             self.emit(QtCore.SIGNAL('timersignal'))
76             time.sleep(0.5)
77
78 class EnterButton(QPushButton):
79     def __init__(self, text, func):
80         QPushButton.__init__(self, text)
81         self.func = func
82         self.clicked.connect(func)
83
84     def keyPressEvent(self, e):
85         if e.key() == QtCore.Qt.Key_Return:
86             apply(self.func,())
87
88 class StatusBarButton(QPushButton):
89     def __init__(self, icon, tooltip, func):
90         QPushButton.__init__(self, icon, '')
91         self.setToolTip(tooltip)
92         self.setFlat(True)
93         self.setMaximumWidth(25)
94         self.clicked.connect(func)
95         self.func = func
96
97     def keyPressEvent(self, e):
98         if e.key() == QtCore.Qt.Key_Return:
99             apply(self.func,())
100
101
102 class QRCodeWidget(QWidget):
103
104     def __init__(self, addr):
105         super(QRCodeWidget, self).__init__()
106         self.setGeometry(300, 300, 350, 350)
107         self.set_addr(addr)
108
109     def set_addr(self, addr):
110         from electrum import pyqrnative
111         self.addr = addr
112         self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
113         self.qr.addData(addr)
114         self.qr.make()
115         
116     def paintEvent(self, e):
117         qp = QtGui.QPainter()
118         qp.begin(self)
119         boxsize = 7
120         size = self.qr.getModuleCount()*boxsize
121         k = self.qr.getModuleCount()
122         black = QColor(0, 0, 0, 255)
123         white = QColor(255, 255, 255, 255)
124         for r in range(k):
125             for c in range(k):
126                 if self.qr.isDark(r, c):
127                     qp.setBrush(black)
128                     qp.setPen(black)
129                 else:
130                     qp.setBrush(white)
131                     qp.setPen(white)
132                 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
133         qp.end()
134         
135
136
137 def ok_cancel_buttons(dialog):
138     hbox = QHBoxLayout()
139     hbox.addStretch(1)
140     b = QPushButton("OK")
141     hbox.addWidget(b)
142     b.clicked.connect(dialog.accept)
143     b = QPushButton("Cancel")
144     hbox.addWidget(b)
145     b.clicked.connect(dialog.reject)
146     return hbox
147
148
149 class ElectrumWindow(QMainWindow):
150
151     def __init__(self, wallet):
152         QMainWindow.__init__(self)
153         self.wallet = wallet
154         self.wallet.gui_callback = self.update_callback
155
156         self.funds_error = False
157
158         self.tabs = tabs = QTabWidget(self)
159         tabs.addTab(self.create_history_tab(), 'History')
160         if self.wallet.seed:
161             tabs.addTab(self.create_send_tab(),    'Send')
162         tabs.addTab(self.create_receive_tab(), 'Receive')
163         tabs.addTab(self.create_contacts_tab(),'Contacts')
164         tabs.addTab(self.create_wall_tab(),    'Wall')
165         tabs.setMinimumSize(600, 400)
166         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
167         self.setCentralWidget(tabs)
168         self.create_status_bar()
169         self.setGeometry(100,100,840,400)
170         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.wallet.path
171         if not self.wallet.seed: title += ' [seedless]'
172         self.setWindowTitle( title )
173         self.show()
174
175         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
176         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
177         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
178         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
179         
180         self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
181
182
183     def connect_slots(self, sender):
184         if self.wallet.seed:
185             self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
186             self.previous_payto_e=''
187
188     def check_recipient(self):
189         if self.payto_e.hasFocus():
190             return
191         r = unicode( self.payto_e.text() )
192         if r != self.previous_payto_e:
193             self.previous_payto_e = r
194             r = r.strip()
195             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
196                 try:
197                     to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
198                 except:
199                     return
200                 if to_address:
201                     s = r + ' <' + to_address + '>'
202                     self.payto_e.setText(s)
203
204
205     def update_callback(self):
206         self.emit(QtCore.SIGNAL('updatesignal'))
207
208     def update_wallet(self):
209         if self.wallet.interface and self.wallet.interface.is_connected:
210             if self.wallet.blocks == -1:
211                 text = "Connecting..."
212                 icon = QIcon(":icons/status_disconnected.png")
213             elif self.wallet.blocks == 0:
214                 text = "Server not ready"
215                 icon = QIcon(":icons/status_disconnected.png")
216             elif not self.wallet.up_to_date:
217                 text = "Synchronizing..."
218                 icon = QIcon(":icons/status_waiting.png")
219             else:
220                 c, u = self.wallet.get_balance()
221                 text =  "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
222                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
223                 icon = QIcon(":icons/status_connected.png")
224         else:
225             text = "Not connected"
226             icon = QIcon(":icons/status_disconnected.png")
227
228         if self.funds_error:
229             text = "Not enough funds"
230
231         self.statusBar().showMessage(text)
232         self.status_button.setIcon( icon )
233
234         if self.wallet.up_to_date:
235             self.textbox.setText( self.wallet.banner )
236             self.update_history_tab()
237             self.update_receive_tab()
238             self.update_contacts_tab()
239
240
241     def create_history_tab(self):
242         self.history_list = w = QTreeWidget(self)
243         #print w.getContentsMargins()
244         w.setColumnCount(5)
245         w.setColumnWidth(0, 40) 
246         w.setColumnWidth(1, 140) 
247         w.setColumnWidth(2, 350) 
248         w.setColumnWidth(3, 140) 
249         w.setColumnWidth(4, 140) 
250         w.setHeaderLabels( [ '', 'Date', 'Description', 'Amount', 'Balance'] )
251         self.connect(w, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.tx_details)
252         self.connect(w, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
253         self.connect(w, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
254         return w
255
256     def tx_details(self, item, column):
257         tx_hash = str(item.toolTip(0))
258         tx = self.wallet.tx_history.get(tx_hash)
259
260         if tx['height']:
261             conf = self.wallet.blocks - tx['height'] + 1
262             time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
263         else:
264             conf = 0
265             time_str = 'pending'
266
267         tx_details = "Transaction Details:\n\n" \
268             + "Transaction ID:\n" + tx_hash + "\n\n" \
269             + "Status: %d confirmations\n\n"%conf  \
270             + "Date: %s\n\n"%time_str \
271             + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
272             + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
273
274         r = self.wallet.receipts.get(tx_hash)
275         if r:
276             tx_details += "\n_______________________________________" \
277                 + '\n\nSigned URI: ' + r[2] \
278                 + "\n\nSigned by: " + r[0] \
279                 + '\n\nSignature: ' + r[1]
280
281         QMessageBox.information(self, 'Details', tx_details, 'OK')
282
283
284     def tx_label_clicked(self, item, column):
285         if column==2 and item.isSelected():
286             tx_hash = str(item.toolTip(0))
287             self.is_edit=True
288             #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
289             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
290             self.history_list.editItem( item, column )
291             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
292             self.is_edit=False
293
294     def tx_label_changed(self, item, column):
295         if self.is_edit: 
296             return
297         self.is_edit=True
298         tx_hash = str(item.toolTip(0))
299         tx = self.wallet.tx_history.get(tx_hash)
300         s = self.wallet.labels.get(tx_hash)
301         text = unicode( item.text(2) )
302         if text: 
303             self.wallet.labels[tx_hash] = text
304             item.setForeground(2, QBrush(QColor('black')))
305         else:
306             if s: self.wallet.labels.pop(tx_hash)
307             text = tx['default_label']
308             item.setText(2, text)
309             item.setForeground(2, QBrush(QColor('gray')))
310         self.is_edit=False
311
312     def address_label_clicked(self, item, column, l):
313         if column==1 and item.isSelected():
314             addr = unicode( item.text(0) )
315             if addr in map(lambda x:x[1], self.wallet.aliases.values()):
316                 return
317             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
318             l.editItem( item, column )
319             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
320
321     def address_label_changed(self, item, column, l):
322         addr = unicode( item.text(0) )
323         text = unicode( item.text(1) )
324         if text:
325             self.wallet.labels[addr] = text
326         else:
327             s = self.wallet.labels.get(addr)
328             if s: self.wallet.labels.pop(addr)
329         self.update_history_tab()
330
331     def update_history_tab(self):
332         self.history_list.clear()
333         balance = 0
334         for tx in self.wallet.get_tx_history():
335             tx_hash = tx['tx_hash']
336             if tx['height']:
337                 conf = self.wallet.blocks - tx['height'] + 1
338                 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
339                 icon = QIcon(":icons/confirmed.png")
340             else:
341                 conf = 0
342                 time_str = 'pending'
343                 icon = QIcon(":icons/unconfirmed.png")
344             v = tx['value']
345             balance += v 
346             label = self.wallet.labels.get(tx_hash)
347             is_default_label = (label == '') or (label is None)
348             if is_default_label: label = tx['default_label']
349
350             item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
351             item.setFont(2, QFont(MONOSPACE_FONT))
352             item.setFont(3, QFont(MONOSPACE_FONT))
353             item.setFont(4, QFont(MONOSPACE_FONT))
354             item.setToolTip(0, tx_hash)
355             if is_default_label:
356                 item.setForeground(2, QBrush(QColor('grey')))
357
358             item.setIcon(0, icon)
359             self.history_list.insertTopLevelItem(0,item)
360
361
362     def create_send_tab(self):
363         w = QWidget()
364
365         grid = QGridLayout()
366         grid.setSpacing(8)
367         grid.setColumnMinimumWidth(3,300)
368         grid.setColumnStretch(4,1)
369
370         self.payto_e = QLineEdit()
371         grid.addWidget(QLabel('Pay to'), 1, 0)
372         grid.addWidget(self.payto_e, 1, 1, 1, 3)
373
374         self.message_e = QLineEdit()
375         grid.addWidget(QLabel('Description'), 2, 0)
376         grid.addWidget(self.message_e, 2, 1, 1, 3)
377
378         self.amount_e = QLineEdit()
379         grid.addWidget(QLabel('Amount'), 3, 0)
380         grid.addWidget(self.amount_e, 3, 1, 1, 2)
381         
382         self.fee_e = QLineEdit()
383         grid.addWidget(QLabel('Fee'), 4, 0)
384         grid.addWidget(self.fee_e, 4, 1, 1, 2)
385         
386         b = EnterButton("Send", self.do_send)
387         grid.addWidget(b, 5, 1)
388
389         b = EnterButton("Clear",self.do_clear)
390         grid.addWidget(b, 5, 2)
391
392         self.payto_sig = QLabel('')
393         grid.addWidget(self.payto_sig, 6, 0, 1, 4)
394
395         w.setLayout(grid) 
396         w.show()
397
398         w2 = QWidget()
399         vbox = QVBoxLayout()
400         vbox.addWidget(w)
401         vbox.addStretch(1)
402         w2.setLayout(vbox)
403
404         def entry_changed( is_fee ):
405             self.funds_error = False
406             amount = numbify(self.amount_e)
407             fee = numbify(self.fee_e)
408             if not is_fee: fee = None
409             if amount is None:
410                 return
411             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
412             if not is_fee:
413                 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
414             if inputs:
415                 palette = QPalette()
416                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
417             else:
418                 palette = QPalette()
419                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
420                 self.funds_error = True
421             self.amount_e.setPalette(palette)
422             self.fee_e.setPalette(palette)
423
424         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
425         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
426
427         return w2
428
429     def do_send(self):
430
431         label = unicode( self.message_e.text() )
432         r = unicode( self.payto_e.text() )
433         r = r.strip()
434
435         m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
436         m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
437         
438         if m1:
439             to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
440             if not to_address:
441                 return
442         elif m2:
443             to_address = m2.group(5)
444         else:
445             to_address = r
446
447         if not self.wallet.is_valid(to_address):
448             QMessageBox.warning(self, 'Error', 'Invalid Bitcoin Address:\n'+to_address, 'OK')
449             return
450
451         try:
452             amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
453         except:
454             QMessageBox.warning(self, 'Error', 'Invalid Amount', 'OK')
455             return
456         try:
457             fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
458         except:
459             QMessageBox.warning(self, 'Error', 'Invalid Fee', 'OK')
460             return
461
462         if self.wallet.use_encryption:
463             password = self.password_dialog()
464             if not password:
465                 return
466         else:
467             password = None
468
469         try:
470             tx = self.wallet.mktx( to_address, amount, label, password, fee )
471         except BaseException, e:
472             self.show_message(str(e))
473             return
474             
475         status, msg = self.wallet.sendtx( tx )
476         if status:
477             QMessageBox.information(self, '', 'Payment sent.\n'+msg, 'OK')
478             self.do_clear()
479             self.update_contacts_tab()
480         else:
481             QMessageBox.warning(self, 'Error', msg, 'OK')
482
483
484     def set_url(self, url):
485         payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
486         self.tabs.setCurrentIndex(1)
487         self.payto_e.setText(payto)
488         self.message_e.setText(message)
489         self.amount_e.setText(amount)
490         if identity:
491             self.set_frozen(self.payto_e,True)
492             self.set_frozen(self.amount_e,True)
493             self.set_frozen(self.message_e,True)
494             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
495         else:
496             self.payto_sig.setVisible(False)
497
498     def do_clear(self):
499         self.payto_sig.setVisible(False)
500         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
501             e.setText('')
502             self.set_frozen(e,False)
503
504     def set_frozen(self,entry,frozen):
505         if frozen:
506             entry.setReadOnly(True)
507             entry.setFrame(False)
508             palette = QPalette()
509             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
510             entry.setPalette(palette)
511         else:
512             entry.setReadOnly(False)
513             entry.setFrame(True)
514             palette = QPalette()
515             palette.setColor(entry.backgroundRole(), QColor('white'))
516             entry.setPalette(palette)
517
518
519     def get_current_addr(self, is_recv):
520         if is_recv:
521             l = self.receive_list
522         else:
523             l = self.contacts_list
524         i = l.currentItem()
525         if i: 
526             return unicode( i.text(0) )
527         else:
528             return ''
529
530
531     def add_receive_buttons(self):
532
533         l = self.receive_list
534         hbox = self.receive_buttons_hbox
535
536         hbox.addWidget(EnterButton("QR",lambda: self.show_address_qrcode(self.get_current_addr(True))))
537         hbox.addWidget(EnterButton("Copy to Clipboard", lambda: self.app.clipboard().setText(self.get_current_addr(True))))
538
539         def toggle_freeze():
540             addr = self.get_current_addr(True)
541             if not addr: return
542             if addr in self.wallet.frozen_addresses:
543                 self.wallet.frozen_addresses.remove(addr)
544             else:
545                 self.wallet.frozen_addresses.append(addr)
546             self.wallet.save()
547             self.update_receive_tab()
548
549         self.freezeButton = b = EnterButton("Freeze", toggle_freeze)
550         hbox.addWidget(b)
551         hbox.addStretch(1)
552
553
554     def add_contacts_buttons(self):
555         l = self.contacts_list
556         hbox = self.contacts_buttons_hbox
557
558         hbox.addWidget(EnterButton("QR",lambda: self.show_address_qrcode(self.get_current_addr(False))))
559         hbox.addWidget(EnterButton("Copy to Clipboard", lambda: self.app.clipboard().setText(self.get_current_addr(False))))
560         def payto():
561             addr = self.get_current_addr(False)
562             if not addr:return
563             self.tabs.setCurrentIndex(1)
564             self.payto_e.setText(addr)
565             self.amount_e.setFocus()
566         hbox.addWidget(EnterButton('Pay to', lambda: payto()))
567         hbox.addWidget(EnterButton("New", self.newaddress_dialog))
568         hbox.addStretch(1)
569
570     def update_receive_buttons(self):
571         addr = self.get_current_addr(True)
572         t = "Unfreeze" if addr in self.wallet.frozen_addresses else "Freeze"
573         self.freezeButton.setText(t)
574     
575
576     def create_receive_tab(self):
577         l = QTreeWidget(self)
578         l.setColumnCount(4)
579         l.setColumnWidth(0, 350) 
580         l.setColumnWidth(1, 330)
581         l.setColumnWidth(2, 100) 
582         l.setColumnWidth(3, 10) 
583         l.setHeaderLabels( ['Address', 'Label','Balance','Tx'])
584
585         w = QWidget()
586         vbox = QVBoxLayout()
587         w.setLayout(vbox)
588
589         vbox.setMargin(0)
590         vbox.setSpacing(0)
591         vbox.addWidget(l)
592         buttons = QWidget()
593         vbox.addWidget(buttons)
594
595         hbox = QHBoxLayout()
596         hbox.setMargin(0)
597         hbox.setSpacing(0)
598         buttons.setLayout(hbox)
599
600
601         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l))
602         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l))
603         l.selectionModel().currentChanged.connect(self.update_receive_buttons)
604
605         self.receive_list = l
606         self.receive_buttons_hbox = hbox
607         self.add_receive_buttons()
608
609         return w
610
611     def create_contacts_tab(self):
612         l = QTreeWidget(self)
613         l.setColumnCount(3)
614         l.setColumnWidth(0, 350) 
615         l.setColumnWidth(1, 330)
616         l.setColumnWidth(2, 20) 
617         l.setHeaderLabels( ['Address', 'Label','Tx'])
618
619         w = QWidget()
620         vbox = QVBoxLayout()
621         w.setLayout(vbox)
622
623         vbox.setMargin(0)
624         vbox.setSpacing(0)
625         vbox.addWidget(l)
626         buttons = QWidget()
627         vbox.addWidget(buttons)
628
629         hbox = QHBoxLayout()
630         hbox.setMargin(0)
631         hbox.setSpacing(0)
632         buttons.setLayout(hbox)
633
634         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l))
635         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l))
636         self.connect(l, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.show_contact_details)
637         self.contacts_list = l
638         self.contacts_buttons_hbox = hbox
639         self.add_contacts_buttons()
640         return w
641
642     def update_receive_tab(self):
643         self.receive_list.clear()
644         for address in self.wallet.all_addresses():
645             if self.wallet.is_change(address):continue
646             label = self.wallet.labels.get(address,'')
647             n = 0 
648             h = self.wallet.history.get(address,[])
649             for item in h:
650                 if not item['is_input'] : n=n+1
651             tx = "None" if n==0 else "%d"%n
652
653             c, u = self.wallet.get_addr_balance(address)
654             balance = format_satoshis( c + u, False, self.wallet.num_zeros )
655             if address in self.wallet.frozen_addresses: 
656                 balance += '[F]'
657
658             item = QTreeWidgetItem( [ address, label, balance, tx] )
659             item.setFont(0, QFont(MONOSPACE_FONT))
660             self.receive_list.addTopLevelItem(item)
661
662     def show_contact_details(self, item, column):
663         m = unicode(item.text(0))
664         a = self.wallet.aliases.get(m)
665         if a:
666             if a[0] in self.wallet.authorities.keys():
667                 s = self.wallet.authorities.get(a[0])
668             else:
669                 s = "self-signed"
670             msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
671             QMessageBox.information(self, 'Alias', msg, 'OK')
672
673     def update_contacts_tab(self):
674         self.contacts_list.clear()
675
676         for alias, v in self.wallet.aliases.items():
677             s, target = v
678             item = QTreeWidgetItem( [ target, alias, '-'] )
679             self.contacts_list.addTopLevelItem(item)
680             
681         for address in self.wallet.addressbook:
682             label = self.wallet.labels.get(address,'')
683             n = 0 
684             for item in self.wallet.tx_history.values():
685                 if address in item['outputs'] : n=n+1
686             tx = "None" if n==0 else "%d"%n
687             item = QTreeWidgetItem( [ address, label, tx] )
688             item.setFont(0, QFont(MONOSPACE_FONT))
689             self.contacts_list.addTopLevelItem(item)
690
691
692     def create_wall_tab(self):
693         self.textbox = textbox = QTextEdit(self)
694         textbox.setFont(QFont(MONOSPACE_FONT))
695         textbox.setReadOnly(True)
696         return textbox
697
698     def create_status_bar(self):
699         sb = QStatusBar()
700         sb.setFixedHeight(35)
701         if self.wallet.seed:
702             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
703         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
704         if self.wallet.seed:
705             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
706         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) ) 
707         sb.addPermanentWidget( self.status_button )
708         self.setStatusBar(sb)
709
710     def newaddress_dialog(self):
711         text, ok = QInputDialog.getText(self, 'New Contact', 'Address:')
712         address = unicode(text)
713         if ok:
714             if self.wallet.is_valid(address):
715                 self.wallet.addressbook.append(address)
716                 self.wallet.save()
717                 self.update_contacts_tab()
718             else:
719                 QMessageBox.warning(self, 'Error', 'Invalid Address', 'OK')
720
721     @staticmethod
722     def show_seed_dialog(wallet, parent=None):
723         from electrum import mnemonic
724
725         if not wallet.seed:
726             QMessageBox.information(parent, 'Message', 'No seed', 'OK')
727             return
728
729         if wallet.use_encryption:
730             password = parent.password_dialog()
731             if not password: return
732         else:
733             password = None
734             
735         try:
736             seed = wallet.pw_decode( wallet.seed, password)
737         except:
738             QMessageBox.warning(parent, 'Error', 'Invalid Password', 'OK')
739             return
740
741         msg = "Your wallet generation seed is:\n\n" + seed \
742               + "\n\nPlease keep it in a safe place; if you lose it,\nyou will not be able to restore your wallet.\n\n" \
743               + "Equivalently, your wallet seed can be stored and\nrecovered with the following mnemonic code:\n\n\"" \
744               + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
745
746         d = QDialog(None)
747         d.setModal(1)
748         d.setWindowTitle("Seed")
749         d.setMinimumSize(400, 270)
750
751         vbox = QVBoxLayout()
752         hbox = QHBoxLayout()
753         vbox2 = QVBoxLayout()
754         l = QLabel()
755         l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
756         vbox2.addWidget(l)
757         vbox2.addStretch(1)
758         hbox.addLayout(vbox2)
759         hbox.addWidget(QLabel(msg))
760         vbox.addLayout(hbox)
761
762         hbox = QHBoxLayout()
763         hbox.addStretch(1)
764
765
766         if parent:
767             app = parent.app
768         else:
769             app = QApplication
770
771         b = QPushButton("Copy to Clipboard")
772         b.clicked.connect(lambda: app.clipboard().setText(' '.join(mnemonic.mn_encode(seed))))
773         hbox.addWidget(b)
774         b = QPushButton("View as QR Code")
775         b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
776         hbox.addWidget(b)
777
778         b = QPushButton("OK")
779         b.clicked.connect(d.accept)
780         hbox.addWidget(b)
781         vbox.addLayout(hbox)
782         d.setLayout(vbox)
783         d.exec_()
784
785     @staticmethod
786     def show_seed_qrcode(seed):
787         if not seed: return
788         d = QDialog(None)
789         d.setModal(1)
790         d.setWindowTitle("Seed")
791         d.setMinimumSize(270, 300)
792         vbox = QVBoxLayout()
793         vbox.addWidget(QRCodeWidget(seed))
794         hbox = QHBoxLayout()
795         hbox.addStretch(1)
796         b = QPushButton("OK")
797         hbox.addWidget(b)
798         b.clicked.connect(d.accept)
799
800         vbox.addLayout(hbox)
801         d.setLayout(vbox)
802         d.exec_()
803
804     def show_address_qrcode(self,address):
805         if not address: return
806         d = QDialog(None)
807         d.setModal(1)
808         d.setWindowTitle(address)
809         d.setMinimumSize(270, 350)
810         vbox = QVBoxLayout()
811         qrw = QRCodeWidget(address)
812         vbox.addWidget(qrw)
813
814         hbox = QHBoxLayout()
815         amount_e = QLineEdit()
816         hbox.addWidget(QLabel('Amount'))
817         hbox.addWidget(amount_e)
818         vbox.addLayout(hbox)
819
820         #hbox = QHBoxLayout()
821         #label_e = QLineEdit()
822         #hbox.addWidget(QLabel('Label'))
823         #hbox.addWidget(label_e)
824         #vbox.addLayout(hbox)
825
826         def amount_changed():
827             amount = numbify(amount_e)
828             #label = str( label_e.getText() )
829             if amount is not None:
830                 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
831             else:
832                 qrw.set_addr( address )
833             qrw.repaint()
834
835         def do_save():
836             from electrum import bmp
837             bmp.save_qrcode(qrw.qr, "qrcode.bmp")
838             self.show_message("QR code saved to file 'qrcode.bmp'")
839             
840         amount_e.textChanged.connect( amount_changed )
841
842         hbox = QHBoxLayout()
843         hbox.addStretch(1)
844         b = QPushButton("Save")
845         b.clicked.connect(do_save)
846         hbox.addWidget(b)
847         b = QPushButton("Close")
848         hbox.addWidget(b)
849         b.clicked.connect(d.accept)
850
851         vbox.addLayout(hbox)
852         d.setLayout(vbox)
853         d.exec_()
854
855     def question(self, msg):
856         return QMessageBox.question(self, 'Message', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
857
858     def show_message(self, msg):
859         QMessageBox.information(self, 'Message', msg, 'OK')
860
861     def password_dialog(self ):
862         d = QDialog(self)
863         d.setModal(1)
864
865         pw = QLineEdit()
866         pw.setEchoMode(2)
867
868         vbox = QVBoxLayout()
869         msg = 'Please enter your password'
870         vbox.addWidget(QLabel(msg))
871
872         grid = QGridLayout()
873         grid.setSpacing(8)
874         grid.addWidget(QLabel('Password'), 1, 0)
875         grid.addWidget(pw, 1, 1)
876         vbox.addLayout(grid)
877
878         vbox.addLayout(ok_cancel_buttons(d))
879         d.setLayout(vbox) 
880
881         if not d.exec_(): return
882         return unicode(pw.text())
883
884     @staticmethod
885     def change_password_dialog( wallet, parent=None ):
886
887         if not wallet.seed:
888             QMessageBox.information(parent, 'Message', 'No seed', 'OK')
889             return
890
891         d = QDialog(parent)
892         d.setModal(1)
893
894         pw = QLineEdit()
895         pw.setEchoMode(2)
896         new_pw = QLineEdit()
897         new_pw.setEchoMode(2)
898         conf_pw = QLineEdit()
899         conf_pw.setEchoMode(2)
900
901         vbox = QVBoxLayout()
902         if parent:
903             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'
904         else:
905             msg = "Please choose a password to encrypt your wallet keys.\nLeave these fields empty if you want to disable encryption."
906         vbox.addWidget(QLabel(msg))
907
908         grid = QGridLayout()
909         grid.setSpacing(8)
910
911         if wallet.use_encryption:
912             grid.addWidget(QLabel('Password'), 1, 0)
913             grid.addWidget(pw, 1, 1)
914
915         grid.addWidget(QLabel('New Password'), 2, 0)
916         grid.addWidget(new_pw, 2, 1)
917
918         grid.addWidget(QLabel('Confirm Password'), 3, 0)
919         grid.addWidget(conf_pw, 3, 1)
920         vbox.addLayout(grid)
921
922         vbox.addLayout(ok_cancel_buttons(d))
923         d.setLayout(vbox) 
924
925         if not d.exec_(): return
926
927         password = unicode(pw.text()) if wallet.use_encryption else None
928         new_password = unicode(new_pw.text())
929         new_password2 = unicode(conf_pw.text())
930
931         try:
932             seed = wallet.pw_decode( wallet.seed, password)
933         except:
934             QMessageBox.warning(parent, 'Error', 'Incorrect Password', 'OK')
935             return
936
937         if new_password != new_password2:
938             QMessageBox.warning(parent, 'Error', 'Passwords do not match', 'OK')
939             return
940
941         wallet.update_password(seed, new_password)
942
943     @staticmethod
944     def seed_dialog(wallet, parent=None):
945         d = QDialog(parent)
946         d.setModal(1)
947
948         vbox = QVBoxLayout()
949         msg = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
950         vbox.addWidget(QLabel(msg))
951
952         grid = QGridLayout()
953         grid.setSpacing(8)
954
955         seed_e = QLineEdit()
956         grid.addWidget(QLabel('Seed or mnemonic'), 1, 0)
957         grid.addWidget(seed_e, 1, 1)
958
959         gap_e = QLineEdit()
960         gap_e.setText("5")
961         grid.addWidget(QLabel('Gap limit'), 2, 0)
962         grid.addWidget(gap_e, 2, 1)
963         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
964         vbox.addLayout(grid)
965
966         vbox.addLayout(ok_cancel_buttons(d))
967         d.setLayout(vbox) 
968
969         if not d.exec_(): return
970
971         try:
972             gap = int(unicode(gap_e.text()))
973         except:
974             QMessageBox.warning(None, 'Error', 'error', 'OK')
975             sys.exit(0)
976
977         try:
978             seed = unicode(seed_e.text())
979             seed.decode('hex')
980         except:
981             from electrum import mnemonic
982             print "not hex, trying decode"
983             try:
984                 seed = mnemonic.mn_decode( seed.split(' ') )
985             except:
986                 QMessageBox.warning(None, 'Error', 'I cannot decode this', 'OK')
987                 sys.exit(0)
988         if not seed:
989             QMessageBox.warning(None, 'Error', 'no seed', 'OK')
990             sys.exit(0)
991         
992         wallet.seed = str(seed)
993         #print repr(wallet.seed)
994         wallet.gap_limit = gap
995         return True
996
997
998     def settings_dialog(self):
999         d = QDialog(self)
1000         d.setModal(1)
1001
1002         vbox = QVBoxLayout()
1003
1004         msg = 'Here are the settings of your wallet.'
1005         vbox.addWidget(QLabel(msg))
1006
1007         grid = QGridLayout()
1008         grid.setSpacing(8)
1009         vbox.addLayout(grid)
1010
1011         fee_e = QLineEdit()
1012         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1013         grid.addWidget(QLabel('Fee per tx. input'), 2, 0)
1014         grid.addWidget(fee_e, 2, 1)
1015         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1016
1017         nz_e = QLineEdit()
1018         nz_e.setText("%d"% self.wallet.num_zeros)
1019         grid.addWidget(QLabel('Zeros displayed after decimal point'), 3, 0)
1020         grid.addWidget(nz_e, 3, 1)
1021         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1022
1023         vbox.addLayout(ok_cancel_buttons(d))
1024         d.setLayout(vbox) 
1025
1026         if not d.exec_(): return
1027
1028         fee = unicode(fee_e.text())
1029         try:
1030             fee = int( 100000000 * Decimal(fee) )
1031         except:
1032             QMessageBox.warning(self, 'Error', 'Invalid value:%s'%fee, 'OK')
1033             return
1034
1035         if self.wallet.fee != fee:
1036             self.wallet.fee = fee
1037             self.wallet.save()
1038         
1039         nz = unicode(nz_e.text())
1040         try:
1041             nz = int( nz )
1042             if nz>8: nz=8
1043         except:
1044             QMessageBox.warning(self, 'Error', 'Invalid value:%s'%nz, 'OK')
1045             return
1046
1047         if self.wallet.num_zeros != nz:
1048             self.wallet.num_zeros = nz
1049             self.update_history_tab()
1050             self.update_receive_tab()
1051             self.wallet.save()
1052
1053     @staticmethod 
1054     def network_dialog(wallet, parent=None):
1055         interface = wallet.interface
1056         if parent:
1057             if interface.is_connected:
1058                 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1059             else:
1060                 status = "Not connected"
1061             server = wallet.server
1062         else:
1063             import random
1064             status = "Please choose a server."
1065             server = random.choice( DEFAULT_SERVERS )
1066
1067         if not wallet.interface.servers:
1068             servers_list = []
1069             for x in DEFAULT_SERVERS:
1070                 h,port,protocol = x.split(':')
1071                 servers_list.append( (h,[(protocol,port)] ) )
1072         else:
1073             servers_list = wallet.interface.servers
1074             
1075         plist = {}
1076         for item in servers_list:
1077             host, pp = item
1078             z = {}
1079             for item2 in pp:
1080                 protocol, port = item2
1081                 z[protocol] = port
1082             plist[host] = z
1083
1084         d = QDialog(parent)
1085         d.setModal(1)
1086         d.setWindowTitle('Server')
1087         d.setMinimumSize(375, 20)
1088
1089         vbox = QVBoxLayout()
1090         vbox.setSpacing(20)
1091
1092         hbox = QHBoxLayout()
1093         l = QLabel()
1094         l.setPixmap(QPixmap(":icons/network.png"))
1095         hbox.addWidget(l)        
1096         hbox.addWidget(QLabel(status))
1097
1098         vbox.addLayout(hbox)
1099
1100         hbox = QHBoxLayout()
1101         host_line = QLineEdit()
1102         host_line.setText(server)
1103         hbox.addWidget(QLabel('Connect to:'))
1104         hbox.addWidget(host_line)
1105         vbox.addLayout(hbox)
1106
1107         hbox = QHBoxLayout()
1108
1109         buttonGroup = QGroupBox("protocol")
1110         radio1 = QRadioButton("tcp", buttonGroup)
1111         radio2 = QRadioButton("http", buttonGroup)
1112
1113         def current_line():
1114             return unicode(host_line.text()).split(':')
1115             
1116         def set_button(protocol):
1117             if protocol == 't':
1118                 radio1.setChecked(1)
1119             elif protocol == 'h':
1120                 radio2.setChecked(1)
1121
1122         def set_protocol(protocol):
1123             host = current_line()[0]
1124             pp = plist[host]
1125             if protocol not in pp.keys():
1126                 protocol = pp.keys()[0]
1127                 set_button(protocol)
1128             port = pp[protocol]
1129             host_line.setText( host + ':' + port + ':' + protocol)
1130
1131         radio1.clicked.connect(lambda x: set_protocol('t') )
1132         radio2.clicked.connect(lambda x: set_protocol('h') )
1133
1134         set_button(current_line()[2])
1135
1136         hbox.addWidget(QLabel('Protocol:'))
1137         hbox.addWidget(radio1)
1138         hbox.addWidget(radio2)
1139
1140         vbox.addLayout(hbox)
1141
1142         if wallet.interface.servers:
1143             label = 'Active Servers'
1144         else:
1145             label = 'Default Servers'
1146         
1147         servers_list_widget = QTreeWidget(parent)
1148         servers_list_widget.setHeaderLabels( [ label ] )
1149         servers_list_widget.setMaximumHeight(150)
1150         for host in plist.keys():
1151             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1152
1153         def do_set_line(x):
1154             host = unicode(x.text(0))
1155             pp = plist[host]
1156             if 't' in pp.keys():
1157                 protocol = 't'
1158             else:
1159                 protocol = pp.keys()[0]
1160             port = pp[protocol]
1161             host_line.setText( host + ':' + port + ':' + protocol)
1162             set_button(protocol)
1163
1164         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1165         vbox.addWidget(servers_list_widget)
1166
1167         vbox.addLayout(ok_cancel_buttons(d))
1168         d.setLayout(vbox) 
1169
1170         if not d.exec_(): return
1171         server = unicode( host_line.text() )
1172
1173         try:
1174             wallet.set_server(server)
1175         except:
1176             QMessageBox.information(None, 'Error', 'error', 'OK')
1177             if parent == None:
1178                 sys.exit(1)
1179             else:
1180                 return
1181
1182         return True
1183
1184
1185
1186 class ElectrumGui():
1187
1188     def __init__(self, wallet):
1189         self.wallet = wallet
1190         self.app = QApplication(sys.argv)
1191
1192     def waiting_dialog(self):
1193
1194         s = Timer()
1195         s.start()
1196         w = QDialog()
1197         w.resize(200, 70)
1198         w.setWindowTitle('Electrum')
1199         l = QLabel('')
1200         vbox = QVBoxLayout()
1201         vbox.addWidget(l)
1202         w.setLayout(vbox)
1203         w.show()
1204         def f():
1205             if self.wallet.up_to_date: w.close()
1206             else:
1207                 l.setText("Please wait...\nGenerating addresses: %d"%len(self.wallet.all_addresses()))
1208                 pass
1209         w.connect(s, QtCore.SIGNAL('timersignal'), f)
1210         self.wallet.interface.poke()
1211         w.exec_()
1212         w.destroy()
1213
1214
1215     def restore_or_create(self):
1216
1217         msg = "Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?"
1218         r = QMessageBox.question(None, 'Message', msg, 'create', 'restore', 'cancel', 0, 2)
1219         if r==2: return False
1220         
1221         is_recovery = (r==1)
1222         wallet = self.wallet
1223         # ask for the server.
1224         if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1225
1226         if not is_recovery:
1227             wallet.new_seed(None)
1228             wallet.init_mpk( wallet.seed )
1229             wallet.up_to_date_event.clear()
1230             wallet.up_to_date = False
1231             self.waiting_dialog()
1232             # run a dialog indicating the seed, ask the user to remember it
1233             ElectrumWindow.show_seed_dialog(wallet)
1234             #ask for password
1235             ElectrumWindow.change_password_dialog(wallet)
1236         else:
1237             # ask for seed and gap.
1238             if not ElectrumWindow.seed_dialog( wallet ): return False
1239             wallet.init_mpk( wallet.seed )
1240             wallet.up_to_date_event.clear()
1241             wallet.up_to_date = False
1242             self.waiting_dialog()
1243             if wallet.is_found():
1244                 # history and addressbook
1245                 wallet.update_tx_history()
1246                 wallet.fill_addressbook()
1247                 print "recovery successful"
1248                 wallet.save()
1249             else:
1250                 QMessageBox.information(None, 'Message', "No transactions found for this seed", 'OK')
1251
1252         wallet.save()
1253         return True
1254
1255     def main(self,url):
1256         s = Timer()
1257         s.start()
1258         w = ElectrumWindow(self.wallet)
1259         if url: w.set_url(url)
1260         w.app = self.app
1261         w.connect_slots(s)
1262         w.update_wallet()
1263
1264         self.app.exec_()