fix: sanitize outputs
[electrum-nvc.git] / gui / qt / main_window.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 electrum.i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
23 import webbrowser
24 import shutil
25 import StringIO
26
27
28 import PyQt4
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
32
33 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
34 from electrum.plugins import run_hook
35
36 import icons_rc
37
38 from electrum.wallet import format_satoshis
39 from electrum import Transaction
40 from electrum import mnemonic
41 from electrum import util, bitcoin, commands, Interface, Wallet
42 from electrum import SimpleConfig, Wallet, WalletStorage
43
44
45 from electrum import bmp, pyqrnative
46
47 from amountedit import AmountEdit, MyLineEdit
48 from network_dialog import NetworkDialog
49 from qrcodewidget import QRCodeWidget
50
51 from decimal import Decimal
52
53 import platform
54 import httplib
55 import socket
56 import webbrowser
57 import csv
58
59 if platform.system() == 'Windows':
60     MONOSPACE_FONT = 'Lucida Console'
61 elif platform.system() == 'Darwin':
62     MONOSPACE_FONT = 'Monaco'
63 else:
64     MONOSPACE_FONT = 'monospace'
65
66 from electrum import ELECTRUM_VERSION
67 import re
68
69 from util import *
70
71
72
73
74
75
76 class StatusBarButton(QPushButton):
77     def __init__(self, icon, tooltip, func):
78         QPushButton.__init__(self, icon, '')
79         self.setToolTip(tooltip)
80         self.setFlat(True)
81         self.setMaximumWidth(25)
82         self.clicked.connect(func)
83         self.func = func
84         self.setIconSize(QSize(25,25))
85
86     def keyPressEvent(self, e):
87         if e.key() == QtCore.Qt.Key_Return:
88             apply(self.func,())
89
90
91
92
93
94
95
96
97
98
99 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
100
101 class ElectrumWindow(QMainWindow):
102
103
104
105     def __init__(self, config, network, gui_object):
106         QMainWindow.__init__(self)
107
108         self.config = config
109         self.network = network
110         self.gui_object = gui_object
111         self.tray = gui_object.tray
112         self.go_lite = gui_object.go_lite
113         self.lite = None
114
115         self.create_status_bar()
116         self.need_update = threading.Event()
117
118         self.decimal_point = config.get('decimal_point', 5)
119         self.num_zeros     = int(config.get('num_zeros',0))
120
121         set_language(config.get('language'))
122
123         self.funds_error = False
124         self.completions = QStringListModel()
125
126         self.tabs = tabs = QTabWidget(self)
127         self.column_widths = self.config.get("column_widths_2", default_column_widths )
128         tabs.addTab(self.create_history_tab(), _('History') )
129         tabs.addTab(self.create_send_tab(), _('Send') )
130         tabs.addTab(self.create_receive_tab(), _('Receive') )
131         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
132         tabs.addTab(self.create_console_tab(), _('Console') )
133         tabs.setMinimumSize(600, 400)
134         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
135         self.setCentralWidget(tabs)
136
137         g = self.config.get("winpos-qt",[100, 100, 840, 400])
138         self.setGeometry(g[0], g[1], g[2], g[3])
139         if self.config.get("is_maximized"):
140             self.showMaximized()
141
142         self.setWindowIcon(QIcon(":icons/electrum.png"))
143         self.init_menubar()
144
145         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
146         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
147         QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
148         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
149         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
150
151         for i in range(tabs.count()):
152             QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
153
154         self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
155         self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
156         self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
157         self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
158         self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
159
160         self.history_list.setFocus(True)
161
162         # network callbacks
163         if self.network:
164             self.network.register_callback('updated', lambda: self.need_update.set())
165             self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
166             self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
167             self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
168             self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
169
170             # set initial message
171             self.console.showMessage(self.network.banner)
172
173         self.wallet = None
174
175
176     def update_account_selector(self):
177         # account selector
178         accounts = self.wallet.get_account_names()
179         self.account_selector.clear()
180         if len(accounts) > 1:
181             self.account_selector.addItems([_("All accounts")] + accounts.values())
182             self.account_selector.setCurrentIndex(0)
183             self.account_selector.show()
184         else:
185             self.account_selector.hide()
186
187
188     def load_wallet(self, wallet):
189         import electrum
190
191         self.wallet = wallet
192         self.update_wallet_format()
193
194         self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
195         self.current_account = self.wallet.storage.get("current_account", None)
196         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.wallet.storage.path
197         if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
198         self.setWindowTitle( title )
199         self.update_wallet()
200         # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
201         self.notify_transactions()
202         self.update_account_selector()
203         # update menus
204         self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
205         self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
206         self.password_menu.setEnabled(not self.wallet.is_watching_only())
207         self.seed_menu.setEnabled(self.wallet.has_seed())
208         self.mpk_menu.setEnabled(self.wallet.is_deterministic())
209         self.import_menu.setEnabled(self.wallet.can_import())
210
211         self.update_lock_icon()
212         self.update_buttons_on_seed()
213         self.update_console()
214
215         run_hook('load_wallet', wallet)
216
217
218     def update_wallet_format(self):
219         # convert old-format imported keys
220         if self.wallet.imported_keys:
221             password = self.password_dialog(_("Please enter your password in order to update imported keys"))
222             try:
223                 self.wallet.convert_imported_keys(password)
224             except:
225                 self.show_message("error")
226
227
228     def open_wallet(self):
229         wallet_folder = self.wallet.storage.path
230         filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
231         if not filename:
232             return
233
234         storage = WalletStorage({'wallet_path': filename})
235         if not storage.file_exists:
236             self.show_message("file not found "+ filename)
237             return
238
239         self.wallet.stop_threads()
240
241         # create new wallet
242         wallet = Wallet(storage)
243         wallet.start_threads(self.network)
244
245         self.load_wallet(wallet)
246
247
248
249     def backup_wallet(self):
250         import shutil
251         path = self.wallet.storage.path
252         wallet_folder = os.path.dirname(path)
253         filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
254         if not filename:
255             return
256
257         new_path = os.path.join(wallet_folder, filename)
258         if new_path != path:
259             try:
260                 shutil.copy2(path, new_path)
261                 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
262             except (IOError, os.error), reason:
263                 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
264
265
266     def new_wallet(self):
267         import installwizard
268
269         wallet_folder = os.path.dirname(self.wallet.storage.path)
270         filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
271         if not filename:
272             return
273         filename = os.path.join(wallet_folder, filename)
274
275         storage = WalletStorage({'wallet_path': filename})
276         if storage.file_exists:
277             QMessageBox.critical(None, "Error", _("File exists"))
278             return
279
280         wizard = installwizard.InstallWizard(self.config, self.network, storage)
281         wallet = wizard.run('new')
282         if wallet:
283             self.load_wallet(wallet)
284
285
286
287     def init_menubar(self):
288         menubar = QMenuBar()
289
290         file_menu = menubar.addMenu(_("&File"))
291         file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
292         file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
293         file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
294         file_menu.addAction(_("&Quit"), self.close)
295
296         wallet_menu = menubar.addMenu(_("&Wallet"))
297         wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
298         self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
299
300         wallet_menu.addSeparator()
301
302         self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
303         self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
304         self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
305
306         wallet_menu.addSeparator()
307         labels_menu = wallet_menu.addMenu(_("&Labels"))
308         labels_menu.addAction(_("&Import"), self.do_import_labels)
309         labels_menu.addAction(_("&Export"), self.do_export_labels)
310
311         self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
312         self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
313         self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
314         self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
315         wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
316
317         tools_menu = menubar.addMenu(_("&Tools"))
318
319         # Settings / Preferences are all reserved keywords in OSX using this as work around
320         tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
321         tools_menu.addAction(_("&Network"), self.run_network_dialog)
322         tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
323         tools_menu.addSeparator()
324         tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
325         #tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
326         tools_menu.addSeparator()
327
328         csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
329         csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
330         csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
331
332         raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
333         raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
334         raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
335         raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
336
337         help_menu = menubar.addMenu(_("&Help"))
338         help_menu.addAction(_("&About"), self.show_about)
339         help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
340         help_menu.addSeparator()
341         help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
342         help_menu.addAction(_("&Report Bug"), self.show_report_bug)
343
344         self.setMenuBar(menubar)
345
346     def show_about(self):
347         QMessageBox.about(self, "Electrum",
348             _("Version")+" %s" % (self.wallet.electrum_version) + "\n\n" + _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin. You do not need to perform regular backups, because your wallet can be recovered from a secret phrase that you can memorize or write on paper. Startup times are instant because it operates in conjunction with high-performance servers that handle the most complicated parts of the Bitcoin system."))
349
350     def show_report_bug(self):
351         QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
352             _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
353
354
355     def notify_transactions(self):
356         if not self.network or not self.network.is_connected():
357             return
358
359         print_error("Notifying GUI")
360         if len(self.network.pending_transactions_for_notifications) > 0:
361             # Combine the transactions if there are more then three
362             tx_amount = len(self.network.pending_transactions_for_notifications)
363             if(tx_amount >= 3):
364                 total_amount = 0
365                 for tx in self.network.pending_transactions_for_notifications:
366                     is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
367                     if(v > 0):
368                         total_amount += v
369
370                 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
371                                 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
372
373                 self.network.pending_transactions_for_notifications = []
374             else:
375               for tx in self.network.pending_transactions_for_notifications:
376                   if tx:
377                       self.network.pending_transactions_for_notifications.remove(tx)
378                       is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
379                       if(v > 0):
380                           self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
381
382     def notify(self, message):
383         self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
384
385
386
387     # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
388     def getOpenFileName(self, title, filter = ""):
389         directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
390         fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
391         if fileName and directory != os.path.dirname(fileName):
392             self.config.set_key('io_dir', os.path.dirname(fileName), True)
393         return fileName
394
395     def getSaveFileName(self, title, filename, filter = ""):
396         directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
397         path = os.path.join( directory, filename )
398         fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
399         if fileName and directory != os.path.dirname(fileName):
400             self.config.set_key('io_dir', os.path.dirname(fileName), True)
401         return fileName
402
403     def close(self):
404         QMainWindow.close(self)
405         run_hook('close_main_window')
406
407     def connect_slots(self, sender):
408         self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
409         self.previous_payto_e=''
410
411     def timer_actions(self):
412         if self.need_update.is_set():
413             self.update_wallet()
414             self.need_update.clear()
415         run_hook('timer_actions')
416
417     def format_amount(self, x, is_diff=False, whitespaces=False):
418         return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
419
420
421     def get_decimal_point(self):
422         return self.decimal_point
423
424
425     def base_unit(self):
426         assert self.decimal_point in [5,8]
427         return "BTC" if self.decimal_point == 8 else "mBTC"
428
429
430     def update_status(self):
431         if self.network is None or not self.network.is_running():
432             text = _("Offline")
433             icon = QIcon(":icons/status_disconnected.png")
434
435         elif self.network.is_connected():
436             if not self.wallet.up_to_date:
437                 text = _("Synchronizing...")
438                 icon = QIcon(":icons/status_waiting.png")
439             elif self.network.server_lag > 1:
440                 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
441                 icon = QIcon(":icons/status_lagging.png")
442             else:
443                 c, u = self.wallet.get_account_balance(self.current_account)
444                 text =  _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
445                 if u: text +=  " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
446
447                 # append fiat balance and price from exchange rate plugin
448                 r = {}
449                 run_hook('get_fiat_status_text', c+u, r)
450                 quote = r.get(0)
451                 if quote:
452                     text += "%s"%quote
453
454                 self.tray.setToolTip(text)
455                 icon = QIcon(":icons/status_connected.png")
456         else:
457             text = _("Not connected")
458             icon = QIcon(":icons/status_disconnected.png")
459
460         self.balance_label.setText(text)
461         self.status_button.setIcon( icon )
462
463
464     def update_wallet(self):
465         self.update_status()
466         if self.wallet.up_to_date or not self.network or not self.network.is_connected():
467             self.update_history_tab()
468             self.update_receive_tab()
469             self.update_contacts_tab()
470             self.update_completions()
471
472
473     def create_history_tab(self):
474         self.history_list = l = MyTreeWidget(self)
475         l.setColumnCount(5)
476         for i,width in enumerate(self.column_widths['history']):
477             l.setColumnWidth(i, width)
478         l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
479         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
480         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
481
482         l.customContextMenuRequested.connect(self.create_history_menu)
483         return l
484
485
486     def create_history_menu(self, position):
487         self.history_list.selectedIndexes()
488         item = self.history_list.currentItem()
489         be = self.config.get('block_explorer', 'Blockchain.info')
490         if be == 'Blockchain.info':
491             block_explorer = 'https://blockchain.info/tx/'
492         elif be == 'Blockr.io':
493             block_explorer = 'https://blockr.io/tx/info/'
494         elif be == 'Insight.is':
495             block_explorer = 'http://live.insight.is/tx/'
496         if not item: return
497         tx_hash = str(item.data(0, Qt.UserRole).toString())
498         if not tx_hash: return
499         menu = QMenu()
500         menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
501         menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
502         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
503         menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
504         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
505
506
507     def show_transaction(self, tx):
508         import transaction_dialog
509         d = transaction_dialog.TxDialog(tx, self)
510         d.exec_()
511
512     def tx_label_clicked(self, item, column):
513         if column==2 and item.isSelected():
514             self.is_edit=True
515             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
516             self.history_list.editItem( item, column )
517             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
518             self.is_edit=False
519
520     def tx_label_changed(self, item, column):
521         if self.is_edit:
522             return
523         self.is_edit=True
524         tx_hash = str(item.data(0, Qt.UserRole).toString())
525         tx = self.wallet.transactions.get(tx_hash)
526         text = unicode( item.text(2) )
527         self.wallet.set_label(tx_hash, text)
528         if text:
529             item.setForeground(2, QBrush(QColor('black')))
530         else:
531             text = self.wallet.get_default_label(tx_hash)
532             item.setText(2, text)
533             item.setForeground(2, QBrush(QColor('gray')))
534         self.is_edit=False
535
536
537     def edit_label(self, is_recv):
538         l = self.receive_list if is_recv else self.contacts_list
539         item = l.currentItem()
540         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
541         l.editItem( item, 1 )
542         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
543
544
545
546     def address_label_clicked(self, item, column, l, column_addr, column_label):
547         if column == column_label and item.isSelected():
548             is_editable = item.data(0, 32).toBool()
549             if not is_editable:
550                 return
551             addr = unicode( item.text(column_addr) )
552             label = unicode( item.text(column_label) )
553             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
554             l.editItem( item, column )
555             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
556
557
558     def address_label_changed(self, item, column, l, column_addr, column_label):
559         if column == column_label:
560             addr = unicode( item.text(column_addr) )
561             text = unicode( item.text(column_label) )
562             is_editable = item.data(0, 32).toBool()
563             if not is_editable:
564                 return
565
566             changed = self.wallet.set_label(addr, text)
567             if changed:
568                 self.update_history_tab()
569                 self.update_completions()
570
571             self.current_item_changed(item)
572
573         run_hook('item_changed', item, column)
574
575
576     def current_item_changed(self, a):
577         run_hook('current_item_changed', a)
578
579
580
581     def update_history_tab(self):
582
583         self.history_list.clear()
584         for item in self.wallet.get_tx_history(self.current_account):
585             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
586             time_str = _("unknown")
587             if conf > 0:
588                 try:
589                     time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
590                 except Exception:
591                     time_str = _("error")
592
593             if conf == -1:
594                 time_str = 'unverified'
595                 icon = QIcon(":icons/unconfirmed.png")
596             elif conf == 0:
597                 time_str = 'pending'
598                 icon = QIcon(":icons/unconfirmed.png")
599             elif conf < 6:
600                 icon = QIcon(":icons/clock%d.png"%conf)
601             else:
602                 icon = QIcon(":icons/confirmed.png")
603
604             if value is not None:
605                 v_str = self.format_amount(value, True, whitespaces=True)
606             else:
607                 v_str = '--'
608
609             balance_str = self.format_amount(balance, whitespaces=True)
610
611             if tx_hash:
612                 label, is_default_label = self.wallet.get_label(tx_hash)
613             else:
614                 label = _('Pruned transaction outputs')
615                 is_default_label = False
616
617             item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
618             item.setFont(2, QFont(MONOSPACE_FONT))
619             item.setFont(3, QFont(MONOSPACE_FONT))
620             item.setFont(4, QFont(MONOSPACE_FONT))
621             if value < 0:
622                 item.setForeground(3, QBrush(QColor("#BC1E1E")))
623             if tx_hash:
624                 item.setData(0, Qt.UserRole, tx_hash)
625                 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
626             if is_default_label:
627                 item.setForeground(2, QBrush(QColor('grey')))
628
629             item.setIcon(0, icon)
630             self.history_list.insertTopLevelItem(0,item)
631
632
633         self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
634         run_hook('history_tab_update')
635
636
637     def create_send_tab(self):
638         w = QWidget()
639
640         grid = QGridLayout(w)
641         grid.setSpacing(8)
642         grid.setColumnMinimumWidth(3,300)
643         grid.setColumnStretch(5,1)
644         grid.setRowStretch(8, 1)
645
646         from paytoedit import PayToEdit
647         self.amount_e = AmountEdit(self.get_decimal_point)
648         self.payto_e = PayToEdit(self.amount_e)
649         self.payto_help = 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)'))
650         grid.addWidget(QLabel(_('Pay to')), 1, 0)
651         grid.addWidget(self.payto_e, 1, 1, 1, 3)
652         grid.addWidget(self.payto_help, 1, 4)
653
654         completer = QCompleter()
655         completer.setCaseSensitivity(False)
656         self.payto_e.setCompleter(completer)
657         completer.setModel(self.completions)
658
659         self.message_e = MyLineEdit()
660         self.message_help = 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.'))
661         grid.addWidget(QLabel(_('Description')), 2, 0)
662         grid.addWidget(self.message_e, 2, 1, 1, 3)
663         grid.addWidget(self.message_help, 2, 4)
664
665         self.from_label = QLabel(_('From'))
666         grid.addWidget(self.from_label, 3, 0)
667         self.from_list = MyTreeWidget(self)
668         self.from_list.setColumnCount(2)
669         self.from_list.setColumnWidth(0, 350)
670         self.from_list.setColumnWidth(1, 50)
671         self.from_list.setHeaderHidden(True)
672         self.from_list.setMaximumHeight(80)
673         self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
674         self.from_list.customContextMenuRequested.connect(self.from_list_menu)
675         grid.addWidget(self.from_list, 3, 1, 1, 3)
676         self.set_pay_from([])
677
678         self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
679                                       + _('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.') \
680                                       + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
681         grid.addWidget(QLabel(_('Amount')), 4, 0)
682         grid.addWidget(self.amount_e, 4, 1, 1, 2)
683         grid.addWidget(self.amount_help, 4, 3)
684
685         self.fee_e = AmountEdit(self.get_decimal_point)
686         grid.addWidget(QLabel(_('Fee')), 5, 0)
687         grid.addWidget(self.fee_e, 5, 1, 1, 2)
688         grid.addWidget(HelpButton(
689                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
690                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
691                     + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 5, 3)
692
693         run_hook('exchange_rate_button', grid)
694
695         self.send_button = EnterButton(_("Send"), self.do_send)
696         grid.addWidget(self.send_button, 6, 1)
697
698         b = EnterButton(_("Clear"), self.do_clear)
699         grid.addWidget(b, 6, 2)
700
701         self.payto_sig = QLabel('')
702         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
703
704         #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
705         #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
706         w.setLayout(grid)
707
708         def entry_changed( is_fee ):
709             self.funds_error = False
710
711             if self.amount_e.is_shortcut:
712                 self.amount_e.is_shortcut = False
713                 sendable = self.get_sendable_balance()
714                 # there is only one output because we are completely spending inputs
715                 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
716                 fee = self.wallet.estimated_fee(inputs, 1)
717                 amount = total - fee
718                 self.amount_e.setText( self.format_amount(amount) )
719                 self.fee_e.setText( self.format_amount( fee ) )
720                 return
721
722             amount = self.amount_e.get_amount()
723             fee = self.fee_e.get_amount()
724
725             if not is_fee: fee = None
726             if amount is None:
727                 return
728             # assume that there will be 2 outputs (one for change)
729             inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
730             if not is_fee:
731                 self.fee_e.setText( self.format_amount( fee ) )
732             if inputs:
733                 palette = QPalette()
734                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
735                 text = ""
736             else:
737                 palette = QPalette()
738                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
739                 self.funds_error = True
740                 text = _( "Not enough funds" )
741                 c, u = self.wallet.get_frozen_balance()
742                 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
743
744             self.statusBar().showMessage(text)
745             self.amount_e.setPalette(palette)
746             self.fee_e.setPalette(palette)
747
748         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
749         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
750
751         run_hook('create_send_tab', grid)
752         return w
753
754     def from_list_delete(self, item):
755         i = self.from_list.indexOfTopLevelItem(item)
756         self.pay_from.pop(i)
757         self.redraw_from_list()
758
759     def from_list_menu(self, position):
760         item = self.from_list.itemAt(position)
761         menu = QMenu()
762         menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
763         menu.exec_(self.from_list.viewport().mapToGlobal(position))
764
765     def set_pay_from(self, domain = None):
766         self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
767         self.redraw_from_list()
768
769     def redraw_from_list(self):
770         self.from_list.clear()
771         self.from_label.setHidden(len(self.pay_from) == 0)
772         self.from_list.setHidden(len(self.pay_from) == 0)
773
774         def format(x):
775             h = x.get('prevout_hash')
776             return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
777
778         for item in self.pay_from:
779             self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
780
781     def update_completions(self):
782         l = []
783         for addr,label in self.wallet.labels.items():
784             if addr in self.wallet.addressbook:
785                 l.append( label + '  <' + addr + '>')
786
787         run_hook('update_completions', l)
788         self.completions.setStringList(l)
789
790
791     def protected(func):
792         return lambda s, *args: s.do_protect(func, args)
793
794
795     def do_send(self):
796         label = unicode( self.message_e.text() )
797
798         if self.gui_object.payment_request:
799             outputs = self.gui_object.payment_request.outputs
800         else:
801             outputs = self.payto_e.get_outputs()
802
803         if not outputs:
804             QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
805             return
806
807         for addr, x in outputs:
808             if addr is None or not bitcoin.is_address(addr):
809                 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
810                 return
811             if type(x) is not int:
812                 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
813                 return
814
815         amount = sum(map(lambda x:x[1], outputs))
816
817         try:
818             fee = self.fee_e.get_amount()
819         except Exception:
820             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
821             return
822
823         confirm_amount = self.config.get('confirm_amount', 100000000)
824         if amount >= confirm_amount:
825             o = '\n'.join(map(lambda x:x[0], outputs))
826             if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
827                 return
828             
829         confirm_fee = self.config.get('confirm_fee', 100000)
830         if fee >= confirm_fee:
831             if not self.question(_("The fee for this transaction seems unusually high.\nAre you really sure you want to pay %(fee)s in fees?")%{ 'fee' : self.format_amount(fee) + ' '+ self.base_unit()}):
832                 return
833
834         self.send_tx(outputs, fee, label)
835
836
837
838     @protected
839     def send_tx(self, outputs, fee, label, password):
840         self.send_button.setDisabled(True)
841
842         # first, create an unsigned tx 
843         coins = self.get_coins()
844         try:
845             tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
846             tx.error = None
847         except Exception as e:
848             traceback.print_exc(file=sys.stdout)
849             self.show_message(str(e))
850             self.send_button.setDisabled(False)
851             return
852
853         # call hook to see if plugin needs gui interaction
854         run_hook('send_tx', tx)
855
856         # sign the tx
857         def sign_thread():
858             time.sleep(0.1)
859             keypairs = {}
860             self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
861             self.wallet.sign_transaction(tx, keypairs, password)
862             return tx, fee, label
863
864         def sign_done(tx, fee, label):
865             if tx.error:
866                 self.show_message(tx.error)
867                 self.send_button.setDisabled(False)
868                 return
869             if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
870                 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
871                 self.send_button.setDisabled(False)
872                 return
873             if label:
874                 self.wallet.set_label(tx.hash(), label)
875
876             if not tx.is_complete() or self.config.get('show_before_broadcast'):
877                 self.show_transaction(tx)
878                 self.do_clear()
879                 self.send_button.setDisabled(False)
880                 return
881
882             self.broadcast_transaction(tx)
883
884         self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
885         self.waiting_dialog.start()
886
887
888
889     def broadcast_transaction(self, tx):
890
891         def broadcast_thread():
892             if self.gui_object.payment_request:
893                 refund_address = self.wallet.addresses()[0]
894                 status, msg = self.gui_object.payment_request.send_ack(str(tx), refund_address)
895                 self.gui_object.payment_request = None
896             else:
897                 status, msg =  self.wallet.sendtx(tx)
898             return status, msg
899
900         def broadcast_done(status, msg):
901             if status:
902                 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
903                 self.do_clear()
904             else:
905                 QMessageBox.warning(self, _('Error'), msg, _('OK'))
906             self.send_button.setDisabled(False)
907
908         self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
909         self.waiting_dialog.start()
910
911
912
913     def prepare_for_payment_request(self):
914         self.tabs.setCurrentIndex(1)
915         for e in [self.payto_e, self.amount_e, self.message_e]:
916             e.setFrozen(True)
917         for h in [self.payto_help, self.amount_help, self.message_help]:
918             h.hide()
919         self.payto_e.setText(_("please wait..."))
920         return True
921
922     def payment_request_ok(self):
923         pr = self.gui_object.payment_request
924         self.payto_help.show()
925         self.payto_help.set_alt(pr.status)
926         self.payto_e.setGreen()
927         self.payto_e.setText(pr.domain)
928         self.amount_e.setText(self.format_amount(pr.get_amount()))
929         self.message_e.setText(pr.memo)
930
931     def payment_request_error(self):
932         self.do_clear()
933         self.show_message(self.gui_object.payment_request.error)
934         self.gui_object.payment_request = None
935
936     def set_send(self, address, amount, label, message):
937
938         if label and self.wallet.labels.get(address) != label:
939             if self.question('Give label "%s" to address %s ?'%(label,address)):
940                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
941                     self.wallet.addressbook.append(address)
942                 self.wallet.set_label(address, label)
943
944         self.tabs.setCurrentIndex(1)
945         label = self.wallet.labels.get(address)
946         m_addr = label + '  <'+ address +'>' if label else address
947         self.payto_e.setText(m_addr)
948
949         self.message_e.setText(message)
950         if amount:
951             self.amount_e.setText(amount)
952
953
954     def do_clear(self):
955         self.payto_sig.setVisible(False)
956         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
957             e.setText('')
958             e.setFrozen(False)
959
960         for h in [self.payto_help, self.amount_help, self.message_help]:
961             h.show()
962
963         self.payto_help.set_alt(None)
964
965         self.set_pay_from([])
966         self.update_status()
967
968
969
970     def set_addrs_frozen(self,addrs,freeze):
971         for addr in addrs:
972             if not addr: continue
973             if addr in self.wallet.frozen_addresses and not freeze:
974                 self.wallet.unfreeze(addr)
975             elif addr not in self.wallet.frozen_addresses and freeze:
976                 self.wallet.freeze(addr)
977         self.update_receive_tab()
978
979
980
981     def create_list_tab(self, headers):
982         "generic tab creation method"
983         l = MyTreeWidget(self)
984         l.setColumnCount( len(headers) )
985         l.setHeaderLabels( headers )
986
987         w = QWidget()
988         vbox = QVBoxLayout()
989         w.setLayout(vbox)
990
991         vbox.setMargin(0)
992         vbox.setSpacing(0)
993         vbox.addWidget(l)
994         buttons = QWidget()
995         vbox.addWidget(buttons)
996
997         hbox = QHBoxLayout()
998         hbox.setMargin(0)
999         hbox.setSpacing(0)
1000         buttons.setLayout(hbox)
1001
1002         return l,w,hbox
1003
1004
1005     def create_receive_tab(self):
1006         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1007         l.setContextMenuPolicy(Qt.CustomContextMenu)
1008         l.customContextMenuRequested.connect(self.create_receive_menu)
1009         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1010         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1011         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1012         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1013         self.receive_list = l
1014         self.receive_buttons_hbox = hbox
1015         hbox.addStretch(1)
1016         return w
1017
1018
1019
1020
1021     def save_column_widths(self):
1022         self.column_widths["receive"] = []
1023         for i in range(self.receive_list.columnCount() -1):
1024             self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1025
1026         self.column_widths["history"] = []
1027         for i in range(self.history_list.columnCount() - 1):
1028             self.column_widths["history"].append(self.history_list.columnWidth(i))
1029
1030         self.column_widths["contacts"] = []
1031         for i in range(self.contacts_list.columnCount() - 1):
1032             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1033
1034         self.config.set_key("column_widths_2", self.column_widths, True)
1035
1036
1037     def create_contacts_tab(self):
1038         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1039         l.setContextMenuPolicy(Qt.CustomContextMenu)
1040         l.customContextMenuRequested.connect(self.create_contact_menu)
1041         for i,width in enumerate(self.column_widths['contacts']):
1042             l.setColumnWidth(i, width)
1043
1044         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1045         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1046         self.contacts_list = l
1047         self.contacts_buttons_hbox = hbox
1048         hbox.addStretch(1)
1049         return w
1050
1051
1052     def delete_imported_key(self, addr):
1053         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1054             self.wallet.delete_imported_key(addr)
1055             self.update_receive_tab()
1056             self.update_history_tab()
1057
1058     def edit_account_label(self, k):
1059         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1060         if ok:
1061             label = unicode(text)
1062             self.wallet.set_label(k,label)
1063             self.update_receive_tab()
1064
1065     def account_set_expanded(self, item, k, b):
1066         item.setExpanded(b)
1067         self.accounts_expanded[k] = b
1068
1069     def create_account_menu(self, position, k, item):
1070         menu = QMenu()
1071         if item.isExpanded():
1072             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1073         else:
1074             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1075         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1076         if self.wallet.seed_version > 4:
1077             menu.addAction(_("View details"), lambda: self.show_account_details(k))
1078         if self.wallet.account_is_pending(k):
1079             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1080         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1081
1082     def delete_pending_account(self, k):
1083         self.wallet.delete_pending_account(k)
1084         self.update_receive_tab()
1085
1086     def create_receive_menu(self, position):
1087         # fixme: this function apparently has a side effect.
1088         # if it is not called the menu pops up several times
1089         #self.receive_list.selectedIndexes()
1090
1091         selected = self.receive_list.selectedItems()
1092         multi_select = len(selected) > 1
1093         addrs = [unicode(item.text(0)) for item in selected]
1094         if not multi_select:
1095             item = self.receive_list.itemAt(position)
1096             if not item: return
1097
1098             addr = addrs[0]
1099             if not is_valid(addr):
1100                 k = str(item.data(0,32).toString())
1101                 if k:
1102                     self.create_account_menu(position, k, item)
1103                 else:
1104                     item.setExpanded(not item.isExpanded())
1105                 return
1106
1107         menu = QMenu()
1108         if not multi_select:
1109             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1110             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1111             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1112             menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1113             if not self.wallet.is_watching_only():
1114                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1115                 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1116                 #menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1117             if self.wallet.is_imported(addr):
1118                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1119
1120         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1121             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1122         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1123             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1124
1125         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1126             menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1127
1128         run_hook('receive_menu', menu, addrs)
1129         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1130
1131
1132     def get_sendable_balance(self):
1133         return sum(map(lambda x:x['value'], self.get_coins()))
1134
1135
1136     def get_coins(self):
1137         if self.pay_from:
1138             return self.pay_from
1139         else:
1140             domain = self.wallet.get_account_addresses(self.current_account)
1141             for i in self.wallet.frozen_addresses:
1142                 if i in domain: domain.remove(i)
1143             return self.wallet.get_unspent_coins(domain)
1144
1145
1146     def send_from_addresses(self, addrs):
1147         self.set_pay_from( addrs )
1148         self.tabs.setCurrentIndex(1)
1149
1150
1151     def payto(self, addr):
1152         if not addr: return
1153         label = self.wallet.labels.get(addr)
1154         m_addr = label + '  <' + addr + '>' if label else addr
1155         self.tabs.setCurrentIndex(1)
1156         self.payto_e.setText(m_addr)
1157         self.amount_e.setFocus()
1158
1159
1160     def delete_contact(self, x):
1161         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1162             self.wallet.delete_contact(x)
1163             self.wallet.set_label(x, None)
1164             self.update_history_tab()
1165             self.update_contacts_tab()
1166             self.update_completions()
1167
1168
1169     def create_contact_menu(self, position):
1170         item = self.contacts_list.itemAt(position)
1171         menu = QMenu()
1172         if not item:
1173             menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1174         else:
1175             addr = unicode(item.text(0))
1176             label = unicode(item.text(1))
1177             is_editable = item.data(0,32).toBool()
1178             payto_addr = item.data(0,33).toString()
1179             menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1180             menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1181             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1182             if is_editable:
1183                 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1184                 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1185
1186         run_hook('create_contact_menu', menu, item)
1187         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1188
1189
1190     def update_receive_item(self, item):
1191         item.setFont(0, QFont(MONOSPACE_FONT))
1192         address = str(item.data(0,0).toString())
1193         label = self.wallet.labels.get(address,'')
1194         item.setData(1,0,label)
1195         item.setData(0,32, True) # is editable
1196
1197         run_hook('update_receive_item', address, item)
1198
1199         if not self.wallet.is_mine(address): return
1200
1201         c, u = self.wallet.get_addr_balance(address)
1202         balance = self.format_amount(c + u)
1203         item.setData(2,0,balance)
1204
1205         if address in self.wallet.frozen_addresses:
1206             item.setBackgroundColor(0, QColor('lightblue'))
1207
1208
1209     def update_receive_tab(self):
1210         l = self.receive_list
1211         # extend the syntax for consistency
1212         l.addChild = l.addTopLevelItem
1213         l.insertChild = l.insertTopLevelItem
1214
1215         l.clear()
1216         for i,width in enumerate(self.column_widths['receive']):
1217             l.setColumnWidth(i, width)
1218
1219         accounts = self.wallet.get_accounts()
1220         if self.current_account is None:
1221             account_items = sorted(accounts.items())
1222         else:
1223             account_items = [(self.current_account, accounts.get(self.current_account))]
1224
1225
1226         for k, account in account_items:
1227
1228             if len(accounts) > 1:
1229                 name = self.wallet.get_account_name(k)
1230                 c,u = self.wallet.get_account_balance(k)
1231                 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1232                 l.addTopLevelItem(account_item)
1233                 account_item.setExpanded(self.accounts_expanded.get(k, True))
1234                 account_item.setData(0, 32, k)
1235             else:
1236                 account_item = l
1237
1238             sequences = [0,1] if account.has_change() else [0]
1239             for is_change in sequences:
1240                 if len(sequences) > 1:
1241                     name = _("Receiving") if not is_change else _("Change")
1242                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1243                     account_item.addChild(seq_item)
1244                     if not is_change: 
1245                         seq_item.setExpanded(True)
1246                 else:
1247                     seq_item = account_item
1248                     
1249                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1250                 used_flag = False
1251
1252                 is_red = False
1253                 gap = 0
1254
1255                 for address in account.get_addresses(is_change):
1256
1257                     num, is_used = self.wallet.is_used(address)
1258                     if num == 0:
1259                         gap += 1
1260                         if gap > self.wallet.gap_limit:
1261                             is_red = True
1262                     else:
1263                         gap = 0
1264
1265                     item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1266                     self.update_receive_item(item)
1267                     if is_red:
1268                         item.setBackgroundColor(1, QColor('red'))
1269
1270                     if is_used:
1271                         if not used_flag:
1272                             seq_item.insertChild(0,used_item)
1273                             used_flag = True
1274                         used_item.addChild(item)
1275                     else:
1276                         seq_item.addChild(item)
1277
1278         # we use column 1 because column 0 may be hidden
1279         l.setCurrentItem(l.topLevelItem(0),1)
1280
1281
1282     def update_contacts_tab(self):
1283         l = self.contacts_list
1284         l.clear()
1285
1286         for address in self.wallet.addressbook:
1287             label = self.wallet.labels.get(address,'')
1288             n = self.wallet.get_num_tx(address)
1289             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1290             item.setFont(0, QFont(MONOSPACE_FONT))
1291             # 32 = label can be edited (bool)
1292             item.setData(0,32, True)
1293             # 33 = payto string
1294             item.setData(0,33, address)
1295             l.addTopLevelItem(item)
1296
1297         run_hook('update_contacts_tab', l)
1298         l.setCurrentItem(l.topLevelItem(0))
1299
1300
1301
1302     def create_console_tab(self):
1303         from console import Console
1304         self.console = console = Console()
1305         return console
1306
1307
1308     def update_console(self):
1309         console = self.console
1310         console.history = self.config.get("console-history",[])
1311         console.history_index = len(console.history)
1312
1313         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1314         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1315
1316         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1317         methods = {}
1318         def mkfunc(f, method):
1319             return lambda *args: apply( f, (method, args, self.password_dialog ))
1320         for m in dir(c):
1321             if m[0]=='_' or m in ['network','wallet']: continue
1322             methods[m] = mkfunc(c._run, m)
1323
1324         console.updateNamespace(methods)
1325
1326
1327     def change_account(self,s):
1328         if s == _("All accounts"):
1329             self.current_account = None
1330         else:
1331             accounts = self.wallet.get_account_names()
1332             for k, v in accounts.items():
1333                 if v == s:
1334                     self.current_account = k
1335         self.update_history_tab()
1336         self.update_status()
1337         self.update_receive_tab()
1338
1339     def create_status_bar(self):
1340
1341         sb = QStatusBar()
1342         sb.setFixedHeight(35)
1343         qtVersion = qVersion()
1344
1345         self.balance_label = QLabel("")
1346         sb.addWidget(self.balance_label)
1347
1348         from version_getter import UpdateLabel
1349         self.updatelabel = UpdateLabel(self.config, sb)
1350
1351         self.account_selector = QComboBox()
1352         self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1353         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1354         sb.addPermanentWidget(self.account_selector)
1355
1356         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1357             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1358
1359         self.lock_icon = QIcon()
1360         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1361         sb.addPermanentWidget( self.password_button )
1362
1363         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1364         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1365         sb.addPermanentWidget( self.seed_button )
1366         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1367         sb.addPermanentWidget( self.status_button )
1368
1369         run_hook('create_status_bar', (sb,))
1370
1371         self.setStatusBar(sb)
1372
1373
1374     def update_lock_icon(self):
1375         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1376         self.password_button.setIcon( icon )
1377
1378
1379     def update_buttons_on_seed(self):
1380         if self.wallet.has_seed():
1381            self.seed_button.show()
1382         else:
1383            self.seed_button.hide()
1384
1385         if not self.wallet.is_watching_only():
1386            self.password_button.show()
1387            self.send_button.setText(_("Send"))
1388         else:
1389            self.password_button.hide()
1390            self.send_button.setText(_("Create unsigned transaction"))
1391
1392
1393     def change_password_dialog(self):
1394         from password_dialog import PasswordDialog
1395         d = PasswordDialog(self.wallet, self)
1396         d.run()
1397         self.update_lock_icon()
1398
1399
1400     def new_contact_dialog(self):
1401
1402         d = QDialog(self)
1403         d.setWindowTitle(_("New Contact"))
1404         vbox = QVBoxLayout(d)
1405         vbox.addWidget(QLabel(_('New Contact')+':'))
1406
1407         grid = QGridLayout()
1408         line1 = QLineEdit()
1409         line2 = QLineEdit()
1410         grid.addWidget(QLabel(_("Address")), 1, 0)
1411         grid.addWidget(line1, 1, 1)
1412         grid.addWidget(QLabel(_("Name")), 2, 0)
1413         grid.addWidget(line2, 2, 1)
1414
1415         vbox.addLayout(grid)
1416         vbox.addLayout(ok_cancel_buttons(d))
1417
1418         if not d.exec_():
1419             return
1420
1421         address = str(line1.text())
1422         label = unicode(line2.text())
1423
1424         if not is_valid(address):
1425             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1426             return
1427
1428         self.wallet.add_contact(address)
1429         if label:
1430             self.wallet.set_label(address, label)
1431
1432         self.update_contacts_tab()
1433         self.update_history_tab()
1434         self.update_completions()
1435         self.tabs.setCurrentIndex(3)
1436
1437
1438     @protected
1439     def new_account_dialog(self, password):
1440
1441         dialog = QDialog(self)
1442         dialog.setModal(1)
1443         dialog.setWindowTitle(_("New Account"))
1444
1445         vbox = QVBoxLayout()
1446         vbox.addWidget(QLabel(_('Account name')+':'))
1447         e = QLineEdit()
1448         vbox.addWidget(e)
1449         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1450             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1451         l = QLabel(msg)
1452         l.setWordWrap(True)
1453         vbox.addWidget(l)
1454
1455         vbox.addLayout(ok_cancel_buttons(dialog))
1456         dialog.setLayout(vbox)
1457         r = dialog.exec_()
1458         if not r: return
1459
1460         name = str(e.text())
1461         if not name: return
1462
1463         self.wallet.create_pending_account(name, password)
1464         self.update_receive_tab()
1465         self.tabs.setCurrentIndex(2)
1466
1467
1468
1469
1470     def show_master_public_keys(self):
1471
1472         dialog = QDialog(self)
1473         dialog.setModal(1)
1474         dialog.setWindowTitle(_("Master Public Keys"))
1475
1476         main_layout = QGridLayout()
1477         mpk_dict = self.wallet.get_master_public_keys()
1478         i = 0
1479         for key, value in mpk_dict.items():
1480             main_layout.addWidget(QLabel(key), i, 0)
1481             mpk_text = QTextEdit()
1482             mpk_text.setReadOnly(True)
1483             mpk_text.setMaximumHeight(170)
1484             mpk_text.setText(value)
1485             main_layout.addWidget(mpk_text, i + 1, 0)
1486             i += 2
1487
1488         vbox = QVBoxLayout()
1489         vbox.addLayout(main_layout)
1490         vbox.addLayout(close_button(dialog))
1491
1492         dialog.setLayout(vbox)
1493         dialog.exec_()
1494
1495
1496     @protected
1497     def show_seed_dialog(self, password):
1498         if not self.wallet.has_seed():
1499             QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1500             return
1501
1502         try:
1503             mnemonic = self.wallet.get_mnemonic(password)
1504         except Exception:
1505             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1506             return
1507         from seed_dialog import SeedDialog
1508         d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1509         d.exec_()
1510
1511
1512
1513     def show_qrcode(self, data, title = _("QR code")):
1514         if not data: return
1515         d = QDialog(self)
1516         d.setModal(1)
1517         d.setWindowTitle(title)
1518         d.setMinimumSize(270, 300)
1519         vbox = QVBoxLayout()
1520         qrw = QRCodeWidget(data)
1521         vbox.addWidget(qrw, 1)
1522         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1523         hbox = QHBoxLayout()
1524         hbox.addStretch(1)
1525
1526         filename = os.path.join(self.config.path, "qrcode.bmp")
1527
1528         def print_qr():
1529             bmp.save_qrcode(qrw.qr, filename)
1530             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1531
1532         def copy_to_clipboard():
1533             bmp.save_qrcode(qrw.qr, filename)
1534             self.app.clipboard().setImage(QImage(filename))
1535             QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1536
1537         b = QPushButton(_("Copy"))
1538         hbox.addWidget(b)
1539         b.clicked.connect(copy_to_clipboard)
1540
1541         b = QPushButton(_("Save"))
1542         hbox.addWidget(b)
1543         b.clicked.connect(print_qr)
1544
1545         b = QPushButton(_("Close"))
1546         hbox.addWidget(b)
1547         b.clicked.connect(d.accept)
1548         b.setDefault(True)
1549
1550         vbox.addLayout(hbox)
1551         d.setLayout(vbox)
1552         d.exec_()
1553
1554
1555     def do_protect(self, func, args):
1556         if self.wallet.use_encryption:
1557             password = self.password_dialog()
1558             if not password:
1559                 return
1560         else:
1561             password = None
1562
1563         if args != (False,):
1564             args = (self,) + args + (password,)
1565         else:
1566             args = (self,password)
1567         apply( func, args)
1568
1569
1570     def show_public_keys(self, address):
1571         if not address: return
1572         try:
1573             pubkey_list = self.wallet.get_public_keys(address)
1574         except Exception as e:
1575             traceback.print_exc(file=sys.stdout)
1576             self.show_message(str(e))
1577             return
1578
1579         d = QDialog(self)
1580         d.setMinimumSize(600, 200)
1581         d.setModal(1)
1582         vbox = QVBoxLayout()
1583         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1584         vbox.addWidget( QLabel(_("Public key") + ':'))
1585         keys = QTextEdit()
1586         keys.setReadOnly(True)
1587         keys.setText('\n'.join(pubkey_list))
1588         vbox.addWidget(keys)
1589         #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1590         vbox.addLayout(close_button(d))
1591         d.setLayout(vbox)
1592         d.exec_()
1593
1594     @protected
1595     def show_private_key(self, address, password):
1596         if not address: return
1597         try:
1598             pk_list = self.wallet.get_private_key(address, password)
1599         except Exception as e:
1600             traceback.print_exc(file=sys.stdout)
1601             self.show_message(str(e))
1602             return
1603
1604         d = QDialog(self)
1605         d.setMinimumSize(600, 200)
1606         d.setModal(1)
1607         vbox = QVBoxLayout()
1608         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1609         vbox.addWidget( QLabel(_("Private key") + ':'))
1610         keys = QTextEdit()
1611         keys.setReadOnly(True)
1612         keys.setText('\n'.join(pk_list))
1613         vbox.addWidget(keys)
1614         vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1615         vbox.addLayout(close_button(d))
1616         d.setLayout(vbox)
1617         d.exec_()
1618
1619
1620     @protected
1621     def do_sign(self, address, message, signature, password):
1622         message = unicode(message.toPlainText())
1623         message = message.encode('utf-8')
1624         try:
1625             sig = self.wallet.sign_message(str(address.text()), message, password)
1626             signature.setText(sig)
1627         except Exception as e:
1628             self.show_message(str(e))
1629
1630     def do_verify(self, address, message, signature):
1631         message = unicode(message.toPlainText())
1632         message = message.encode('utf-8')
1633         if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1634             self.show_message(_("Signature verified"))
1635         else:
1636             self.show_message(_("Error: wrong signature"))
1637
1638
1639     def sign_verify_message(self, address=''):
1640         d = QDialog(self)
1641         d.setModal(1)
1642         d.setWindowTitle(_('Sign/verify Message'))
1643         d.setMinimumSize(410, 290)
1644
1645         layout = QGridLayout(d)
1646
1647         message_e = QTextEdit()
1648         layout.addWidget(QLabel(_('Message')), 1, 0)
1649         layout.addWidget(message_e, 1, 1)
1650         layout.setRowStretch(2,3)
1651
1652         address_e = QLineEdit()
1653         address_e.setText(address)
1654         layout.addWidget(QLabel(_('Address')), 2, 0)
1655         layout.addWidget(address_e, 2, 1)
1656
1657         signature_e = QTextEdit()
1658         layout.addWidget(QLabel(_('Signature')), 3, 0)
1659         layout.addWidget(signature_e, 3, 1)
1660         layout.setRowStretch(3,1)
1661
1662         hbox = QHBoxLayout()
1663
1664         b = QPushButton(_("Sign"))
1665         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1666         hbox.addWidget(b)
1667
1668         b = QPushButton(_("Verify"))
1669         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1670         hbox.addWidget(b)
1671
1672         b = QPushButton(_("Close"))
1673         b.clicked.connect(d.accept)
1674         hbox.addWidget(b)
1675         layout.addLayout(hbox, 4, 1)
1676         d.exec_()
1677
1678
1679     @protected
1680     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1681         try:
1682             decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1683             message_e.setText(decrypted)
1684         except Exception as e:
1685             self.show_message(str(e))
1686
1687
1688     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1689         message = unicode(message_e.toPlainText())
1690         message = message.encode('utf-8')
1691         try:
1692             encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1693             encrypted_e.setText(encrypted)
1694         except Exception as e:
1695             self.show_message(str(e))
1696
1697
1698
1699     def encrypt_message(self, address = ''):
1700         d = QDialog(self)
1701         d.setModal(1)
1702         d.setWindowTitle(_('Encrypt/decrypt Message'))
1703         d.setMinimumSize(610, 490)
1704
1705         layout = QGridLayout(d)
1706
1707         message_e = QTextEdit()
1708         layout.addWidget(QLabel(_('Message')), 1, 0)
1709         layout.addWidget(message_e, 1, 1)
1710         layout.setRowStretch(2,3)
1711
1712         pubkey_e = QLineEdit()
1713         if address:
1714             pubkey = self.wallet.getpubkeys(address)[0]
1715             pubkey_e.setText(pubkey)
1716         layout.addWidget(QLabel(_('Public key')), 2, 0)
1717         layout.addWidget(pubkey_e, 2, 1)
1718
1719         encrypted_e = QTextEdit()
1720         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1721         layout.addWidget(encrypted_e, 3, 1)
1722         layout.setRowStretch(3,1)
1723
1724         hbox = QHBoxLayout()
1725         b = QPushButton(_("Encrypt"))
1726         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1727         hbox.addWidget(b)
1728
1729         b = QPushButton(_("Decrypt"))
1730         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1731         hbox.addWidget(b)
1732
1733         b = QPushButton(_("Close"))
1734         b.clicked.connect(d.accept)
1735         hbox.addWidget(b)
1736
1737         layout.addLayout(hbox, 4, 1)
1738         d.exec_()
1739
1740
1741     def question(self, msg):
1742         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1743
1744     def show_message(self, msg):
1745         QMessageBox.information(self, _('Message'), msg, _('OK'))
1746
1747     def password_dialog(self, msg=None):
1748         d = QDialog(self)
1749         d.setModal(1)
1750         d.setWindowTitle(_("Enter Password"))
1751
1752         pw = QLineEdit()
1753         pw.setEchoMode(2)
1754
1755         vbox = QVBoxLayout()
1756         if not msg:
1757             msg = _('Please enter your password')
1758         vbox.addWidget(QLabel(msg))
1759
1760         grid = QGridLayout()
1761         grid.setSpacing(8)
1762         grid.addWidget(QLabel(_('Password')), 1, 0)
1763         grid.addWidget(pw, 1, 1)
1764         vbox.addLayout(grid)
1765
1766         vbox.addLayout(ok_cancel_buttons(d))
1767         d.setLayout(vbox)
1768
1769         run_hook('password_dialog', pw, grid, 1)
1770         if not d.exec_(): return
1771         return unicode(pw.text())
1772
1773
1774
1775
1776
1777
1778
1779
1780     def tx_from_text(self, txt):
1781         "json or raw hexadecimal"
1782         try:
1783             txt.decode('hex')
1784             tx = Transaction(txt)
1785             return tx
1786         except Exception:
1787             pass
1788
1789         try:
1790             tx_dict = json.loads(str(txt))
1791             assert "hex" in tx_dict.keys()
1792             tx = Transaction(tx_dict["hex"])
1793             if tx_dict.has_key("input_info"):
1794                 input_info = json.loads(tx_dict['input_info'])
1795                 tx.add_input_info(input_info)
1796             return tx
1797         except Exception:
1798             traceback.print_exc(file=sys.stdout)
1799             pass
1800
1801         QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1802
1803
1804
1805     def read_tx_from_file(self):
1806         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1807         if not fileName:
1808             return
1809         try:
1810             with open(fileName, "r") as f:
1811                 file_content = f.read()
1812         except (ValueError, IOError, os.error), reason:
1813             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1814
1815         return self.tx_from_text(file_content)
1816
1817
1818     @protected
1819     def sign_raw_transaction(self, tx, input_info, password):
1820         self.wallet.signrawtransaction(tx, input_info, [], password)
1821
1822     def do_process_from_text(self):
1823         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1824         if not text:
1825             return
1826         tx = self.tx_from_text(text)
1827         if tx:
1828             self.show_transaction(tx)
1829
1830     def do_process_from_file(self):
1831         tx = self.read_tx_from_file()
1832         if tx:
1833             self.show_transaction(tx)
1834
1835     def do_process_from_txid(self):
1836         from electrum import transaction
1837         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1838         if ok and txid:
1839             r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1840             if r:
1841                 tx = transaction.Transaction(r)
1842                 if tx:
1843                     self.show_transaction(tx)
1844                 else:
1845                     self.show_message("unknown transaction")
1846
1847     def do_process_from_csvReader(self, csvReader):
1848         outputs = []
1849         errors = []
1850         errtext = ""
1851         try:
1852             for position, row in enumerate(csvReader):
1853                 address = row[0]
1854                 if not is_valid(address):
1855                     errors.append((position, address))
1856                     continue
1857                 amount = Decimal(row[1])
1858                 amount = int(100000000*amount)
1859                 outputs.append((address, amount))
1860         except (ValueError, IOError, os.error), reason:
1861             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1862             return
1863         if errors != []:
1864             for x in errors:
1865                 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1866             QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1867             return
1868
1869         try:
1870             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1871         except Exception as e:
1872             self.show_message(str(e))
1873             return
1874
1875         self.show_transaction(tx)
1876
1877     def do_process_from_csv_file(self):
1878         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1879         if not fileName:
1880             return
1881         try:
1882             with open(fileName, "r") as f:
1883                 csvReader = csv.reader(f)
1884                 self.do_process_from_csvReader(csvReader)
1885         except (ValueError, IOError, os.error), reason:
1886             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1887             return
1888
1889     def do_process_from_csv_text(self):
1890         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1891                                + _("Format: address, amount. One output per line"), _("Load CSV"))
1892         if not text:
1893             return
1894         f = StringIO.StringIO(text)
1895         csvReader = csv.reader(f)
1896         self.do_process_from_csvReader(csvReader)
1897
1898
1899
1900     @protected
1901     def export_privkeys_dialog(self, password):
1902         if self.wallet.is_watching_only():
1903             self.show_message(_("This is a watching-only wallet"))
1904             return
1905
1906         d = QDialog(self)
1907         d.setWindowTitle(_('Private keys'))
1908         d.setMinimumSize(850, 300)
1909         vbox = QVBoxLayout(d)
1910
1911         msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."), 
1912                               _("Exposing a single private key can compromise your entire wallet!"), 
1913                               _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
1914         vbox.addWidget(QLabel(msg))
1915
1916         e = QTextEdit()
1917         e.setReadOnly(True)
1918         vbox.addWidget(e)
1919
1920         defaultname = 'electrum-private-keys.csv'
1921         select_msg = _('Select file to export your private keys to')
1922         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
1923         vbox.addLayout(hbox)
1924
1925         h, b = ok_cancel_buttons2(d, _('Export'))
1926         b.setEnabled(False)
1927         vbox.addLayout(h)
1928
1929         private_keys = {}
1930         addresses = self.wallet.addresses(True)
1931         done = False
1932         def privkeys_thread():
1933             for addr in addresses:
1934                 time.sleep(0.1)
1935                 if done: 
1936                     break
1937                 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
1938                 d.emit(SIGNAL('computing_privkeys'))
1939             d.emit(SIGNAL('show_privkeys'))
1940
1941         def show_privkeys():
1942             s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
1943             e.setText(s)
1944             b.setEnabled(True)
1945
1946         d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
1947         d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
1948         threading.Thread(target=privkeys_thread).start()
1949
1950         if not d.exec_():
1951             done = True
1952             return
1953
1954         filename = filename_e.text()
1955         if not filename:
1956             return
1957
1958         try:
1959             self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
1960         except (IOError, os.error), reason:
1961             export_error_label = _("Electrum was unable to produce a private key-export.")
1962             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1963
1964         except Exception as e:
1965             self.show_message(str(e))
1966             return
1967
1968         self.show_message(_("Private keys exported."))
1969
1970
1971     def do_export_privkeys(self, fileName, pklist, is_csv):
1972         with open(fileName, "w+") as f:
1973             if is_csv:
1974                 transaction = csv.writer(f)
1975                 transaction.writerow(["address", "private_key"])
1976                 for addr, pk in pklist.items():
1977                     transaction.writerow(["%34s"%addr,pk])
1978             else:
1979                 import json
1980                 f.write(json.dumps(pklist, indent = 4))
1981
1982
1983     def do_import_labels(self):
1984         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1985         if not labelsFile: return
1986         try:
1987             f = open(labelsFile, 'r')
1988             data = f.read()
1989             f.close()
1990             for key, value in json.loads(data).items():
1991                 self.wallet.set_label(key, value)
1992             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1993         except (IOError, os.error), reason:
1994             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1995
1996
1997     def do_export_labels(self):
1998         labels = self.wallet.labels
1999         try:
2000             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2001             if fileName:
2002                 with open(fileName, 'w+') as f:
2003                     json.dump(labels, f)
2004                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2005         except (IOError, os.error), reason:
2006             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2007
2008
2009     def export_history_dialog(self):
2010
2011         d = QDialog(self)
2012         d.setWindowTitle(_('Export History'))
2013         d.setMinimumSize(400, 200)
2014         vbox = QVBoxLayout(d)
2015
2016         defaultname = os.path.expanduser('~/electrum-history.csv')
2017         select_msg = _('Select file to export your wallet transactions to')
2018
2019         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2020         vbox.addLayout(hbox)
2021
2022         vbox.addStretch(1)
2023
2024         h, b = ok_cancel_buttons2(d, _('Export'))
2025         vbox.addLayout(h)
2026         if not d.exec_():
2027             return
2028
2029         filename = filename_e.text()
2030         if not filename:
2031             return
2032
2033         try:
2034             self.do_export_history(self.wallet, filename, csv_button.isChecked())
2035         except (IOError, os.error), reason:
2036             export_error_label = _("Electrum was unable to produce a transaction export.")
2037             QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2038             return
2039
2040         QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2041
2042
2043     def do_export_history(self, wallet, fileName, is_csv):
2044         history = wallet.get_tx_history()
2045         lines = []
2046         for item in history:
2047             tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2048             if confirmations:
2049                 if timestamp is not None:
2050                     try:
2051                         time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2052                     except [RuntimeError, TypeError, NameError] as reason:
2053                         time_string = "unknown"
2054                         pass
2055                 else:
2056                     time_string = "unknown"
2057             else:
2058                 time_string = "pending"
2059
2060             if value is not None:
2061                 value_string = format_satoshis(value, True)
2062             else:
2063                 value_string = '--'
2064
2065             if fee is not None:
2066                 fee_string = format_satoshis(fee, True)
2067             else:
2068                 fee_string = '0'
2069
2070             if tx_hash:
2071                 label, is_default_label = wallet.get_label(tx_hash)
2072                 label = label.encode('utf-8')
2073             else:
2074                 label = ""
2075
2076             balance_string = format_satoshis(balance, False)
2077             if is_csv:
2078                 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2079             else:
2080                 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2081
2082         with open(fileName, "w+") as f:
2083             if is_csv:
2084                 transaction = csv.writer(f)
2085                 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2086                 for line in lines:
2087                     transaction.writerow(line)
2088             else:
2089                 import json
2090                 f.write(json.dumps(lines, indent = 4))
2091
2092
2093     def sweep_key_dialog(self):
2094         d = QDialog(self)
2095         d.setWindowTitle(_('Sweep private keys'))
2096         d.setMinimumSize(600, 300)
2097
2098         vbox = QVBoxLayout(d)
2099         vbox.addWidget(QLabel(_("Enter private keys")))
2100
2101         keys_e = QTextEdit()
2102         keys_e.setTabChangesFocus(True)
2103         vbox.addWidget(keys_e)
2104
2105         h, address_e = address_field(self.wallet.addresses())
2106         vbox.addLayout(h)
2107
2108         vbox.addStretch(1)
2109         hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2110         vbox.addLayout(hbox)
2111         button.setEnabled(False)
2112
2113         def get_address():
2114             addr = str(address_e.text())
2115             if bitcoin.is_address(addr):
2116                 return addr
2117
2118         def get_pk():
2119             pk = str(keys_e.toPlainText()).strip()
2120             if Wallet.is_private_key(pk):
2121                 return pk.split()
2122
2123         f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2124         keys_e.textChanged.connect(f)
2125         address_e.textChanged.connect(f)
2126         if not d.exec_():
2127             return
2128
2129         fee = self.wallet.fee
2130         tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2131         self.show_transaction(tx)
2132
2133
2134     @protected
2135     def do_import_privkey(self, password):
2136         if not self.wallet.has_imported_keys():
2137             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2138                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2139                                          + _('Are you sure you understand what you are doing?'), 3, 4)
2140             if r == 4: return
2141
2142         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2143         if not text: return
2144
2145         text = str(text).split()
2146         badkeys = []
2147         addrlist = []
2148         for key in text:
2149             try:
2150                 addr = self.wallet.import_key(key, password)
2151             except Exception as e:
2152                 badkeys.append(key)
2153                 continue
2154             if not addr:
2155                 badkeys.append(key)
2156             else:
2157                 addrlist.append(addr)
2158         if addrlist:
2159             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2160         if badkeys:
2161             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2162         self.update_receive_tab()
2163         self.update_history_tab()
2164
2165
2166     def settings_dialog(self):
2167         d = QDialog(self)
2168         d.setWindowTitle(_('Electrum Settings'))
2169         d.setModal(1)
2170         vbox = QVBoxLayout()
2171         grid = QGridLayout()
2172         grid.setColumnStretch(0,1)
2173
2174         nz_label = QLabel(_('Display zeros') + ':')
2175         grid.addWidget(nz_label, 0, 0)
2176         nz_e = AmountEdit(None,True)
2177         nz_e.setText("%d"% self.num_zeros)
2178         grid.addWidget(nz_e, 0, 1)
2179         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2180         grid.addWidget(HelpButton(msg), 0, 2)
2181         if not self.config.is_modifiable('num_zeros'):
2182             for w in [nz_e, nz_label]: w.setEnabled(False)
2183
2184         lang_label=QLabel(_('Language') + ':')
2185         grid.addWidget(lang_label, 1, 0)
2186         lang_combo = QComboBox()
2187         from electrum.i18n import languages
2188         lang_combo.addItems(languages.values())
2189         try:
2190             index = languages.keys().index(self.config.get("language",''))
2191         except Exception:
2192             index = 0
2193         lang_combo.setCurrentIndex(index)
2194         grid.addWidget(lang_combo, 1, 1)
2195         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2196         if not self.config.is_modifiable('language'):
2197             for w in [lang_combo, lang_label]: w.setEnabled(False)
2198
2199
2200         fee_label = QLabel(_('Transaction fee') + ':')
2201         grid.addWidget(fee_label, 2, 0)
2202         fee_e = AmountEdit(self.get_decimal_point)
2203         fee_e.setText(self.format_amount(self.wallet.fee).strip())
2204         grid.addWidget(fee_e, 2, 1)
2205         msg = _('Fee per kilobyte of transaction.') + ' ' \
2206             + _('Recommended value') + ': ' + self.format_amount(20000)
2207         grid.addWidget(HelpButton(msg), 2, 2)
2208         if not self.config.is_modifiable('fee_per_kb'):
2209             for w in [fee_e, fee_label]: w.setEnabled(False)
2210
2211         units = ['BTC', 'mBTC']
2212         unit_label = QLabel(_('Base unit') + ':')
2213         grid.addWidget(unit_label, 3, 0)
2214         unit_combo = QComboBox()
2215         unit_combo.addItems(units)
2216         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2217         grid.addWidget(unit_combo, 3, 1)
2218         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2219                                              + '\n1BTC=1000mBTC.\n' \
2220                                              + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2221
2222         usechange_cb = QCheckBox(_('Use change addresses'))
2223         usechange_cb.setChecked(self.wallet.use_change)
2224         grid.addWidget(usechange_cb, 4, 0)
2225         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2226         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2227
2228         block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2229         block_ex_label = QLabel(_('Online Block Explorer') + ':')
2230         grid.addWidget(block_ex_label, 5, 0)
2231         block_ex_combo = QComboBox()
2232         block_ex_combo.addItems(block_explorers)
2233         block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2234         grid.addWidget(block_ex_combo, 5, 1)
2235         grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2236
2237         show_tx = self.config.get('show_before_broadcast', False)
2238         showtx_cb = QCheckBox(_('Show before broadcast'))
2239         showtx_cb.setChecked(show_tx)
2240         grid.addWidget(showtx_cb, 6, 0)
2241         grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2242
2243         vbox.addLayout(grid)
2244         vbox.addStretch(1)
2245         vbox.addLayout(ok_cancel_buttons(d))
2246         d.setLayout(vbox)
2247
2248         # run the dialog
2249         if not d.exec_(): return
2250
2251         try:
2252             fee = self.fee_e.get_amount()
2253         except Exception:
2254             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2255             return
2256
2257         self.wallet.set_fee(fee)
2258
2259         nz = unicode(nz_e.text())
2260         try:
2261             nz = int( nz )
2262             if nz>8: nz=8
2263         except Exception:
2264             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2265             return
2266
2267         if self.num_zeros != nz:
2268             self.num_zeros = nz
2269             self.config.set_key('num_zeros', nz, True)
2270             self.update_history_tab()
2271             self.update_receive_tab()
2272
2273         usechange_result = usechange_cb.isChecked()
2274         if self.wallet.use_change != usechange_result:
2275             self.wallet.use_change = usechange_result
2276             self.wallet.storage.put('use_change', self.wallet.use_change)
2277
2278         if showtx_cb.isChecked() != show_tx:
2279             self.config.set_key('show_before_broadcast', not show_tx)
2280
2281         unit_result = units[unit_combo.currentIndex()]
2282         if self.base_unit() != unit_result:
2283             self.decimal_point = 8 if unit_result == 'BTC' else 5
2284             self.config.set_key('decimal_point', self.decimal_point, True)
2285             self.update_history_tab()
2286             self.update_status()
2287
2288         need_restart = False
2289
2290         lang_request = languages.keys()[lang_combo.currentIndex()]
2291         if lang_request != self.config.get('language'):
2292             self.config.set_key("language", lang_request, True)
2293             need_restart = True
2294
2295         be_result = block_explorers[block_ex_combo.currentIndex()]
2296         self.config.set_key('block_explorer', be_result, True)
2297
2298         run_hook('close_settings_dialog')
2299
2300         if need_restart:
2301             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2302
2303
2304     def run_network_dialog(self):
2305         if not self.network:
2306             return
2307         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2308
2309     def closeEvent(self, event):
2310         self.tray.hide()
2311         self.config.set_key("is_maximized", self.isMaximized())
2312         if not self.isMaximized():
2313             g = self.geometry()
2314             self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2315         self.save_column_widths()
2316         self.config.set_key("console-history", self.console.history[-50:], True)
2317         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2318         event.accept()
2319
2320
2321     def plugins_dialog(self):
2322         from electrum.plugins import plugins
2323
2324         d = QDialog(self)
2325         d.setWindowTitle(_('Electrum Plugins'))
2326         d.setModal(1)
2327
2328         vbox = QVBoxLayout(d)
2329
2330         # plugins
2331         scroll = QScrollArea()
2332         scroll.setEnabled(True)
2333         scroll.setWidgetResizable(True)
2334         scroll.setMinimumSize(400,250)
2335         vbox.addWidget(scroll)
2336
2337         w = QWidget()
2338         scroll.setWidget(w)
2339         w.setMinimumHeight(len(plugins)*35)
2340
2341         grid = QGridLayout()
2342         grid.setColumnStretch(0,1)
2343         w.setLayout(grid)
2344
2345         def do_toggle(cb, p, w):
2346             r = p.toggle()
2347             cb.setChecked(r)
2348             if w: w.setEnabled(r)
2349
2350         def mk_toggle(cb, p, w):
2351             return lambda: do_toggle(cb,p,w)
2352
2353         for i, p in enumerate(plugins):
2354             try:
2355                 cb = QCheckBox(p.fullname())
2356                 cb.setDisabled(not p.is_available())
2357                 cb.setChecked(p.is_enabled())
2358                 grid.addWidget(cb, i, 0)
2359                 if p.requires_settings():
2360                     w = p.settings_widget(self)
2361                     w.setEnabled( p.is_enabled() )
2362                     grid.addWidget(w, i, 1)
2363                 else:
2364                     w = None
2365                 cb.clicked.connect(mk_toggle(cb,p,w))
2366                 grid.addWidget(HelpButton(p.description()), i, 2)
2367             except Exception:
2368                 print_msg(_("Error: cannot display plugin"), p)
2369                 traceback.print_exc(file=sys.stdout)
2370         grid.setRowStretch(i+1,1)
2371
2372         vbox.addLayout(close_button(d))
2373
2374         d.exec_()
2375
2376
2377     def show_account_details(self, k):
2378         account = self.wallet.accounts[k]
2379
2380         d = QDialog(self)
2381         d.setWindowTitle(_('Account Details'))
2382         d.setModal(1)
2383
2384         vbox = QVBoxLayout(d)
2385         name = self.wallet.get_account_name(k)
2386         label = QLabel('Name: ' + name)
2387         vbox.addWidget(label)
2388
2389         vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2390
2391         vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2392
2393         vbox.addWidget(QLabel(_('Master Public Key:')))
2394
2395         text = QTextEdit()
2396         text.setReadOnly(True)
2397         text.setMaximumHeight(170)
2398         vbox.addWidget(text)
2399
2400         mpk_text = '\n'.join( account.get_master_pubkeys() )
2401         text.setText(mpk_text)
2402
2403         vbox.addLayout(close_button(d))
2404         d.exec_()