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