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