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