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