ff580636c695a2d8ca6062c092024625b2f89e16
[electrum-nvc.git] / gui / gui_classic.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 _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
23 from qrcodewidget import QRCodeWidget
24
25 try:
26     import PyQt4
27 except:
28     sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
29
30 from PyQt4.QtGui import *
31 from PyQt4.QtCore import *
32 import PyQt4.QtCore as QtCore
33 import PyQt4.QtGui as QtGui
34 from electrum.interface import DEFAULT_SERVERS
35
36 try:
37     import icons_rc
38 except:
39     sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o gui/icons_rc.py'")
40
41 from electrum.wallet import format_satoshis
42 from electrum.bitcoin import Transaction, is_valid
43 from electrum import mnemonic
44
45 import bmp, pyqrnative
46 import exchange_rate
47
48 from decimal import Decimal
49
50 import platform
51 import httplib
52 import socket
53 import webbrowser
54 import csv
55
56 if platform.system() == 'Windows':
57     MONOSPACE_FONT = 'Lucida Console'
58 elif platform.system() == 'Darwin':
59     MONOSPACE_FONT = 'Monaco'
60 else:
61     MONOSPACE_FONT = 'monospace'
62
63 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'    
64
65 from electrum import ELECTRUM_VERSION
66 import re
67
68 class UpdateLabel(QtGui.QLabel):
69     def __init__(self, config, parent=None):
70         QtGui.QLabel.__init__(self, parent)
71         self.new_version = False
72
73         try:
74             con = httplib.HTTPConnection('electrum.org', 80, timeout=5)
75             con.request("GET", "/version")
76             res = con.getresponse()
77         except socket.error as msg:
78             print_error("Could not retrieve version information")
79             return
80             
81         if res.status == 200:
82             self.latest_version = res.read()
83             self.latest_version = self.latest_version.replace("\n","")
84             if(re.match('^\d+(\.\d+)*$', self.latest_version)):
85                 self.config = config
86                 self.current_version = ELECTRUM_VERSION
87                 if(self.compare_versions(self.latest_version, self.current_version) == 1):
88                     latest_seen = self.config.get("last_seen_version",ELECTRUM_VERSION)
89                     if(self.compare_versions(self.latest_version, latest_seen) == 1):
90                         self.new_version = True
91                         self.setText(_("New version available") + ": " + self.latest_version)
92
93
94     def compare_versions(self, version1, version2):
95         def normalize(v):
96             return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
97         return cmp(normalize(version1), normalize(version2))
98
99     def ignore_this_version(self):
100         self.setText("")
101         self.config.set_key("last_seen_version", self.latest_version, True)
102         QMessageBox.information(self, _("Preference saved"), _("Notifications about this update will not be shown again."))
103         self.dialog.done(0)
104
105     def ignore_all_version(self):
106         self.setText("")
107         self.config.set_key("last_seen_version", "9.9.9", True)
108         QMessageBox.information(self, _("Preference saved"), _("No more notifications about version updates will be shown."))
109         self.dialog.done(0)
110   
111     def open_website(self):
112         webbrowser.open("http://electrum.org/download.html")
113         self.dialog.done(0)
114
115     def mouseReleaseEvent(self, event):
116         dialog = QDialog(self)
117         dialog.setWindowTitle(_('Electrum update'))
118         dialog.setModal(1)
119
120         main_layout = QGridLayout()
121         main_layout.addWidget(QLabel(_("A new version of Electrum is available:")+" " + self.latest_version), 0,0,1,3)
122         
123         ignore_version = QPushButton(_("Ignore this version"))
124         ignore_version.clicked.connect(self.ignore_this_version)
125
126         ignore_all_versions = QPushButton(_("Ignore all versions"))
127         ignore_all_versions.clicked.connect(self.ignore_all_version)
128
129         open_website = QPushButton(_("Goto download page"))
130         open_website.clicked.connect(self.open_website)
131
132         main_layout.addWidget(ignore_version, 1, 0)
133         main_layout.addWidget(ignore_all_versions, 1, 1)
134         main_layout.addWidget(open_website, 1, 2)
135
136         dialog.setLayout(main_layout)
137
138         self.dialog = dialog
139         
140         if not dialog.exec_(): return
141
142 def numbify(entry, is_int = False):
143     text = unicode(entry.text()).strip()
144     pos = entry.cursorPosition()
145     chars = '0123456789'
146     if not is_int: chars +='.'
147     s = ''.join([i for i in text if i in chars])
148     if not is_int:
149         if '.' in s:
150             p = s.find('.')
151             s = s.replace('.','')
152             s = s[:p] + '.' + s[p:p+8]
153         try:
154             amount = int( Decimal(s) * 100000000 )
155         except:
156             amount = None
157     else:
158         try:
159             amount = int( s )
160         except:
161             amount = None
162     entry.setText(s)
163     entry.setCursorPosition(pos)
164     return amount
165
166
167 class Timer(QtCore.QThread):
168     def run(self):
169         while True:
170             self.emit(QtCore.SIGNAL('timersignal'))
171             time.sleep(0.5)
172
173 class HelpButton(QPushButton):
174     def __init__(self, text):
175         QPushButton.__init__(self, '?')
176         self.setFocusPolicy(Qt.NoFocus)
177         self.setFixedWidth(20)
178         self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
179
180
181 class EnterButton(QPushButton):
182     def __init__(self, text, func):
183         QPushButton.__init__(self, text)
184         self.func = func
185         self.clicked.connect(func)
186
187     def keyPressEvent(self, e):
188         if e.key() == QtCore.Qt.Key_Return:
189             apply(self.func,())
190
191 class MyTreeWidget(QTreeWidget):
192     def __init__(self, parent):
193         QTreeWidget.__init__(self, parent)
194         def ddfr(item):
195             if not item: return
196             for i in range(0,self.viewport().height()/5):
197                 if self.itemAt(QPoint(0,i*5)) == item:
198                     break
199             else:
200                 return
201             for j in range(0,30):
202                 if self.itemAt(QPoint(0,i*5 + j)) != item:
203                     break
204             self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
205
206         self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
207         
208
209
210
211 class StatusBarButton(QPushButton):
212     def __init__(self, icon, tooltip, func):
213         QPushButton.__init__(self, icon, '')
214         self.setToolTip(tooltip)
215         self.setFlat(True)
216         self.setMaximumWidth(25)
217         self.clicked.connect(func)
218         self.func = func
219
220     def keyPressEvent(self, e):
221         if e.key() == QtCore.Qt.Key_Return:
222             apply(self.func,())
223
224
225
226
227
228
229 def waiting_dialog(f):
230
231     s = Timer()
232     s.start()
233     w = QDialog()
234     w.resize(200, 70)
235     w.setWindowTitle('Electrum')
236     l = QLabel('')
237     vbox = QVBoxLayout()
238     vbox.addWidget(l)
239     w.setLayout(vbox)
240     w.show()
241     def ff():
242         s = f()
243         if s: l.setText(s)
244         else: w.close()
245     w.connect(s, QtCore.SIGNAL('timersignal'), ff)
246     w.exec_()
247     w.destroy()
248
249
250 def ok_cancel_buttons(dialog):
251     hbox = QHBoxLayout()
252     hbox.addStretch(1)
253     b = QPushButton("OK")
254     hbox.addWidget(b)
255     b.clicked.connect(dialog.accept)
256     b = QPushButton("Cancel")
257     hbox.addWidget(b)
258     b.clicked.connect(dialog.reject)
259     return hbox
260
261
262 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], 
263         "receive":[[370],[370,200,130,130],[370,200,130,130]] }
264
265 class ElectrumWindow(QMainWindow):
266
267     def __init__(self, wallet, config):
268         QMainWindow.__init__(self)
269         self.lite = None
270         self.wallet = wallet
271         self.config = config
272         self.wallet.interface.register_callback('updated', self.update_callback)
273         self.wallet.interface.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')) )
274         self.wallet.interface.register_callback('disconnected', self.update_callback)
275         self.wallet.interface.register_callback('disconnecting', self.update_callback)
276
277         self.expert_mode = config.get('classic_expert_mode', False)
278         self.merchant_name = config.get('merchant_name', 'Invoice')
279
280         set_language(config.get('language'))
281
282         self.funds_error = False
283         self.completions = QStringListModel()
284
285         self.tabs = tabs = QTabWidget(self)
286         self.column_widths = self.config.get("column-widths", default_column_widths )
287         tabs.addTab(self.create_history_tab(), _('History') )
288         tabs.addTab(self.create_send_tab(), _('Send') )
289         tabs.addTab(self.create_receive_tab(), _('Receive') )
290         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
291         tabs.addTab(self.create_console_tab(), _('Console') )
292         tabs.setMinimumSize(600, 400)
293         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
294         self.setCentralWidget(tabs)
295         self.create_status_bar()
296
297         g = self.config.get("winpos-qt",[100, 100, 840, 400])
298         self.setGeometry(g[0], g[1], g[2], g[3])
299         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.config.path
300         if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
301         self.setWindowTitle( title )
302
303         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
304         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
305         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
306         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
307         
308         self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
309         self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.wallet.banner) )
310         self.history_list.setFocus(True)
311         
312         self.exchanger = exchange_rate.Exchanger(self)
313         self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
314
315         # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
316         if platform.system() == 'Windows':
317             n = 3 if self.wallet.seed else 2
318             tabs.setCurrentIndex (n)
319             tabs.setCurrentIndex (0)
320
321         # set initial message
322         self.console.showMessage(self.wallet.banner)
323
324         #init plugins
325         for p in self.wallet.plugins:
326             try:
327                 p.init_gui(self)
328             except:
329                 print_msg("Error:cannot initialize plugin",p)
330                 traceback.print_exc(file=sys.stdout)
331
332
333     def close(self):
334         QMainWindow.close(self)
335         self.wallet.run_hook('close_main_window', (self,))
336
337     def connect_slots(self, sender):
338         self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
339         self.previous_payto_e=''
340
341     def timer_actions(self):
342         self.wallet.run_hook('timer_actions', (self,))
343             
344         if self.payto_e.hasFocus():
345             return
346         r = unicode( self.payto_e.text() )
347         if r != self.previous_payto_e:
348             self.previous_payto_e = r
349             r = r.strip()
350             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
351                 try:
352                     to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
353                 except:
354                     return
355                 if to_address:
356                     s = r + '  <' + to_address + '>'
357                     self.payto_e.setText(s)
358
359
360     def update_callback(self):
361         self.emit(QtCore.SIGNAL('updatesignal'))
362
363     def update_wallet(self):
364         if self.wallet.interface and self.wallet.interface.is_connected:
365             if not self.wallet.up_to_date:
366                 text = _("Synchronizing...")
367                 icon = QIcon(":icons/status_waiting.png")
368             else:
369                 c, u = self.wallet.get_balance()
370                 text =  _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
371                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
372                 text += self.create_quote_text(Decimal(c+u)/100000000)
373                 icon = QIcon(":icons/status_connected.png")
374         else:
375             text = _("Not connected")
376             icon = QIcon(":icons/status_disconnected.png")
377
378         self.status_text = text
379         self.statusBar().showMessage(text)
380         self.status_button.setIcon( icon )
381
382         if self.wallet.up_to_date or not self.wallet.interface.is_connected:
383             self.update_history_tab()
384             self.update_receive_tab()
385             self.update_contacts_tab()
386             self.update_completions()
387
388
389     def create_quote_text(self, btc_balance):
390         quote_currency = self.config.get("currency", "None")
391         quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
392         if quote_balance is None:
393             quote_text = ""
394         else:
395             quote_text = "  (%.2f %s)" % (quote_balance, quote_currency)
396         return quote_text
397         
398     def create_history_tab(self):
399         self.history_list = l = MyTreeWidget(self)
400         l.setColumnCount(5)
401         for i,width in enumerate(self.column_widths['history']):
402             l.setColumnWidth(i, width)
403         l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
404         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
405         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
406
407         l.setContextMenuPolicy(Qt.CustomContextMenu)
408         l.customContextMenuRequested.connect(self.create_history_menu)
409         return l
410
411
412     def create_history_menu(self, position):
413         self.history_list.selectedIndexes() 
414         item = self.history_list.currentItem()
415         if not item: return
416         tx_hash = str(item.data(0, Qt.UserRole).toString())
417         if not tx_hash: return
418         menu = QMenu()
419         #menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
420         menu.addAction(_("Details"), lambda: self.show_tx_details(self.wallet.transactions.get(tx_hash)))
421         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
422         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
423
424
425     def show_tx_details(self, tx):
426         dialog = QDialog(None)
427         dialog.setModal(1)
428         dialog.setWindowTitle(_("Transaction Details"))
429         vbox = QVBoxLayout()
430         dialog.setLayout(vbox)
431         dialog.setMinimumSize(600,300)
432
433         tx_hash = tx.hash()
434         if tx_hash in self.wallet.transactions.keys():
435             is_mine, v, fee = self.wallet.get_tx_value(tx)
436             conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
437             if timestamp:
438                 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
439             else:
440                 time_str = 'pending'
441         else:
442             is_mine = False
443
444         vbox.addWidget(QLabel("Transaction ID:"))
445         e  = QLineEdit(tx_hash)
446         e.setReadOnly(True)
447         vbox.addWidget(e)
448
449         vbox.addWidget(QLabel("Date: %s"%time_str))
450         vbox.addWidget(QLabel("Status: %d confirmations"%conf))
451         if is_mine:
452             if fee: 
453                 vbox.addWidget(QLabel("Amount sent: %s"% format_satoshis(v-fee, False)))
454                 vbox.addWidget(QLabel("Transaction fee: %s"% format_satoshis(fee, False)))
455             else:
456                 vbox.addWidget(QLabel("Amount sent: %s"% format_satoshis(v, False)))
457                 vbox.addWidget(QLabel("Transaction fee: unknown"))
458         else:
459             vbox.addWidget(QLabel("Amount received: %s"% format_satoshis(v, False)))
460
461         vbox.addWidget( self.generate_transaction_information_widget(tx) )
462
463         ok_button = QPushButton(_("Close"))
464         ok_button.setDefault(True)
465         ok_button.clicked.connect(dialog.accept)
466         
467         hbox = QHBoxLayout()
468         hbox.addStretch(1)
469         hbox.addWidget(ok_button)
470         vbox.addLayout(hbox)
471         dialog.exec_()
472
473     def tx_label_clicked(self, item, column):
474         if column==2 and item.isSelected():
475             self.is_edit=True
476             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
477             self.history_list.editItem( item, column )
478             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
479             self.is_edit=False
480
481     def tx_label_changed(self, item, column):
482         if self.is_edit: 
483             return
484         self.is_edit=True
485         tx_hash = str(item.data(0, Qt.UserRole).toString())
486         tx = self.wallet.transactions.get(tx_hash)
487         s = self.wallet.labels.get(tx_hash)
488         text = unicode( item.text(2) )
489         if text: 
490             self.wallet.labels[tx_hash] = text
491             item.setForeground(2, QBrush(QColor('black')))
492         else:
493             if s: self.wallet.labels.pop(tx_hash)
494             text = self.wallet.get_default_label(tx_hash)
495             item.setText(2, text)
496             item.setForeground(2, QBrush(QColor('gray')))
497         self.is_edit=False
498
499
500     def edit_label(self, is_recv):
501         l = self.receive_list if is_recv else self.contacts_list
502         c = 2 if is_recv else 1
503         item = l.currentItem()
504         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
505         l.editItem( item, c )
506         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
507
508
509
510     def address_label_clicked(self, item, column, l, column_addr, column_label):
511         if column == column_label and item.isSelected():
512             addr = unicode( item.text(column_addr) )
513             label = unicode( item.text(column_label) )
514             if label in self.wallet.aliases.keys():
515                 return
516             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
517             l.editItem( item, column )
518             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
519
520
521     def address_label_changed(self, item, column, l, column_addr, column_label):
522
523         if column == column_label:
524             addr = unicode( item.text(column_addr) )
525             text = unicode( item.text(column_label) )
526             changed = False
527
528             if text:
529                 if text not in self.wallet.aliases.keys():
530                     old_addr = self.wallet.labels.get(text)
531                     if old_addr != addr:
532                         self.wallet.labels[addr] = text
533                         changed = True
534                 else:
535                     print_error("Error: This is one of your aliases")
536                     label = self.wallet.labels.get(addr,'')
537                     item.setText(column_label, QString(label))
538             else:
539                 s = self.wallet.labels.get(addr)
540                 if s: 
541                     self.wallet.labels.pop(addr)
542                     changed = True
543
544             if changed:
545                 self.update_history_tab()
546                 self.update_completions()
547                 
548             self.current_item_changed(item)
549
550         self.wallet.run_hook('item_changed',(self, item, column))
551
552
553     def current_item_changed(self, a):
554         self.wallet.run_hook('current_item_changed',(self, a))
555
556
557
558     def update_history_tab(self):
559
560         self.history_list.clear()
561         for item in self.wallet.get_tx_history():
562             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
563             if conf:
564                 try:
565                     time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
566                 except:
567                     time_str = "unknown"
568                 if conf == -1:
569                     icon = None
570                 if conf == 0:
571                     icon = QIcon(":icons/unconfirmed.png")
572                 elif conf < 6:
573                     icon = QIcon(":icons/clock%d.png"%conf)
574                 else:
575                     icon = QIcon(":icons/confirmed.png")
576             else:
577                 time_str = 'pending'
578                 icon = QIcon(":icons/unconfirmed.png")
579
580             if value is not None:
581                 v_str = format_satoshis(value, True, self.wallet.num_zeros)
582             else:
583                 v_str = '--'
584
585             balance_str = format_satoshis(balance, False, self.wallet.num_zeros)
586             
587             if tx_hash:
588                 label, is_default_label = self.wallet.get_label(tx_hash)
589             else:
590                 label = _('Pruned transaction outputs')
591                 is_default_label = False
592
593             item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
594             item.setFont(2, QFont(MONOSPACE_FONT))
595             item.setFont(3, QFont(MONOSPACE_FONT))
596             item.setFont(4, QFont(MONOSPACE_FONT))
597             if value < 0:
598                 item.setForeground(3, QBrush(QColor("#BC1E1E")))
599             if tx_hash:
600                 item.setData(0, Qt.UserRole, tx_hash)
601                 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
602             if is_default_label:
603                 item.setForeground(2, QBrush(QColor('grey')))
604
605             item.setIcon(0, icon)
606             self.history_list.insertTopLevelItem(0,item)
607             
608
609         self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
610
611
612     def create_send_tab(self):
613         w = QWidget()
614
615         grid = QGridLayout()
616         grid.setSpacing(8)
617         grid.setColumnMinimumWidth(3,300)
618         grid.setColumnStretch(5,1)
619
620         self.payto_e = QLineEdit()
621         grid.addWidget(QLabel(_('Pay to')), 1, 0)
622         grid.addWidget(self.payto_e, 1, 1, 1, 3)
623             
624         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)
625
626         completer = QCompleter()
627         completer.setCaseSensitivity(False)
628         self.payto_e.setCompleter(completer)
629         completer.setModel(self.completions)
630
631         self.message_e = QLineEdit()
632         grid.addWidget(QLabel(_('Description')), 2, 0)
633         grid.addWidget(self.message_e, 2, 1, 1, 3)
634         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)
635
636         self.amount_e = QLineEdit()
637         grid.addWidget(QLabel(_('Amount')), 3, 0)
638         grid.addWidget(self.amount_e, 3, 1, 1, 2)
639         grid.addWidget(HelpButton(
640                 _('Amount to be sent.') + '\n\n' \
641                     + _('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)
642         
643         self.fee_e = QLineEdit()
644         grid.addWidget(QLabel(_('Fee')), 4, 0)
645         grid.addWidget(self.fee_e, 4, 1, 1, 2) 
646         grid.addWidget(HelpButton(
647                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
648                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
649                     + _('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)
650         b = ''
651         if self.wallet.seed: 
652             b = EnterButton(_("Send"), self.do_send)
653         else:
654             b = EnterButton(_("Create unsigned transaction"), self.do_send)
655         grid.addWidget(b, 6, 1)
656
657         b = EnterButton(_("Clear"),self.do_clear)
658         grid.addWidget(b, 6, 2)
659
660         self.payto_sig = QLabel('')
661         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
662
663         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
664         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
665         w.setLayout(grid) 
666
667         w2 = QWidget()
668         vbox = QVBoxLayout()
669         vbox.addWidget(w)
670         vbox.addStretch(1)
671         w2.setLayout(vbox)
672
673         def entry_changed( is_fee ):
674             self.funds_error = False
675             amount = numbify(self.amount_e)
676             fee = numbify(self.fee_e)
677             if not is_fee: fee = None
678             if amount is None:
679                 return
680             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
681             if not is_fee:
682                 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
683             if inputs:
684                 palette = QPalette()
685                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
686                 text = self.status_text
687             else:
688                 palette = QPalette()
689                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
690                 self.funds_error = True
691                 text = _( "Not enough funds" )
692
693             self.statusBar().showMessage(text)
694             self.amount_e.setPalette(palette)
695             self.fee_e.setPalette(palette)
696
697         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
698         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
699
700         self.wallet.run_hook('create_send_tab',(self,grid))
701         return w2
702
703
704     def update_completions(self):
705         l = []
706         for addr,label in self.wallet.labels.items():
707             if addr in self.wallet.addressbook:
708                 l.append( label + '  <' + addr + '>')
709         l = l + self.wallet.aliases.keys()
710
711         self.completions.setStringList(l)
712
713
714
715     def do_send(self):
716
717         label = unicode( self.message_e.text() )
718         r = unicode( self.payto_e.text() )
719         r = r.strip()
720
721         # alias
722         m1 = re.match(ALIAS_REGEXP, r)
723         # label or alias, with address in brackets
724         m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
725         
726         if m1:
727             to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
728             if not to_address:
729                 return
730         elif m2:
731             to_address = m2.group(2)
732         else:
733             to_address = r
734
735         if not is_valid(to_address):
736             QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
737             return
738
739         try:
740             amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
741         except:
742             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
743             return
744         try:
745             fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
746         except:
747             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
748             return
749
750         if self.wallet.use_encryption:
751             password = self.password_dialog()
752             if not password:
753                 return
754         else:
755             password = None
756
757         try:
758             tx = self.wallet.mktx( [(to_address, amount)], password, fee)
759         except BaseException, e:
760             self.show_message(str(e))
761             return
762
763         self.wallet.run_hook('send_tx', (wallet, self, tx))
764
765         if label: 
766             self.wallet.labels[tx.hash()] = label
767
768         if tx.is_complete:
769             h = self.wallet.send_tx(tx)
770             waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
771             status, msg = self.wallet.receive_tx( h )
772             if status:
773                 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
774                 self.do_clear()
775                 self.update_contacts_tab()
776             else:
777                 QMessageBox.warning(self, _('Error'), msg, _('OK'))
778         else:
779             filename = 'unsigned_tx_%s' % (time.mktime(time.gmtime()))
780             try:
781                 fileName = QFileDialog.getSaveFileName(QWidget(), _("Select a transaction filename"), os.path.expanduser('~/%s' % (filename)))
782                 with open(fileName,'w') as f:
783                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
784                 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
785             except:
786                 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
787
788
789
790
791     def set_url(self, url):
792         payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
793         self.tabs.setCurrentIndex(1)
794         label = self.wallet.labels.get(payto)
795         m_addr = label + '  <'+ payto+'>' if label else payto
796         self.payto_e.setText(m_addr)
797
798         self.message_e.setText(message)
799         self.amount_e.setText(amount)
800         if identity:
801             self.set_frozen(self.payto_e,True)
802             self.set_frozen(self.amount_e,True)
803             self.set_frozen(self.message_e,True)
804             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
805         else:
806             self.payto_sig.setVisible(False)
807
808     def do_clear(self):
809         self.payto_sig.setVisible(False)
810         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
811             e.setText('')
812             self.set_frozen(e,False)
813
814     def set_frozen(self,entry,frozen):
815         if frozen:
816             entry.setReadOnly(True)
817             entry.setFrame(False)
818             palette = QPalette()
819             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
820             entry.setPalette(palette)
821         else:
822             entry.setReadOnly(False)
823             entry.setFrame(True)
824             palette = QPalette()
825             palette.setColor(entry.backgroundRole(), QColor('white'))
826             entry.setPalette(palette)
827
828
829     def toggle_freeze(self,addr):
830         if not addr: return
831         if addr in self.wallet.frozen_addresses:
832             self.wallet.unfreeze(addr)
833         else:
834             self.wallet.freeze(addr)
835         self.update_receive_tab()
836
837     def toggle_priority(self,addr):
838         if not addr: return
839         if addr in self.wallet.prioritized_addresses:
840             self.wallet.unprioritize(addr)
841         else:
842             self.wallet.prioritize(addr)
843         self.update_receive_tab()
844
845
846     def create_list_tab(self, headers):
847         "generic tab creation method"
848         l = MyTreeWidget(self)
849         l.setColumnCount( len(headers) )
850         l.setHeaderLabels( headers )
851
852         w = QWidget()
853         vbox = QVBoxLayout()
854         w.setLayout(vbox)
855
856         vbox.setMargin(0)
857         vbox.setSpacing(0)
858         vbox.addWidget(l)
859         buttons = QWidget()
860         vbox.addWidget(buttons)
861
862         hbox = QHBoxLayout()
863         hbox.setMargin(0)
864         hbox.setSpacing(0)
865         buttons.setLayout(hbox)
866
867         return l,w,hbox
868
869
870     def create_receive_tab(self):
871         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _(''), _('Balance'), _('Tx')])
872         l.setContextMenuPolicy(Qt.CustomContextMenu)
873         l.customContextMenuRequested.connect(self.create_receive_menu)
874         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
875         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
876         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
877         self.receive_list = l
878         self.receive_buttons_hbox = hbox
879         hbox.addStretch(1)
880         return w
881
882
883     def receive_tab_set_mode(self, i):
884         self.save_column_widths()
885         self.expert_mode = (i == 1)
886         self.config.set_key('classic_expert_mode', self.expert_mode, True)
887         self.wallet.save()
888         self.update_receive_tab()
889
890
891     def save_column_widths(self):
892         if not self.expert_mode:
893             widths = [ self.receive_list.columnWidth(0) ]
894         else:
895             widths = []
896             for i in range(self.receive_list.columnCount() -1):
897                 widths.append(self.receive_list.columnWidth(i))
898         self.column_widths["receive"][self.expert_mode] = widths
899         
900         self.column_widths["history"] = []
901         for i in range(self.history_list.columnCount() - 1):
902             self.column_widths["history"].append(self.history_list.columnWidth(i))
903
904         self.column_widths["contacts"] = []
905         for i in range(self.contacts_list.columnCount() - 1):
906             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
907
908
909     def create_contacts_tab(self):
910         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
911         l.setContextMenuPolicy(Qt.CustomContextMenu)
912         l.customContextMenuRequested.connect(self.create_contact_menu)
913         for i,width in enumerate(self.column_widths['contacts']):
914             l.setColumnWidth(i, width)
915
916         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
917         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
918         self.contacts_list = l
919         self.contacts_buttons_hbox = hbox
920         hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
921         hbox.addStretch(1)
922         return w
923
924
925     def delete_imported_key(self, addr):
926         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
927             self.wallet.imported_keys.pop(addr)
928             self.update_receive_tab()
929             self.update_history_tab()
930             self.wallet.save()
931
932
933     def create_receive_menu(self, position):
934         # fixme: this function apparently has a side effect.
935         # if it is not called the menu pops up several times
936         #self.receive_list.selectedIndexes() 
937
938         item = self.receive_list.itemAt(position)
939         if not item: return
940         addr = unicode(item.text(0))
941         menu = QMenu()
942         menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
943         menu.addAction(_("QR code"), lambda: ElectrumWindow.show_qrcode("bitcoin:" + addr, _("Address")) )
944         menu.addAction(_("Edit label"), lambda: self.edit_label(True))
945         menu.addAction(_("Private key"), lambda: self.view_private_key(addr))
946         menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
947         if addr in self.wallet.imported_keys:
948             menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
949
950         if self.expert_mode:
951             t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
952             menu.addAction(t, lambda: self.toggle_freeze(addr))
953             t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
954             menu.addAction(t, lambda: self.toggle_priority(addr))
955             
956         self.wallet.run_hook('receive_menu', (self, menu,))
957         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
958
959
960     def payto(self, x, is_alias):
961         if not x: return
962         if is_alias:
963             label = x
964             m_addr = label
965         else:
966             addr = x
967             label = self.wallet.labels.get(addr)
968             m_addr = label + '  <' + addr + '>' if label else addr
969         self.tabs.setCurrentIndex(1)
970         self.payto_e.setText(m_addr)
971         self.amount_e.setFocus()
972
973     def delete_contact(self, x, is_alias):
974         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
975             if not is_alias and x in self.wallet.addressbook:
976                 self.wallet.addressbook.remove(x)
977                 if x in self.wallet.labels.keys():
978                     self.wallet.labels.pop(x)
979             elif is_alias and x in self.wallet.aliases:
980                 self.wallet.aliases.pop(x)
981             self.update_history_tab()
982             self.update_contacts_tab()
983             self.update_completions()
984
985     def create_contact_menu(self, position):
986         # fixme: this function apparently has a side effect.
987         # if it is not called the menu pops up several times
988         #self.contacts_list.selectedIndexes() 
989
990         item = self.contacts_list.itemAt(position)
991         if not item: return
992         addr = unicode(item.text(0))
993         label = unicode(item.text(1))
994         is_alias = label in self.wallet.aliases.keys()
995         x = label if is_alias else addr
996         menu = QMenu()
997         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
998         menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
999         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1000         if not is_alias:
1001             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1002         else:
1003             menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
1004         menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
1005         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1006
1007
1008     def update_receive_item(self, item):
1009         address = str(item.data(0,0).toString())
1010         label = self.wallet.labels.get(address,'')
1011         item.setData(1,0,label)
1012
1013         self.wallet.run_hook('update_receive_item', (self, address, item))
1014                 
1015         c, u = self.wallet.get_addr_balance(address)
1016         balance = format_satoshis( c + u, False, self.wallet.num_zeros )
1017         item.setData(3,0,balance)
1018
1019         if self.expert_mode:
1020             if address in self.wallet.frozen_addresses: 
1021                 item.setBackgroundColor(0, QColor('lightblue'))
1022             elif address in self.wallet.prioritized_addresses: 
1023                 item.setBackgroundColor(0, QColor('lightgreen'))
1024         
1025
1026     def update_receive_tab(self):
1027         l = self.receive_list
1028         
1029         l.clear()
1030         l.setColumnHidden(3, not self.expert_mode)
1031         l.setColumnHidden(4, not self.expert_mode)
1032         if not self.expert_mode:
1033             width = self.column_widths['receive'][0][0]
1034             l.setColumnWidth(0, width)
1035         else:
1036             for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1037                 l.setColumnWidth(i, width)        
1038
1039
1040         for k, account in self.wallet.accounts.items():
1041             name = account.get('name',str(k))
1042             c,u = self.wallet.get_account_balance(k)
1043             account_item = QTreeWidgetItem( [ name, '', '', format_satoshis(c+u), ''] )
1044             l.addTopLevelItem(account_item)
1045             account_item.setExpanded(True)
1046             
1047
1048             for is_change in [0,1]:
1049                 name = "Receiving" if not is_change else "Change"
1050                 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1051                 account_item.addChild(seq_item)
1052                 if not is_change: seq_item.setExpanded(True)
1053                 is_red = False
1054                 gap = 0
1055
1056                 for address in account[is_change]:
1057                     h = self.wallet.history.get(address,[])
1058             
1059                     if not is_change:
1060                         if h == []:
1061                             gap += 1
1062                             if gap > self.wallet.gap_limit:
1063                                 is_red = True
1064                         else:
1065                             gap = 0
1066
1067                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1068                     item = QTreeWidgetItem( [ address, '', '', '', num_tx] )
1069                     item.setFont(0, QFont(MONOSPACE_FONT))
1070                     item.setFont(2, QFont(MONOSPACE_FONT))
1071                     self.update_receive_item(item)
1072                     if is_red:
1073                         item.setBackgroundColor(1, QColor('red'))
1074                     seq_item.addChild(item)
1075
1076         if self.wallet.imported_keys:
1077             c,u = self.wallet.get_imported_balance()
1078             account_item = QTreeWidgetItem( [ _('Imported'), '', '', format_satoshis(c+u), ''] )
1079             l.addTopLevelItem(account_item)
1080             account_item.setExpanded(True)
1081             for address in self.wallet.imported_keys.keys():
1082                 item = QTreeWidgetItem( [ address, '', '', '', ''] )
1083                 item.setFont(0, QFont(MONOSPACE_FONT))
1084                 item.setFont(2, QFont(MONOSPACE_FONT))
1085                 self.update_receive_item(item)
1086                 account_item.addChild(item)
1087                 
1088
1089         # we use column 1 because column 0 may be hidden
1090         l.setCurrentItem(l.topLevelItem(0),1)
1091
1092     def show_contact_details(self, m):
1093         a = self.wallet.aliases.get(m)
1094         if a:
1095             if a[0] in self.wallet.authorities.keys():
1096                 s = self.wallet.authorities.get(a[0])
1097             else:
1098                 s = "self-signed"
1099             msg = _('Alias:')+' '+ m + '\n'+_('Target address:')+' '+ a[1] + '\n\n'+_('Signed by:')+' ' + s + '\n'+_('Signing address:')+' ' + a[0]
1100             QMessageBox.information(self, 'Alias', msg, 'OK')
1101
1102     def update_contacts_tab(self):
1103
1104         l = self.contacts_list
1105         l.clear()
1106
1107         alias_targets = []
1108         for alias, v in self.wallet.aliases.items():
1109             s, target = v
1110             alias_targets.append(target)
1111             item = QTreeWidgetItem( [ target, alias, '-'] )
1112             item.setBackgroundColor(0, QColor('lightgray'))
1113             l.addTopLevelItem(item)
1114             
1115         for address in self.wallet.addressbook:
1116             if address in alias_targets: continue
1117             label = self.wallet.labels.get(address,'')
1118             n = 0 
1119             for tx in self.wallet.transactions.values():
1120                 if address in map(lambda x: x[0], tx.outputs): n += 1
1121             
1122             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1123             item.setFont(0, QFont(MONOSPACE_FONT))
1124             l.addTopLevelItem(item)
1125
1126         l.setCurrentItem(l.topLevelItem(0))
1127
1128
1129     def create_console_tab(self):
1130         from qt_console import Console
1131         from electrum import util, bitcoin, commands
1132         self.console = console = Console()
1133         self.console.history = self.config.get("console-history",[])
1134         self.console.history_index = len(self.console.history)
1135
1136         console.updateNamespace({'wallet' : self.wallet, 'interface' : self.wallet.interface, 'gui':self})
1137         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1138
1139         c = commands.Commands(self.wallet, self.wallet.interface, lambda: self.console.set_json(True))
1140         methods = {}
1141         def mkfunc(f, method):
1142             return lambda *args: apply( f, (method, args, self.password_dialog ))
1143         for m in dir(c):
1144             if m[0]=='_' or m=='wallet' or m == 'interface': continue
1145             methods[m] = mkfunc(c._run, m)
1146             
1147         console.updateNamespace(methods)
1148         return console
1149
1150
1151     def create_status_bar(self):
1152         self.status_text = ""
1153         sb = QStatusBar()
1154         sb.setFixedHeight(35)
1155         qtVersion = qVersion()
1156
1157         update_notification = UpdateLabel(self.config)
1158         if(update_notification.new_version):
1159             sb.addPermanentWidget(update_notification)
1160
1161         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1162             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1163         if self.wallet.seed:
1164             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), _("Password"), lambda: self.change_password_dialog(self.wallet, self) ) )
1165         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1166         if self.wallet.seed:
1167             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), lambda: self.show_seed_dialog(self.wallet, self) ) )
1168         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), lambda: self.network_dialog(self.wallet, self) ) 
1169         sb.addPermanentWidget( self.status_button )
1170
1171         self.setStatusBar(sb)
1172         
1173     def go_lite(self):
1174         import gui_lite
1175         self.config.set_key('gui', 'lite', True)
1176         self.hide()
1177         if self.lite:
1178             self.lite.mini.show()
1179         else:
1180             self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
1181             self.lite.main(None)
1182
1183     def new_contact_dialog(self):
1184         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1185         address = unicode(text)
1186         if ok:
1187             if is_valid(address):
1188                 self.wallet.addressbook.append(address)
1189                 self.wallet.save()
1190                 self.update_contacts_tab()
1191                 self.update_history_tab()
1192                 self.update_completions()
1193             else:
1194                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1195
1196     def show_master_public_key(self):
1197         dialog = QDialog(None)
1198         dialog.setModal(1)
1199         dialog.setWindowTitle(_("Master Public Key"))
1200
1201         main_text = QTextEdit()
1202         main_text.setText(self.wallet.get_master_public_key())
1203         main_text.setReadOnly(True)
1204         main_text.setMaximumHeight(170)
1205         qrw = QRCodeWidget(self.wallet.get_master_public_key(), 6)
1206
1207         ok_button = QPushButton(_("OK"))
1208         ok_button.setDefault(True)
1209         ok_button.clicked.connect(dialog.accept)
1210
1211         main_layout = QGridLayout()
1212         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1213
1214         main_layout.addWidget(main_text, 1, 0)
1215         main_layout.addWidget(qrw, 1, 1 )
1216
1217         vbox = QVBoxLayout()
1218         vbox.addLayout(main_layout)
1219         hbox = QHBoxLayout()
1220         hbox.addStretch(1)
1221         hbox.addWidget(ok_button)
1222         vbox.addLayout(hbox)
1223
1224         dialog.setLayout(vbox)
1225         dialog.exec_()
1226         
1227
1228     @classmethod
1229     def show_seed_dialog(self, wallet, parent=None):
1230         if not wallet.seed:
1231             QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1232             return
1233
1234         if wallet.use_encryption:
1235             password = parent.password_dialog()
1236             if not password:
1237                 return
1238         else:
1239             password = None
1240             
1241         try:
1242             seed = wallet.decode_seed(password)
1243         except:
1244             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1245             return
1246
1247         self.show_seed(seed)
1248
1249     @classmethod
1250     def show_seed(self, seed):
1251         dialog = QDialog(None)
1252         dialog.setModal(1)
1253         dialog.setWindowTitle('Electrum' + ' - ' + _('Seed'))
1254
1255         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1256
1257         label1 = QLabel(_("Your wallet generation seed is")+ ":")
1258
1259         seed_text = QTextEdit(brainwallet)
1260         seed_text.setReadOnly(True)
1261         seed_text.setMaximumHeight(130)
1262         
1263         msg2 =  _("Please write down or memorize these 12 words (order is important).") + " " \
1264               + _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
1265               + _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
1266               + "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
1267         label2 = QLabel(msg2)
1268         label2.setWordWrap(True)
1269
1270         logo = QLabel()
1271         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1272         logo.setMaximumWidth(60)
1273
1274         qrw = QRCodeWidget(seed, 4)
1275
1276         ok_button = QPushButton(_("OK"))
1277         ok_button.setDefault(True)
1278         ok_button.clicked.connect(dialog.accept)
1279
1280         grid = QGridLayout()
1281         #main_layout.addWidget(logo, 0, 0)
1282
1283         grid.addWidget(logo, 0, 0)
1284         grid.addWidget(label1, 0, 1)
1285
1286         grid.addWidget(seed_text, 1, 0, 1, 2)
1287
1288         grid.addWidget(qrw, 0, 2, 2, 1)
1289
1290         vbox = QVBoxLayout()
1291         vbox.addLayout(grid)
1292         vbox.addWidget(label2)
1293
1294         hbox = QHBoxLayout()
1295         hbox.addStretch(1)
1296         hbox.addWidget(ok_button)
1297         vbox.addLayout(hbox)
1298
1299         dialog.setLayout(vbox)
1300         dialog.exec_()
1301
1302     @staticmethod
1303     def show_qrcode(data, title = "QR code"):
1304         if not data: return
1305         d = QDialog(None)
1306         d.setModal(1)
1307         d.setWindowTitle(title)
1308         d.setMinimumSize(270, 300)
1309         vbox = QVBoxLayout()
1310         qrw = QRCodeWidget(data)
1311         vbox.addWidget(qrw, 1)
1312         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1313         hbox = QHBoxLayout()
1314         hbox.addStretch(1)
1315
1316         def print_qr(self):
1317             filename = "qrcode.bmp"
1318             bmp.save_qrcode(qrw.qr, filename)
1319             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1320
1321         b = QPushButton(_("Save"))
1322         hbox.addWidget(b)
1323         b.clicked.connect(print_qr)
1324
1325         b = QPushButton(_("Close"))
1326         hbox.addWidget(b)
1327         b.clicked.connect(d.accept)
1328         b.setDefault(True)
1329
1330         vbox.addLayout(hbox)
1331         d.setLayout(vbox)
1332         d.exec_()
1333
1334     def view_private_key(self,address):
1335         if not address: return
1336         if self.wallet.use_encryption:
1337             password = self.password_dialog()
1338             if not password:
1339                 return
1340         else:
1341             password = None
1342
1343         try:
1344             pk = self.wallet.get_private_key(address, password)
1345         except BaseException, e:
1346             self.show_message(str(e))
1347             return
1348
1349         QMessageBox.information(self, _('Private key'), 'Address'+ ': ' + address + '\n\n' + _('Private key') + ': ' + pk, _('OK'))
1350
1351
1352     def sign_message(self,address):
1353         if not address: return
1354         d = QDialog(self)
1355         d.setModal(1)
1356         d.setWindowTitle(_('Sign Message'))
1357         d.setMinimumSize(410, 290)
1358
1359         tab_widget = QTabWidget()
1360         tab = QWidget()
1361         layout = QGridLayout(tab)
1362
1363         sign_address = QLineEdit()
1364
1365         sign_address.setText(address)
1366         layout.addWidget(QLabel(_('Address')), 1, 0)
1367         layout.addWidget(sign_address, 1, 1)
1368
1369         sign_message = QTextEdit()
1370         layout.addWidget(QLabel(_('Message')), 2, 0)
1371         layout.addWidget(sign_message, 2, 1)
1372         layout.setRowStretch(2,3)
1373
1374         sign_signature = QTextEdit()
1375         layout.addWidget(QLabel(_('Signature')), 3, 0)
1376         layout.addWidget(sign_signature, 3, 1)
1377         layout.setRowStretch(3,1)
1378
1379         def do_sign():
1380             if self.wallet.use_encryption:
1381                 password = self.password_dialog()
1382                 if not password:
1383                     return
1384             else:
1385                 password = None
1386
1387             try:
1388                 signature = self.wallet.sign_message(str(sign_address.text()), str(sign_message.toPlainText()), password)
1389                 sign_signature.setText(signature)
1390             except BaseException, e:
1391                 self.show_message(str(e))
1392                 return
1393
1394         hbox = QHBoxLayout()
1395         b = QPushButton(_("Sign"))
1396         hbox.addWidget(b)
1397         b.clicked.connect(do_sign)
1398         b = QPushButton(_("Close"))
1399         b.clicked.connect(d.accept)
1400         hbox.addWidget(b)
1401         layout.addLayout(hbox, 4, 1)
1402         tab_widget.addTab(tab, _("Sign"))
1403
1404
1405         tab = QWidget()
1406         layout = QGridLayout(tab)
1407
1408         verify_address = QLineEdit()
1409         layout.addWidget(QLabel(_('Address')), 1, 0)
1410         layout.addWidget(verify_address, 1, 1)
1411
1412         verify_message = QTextEdit()
1413         layout.addWidget(QLabel(_('Message')), 2, 0)
1414         layout.addWidget(verify_message, 2, 1)
1415         layout.setRowStretch(2,3)
1416
1417         verify_signature = QTextEdit()
1418         layout.addWidget(QLabel(_('Signature')), 3, 0)
1419         layout.addWidget(verify_signature, 3, 1)
1420         layout.setRowStretch(3,1)
1421
1422         def do_verify():
1423             try:
1424                 self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText()))
1425                 self.show_message(_("Signature verified"))
1426             except BaseException, e:
1427                 self.show_message(str(e))
1428                 return
1429
1430         hbox = QHBoxLayout()
1431         b = QPushButton(_("Verify"))
1432         b.clicked.connect(do_verify)
1433         hbox.addWidget(b)
1434         b = QPushButton(_("Close"))
1435         b.clicked.connect(d.accept)
1436         hbox.addWidget(b)
1437         layout.addLayout(hbox, 4, 1)
1438         tab_widget.addTab(tab, _("Verify"))
1439
1440         vbox = QVBoxLayout()
1441         vbox.addWidget(tab_widget)
1442         d.setLayout(vbox)
1443         d.exec_()
1444
1445         
1446
1447
1448     def question(self, msg):
1449         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1450
1451     def show_message(self, msg):
1452         QMessageBox.information(self, _('Message'), msg, _('OK'))
1453
1454     def password_dialog(self ):
1455         d = QDialog(self)
1456         d.setModal(1)
1457
1458         pw = QLineEdit()
1459         pw.setEchoMode(2)
1460
1461         vbox = QVBoxLayout()
1462         msg = _('Please enter your password')
1463         vbox.addWidget(QLabel(msg))
1464
1465         grid = QGridLayout()
1466         grid.setSpacing(8)
1467         grid.addWidget(QLabel(_('Password')), 1, 0)
1468         grid.addWidget(pw, 1, 1)
1469         vbox.addLayout(grid)
1470
1471         vbox.addLayout(ok_cancel_buttons(d))
1472         d.setLayout(vbox) 
1473
1474         if not d.exec_(): return
1475         return unicode(pw.text())
1476
1477
1478
1479
1480
1481     @staticmethod
1482     def change_password_dialog( wallet, parent=None ):
1483
1484         if not wallet.seed:
1485             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1486             return
1487
1488         d = QDialog(parent)
1489         d.setModal(1)
1490
1491         pw = QLineEdit()
1492         pw.setEchoMode(2)
1493         new_pw = QLineEdit()
1494         new_pw.setEchoMode(2)
1495         conf_pw = QLineEdit()
1496         conf_pw.setEchoMode(2)
1497
1498         vbox = QVBoxLayout()
1499         if parent:
1500             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1501                    +_('To disable wallet encryption, enter an empty new password.')) \
1502                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1503         else:
1504             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1505                   +_("Leave these fields empty if you want to disable encryption.")
1506         vbox.addWidget(QLabel(msg))
1507
1508         grid = QGridLayout()
1509         grid.setSpacing(8)
1510
1511         if wallet.use_encryption:
1512             grid.addWidget(QLabel(_('Password')), 1, 0)
1513             grid.addWidget(pw, 1, 1)
1514
1515         grid.addWidget(QLabel(_('New Password')), 2, 0)
1516         grid.addWidget(new_pw, 2, 1)
1517
1518         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1519         grid.addWidget(conf_pw, 3, 1)
1520         vbox.addLayout(grid)
1521
1522         vbox.addLayout(ok_cancel_buttons(d))
1523         d.setLayout(vbox) 
1524
1525         if not d.exec_(): return
1526
1527         password = unicode(pw.text()) if wallet.use_encryption else None
1528         new_password = unicode(new_pw.text())
1529         new_password2 = unicode(conf_pw.text())
1530
1531         try:
1532             seed = wallet.decode_seed(password)
1533         except:
1534             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1535             return
1536
1537         if new_password != new_password2:
1538             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1539             return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1540
1541         wallet.update_password(seed, password, new_password)
1542
1543     @staticmethod
1544     def seed_dialog(wallet, parent=None):
1545         d = QDialog(parent)
1546         d.setModal(1)
1547
1548         vbox = QVBoxLayout()
1549         msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1550         vbox.addWidget(QLabel(msg))
1551
1552         grid = QGridLayout()
1553         grid.setSpacing(8)
1554
1555         seed_e = QLineEdit()
1556         grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1557         grid.addWidget(seed_e, 1, 1)
1558
1559         gap_e = QLineEdit()
1560         gap_e.setText("5")
1561         grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1562         grid.addWidget(gap_e, 2, 1)
1563         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1564         vbox.addLayout(grid)
1565
1566         vbox.addLayout(ok_cancel_buttons(d))
1567         d.setLayout(vbox) 
1568
1569         if not d.exec_(): return
1570
1571         try:
1572             gap = int(unicode(gap_e.text()))
1573         except:
1574             QMessageBox.warning(None, _('Error'), 'error', 'OK')
1575             return
1576
1577         try:
1578             seed = str(seed_e.text())
1579             seed.decode('hex')
1580         except:
1581             print_error("Warning: Not hex, trying decode")
1582             try:
1583                 seed = mnemonic.mn_decode( seed.split(' ') )
1584             except:
1585                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1586                 return
1587
1588         if not seed:
1589             QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
1590             return
1591
1592         return seed, gap
1593
1594     def generate_transaction_information_widget(self, tx):
1595         tabs = QTabWidget(self)
1596
1597         tab1 = QWidget()
1598         grid_ui = QGridLayout(tab1)
1599         grid_ui.setColumnStretch(0,1)
1600         tabs.addTab(tab1, _('Outputs') )
1601
1602         tree_widget = MyTreeWidget(self)
1603         tree_widget.setColumnCount(2)
1604         tree_widget.setHeaderLabels( [_('Address'), _('Amount')] )
1605         tree_widget.setColumnWidth(0, 300)
1606         tree_widget.setColumnWidth(1, 50)
1607
1608         for output in tx.d["outputs"]:
1609             item = QTreeWidgetItem( ["%s" %(output["address"]), "%s" % ( format_satoshis(output["value"]))] )
1610             tree_widget.addTopLevelItem(item)
1611
1612         tree_widget.setMaximumHeight(100)
1613
1614         grid_ui.addWidget(tree_widget)
1615
1616         tab2 = QWidget()
1617         grid_ui = QGridLayout(tab2)
1618         grid_ui.setColumnStretch(0,1)
1619         tabs.addTab(tab2, _('Inputs') )
1620         
1621         tree_widget = MyTreeWidget(self)
1622         tree_widget.setColumnCount(2)
1623         tree_widget.setHeaderLabels( [ _('Address'), _('Previous output')] )
1624
1625         for input_line in tx.inputs:
1626             item = QTreeWidgetItem( [ str(input_line["address"]), str(input_line["prevout_hash"])] )
1627             tree_widget.addTopLevelItem(item)
1628
1629         tree_widget.setMaximumHeight(100)
1630
1631         grid_ui.addWidget(tree_widget)
1632         return tabs
1633
1634
1635     def tx_dict_from_text(self, txt):
1636         try:
1637             tx_dict = json.loads(str(txt))
1638             assert "hex" in tx_dict.keys()
1639             assert "complete" in tx_dict.keys()
1640             if not tx_dict["complete"]:
1641                 assert "input_info" in tx_dict.keys()
1642         except:
1643             QMessageBox.critical(None, "Unable to parse transaction", _("Electrum was unable to parse your transaction:"))
1644             return None
1645         return tx_dict
1646
1647
1648     def read_tx_from_file(self):
1649         fileName = QFileDialog.getOpenFileName(QWidget(), _("Select your transaction file"), os.path.expanduser('~'))
1650         if not fileName:
1651             return
1652         try:
1653             with open(fileName, "r") as f:
1654                 file_content = f.read()
1655         except (ValueError, IOError, os.error), reason:
1656             QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1657
1658         return self.tx_dict_from_text(file_content)
1659
1660
1661     def sign_raw_transaction(self, tx, input_info):
1662         if self.wallet.use_encryption:
1663             password = self.password_dialog()
1664             if not password:
1665                 return
1666         else:
1667             password = None
1668
1669         try:
1670             self.wallet.signrawtransaction(tx, input_info, [], password)
1671             
1672             fileName = QFileDialog.getSaveFileName(QWidget(), _("Select where to save your signed transaction"), os.path.expanduser('~/signed_tx_%s' % (tx.hash()[0:8])))
1673             if fileName:
1674                 with open(fileName, "w+") as f:
1675                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
1676                 self.show_message(_("Transaction saved succesfully"))
1677         except BaseException, e:
1678             self.show_message(str(e))
1679
1680
1681     def create_sign_transaction_window(self, tx_dict):
1682         tx = Transaction(tx_dict["hex"])
1683
1684         dialog = QDialog(self)
1685         dialog.setMinimumWidth(500)
1686         dialog.setWindowTitle(_('Sign unsigned transaction'))
1687         dialog.setModal(1)
1688
1689         vbox = QVBoxLayout()
1690         dialog.setLayout(vbox)
1691         vbox.addWidget( self.generate_transaction_information_widget(tx) )
1692
1693         if tx_dict["complete"] == True:
1694             vbox.addWidget(QLabel(_("This transaction is already signed.")))
1695         else:
1696             vbox.addWidget(QLabel(_("Create a signed transaction.")))
1697             vbox.addLayout(ok_cancel_buttons(dialog))
1698             input_info = json.loads(tx_dict["input_info"])
1699
1700         if dialog.exec_():
1701             self.sign_raw_transaction(tx, input_info)
1702
1703
1704
1705     def do_sign_from_text(self):
1706         txt, ok = QInputDialog.getText(QTextEdit(), _('Sign raw transaction'), _('Transaction data in JSON') + ':')
1707         if not ok:
1708             return
1709         tx_dict = self.tx_dict_from_text(unicode(txt))
1710         if tx_dict:
1711             self.create_sign_transaction_window(tx_dict)
1712
1713
1714     def do_sign_from_file(self):
1715         tx_dict = self.read_tx_from_file()
1716         if tx_dict:
1717             self.create_sign_transaction_window(tx_dict)
1718     
1719
1720     def send_raw_transaction(self, raw_tx):
1721         result, result_message = self.wallet.sendtx( raw_tx )
1722         if result:
1723             self.show_message("Transaction succesfully sent: %s" % (result_message))
1724         else:
1725             self.show_message("There was a problem sending your transaction:\n %s" % (result_message))
1726
1727
1728     def create_send_transaction_window(self, tx_dict):
1729         tx = Transaction(tx_dict["hex"])
1730
1731         dialog = QDialog(self)
1732         dialog.setMinimumWidth(500)
1733         dialog.setWindowTitle(_('Send raw transaction'))
1734         dialog.setModal(1)
1735
1736         vbox = QVBoxLayout()
1737         dialog.setLayout(vbox)
1738         vbox.addWidget( self.generate_transaction_information_widget(tx))
1739
1740         if tx_dict["complete"] == False:
1741             vbox.addWidget(QLabel(_("This transaction is not signed yet.")))
1742         else:
1743             vbox.addWidget(QLabel(_("Broadcast this transaction")))
1744             vbox.addLayout(ok_cancel_buttons(dialog))
1745
1746         if dialog.exec_():
1747             self.send_raw_transaction(tx_dict["hex"])
1748
1749
1750     def do_send_from_file(self):
1751         tx_dict = self.read_tx_from_file()
1752         if tx_dict: 
1753             self.create_send_transaction_window(tx_dict)
1754         
1755
1756     def do_send_from_text(self):
1757         txt, ok = QInputDialog.getText(QTextEdit(), _('Send raw transaction'), _('Transaction data in JSON') + ':')
1758         if not ok:
1759             return
1760         tx_dict = self.tx_dict_from_text(unicode(txt))
1761         if tx_dict:
1762             self.create_send_transaction_window(tx_dict)
1763
1764
1765     def do_export_privkeys(self):
1766         self.show_message("%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),  _("Exposing a single private key can compromise your entire wallet!"), _("In particular, DO NOT use 'redeem private key' services proposed by third parties.")))
1767
1768         if self.wallet.use_encryption:
1769             password = self.password_dialog()
1770             if not password:
1771                 return
1772         else:
1773             password = None
1774         try:
1775             select_export = _('Select file to export your private keys to')
1776             fileName = QFileDialog.getSaveFileName(QWidget(), select_export, os.path.expanduser('~/electrum-private-keys.csv'), "*.csv")
1777             if fileName:
1778                 with open(fileName, "w+") as csvfile:
1779                     transaction = csv.writer(csvfile)
1780                     transaction.writerow(["address", "private_key"])
1781
1782                     
1783                     for addr, pk in self.wallet.get_private_keys(self.wallet.addresses(True), password).items():
1784                         transaction.writerow(["%34s"%addr,pk])
1785
1786                     self.show_message(_("Private keys exported."))
1787
1788         except (IOError, os.error), reason:
1789             export_error_label = _("Electrum was unable to produce a private key-export.")
1790             QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
1791
1792         except BaseException, e:
1793           self.show_message(str(e))
1794           return
1795
1796
1797     def do_import_labels(self):
1798         labelsFile = QFileDialog.getOpenFileName(QWidget(), _("Open text file"), util.user_dir(), self.tr("Text Files (labels.dat)"))
1799         if not labelsFile: return
1800         try:
1801             f = open(labelsFile, 'r')
1802             data = f.read()
1803             f.close()
1804             for key, value in json.loads(data).items():
1805                 self.wallet.labels[key] = value
1806             self.wallet.save()
1807             QMessageBox.information(None, _("Labels imported"), _("Your labels where imported from")+" '%s'" % str(labelsFile))
1808         except (IOError, os.error), reason:
1809             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1810         
1811
1812
1813     def do_export_labels(self):
1814         labels = self.wallet.labels
1815         try:
1816             labelsFile = util.user_dir() + '/labels.dat'
1817             f = open(labelsFile, 'w+')
1818             json.dump(labels, f)
1819             f.close()
1820             QMessageBox.information(None, "Labels exported", _("Your labels where exported to")+" '%s'" % str(labelsFile))
1821         except (IOError, os.error), reason:
1822             QMessageBox.critical(None, "Unable to export labels", _("Electrum was unable to export your labels.")+"\n" + str(reason))
1823
1824     def do_export_history(self):
1825         from gui_lite import csv_transaction
1826         csv_transaction(self.wallet)
1827
1828     def do_import_privkey(self):
1829         if not self.wallet.imported_keys:
1830             r = QMessageBox.question(None, _('Warning'), _('Warning: Imported keys are not recoverable from seed.') + ' ' \
1831                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '\n\n' \
1832                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1833             if r == 4: return
1834
1835         text, ok = QInputDialog.getText(self, _('Import private key'), _('Private Key') + ':')
1836         if not ok: return
1837         sec = str(text).strip()
1838         if self.wallet.use_encryption:
1839             password = self.password_dialog()
1840             if not password:
1841                 return
1842         else:
1843             password = None
1844         try:
1845             addr = self.wallet.import_key(sec, password)
1846             if not addr:
1847                 QMessageBox.critical(None, _("Unable to import key"), "error")
1848             else:
1849                 QMessageBox.information(None, _("Key imported"), addr)
1850                 self.update_receive_tab()
1851                 self.update_history_tab()
1852         except BaseException as e:
1853             QMessageBox.critical(None, _("Unable to import key"), str(e))
1854
1855     def settings_dialog(self):
1856         d = QDialog(self)
1857         d.setWindowTitle(_('Electrum Settings'))
1858         d.setModal(1)
1859         vbox = QVBoxLayout()
1860
1861         tabs = QTabWidget(self)
1862         vbox.addWidget(tabs)
1863
1864         tab1 = QWidget()
1865         grid_ui = QGridLayout(tab1)
1866         grid_ui.setColumnStretch(0,1)
1867         tabs.addTab(tab1, _('Display') )
1868
1869         nz_label = QLabel(_('Display zeros'))
1870         grid_ui.addWidget(nz_label, 3, 0)
1871         nz_e = QLineEdit()
1872         nz_e.setText("%d"% self.wallet.num_zeros)
1873         grid_ui.addWidget(nz_e, 3, 1)
1874         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1875         grid_ui.addWidget(HelpButton(msg), 3, 2)
1876         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1877         if not self.config.is_modifiable('num_zeros'):
1878             for w in [nz_e, nz_label]: w.setEnabled(False)
1879         
1880         lang_label=QLabel(_('Language') + ':')
1881         grid_ui.addWidget(lang_label , 8, 0)
1882         lang_combo = QComboBox()
1883         from i18n import languages
1884         lang_combo.addItems(languages.values())
1885         try:
1886             index = languages.keys().index(self.config.get("language",''))
1887         except:
1888             index = 0
1889         lang_combo.setCurrentIndex(index)
1890         grid_ui.addWidget(lang_combo, 8, 1)
1891         grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 8, 2)
1892         if not self.config.is_modifiable('language'):
1893             for w in [lang_combo, lang_label]: w.setEnabled(False)
1894
1895         currencies = self.exchanger.get_currencies()
1896         currencies.insert(0, "None")
1897
1898         cur_label=QLabel(_('Currency') + ':')
1899         grid_ui.addWidget(cur_label , 9, 0)
1900         cur_combo = QComboBox()
1901         cur_combo.addItems(currencies)
1902         try:
1903             index = currencies.index(self.config.get('currency', "None"))
1904         except:
1905             index = 0
1906         cur_combo.setCurrentIndex(index)
1907         grid_ui.addWidget(cur_combo, 9, 1)
1908         grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 9, 2)
1909         
1910         view_label=QLabel(_('Receive Tab') + ':')
1911         grid_ui.addWidget(view_label , 10, 0)
1912         view_combo = QComboBox()
1913         view_combo.addItems([_('Simple'), _('Advanced')])
1914         view_combo.setCurrentIndex(self.expert_mode)
1915         grid_ui.addWidget(view_combo, 10, 1)
1916         hh = _('This selects the interaction mode of the "Receive" tab.')+' ' + '\n\n' \
1917              + _('Simple') +   ': ' + _('Show only addresses and labels.') + '\n\n' \
1918              + _('Advanced') + ': ' + _('Show address balances and add extra menu items to freeze/prioritize addresses.') + '\n\n' 
1919         
1920         grid_ui.addWidget(HelpButton(hh), 10, 2)
1921
1922         # wallet tab
1923         tab2 = QWidget()
1924         grid_wallet = QGridLayout(tab2)
1925         grid_wallet.setColumnStretch(0,1)
1926         tabs.addTab(tab2, _('Wallet') )
1927         
1928         fee_label = QLabel(_('Transaction fee'))
1929         grid_wallet.addWidget(fee_label, 0, 0)
1930         fee_e = QLineEdit()
1931         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1932         grid_wallet.addWidget(fee_e, 0, 1)
1933         msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1934             + _('Recommended value') + ': 0.001'
1935         grid_wallet.addWidget(HelpButton(msg), 0, 2)
1936         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1937         if not self.config.is_modifiable('fee'):
1938             for w in [fee_e, fee_label]: w.setEnabled(False)
1939
1940         usechange_label = QLabel(_('Use change addresses'))
1941         grid_wallet.addWidget(usechange_label, 1, 0)
1942         usechange_combo = QComboBox()
1943         usechange_combo.addItems([_('Yes'), _('No')])
1944         usechange_combo.setCurrentIndex(not self.wallet.use_change)
1945         grid_wallet.addWidget(usechange_combo, 1, 1)
1946         grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 2)
1947         if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
1948
1949         gap_label = QLabel(_('Gap limit'))
1950         grid_wallet.addWidget(gap_label, 2, 0)
1951         gap_e = QLineEdit()
1952         gap_e.setText("%d"% self.wallet.gap_limit)
1953         grid_wallet.addWidget(gap_e, 2, 1)
1954         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1955               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1956               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1957               + _('Given the current status of your address sequence, the minimum gap limit you can use is:')+' ' + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1958               + _('Warning') + ': ' \
1959               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1960               + _('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' 
1961         grid_wallet.addWidget(HelpButton(msg), 2, 2)
1962         gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1963         if not self.config.is_modifiable('gap_limit'):
1964             for w in [gap_e, gap_label]: w.setEnabled(False)
1965
1966         grid_wallet.setRowStretch(3,1)
1967
1968
1969         # import/export tab
1970         tab3 = QWidget()
1971         grid_io = QGridLayout(tab3)
1972         grid_io.setColumnStretch(0,1)
1973         tabs.addTab(tab3, _('Import/Export') )
1974         
1975         grid_io.addWidget(QLabel(_('Labels')), 1, 0)
1976         grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
1977         grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
1978         grid_io.addWidget(HelpButton(_('Export your labels as json')), 1, 3)
1979
1980         grid_io.addWidget(QLabel(_('History')), 2, 0)
1981         grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
1982         grid_io.addWidget(HelpButton(_('Export your transaction history as csv')), 2, 3)
1983
1984         grid_io.addWidget(QLabel(_('Private keys')), 3, 0)
1985
1986         grid_io.addWidget(EnterButton(_("Export"), self.do_export_privkeys), 3, 1)
1987         grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
1988         grid_io.addWidget(HelpButton(_('Import private key')), 3, 3)
1989
1990         grid_io.addWidget(QLabel(_('Master Public Key')), 4, 0)
1991         grid_io.addWidget(EnterButton(_("Show"), self.show_master_public_key), 4, 1)
1992         grid_io.addWidget(HelpButton(_('Your Master Public Key can be used to create receiving addresses, but not to sign transactions.') + ' ' \
1993                               + _('If you give it to someone, they will be able to see your transactions, but not to spend your money.') + ' ' \
1994                               + _('If you restore your wallet from it, a watching-only (deseeded) wallet will be created.')), 4, 3)
1995
1996         grid_io.setRowStretch(4,1)
1997
1998         tab4 = QWidget()
1999         grid_raw = QGridLayout(tab4)
2000         grid_raw.setColumnStretch(0,1)
2001         tabs.addTab(tab4, _('Raw tx') )  # move this to wallet tab
2002
2003         if self.wallet.seed:
2004             grid_raw.addWidget(QLabel(_("Sign transaction")), 1, 0)
2005             grid_raw.addWidget(EnterButton(_("From file"), self.do_sign_from_file),1,1)
2006             grid_raw.addWidget(EnterButton(_("From text"), self.do_sign_from_text),1,2)
2007             grid_raw.addWidget(HelpButton(_("Sign an unsigned transaction generated by a watching-only wallet")),1,3)
2008
2009         grid_raw.addWidget(QLabel(_("Send signed transaction")), 2, 0)
2010         grid_raw.addWidget(EnterButton(_("From file"), self.do_send_from_file),2,1)
2011         grid_raw.addWidget(EnterButton(_("From text"), self.do_send_from_text),2,2)
2012         grid_raw.addWidget(HelpButton(_("This will broadcast a transaction to the network.")),2,3)
2013         grid_raw.setRowStretch(3,1)
2014
2015         # plugins
2016         tab5 = QWidget()
2017         grid_plugins = QGridLayout(tab5)
2018         grid_plugins.setColumnStretch(0,1)
2019         tabs.addTab(tab5, _('Plugins') )
2020         def mk_toggle(cb, p):
2021             return lambda: cb.setChecked(p.toggle(self))
2022         for i, p in enumerate(self.wallet.plugins):
2023             try:
2024                 name, description = p.get_info()
2025                 cb = QCheckBox(name)
2026                 cb.setChecked(p.is_enabled())
2027                 cb.stateChanged.connect(mk_toggle(cb,p))
2028                 grid_plugins.addWidget(cb, i, 0)
2029                 grid_plugins.addWidget(HelpButton(description), i, 2)
2030             except:
2031                 print_msg("Error: cannot display plugin", p)
2032                 traceback.print_exc(file=sys.stdout)
2033
2034         grid_plugins.setRowStretch(i+1,1)
2035
2036         vbox.addLayout(ok_cancel_buttons(d))
2037         d.setLayout(vbox) 
2038
2039         # run the dialog
2040         if not d.exec_(): return
2041
2042         fee = unicode(fee_e.text())
2043         try:
2044             fee = int( 100000000 * Decimal(fee) )
2045         except:
2046             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2047             return
2048
2049         if self.wallet.fee != fee:
2050             self.wallet.fee = fee
2051             self.wallet.save()
2052         
2053         nz = unicode(nz_e.text())
2054         try:
2055             nz = int( nz )
2056             if nz>8: nz=8
2057         except:
2058             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2059             return
2060
2061         if self.wallet.num_zeros != nz:
2062             self.wallet.num_zeros = nz
2063             self.config.set_key('num_zeros', nz, True)
2064             self.update_history_tab()
2065             self.update_receive_tab()
2066
2067         usechange_result = usechange_combo.currentIndex() == 0
2068         if self.wallet.use_change != usechange_result:
2069             self.wallet.use_change = usechange_result
2070             self.config.set_key('use_change', self.wallet.use_change, True)
2071         
2072         try:
2073             n = int(gap_e.text())
2074         except:
2075             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2076             return
2077
2078         if self.wallet.gap_limit != n:
2079             r = self.wallet.change_gap_limit(n)
2080             if r:
2081                 self.update_receive_tab()
2082                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
2083             else:
2084                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2085
2086         need_restart = False
2087
2088         lang_request = languages.keys()[lang_combo.currentIndex()]
2089         if lang_request != self.config.get('language'):
2090             self.config.set_key("language", lang_request, True)
2091             need_restart = True
2092             
2093         cur_request = str(currencies[cur_combo.currentIndex()])
2094         if cur_request != self.config.get('currency', "None"):
2095             self.config.set_key('currency', cur_request, True)
2096             self.update_wallet()
2097
2098         if need_restart:
2099             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2100
2101         self.receive_tab_set_mode(view_combo.currentIndex())
2102
2103
2104     @staticmethod 
2105     def network_dialog(wallet, parent=None):
2106         interface = wallet.interface
2107         if parent:
2108             if interface.is_connected:
2109                 status = _("Connected to")+" %s\n%d "%(interface.host, wallet.verifier.height)+_("blocks")
2110             else:
2111                 status = _("Not connected")
2112             server = interface.server
2113         else:
2114             import random
2115             status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
2116             server = interface.server
2117
2118         plist, servers_list = interface.get_servers_list()
2119
2120         d = QDialog(parent)
2121         d.setModal(1)
2122         d.setWindowTitle(_('Server'))
2123         d.setMinimumSize(375, 20)
2124
2125         vbox = QVBoxLayout()
2126         vbox.setSpacing(30)
2127
2128         hbox = QHBoxLayout()
2129         l = QLabel()
2130         l.setPixmap(QPixmap(":icons/network.png"))
2131         hbox.addStretch(10)
2132         hbox.addWidget(l)
2133         hbox.addWidget(QLabel(status))
2134         hbox.addStretch(50)
2135         vbox.addLayout(hbox)
2136
2137
2138         # grid layout
2139         grid = QGridLayout()
2140         grid.setSpacing(8)
2141         vbox.addLayout(grid)
2142
2143         # server
2144         server_protocol = QComboBox()
2145         server_host = QLineEdit()
2146         server_host.setFixedWidth(200)
2147         server_port = QLineEdit()
2148         server_port.setFixedWidth(60)
2149
2150         protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
2151         protocol_letters = 'thsg'
2152         DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
2153         server_protocol.addItems(protocol_names)
2154
2155         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
2156         grid.addWidget(server_protocol, 0, 1)
2157         grid.addWidget(server_host, 0, 2)
2158         grid.addWidget(server_port, 0, 3)
2159
2160         def change_protocol(p):
2161             protocol = protocol_letters[p]
2162             host = unicode(server_host.text())
2163             pp = plist.get(host,DEFAULT_PORTS)
2164             if protocol not in pp.keys():
2165                 protocol = pp.keys()[0]
2166             port = pp[protocol]
2167             server_host.setText( host )
2168             server_port.setText( port )
2169
2170         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
2171         
2172         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
2173         servers_list_widget = QTreeWidget(parent)
2174         servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
2175         servers_list_widget.setMaximumHeight(150)
2176         servers_list_widget.setColumnWidth(0, 240)
2177         for _host in servers_list.keys():
2178             _type = 'P' if servers_list[_host].get('pruning') else 'F'
2179             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
2180
2181         def change_server(host, protocol=None):
2182             pp = plist.get(host,DEFAULT_PORTS)
2183             if protocol:
2184                 port = pp.get(protocol)
2185                 if not port: protocol = None
2186                     
2187             if not protocol:
2188                 if 't' in pp.keys():
2189                     protocol = 't'
2190                     port = pp.get(protocol)
2191                 else:
2192                     protocol = pp.keys()[0]
2193                     port = pp.get(protocol)
2194             
2195             server_host.setText( host )
2196             server_port.setText( port )
2197             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
2198
2199             if not plist: return
2200             for p in protocol_letters:
2201                 i = protocol_letters.index(p)
2202                 j = server_protocol.model().index(i,0)
2203                 if p not in pp.keys():
2204                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
2205                 else:
2206                     server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
2207
2208
2209         if server:
2210             host, port, protocol = server.split(':')
2211             change_server(host,protocol)
2212
2213         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
2214         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
2215
2216         if not wallet.config.is_modifiable('server'):
2217             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
2218
2219         # auto cycle
2220         autocycle_cb = QCheckBox(_('Try random servers if disconnected'))
2221         autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
2222         grid.addWidget(autocycle_cb, 3, 1, 3, 2)
2223         if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
2224
2225         # proxy setting
2226         proxy_mode = QComboBox()
2227         proxy_host = QLineEdit()
2228         proxy_host.setFixedWidth(200)
2229         proxy_port = QLineEdit()
2230         proxy_port.setFixedWidth(60)
2231         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
2232
2233         def check_for_disable(index = False):
2234             if proxy_mode.currentText() != 'NONE':
2235                 proxy_host.setEnabled(True)
2236                 proxy_port.setEnabled(True)
2237             else:
2238                 proxy_host.setEnabled(False)
2239                 proxy_port.setEnabled(False)
2240
2241         check_for_disable()
2242         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
2243
2244         if not wallet.config.is_modifiable('proxy'):
2245             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
2246
2247         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
2248         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
2249         proxy_host.setText(proxy_config.get("host"))
2250         proxy_port.setText(proxy_config.get("port"))
2251
2252         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
2253         grid.addWidget(proxy_mode, 2, 1)
2254         grid.addWidget(proxy_host, 2, 2)
2255         grid.addWidget(proxy_port, 2, 3)
2256
2257         # buttons
2258         vbox.addLayout(ok_cancel_buttons(d))
2259         d.setLayout(vbox) 
2260
2261         if not d.exec_(): return
2262
2263         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
2264         if proxy_mode.currentText() != 'NONE':
2265             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
2266         else:
2267             proxy = None
2268
2269         wallet.config.set_key("proxy", proxy, True)
2270         wallet.config.set_key("server", server, True)
2271         interface.set_server(server, proxy)
2272         wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
2273         return True
2274
2275     def closeEvent(self, event):
2276         g = self.geometry()
2277         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2278         self.save_column_widths()
2279         self.config.set_key("column-widths", self.column_widths, True)
2280         self.config.set_key("console-history",self.console.history[-50:])
2281         event.accept()
2282
2283
2284 class ElectrumGui:
2285
2286     def __init__(self, wallet, config, app=None):
2287         self.wallet = wallet
2288         self.config = config
2289         if app is None:
2290             self.app = QApplication(sys.argv)
2291
2292
2293     def restore_or_create(self):
2294         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
2295         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
2296         if r==2: return None
2297         return 'restore' if r==1 else 'create'
2298
2299     def seed_dialog(self):
2300         return ElectrumWindow.seed_dialog( self.wallet )
2301
2302     def network_dialog(self):
2303         return ElectrumWindow.network_dialog( self.wallet, parent=None )
2304         
2305
2306     def show_seed(self):
2307         ElectrumWindow.show_seed_dialog(self.wallet)
2308
2309
2310     def password_dialog(self):
2311         ElectrumWindow.change_password_dialog(self.wallet)
2312
2313
2314     def restore_wallet(self):
2315         wallet = self.wallet
2316         # wait until we are connected, because the user might have selected another server
2317         if not wallet.interface.is_connected:
2318             waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
2319             waiting_dialog(waiting)
2320
2321         waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
2322             %(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
2323
2324         wallet.set_up_to_date(False)
2325         wallet.interface.poke('synchronizer')
2326         waiting_dialog(waiting)
2327         if wallet.is_found():
2328             print_error( "Recovery successful" )
2329         else:
2330             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2331
2332         return True
2333
2334     def main(self,url):
2335         s = Timer()
2336         s.start()
2337         w = ElectrumWindow(self.wallet, self.config)
2338         if url: w.set_url(url)
2339         w.app = self.app
2340         w.connect_slots(s)
2341         w.update_wallet()
2342         w.show()
2343
2344         self.app.exec_()
2345
2346