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