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