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