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