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