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