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