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