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