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