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