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