a309eecab1a13253facd312e0e722baac8981c26
[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.create_status_bar()
290
291         self.wallet.interface.register_callback('updated', lambda: self.emit(QtCore.SIGNAL('update_wallet')))
292         self.wallet.interface.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
293         self.wallet.interface.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
294         self.wallet.interface.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
295
296         self.expert_mode = config.get('classic_expert_mode', False)
297
298         set_language(config.get('language'))
299
300         self.funds_error = False
301         self.completions = QStringListModel()
302
303         self.tabs = tabs = QTabWidget(self)
304         self.column_widths = self.config.get("column-widths", default_column_widths )
305         tabs.addTab(self.create_history_tab(), _('History') )
306         tabs.addTab(self.create_send_tab(), _('Send') )
307         tabs.addTab(self.create_receive_tab(), _('Receive') )
308         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
309         tabs.addTab(self.create_console_tab(), _('Console') )
310         tabs.setMinimumSize(600, 400)
311         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
312         self.setCentralWidget(tabs)
313
314         g = self.config.get("winpos-qt",[100, 100, 840, 400])
315         self.setGeometry(g[0], g[1], g[2], g[3])
316         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.config.path
317         if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
318         self.setWindowTitle( title )
319
320         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
321         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
322         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
323         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
324         
325         self.connect(self, QtCore.SIGNAL('update_wallet'), self.update_wallet)
326         self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
327         self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.wallet.banner) )
328         self.history_list.setFocus(True)
329         
330         self.exchanger = exchange_rate.Exchanger(self)
331         self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
332
333         # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
334         if platform.system() == 'Windows':
335             n = 3 if self.wallet.seed else 2
336             tabs.setCurrentIndex (n)
337             tabs.setCurrentIndex (0)
338
339         # set initial message
340         self.console.showMessage(self.wallet.banner)
341
342         # plugins that need to change the GUI do it here
343         self.run_hook('init_gui')
344
345
346     # plugins
347     def init_plugins(self):
348         import imp, pkgutil, __builtin__
349         if __builtin__.use_local_modules:
350             fp, pathname, description = imp.find_module('plugins')
351             plugin_names = [name for a, name, b in pkgutil.iter_modules([pathname])]
352             plugin_names = filter( lambda name: os.path.exists(os.path.join(pathname,name+'.py')), plugin_names)
353             imp.load_module('electrum_plugins', fp, pathname, description)
354             plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names)
355         else:
356             import electrum_plugins
357             plugin_names = [name for a, name, b in pkgutil.iter_modules(electrum_plugins.__path__)]
358             plugins = [ __import__('electrum_plugins.'+name, fromlist=['electrum_plugins']) for name in plugin_names]
359
360         self.plugins = []
361         for p in plugins:
362             try:
363                 self.plugins.append( p.Plugin(self) )
364             except:
365                 print_msg("Error:cannot initialize plugin",p)
366                 traceback.print_exc(file=sys.stdout)
367
368
369     def run_hook(self, name, *args):
370         for p in self.plugins:
371             if not p.is_enabled():
372                 continue
373             try:
374                 f = eval('p.'+name)
375             except:
376                 continue
377             apply(f, args)
378         return
379
380         
381     def set_label(self, name, text = None):
382         changed = False
383         old_text = self.wallet.labels.get(name)
384         if text:
385             if old_text != text:
386                 self.wallet.labels[name] = text
387                 changed = True
388         else:
389             if old_text:
390                 self.wallet.labels.pop(name)
391                 changed = True
392         self.run_hook('set_label', name, text, changed)
393         return changed
394
395
396     # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
397     def getOpenFileName(self, title, filter = None):
398         directory = self.config.get('io_dir', os.path.expanduser('~'))
399         fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
400         if fileName and directory != os.path.dirname(fileName):
401             self.config.set_key('io_dir', os.path.dirname(fileName), True)
402         return fileName
403
404     def getSaveFileName(self, title, filename, filter = None):
405         directory = self.config.get('io_dir', os.path.expanduser('~'))
406         path = os.path.join( directory, filename )
407         fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
408         if fileName and directory != os.path.dirname(fileName):
409             self.config.set_key('io_dir', os.path.dirname(fileName), True)
410         return fileName
411
412
413
414     def close(self):
415         QMainWindow.close(self)
416         self.run_hook('close_main_window')
417
418     def connect_slots(self, sender):
419         self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
420         self.previous_payto_e=''
421
422     def timer_actions(self):
423         self.run_hook('timer_actions')
424             
425     def update_status(self):
426         if self.wallet.interface and self.wallet.interface.is_connected:
427             if not self.wallet.up_to_date:
428                 text = _("Synchronizing...")
429                 icon = QIcon(":icons/status_waiting.png")
430             else:
431                 c, u = self.wallet.get_balance()
432                 text =  _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
433                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
434                 text += self.create_quote_text(Decimal(c+u)/100000000)
435                 icon = QIcon(":icons/status_connected.png")
436         else:
437             text = _("Not connected")
438             icon = QIcon(":icons/status_disconnected.png")
439
440         self.status_text = text
441         self.statusBar().showMessage(text)
442         self.status_button.setIcon( icon )
443
444     def update_wallet(self):
445         self.update_status()
446         if self.wallet.up_to_date or not self.wallet.interface.is_connected:
447             self.update_history_tab()
448             self.update_receive_tab()
449             self.update_contacts_tab()
450             self.update_completions()
451
452
453     def create_quote_text(self, btc_balance):
454         quote_currency = self.config.get("currency", "None")
455         quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
456         if quote_balance is None:
457             quote_text = ""
458         else:
459             quote_text = "  (%.2f %s)" % (quote_balance, quote_currency)
460         return quote_text
461         
462     def create_history_tab(self):
463         self.history_list = l = MyTreeWidget(self)
464         l.setColumnCount(5)
465         for i,width in enumerate(self.column_widths['history']):
466             l.setColumnWidth(i, width)
467         l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
468         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
469         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
470
471         l.setContextMenuPolicy(Qt.CustomContextMenu)
472         l.customContextMenuRequested.connect(self.create_history_menu)
473         return l
474
475
476     def create_history_menu(self, position):
477         self.history_list.selectedIndexes() 
478         item = self.history_list.currentItem()
479         if not item: return
480         tx_hash = str(item.data(0, Qt.UserRole).toString())
481         if not tx_hash: return
482         menu = QMenu()
483         #menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
484         menu.addAction(_("Details"), lambda: self.show_tx_details(self.wallet.transactions.get(tx_hash)))
485         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
486         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
487
488
489     def show_tx_details(self, tx):
490         dialog = QDialog(self)
491         dialog.setModal(1)
492         dialog.setWindowTitle(_("Transaction Details"))
493         vbox = QVBoxLayout()
494         dialog.setLayout(vbox)
495         dialog.setMinimumSize(600,300)
496
497         tx_hash = tx.hash()
498         if tx_hash in self.wallet.transactions.keys():
499             is_mine, v, fee = self.wallet.get_tx_value(tx)
500             conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
501             if timestamp:
502                 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
503             else:
504                 time_str = 'pending'
505         else:
506             is_mine = False
507
508         vbox.addWidget(QLabel("Transaction ID:"))
509         e  = QLineEdit(tx_hash)
510         e.setReadOnly(True)
511         vbox.addWidget(e)
512
513         vbox.addWidget(QLabel("Date: %s"%time_str))
514         vbox.addWidget(QLabel("Status: %d confirmations"%conf))
515         if is_mine:
516             if fee is not None: 
517                 vbox.addWidget(QLabel("Amount sent: %s"% format_satoshis(v-fee, False)))
518                 vbox.addWidget(QLabel("Transaction fee: %s"% format_satoshis(fee, False)))
519             else:
520                 vbox.addWidget(QLabel("Amount sent: %s"% format_satoshis(v, False)))
521                 vbox.addWidget(QLabel("Transaction fee: unknown"))
522         else:
523             vbox.addWidget(QLabel("Amount received: %s"% format_satoshis(v, False)))
524
525         vbox.addWidget( self.generate_transaction_information_widget(tx) )
526
527         ok_button = QPushButton(_("Close"))
528         ok_button.setDefault(True)
529         ok_button.clicked.connect(dialog.accept)
530         
531         hbox = QHBoxLayout()
532         hbox.addStretch(1)
533         hbox.addWidget(ok_button)
534         vbox.addLayout(hbox)
535         dialog.exec_()
536
537     def tx_label_clicked(self, item, column):
538         if column==2 and item.isSelected():
539             self.is_edit=True
540             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
541             self.history_list.editItem( item, column )
542             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
543             self.is_edit=False
544
545     def tx_label_changed(self, item, column):
546         if self.is_edit: 
547             return
548         self.is_edit=True
549         tx_hash = str(item.data(0, Qt.UserRole).toString())
550         tx = self.wallet.transactions.get(tx_hash)
551         text = unicode( item.text(2) )
552         self.set_label(tx_hash, text) 
553         if text: 
554             item.setForeground(2, QBrush(QColor('black')))
555         else:
556             text = self.wallet.get_default_label(tx_hash)
557             item.setText(2, text)
558             item.setForeground(2, QBrush(QColor('gray')))
559         self.is_edit=False
560
561
562     def edit_label(self, is_recv):
563         l = self.receive_list if is_recv else self.contacts_list
564         item = l.currentItem()
565         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
566         l.editItem( item, 1 )
567         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
568
569
570
571     def address_label_clicked(self, item, column, l, column_addr, column_label):
572         if column == column_label and item.isSelected():
573             is_editable = item.data(0, 32).toBool()
574             if not is_editable:
575                 return
576             addr = unicode( item.text(column_addr) )
577             label = unicode( item.text(column_label) )
578             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
579             l.editItem( item, column )
580             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
581
582
583     def address_label_changed(self, item, column, l, column_addr, column_label):
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 h == []:
1098                         gap += 1
1099                         if gap > self.wallet.gap_limit:
1100                             is_red = True
1101                     else:
1102                         gap = 0
1103
1104                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1105                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1106                     self.update_receive_item(item)
1107                     if is_red:
1108                         item.setBackgroundColor(1, QColor('red'))
1109                     seq_item.addChild(item)
1110
1111         if self.wallet.imported_keys:
1112             c,u = self.wallet.get_imported_balance()
1113             account_item = QTreeWidgetItem( [ _('Imported'), '', format_satoshis(c+u), ''] )
1114             l.addTopLevelItem(account_item)
1115             account_item.setExpanded(True)
1116             for address in self.wallet.imported_keys.keys():
1117                 item = QTreeWidgetItem( [ address, '', '', ''] )
1118                 self.update_receive_item(item)
1119                 account_item.addChild(item)
1120                 
1121
1122         # we use column 1 because column 0 may be hidden
1123         l.setCurrentItem(l.topLevelItem(0),1)
1124
1125
1126     def update_contacts_tab(self):
1127
1128         l = self.contacts_list
1129         l.clear()
1130
1131         for address in self.wallet.addressbook:
1132             label = self.wallet.labels.get(address,'')
1133             n = self.wallet.get_num_tx(address)
1134             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1135             item.setFont(0, QFont(MONOSPACE_FONT))
1136             # 32 = label can be edited (bool)
1137             item.setData(0,32, True)
1138             # 33 = payto string
1139             item.setData(0,33, address)
1140             l.addTopLevelItem(item)
1141
1142         self.run_hook('update_contacts_tab', l)
1143         l.setCurrentItem(l.topLevelItem(0))
1144
1145
1146
1147     def create_console_tab(self):
1148         from qt_console import Console
1149         self.console = console = Console()
1150         self.console.history = self.config.get("console-history",[])
1151         self.console.history_index = len(self.console.history)
1152
1153         console.updateNamespace({'wallet' : self.wallet, 'interface' : self.wallet.interface, 'gui':self})
1154         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1155
1156         c = commands.Commands(self.wallet, self.wallet.interface, lambda: self.console.set_json(True))
1157         methods = {}
1158         def mkfunc(f, method):
1159             return lambda *args: apply( f, (method, args, self.password_dialog ))
1160         for m in dir(c):
1161             if m[0]=='_' or m=='wallet' or m == 'interface': continue
1162             methods[m] = mkfunc(c._run, m)
1163             
1164         console.updateNamespace(methods)
1165         return console
1166
1167
1168     def create_status_bar(self):
1169         self.status_text = ""
1170         sb = QStatusBar()
1171         sb.setFixedHeight(35)
1172         qtVersion = qVersion()
1173
1174         update_notification = UpdateLabel(self.config)
1175         if(update_notification.new_version):
1176             sb.addPermanentWidget(update_notification)
1177
1178         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1179             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1180         if self.wallet.seed:
1181             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), _("Password"), lambda: self.change_password_dialog(self.wallet, self) ) )
1182         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1183         if self.wallet.seed:
1184             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) )
1185         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), lambda: self.network_dialog(self.wallet, self) ) 
1186         sb.addPermanentWidget( self.status_button )
1187
1188         self.run_hook('create_status_bar', (sb,))
1189
1190         self.setStatusBar(sb)
1191         
1192     def go_lite(self):
1193         import gui_lite
1194         self.config.set_key('gui', 'lite', True)
1195         self.hide()
1196         if self.lite:
1197             self.lite.mini.show()
1198         else:
1199             self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
1200             self.lite.main(None)
1201
1202     def new_contact_dialog(self):
1203         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1204         address = unicode(text)
1205         if ok:
1206             if is_valid(address):
1207                 self.wallet.addressbook.append(address)
1208                 self.wallet.save()
1209                 self.update_contacts_tab()
1210                 self.update_history_tab()
1211                 self.update_completions()
1212             else:
1213                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1214
1215     def show_master_public_key(self):
1216         dialog = QDialog(self)
1217         dialog.setModal(1)
1218         dialog.setWindowTitle(_("Master Public Key"))
1219
1220         main_text = QTextEdit()
1221         main_text.setText(self.wallet.get_master_public_key())
1222         main_text.setReadOnly(True)
1223         main_text.setMaximumHeight(170)
1224         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1225
1226         ok_button = QPushButton(_("OK"))
1227         ok_button.setDefault(True)
1228         ok_button.clicked.connect(dialog.accept)
1229
1230         main_layout = QGridLayout()
1231         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1232
1233         main_layout.addWidget(main_text, 1, 0)
1234         main_layout.addWidget(qrw, 1, 1 )
1235
1236         vbox = QVBoxLayout()
1237         vbox.addLayout(main_layout)
1238         hbox = QHBoxLayout()
1239         hbox.addStretch(1)
1240         hbox.addWidget(ok_button)
1241         vbox.addLayout(hbox)
1242
1243         dialog.setLayout(vbox)
1244         dialog.exec_()
1245         
1246
1247     @protected
1248     def show_seed_dialog(self, password):
1249         if not self.wallet.seed:
1250             QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1251             return
1252         try:
1253             seed = self.wallet.decode_seed(password)
1254         except:
1255             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1256             return
1257         self.show_seed(seed, self)
1258
1259
1260     @classmethod
1261     def show_seed(self, seed, parent=None):
1262         dialog = QDialog(parent)
1263         dialog.setModal(1)
1264         dialog.setWindowTitle('Electrum' + ' - ' + _('Seed'))
1265
1266         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1267
1268         label1 = QLabel(_("Your wallet generation seed is")+ ":")
1269
1270         seed_text = QTextEdit(brainwallet)
1271         seed_text.setReadOnly(True)
1272         seed_text.setMaximumHeight(130)
1273         
1274         msg2 =  _("Please write down or memorize these 12 words (order is important).") + " " \
1275               + _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
1276               + _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
1277               + "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
1278         label2 = QLabel(msg2)
1279         label2.setWordWrap(True)
1280
1281         logo = QLabel()
1282         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1283         logo.setMaximumWidth(60)
1284
1285         qrw = QRCodeWidget(seed)
1286
1287         ok_button = QPushButton(_("OK"))
1288         ok_button.setDefault(True)
1289         ok_button.clicked.connect(dialog.accept)
1290
1291         grid = QGridLayout()
1292         #main_layout.addWidget(logo, 0, 0)
1293
1294         grid.addWidget(logo, 0, 0)
1295         grid.addWidget(label1, 0, 1)
1296
1297         grid.addWidget(seed_text, 1, 0, 1, 2)
1298
1299         grid.addWidget(qrw, 0, 2, 2, 1)
1300
1301         vbox = QVBoxLayout()
1302         vbox.addLayout(grid)
1303         vbox.addWidget(label2)
1304
1305         hbox = QHBoxLayout()
1306         hbox.addStretch(1)
1307         hbox.addWidget(ok_button)
1308         vbox.addLayout(hbox)
1309
1310         dialog.setLayout(vbox)
1311         dialog.exec_()
1312
1313     def show_qrcode(self, data, title = "QR code"):
1314         if not data: return
1315         d = QDialog(self)
1316         d.setModal(1)
1317         d.setWindowTitle(title)
1318         d.setMinimumSize(270, 300)
1319         vbox = QVBoxLayout()
1320         qrw = QRCodeWidget(data)
1321         vbox.addWidget(qrw, 1)
1322         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1323         hbox = QHBoxLayout()
1324         hbox.addStretch(1)
1325
1326         def print_qr(self):
1327             filename = "qrcode.bmp"
1328             bmp.save_qrcode(qrw.qr, filename)
1329             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1330
1331         b = QPushButton(_("Save"))
1332         hbox.addWidget(b)
1333         b.clicked.connect(print_qr)
1334
1335         b = QPushButton(_("Close"))
1336         hbox.addWidget(b)
1337         b.clicked.connect(d.accept)
1338         b.setDefault(True)
1339
1340         vbox.addLayout(hbox)
1341         d.setLayout(vbox)
1342         d.exec_()
1343
1344
1345     def do_protect(self, func, args):
1346         if self.wallet.use_encryption:
1347             password = self.password_dialog()
1348             if not password:
1349                 return
1350         else:
1351             password = None
1352             
1353         if args != (False,):
1354             args = (self,) + args + (password,)
1355         else:
1356             args = (self,password)
1357         apply( func, args)
1358
1359
1360     @protected
1361     def show_private_key(self, address, password):
1362         if not address: return
1363         try:
1364             pk = self.wallet.get_private_key(address, password)
1365         except BaseException, e:
1366             self.show_message(str(e))
1367             return
1368         QMessageBox.information(self, _('Private key'), 'Address'+ ': ' + address + '\n\n' + _('Private key') + ': ' + pk, _('OK'))
1369
1370
1371     @protected
1372     def do_sign(self, address, message, signature, password):
1373         try:
1374             sig = self.wallet.sign_message(str(address.text()), str(message.toPlainText()), password)
1375             signature.setText(sig)
1376         except BaseException, e:
1377             self.show_message(str(e))
1378
1379     def sign_message(self, address):
1380         if not address: return
1381         d = QDialog(self)
1382         d.setModal(1)
1383         d.setWindowTitle(_('Sign Message'))
1384         d.setMinimumSize(410, 290)
1385
1386         tab_widget = QTabWidget()
1387         tab = QWidget()
1388         layout = QGridLayout(tab)
1389
1390         sign_address = QLineEdit()
1391
1392         sign_address.setText(address)
1393         layout.addWidget(QLabel(_('Address')), 1, 0)
1394         layout.addWidget(sign_address, 1, 1)
1395
1396         sign_message = QTextEdit()
1397         layout.addWidget(QLabel(_('Message')), 2, 0)
1398         layout.addWidget(sign_message, 2, 1)
1399         layout.setRowStretch(2,3)
1400
1401         sign_signature = QTextEdit()
1402         layout.addWidget(QLabel(_('Signature')), 3, 0)
1403         layout.addWidget(sign_signature, 3, 1)
1404         layout.setRowStretch(3,1)
1405
1406
1407         hbox = QHBoxLayout()
1408         b = QPushButton(_("Sign"))
1409         hbox.addWidget(b)
1410         b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1411         b = QPushButton(_("Close"))
1412         b.clicked.connect(d.accept)
1413         hbox.addWidget(b)
1414         layout.addLayout(hbox, 4, 1)
1415         tab_widget.addTab(tab, _("Sign"))
1416
1417
1418         tab = QWidget()
1419         layout = QGridLayout(tab)
1420
1421         verify_address = QLineEdit()
1422         layout.addWidget(QLabel(_('Address')), 1, 0)
1423         layout.addWidget(verify_address, 1, 1)
1424
1425         verify_message = QTextEdit()
1426         layout.addWidget(QLabel(_('Message')), 2, 0)
1427         layout.addWidget(verify_message, 2, 1)
1428         layout.setRowStretch(2,3)
1429
1430         verify_signature = QTextEdit()
1431         layout.addWidget(QLabel(_('Signature')), 3, 0)
1432         layout.addWidget(verify_signature, 3, 1)
1433         layout.setRowStretch(3,1)
1434
1435         def do_verify():
1436             try:
1437                 self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText()))
1438                 self.show_message(_("Signature verified"))
1439             except BaseException, e:
1440                 self.show_message(str(e))
1441                 return
1442
1443         hbox = QHBoxLayout()
1444         b = QPushButton(_("Verify"))
1445         b.clicked.connect(do_verify)
1446         hbox.addWidget(b)
1447         b = QPushButton(_("Close"))
1448         b.clicked.connect(d.accept)
1449         hbox.addWidget(b)
1450         layout.addLayout(hbox, 4, 1)
1451         tab_widget.addTab(tab, _("Verify"))
1452
1453         vbox = QVBoxLayout()
1454         vbox.addWidget(tab_widget)
1455         d.setLayout(vbox)
1456         d.exec_()
1457
1458         
1459
1460
1461     def question(self, msg):
1462         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1463
1464     def show_message(self, msg):
1465         QMessageBox.information(self, _('Message'), msg, _('OK'))
1466
1467     def password_dialog(self ):
1468         d = QDialog(self)
1469         d.setModal(1)
1470
1471         pw = QLineEdit()
1472         pw.setEchoMode(2)
1473
1474         vbox = QVBoxLayout()
1475         msg = _('Please enter your password')
1476         vbox.addWidget(QLabel(msg))
1477
1478         grid = QGridLayout()
1479         grid.setSpacing(8)
1480         grid.addWidget(QLabel(_('Password')), 1, 0)
1481         grid.addWidget(pw, 1, 1)
1482         vbox.addLayout(grid)
1483
1484         vbox.addLayout(ok_cancel_buttons(d))
1485         d.setLayout(vbox)
1486
1487         self.run_hook('password_dialog', pw, grid, 1)
1488         if not d.exec_(): return
1489         return unicode(pw.text())
1490
1491
1492
1493
1494
1495     @staticmethod
1496     def change_password_dialog( wallet, parent=None ):
1497
1498         if not wallet.seed:
1499             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1500             return
1501
1502         d = QDialog(parent)
1503         d.setModal(1)
1504
1505         pw = QLineEdit()
1506         pw.setEchoMode(2)
1507         new_pw = QLineEdit()
1508         new_pw.setEchoMode(2)
1509         conf_pw = QLineEdit()
1510         conf_pw.setEchoMode(2)
1511
1512         vbox = QVBoxLayout()
1513         if parent:
1514             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1515                    +_('To disable wallet encryption, enter an empty new password.')) \
1516                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1517         else:
1518             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1519                   +_("Leave these fields empty if you want to disable encryption.")
1520         vbox.addWidget(QLabel(msg))
1521
1522         grid = QGridLayout()
1523         grid.setSpacing(8)
1524
1525         if wallet.use_encryption:
1526             grid.addWidget(QLabel(_('Password')), 1, 0)
1527             grid.addWidget(pw, 1, 1)
1528
1529         grid.addWidget(QLabel(_('New Password')), 2, 0)
1530         grid.addWidget(new_pw, 2, 1)
1531
1532         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1533         grid.addWidget(conf_pw, 3, 1)
1534         vbox.addLayout(grid)
1535
1536         vbox.addLayout(ok_cancel_buttons(d))
1537         d.setLayout(vbox) 
1538
1539         if not d.exec_(): return
1540
1541         password = unicode(pw.text()) if wallet.use_encryption else None
1542         new_password = unicode(new_pw.text())
1543         new_password2 = unicode(conf_pw.text())
1544
1545         try:
1546             seed = wallet.decode_seed(password)
1547         except:
1548             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1549             return
1550
1551         if new_password != new_password2:
1552             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1553             return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1554
1555         wallet.update_password(seed, password, new_password)
1556
1557     @staticmethod
1558     def seed_dialog(wallet, parent=None):
1559         d = QDialog(parent)
1560         d.setModal(1)
1561
1562         vbox = QVBoxLayout()
1563         msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + '\n')
1564         vbox.addWidget(QLabel(msg))
1565
1566         grid = QGridLayout()
1567         grid.setSpacing(8)
1568
1569         seed_e = QLineEdit()
1570         grid.addWidget(QLabel(_('Seed or master public key')), 1, 0)
1571         grid.addWidget(seed_e, 1, 1)
1572         grid.addWidget(HelpButton(_("Your seed can be entered as a mnemonic (sequence of words), or as a hexadecimal string.")), 1, 3)
1573
1574         gap_e = QLineEdit()
1575         gap_e.setText("5")
1576         grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1577         grid.addWidget(gap_e, 2, 1)
1578         grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3)
1579         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1580         vbox.addLayout(grid)
1581
1582         vbox.addLayout(ok_cancel_buttons(d))
1583         d.setLayout(vbox) 
1584
1585         if not d.exec_(): return
1586
1587         try:
1588             gap = int(unicode(gap_e.text()))
1589         except:
1590             QMessageBox.warning(None, _('Error'), 'error', 'OK')
1591             return
1592
1593         try:
1594             seed = str(seed_e.text())
1595             seed.decode('hex')
1596         except:
1597             print_error("Warning: Not hex, trying decode")
1598             try:
1599                 seed = mnemonic.mn_decode( seed.split(' ') )
1600             except:
1601                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1602                 return
1603
1604         if not seed:
1605             QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
1606             return
1607
1608         return seed, gap
1609
1610     def generate_transaction_information_widget(self, tx):
1611         tabs = QTabWidget(self)
1612
1613         tab1 = QWidget()
1614         grid_ui = QGridLayout(tab1)
1615         grid_ui.setColumnStretch(0,1)
1616         tabs.addTab(tab1, _('Outputs') )
1617
1618         tree_widget = MyTreeWidget(self)
1619         tree_widget.setColumnCount(2)
1620         tree_widget.setHeaderLabels( [_('Address'), _('Amount')] )
1621         tree_widget.setColumnWidth(0, 300)
1622         tree_widget.setColumnWidth(1, 50)
1623
1624         for address, value in tx.outputs:
1625             item = QTreeWidgetItem( [address, "%s" % ( format_satoshis(value))] )
1626             tree_widget.addTopLevelItem(item)
1627
1628         tree_widget.setMaximumHeight(100)
1629
1630         grid_ui.addWidget(tree_widget)
1631
1632         tab2 = QWidget()
1633         grid_ui = QGridLayout(tab2)
1634         grid_ui.setColumnStretch(0,1)
1635         tabs.addTab(tab2, _('Inputs') )
1636         
1637         tree_widget = MyTreeWidget(self)
1638         tree_widget.setColumnCount(2)
1639         tree_widget.setHeaderLabels( [ _('Address'), _('Previous output')] )
1640
1641         for input_line in tx.inputs:
1642             item = QTreeWidgetItem( [ str(input_line["address"]), str(input_line["prevout_hash"])] )
1643             tree_widget.addTopLevelItem(item)
1644
1645         tree_widget.setMaximumHeight(100)
1646
1647         grid_ui.addWidget(tree_widget)
1648         return tabs
1649
1650
1651     def tx_dict_from_text(self, txt):
1652         try:
1653             tx_dict = json.loads(str(txt))
1654             assert "hex" in tx_dict.keys()
1655             assert "complete" in tx_dict.keys()
1656             if not tx_dict["complete"]:
1657                 assert "input_info" in tx_dict.keys()
1658         except:
1659             QMessageBox.critical(None, "Unable to parse transaction", _("Electrum was unable to parse your transaction"))
1660             return None
1661         return tx_dict
1662
1663
1664     def read_tx_from_file(self):
1665         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1666         if not fileName:
1667             return
1668         try:
1669             with open(fileName, "r") as f:
1670                 file_content = f.read()
1671         except (ValueError, IOError, os.error), reason:
1672             QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1673
1674         return self.tx_dict_from_text(file_content)
1675
1676
1677     @protected
1678     def sign_raw_transaction(self, tx, input_info, dialog ="", password = ""):
1679         try:
1680             self.wallet.signrawtransaction(tx, input_info, [], password)
1681             
1682             fileName = self.getSaveFileName(_("Select where to save your signed transaction"), 'signed_%s.txn' % (tx.hash()[0:8]), "*.txn")
1683             if fileName:
1684                 with open(fileName, "w+") as f:
1685                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
1686                 self.show_message(_("Transaction saved successfully"))
1687                 if dialog:
1688                     dialog.done(0)
1689         except BaseException, e:
1690             self.show_message(str(e))
1691     
1692
1693     def send_raw_transaction(self, raw_tx, dialog = ""):
1694         result, result_message = self.wallet.sendtx( raw_tx )
1695         if result:
1696             self.show_message("Transaction successfully sent: %s" % (result_message))
1697             if dialog:
1698                 dialog.done(0)
1699         else:
1700             self.show_message("There was a problem sending your transaction:\n %s" % (result_message))
1701
1702     def do_process_from_text(self):
1703         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1704         if not text:
1705             return
1706         tx_dict = self.tx_dict_from_text(text)
1707         if tx_dict:
1708             self.create_process_transaction_window(tx_dict)
1709
1710     def do_process_from_file(self):
1711         tx_dict = self.read_tx_from_file()
1712         if tx_dict: 
1713             self.create_process_transaction_window(tx_dict)
1714
1715     def create_process_transaction_window(self, tx_dict):
1716         tx = Transaction(tx_dict["hex"])
1717             
1718         dialog = QDialog(self)
1719         dialog.setMinimumWidth(500)
1720         dialog.setWindowTitle(_('Process raw transaction'))
1721         dialog.setModal(1)
1722
1723         l = QGridLayout()
1724         dialog.setLayout(l)
1725
1726         l.addWidget(QLabel(_("Transaction status:")), 3,0)
1727         l.addWidget(QLabel(_("Actions")), 4,0)
1728
1729         if tx_dict["complete"] == False:
1730             l.addWidget(QLabel(_("Unsigned")), 3,1)
1731             if self.wallet.seed :
1732                 b = QPushButton("Sign transaction")
1733                 input_info = json.loads(tx_dict["input_info"])
1734                 b.clicked.connect(lambda: self.sign_raw_transaction(tx, input_info, dialog))
1735                 l.addWidget(b, 4, 1)
1736             else:
1737                 l.addWidget(QLabel(_("Wallet is de-seeded, can't sign.")), 4,1)
1738         else:
1739             l.addWidget(QLabel(_("Signed")), 3,1)
1740             b = QPushButton("Broadcast transaction")
1741             b.clicked.connect(lambda: self.send_raw_transaction(tx, dialog))
1742             l.addWidget(b,4,1)
1743
1744         l.addWidget( self.generate_transaction_information_widget(tx), 0,0,2,3)
1745         cancelButton = QPushButton(_("Cancel"))
1746         cancelButton.clicked.connect(lambda: dialog.done(0))
1747         l.addWidget(cancelButton, 4,2)
1748
1749         dialog.exec_()
1750
1751
1752     @protected
1753     def do_export_privkeys(self, password):
1754         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.")))
1755
1756         try:
1757             select_export = _('Select file to export your private keys to')
1758             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1759             if fileName:
1760                 with open(fileName, "w+") as csvfile:
1761                     transaction = csv.writer(csvfile)
1762                     transaction.writerow(["address", "private_key"])
1763
1764                     
1765                     for addr, pk in self.wallet.get_private_keys(self.wallet.addresses(True), password).items():
1766                         transaction.writerow(["%34s"%addr,pk])
1767
1768                     self.show_message(_("Private keys exported."))
1769
1770         except (IOError, os.error), reason:
1771             export_error_label = _("Electrum was unable to produce a private key-export.")
1772             QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
1773
1774         except BaseException, e:
1775           self.show_message(str(e))
1776           return
1777
1778
1779     def do_import_labels(self):
1780         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1781         if not labelsFile: return
1782         try:
1783             f = open(labelsFile, 'r')
1784             data = f.read()
1785             f.close()
1786             for key, value in json.loads(data).items():
1787                 self.wallet.labels[key] = value
1788             self.wallet.save()
1789             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1790         except (IOError, os.error), reason:
1791             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1792             
1793
1794     def do_export_labels(self):
1795         labels = self.wallet.labels
1796         try:
1797             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1798             if fileName:
1799                 with open(fileName, 'w+') as f:
1800                     json.dump(labels, f)
1801                 QMessageBox.information(None, "Labels exported", _("Your labels where exported to")+" '%s'" % str(fileName))
1802         except (IOError, os.error), reason:
1803             QMessageBox.critical(None, "Unable to export labels", _("Electrum was unable to export your labels.")+"\n" + str(reason))
1804
1805
1806     def do_export_history(self):
1807         from gui_lite import csv_transaction
1808         csv_transaction(self.wallet)
1809
1810
1811     @protected
1812     def do_import_privkey(self, password):
1813         if not self.wallet.imported_keys:
1814             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1815                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1816                                          + _('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>' \
1817                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1818             if r == 4: return
1819
1820         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1821         if not text: return
1822
1823         text = str(text).split()
1824         badkeys = []
1825         addrlist = []
1826         for key in text:
1827             try:
1828                 addr = self.wallet.import_key(key, password)
1829             except BaseException as e:
1830                 badkeys.append(key)
1831                 continue
1832             if not addr: 
1833                 badkeys.append(key)
1834             else:
1835                 addrlist.append(addr)
1836         if addrlist:
1837             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1838         if badkeys:
1839             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1840         self.update_receive_tab()
1841         self.update_history_tab()
1842
1843
1844     def settings_dialog(self):
1845         d = QDialog(self)
1846         d.setWindowTitle(_('Electrum Settings'))
1847         d.setModal(1)
1848         vbox = QVBoxLayout()
1849
1850         tabs = QTabWidget(self)
1851         self.settings_tab = tabs
1852         vbox.addWidget(tabs)
1853
1854         tab1 = QWidget()
1855         grid_ui = QGridLayout(tab1)
1856         grid_ui.setColumnStretch(0,1)
1857         tabs.addTab(tab1, _('Display') )
1858
1859         nz_label = QLabel(_('Display zeros'))
1860         grid_ui.addWidget(nz_label, 0, 0)
1861         nz_e = QLineEdit()
1862         nz_e.setText("%d"% self.wallet.num_zeros)
1863         grid_ui.addWidget(nz_e, 0, 1)
1864         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1865         grid_ui.addWidget(HelpButton(msg), 0, 2)
1866         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1867         if not self.config.is_modifiable('num_zeros'):
1868             for w in [nz_e, nz_label]: w.setEnabled(False)
1869         
1870         lang_label=QLabel(_('Language') + ':')
1871         grid_ui.addWidget(lang_label, 1, 0)
1872         lang_combo = QComboBox()
1873         from i18n import languages
1874         lang_combo.addItems(languages.values())
1875         try:
1876             index = languages.keys().index(self.config.get("language",''))
1877         except:
1878             index = 0
1879         lang_combo.setCurrentIndex(index)
1880         grid_ui.addWidget(lang_combo, 1, 1)
1881         grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1882         if not self.config.is_modifiable('language'):
1883             for w in [lang_combo, lang_label]: w.setEnabled(False)
1884
1885         currencies = self.exchanger.get_currencies()
1886         currencies.insert(0, "None")
1887
1888         cur_label=QLabel(_('Currency') + ':')
1889         grid_ui.addWidget(cur_label , 2, 0)
1890         cur_combo = QComboBox()
1891         cur_combo.addItems(currencies)
1892         try:
1893             index = currencies.index(self.config.get('currency', "None"))
1894         except:
1895             index = 0
1896         cur_combo.setCurrentIndex(index)
1897         grid_ui.addWidget(cur_combo, 2, 1)
1898         grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 2, 2)
1899         
1900         view_label=QLabel(_('Receive Tab') + ':')
1901         grid_ui.addWidget(view_label , 3, 0)
1902         view_combo = QComboBox()
1903         view_combo.addItems([_('Simple'), _('Advanced')])
1904         view_combo.setCurrentIndex(self.expert_mode)
1905         grid_ui.addWidget(view_combo, 3, 1)
1906         hh = _('This selects the interaction mode of the "Receive" tab.')+' ' + '\n\n' \
1907              + _('Simple') +   ': ' + _('Show only addresses and labels.') + '\n\n' \
1908              + _('Advanced') + ': ' + _('Show address balances and add extra menu items to freeze/prioritize addresses.') + '\n\n' 
1909         
1910         grid_ui.addWidget(HelpButton(hh), 3, 2)
1911         grid_ui.setRowStretch(4,1)
1912
1913         # wallet tab
1914         tab2 = QWidget()
1915         grid_wallet = QGridLayout(tab2)
1916         grid_wallet.setColumnStretch(0,1)
1917         tabs.addTab(tab2, _('Wallet') )
1918         
1919         fee_label = QLabel(_('Transaction fee'))
1920         grid_wallet.addWidget(fee_label, 0, 0)
1921         fee_e = QLineEdit()
1922         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1923         grid_wallet.addWidget(fee_e, 0, 2)
1924         msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1925             + _('Recommended value') + ': 0.001'
1926         grid_wallet.addWidget(HelpButton(msg), 0, 3)
1927         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1928         if not self.config.is_modifiable('fee'):
1929             for w in [fee_e, fee_label]: w.setEnabled(False)
1930
1931         usechange_label = QLabel(_('Use change addresses'))
1932         grid_wallet.addWidget(usechange_label, 1, 0)
1933         usechange_combo = QComboBox()
1934         usechange_combo.addItems([_('Yes'), _('No')])
1935         usechange_combo.setCurrentIndex(not self.wallet.use_change)
1936         grid_wallet.addWidget(usechange_combo, 1, 2)
1937         grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1938         if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
1939
1940         gap_label = QLabel(_('Gap limit'))
1941         grid_wallet.addWidget(gap_label, 2, 0)
1942         gap_e = QLineEdit()
1943         gap_e.setText("%d"% self.wallet.gap_limit)
1944         grid_wallet.addWidget(gap_e, 2, 2)
1945         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1946               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1947               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1948               + _('Given the current status of your address sequence, the minimum gap limit you can use is:')+' ' + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1949               + _('Warning') + ': ' \
1950               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1951               + _('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' 
1952         grid_wallet.addWidget(HelpButton(msg), 2, 3)
1953         gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1954         if not self.config.is_modifiable('gap_limit'):
1955             for w in [gap_e, gap_label]: w.setEnabled(False)
1956
1957         grid_wallet.setRowStretch(3,1)
1958
1959
1960         # import/export tab
1961         tab3 = QWidget()
1962         grid_io = QGridLayout(tab3)
1963         grid_io.setColumnStretch(0,1)
1964         tabs.addTab(tab3, _('Import/Export') )
1965         
1966         grid_io.addWidget(QLabel(_('Labels')), 1, 0)
1967         grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
1968         grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
1969         grid_io.addWidget(HelpButton(_('Export your labels as json')), 1, 3)
1970
1971         grid_io.addWidget(QLabel(_('History')), 2, 0)
1972         grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
1973         grid_io.addWidget(HelpButton(_('Export your transaction history as csv')), 2, 3)
1974
1975         grid_io.addWidget(QLabel(_('Private keys')), 3, 0)
1976
1977         grid_io.addWidget(EnterButton(_("Export"), self.do_export_privkeys), 3, 1)
1978         grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
1979         grid_io.addWidget(HelpButton(_('Import private key')), 3, 3)
1980
1981         grid_io.addWidget(QLabel(_('Master Public Key')), 4, 0)
1982         grid_io.addWidget(EnterButton(_("Show"), self.show_master_public_key), 4, 1)
1983         grid_io.addWidget(HelpButton(_('Your Master Public Key can be used to create receiving addresses, but not to sign transactions.') + ' ' \
1984                               + _('If you give it to someone, they will be able to see your transactions, but not to spend your money.') + ' ' \
1985                               + _('If you restore your wallet from it, a watching-only (deseeded) wallet will be created.')), 4, 3)
1986
1987
1988         grid_io.addWidget(QLabel(_("Load transaction")), 5, 0)
1989         grid_io.addWidget(EnterButton(_("From file"), self.do_process_from_file), 5, 1)
1990         grid_io.addWidget(EnterButton(_("From text"), self.do_process_from_text), 5, 2)
1991         grid_io.addWidget(HelpButton(_("This will give you the option to sign or broadcast a transaction based on it's status.")), 5, 3)
1992
1993         grid_io.setRowStretch(5,1)
1994
1995
1996         # plugins
1997         if self.plugins:
1998             tab5 = QScrollArea()
1999             grid_plugins = QGridLayout(tab5)
2000             grid_plugins.setColumnStretch(0,1)
2001             tabs.addTab(tab5, _('Plugins') )
2002             def mk_toggle(cb, p):
2003                 return lambda: cb.setChecked(p.toggle())
2004             for i, p in enumerate(self.plugins):
2005                 try:
2006                     name, description = p.get_info()
2007                     cb = QCheckBox(name)
2008                     cb.setDisabled(not p.is_available())
2009                     cb.setChecked(p.is_enabled())
2010                     cb.clicked.connect(mk_toggle(cb,p))
2011                     grid_plugins.addWidget(cb, i, 0)
2012                     grid_plugins.addWidget(HelpButton(description), i, 1)
2013                 except:
2014                     print_msg("Error: cannot display plugin", p)
2015                     traceback.print_exc(file=sys.stdout)
2016             grid_plugins.setRowStretch(i+1,1)
2017
2018         self.run_hook('create_settings_tab', (tabs,))
2019
2020         vbox.addLayout(ok_cancel_buttons(d))
2021         d.setLayout(vbox) 
2022
2023         # run the dialog
2024         if not d.exec_(): return
2025
2026         fee = unicode(fee_e.text())
2027         try:
2028             fee = int( 100000000 * Decimal(fee) )
2029         except:
2030             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2031             return
2032
2033         if self.wallet.fee != fee:
2034             self.wallet.fee = fee
2035             self.wallet.save()
2036         
2037         nz = unicode(nz_e.text())
2038         try:
2039             nz = int( nz )
2040             if nz>8: nz=8
2041         except:
2042             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2043             return
2044
2045         if self.wallet.num_zeros != nz:
2046             self.wallet.num_zeros = nz
2047             self.config.set_key('num_zeros', nz, True)
2048             self.update_history_tab()
2049             self.update_receive_tab()
2050
2051         usechange_result = usechange_combo.currentIndex() == 0
2052         if self.wallet.use_change != usechange_result:
2053             self.wallet.use_change = usechange_result
2054             self.config.set_key('use_change', self.wallet.use_change, True)
2055         
2056         try:
2057             n = int(gap_e.text())
2058         except:
2059             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2060             return
2061
2062         if self.wallet.gap_limit != n:
2063             r = self.wallet.change_gap_limit(n)
2064             if r:
2065                 self.update_receive_tab()
2066                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
2067             else:
2068                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2069
2070         need_restart = False
2071
2072         lang_request = languages.keys()[lang_combo.currentIndex()]
2073         if lang_request != self.config.get('language'):
2074             self.config.set_key("language", lang_request, True)
2075             need_restart = True
2076             
2077         cur_request = str(currencies[cur_combo.currentIndex()])
2078         if cur_request != self.config.get('currency', "None"):
2079             self.config.set_key('currency', cur_request, True)
2080             self.update_wallet()
2081
2082         self.run_hook('close_settings_dialog', ())
2083
2084         if need_restart:
2085             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2086
2087         self.receive_tab_set_mode(view_combo.currentIndex())
2088
2089
2090     @staticmethod 
2091     def network_dialog(wallet, parent=None):
2092         interface = wallet.interface
2093         if parent:
2094             if interface.is_connected:
2095                 status = _("Connected to")+" %s\n%d "%(interface.host, wallet.verifier.height)+_("blocks")
2096             else:
2097                 status = _("Not connected")
2098             server = interface.server
2099         else:
2100             import random
2101             status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
2102             server = interface.server
2103
2104         plist, servers_list = interface.get_servers_list()
2105
2106         d = QDialog(parent)
2107         d.setModal(1)
2108         d.setWindowTitle(_('Server'))
2109         d.setMinimumSize(375, 20)
2110
2111         vbox = QVBoxLayout()
2112         vbox.setSpacing(30)
2113
2114         hbox = QHBoxLayout()
2115         l = QLabel()
2116         l.setPixmap(QPixmap(":icons/network.png"))
2117         hbox.addStretch(10)
2118         hbox.addWidget(l)
2119         hbox.addWidget(QLabel(status))
2120         hbox.addStretch(50)
2121         vbox.addLayout(hbox)
2122
2123
2124         # grid layout
2125         grid = QGridLayout()
2126         grid.setSpacing(8)
2127         vbox.addLayout(grid)
2128
2129         # server
2130         server_protocol = QComboBox()
2131         server_host = QLineEdit()
2132         server_host.setFixedWidth(200)
2133         server_port = QLineEdit()
2134         server_port.setFixedWidth(60)
2135
2136         protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
2137         protocol_letters = 'thsg'
2138         DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
2139         server_protocol.addItems(protocol_names)
2140
2141         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
2142         grid.addWidget(server_protocol, 0, 1)
2143         grid.addWidget(server_host, 0, 2)
2144         grid.addWidget(server_port, 0, 3)
2145
2146         def change_protocol(p):
2147             protocol = protocol_letters[p]
2148             host = unicode(server_host.text())
2149             pp = plist.get(host,DEFAULT_PORTS)
2150             if protocol not in pp.keys():
2151                 protocol = pp.keys()[0]
2152             port = pp[protocol]
2153             server_host.setText( host )
2154             server_port.setText( port )
2155
2156         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
2157         
2158         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
2159         servers_list_widget = QTreeWidget(parent)
2160         servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
2161         servers_list_widget.setMaximumHeight(150)
2162         servers_list_widget.setColumnWidth(0, 240)
2163         for _host in servers_list.keys():
2164             _type = 'P' if servers_list[_host].get('pruning') else 'F'
2165             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
2166
2167         def change_server(host, protocol=None):
2168             pp = plist.get(host,DEFAULT_PORTS)
2169             if protocol:
2170                 port = pp.get(protocol)
2171                 if not port: protocol = None
2172                     
2173             if not protocol:
2174                 if 't' in pp.keys():
2175                     protocol = 't'
2176                     port = pp.get(protocol)
2177                 else:
2178                     protocol = pp.keys()[0]
2179                     port = pp.get(protocol)
2180             
2181             server_host.setText( host )
2182             server_port.setText( port )
2183             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
2184
2185             if not plist: return
2186             for p in protocol_letters:
2187                 i = protocol_letters.index(p)
2188                 j = server_protocol.model().index(i,0)
2189                 if p not in pp.keys() and interface.is_connected:
2190                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
2191                 else:
2192                     server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
2193
2194
2195         if server:
2196             host, port, protocol = server.split(':')
2197             change_server(host,protocol)
2198
2199         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
2200         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
2201
2202         if not wallet.config.is_modifiable('server'):
2203             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
2204
2205         # auto cycle
2206         autocycle_cb = QCheckBox(_('Try random servers if disconnected'))
2207         autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
2208         grid.addWidget(autocycle_cb, 3, 1, 3, 2)
2209         if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
2210
2211         # proxy setting
2212         proxy_mode = QComboBox()
2213         proxy_host = QLineEdit()
2214         proxy_host.setFixedWidth(200)
2215         proxy_port = QLineEdit()
2216         proxy_port.setFixedWidth(60)
2217         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
2218
2219         def check_for_disable(index = False):
2220             if proxy_mode.currentText() != 'NONE':
2221                 proxy_host.setEnabled(True)
2222                 proxy_port.setEnabled(True)
2223             else:
2224                 proxy_host.setEnabled(False)
2225                 proxy_port.setEnabled(False)
2226
2227         check_for_disable()
2228         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
2229
2230         if not wallet.config.is_modifiable('proxy'):
2231             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
2232
2233         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
2234         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
2235         proxy_host.setText(proxy_config.get("host"))
2236         proxy_port.setText(proxy_config.get("port"))
2237
2238         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
2239         grid.addWidget(proxy_mode, 2, 1)
2240         grid.addWidget(proxy_host, 2, 2)
2241         grid.addWidget(proxy_port, 2, 3)
2242
2243         # buttons
2244         vbox.addLayout(ok_cancel_buttons(d))
2245         d.setLayout(vbox) 
2246
2247         if not d.exec_(): return
2248
2249         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
2250         if proxy_mode.currentText() != 'NONE':
2251             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
2252         else:
2253             proxy = None
2254
2255         wallet.config.set_key("proxy", proxy, True)
2256         wallet.config.set_key("server", server, True)
2257         interface.set_server(server, proxy)
2258         wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
2259         return True
2260
2261     def closeEvent(self, event):
2262         g = self.geometry()
2263         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2264         self.save_column_widths()
2265         self.config.set_key("column-widths", self.column_widths, True)
2266         self.config.set_key("console-history",self.console.history[-50:])
2267         event.accept()
2268
2269
2270 class ElectrumGui:
2271
2272     def __init__(self, wallet, config, app=None):
2273         self.wallet = wallet
2274         self.config = config
2275         if app is None:
2276             self.app = QApplication(sys.argv)
2277
2278
2279     def restore_or_create(self):
2280         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
2281         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
2282         if r==2: return None
2283         return 'restore' if r==1 else 'create'
2284
2285     def seed_dialog(self):
2286         return ElectrumWindow.seed_dialog( self.wallet )
2287
2288     def network_dialog(self):
2289         return ElectrumWindow.network_dialog( self.wallet, parent=None )
2290         
2291
2292     def show_seed(self):
2293         ElectrumWindow.show_seed(self.wallet.seed)
2294
2295
2296     def password_dialog(self):
2297         if self.wallet.seed:
2298             ElectrumWindow.change_password_dialog(self.wallet)
2299
2300
2301     def restore_wallet(self):
2302         wallet = self.wallet
2303         # wait until we are connected, because the user might have selected another server
2304         if not wallet.interface.is_connected:
2305             waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
2306             waiting_dialog(waiting)
2307
2308         waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
2309             %(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
2310
2311         wallet.set_up_to_date(False)
2312         wallet.interface.poke('synchronizer')
2313         waiting_dialog(waiting)
2314         if wallet.is_found():
2315             print_error( "Recovery successful" )
2316         else:
2317             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2318
2319         return True
2320
2321     def main(self,url):
2322         s = Timer()
2323         s.start()
2324         w = ElectrumWindow(self.wallet, self.config)
2325         if url: w.set_url(url)
2326         w.app = self.app
2327         w.connect_slots(s)
2328         w.update_wallet()
2329         w.show()
2330
2331         self.app.exec_()
2332
2333