minor fixes
[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         if not URI:
984             return
985         address, amount, label, message, request_url = util.parse_URI(URI)
986         try:
987             address, amount, label, message, request_url = util.parse_URI(URI)
988         except Exception as e:
989             QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
990             return
991
992         self.tabs.setCurrentIndex(1)
993
994         if not request_url:
995             if label:
996                 if self.wallet.labels.get(address) != label:
997                     if self.question(_('Save label "%s" for address %s ?'%(label,address))):
998                         if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
999                             self.wallet.addressbook.append(address)
1000                             self.wallet.set_label(address, label)
1001             else:
1002                 label = self.wallet.labels.get(address)
1003             if address:
1004                 self.payto_e.setText(label + '  <'+ address +'>' if label else address)
1005             if message:
1006                 self.message_e.setText(message)
1007             if amount:
1008                 self.amount_e.setAmount(amount)
1009             return
1010
1011         from electrum import paymentrequest
1012         def payment_request():
1013             self.payment_request = paymentrequest.PaymentRequest(self.config)
1014             self.payment_request.read(request_url)
1015             if self.payment_request.verify():
1016                 self.emit(SIGNAL('payment_request_ok'))
1017             else:
1018                 self.emit(SIGNAL('payment_request_error'))
1019
1020         self.pr_thread = threading.Thread(target=payment_request).start()
1021         self.prepare_for_payment_request()
1022
1023
1024
1025     def do_clear(self):
1026         self.payto_e.is_pr = False
1027         self.payto_sig.setVisible(False)
1028         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1029             e.setText('')
1030             e.setFrozen(False)
1031
1032         for h in [self.payto_help, self.amount_help, self.message_help]:
1033             h.show()
1034
1035         self.payto_help.set_alt(None)
1036         self.set_pay_from([])
1037         self.update_status()
1038
1039
1040
1041     def set_addrs_frozen(self,addrs,freeze):
1042         for addr in addrs:
1043             if not addr: continue
1044             if addr in self.wallet.frozen_addresses and not freeze:
1045                 self.wallet.unfreeze(addr)
1046             elif addr not in self.wallet.frozen_addresses and freeze:
1047                 self.wallet.freeze(addr)
1048         self.update_receive_tab()
1049
1050
1051
1052     def create_list_tab(self, headers):
1053         "generic tab creation method"
1054         l = MyTreeWidget(self)
1055         l.setColumnCount( len(headers) )
1056         l.setHeaderLabels( headers )
1057
1058         w = QWidget()
1059         vbox = QVBoxLayout()
1060         w.setLayout(vbox)
1061
1062         vbox.setMargin(0)
1063         vbox.setSpacing(0)
1064         vbox.addWidget(l)
1065         buttons = QWidget()
1066         vbox.addWidget(buttons)
1067
1068         return l, w
1069
1070
1071     def create_receive_tab(self):
1072         l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1073         for i,width in enumerate(self.column_widths['receive']):
1074             l.setColumnWidth(i, width)
1075         l.setContextMenuPolicy(Qt.CustomContextMenu)
1076         l.customContextMenuRequested.connect(self.create_receive_menu)
1077         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1078         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1079         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1080         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1081         self.receive_list = l
1082         return w
1083
1084
1085
1086
1087     def save_column_widths(self):
1088         self.column_widths["receive"] = []
1089         for i in range(self.receive_list.columnCount() -1):
1090             self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1091
1092         self.column_widths["history"] = []
1093         for i in range(self.history_list.columnCount() - 1):
1094             self.column_widths["history"].append(self.history_list.columnWidth(i))
1095
1096         self.column_widths["contacts"] = []
1097         for i in range(self.contacts_list.columnCount() - 1):
1098             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1099
1100         self.config.set_key("column_widths_2", self.column_widths, True)
1101
1102
1103     def create_contacts_tab(self):
1104         l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1105         l.setContextMenuPolicy(Qt.CustomContextMenu)
1106         l.customContextMenuRequested.connect(self.create_contact_menu)
1107         for i,width in enumerate(self.column_widths['contacts']):
1108             l.setColumnWidth(i, width)
1109         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1110         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1111         self.contacts_list = l
1112         return w
1113
1114
1115     def create_invoices_tab(self):
1116         l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1117         h = l.header()
1118         h.setStretchLastSection(False)
1119         h.setResizeMode(1, QHeaderView.Stretch)
1120         l.setContextMenuPolicy(Qt.CustomContextMenu)
1121         l.customContextMenuRequested.connect(self.create_invoice_menu)
1122         self.invoices_list = l
1123         return w
1124
1125     def update_invoices_tab(self):
1126         invoices = self.wallet.storage.get('invoices', {})
1127         l = self.invoices_list
1128         l.clear()
1129         for key, value in invoices.items():
1130             try:
1131                 domain, memo, amount, status, tx_hash = value
1132             except:
1133                 invoices.pop(key)
1134                 continue
1135             item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1136             l.addTopLevelItem(item)
1137
1138         l.setCurrentItem(l.topLevelItem(0))
1139
1140
1141
1142     def delete_imported_key(self, addr):
1143         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1144             self.wallet.delete_imported_key(addr)
1145             self.update_receive_tab()
1146             self.update_history_tab()
1147
1148     def edit_account_label(self, k):
1149         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1150         if ok:
1151             label = unicode(text)
1152             self.wallet.set_label(k,label)
1153             self.update_receive_tab()
1154
1155     def account_set_expanded(self, item, k, b):
1156         item.setExpanded(b)
1157         self.accounts_expanded[k] = b
1158
1159     def create_account_menu(self, position, k, item):
1160         menu = QMenu()
1161         if item.isExpanded():
1162             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1163         else:
1164             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1165         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1166         if self.wallet.seed_version > 4:
1167             menu.addAction(_("View details"), lambda: self.show_account_details(k))
1168         if self.wallet.account_is_pending(k):
1169             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1170         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1171
1172     def delete_pending_account(self, k):
1173         self.wallet.delete_pending_account(k)
1174         self.update_receive_tab()
1175
1176     def create_receive_menu(self, position):
1177         # fixme: this function apparently has a side effect.
1178         # if it is not called the menu pops up several times
1179         #self.receive_list.selectedIndexes()
1180
1181         selected = self.receive_list.selectedItems()
1182         multi_select = len(selected) > 1
1183         addrs = [unicode(item.text(0)) for item in selected]
1184         if not multi_select:
1185             item = self.receive_list.itemAt(position)
1186             if not item: return
1187
1188             addr = addrs[0]
1189             if not is_valid(addr):
1190                 k = str(item.data(0,32).toString())
1191                 if k:
1192                     self.create_account_menu(position, k, item)
1193                 else:
1194                     item.setExpanded(not item.isExpanded())
1195                 return
1196
1197         menu = QMenu()
1198         if not multi_select:
1199             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1200             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1201             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1202             menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1203             if not self.wallet.is_watching_only():
1204                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1205                 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1206                 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1207             if self.wallet.is_imported(addr):
1208                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1209
1210         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1211             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1212         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1213             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1214
1215         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1216             menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1217
1218         run_hook('receive_menu', menu, addrs)
1219         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1220
1221
1222     def get_sendable_balance(self):
1223         return sum(map(lambda x:x['value'], self.get_coins()))
1224
1225
1226     def get_coins(self):
1227         if self.pay_from:
1228             return self.pay_from
1229         else:
1230             domain = self.wallet.get_account_addresses(self.current_account)
1231             for i in self.wallet.frozen_addresses:
1232                 if i in domain: domain.remove(i)
1233             return self.wallet.get_unspent_coins(domain)
1234
1235
1236     def send_from_addresses(self, addrs):
1237         self.set_pay_from( addrs )
1238         self.tabs.setCurrentIndex(1)
1239
1240
1241     def payto(self, addr):
1242         if not addr: return
1243         label = self.wallet.labels.get(addr)
1244         m_addr = label + '  <' + addr + '>' if label else addr
1245         self.tabs.setCurrentIndex(1)
1246         self.payto_e.setText(m_addr)
1247         self.amount_e.setFocus()
1248
1249
1250     def delete_contact(self, x):
1251         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1252             self.wallet.delete_contact(x)
1253             self.wallet.set_label(x, None)
1254             self.update_history_tab()
1255             self.update_contacts_tab()
1256             self.update_completions()
1257
1258
1259     def create_contact_menu(self, position):
1260         item = self.contacts_list.itemAt(position)
1261         menu = QMenu()
1262         if not item:
1263             menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1264         else:
1265             addr = unicode(item.text(0))
1266             label = unicode(item.text(1))
1267             is_editable = item.data(0,32).toBool()
1268             payto_addr = item.data(0,33).toString()
1269             menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1270             menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1271             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1272             if is_editable:
1273                 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1274                 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1275
1276         run_hook('create_contact_menu', menu, item)
1277         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1278
1279     def delete_invoice(self, key):
1280         self.invoices.pop(key)
1281         self.wallet.storage.put('invoices', self.invoices)
1282         self.update_invoices_tab()
1283
1284     def show_invoice(self, key):
1285         from electrum.paymentrequest import PaymentRequest
1286         domain, memo, value, status, tx_hash = self.invoices[key]
1287         pr = PaymentRequest(self.config)
1288         pr.read_file(key)
1289         pr.domain = domain
1290         pr.verify()
1291         self.show_pr_details(pr)
1292
1293     def show_pr_details(self, pr):
1294         msg = 'Domain: ' + pr.domain
1295         msg += '\nStatus: ' + pr.get_status()
1296         msg += '\nMemo: ' + pr.get_memo()
1297         msg += '\nPayment URL: ' + pr.payment_url
1298         msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1299         QMessageBox.information(self, 'Invoice', msg , 'OK')
1300
1301     def do_pay_invoice(self, key):
1302         from electrum.paymentrequest import PaymentRequest
1303         domain, memo, value, status, tx_hash = self.invoices[key]
1304         pr = PaymentRequest(self.config)
1305         pr.read_file(key)
1306         pr.domain = domain
1307         self.payment_request = pr
1308         self.prepare_for_payment_request()
1309         if pr.verify():
1310             self.payment_request_ok()
1311         else:
1312             self.payment_request_error()
1313             
1314
1315     def create_invoice_menu(self, position):
1316         item = self.invoices_list.itemAt(position)
1317         if not item:
1318             return
1319         k = self.invoices_list.indexOfTopLevelItem(item)
1320         key = self.invoices.keys()[k]
1321         domain, memo, value, status, tx_hash = self.invoices[key]
1322         menu = QMenu()
1323         menu.addAction(_("Details"), lambda: self.show_invoice(key))
1324         if status == PR_UNPAID:
1325             menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1326         menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1327         menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1328
1329
1330     def update_receive_item(self, item):
1331         item.setFont(0, QFont(MONOSPACE_FONT))
1332         address = str(item.data(0,0).toString())
1333         label = self.wallet.labels.get(address,'')
1334         item.setData(1,0,label)
1335         item.setData(0,32, True) # is editable
1336
1337         run_hook('update_receive_item', address, item)
1338
1339         if not self.wallet.is_mine(address): return
1340
1341         c, u = self.wallet.get_addr_balance(address)
1342         balance = self.format_amount(c + u)
1343         item.setData(2,0,balance)
1344
1345         if address in self.wallet.frozen_addresses:
1346             item.setBackgroundColor(0, QColor('lightblue'))
1347
1348
1349     def update_receive_tab(self):
1350         l = self.receive_list
1351         # extend the syntax for consistency
1352         l.addChild = l.addTopLevelItem
1353         l.insertChild = l.insertTopLevelItem
1354
1355         l.clear()
1356
1357         accounts = self.wallet.get_accounts()
1358         if self.current_account is None:
1359             account_items = sorted(accounts.items())
1360         else:
1361             account_items = [(self.current_account, accounts.get(self.current_account))]
1362
1363
1364         for k, account in account_items:
1365
1366             if len(accounts) > 1:
1367                 name = self.wallet.get_account_name(k)
1368                 c,u = self.wallet.get_account_balance(k)
1369                 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1370                 l.addTopLevelItem(account_item)
1371                 account_item.setExpanded(self.accounts_expanded.get(k, True))
1372                 account_item.setData(0, 32, k)
1373             else:
1374                 account_item = l
1375
1376             sequences = [0,1] if account.has_change() else [0]
1377             for is_change in sequences:
1378                 if len(sequences) > 1:
1379                     name = _("Receiving") if not is_change else _("Change")
1380                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1381                     account_item.addChild(seq_item)
1382                     if not is_change: 
1383                         seq_item.setExpanded(True)
1384                 else:
1385                     seq_item = account_item
1386                     
1387                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1388                 used_flag = False
1389
1390                 is_red = False
1391                 gap = 0
1392
1393                 for address in account.get_addresses(is_change):
1394
1395                     num, is_used = self.wallet.is_used(address)
1396                     if num == 0:
1397                         gap += 1
1398                         if gap > self.wallet.gap_limit:
1399                             is_red = True
1400                     else:
1401                         gap = 0
1402
1403                     item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1404                     self.update_receive_item(item)
1405                     if is_red:
1406                         item.setBackgroundColor(1, QColor('red'))
1407
1408                     if is_used:
1409                         if not used_flag:
1410                             seq_item.insertChild(0,used_item)
1411                             used_flag = True
1412                         used_item.addChild(item)
1413                     else:
1414                         seq_item.addChild(item)
1415
1416         # we use column 1 because column 0 may be hidden
1417         l.setCurrentItem(l.topLevelItem(0),1)
1418
1419
1420     def update_contacts_tab(self):
1421         l = self.contacts_list
1422         l.clear()
1423
1424         for address in self.wallet.addressbook:
1425             label = self.wallet.labels.get(address,'')
1426             n = self.wallet.get_num_tx(address)
1427             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1428             item.setFont(0, QFont(MONOSPACE_FONT))
1429             # 32 = label can be edited (bool)
1430             item.setData(0,32, True)
1431             # 33 = payto string
1432             item.setData(0,33, address)
1433             l.addTopLevelItem(item)
1434
1435         run_hook('update_contacts_tab', l)
1436         l.setCurrentItem(l.topLevelItem(0))
1437
1438
1439
1440     def create_console_tab(self):
1441         from console import Console
1442         self.console = console = Console()
1443         return console
1444
1445
1446     def update_console(self):
1447         console = self.console
1448         console.history = self.config.get("console-history",[])
1449         console.history_index = len(console.history)
1450
1451         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1452         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1453
1454         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1455         methods = {}
1456         def mkfunc(f, method):
1457             return lambda *args: apply( f, (method, args, self.password_dialog ))
1458         for m in dir(c):
1459             if m[0]=='_' or m in ['network','wallet']: continue
1460             methods[m] = mkfunc(c._run, m)
1461
1462         console.updateNamespace(methods)
1463
1464
1465     def change_account(self,s):
1466         if s == _("All accounts"):
1467             self.current_account = None
1468         else:
1469             accounts = self.wallet.get_account_names()
1470             for k, v in accounts.items():
1471                 if v == s:
1472                     self.current_account = k
1473         self.update_history_tab()
1474         self.update_status()
1475         self.update_receive_tab()
1476
1477     def create_status_bar(self):
1478
1479         sb = QStatusBar()
1480         sb.setFixedHeight(35)
1481         qtVersion = qVersion()
1482
1483         self.balance_label = QLabel("")
1484         sb.addWidget(self.balance_label)
1485
1486         from version_getter import UpdateLabel
1487         self.updatelabel = UpdateLabel(self.config, sb)
1488
1489         self.account_selector = QComboBox()
1490         self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1491         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1492         sb.addPermanentWidget(self.account_selector)
1493
1494         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1495             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1496
1497         self.lock_icon = QIcon()
1498         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1499         sb.addPermanentWidget( self.password_button )
1500
1501         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1502         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1503         sb.addPermanentWidget( self.seed_button )
1504         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1505         sb.addPermanentWidget( self.status_button )
1506
1507         run_hook('create_status_bar', (sb,))
1508
1509         self.setStatusBar(sb)
1510
1511
1512     def update_lock_icon(self):
1513         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1514         self.password_button.setIcon( icon )
1515
1516
1517     def update_buttons_on_seed(self):
1518         if self.wallet.has_seed():
1519            self.seed_button.show()
1520         else:
1521            self.seed_button.hide()
1522
1523         if not self.wallet.is_watching_only():
1524            self.password_button.show()
1525            self.send_button.setText(_("Send"))
1526         else:
1527            self.password_button.hide()
1528            self.send_button.setText(_("Create unsigned transaction"))
1529
1530
1531     def change_password_dialog(self):
1532         from password_dialog import PasswordDialog
1533         d = PasswordDialog(self.wallet, self)
1534         d.run()
1535         self.update_lock_icon()
1536
1537
1538     def new_contact_dialog(self):
1539
1540         d = QDialog(self)
1541         d.setWindowTitle(_("New Contact"))
1542         vbox = QVBoxLayout(d)
1543         vbox.addWidget(QLabel(_('New Contact')+':'))
1544
1545         grid = QGridLayout()
1546         line1 = QLineEdit()
1547         line2 = QLineEdit()
1548         grid.addWidget(QLabel(_("Address")), 1, 0)
1549         grid.addWidget(line1, 1, 1)
1550         grid.addWidget(QLabel(_("Name")), 2, 0)
1551         grid.addWidget(line2, 2, 1)
1552
1553         vbox.addLayout(grid)
1554         vbox.addLayout(ok_cancel_buttons(d))
1555
1556         if not d.exec_():
1557             return
1558
1559         address = str(line1.text())
1560         label = unicode(line2.text())
1561
1562         if not is_valid(address):
1563             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1564             return
1565
1566         self.wallet.add_contact(address)
1567         if label:
1568             self.wallet.set_label(address, label)
1569
1570         self.update_contacts_tab()
1571         self.update_history_tab()
1572         self.update_completions()
1573         self.tabs.setCurrentIndex(3)
1574
1575
1576     @protected
1577     def new_account_dialog(self, password):
1578
1579         dialog = QDialog(self)
1580         dialog.setModal(1)
1581         dialog.setWindowTitle(_("New Account"))
1582
1583         vbox = QVBoxLayout()
1584         vbox.addWidget(QLabel(_('Account name')+':'))
1585         e = QLineEdit()
1586         vbox.addWidget(e)
1587         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1588             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1589         l = QLabel(msg)
1590         l.setWordWrap(True)
1591         vbox.addWidget(l)
1592
1593         vbox.addLayout(ok_cancel_buttons(dialog))
1594         dialog.setLayout(vbox)
1595         r = dialog.exec_()
1596         if not r: return
1597
1598         name = str(e.text())
1599         if not name: return
1600
1601         self.wallet.create_pending_account(name, password)
1602         self.update_receive_tab()
1603         self.tabs.setCurrentIndex(2)
1604
1605
1606
1607
1608     def show_master_public_keys(self):
1609
1610         dialog = QDialog(self)
1611         dialog.setModal(1)
1612         dialog.setWindowTitle(_("Master Public Keys"))
1613
1614         main_layout = QGridLayout()
1615         mpk_dict = self.wallet.get_master_public_keys()
1616         i = 0
1617         for key, value in mpk_dict.items():
1618             main_layout.addWidget(QLabel(key), i, 0)
1619             mpk_text = QTextEdit()
1620             mpk_text.setReadOnly(True)
1621             mpk_text.setMaximumHeight(170)
1622             mpk_text.setText(value)
1623             main_layout.addWidget(mpk_text, i + 1, 0)
1624             i += 2
1625
1626         vbox = QVBoxLayout()
1627         vbox.addLayout(main_layout)
1628         vbox.addLayout(close_button(dialog))
1629
1630         dialog.setLayout(vbox)
1631         dialog.exec_()
1632
1633
1634     @protected
1635     def show_seed_dialog(self, password):
1636         if not self.wallet.has_seed():
1637             QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1638             return
1639
1640         try:
1641             mnemonic = self.wallet.get_mnemonic(password)
1642         except Exception:
1643             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1644             return
1645         from seed_dialog import SeedDialog
1646         d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1647         d.exec_()
1648
1649
1650
1651     def show_qrcode(self, data, title = _("QR code")):
1652         if not data: return
1653         d = QDialog(self)
1654         d.setModal(1)
1655         d.setWindowTitle(title)
1656         d.setMinimumSize(270, 300)
1657         vbox = QVBoxLayout()
1658         qrw = QRCodeWidget(data)
1659         vbox.addWidget(qrw, 1)
1660         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1661         hbox = QHBoxLayout()
1662         hbox.addStretch(1)
1663
1664         filename = os.path.join(self.config.path, "qrcode.bmp")
1665
1666         def print_qr():
1667             bmp.save_qrcode(qrw.qr, filename)
1668             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1669
1670         def copy_to_clipboard():
1671             bmp.save_qrcode(qrw.qr, filename)
1672             self.app.clipboard().setImage(QImage(filename))
1673             QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1674
1675         b = QPushButton(_("Copy"))
1676         hbox.addWidget(b)
1677         b.clicked.connect(copy_to_clipboard)
1678
1679         b = QPushButton(_("Save"))
1680         hbox.addWidget(b)
1681         b.clicked.connect(print_qr)
1682
1683         b = QPushButton(_("Close"))
1684         hbox.addWidget(b)
1685         b.clicked.connect(d.accept)
1686         b.setDefault(True)
1687
1688         vbox.addLayout(hbox)
1689         d.setLayout(vbox)
1690         d.exec_()
1691
1692
1693     def do_protect(self, func, args):
1694         if self.wallet.use_encryption:
1695             password = self.password_dialog()
1696             if not password:
1697                 return
1698         else:
1699             password = None
1700
1701         if args != (False,):
1702             args = (self,) + args + (password,)
1703         else:
1704             args = (self,password)
1705         apply( func, args)
1706
1707
1708     def show_public_keys(self, address):
1709         if not address: return
1710         try:
1711             pubkey_list = self.wallet.get_public_keys(address)
1712         except Exception as e:
1713             traceback.print_exc(file=sys.stdout)
1714             self.show_message(str(e))
1715             return
1716
1717         d = QDialog(self)
1718         d.setMinimumSize(600, 200)
1719         d.setModal(1)
1720         vbox = QVBoxLayout()
1721         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1722         vbox.addWidget( QLabel(_("Public key") + ':'))
1723         keys = QTextEdit()
1724         keys.setReadOnly(True)
1725         keys.setText('\n'.join(pubkey_list))
1726         vbox.addWidget(keys)
1727         #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1728         vbox.addLayout(close_button(d))
1729         d.setLayout(vbox)
1730         d.exec_()
1731
1732     @protected
1733     def show_private_key(self, address, password):
1734         if not address: return
1735         try:
1736             pk_list = self.wallet.get_private_key(address, password)
1737         except Exception as e:
1738             traceback.print_exc(file=sys.stdout)
1739             self.show_message(str(e))
1740             return
1741
1742         d = QDialog(self)
1743         d.setMinimumSize(600, 200)
1744         d.setModal(1)
1745         vbox = QVBoxLayout()
1746         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1747         vbox.addWidget( QLabel(_("Private key") + ':'))
1748         keys = QTextEdit()
1749         keys.setReadOnly(True)
1750         keys.setText('\n'.join(pk_list))
1751         vbox.addWidget(keys)
1752         vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1753         vbox.addLayout(close_button(d))
1754         d.setLayout(vbox)
1755         d.exec_()
1756
1757
1758     @protected
1759     def do_sign(self, address, message, signature, password):
1760         message = unicode(message.toPlainText())
1761         message = message.encode('utf-8')
1762         try:
1763             sig = self.wallet.sign_message(str(address.text()), message, password)
1764             signature.setText(sig)
1765         except Exception as e:
1766             self.show_message(str(e))
1767
1768     def do_verify(self, address, message, signature):
1769         message = unicode(message.toPlainText())
1770         message = message.encode('utf-8')
1771         if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1772             self.show_message(_("Signature verified"))
1773         else:
1774             self.show_message(_("Error: wrong signature"))
1775
1776
1777     def sign_verify_message(self, address=''):
1778         d = QDialog(self)
1779         d.setModal(1)
1780         d.setWindowTitle(_('Sign/verify Message'))
1781         d.setMinimumSize(410, 290)
1782
1783         layout = QGridLayout(d)
1784
1785         message_e = QTextEdit()
1786         layout.addWidget(QLabel(_('Message')), 1, 0)
1787         layout.addWidget(message_e, 1, 1)
1788         layout.setRowStretch(2,3)
1789
1790         address_e = QLineEdit()
1791         address_e.setText(address)
1792         layout.addWidget(QLabel(_('Address')), 2, 0)
1793         layout.addWidget(address_e, 2, 1)
1794
1795         signature_e = QTextEdit()
1796         layout.addWidget(QLabel(_('Signature')), 3, 0)
1797         layout.addWidget(signature_e, 3, 1)
1798         layout.setRowStretch(3,1)
1799
1800         hbox = QHBoxLayout()
1801
1802         b = QPushButton(_("Sign"))
1803         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1804         hbox.addWidget(b)
1805
1806         b = QPushButton(_("Verify"))
1807         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1808         hbox.addWidget(b)
1809
1810         b = QPushButton(_("Close"))
1811         b.clicked.connect(d.accept)
1812         hbox.addWidget(b)
1813         layout.addLayout(hbox, 4, 1)
1814         d.exec_()
1815
1816
1817     @protected
1818     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1819         try:
1820             decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1821             message_e.setText(decrypted)
1822         except Exception as e:
1823             self.show_message(str(e))
1824
1825
1826     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1827         message = unicode(message_e.toPlainText())
1828         message = message.encode('utf-8')
1829         try:
1830             encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1831             encrypted_e.setText(encrypted)
1832         except Exception as e:
1833             self.show_message(str(e))
1834
1835
1836
1837     def encrypt_message(self, address = ''):
1838         d = QDialog(self)
1839         d.setModal(1)
1840         d.setWindowTitle(_('Encrypt/decrypt Message'))
1841         d.setMinimumSize(610, 490)
1842
1843         layout = QGridLayout(d)
1844
1845         message_e = QTextEdit()
1846         layout.addWidget(QLabel(_('Message')), 1, 0)
1847         layout.addWidget(message_e, 1, 1)
1848         layout.setRowStretch(2,3)
1849
1850         pubkey_e = QLineEdit()
1851         if address:
1852             pubkey = self.wallet.getpubkeys(address)[0]
1853             pubkey_e.setText(pubkey)
1854         layout.addWidget(QLabel(_('Public key')), 2, 0)
1855         layout.addWidget(pubkey_e, 2, 1)
1856
1857         encrypted_e = QTextEdit()
1858         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1859         layout.addWidget(encrypted_e, 3, 1)
1860         layout.setRowStretch(3,1)
1861
1862         hbox = QHBoxLayout()
1863         b = QPushButton(_("Encrypt"))
1864         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1865         hbox.addWidget(b)
1866
1867         b = QPushButton(_("Decrypt"))
1868         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1869         hbox.addWidget(b)
1870
1871         b = QPushButton(_("Close"))
1872         b.clicked.connect(d.accept)
1873         hbox.addWidget(b)
1874
1875         layout.addLayout(hbox, 4, 1)
1876         d.exec_()
1877
1878
1879     def question(self, msg):
1880         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1881
1882     def show_message(self, msg):
1883         QMessageBox.information(self, _('Message'), msg, _('OK'))
1884
1885     def password_dialog(self, msg=None):
1886         d = QDialog(self)
1887         d.setModal(1)
1888         d.setWindowTitle(_("Enter Password"))
1889
1890         pw = QLineEdit()
1891         pw.setEchoMode(2)
1892
1893         vbox = QVBoxLayout()
1894         if not msg:
1895             msg = _('Please enter your password')
1896         vbox.addWidget(QLabel(msg))
1897
1898         grid = QGridLayout()
1899         grid.setSpacing(8)
1900         grid.addWidget(QLabel(_('Password')), 1, 0)
1901         grid.addWidget(pw, 1, 1)
1902         vbox.addLayout(grid)
1903
1904         vbox.addLayout(ok_cancel_buttons(d))
1905         d.setLayout(vbox)
1906
1907         run_hook('password_dialog', pw, grid, 1)
1908         if not d.exec_(): return
1909         return unicode(pw.text())
1910
1911
1912
1913
1914
1915
1916
1917
1918     def tx_from_text(self, txt):
1919         "json or raw hexadecimal"
1920         try:
1921             txt.decode('hex')
1922             tx = Transaction(txt)
1923             return tx
1924         except Exception:
1925             pass
1926
1927         try:
1928             tx_dict = json.loads(str(txt))
1929             assert "hex" in tx_dict.keys()
1930             tx = Transaction(tx_dict["hex"])
1931             if tx_dict.has_key("input_info"):
1932                 input_info = json.loads(tx_dict['input_info'])
1933                 tx.add_input_info(input_info)
1934             return tx
1935         except Exception:
1936             traceback.print_exc(file=sys.stdout)
1937             pass
1938
1939         QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1940
1941
1942
1943     def read_tx_from_file(self):
1944         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1945         if not fileName:
1946             return
1947         try:
1948             with open(fileName, "r") as f:
1949                 file_content = f.read()
1950         except (ValueError, IOError, os.error), reason:
1951             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1952
1953         return self.tx_from_text(file_content)
1954
1955
1956     @protected
1957     def sign_raw_transaction(self, tx, input_info, password):
1958         self.wallet.signrawtransaction(tx, input_info, [], password)
1959
1960     def do_process_from_text(self):
1961         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1962         if not text:
1963             return
1964         tx = self.tx_from_text(text)
1965         if tx:
1966             self.show_transaction(tx)
1967
1968     def do_process_from_file(self):
1969         tx = self.read_tx_from_file()
1970         if tx:
1971             self.show_transaction(tx)
1972
1973     def do_process_from_txid(self):
1974         from electrum import transaction
1975         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1976         if ok and txid:
1977             r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1978             if r:
1979                 tx = transaction.Transaction(r)
1980                 if tx:
1981                     self.show_transaction(tx)
1982                 else:
1983                     self.show_message("unknown transaction")
1984
1985     def do_process_from_csvReader(self, csvReader):
1986         outputs = []
1987         errors = []
1988         errtext = ""
1989         try:
1990             for position, row in enumerate(csvReader):
1991                 address = row[0]
1992                 if not is_valid(address):
1993                     errors.append((position, address))
1994                     continue
1995                 amount = Decimal(row[1])
1996                 amount = int(100000000*amount)
1997                 outputs.append((address, amount))
1998         except (ValueError, IOError, os.error), reason:
1999             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2000             return
2001         if errors != []:
2002             for x in errors:
2003                 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2004             QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2005             return
2006
2007         try:
2008             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2009         except Exception as e:
2010             self.show_message(str(e))
2011             return
2012
2013         self.show_transaction(tx)
2014
2015     def do_process_from_csv_file(self):
2016         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2017         if not fileName:
2018             return
2019         try:
2020             with open(fileName, "r") as f:
2021                 csvReader = csv.reader(f)
2022                 self.do_process_from_csvReader(csvReader)
2023         except (ValueError, IOError, os.error), reason:
2024             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2025             return
2026
2027     def do_process_from_csv_text(self):
2028         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2029                                + _("Format: address, amount. One output per line"), _("Load CSV"))
2030         if not text:
2031             return
2032         f = StringIO.StringIO(text)
2033         csvReader = csv.reader(f)
2034         self.do_process_from_csvReader(csvReader)
2035
2036
2037
2038     @protected
2039     def export_privkeys_dialog(self, password):
2040         if self.wallet.is_watching_only():
2041             self.show_message(_("This is a watching-only wallet"))
2042             return
2043
2044         d = QDialog(self)
2045         d.setWindowTitle(_('Private keys'))
2046         d.setMinimumSize(850, 300)
2047         vbox = QVBoxLayout(d)
2048
2049         msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."), 
2050                               _("Exposing a single private key can compromise your entire wallet!"), 
2051                               _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2052         vbox.addWidget(QLabel(msg))
2053
2054         e = QTextEdit()
2055         e.setReadOnly(True)
2056         vbox.addWidget(e)
2057
2058         defaultname = 'electrum-private-keys.csv'
2059         select_msg = _('Select file to export your private keys to')
2060         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2061         vbox.addLayout(hbox)
2062
2063         h, b = ok_cancel_buttons2(d, _('Export'))
2064         b.setEnabled(False)
2065         vbox.addLayout(h)
2066
2067         private_keys = {}
2068         addresses = self.wallet.addresses(True)
2069         done = False
2070         def privkeys_thread():
2071             for addr in addresses:
2072                 time.sleep(0.1)
2073                 if done: 
2074                     break
2075                 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2076                 d.emit(SIGNAL('computing_privkeys'))
2077             d.emit(SIGNAL('show_privkeys'))
2078
2079         def show_privkeys():
2080             s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2081             e.setText(s)
2082             b.setEnabled(True)
2083
2084         d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2085         d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2086         threading.Thread(target=privkeys_thread).start()
2087
2088         if not d.exec_():
2089             done = True
2090             return
2091
2092         filename = filename_e.text()
2093         if not filename:
2094             return
2095
2096         try:
2097             self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2098         except (IOError, os.error), reason:
2099             export_error_label = _("Electrum was unable to produce a private key-export.")
2100             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2101
2102         except Exception as e:
2103             self.show_message(str(e))
2104             return
2105
2106         self.show_message(_("Private keys exported."))
2107
2108
2109     def do_export_privkeys(self, fileName, pklist, is_csv):
2110         with open(fileName, "w+") as f:
2111             if is_csv:
2112                 transaction = csv.writer(f)
2113                 transaction.writerow(["address", "private_key"])
2114                 for addr, pk in pklist.items():
2115                     transaction.writerow(["%34s"%addr,pk])
2116             else:
2117                 import json
2118                 f.write(json.dumps(pklist, indent = 4))
2119
2120
2121     def do_import_labels(self):
2122         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2123         if not labelsFile: return
2124         try:
2125             f = open(labelsFile, 'r')
2126             data = f.read()
2127             f.close()
2128             for key, value in json.loads(data).items():
2129                 self.wallet.set_label(key, value)
2130             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2131         except (IOError, os.error), reason:
2132             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2133
2134
2135     def do_export_labels(self):
2136         labels = self.wallet.labels
2137         try:
2138             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2139             if fileName:
2140                 with open(fileName, 'w+') as f:
2141                     json.dump(labels, f)
2142                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2143         except (IOError, os.error), reason:
2144             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2145
2146
2147     def export_history_dialog(self):
2148
2149         d = QDialog(self)
2150         d.setWindowTitle(_('Export History'))
2151         d.setMinimumSize(400, 200)
2152         vbox = QVBoxLayout(d)
2153
2154         defaultname = os.path.expanduser('~/electrum-history.csv')
2155         select_msg = _('Select file to export your wallet transactions to')
2156
2157         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2158         vbox.addLayout(hbox)
2159
2160         vbox.addStretch(1)
2161
2162         h, b = ok_cancel_buttons2(d, _('Export'))
2163         vbox.addLayout(h)
2164         if not d.exec_():
2165             return
2166
2167         filename = filename_e.text()
2168         if not filename:
2169             return
2170
2171         try:
2172             self.do_export_history(self.wallet, filename, csv_button.isChecked())
2173         except (IOError, os.error), reason:
2174             export_error_label = _("Electrum was unable to produce a transaction export.")
2175             QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2176             return
2177
2178         QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2179
2180
2181     def do_export_history(self, wallet, fileName, is_csv):
2182         history = wallet.get_tx_history()
2183         lines = []
2184         for item in history:
2185             tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2186             if confirmations:
2187                 if timestamp is not None:
2188                     try:
2189                         time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2190                     except [RuntimeError, TypeError, NameError] as reason:
2191                         time_string = "unknown"
2192                         pass
2193                 else:
2194                     time_string = "unknown"
2195             else:
2196                 time_string = "pending"
2197
2198             if value is not None:
2199                 value_string = format_satoshis(value, True)
2200             else:
2201                 value_string = '--'
2202
2203             if fee is not None:
2204                 fee_string = format_satoshis(fee, True)
2205             else:
2206                 fee_string = '0'
2207
2208             if tx_hash:
2209                 label, is_default_label = wallet.get_label(tx_hash)
2210                 label = label.encode('utf-8')
2211             else:
2212                 label = ""
2213
2214             balance_string = format_satoshis(balance, False)
2215             if is_csv:
2216                 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2217             else:
2218                 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2219
2220         with open(fileName, "w+") as f:
2221             if is_csv:
2222                 transaction = csv.writer(f)
2223                 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2224                 for line in lines:
2225                     transaction.writerow(line)
2226             else:
2227                 import json
2228                 f.write(json.dumps(lines, indent = 4))
2229
2230
2231     def sweep_key_dialog(self):
2232         d = QDialog(self)
2233         d.setWindowTitle(_('Sweep private keys'))
2234         d.setMinimumSize(600, 300)
2235
2236         vbox = QVBoxLayout(d)
2237         vbox.addWidget(QLabel(_("Enter private keys")))
2238
2239         keys_e = QTextEdit()
2240         keys_e.setTabChangesFocus(True)
2241         vbox.addWidget(keys_e)
2242
2243         h, address_e = address_field(self.wallet.addresses())
2244         vbox.addLayout(h)
2245
2246         vbox.addStretch(1)
2247         hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2248         vbox.addLayout(hbox)
2249         button.setEnabled(False)
2250
2251         def get_address():
2252             addr = str(address_e.text())
2253             if bitcoin.is_address(addr):
2254                 return addr
2255
2256         def get_pk():
2257             pk = str(keys_e.toPlainText()).strip()
2258             if Wallet.is_private_key(pk):
2259                 return pk.split()
2260
2261         f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2262         keys_e.textChanged.connect(f)
2263         address_e.textChanged.connect(f)
2264         if not d.exec_():
2265             return
2266
2267         fee = self.wallet.fee
2268         tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2269         self.show_transaction(tx)
2270
2271
2272     @protected
2273     def do_import_privkey(self, password):
2274         if not self.wallet.has_imported_keys():
2275             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2276                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2277                                          + _('Are you sure you understand what you are doing?'), 3, 4)
2278             if r == 4: return
2279
2280         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2281         if not text: return
2282
2283         text = str(text).split()
2284         badkeys = []
2285         addrlist = []
2286         for key in text:
2287             try:
2288                 addr = self.wallet.import_key(key, password)
2289             except Exception as e:
2290                 badkeys.append(key)
2291                 continue
2292             if not addr:
2293                 badkeys.append(key)
2294             else:
2295                 addrlist.append(addr)
2296         if addrlist:
2297             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2298         if badkeys:
2299             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2300         self.update_receive_tab()
2301         self.update_history_tab()
2302
2303
2304     def settings_dialog(self):
2305         d = QDialog(self)
2306         d.setWindowTitle(_('Electrum Settings'))
2307         d.setModal(1)
2308         vbox = QVBoxLayout()
2309         grid = QGridLayout()
2310         grid.setColumnStretch(0,1)
2311
2312         nz_label = QLabel(_('Display zeros') + ':')
2313         grid.addWidget(nz_label, 0, 0)
2314         nz_e = AmountEdit(None,True)
2315         nz_e.setText("%d"% self.num_zeros)
2316         grid.addWidget(nz_e, 0, 1)
2317         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2318         grid.addWidget(HelpButton(msg), 0, 2)
2319         if not self.config.is_modifiable('num_zeros'):
2320             for w in [nz_e, nz_label]: w.setEnabled(False)
2321
2322         lang_label=QLabel(_('Language') + ':')
2323         grid.addWidget(lang_label, 1, 0)
2324         lang_combo = QComboBox()
2325         from electrum.i18n import languages
2326         lang_combo.addItems(languages.values())
2327         try:
2328             index = languages.keys().index(self.config.get("language",''))
2329         except Exception:
2330             index = 0
2331         lang_combo.setCurrentIndex(index)
2332         grid.addWidget(lang_combo, 1, 1)
2333         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2334         if not self.config.is_modifiable('language'):
2335             for w in [lang_combo, lang_label]: w.setEnabled(False)
2336
2337
2338         fee_label = QLabel(_('Transaction fee') + ':')
2339         grid.addWidget(fee_label, 2, 0)
2340         fee_e = BTCAmountEdit(self.get_decimal_point)
2341         fee_e.setAmount(self.wallet.fee)
2342         grid.addWidget(fee_e, 2, 1)
2343         msg = _('Fee per kilobyte of transaction.') + '\n' \
2344             + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2345         grid.addWidget(HelpButton(msg), 2, 2)
2346         if not self.config.is_modifiable('fee_per_kb'):
2347             for w in [fee_e, fee_label]: w.setEnabled(False)
2348
2349         units = ['BTC', 'mBTC']
2350         unit_label = QLabel(_('Base unit') + ':')
2351         grid.addWidget(unit_label, 3, 0)
2352         unit_combo = QComboBox()
2353         unit_combo.addItems(units)
2354         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2355         grid.addWidget(unit_combo, 3, 1)
2356         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2357                                              + '\n1BTC=1000mBTC.\n' \
2358                                              + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2359
2360         usechange_cb = QCheckBox(_('Use change addresses'))
2361         usechange_cb.setChecked(self.wallet.use_change)
2362         grid.addWidget(usechange_cb, 4, 0)
2363         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2364         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2365
2366         block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2367         block_ex_label = QLabel(_('Online Block Explorer') + ':')
2368         grid.addWidget(block_ex_label, 5, 0)
2369         block_ex_combo = QComboBox()
2370         block_ex_combo.addItems(block_explorers)
2371         block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2372         grid.addWidget(block_ex_combo, 5, 1)
2373         grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2374
2375         show_tx = self.config.get('show_before_broadcast', False)
2376         showtx_cb = QCheckBox(_('Show before broadcast'))
2377         showtx_cb.setChecked(show_tx)
2378         grid.addWidget(showtx_cb, 6, 0)
2379         grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2380
2381         vbox.addLayout(grid)
2382         vbox.addStretch(1)
2383         vbox.addLayout(ok_cancel_buttons(d))
2384         d.setLayout(vbox)
2385
2386         # run the dialog
2387         if not d.exec_(): return
2388
2389         fee = fee_e.get_amount()
2390         if fee is None:
2391             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2392             return
2393
2394         self.wallet.set_fee(fee)
2395
2396         nz = unicode(nz_e.text())
2397         try:
2398             nz = int( nz )
2399             if nz>8: nz=8
2400         except Exception:
2401             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2402             return
2403
2404         if self.num_zeros != nz:
2405             self.num_zeros = nz
2406             self.config.set_key('num_zeros', nz, True)
2407             self.update_history_tab()
2408             self.update_receive_tab()
2409
2410         usechange_result = usechange_cb.isChecked()
2411         if self.wallet.use_change != usechange_result:
2412             self.wallet.use_change = usechange_result
2413             self.wallet.storage.put('use_change', self.wallet.use_change)
2414
2415         if showtx_cb.isChecked() != show_tx:
2416             self.config.set_key('show_before_broadcast', not show_tx)
2417
2418         unit_result = units[unit_combo.currentIndex()]
2419         if self.base_unit() != unit_result:
2420             self.decimal_point = 8 if unit_result == 'BTC' else 5
2421             self.config.set_key('decimal_point', self.decimal_point, True)
2422             self.update_history_tab()
2423             self.update_status()
2424
2425         need_restart = False
2426
2427         lang_request = languages.keys()[lang_combo.currentIndex()]
2428         if lang_request != self.config.get('language'):
2429             self.config.set_key("language", lang_request, True)
2430             need_restart = True
2431
2432         be_result = block_explorers[block_ex_combo.currentIndex()]
2433         self.config.set_key('block_explorer', be_result, True)
2434
2435         run_hook('close_settings_dialog')
2436
2437         if need_restart:
2438             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2439
2440
2441     def run_network_dialog(self):
2442         if not self.network:
2443             return
2444         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2445
2446     def closeEvent(self, event):
2447         self.tray.hide()
2448         self.config.set_key("is_maximized", self.isMaximized())
2449         if not self.isMaximized():
2450             g = self.geometry()
2451             self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2452         self.save_column_widths()
2453         self.config.set_key("console-history", self.console.history[-50:], True)
2454         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2455         event.accept()
2456
2457
2458     def plugins_dialog(self):
2459         from electrum.plugins import plugins
2460
2461         d = QDialog(self)
2462         d.setWindowTitle(_('Electrum Plugins'))
2463         d.setModal(1)
2464
2465         vbox = QVBoxLayout(d)
2466
2467         # plugins
2468         scroll = QScrollArea()
2469         scroll.setEnabled(True)
2470         scroll.setWidgetResizable(True)
2471         scroll.setMinimumSize(400,250)
2472         vbox.addWidget(scroll)
2473
2474         w = QWidget()
2475         scroll.setWidget(w)
2476         w.setMinimumHeight(len(plugins)*35)
2477
2478         grid = QGridLayout()
2479         grid.setColumnStretch(0,1)
2480         w.setLayout(grid)
2481
2482         def do_toggle(cb, p, w):
2483             r = p.toggle()
2484             cb.setChecked(r)
2485             if w: w.setEnabled(r)
2486
2487         def mk_toggle(cb, p, w):
2488             return lambda: do_toggle(cb,p,w)
2489
2490         for i, p in enumerate(plugins):
2491             try:
2492                 cb = QCheckBox(p.fullname())
2493                 cb.setDisabled(not p.is_available())
2494                 cb.setChecked(p.is_enabled())
2495                 grid.addWidget(cb, i, 0)
2496                 if p.requires_settings():
2497                     w = p.settings_widget(self)
2498                     w.setEnabled( p.is_enabled() )
2499                     grid.addWidget(w, i, 1)
2500                 else:
2501                     w = None
2502                 cb.clicked.connect(mk_toggle(cb,p,w))
2503                 grid.addWidget(HelpButton(p.description()), i, 2)
2504             except Exception:
2505                 print_msg(_("Error: cannot display plugin"), p)
2506                 traceback.print_exc(file=sys.stdout)
2507         grid.setRowStretch(i+1,1)
2508
2509         vbox.addLayout(close_button(d))
2510
2511         d.exec_()
2512
2513
2514     def show_account_details(self, k):
2515         account = self.wallet.accounts[k]
2516
2517         d = QDialog(self)
2518         d.setWindowTitle(_('Account Details'))
2519         d.setModal(1)
2520
2521         vbox = QVBoxLayout(d)
2522         name = self.wallet.get_account_name(k)
2523         label = QLabel('Name: ' + name)
2524         vbox.addWidget(label)
2525
2526         vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2527
2528         vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2529
2530         vbox.addWidget(QLabel(_('Master Public Key:')))
2531
2532         text = QTextEdit()
2533         text.setReadOnly(True)
2534         text.setMaximumHeight(170)
2535         vbox.addWidget(text)
2536
2537         mpk_text = '\n'.join( account.get_master_pubkeys() )
2538         text.setText(mpk_text)
2539
2540         vbox.addLayout(close_button(d))
2541         d.exec_()