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