allow multiple-outputs transactions with mktx()
[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 from i18n import _
21 from util import print_error
22
23 try:
24     import PyQt4
25 except:
26     sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
27
28 from PyQt4.QtGui import *
29 from PyQt4.QtCore import *
30 import PyQt4.QtCore as QtCore
31 import PyQt4.QtGui as QtGui
32 from interface import DEFAULT_SERVERS
33
34 try:
35     import icons_rc
36 except:
37     sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o lib/icons_rc.py'")
38
39 from wallet import format_satoshis
40 import bmp, mnemonic, pyqrnative, qrscanner
41
42 from decimal import Decimal
43
44 import platform
45
46 if platform.system() == 'Windows':
47     MONOSPACE_FONT = 'Lucida Console'
48 elif platform.system() == 'Darwin':
49     MONOSPACE_FONT = 'Monaco'
50 else:
51     MONOSPACE_FONT = 'monospace'
52
53 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'    
54
55 def numbify(entry, is_int = False):
56     text = unicode(entry.text()).strip()
57     pos = entry.cursorPosition()
58     chars = '0123456789'
59     if not is_int: chars +='.'
60     s = ''.join([i for i in text if i in chars])
61     if not is_int:
62         if '.' in s:
63             p = s.find('.')
64             s = s.replace('.','')
65             s = s[:p] + '.' + s[p:p+8]
66         try:
67             amount = int( Decimal(s) * 100000000 )
68         except:
69             amount = None
70     else:
71         try:
72             amount = int( s )
73         except:
74             amount = None
75     entry.setText(s)
76     entry.setCursorPosition(pos)
77     return amount
78
79
80 class Timer(QtCore.QThread):
81     def run(self):
82         while True:
83             self.emit(QtCore.SIGNAL('timersignal'))
84             time.sleep(0.5)
85
86 class HelpButton(QPushButton):
87     def __init__(self, text):
88         QPushButton.__init__(self, '?')
89         self.setFocusPolicy(Qt.NoFocus)
90         self.setFixedWidth(20)
91         self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
92
93
94 class EnterButton(QPushButton):
95     def __init__(self, text, func):
96         QPushButton.__init__(self, text)
97         self.func = func
98         self.clicked.connect(func)
99
100     def keyPressEvent(self, e):
101         if e.key() == QtCore.Qt.Key_Return:
102             apply(self.func,())
103
104 class MyTreeWidget(QTreeWidget):
105     def __init__(self, parent):
106         QTreeWidget.__init__(self, parent)
107         def ddfr(item):
108             if not item: return
109             for i in range(0,self.viewport().height()/5):
110                 if self.itemAt(QPoint(0,i*5)) == item:
111                     break
112             else:
113                 return
114             for j in range(0,30):
115                 if self.itemAt(QPoint(0,i*5 + j)) != item:
116                     break
117             self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
118
119         self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
120         
121
122
123
124 class StatusBarButton(QPushButton):
125     def __init__(self, icon, tooltip, func):
126         QPushButton.__init__(self, icon, '')
127         self.setToolTip(tooltip)
128         self.setFlat(True)
129         self.setMaximumWidth(25)
130         self.clicked.connect(func)
131         self.func = func
132
133     def keyPressEvent(self, e):
134         if e.key() == QtCore.Qt.Key_Return:
135             apply(self.func,())
136
137
138 class QRCodeWidget(QWidget):
139
140     def __init__(self, data = None):
141         QWidget.__init__(self)
142         self.setMinimumSize(210, 210)
143         self.addr = None
144         self.qr = None
145         if data:
146             self.set_addr(data)
147             self.update_qr()
148
149     def set_addr(self, addr):
150         if self.addr != addr:
151             self.addr = addr
152             self.qr = None
153             self.update()
154
155     def update_qr(self):
156         if self.addr and not self.qr:
157             self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
158             self.qr.addData(self.addr)
159             self.qr.make()
160             self.update()
161
162     def paintEvent(self, e):
163
164         if not self.addr:
165             return
166
167         black = QColor(0, 0, 0, 255)
168         white = QColor(255, 255, 255, 255)
169         boxsize = 6
170
171         if not self.qr:
172             qp = QtGui.QPainter()
173             qp.begin(self)
174             qp.setBrush(white)
175             qp.setPen(white)
176             qp.drawRect(0, 0, 198, 198)
177             qp.end()
178             return
179         
180         size = self.qr.getModuleCount()*boxsize
181         k = self.qr.getModuleCount()
182         qp = QtGui.QPainter()
183         qp.begin(self)
184         for r in range(k):
185             for c in range(k):
186                 if self.qr.isDark(r, c):
187                     qp.setBrush(black)
188                     qp.setPen(black)
189                 else:
190                     qp.setBrush(white)
191                     qp.setPen(white)
192                 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
193         qp.end()
194         
195
196
197 class QR_Window(QWidget):
198
199     def __init__(self):
200         QWidget.__init__(self)
201         self.setWindowTitle('Electrum - Invoice')
202         self.setMinimumSize(800, 250)
203         self.address = ''
204         self.labe = ''
205         self.amount = 0
206         self.setFocusPolicy(QtCore.Qt.NoFocus)
207
208         main_box = QHBoxLayout()
209         
210         self.qrw = QRCodeWidget()
211         main_box.addWidget(self.qrw)
212
213         vbox = QVBoxLayout()
214         main_box.addLayout(vbox)
215
216         main_box.addStretch(1)
217
218         self.address_label = QLabel("")
219         self.address_label.setFont(QFont(MONOSPACE_FONT))
220         vbox.addWidget(self.address_label)
221
222         self.label_label = QLabel("")
223         vbox.addWidget(self.label_label)
224
225         self.amount_label = QLabel("")
226         vbox.addWidget(self.amount_label)
227
228         vbox.addStretch(1)
229         self.setLayout(main_box)
230
231
232     def set_content(self, addr, label, amount):
233         self.address = addr
234         address_text = "<span style='font-size: 18pt'>%s</span>" % addr if addr else ""
235         self.address_label.setText(address_text)
236
237         self.amount = amount
238         amount_text = "<span style='font-size: 21pt'>%s</span> <span style='font-size: 16pt'>BTC</span> " % format_satoshis(amount) if amount else ""
239         self.amount_label.setText(amount_text)
240
241         self.label = label
242         label_text = "<span style='font-size: 21pt'>%s</span>" % label if label else ""
243         self.label_label.setText(label_text)
244
245         msg = 'bitcoin:'+self.address
246         if self.amount is not None:
247             msg += '?amount=%s'%(str( Decimal(self.amount) /100000000))
248             if self.label is not None:
249                 msg += '&label=%s'%(self.label)
250         elif self.label is not None:
251             msg += '?label=%s'%(self.label)
252             
253         self.qrw.set_addr( msg )
254
255             
256
257
258 def waiting_dialog(f):
259
260     s = Timer()
261     s.start()
262     w = QDialog()
263     w.resize(200, 70)
264     w.setWindowTitle('Electrum')
265     l = QLabel('')
266     vbox = QVBoxLayout()
267     vbox.addWidget(l)
268     w.setLayout(vbox)
269     w.show()
270     def ff():
271         s = f()
272         if s: l.setText(s)
273         else: w.close()
274     w.connect(s, QtCore.SIGNAL('timersignal'), ff)
275     w.exec_()
276     w.destroy()
277
278
279 def ok_cancel_buttons(dialog):
280     hbox = QHBoxLayout()
281     hbox.addStretch(1)
282     b = QPushButton("OK")
283     hbox.addWidget(b)
284     b.clicked.connect(dialog.accept)
285     b = QPushButton("Cancel")
286     hbox.addWidget(b)
287     b.clicked.connect(dialog.reject)
288     return hbox
289
290
291 class ElectrumWindow(QMainWindow):
292
293     def __init__(self, wallet, config):
294         QMainWindow.__init__(self)
295         self.wallet = wallet
296         self.config = config
297         self.wallet.interface.register_callback('updated', self.update_callback)
298         self.wallet.interface.register_callback('connected', self.update_callback)
299         self.wallet.interface.register_callback('disconnected', self.update_callback)
300         self.wallet.interface.register_callback('disconnecting', self.update_callback)
301
302         self.receive_tab_mode = config.get('qt_receive_tab_mode', 0)
303         self.merchant_name = config.get('merchant_name', 'Invoice')
304
305         self.qr_window = None
306         self.funds_error = False
307         self.completions = QStringListModel()
308
309         self.tabs = tabs = QTabWidget(self)
310         tabs.addTab(self.create_history_tab(), _('History') )
311         if self.wallet.seed:
312             tabs.addTab(self.create_send_tab(), _('Send') )
313         tabs.addTab(self.create_receive_tab(), _('Receive') )
314         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
315         tabs.addTab(self.create_wall_tab(), _('Wall') )
316         tabs.setMinimumSize(600, 400)
317         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
318         self.setCentralWidget(tabs)
319         self.create_status_bar()
320         self.toggle_QR_window(self.receive_tab_mode == 2)
321
322         g = self.config.get("winpos-qt",[100, 100, 840, 400])
323         self.setGeometry(g[0], g[1], g[2], g[3])
324         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.config.path
325         if not self.wallet.seed: title += ' [seedless]'
326         self.setWindowTitle( title )
327
328         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
329         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
330         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
331         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
332         
333         self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
334         #self.connect(self, SIGNAL('editamount'), self.edit_amount)
335         self.history_list.setFocus(True)
336
337         # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
338         if platform.system() == 'Windows':
339             n = 3 if self.wallet.seed else 2
340             tabs.setCurrentIndex (n)
341             tabs.setCurrentIndex (0)
342
343     def close(self):
344         QMainWindow.close(self)
345         if self.qr_window: 
346             self.qr_window.close()
347             self.qr_window = None
348
349     def connect_slots(self, sender):
350         if self.wallet.seed:
351             self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
352             self.previous_payto_e=''
353
354     def timer_actions(self):
355         if self.qr_window:
356             self.qr_window.qrw.update_qr()
357             
358         if self.payto_e.hasFocus():
359             return
360         r = unicode( self.payto_e.text() )
361         if r != self.previous_payto_e:
362             self.previous_payto_e = r
363             r = r.strip()
364             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
365                 try:
366                     to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
367                 except:
368                     return
369                 if to_address:
370                     s = r + '  <' + to_address + '>'
371                     self.payto_e.setText(s)
372
373
374     def update_callback(self):
375         self.emit(QtCore.SIGNAL('updatesignal'))
376
377     def update_wallet(self):
378         if self.wallet.interface and self.wallet.interface.is_connected:
379             if not self.wallet.up_to_date:
380                 text = _( "Synchronizing..." )
381                 icon = QIcon(":icons/status_waiting.png")
382             else:
383                 c, u = self.wallet.get_balance()
384                 text =  _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
385                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
386                 icon = QIcon(":icons/status_connected.png")
387         else:
388             text = _( "Not connected" )
389             icon = QIcon(":icons/status_disconnected.png")
390
391         if self.funds_error:
392             text = _( "Not enough funds" )
393
394         self.statusBar().showMessage(text)
395         self.status_button.setIcon( icon )
396
397         if self.wallet.up_to_date or not self.wallet.interface.is_connected:
398             self.textbox.setText( self.wallet.banner )
399             self.update_history_tab()
400             self.update_receive_tab()
401             self.update_contacts_tab()
402             self.update_completions()
403
404
405     def create_history_tab(self):
406         self.history_list = l = MyTreeWidget(self)
407         l.setColumnCount(5)
408         l.setColumnWidth(0, 40) 
409         l.setColumnWidth(1, 140) 
410         l.setColumnWidth(2, 350) 
411         l.setColumnWidth(3, 140) 
412         l.setColumnWidth(4, 140) 
413         l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
414         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
415         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
416
417         l.setContextMenuPolicy(Qt.CustomContextMenu)
418         l.customContextMenuRequested.connect(self.create_history_menu)
419         return l
420
421
422     def create_history_menu(self, position):
423         self.history_list.selectedIndexes() 
424         item = self.history_list.currentItem()
425         if not item: return
426         tx_hash = str(item.toolTip(0))
427         if not tx_hash: return
428         menu = QMenu()
429         menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
430         menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
431         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
432         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
433
434
435     def tx_details(self, tx_hash):
436         tx_details = self.wallet.get_tx_details(tx_hash)
437         QMessageBox.information(self, 'Details', tx_details, 'OK')
438
439
440     def tx_label_clicked(self, item, column):
441         if column==2 and item.isSelected():
442             tx_hash = str(item.toolTip(0))
443             self.is_edit=True
444             #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
445             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
446             self.history_list.editItem( item, column )
447             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
448             self.is_edit=False
449
450     def tx_label_changed(self, item, column):
451         if self.is_edit: 
452             return
453         self.is_edit=True
454         tx_hash = str(item.toolTip(0))
455         tx = self.wallet.transactions.get(tx_hash)
456         s = self.wallet.labels.get(tx_hash)
457         text = unicode( item.text(2) )
458         if text: 
459             self.wallet.labels[tx_hash] = text
460             item.setForeground(2, QBrush(QColor('black')))
461         else:
462             if s: self.wallet.labels.pop(tx_hash)
463             text = self.wallet.get_default_label(tx_hash)
464             item.setText(2, text)
465             item.setForeground(2, QBrush(QColor('gray')))
466         self.is_edit=False
467
468
469     def edit_label(self, is_recv):
470         l = self.receive_list if is_recv else self.contacts_list
471         c = 2 if is_recv else 1
472         item = l.currentItem()
473         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
474         l.editItem( item, c )
475         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
476
477     def edit_amount(self):
478         l = self.receive_list
479         item = l.currentItem()
480         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
481         l.editItem( item, 3 )
482         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
483
484
485     def address_label_clicked(self, item, column, l, column_addr, column_label):
486         if column == column_label and item.isSelected():
487             addr = unicode( item.text(column_addr) )
488             label = unicode( item.text(column_label) )
489             if label in self.wallet.aliases.keys():
490                 return
491             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
492             l.editItem( item, column )
493             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
494
495
496     def address_label_changed(self, item, column, l, column_addr, column_label):
497
498         if column == column_label:
499             addr = unicode( item.text(column_addr) )
500             text = unicode( item.text(column_label) )
501             changed = False
502
503             if text:
504                 if text not in self.wallet.aliases.keys():
505                     old_addr = self.wallet.labels.get(text)
506                     if old_addr != addr:
507                         self.wallet.labels[addr] = text
508                         changed = True
509                 else:
510                     print_error("Error: This is one of your aliases")
511                     label = self.wallet.labels.get(addr,'')
512                     item.setText(column_label, QString(label))
513             else:
514                 s = self.wallet.labels.get(addr)
515                 if s: 
516                     self.wallet.labels.pop(addr)
517                     changed = True
518
519             if changed:
520                 self.update_history_tab()
521                 self.update_completions()
522                 
523             self.recv_changed(item)
524
525         if column == 3:
526             address = unicode( item.text(column_addr) )
527             text = unicode( item.text(3) )
528             try:
529                 index = self.wallet.addresses.index(address)
530             except:
531                 return
532
533             try:
534                 amount = int( Decimal(text) * 100000000 )
535                 item.setText(3,format_satoshis(amount,False, self.wallet.num_zeros))
536             except:
537                 amount = self.wallet.requested_amounts.get(address)
538                 if amount: 
539                     item.setText(3,format_satoshis(amount,False, self.wallet.num_zeros))
540                 else:
541                     item.setText(3,"")
542                 return
543
544             self.wallet.requested_amounts[address] = amount
545
546             label = self.wallet.labels.get(address)
547             if label is None:
548                 label = self.merchant_name + ' - %04d'%(index+1)
549                 self.wallet.labels[address] = label
550
551             self.update_receive_item(self.receive_list.currentItem())
552             if self.qr_window:
553                 self.qr_window.set_content( address, label, amount )
554
555
556     def recv_changed(self, a):
557         "current item changed"
558         if a is not None and self.qr_window and self.qr_window.isVisible():
559             address = str(a.text(1))
560             label = self.wallet.labels.get(address)
561             amount = self.wallet.requested_amounts.get(address)
562             self.qr_window.set_content( address, label, amount )
563
564
565     def update_history_tab(self):
566
567         self.history_list.clear()
568         for item in self.wallet.get_tx_history():
569             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
570             if conf:
571                 try:
572                     time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
573                 except:
574                     time_str = "unknown"
575                 if conf == -1:
576                     icon = None
577                 if conf == 0:
578                     icon = QIcon(":icons/unconfirmed.png")
579                 elif conf < 6:
580                     icon = QIcon(":icons/clock%d.png"%conf)
581                 else:
582                     icon = QIcon(":icons/confirmed.png")
583             else:
584                 time_str = 'pending'
585                 icon = QIcon(":icons/unconfirmed.png")
586
587             if value is not None:
588                 v_str = format_satoshis(value, True, self.wallet.num_zeros)
589             else:
590                 v_str = '--'
591
592             balance_str = format_satoshis(balance, False, self.wallet.num_zeros)
593             
594             if tx_hash:
595                 label, is_default_label = self.wallet.get_label(tx_hash)
596             else:
597                 label = _('Pruned transaction outputs')
598                 is_default_label = False
599
600             item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
601             item.setFont(2, QFont(MONOSPACE_FONT))
602             item.setFont(3, QFont(MONOSPACE_FONT))
603             item.setFont(4, QFont(MONOSPACE_FONT))
604             if tx_hash:
605                 item.setToolTip(0, tx_hash)
606             if is_default_label:
607                 item.setForeground(2, QBrush(QColor('grey')))
608
609             item.setIcon(0, icon)
610             self.history_list.insertTopLevelItem(0,item)
611             
612
613         self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
614
615
616     def create_send_tab(self):
617         w = QWidget()
618
619         grid = QGridLayout()
620         grid.setSpacing(8)
621         grid.setColumnMinimumWidth(3,300)
622         grid.setColumnStretch(5,1)
623
624         self.payto_e = QLineEdit()
625         grid.addWidget(QLabel(_('Pay to')), 1, 0)
626         grid.addWidget(self.payto_e, 1, 1, 1, 3)
627         
628         def fill_from_qr():
629             qrcode = qrscanner.scan_qr()
630             if 'address' in qrcode:
631                 self.payto_e.setText(qrcode['address'])
632             if 'amount' in qrcode:
633                 self.amount_e.setText(str(qrcode['amount']))
634             if 'label' in qrcode:
635                 self.message_e.setText(qrcode['label'])
636             if 'message' in qrcode:
637                 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
638                 
639
640         if qrscanner.is_available():
641             b = QPushButton(_("Scan QR code"))
642             b.clicked.connect(fill_from_qr)
643             grid.addWidget(b, 1, 5)
644     
645         grid.addWidget(HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)')), 1, 4)
646
647         completer = QCompleter()
648         completer.setCaseSensitivity(False)
649         self.payto_e.setCompleter(completer)
650         completer.setModel(self.completions)
651
652         self.message_e = QLineEdit()
653         grid.addWidget(QLabel(_('Description')), 2, 0)
654         grid.addWidget(self.message_e, 2, 1, 1, 3)
655         grid.addWidget(HelpButton(_('Description of the transaction (not mandatory).') + '\n\n' + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')), 2, 4)
656
657         self.amount_e = QLineEdit()
658         grid.addWidget(QLabel(_('Amount')), 3, 0)
659         grid.addWidget(self.amount_e, 3, 1, 1, 2)
660         grid.addWidget(HelpButton(
661                 _('Amount to be sent.') + '\n\n' \
662                     + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.')), 3, 3)
663         
664         self.fee_e = QLineEdit()
665         grid.addWidget(QLabel(_('Fee')), 4, 0)
666         grid.addWidget(self.fee_e, 4, 1, 1, 2) 
667         grid.addWidget(HelpButton(
668                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
669                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
670                     + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
671         
672         b = EnterButton(_("Send"), self.do_send)
673         grid.addWidget(b, 6, 1)
674
675         b = EnterButton(_("Clear"),self.do_clear)
676         grid.addWidget(b, 6, 2)
677
678         self.payto_sig = QLabel('')
679         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
680
681         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
682         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
683         w.setLayout(grid) 
684
685         w2 = QWidget()
686         vbox = QVBoxLayout()
687         vbox.addWidget(w)
688         vbox.addStretch(1)
689         w2.setLayout(vbox)
690
691         def entry_changed( is_fee ):
692             self.funds_error = False
693             amount = numbify(self.amount_e)
694             fee = numbify(self.fee_e)
695             if not is_fee: fee = None
696             if amount is None:
697                 return
698             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
699             if not is_fee:
700                 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
701             if inputs:
702                 palette = QPalette()
703                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
704             else:
705                 palette = QPalette()
706                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
707                 self.funds_error = True
708             self.amount_e.setPalette(palette)
709             self.fee_e.setPalette(palette)
710
711         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
712         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
713
714         return w2
715
716
717     def update_completions(self):
718         l = []
719         for addr,label in self.wallet.labels.items():
720             if addr in self.wallet.addressbook:
721                 l.append( label + '  <' + addr + '>')
722         l = l + self.wallet.aliases.keys()
723
724         self.completions.setStringList(l)
725
726
727
728     def do_send(self):
729
730         label = unicode( self.message_e.text() )
731         r = unicode( self.payto_e.text() )
732         r = r.strip()
733
734         # alias
735         m1 = re.match(ALIAS_REGEXP, r)
736         # label or alias, with address in brackets
737         m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
738         
739         if m1:
740             to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
741             if not to_address:
742                 return
743         elif m2:
744             to_address = m2.group(2)
745         else:
746             to_address = r
747
748         if not self.wallet.is_valid(to_address):
749             QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
750             return
751
752         try:
753             amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
754         except:
755             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
756             return
757         try:
758             fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
759         except:
760             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
761             return
762
763         if self.wallet.use_encryption:
764             password = self.password_dialog()
765             if not password:
766                 return
767         else:
768             password = None
769
770         try:
771             tx = self.wallet.mktx( [(to_address, amount)], label, password, fee)
772         except BaseException, e:
773             self.show_message(str(e))
774             return
775             
776         h = self.wallet.send_tx(tx)
777         waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
778         status, msg = self.wallet.receive_tx( h )
779
780         if status:
781             QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
782             self.do_clear()
783             self.update_contacts_tab()
784         else:
785             QMessageBox.warning(self, _('Error'), msg, _('OK'))
786
787
788     def set_url(self, url):
789         payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
790         self.tabs.setCurrentIndex(1)
791         label = self.wallet.labels.get(payto)
792         m_addr = label + '  <'+ payto+'>' if label else payto
793         self.payto_e.setText(m_addr)
794
795         self.message_e.setText(message)
796         self.amount_e.setText(amount)
797         if identity:
798             self.set_frozen(self.payto_e,True)
799             self.set_frozen(self.amount_e,True)
800             self.set_frozen(self.message_e,True)
801             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
802         else:
803             self.payto_sig.setVisible(False)
804
805     def do_clear(self):
806         self.payto_sig.setVisible(False)
807         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
808             e.setText('')
809             self.set_frozen(e,False)
810
811     def set_frozen(self,entry,frozen):
812         if frozen:
813             entry.setReadOnly(True)
814             entry.setFrame(False)
815             palette = QPalette()
816             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
817             entry.setPalette(palette)
818         else:
819             entry.setReadOnly(False)
820             entry.setFrame(True)
821             palette = QPalette()
822             palette.setColor(entry.backgroundRole(), QColor('white'))
823             entry.setPalette(palette)
824
825
826     def toggle_freeze(self,addr):
827         if not addr: return
828         if addr in self.wallet.frozen_addresses:
829             self.wallet.unfreeze(addr)
830         else:
831             self.wallet.freeze(addr)
832         self.update_receive_tab()
833
834     def toggle_priority(self,addr):
835         if not addr: return
836         if addr in self.wallet.prioritized_addresses:
837             self.wallet.unprioritize(addr)
838         else:
839             self.wallet.prioritize(addr)
840         self.update_receive_tab()
841
842
843     def create_list_tab(self, headers):
844         "generic tab creation method"
845         l = MyTreeWidget(self)
846         l.setColumnCount( len(headers) )
847         l.setHeaderLabels( headers )
848
849         w = QWidget()
850         vbox = QVBoxLayout()
851         w.setLayout(vbox)
852
853         vbox.setMargin(0)
854         vbox.setSpacing(0)
855         vbox.addWidget(l)
856         buttons = QWidget()
857         vbox.addWidget(buttons)
858
859         hbox = QHBoxLayout()
860         hbox.setMargin(0)
861         hbox.setSpacing(0)
862         buttons.setLayout(hbox)
863
864         return l,w,hbox
865
866
867     def create_receive_tab(self):
868         l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Requested'), _('Balance'), _('Tx')])
869         l.setContextMenuPolicy(Qt.CustomContextMenu)
870         l.customContextMenuRequested.connect(self.create_receive_menu)
871         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
872         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
873         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.recv_changed(a))
874         self.receive_list = l
875         self.receive_buttons_hbox = hbox
876         view_combo = QComboBox()
877         view_combo.addItems([_('Simple View'), _('Detailed View'), _('Point of Sale')])
878         view_combo.setCurrentIndex(self.receive_tab_mode)
879         hbox.addWidget(view_combo)
880         view_combo.currentIndexChanged.connect(self.receive_tab_set_mode)
881         hbox.addStretch(1)
882         return w
883
884
885
886     def receive_tab_set_mode(self, i):
887         self.receive_tab_mode = i
888         self.config.set_key('qt_receive_tab_mode', self.receive_tab_mode, True)
889         self.wallet.save()
890         self.update_receive_tab()
891         self.toggle_QR_window(self.receive_tab_mode == 2)
892
893
894     def create_contacts_tab(self):
895         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
896         l.setContextMenuPolicy(Qt.CustomContextMenu)
897         l.customContextMenuRequested.connect(self.create_contact_menu)
898         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
899         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
900         self.contacts_list = l
901         self.contacts_buttons_hbox = hbox
902         hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
903         hbox.addStretch(1)
904         return w
905
906
907     def create_receive_menu(self, position):
908         # fixme: this function apparently has a side effect.
909         # if it is not called the menu pops up several times
910         #self.receive_list.selectedIndexes() 
911
912         item = self.receive_list.itemAt(position)
913         if not item: return
914         addr = unicode(item.text(1))
915         menu = QMenu()
916         menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
917         if self.receive_tab_mode == 2:
918             menu.addAction(_("Request amount"), lambda: self.edit_amount())
919         menu.addAction(_("View QR"), lambda: ElectrumWindow.show_qrcode("Address","bitcoin:"+addr) )
920         menu.addAction(_("Edit label"), lambda: self.edit_label(True))
921         menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
922
923         t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
924         menu.addAction(t, lambda: self.toggle_freeze(addr))
925         t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
926         menu.addAction(t, lambda: self.toggle_priority(addr))
927         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
928
929
930     def payto(self, x, is_alias):
931         if not x: return
932         if is_alias:
933             label = x
934             m_addr = label
935         else:
936             addr = x
937             label = self.wallet.labels.get(addr)
938             m_addr = label + '  <' + addr + '>' if label else addr
939         self.tabs.setCurrentIndex(1)
940         self.payto_e.setText(m_addr)
941         self.amount_e.setFocus()
942
943     def delete_contact(self, x, is_alias):
944         if self.question("Do you want to remove %s from your list of contacts?"%x):
945             if not is_alias and x in self.wallet.addressbook:
946                 self.wallet.addressbook.remove(x)
947                 if x in self.wallet.labels.keys():
948                     self.wallet.labels.pop(x)
949             elif is_alias and x in self.wallet.aliases:
950                 self.wallet.aliases.pop(x)
951             self.update_history_tab()
952             self.update_contacts_tab()
953             self.update_completions()
954
955     def create_contact_menu(self, position):
956         # fixme: this function apparently has a side effect.
957         # if it is not called the menu pops up several times
958         #self.contacts_list.selectedIndexes() 
959
960         item = self.contacts_list.itemAt(position)
961         if not item: return
962         addr = unicode(item.text(0))
963         label = unicode(item.text(1))
964         is_alias = label in self.wallet.aliases.keys()
965         x = label if is_alias else addr
966         menu = QMenu()
967         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
968         menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
969         menu.addAction(_("View QR code"),lambda: self.show_qrcode("Address","bitcoin:"+addr))
970         if not is_alias:
971             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
972         else:
973             menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
974         menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
975         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
976
977
978     def update_receive_item(self, item):
979         address = str( item.data(1,0).toString() )
980
981         flags = self.wallet.get_address_flags(address)
982         item.setData(0,0,flags)
983
984         label = self.wallet.labels.get(address,'')
985         item.setData(2,0,label)
986
987         amount = self.wallet.requested_amounts.get(address,None)
988         amount_str = format_satoshis( amount, False, self.wallet.num_zeros ) if amount is not None  else ""
989         item.setData(3,0,amount_str)
990         
991         c, u = self.wallet.get_addr_balance(address)
992         balance = format_satoshis( c + u, False, self.wallet.num_zeros )
993         item.setData(4,0,balance)
994
995         if address in self.wallet.frozen_addresses: 
996             item.setBackgroundColor(1, QColor('lightblue'))
997         elif address in self.wallet.prioritized_addresses: 
998             item.setBackgroundColor(1, QColor('lightgreen'))
999         
1000
1001     def update_receive_tab(self):
1002         l = self.receive_list
1003         
1004         l.clear()
1005         l.setColumnHidden(0, not self.receive_tab_mode == 1)
1006         l.setColumnHidden(3, not self.receive_tab_mode == 2)
1007         l.setColumnHidden(4, self.receive_tab_mode == 0)
1008         l.setColumnHidden(5, not self.receive_tab_mode == 1)
1009         l.setColumnWidth(0, 50)
1010         l.setColumnWidth(1, 310) 
1011         l.setColumnWidth(2, 200)
1012         l.setColumnWidth(3, 130)
1013         l.setColumnWidth(4, 130)
1014         l.setColumnWidth(5, 10)
1015
1016         gap = 0
1017         is_red = False
1018         for address in self.wallet.all_addresses():
1019
1020             if self.wallet.is_change(address) and self.receive_tab_mode != 1:
1021                 continue
1022
1023             n = 0 
1024             h = self.wallet.history.get(address,[])
1025
1026             if h != ['*']: 
1027                 for tx_hash, tx_height in h:
1028                     tx = self.wallet.transactions.get(tx_hash)
1029                     if tx: n += 1
1030                 num_tx = "%d "%n
1031             else:
1032                 n = -1
1033                 num_tx = "*"
1034
1035             if n==0:
1036                 if address in self.wallet.addresses:
1037                     gap += 1
1038                     if gap > self.wallet.gap_limit:
1039                         is_red = True
1040             else:
1041                 if address in self.wallet.addresses:
1042                     gap = 0
1043
1044             item = QTreeWidgetItem( [ '', address, '', '', '', num_tx] )
1045             item.setFont(0, QFont(MONOSPACE_FONT))
1046             item.setFont(1, QFont(MONOSPACE_FONT))
1047             item.setFont(3, QFont(MONOSPACE_FONT))
1048             self.update_receive_item(item)
1049             if is_red and address in self.wallet.addresses:
1050                 item.setBackgroundColor(1, QColor('red'))
1051             l.addTopLevelItem(item)
1052
1053         # we use column 1 because column 0 may be hidden
1054         l.setCurrentItem(l.topLevelItem(0),1)
1055
1056     def show_contact_details(self, m):
1057         a = self.wallet.aliases.get(m)
1058         if a:
1059             if a[0] in self.wallet.authorities.keys():
1060                 s = self.wallet.authorities.get(a[0])
1061             else:
1062                 s = "self-signed"
1063             msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
1064             QMessageBox.information(self, 'Alias', msg, 'OK')
1065
1066     def update_contacts_tab(self):
1067
1068         l = self.contacts_list
1069         l.clear()
1070         l.setColumnWidth(0, 350) 
1071         l.setColumnWidth(1, 330)
1072         l.setColumnWidth(2, 100) 
1073
1074         alias_targets = []
1075         for alias, v in self.wallet.aliases.items():
1076             s, target = v
1077             alias_targets.append(target)
1078             item = QTreeWidgetItem( [ target, alias, '-'] )
1079             item.setBackgroundColor(0, QColor('lightgray'))
1080             l.addTopLevelItem(item)
1081             
1082         for address in self.wallet.addressbook:
1083             if address in alias_targets: continue
1084             label = self.wallet.labels.get(address,'')
1085             n = 0 
1086             for item in self.wallet.transactions.values():
1087                 if address in item['outputs'] : n=n+1
1088             tx = "%d"%n
1089             item = QTreeWidgetItem( [ address, label, tx] )
1090             item.setFont(0, QFont(MONOSPACE_FONT))
1091             l.addTopLevelItem(item)
1092
1093         l.setCurrentItem(l.topLevelItem(0))
1094
1095     def create_wall_tab(self):
1096         self.textbox = textbox = QTextEdit(self)
1097         textbox.setFont(QFont(MONOSPACE_FONT))
1098         textbox.setReadOnly(True)
1099         return textbox
1100
1101     def create_status_bar(self):
1102         sb = QStatusBar()
1103         sb.setFixedHeight(35)
1104         if self.wallet.seed:
1105             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
1106         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
1107         if self.wallet.seed:
1108             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
1109         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) ) 
1110         sb.addPermanentWidget( self.status_button )
1111         self.setStatusBar(sb)
1112
1113     def new_contact_dialog(self):
1114         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1115         address = unicode(text)
1116         if ok:
1117             if self.wallet.is_valid(address):
1118                 self.wallet.addressbook.append(address)
1119                 self.wallet.save()
1120                 self.update_contacts_tab()
1121                 self.update_history_tab()
1122                 self.update_completions()
1123             else:
1124                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1125
1126     @staticmethod
1127     def show_seed_dialog(wallet, parent=None):
1128         if not wallet.seed:
1129             QMessageBox.information(parent, _('Message'),
1130                                     _('No seed'), _('OK'))
1131             return
1132
1133         if wallet.use_encryption:
1134             password = parent.password_dialog()
1135             if not password:
1136                 return
1137         else:
1138             password = None
1139             
1140         try:
1141             seed = wallet.pw_decode(wallet.seed, password)
1142         except:
1143             QMessageBox.warning(parent, _('Error'),
1144                                 _('Incorrect Password'), _('OK'))
1145             return
1146
1147         dialog = QDialog(None)
1148         dialog.setModal(1)
1149         dialog.setWindowTitle("Electrum")
1150
1151         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1152
1153         msg =   _("Your wallet generation seed is") +":<p>\"" + brainwallet + "\"<p>" \
1154               + _("Please write down or memorize these 12 words (order is important).") + " " \
1155               + _("This seed will allow you to recover your wallet in case of computer failure.") + "<p>" \
1156               + _("WARNING: Never disclose your seed. Never type it on a website.") + "<p>"
1157
1158         main_text = QLabel(msg)
1159         main_text.setWordWrap(True)
1160
1161         logo = QLabel()
1162         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1163
1164         if parent:
1165             app = parent.app
1166         else:
1167             app = QApplication
1168
1169         copy_function = lambda: app.clipboard().setText(brainwallet)
1170         copy_button = QPushButton(_("Copy to Clipboard"))
1171         copy_button.clicked.connect(copy_function)
1172
1173         show_qr_function = lambda: ElectrumWindow.show_qrcode(_("Seed"), seed)
1174         qr_button = QPushButton(_("View as QR Code"))
1175         qr_button.clicked.connect(show_qr_function)
1176
1177         ok_button = QPushButton(_("OK"))
1178         ok_button.setDefault(True)
1179         ok_button.clicked.connect(dialog.accept)
1180
1181         main_layout = QGridLayout()
1182         main_layout.addWidget(logo, 0, 0)
1183         main_layout.addWidget(main_text, 0, 1, 1, -1)
1184         main_layout.addWidget(copy_button, 1, 1)
1185         main_layout.addWidget(qr_button, 1, 2)
1186         main_layout.addWidget(ok_button, 1, 3)
1187         dialog.setLayout(main_layout)
1188
1189         dialog.exec_()
1190
1191     @staticmethod
1192     def show_qrcode(title, data):
1193         if not data: return
1194         d = QDialog(None)
1195         d.setModal(1)
1196         d.setWindowTitle(title)
1197         d.setMinimumSize(270, 300)
1198         vbox = QVBoxLayout()
1199         qrw = QRCodeWidget(data)
1200         vbox.addWidget(qrw)
1201         vbox.addWidget(QLabel(data))
1202         hbox = QHBoxLayout()
1203         hbox.addStretch(1)
1204
1205         def print_qr(self):
1206             filename = "qrcode.bmp"
1207             bmp.save_qrcode(qrw.qr, filename)
1208             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1209
1210         b = QPushButton(_("Print"))
1211         hbox.addWidget(b)
1212         b.clicked.connect(print_qr)
1213
1214         b = QPushButton(_("Close"))
1215         hbox.addWidget(b)
1216         b.clicked.connect(d.accept)
1217
1218         vbox.addLayout(hbox)
1219         d.setLayout(vbox)
1220         d.exec_()
1221
1222     def sign_message(self,address):
1223         if not address: return
1224         d = QDialog(self)
1225         d.setModal(1)
1226         d.setWindowTitle('Sign Message')
1227         d.setMinimumSize(270, 350)
1228
1229         tab_widget = QTabWidget()
1230         tab = QWidget()
1231         layout = QGridLayout(tab)
1232
1233         sign_address = QLineEdit()
1234         sign_address.setText(address)
1235         layout.addWidget(QLabel(_('Address')), 1, 0)
1236         layout.addWidget(sign_address, 1, 1)
1237
1238         sign_message = QTextEdit()
1239         layout.addWidget(QLabel(_('Message')), 2, 0)
1240         layout.addWidget(sign_message, 2, 1, 2, 1)
1241
1242         sign_signature = QLineEdit()
1243         layout.addWidget(QLabel(_('Signature')), 3, 0)
1244         layout.addWidget(sign_signature, 3, 1)
1245
1246         def do_sign():
1247             if self.wallet.use_encryption:
1248                 password = self.password_dialog()
1249                 if not password:
1250                     return
1251             else:
1252                 password = None
1253
1254             try:
1255                 signature = self.wallet.sign_message(sign_address.text(), sign_message.toPlainText(), password)
1256                 sign_signature.setText(signature)
1257             except BaseException, e:
1258                 self.show_message(str(e))
1259                 return
1260
1261         hbox = QHBoxLayout()
1262         b = QPushButton(_("Sign"))
1263         hbox.addWidget(b)
1264         b.clicked.connect(do_sign)
1265         b = QPushButton(_("Close"))
1266         b.clicked.connect(d.accept)
1267         hbox.addWidget(b)
1268         layout.addLayout(hbox, 4, 1)
1269         tab_widget.addTab(tab, "Sign")
1270
1271
1272         tab = QWidget()
1273         layout = QGridLayout(tab)
1274
1275         verify_address = QLineEdit()
1276         layout.addWidget(QLabel(_('Address')), 1, 0)
1277         layout.addWidget(verify_address, 1, 1)
1278
1279         verify_message = QTextEdit()
1280         layout.addWidget(QLabel(_('Message')), 2, 0)
1281         layout.addWidget(verify_message, 2, 1, 2, 1)
1282
1283         verify_signature = QLineEdit()
1284         layout.addWidget(QLabel(_('Signature')), 3, 0)
1285         layout.addWidget(verify_signature, 3, 1)
1286
1287         def do_verify():
1288             try:
1289                 self.wallet.verify_message(verify_address.text(), verify_signature.text(), verify_message.toPlainText())
1290                 self.show_message("Signature verified")
1291             except BaseException, e:
1292                 self.show_message(str(e))
1293                 return
1294
1295         hbox = QHBoxLayout()
1296         b = QPushButton(_("Verify"))
1297         b.clicked.connect(do_verify)
1298         hbox.addWidget(b)
1299         b = QPushButton(_("Close"))
1300         b.clicked.connect(d.accept)
1301         hbox.addWidget(b)
1302         layout.addLayout(hbox, 4, 1)
1303         tab_widget.addTab(tab, "Verify")
1304
1305         vbox = QVBoxLayout()
1306         vbox.addWidget(tab_widget)
1307         d.setLayout(vbox)
1308         d.exec_()
1309
1310         
1311     def toggle_QR_window(self, show):
1312         if show and not self.qr_window:
1313             self.qr_window = QR_Window()
1314             self.qr_window.setVisible(True)
1315             self.qr_window_geometry = self.qr_window.geometry()
1316             item = self.receive_list.currentItem()
1317             if item:
1318                 address = str(item.text(1))
1319                 label = self.wallet.labels.get(address)
1320                 amount = self.wallet.requested_amounts.get(address)
1321                 self.qr_window.set_content( address, label, amount )
1322
1323         elif show and self.qr_window and not self.qr_window.isVisible():
1324             self.qr_window.setVisible(True)
1325             self.qr_window.setGeometry(self.qr_window_geometry)
1326
1327         elif not show and self.qr_window and self.qr_window.isVisible():
1328             self.qr_window_geometry = self.qr_window.geometry()
1329             self.qr_window.setVisible(False)
1330
1331         #self.print_button.setHidden(self.qr_window is None or not self.qr_window.isVisible())
1332         self.receive_list.setColumnHidden(3, self.qr_window is None or not self.qr_window.isVisible())
1333         self.receive_list.setColumnWidth(2, 200)
1334
1335
1336     def question(self, msg):
1337         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1338
1339     def show_message(self, msg):
1340         QMessageBox.information(self, _('Message'), msg, _('OK'))
1341
1342     def password_dialog(self ):
1343         d = QDialog(self)
1344         d.setModal(1)
1345
1346         pw = QLineEdit()
1347         pw.setEchoMode(2)
1348
1349         vbox = QVBoxLayout()
1350         msg = _('Please enter your password')
1351         vbox.addWidget(QLabel(msg))
1352
1353         grid = QGridLayout()
1354         grid.setSpacing(8)
1355         grid.addWidget(QLabel(_('Password')), 1, 0)
1356         grid.addWidget(pw, 1, 1)
1357         vbox.addLayout(grid)
1358
1359         vbox.addLayout(ok_cancel_buttons(d))
1360         d.setLayout(vbox) 
1361
1362         if not d.exec_(): return
1363         return unicode(pw.text())
1364
1365
1366
1367
1368
1369     @staticmethod
1370     def change_password_dialog( wallet, parent=None ):
1371
1372         if not wallet.seed:
1373             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1374             return
1375
1376         d = QDialog(parent)
1377         d.setModal(1)
1378
1379         pw = QLineEdit()
1380         pw.setEchoMode(2)
1381         new_pw = QLineEdit()
1382         new_pw.setEchoMode(2)
1383         conf_pw = QLineEdit()
1384         conf_pw.setEchoMode(2)
1385
1386         vbox = QVBoxLayout()
1387         if parent:
1388             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1389                    +_('To disable wallet encryption, enter an empty new password.')) \
1390                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1391         else:
1392             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1393                   +_("Leave these fields empty if you want to disable encryption.")
1394         vbox.addWidget(QLabel(msg))
1395
1396         grid = QGridLayout()
1397         grid.setSpacing(8)
1398
1399         if wallet.use_encryption:
1400             grid.addWidget(QLabel(_('Password')), 1, 0)
1401             grid.addWidget(pw, 1, 1)
1402
1403         grid.addWidget(QLabel(_('New Password')), 2, 0)
1404         grid.addWidget(new_pw, 2, 1)
1405
1406         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1407         grid.addWidget(conf_pw, 3, 1)
1408         vbox.addLayout(grid)
1409
1410         vbox.addLayout(ok_cancel_buttons(d))
1411         d.setLayout(vbox) 
1412
1413         if not d.exec_(): return
1414
1415         password = unicode(pw.text()) if wallet.use_encryption else None
1416         new_password = unicode(new_pw.text())
1417         new_password2 = unicode(conf_pw.text())
1418
1419         try:
1420             seed = wallet.pw_decode( wallet.seed, password)
1421         except:
1422             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1423             return
1424
1425         if new_password != new_password2:
1426             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1427             return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1428
1429         wallet.update_password(seed, password, new_password)
1430
1431     @staticmethod
1432     def seed_dialog(wallet, parent=None):
1433         d = QDialog(parent)
1434         d.setModal(1)
1435
1436         vbox = QVBoxLayout()
1437         msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1438         vbox.addWidget(QLabel(msg))
1439
1440         grid = QGridLayout()
1441         grid.setSpacing(8)
1442
1443         seed_e = QLineEdit()
1444         grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1445         grid.addWidget(seed_e, 1, 1)
1446
1447         gap_e = QLineEdit()
1448         gap_e.setText("5")
1449         grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1450         grid.addWidget(gap_e, 2, 1)
1451         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1452         vbox.addLayout(grid)
1453
1454         vbox.addLayout(ok_cancel_buttons(d))
1455         d.setLayout(vbox) 
1456
1457         if not d.exec_(): return
1458
1459         try:
1460             gap = int(unicode(gap_e.text()))
1461         except:
1462             QMessageBox.warning(None, _('Error'), 'error', 'OK')
1463             sys.exit(0)
1464
1465         try:
1466             seed = unicode(seed_e.text())
1467             seed.decode('hex')
1468         except:
1469             print_error("Warning: Not hex, trying decode")
1470             try:
1471                 seed = mnemonic.mn_decode( seed.split(' ') )
1472             except:
1473                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1474                 sys.exit(0)
1475         if not seed:
1476             QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1477             sys.exit(0)
1478         
1479         wallet.seed = str(seed)
1480         #print repr(wallet.seed)
1481         wallet.gap_limit = gap
1482         return True
1483
1484
1485
1486     def settings_dialog(self):
1487         d = QDialog(self)
1488         d.setModal(1)
1489         vbox = QVBoxLayout()
1490         msg = _('Here are the settings of your wallet.') + '\n'\
1491               + _('For more explanations, click on the help buttons next to each field.')
1492
1493         label = QLabel(msg)
1494         label.setFixedWidth(250)
1495         label.setWordWrap(True)
1496         label.setAlignment(Qt.AlignJustify)
1497         vbox.addWidget(label)
1498
1499         grid = QGridLayout()
1500         grid.setSpacing(8)
1501         vbox.addLayout(grid)
1502
1503         fee_label = QLabel(_('Transaction fee'))
1504         grid.addWidget(fee_label, 2, 0)
1505         fee_e = QLineEdit()
1506         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1507         grid.addWidget(fee_e, 2, 1)
1508         msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1509             + _('Recommended value') + ': 0.001'
1510         grid.addWidget(HelpButton(msg), 2, 2)
1511         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1512         if not self.config.is_modifiable('fee'):
1513             for w in [fee_e, fee_label]: w.setEnabled(False)
1514
1515         nz_label = QLabel(_('Display zeros'))
1516         grid.addWidget(nz_label, 3, 0)
1517         nz_e = QLineEdit()
1518         nz_e.setText("%d"% self.wallet.num_zeros)
1519         grid.addWidget(nz_e, 3, 1)
1520         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1521         grid.addWidget(HelpButton(msg), 3, 2)
1522         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1523         if not self.config.is_modifiable('num_zeros'):
1524             for w in [nz_e, nz_label]: w.setEnabled(False)
1525
1526         usechange_cb = QCheckBox(_('Use change addresses'))
1527         grid.addWidget(usechange_cb, 5, 0)
1528         usechange_cb.setChecked(self.wallet.use_change)
1529         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1530         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1531
1532         gap_label = QLabel(_('Gap limit'))
1533         grid.addWidget(gap_label, 6, 0)
1534         gap_e = QLineEdit()
1535         gap_e.setText("%d"% self.wallet.gap_limit)
1536         grid.addWidget(gap_e, 6, 1)
1537         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1538               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1539               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1540               + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1541               + _('Warning') + ': ' \
1542               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1543               + _('Do not modify it if you do not understand what you are doing, or if you expect to recover your wallet without knowing it!') + '\n\n' 
1544         grid.addWidget(HelpButton(msg), 6, 2)
1545         gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1546         if not self.config.is_modifiable('gap_limit'):
1547             for w in [gap_e, gap_label]: w.setEnabled(False)
1548         
1549         gui_label=QLabel(_('Default GUI') + ':')
1550         grid.addWidget(gui_label , 7, 0)
1551         gui_combo = QComboBox()
1552         gui_combo.addItems(['Lite', 'Classic', 'Gtk', 'Text'])
1553         index = gui_combo.findText(self.config.get("gui","classic").capitalize())
1554         if index==-1: index = 1
1555         gui_combo.setCurrentIndex(index)
1556         grid.addWidget(gui_combo, 7, 1)
1557         grid.addWidget(HelpButton(_('Select which GUI mode to use at start up. ')), 7, 2)
1558         if not self.config.is_modifiable('gui'):
1559             for w in [gui_combo, gui_label]: w.setEnabled(False)
1560
1561         vbox.addLayout(ok_cancel_buttons(d))
1562         d.setLayout(vbox) 
1563
1564         # run the dialog
1565         if not d.exec_(): return
1566
1567         fee = unicode(fee_e.text())
1568         try:
1569             fee = int( 100000000 * Decimal(fee) )
1570         except:
1571             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1572             return
1573
1574         if self.wallet.fee != fee:
1575             self.wallet.fee = fee
1576             self.wallet.save()
1577         
1578         nz = unicode(nz_e.text())
1579         try:
1580             nz = int( nz )
1581             if nz>8: nz=8
1582         except:
1583             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1584             return
1585
1586         if self.wallet.num_zeros != nz:
1587             self.wallet.num_zeros = nz
1588             self.config.set_key('num_zeros', nz, True)
1589             self.update_history_tab()
1590             self.update_receive_tab()
1591
1592         if self.wallet.use_change != usechange_cb.isChecked():
1593             self.wallet.use_change = usechange_cb.isChecked()
1594             self.config.set_key('use_change', self.wallet.use_change, True)
1595         
1596         try:
1597             n = int(gap_e.text())
1598         except:
1599             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1600             return
1601
1602         if self.wallet.gap_limit != n:
1603             r = self.wallet.change_gap_limit(n)
1604             if r:
1605                 self.update_receive_tab()
1606                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
1607             else:
1608                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1609                     
1610         self.config.set_key("gui", str(gui_combo.currentText()).lower(), True)
1611
1612
1613
1614     @staticmethod 
1615     def network_dialog(wallet, parent=None):
1616         interface = wallet.interface
1617         if parent:
1618             if interface.is_connected:
1619                 status = _("Connected to")+" %s\n%d blocks"%(interface.host, wallet.verifier.height)
1620             else:
1621                 status = _("Not connected")
1622             server = interface.server
1623         else:
1624             import random
1625             status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
1626             server = interface.server
1627
1628         plist, servers_list = interface.get_servers_list()
1629
1630         d = QDialog(parent)
1631         d.setModal(1)
1632         d.setWindowTitle(_('Server'))
1633         d.setMinimumSize(375, 20)
1634
1635         vbox = QVBoxLayout()
1636         vbox.setSpacing(30)
1637
1638         hbox = QHBoxLayout()
1639         l = QLabel()
1640         l.setPixmap(QPixmap(":icons/network.png"))
1641         hbox.addStretch(10)
1642         hbox.addWidget(l)
1643         hbox.addWidget(QLabel(status))
1644         hbox.addStretch(50)
1645         vbox.addLayout(hbox)
1646
1647
1648         # grid layout
1649         grid = QGridLayout()
1650         grid.setSpacing(8)
1651         vbox.addLayout(grid)
1652
1653         # server
1654         server_protocol = QComboBox()
1655         server_host = QLineEdit()
1656         server_host.setFixedWidth(200)
1657         server_port = QLineEdit()
1658         server_port.setFixedWidth(60)
1659
1660         protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
1661         protocol_letters = 'thsg'
1662         DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
1663         server_protocol.addItems(protocol_names)
1664
1665         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
1666         grid.addWidget(server_protocol, 0, 1)
1667         grid.addWidget(server_host, 0, 2)
1668         grid.addWidget(server_port, 0, 3)
1669
1670         def change_protocol(p):
1671             protocol = protocol_letters[p]
1672             host = unicode(server_host.text())
1673             pp = plist.get(host,DEFAULT_PORTS)
1674             if protocol not in pp.keys():
1675                 protocol = pp.keys()[0]
1676             port = pp[protocol]
1677             server_host.setText( host )
1678             server_port.setText( port )
1679
1680         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
1681         
1682         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
1683         servers_list_widget = QTreeWidget(parent)
1684         servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
1685         servers_list_widget.setMaximumHeight(150)
1686         servers_list_widget.setColumnWidth(0, 240)
1687         for _host in servers_list.keys():
1688             _type = 'pruning' if servers_list[_host].get('pruning') else 'full'
1689             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
1690
1691         def change_server(host, protocol=None):
1692             pp = plist.get(host,DEFAULT_PORTS)
1693             if protocol:
1694                 port = pp.get(protocol)
1695                 if not port: protocol = None
1696                     
1697             if not protocol:
1698                 if 't' in pp.keys():
1699                     protocol = 't'
1700                     port = pp.get(protocol)
1701                 else:
1702                     protocol = pp.keys()[0]
1703                     port = pp.get(protocol)
1704             
1705             server_host.setText( host )
1706             server_port.setText( port )
1707             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
1708
1709             if not plist: return
1710             for p in protocol_letters:
1711                 i = protocol_letters.index(p)
1712                 j = server_protocol.model().index(i,0)
1713                 if p not in pp.keys():
1714                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
1715                 else:
1716                     server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
1717
1718
1719         if server:
1720             host, port, protocol = server.split(':')
1721             change_server(host,protocol)
1722
1723         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
1724         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
1725
1726         if not wallet.config.is_modifiable('server'):
1727             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
1728
1729         # proxy setting
1730         proxy_mode = QComboBox()
1731         proxy_host = QLineEdit()
1732         proxy_host.setFixedWidth(200)
1733         proxy_port = QLineEdit()
1734         proxy_port.setFixedWidth(60)
1735         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
1736
1737         def check_for_disable(index = False):
1738             if proxy_mode.currentText() != 'NONE':
1739                 proxy_host.setEnabled(True)
1740                 proxy_port.setEnabled(True)
1741             else:
1742                 proxy_host.setEnabled(False)
1743                 proxy_port.setEnabled(False)
1744
1745         check_for_disable()
1746         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
1747
1748         if not wallet.config.is_modifiable('proxy'):
1749             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
1750
1751         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
1752         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
1753         proxy_host.setText(proxy_config.get("host"))
1754         proxy_port.setText(proxy_config.get("port"))
1755
1756         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
1757         grid.addWidget(proxy_mode, 2, 1)
1758         grid.addWidget(proxy_host, 2, 2)
1759         grid.addWidget(proxy_port, 2, 3)
1760
1761         # buttons
1762         vbox.addLayout(ok_cancel_buttons(d))
1763         d.setLayout(vbox) 
1764
1765         if not d.exec_(): return
1766
1767         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
1768         if proxy_mode.currentText() != 'NONE':
1769             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
1770         else:
1771             proxy = None
1772
1773         wallet.config.set_key("proxy", proxy, True)
1774         wallet.config.set_key("server", server, True)
1775         interface.set_server(server, proxy)
1776                 
1777         return True
1778
1779     def closeEvent(self, event):
1780         g = self.geometry()
1781         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1782         event.accept()
1783
1784
1785 class ElectrumGui:
1786
1787     def __init__(self, wallet, config, app=None):
1788         self.wallet = wallet
1789         self.config = config
1790         if app is None:
1791             self.app = QApplication(sys.argv)
1792
1793
1794     def restore_or_create(self):
1795         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1796         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1797         if r==2: return None
1798         return 'restore' if r==1 else 'create'
1799
1800     def seed_dialog(self):
1801         return ElectrumWindow.seed_dialog( self.wallet )
1802
1803     def network_dialog(self):
1804         return ElectrumWindow.network_dialog( self.wallet, parent=None )
1805         
1806
1807     def show_seed(self):
1808         ElectrumWindow.show_seed_dialog(self.wallet)
1809
1810
1811     def password_dialog(self):
1812         ElectrumWindow.change_password_dialog(self.wallet)
1813
1814
1815     def restore_wallet(self):
1816         wallet = self.wallet
1817         # wait until we are connected, because the user might have selected another server
1818         if not wallet.interface.is_connected:
1819             waiting = lambda: False if wallet.interface.is_connected else "connecting...\n"
1820             waiting_dialog(waiting)
1821
1822         waiting = lambda: False if wallet.is_up_to_date() else "Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1823             %(len(wallet.all_addresses()), wallet.interface.bytes_received/1024.)
1824
1825         wallet.set_up_to_date(False)
1826         wallet.interface.poke('synchronizer')
1827         waiting_dialog(waiting)
1828         if wallet.is_found():
1829             print_error( "Recovery successful" )
1830         else:
1831             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1832
1833         return True
1834
1835     def main(self,url):
1836         s = Timer()
1837         s.start()
1838         w = ElectrumWindow(self.wallet, self.config)
1839         if url: w.set_url(url)
1840         w.app = self.app
1841         w.connect_slots(s)
1842         w.update_wallet()
1843         w.show()
1844
1845         self.app.exec_()