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