define frozen signal for exchange rate plugin
[electrum-nvc.git] / gui / qt / main_window.py
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 import sys, time, datetime, re, threading
20 from electrum.i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
23 import webbrowser
24 import shutil
25 import StringIO
26
27
28 import PyQt4
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
32
33 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
34 from electrum.plugins import run_hook
35
36 import icons_rc
37
38 from electrum.util import format_satoshis
39 from electrum import Transaction
40 from electrum import mnemonic
41 from electrum import util, bitcoin, commands, Interface, Wallet
42 from electrum import SimpleConfig, Wallet, WalletStorage
43
44 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
45 from network_dialog import NetworkDialog
46 from qrcodewidget import QRCodeWidget, QRDialog
47 from qrtextedit import QRTextEdit
48
49 from decimal import Decimal
50
51 import platform
52 import httplib
53 import socket
54 import webbrowser
55 import csv
56
57 if platform.system() == 'Windows':
58     MONOSPACE_FONT = 'Lucida Console'
59 elif platform.system() == 'Darwin':
60     MONOSPACE_FONT = 'Monaco'
61 else:
62     MONOSPACE_FONT = 'monospace'
63
64
65
66 # status of payment requests
67 PR_UNPAID  = 0
68 PR_EXPIRED = 1
69 PR_SENT    = 2     # sent but not propagated
70 PR_PAID    = 3     # send and propagated
71 PR_ERROR   = 4     # could not parse
72
73
74 from electrum import ELECTRUM_VERSION
75 import re
76
77 from util import *
78
79
80 def format_status(x):
81     if x == PR_UNPAID:
82         return _('Unpaid')
83     elif x == PR_PAID:
84         return _('Paid')
85     elif x == PR_EXPIRED:
86         return _('Expired')
87
88
89 class StatusBarButton(QPushButton):
90     def __init__(self, icon, tooltip, func):
91         QPushButton.__init__(self, icon, '')
92         self.setToolTip(tooltip)
93         self.setFlat(True)
94         self.setMaximumWidth(25)
95         self.clicked.connect(func)
96         self.func = func
97         self.setIconSize(QSize(25,25))
98
99     def keyPressEvent(self, e):
100         if e.key() == QtCore.Qt.Key_Return:
101             apply(self.func,())
102
103
104
105
106
107
108
109
110
111
112 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
113
114 class ElectrumWindow(QMainWindow):
115
116
117
118     def __init__(self, config, network, gui_object):
119         QMainWindow.__init__(self)
120
121         self.config = config
122         self.network = network
123         self.gui_object = gui_object
124         self.tray = gui_object.tray
125         self.go_lite = gui_object.go_lite
126         self.lite = None
127
128         self.create_status_bar()
129         self.need_update = threading.Event()
130
131         self.decimal_point = config.get('decimal_point', 5)
132         self.num_zeros     = int(config.get('num_zeros',0))
133         self.invoices      = {}
134
135         set_language(config.get('language'))
136
137         self.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.address_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             item.setFont(0, QFont(MONOSPACE_FONT))
772             self.receive_list.addTopLevelItem(item)
773
774
775     def update_receive_qr(self):
776         import urlparse, urllib
777         addr = str(self.receive_address_e.text())
778         amount = self.receive_amount_e.get_amount()
779         message = unicode(self.receive_message_e.text()).encode('utf8')
780         self.save_request_button.setEnabled((amount is not None) or (message != ""))
781         if addr:
782             query = []
783             if amount:
784                 query.append('amount=%s'%format_satoshis(amount))
785             if message:
786                 query.append('message=%s'%urllib.quote(message))
787             p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
788             url = urlparse.urlunparse(p)
789         else:
790             url = ""
791         self.receive_qr.setData(url)
792         run_hook('update_receive_qr', addr, amount, message, url)
793
794
795     def create_send_tab(self):
796         w = QWidget()
797
798         self.send_grid = grid = QGridLayout(w)
799         grid.setSpacing(8)
800         grid.setColumnMinimumWidth(3,300)
801         grid.setColumnStretch(5,1)
802         grid.setRowStretch(8, 1)
803
804         from paytoedit import PayToEdit
805         self.amount_e = BTCAmountEdit(self.get_decimal_point)
806         self.payto_e = PayToEdit(self)
807         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)'))
808         grid.addWidget(QLabel(_('Pay to')), 1, 0)
809         grid.addWidget(self.payto_e, 1, 1, 1, 3)
810         grid.addWidget(self.payto_help, 1, 4)
811
812         completer = QCompleter()
813         completer.setCaseSensitivity(False)
814         self.payto_e.setCompleter(completer)
815         completer.setModel(self.completions)
816
817         self.message_e = MyLineEdit()
818         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.'))
819         grid.addWidget(QLabel(_('Description')), 2, 0)
820         grid.addWidget(self.message_e, 2, 1, 1, 3)
821         grid.addWidget(self.message_help, 2, 4)
822
823         self.from_label = QLabel(_('From'))
824         grid.addWidget(self.from_label, 3, 0)
825         self.from_list = MyTreeWidget(self)
826         self.from_list.setColumnCount(2)
827         self.from_list.setColumnWidth(0, 350)
828         self.from_list.setColumnWidth(1, 50)
829         self.from_list.setHeaderHidden(True)
830         self.from_list.setMaximumHeight(80)
831         self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
832         self.from_list.customContextMenuRequested.connect(self.from_list_menu)
833         grid.addWidget(self.from_list, 3, 1, 1, 3)
834         self.set_pay_from([])
835
836         self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
837                                       + _('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.') \
838                                       + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
839         grid.addWidget(QLabel(_('Amount')), 4, 0)
840         grid.addWidget(self.amount_e, 4, 1, 1, 2)
841         grid.addWidget(self.amount_help, 4, 3)
842
843         self.fee_e = BTCAmountEdit(self.get_decimal_point)
844         grid.addWidget(QLabel(_('Fee')), 5, 0)
845         grid.addWidget(self.fee_e, 5, 1, 1, 2)
846         grid.addWidget(HelpButton(
847                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
848                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
849                     + _('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)
850
851         self.send_button = EnterButton(_("Send"), self.do_send)
852         grid.addWidget(self.send_button, 6, 1)
853
854         b = EnterButton(_("Clear"), self.do_clear)
855         grid.addWidget(b, 6, 2)
856
857         self.payto_sig = QLabel('')
858         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
859
860         #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
861         #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
862         w.setLayout(grid)
863
864         def entry_changed( is_fee ):
865             self.funds_error = False
866
867             if self.amount_e.is_shortcut:
868                 self.amount_e.is_shortcut = False
869                 sendable = self.get_sendable_balance()
870                 # there is only one output because we are completely spending inputs
871                 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
872                 fee = self.wallet.estimated_fee(inputs, 1)
873                 amount = total - fee
874                 self.amount_e.setAmount(amount)
875                 self.amount_e.textEdited.emit("")
876                 self.fee_e.setAmount(fee)
877                 return
878
879             amount = self.amount_e.get_amount()
880             fee = self.fee_e.get_amount()
881
882             if not is_fee: fee = None
883             if amount is None:
884                 return
885             # assume that there will be 2 outputs (one for change)
886             inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
887             if not is_fee:
888                 self.fee_e.setAmount(fee)
889             if inputs:
890                 palette = QPalette()
891                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
892                 text = ""
893             else:
894                 palette = QPalette()
895                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
896                 self.funds_error = True
897                 text = _( "Not enough funds" )
898                 c, u = self.wallet.get_frozen_balance()
899                 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
900
901             self.statusBar().showMessage(text)
902             self.amount_e.setPalette(palette)
903             self.fee_e.setPalette(palette)
904
905         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
906         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
907
908         run_hook('create_send_tab', grid)
909         return w
910
911     def from_list_delete(self, item):
912         i = self.from_list.indexOfTopLevelItem(item)
913         self.pay_from.pop(i)
914         self.redraw_from_list()
915
916     def from_list_menu(self, position):
917         item = self.from_list.itemAt(position)
918         menu = QMenu()
919         menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
920         menu.exec_(self.from_list.viewport().mapToGlobal(position))
921
922     def set_pay_from(self, domain = None):
923         self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
924         self.redraw_from_list()
925
926     def redraw_from_list(self):
927         self.from_list.clear()
928         self.from_label.setHidden(len(self.pay_from) == 0)
929         self.from_list.setHidden(len(self.pay_from) == 0)
930
931         def format(x):
932             h = x.get('prevout_hash')
933             return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
934
935         for item in self.pay_from:
936             self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
937
938     def update_completions(self):
939         l = []
940         for addr,label in self.wallet.labels.items():
941             if addr in self.wallet.addressbook:
942                 l.append( label + '  <' + addr + '>')
943
944         run_hook('update_completions', l)
945         self.completions.setStringList(l)
946
947
948     def protected(func):
949         return lambda s, *args: s.do_protect(func, args)
950
951
952     def read_send_tab(self):
953
954         if self.payment_request and self.payment_request.has_expired():
955             QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
956             return
957
958         label = unicode( self.message_e.text() )
959
960         if self.payment_request:
961             outputs = self.payment_request.get_outputs()
962         else:
963             outputs = self.payto_e.get_outputs()
964
965         if not outputs:
966             QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
967             return
968
969         for addr, x in outputs:
970             if addr is None or not bitcoin.is_address(addr):
971                 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
972                 return
973             if x is None:
974                 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
975                 return
976
977         amount = sum(map(lambda x:x[1], outputs))
978
979         fee = self.fee_e.get_amount()
980         if fee is None:
981             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
982             return
983
984         confirm_amount = self.config.get('confirm_amount', 100000000)
985         if amount >= confirm_amount:
986             o = '\n'.join(map(lambda x:x[0], outputs))
987             if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
988                 return
989             
990         confirm_fee = self.config.get('confirm_fee', 100000)
991         if fee >= confirm_fee:
992             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()}):
993                 return
994
995         coins = self.get_coins()
996         return outputs, fee, label, coins
997
998
999     def do_send(self):
1000         r = self.read_send_tab()
1001         if not r:
1002             return
1003         outputs, fee, label, coins = r
1004         self.send_tx(outputs, fee, label, coins)
1005
1006
1007     @protected
1008     def send_tx(self, outputs, fee, label, coins, password):
1009         self.send_button.setDisabled(True)
1010
1011         # first, create an unsigned tx 
1012         try:
1013             tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1014             tx.error = None
1015         except Exception as e:
1016             traceback.print_exc(file=sys.stdout)
1017             self.show_message(str(e))
1018             self.send_button.setDisabled(False)
1019             return
1020
1021         # call hook to see if plugin needs gui interaction
1022         run_hook('send_tx', tx)
1023
1024         # sign the tx
1025         def sign_thread():
1026             time.sleep(0.1)
1027             keypairs = {}
1028             self.wallet.add_keypairs(tx, keypairs, password)
1029             self.wallet.sign_transaction(tx, keypairs, password)
1030             return tx, fee, label
1031
1032         def sign_done(tx, fee, label):
1033             if tx.error:
1034                 self.show_message(tx.error)
1035                 self.send_button.setDisabled(False)
1036                 return
1037             if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1038                 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1039                 self.send_button.setDisabled(False)
1040                 return
1041             if label:
1042                 self.wallet.set_label(tx.hash(), label)
1043
1044             if not tx.is_complete() or self.config.get('show_before_broadcast'):
1045                 self.show_transaction(tx)
1046                 self.do_clear()
1047                 self.send_button.setDisabled(False)
1048                 return
1049
1050             self.broadcast_transaction(tx)
1051
1052         self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1053         self.waiting_dialog.start()
1054
1055
1056
1057     def broadcast_transaction(self, tx):
1058
1059         def broadcast_thread():
1060             pr = self.payment_request
1061             if pr is None:
1062                 return self.wallet.sendtx(tx)
1063
1064             if pr.has_expired():
1065                 self.payment_request = None
1066                 return False, _("Payment request has expired")
1067
1068             status, msg =  self.wallet.sendtx(tx)
1069             if not status:
1070                 return False, msg
1071
1072             self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1073             self.wallet.storage.put('invoices', self.invoices)
1074             self.update_invoices_tab()
1075             self.payment_request = None
1076             refund_address = self.wallet.addresses()[0]
1077             ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1078             if ack_status:
1079                 msg = ack_msg
1080
1081             return status, msg
1082
1083         def broadcast_done(status, msg):
1084             if status:
1085                 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1086                 self.do_clear()
1087             else:
1088                 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1089             self.send_button.setDisabled(False)
1090
1091         self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1092         self.waiting_dialog.start()
1093
1094
1095
1096     def prepare_for_payment_request(self):
1097         self.tabs.setCurrentIndex(1)
1098         self.payto_e.is_pr = True
1099         for e in [self.payto_e, self.amount_e, self.message_e]:
1100             e.setFrozen(True)
1101         for h in [self.payto_help, self.amount_help, self.message_help]:
1102             h.hide()
1103         self.payto_e.setText(_("please wait..."))
1104         return True
1105
1106     def payment_request_ok(self):
1107         pr = self.payment_request
1108         pr_id = pr.get_id()
1109         if pr_id not in self.invoices:
1110             self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1111             self.wallet.storage.put('invoices', self.invoices)
1112             self.update_invoices_tab()
1113         else:
1114             print_error('invoice already in list')
1115
1116         status = self.invoices[pr_id][4]
1117         if status == PR_PAID:
1118             self.do_clear()
1119             self.show_message("invoice already paid")
1120             self.payment_request = None
1121             return
1122
1123         self.payto_help.show()
1124         self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1125
1126         if not pr.has_expired():
1127             self.payto_e.setGreen()
1128         else:
1129             self.payto_e.setExpired()
1130
1131         self.payto_e.setText(pr.domain)
1132         self.amount_e.setText(self.format_amount(pr.get_amount()))
1133         self.message_e.setText(pr.get_memo())
1134
1135     def payment_request_error(self):
1136         self.do_clear()
1137         self.show_message(self.payment_request.error)
1138         self.payment_request = None
1139
1140     def pay_from_URI(self,URI):
1141         if not URI:
1142             return
1143         address, amount, label, message, request_url = util.parse_URI(URI)
1144         try:
1145             address, amount, label, message, request_url = util.parse_URI(URI)
1146         except Exception as e:
1147             QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1148             return
1149
1150         self.tabs.setCurrentIndex(1)
1151
1152         if not request_url:
1153             if label:
1154                 if self.wallet.labels.get(address) != label:
1155                     if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1156                         if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1157                             self.wallet.addressbook.append(address)
1158                             self.wallet.set_label(address, label)
1159             else:
1160                 label = self.wallet.labels.get(address)
1161             if address:
1162                 self.payto_e.setText(label + '  <'+ address +'>' if label else address)
1163             if message:
1164                 self.message_e.setText(message)
1165             if amount:
1166                 self.amount_e.setAmount(amount)
1167             return
1168
1169         from electrum import paymentrequest
1170         def payment_request():
1171             self.payment_request = paymentrequest.PaymentRequest(self.config)
1172             self.payment_request.read(request_url)
1173             if self.payment_request.verify():
1174                 self.emit(SIGNAL('payment_request_ok'))
1175             else:
1176                 self.emit(SIGNAL('payment_request_error'))
1177
1178         self.pr_thread = threading.Thread(target=payment_request).start()
1179         self.prepare_for_payment_request()
1180
1181
1182
1183     def do_clear(self):
1184         self.payto_e.is_pr = False
1185         self.payto_sig.setVisible(False)
1186         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1187             e.setText('')
1188             e.setFrozen(False)
1189
1190         for h in [self.payto_help, self.amount_help, self.message_help]:
1191             h.show()
1192
1193         self.payto_help.set_alt(None)
1194         self.set_pay_from([])
1195         self.update_status()
1196
1197
1198
1199     def set_addrs_frozen(self,addrs,freeze):
1200         for addr in addrs:
1201             if not addr: continue
1202             if addr in self.wallet.frozen_addresses and not freeze:
1203                 self.wallet.unfreeze(addr)
1204             elif addr not in self.wallet.frozen_addresses and freeze:
1205                 self.wallet.freeze(addr)
1206         self.update_address_tab()
1207
1208
1209
1210     def create_list_tab(self, headers):
1211         "generic tab creation method"
1212         l = MyTreeWidget(self)
1213         l.setColumnCount( len(headers) )
1214         l.setHeaderLabels( headers )
1215
1216         w = QWidget()
1217         vbox = QVBoxLayout()
1218         w.setLayout(vbox)
1219
1220         vbox.setMargin(0)
1221         vbox.setSpacing(0)
1222         vbox.addWidget(l)
1223         buttons = QWidget()
1224         vbox.addWidget(buttons)
1225
1226         return l, w
1227
1228
1229     def create_addresses_tab(self):
1230         l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1231         for i,width in enumerate(self.column_widths['receive']):
1232             l.setColumnWidth(i, width)
1233         l.setContextMenuPolicy(Qt.CustomContextMenu)
1234         l.customContextMenuRequested.connect(self.create_receive_menu)
1235         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1236         l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1237         l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1238         l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1239         self.address_list = l
1240         return w
1241
1242
1243
1244
1245     def save_column_widths(self):
1246         self.column_widths["receive"] = []
1247         for i in range(self.address_list.columnCount() -1):
1248             self.column_widths["receive"].append(self.address_list.columnWidth(i))
1249
1250         self.column_widths["history"] = []
1251         for i in range(self.history_list.columnCount() - 1):
1252             self.column_widths["history"].append(self.history_list.columnWidth(i))
1253
1254         self.column_widths["contacts"] = []
1255         for i in range(self.contacts_list.columnCount() - 1):
1256             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1257
1258         self.config.set_key("column_widths_2", self.column_widths, True)
1259
1260
1261     def create_contacts_tab(self):
1262         l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1263         l.setContextMenuPolicy(Qt.CustomContextMenu)
1264         l.customContextMenuRequested.connect(self.create_contact_menu)
1265         for i,width in enumerate(self.column_widths['contacts']):
1266             l.setColumnWidth(i, width)
1267         l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1268         l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1269         self.contacts_list = l
1270         return w
1271
1272
1273     def create_invoices_tab(self):
1274         l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1275         l.setColumnWidth(0, 150)
1276         h = l.header()
1277         h.setStretchLastSection(False)
1278         h.setResizeMode(1, QHeaderView.Stretch)
1279         l.setContextMenuPolicy(Qt.CustomContextMenu)
1280         l.customContextMenuRequested.connect(self.create_invoice_menu)
1281         self.invoices_list = l
1282         return w
1283
1284     def update_invoices_tab(self):
1285         invoices = self.wallet.storage.get('invoices', {})
1286         l = self.invoices_list
1287         l.clear()
1288         for key, value in invoices.items():
1289             try:
1290                 domain, memo, amount, expiration_date, status, tx_hash = value
1291             except:
1292                 invoices.pop(key)
1293                 continue
1294             if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1295                 status = PR_EXPIRED
1296             item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1297             l.addTopLevelItem(item)
1298
1299         l.setCurrentItem(l.topLevelItem(0))
1300
1301
1302
1303     def delete_imported_key(self, addr):
1304         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1305             self.wallet.delete_imported_key(addr)
1306             self.update_address_tab()
1307             self.update_history_tab()
1308
1309     def edit_account_label(self, k):
1310         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1311         if ok:
1312             label = unicode(text)
1313             self.wallet.set_label(k,label)
1314             self.update_address_tab()
1315
1316     def account_set_expanded(self, item, k, b):
1317         item.setExpanded(b)
1318         self.accounts_expanded[k] = b
1319
1320     def create_account_menu(self, position, k, item):
1321         menu = QMenu()
1322         if item.isExpanded():
1323             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1324         else:
1325             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1326         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1327         if self.wallet.seed_version > 4:
1328             menu.addAction(_("View details"), lambda: self.show_account_details(k))
1329         if self.wallet.account_is_pending(k):
1330             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1331         menu.exec_(self.address_list.viewport().mapToGlobal(position))
1332
1333     def delete_pending_account(self, k):
1334         self.wallet.delete_pending_account(k)
1335         self.update_address_tab()
1336
1337     def create_receive_menu(self, position):
1338         # fixme: this function apparently has a side effect.
1339         # if it is not called the menu pops up several times
1340         #self.address_list.selectedIndexes()
1341
1342         selected = self.address_list.selectedItems()
1343         multi_select = len(selected) > 1
1344         addrs = [unicode(item.text(0)) for item in selected]
1345         if not multi_select:
1346             item = self.address_list.itemAt(position)
1347             if not item: return
1348
1349             addr = addrs[0]
1350             if not is_valid(addr):
1351                 k = str(item.data(0,32).toString())
1352                 if k:
1353                     self.create_account_menu(position, k, item)
1354                 else:
1355                     item.setExpanded(not item.isExpanded())
1356                 return
1357
1358         menu = QMenu()
1359         if not multi_select:
1360             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1361             menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1362             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1363             menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1364             if not self.wallet.is_watching_only():
1365                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1366                 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1367                 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1368             if self.wallet.is_imported(addr):
1369                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1370
1371         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1372             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1373         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1374             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1375
1376         def can_send(addr):
1377             return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1378         if any(can_send(addr) for addr in addrs):
1379             menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1380
1381         run_hook('receive_menu', menu, addrs)
1382         menu.exec_(self.address_list.viewport().mapToGlobal(position))
1383
1384
1385     def get_sendable_balance(self):
1386         return sum(map(lambda x:x['value'], self.get_coins()))
1387
1388
1389     def get_coins(self):
1390         if self.pay_from:
1391             return self.pay_from
1392         else:
1393             domain = self.wallet.get_account_addresses(self.current_account)
1394             for i in self.wallet.frozen_addresses:
1395                 if i in domain: domain.remove(i)
1396             return self.wallet.get_unspent_coins(domain)
1397
1398
1399     def send_from_addresses(self, addrs):
1400         self.set_pay_from( addrs )
1401         self.tabs.setCurrentIndex(1)
1402
1403
1404     def payto(self, addr):
1405         if not addr: return
1406         label = self.wallet.labels.get(addr)
1407         m_addr = label + '  <' + addr + '>' if label else addr
1408         self.tabs.setCurrentIndex(1)
1409         self.payto_e.setText(m_addr)
1410         self.amount_e.setFocus()
1411
1412
1413     def delete_contact(self, x):
1414         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1415             self.wallet.delete_contact(x)
1416             self.wallet.set_label(x, None)
1417             self.update_history_tab()
1418             self.update_contacts_tab()
1419             self.update_completions()
1420
1421
1422     def create_contact_menu(self, position):
1423         item = self.contacts_list.itemAt(position)
1424         menu = QMenu()
1425         if not item:
1426             menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1427         else:
1428             addr = unicode(item.text(0))
1429             label = unicode(item.text(1))
1430             is_editable = item.data(0,32).toBool()
1431             payto_addr = item.data(0,33).toString()
1432             menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1433             menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1434             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1435             if is_editable:
1436                 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1437                 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1438
1439         run_hook('create_contact_menu', menu, item)
1440         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1441
1442     def delete_invoice(self, key):
1443         self.invoices.pop(key)
1444         self.wallet.storage.put('invoices', self.invoices)
1445         self.update_invoices_tab()
1446
1447     def show_invoice(self, key):
1448         from electrum.paymentrequest import PaymentRequest
1449         domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1450         pr = PaymentRequest(self.config)
1451         pr.read_file(key)
1452         pr.domain = domain
1453         pr.verify()
1454         self.show_pr_details(pr)
1455
1456     def show_pr_details(self, pr):
1457         msg = 'Domain: ' + pr.domain
1458         msg += '\nStatus: ' + pr.get_status()
1459         msg += '\nMemo: ' + pr.get_memo()
1460         msg += '\nPayment URL: ' + pr.payment_url
1461         msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1462         QMessageBox.information(self, 'Invoice', msg , 'OK')
1463
1464     def do_pay_invoice(self, key):
1465         from electrum.paymentrequest import PaymentRequest
1466         domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1467         pr = PaymentRequest(self.config)
1468         pr.read_file(key)
1469         pr.domain = domain
1470         self.payment_request = pr
1471         self.prepare_for_payment_request()
1472         if pr.verify():
1473             self.payment_request_ok()
1474         else:
1475             self.payment_request_error()
1476             
1477
1478     def create_invoice_menu(self, position):
1479         item = self.invoices_list.itemAt(position)
1480         if not item:
1481             return
1482         k = self.invoices_list.indexOfTopLevelItem(item)
1483         key = self.invoices.keys()[k]
1484         domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1485         menu = QMenu()
1486         menu.addAction(_("Details"), lambda: self.show_invoice(key))
1487         if status == PR_UNPAID:
1488             menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1489         menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1490         menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1491
1492
1493     def update_address_item(self, item):
1494         item.setFont(0, QFont(MONOSPACE_FONT))
1495         address = str(item.data(0,0).toString())
1496         label = self.wallet.labels.get(address,'')
1497         item.setData(1,0,label)
1498         item.setData(0,32, True) # is editable
1499
1500         run_hook('update_address_item', address, item)
1501
1502         if not self.wallet.is_mine(address): return
1503
1504         c, u = self.wallet.get_addr_balance(address)
1505         balance = self.format_amount(c + u)
1506         item.setData(2,0,balance)
1507
1508         if address in self.wallet.frozen_addresses:
1509             item.setBackgroundColor(0, QColor('lightblue'))
1510
1511
1512     def update_address_tab(self):
1513         l = self.address_list
1514         # extend the syntax for consistency
1515         l.addChild = l.addTopLevelItem
1516         l.insertChild = l.insertTopLevelItem
1517
1518         l.clear()
1519
1520         accounts = self.wallet.get_accounts()
1521         if self.current_account is None:
1522             account_items = sorted(accounts.items())
1523         else:
1524             account_items = [(self.current_account, accounts.get(self.current_account))]
1525
1526
1527         for k, account in account_items:
1528
1529             if len(accounts) > 1:
1530                 name = self.wallet.get_account_name(k)
1531                 c,u = self.wallet.get_account_balance(k)
1532                 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1533                 l.addTopLevelItem(account_item)
1534                 account_item.setExpanded(self.accounts_expanded.get(k, True))
1535                 account_item.setData(0, 32, k)
1536             else:
1537                 account_item = l
1538
1539             sequences = [0,1] if account.has_change() else [0]
1540             for is_change in sequences:
1541                 if len(sequences) > 1:
1542                     name = _("Receiving") if not is_change else _("Change")
1543                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1544                     account_item.addChild(seq_item)
1545                     if not is_change: 
1546                         seq_item.setExpanded(True)
1547                 else:
1548                     seq_item = account_item
1549                     
1550                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1551                 used_flag = False
1552
1553                 is_red = False
1554                 gap = 0
1555
1556                 for address in account.get_addresses(is_change):
1557
1558                     num, is_used = self.wallet.is_used(address)
1559                     if num == 0:
1560                         gap += 1
1561                         if gap > self.wallet.gap_limit:
1562                             is_red = True
1563                     else:
1564                         gap = 0
1565
1566                     item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1567                     self.update_address_item(item)
1568                     if is_red:
1569                         item.setBackgroundColor(1, QColor('red'))
1570
1571                     if is_used:
1572                         if not used_flag:
1573                             seq_item.insertChild(0,used_item)
1574                             used_flag = True
1575                         used_item.addChild(item)
1576                     else:
1577                         seq_item.addChild(item)
1578
1579         # we use column 1 because column 0 may be hidden
1580         l.setCurrentItem(l.topLevelItem(0),1)
1581
1582
1583     def update_contacts_tab(self):
1584         l = self.contacts_list
1585         l.clear()
1586
1587         for address in self.wallet.addressbook:
1588             label = self.wallet.labels.get(address,'')
1589             n = self.wallet.get_num_tx(address)
1590             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1591             item.setFont(0, QFont(MONOSPACE_FONT))
1592             # 32 = label can be edited (bool)
1593             item.setData(0,32, True)
1594             # 33 = payto string
1595             item.setData(0,33, address)
1596             l.addTopLevelItem(item)
1597
1598         run_hook('update_contacts_tab', l)
1599         l.setCurrentItem(l.topLevelItem(0))
1600
1601
1602
1603     def create_console_tab(self):
1604         from console import Console
1605         self.console = console = Console()
1606         return console
1607
1608
1609     def update_console(self):
1610         console = self.console
1611         console.history = self.config.get("console-history",[])
1612         console.history_index = len(console.history)
1613
1614         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1615         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1616
1617         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1618         methods = {}
1619         def mkfunc(f, method):
1620             return lambda *args: apply( f, (method, args, self.password_dialog ))
1621         for m in dir(c):
1622             if m[0]=='_' or m in ['network','wallet']: continue
1623             methods[m] = mkfunc(c._run, m)
1624
1625         console.updateNamespace(methods)
1626
1627
1628     def change_account(self,s):
1629         if s == _("All accounts"):
1630             self.current_account = None
1631         else:
1632             accounts = self.wallet.get_account_names()
1633             for k, v in accounts.items():
1634                 if v == s:
1635                     self.current_account = k
1636         self.update_history_tab()
1637         self.update_status()
1638         self.update_address_tab()
1639         self.update_receive_tab()
1640
1641     def create_status_bar(self):
1642
1643         sb = QStatusBar()
1644         sb.setFixedHeight(35)
1645         qtVersion = qVersion()
1646
1647         self.balance_label = QLabel("")
1648         sb.addWidget(self.balance_label)
1649
1650         from version_getter import UpdateLabel
1651         self.updatelabel = UpdateLabel(self.config, sb)
1652
1653         self.account_selector = QComboBox()
1654         self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1655         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1656         sb.addPermanentWidget(self.account_selector)
1657
1658         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1659             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1660
1661         self.lock_icon = QIcon()
1662         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1663         sb.addPermanentWidget( self.password_button )
1664
1665         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1666         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1667         sb.addPermanentWidget( self.seed_button )
1668         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1669         sb.addPermanentWidget( self.status_button )
1670
1671         run_hook('create_status_bar', (sb,))
1672
1673         self.setStatusBar(sb)
1674
1675
1676     def update_lock_icon(self):
1677         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1678         self.password_button.setIcon( icon )
1679
1680
1681     def update_buttons_on_seed(self):
1682         if self.wallet.has_seed():
1683            self.seed_button.show()
1684         else:
1685            self.seed_button.hide()
1686
1687         if not self.wallet.is_watching_only():
1688            self.password_button.show()
1689            self.send_button.setText(_("Send"))
1690         else:
1691            self.password_button.hide()
1692            self.send_button.setText(_("Create unsigned transaction"))
1693
1694
1695     def change_password_dialog(self):
1696         from password_dialog import PasswordDialog
1697         d = PasswordDialog(self.wallet, self)
1698         d.run()
1699         self.update_lock_icon()
1700
1701
1702     def new_contact_dialog(self):
1703
1704         d = QDialog(self)
1705         d.setWindowTitle(_("New Contact"))
1706         vbox = QVBoxLayout(d)
1707         vbox.addWidget(QLabel(_('New Contact')+':'))
1708
1709         grid = QGridLayout()
1710         line1 = QLineEdit()
1711         line2 = QLineEdit()
1712         grid.addWidget(QLabel(_("Address")), 1, 0)
1713         grid.addWidget(line1, 1, 1)
1714         grid.addWidget(QLabel(_("Name")), 2, 0)
1715         grid.addWidget(line2, 2, 1)
1716
1717         vbox.addLayout(grid)
1718         vbox.addLayout(ok_cancel_buttons(d))
1719
1720         if not d.exec_():
1721             return
1722
1723         address = str(line1.text())
1724         label = unicode(line2.text())
1725
1726         if not is_valid(address):
1727             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1728             return
1729
1730         self.wallet.add_contact(address)
1731         if label:
1732             self.wallet.set_label(address, label)
1733
1734         self.update_contacts_tab()
1735         self.update_history_tab()
1736         self.update_completions()
1737         self.tabs.setCurrentIndex(3)
1738
1739
1740     @protected
1741     def new_account_dialog(self, password):
1742
1743         dialog = QDialog(self)
1744         dialog.setModal(1)
1745         dialog.setWindowTitle(_("New Account"))
1746
1747         vbox = QVBoxLayout()
1748         vbox.addWidget(QLabel(_('Account name')+':'))
1749         e = QLineEdit()
1750         vbox.addWidget(e)
1751         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1752             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1753         l = QLabel(msg)
1754         l.setWordWrap(True)
1755         vbox.addWidget(l)
1756
1757         vbox.addLayout(ok_cancel_buttons(dialog))
1758         dialog.setLayout(vbox)
1759         r = dialog.exec_()
1760         if not r: return
1761
1762         name = str(e.text())
1763         if not name: return
1764
1765         self.wallet.create_pending_account(name, password)
1766         self.update_address_tab()
1767         self.tabs.setCurrentIndex(2)
1768
1769
1770
1771
1772     def show_master_public_keys(self):
1773
1774         dialog = QDialog(self)
1775         dialog.setModal(1)
1776         dialog.setWindowTitle(_("Master Public Keys"))
1777
1778         main_layout = QGridLayout()
1779         mpk_dict = self.wallet.get_master_public_keys()
1780         i = 0
1781         for key, value in mpk_dict.items():
1782             main_layout.addWidget(QLabel(key), i, 0)
1783             mpk_text = QTextEdit()
1784             mpk_text.setReadOnly(True)
1785             mpk_text.setMaximumHeight(170)
1786             mpk_text.setText(value)
1787             main_layout.addWidget(mpk_text, i + 1, 0)
1788             i += 2
1789
1790         vbox = QVBoxLayout()
1791         vbox.addLayout(main_layout)
1792         vbox.addLayout(close_button(dialog))
1793
1794         dialog.setLayout(vbox)
1795         dialog.exec_()
1796
1797
1798     @protected
1799     def show_seed_dialog(self, password):
1800         if not self.wallet.has_seed():
1801             QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1802             return
1803
1804         try:
1805             mnemonic = self.wallet.get_mnemonic(password)
1806         except Exception:
1807             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1808             return
1809         from seed_dialog import SeedDialog
1810         d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1811         d.exec_()
1812
1813
1814
1815     def show_qrcode(self, data, title = _("QR code")):
1816         if not data: 
1817             return
1818         d = QRDialog(data, self, title)
1819         d.exec_()
1820
1821
1822     def do_protect(self, func, args):
1823         if self.wallet.use_encryption:
1824             password = self.password_dialog()
1825             if not password:
1826                 return
1827         else:
1828             password = None
1829
1830         if args != (False,):
1831             args = (self,) + args + (password,)
1832         else:
1833             args = (self,password)
1834         apply( func, args)
1835
1836
1837     def show_public_keys(self, address):
1838         if not address: return
1839         try:
1840             pubkey_list = self.wallet.get_public_keys(address)
1841         except Exception as e:
1842             traceback.print_exc(file=sys.stdout)
1843             self.show_message(str(e))
1844             return
1845
1846         d = QDialog(self)
1847         d.setMinimumSize(600, 200)
1848         d.setModal(1)
1849         vbox = QVBoxLayout()
1850         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1851         vbox.addWidget( QLabel(_("Public key") + ':'))
1852         keys = QRTextEdit()
1853         keys.setReadOnly(True)
1854         keys.setText('\n'.join(pubkey_list))
1855         vbox.addWidget(keys)
1856         vbox.addLayout(close_button(d))
1857         d.setLayout(vbox)
1858         d.exec_()
1859
1860     @protected
1861     def show_private_key(self, address, password):
1862         if not address: return
1863         try:
1864             pk_list = self.wallet.get_private_key(address, password)
1865         except Exception as e:
1866             traceback.print_exc(file=sys.stdout)
1867             self.show_message(str(e))
1868             return
1869
1870         d = QDialog(self)
1871         d.setMinimumSize(600, 200)
1872         d.setModal(1)
1873         vbox = QVBoxLayout()
1874         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1875         vbox.addWidget( QLabel(_("Private key") + ':'))
1876         keys = QRTextEdit()
1877         keys.setReadOnly(True)
1878         keys.setText('\n'.join(pk_list))
1879         vbox.addWidget(keys)
1880         vbox.addLayout(close_button(d))
1881         d.setLayout(vbox)
1882         d.exec_()
1883
1884
1885     @protected
1886     def do_sign(self, address, message, signature, password):
1887         message = unicode(message.toPlainText())
1888         message = message.encode('utf-8')
1889         try:
1890             sig = self.wallet.sign_message(str(address.text()), message, password)
1891             signature.setText(sig)
1892         except Exception as e:
1893             self.show_message(str(e))
1894
1895     def do_verify(self, address, message, signature):
1896         message = unicode(message.toPlainText())
1897         message = message.encode('utf-8')
1898         if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1899             self.show_message(_("Signature verified"))
1900         else:
1901             self.show_message(_("Error: wrong signature"))
1902
1903
1904     def sign_verify_message(self, address=''):
1905         d = QDialog(self)
1906         d.setModal(1)
1907         d.setWindowTitle(_('Sign/verify Message'))
1908         d.setMinimumSize(410, 290)
1909
1910         layout = QGridLayout(d)
1911
1912         message_e = QTextEdit()
1913         layout.addWidget(QLabel(_('Message')), 1, 0)
1914         layout.addWidget(message_e, 1, 1)
1915         layout.setRowStretch(2,3)
1916
1917         address_e = QLineEdit()
1918         address_e.setText(address)
1919         layout.addWidget(QLabel(_('Address')), 2, 0)
1920         layout.addWidget(address_e, 2, 1)
1921
1922         signature_e = QTextEdit()
1923         layout.addWidget(QLabel(_('Signature')), 3, 0)
1924         layout.addWidget(signature_e, 3, 1)
1925         layout.setRowStretch(3,1)
1926
1927         hbox = QHBoxLayout()
1928
1929         b = QPushButton(_("Sign"))
1930         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1931         hbox.addWidget(b)
1932
1933         b = QPushButton(_("Verify"))
1934         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1935         hbox.addWidget(b)
1936
1937         b = QPushButton(_("Close"))
1938         b.clicked.connect(d.accept)
1939         hbox.addWidget(b)
1940         layout.addLayout(hbox, 4, 1)
1941         d.exec_()
1942
1943
1944     @protected
1945     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1946         try:
1947             decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1948             message_e.setText(decrypted)
1949         except Exception as e:
1950             self.show_message(str(e))
1951
1952
1953     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1954         message = unicode(message_e.toPlainText())
1955         message = message.encode('utf-8')
1956         try:
1957             encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1958             encrypted_e.setText(encrypted)
1959         except Exception as e:
1960             self.show_message(str(e))
1961
1962
1963
1964     def encrypt_message(self, address = ''):
1965         d = QDialog(self)
1966         d.setModal(1)
1967         d.setWindowTitle(_('Encrypt/decrypt Message'))
1968         d.setMinimumSize(610, 490)
1969
1970         layout = QGridLayout(d)
1971
1972         message_e = QTextEdit()
1973         layout.addWidget(QLabel(_('Message')), 1, 0)
1974         layout.addWidget(message_e, 1, 1)
1975         layout.setRowStretch(2,3)
1976
1977         pubkey_e = QLineEdit()
1978         if address:
1979             pubkey = self.wallet.getpubkeys(address)[0]
1980             pubkey_e.setText(pubkey)
1981         layout.addWidget(QLabel(_('Public key')), 2, 0)
1982         layout.addWidget(pubkey_e, 2, 1)
1983
1984         encrypted_e = QTextEdit()
1985         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1986         layout.addWidget(encrypted_e, 3, 1)
1987         layout.setRowStretch(3,1)
1988
1989         hbox = QHBoxLayout()
1990         b = QPushButton(_("Encrypt"))
1991         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1992         hbox.addWidget(b)
1993
1994         b = QPushButton(_("Decrypt"))
1995         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1996         hbox.addWidget(b)
1997
1998         b = QPushButton(_("Close"))
1999         b.clicked.connect(d.accept)
2000         hbox.addWidget(b)
2001
2002         layout.addLayout(hbox, 4, 1)
2003         d.exec_()
2004
2005
2006     def question(self, msg):
2007         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2008
2009     def show_message(self, msg):
2010         QMessageBox.information(self, _('Message'), msg, _('OK'))
2011
2012     def password_dialog(self, msg=None):
2013         d = QDialog(self)
2014         d.setModal(1)
2015         d.setWindowTitle(_("Enter Password"))
2016
2017         pw = QLineEdit()
2018         pw.setEchoMode(2)
2019
2020         vbox = QVBoxLayout()
2021         if not msg:
2022             msg = _('Please enter your password')
2023         vbox.addWidget(QLabel(msg))
2024
2025         grid = QGridLayout()
2026         grid.setSpacing(8)
2027         grid.addWidget(QLabel(_('Password')), 1, 0)
2028         grid.addWidget(pw, 1, 1)
2029         vbox.addLayout(grid)
2030
2031         vbox.addLayout(ok_cancel_buttons(d))
2032         d.setLayout(vbox)
2033
2034         run_hook('password_dialog', pw, grid, 1)
2035         if not d.exec_(): return
2036         return unicode(pw.text())
2037
2038
2039
2040
2041
2042
2043
2044
2045     def tx_from_text(self, txt):
2046         "json or raw hexadecimal"
2047         try:
2048             txt.decode('hex')
2049             is_hex = True
2050         except:
2051             is_hex = False
2052
2053         if is_hex:
2054             try:
2055                 return Transaction(txt)
2056             except:
2057                 traceback.print_exc(file=sys.stdout)
2058                 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2059                 return
2060
2061         try:
2062             tx_dict = json.loads(str(txt))
2063             assert "hex" in tx_dict.keys()
2064             tx = Transaction(tx_dict["hex"])
2065             #if tx_dict.has_key("input_info"):
2066             #    input_info = json.loads(tx_dict['input_info'])
2067             #    tx.add_input_info(input_info)
2068             return tx
2069         except Exception:
2070             traceback.print_exc(file=sys.stdout)
2071             QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2072
2073
2074
2075     def read_tx_from_file(self):
2076         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2077         if not fileName:
2078             return
2079         try:
2080             with open(fileName, "r") as f:
2081                 file_content = f.read()
2082         except (ValueError, IOError, os.error), reason:
2083             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2084
2085         return self.tx_from_text(file_content)
2086
2087
2088     @protected
2089     def sign_raw_transaction(self, tx, password):
2090         try:
2091             self.wallet.signrawtransaction(tx, [], password)
2092         except Exception as e:
2093             traceback.print_exc(file=sys.stdout)
2094             QMessageBox.warning(self, _("Error"), str(e))
2095
2096     def do_process_from_text(self):
2097         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2098         if not text:
2099             return
2100         tx = self.tx_from_text(text)
2101         if tx:
2102             self.show_transaction(tx)
2103
2104     def do_process_from_file(self):
2105         tx = self.read_tx_from_file()
2106         if tx:
2107             self.show_transaction(tx)
2108
2109     def do_process_from_txid(self):
2110         from electrum import transaction
2111         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2112         if ok and txid:
2113             r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2114             if r:
2115                 tx = transaction.Transaction(r)
2116                 if tx:
2117                     self.show_transaction(tx)
2118                 else:
2119                     self.show_message("unknown transaction")
2120
2121     def do_process_from_csvReader(self, csvReader):
2122         outputs = []
2123         errors = []
2124         errtext = ""
2125         try:
2126             for position, row in enumerate(csvReader):
2127                 address = row[0]
2128                 if not is_valid(address):
2129                     errors.append((position, address))
2130                     continue
2131                 amount = Decimal(row[1])
2132                 amount = int(100000000*amount)
2133                 outputs.append((address, amount))
2134         except (ValueError, IOError, os.error), reason:
2135             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2136             return
2137         if errors != []:
2138             for x in errors:
2139                 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2140             QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2141             return
2142
2143         try:
2144             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2145         except Exception as e:
2146             self.show_message(str(e))
2147             return
2148
2149         self.show_transaction(tx)
2150
2151     def do_process_from_csv_file(self):
2152         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2153         if not fileName:
2154             return
2155         try:
2156             with open(fileName, "r") as f:
2157                 csvReader = csv.reader(f)
2158                 self.do_process_from_csvReader(csvReader)
2159         except (ValueError, IOError, os.error), reason:
2160             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2161             return
2162
2163     def do_process_from_csv_text(self):
2164         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2165                                + _("Format: address, amount. One output per line"), _("Load CSV"))
2166         if not text:
2167             return
2168         f = StringIO.StringIO(text)
2169         csvReader = csv.reader(f)
2170         self.do_process_from_csvReader(csvReader)
2171
2172
2173
2174     @protected
2175     def export_privkeys_dialog(self, password):
2176         if self.wallet.is_watching_only():
2177             self.show_message(_("This is a watching-only wallet"))
2178             return
2179
2180         d = QDialog(self)
2181         d.setWindowTitle(_('Private keys'))
2182         d.setMinimumSize(850, 300)
2183         vbox = QVBoxLayout(d)
2184
2185         msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."), 
2186                               _("Exposing a single private key can compromise your entire wallet!"), 
2187                               _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2188         vbox.addWidget(QLabel(msg))
2189
2190         e = QTextEdit()
2191         e.setReadOnly(True)
2192         vbox.addWidget(e)
2193
2194         defaultname = 'electrum-private-keys.csv'
2195         select_msg = _('Select file to export your private keys to')
2196         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2197         vbox.addLayout(hbox)
2198
2199         h, b = ok_cancel_buttons2(d, _('Export'))
2200         b.setEnabled(False)
2201         vbox.addLayout(h)
2202
2203         private_keys = {}
2204         addresses = self.wallet.addresses(True)
2205         done = False
2206         def privkeys_thread():
2207             for addr in addresses:
2208                 time.sleep(0.1)
2209                 if done: 
2210                     break
2211                 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2212                 d.emit(SIGNAL('computing_privkeys'))
2213             d.emit(SIGNAL('show_privkeys'))
2214
2215         def show_privkeys():
2216             s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2217             e.setText(s)
2218             b.setEnabled(True)
2219
2220         d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2221         d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2222         threading.Thread(target=privkeys_thread).start()
2223
2224         if not d.exec_():
2225             done = True
2226             return
2227
2228         filename = filename_e.text()
2229         if not filename:
2230             return
2231
2232         try:
2233             self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2234         except (IOError, os.error), reason:
2235             export_error_label = _("Electrum was unable to produce a private key-export.")
2236             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2237
2238         except Exception as e:
2239             self.show_message(str(e))
2240             return
2241
2242         self.show_message(_("Private keys exported."))
2243
2244
2245     def do_export_privkeys(self, fileName, pklist, is_csv):
2246         with open(fileName, "w+") as f:
2247             if is_csv:
2248                 transaction = csv.writer(f)
2249                 transaction.writerow(["address", "private_key"])
2250                 for addr, pk in pklist.items():
2251                     transaction.writerow(["%34s"%addr,pk])
2252             else:
2253                 import json
2254                 f.write(json.dumps(pklist, indent = 4))
2255
2256
2257     def do_import_labels(self):
2258         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2259         if not labelsFile: return
2260         try:
2261             f = open(labelsFile, 'r')
2262             data = f.read()
2263             f.close()
2264             for key, value in json.loads(data).items():
2265                 self.wallet.set_label(key, value)
2266             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2267         except (IOError, os.error), reason:
2268             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2269
2270
2271     def do_export_labels(self):
2272         labels = self.wallet.labels
2273         try:
2274             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2275             if fileName:
2276                 with open(fileName, 'w+') as f:
2277                     json.dump(labels, f)
2278                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2279         except (IOError, os.error), reason:
2280             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2281
2282
2283     def export_history_dialog(self):
2284
2285         d = QDialog(self)
2286         d.setWindowTitle(_('Export History'))
2287         d.setMinimumSize(400, 200)
2288         vbox = QVBoxLayout(d)
2289
2290         defaultname = os.path.expanduser('~/electrum-history.csv')
2291         select_msg = _('Select file to export your wallet transactions to')
2292
2293         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2294         vbox.addLayout(hbox)
2295
2296         vbox.addStretch(1)
2297
2298         h, b = ok_cancel_buttons2(d, _('Export'))
2299         vbox.addLayout(h)
2300         if not d.exec_():
2301             return
2302
2303         filename = filename_e.text()
2304         if not filename:
2305             return
2306
2307         try:
2308             self.do_export_history(self.wallet, filename, csv_button.isChecked())
2309         except (IOError, os.error), reason:
2310             export_error_label = _("Electrum was unable to produce a transaction export.")
2311             QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2312             return
2313
2314         QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2315
2316
2317     def do_export_history(self, wallet, fileName, is_csv):
2318         history = wallet.get_tx_history()
2319         lines = []
2320         for item in history:
2321             tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2322             if confirmations:
2323                 if timestamp is not None:
2324                     try:
2325                         time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2326                     except [RuntimeError, TypeError, NameError] as reason:
2327                         time_string = "unknown"
2328                         pass
2329                 else:
2330                     time_string = "unknown"
2331             else:
2332                 time_string = "pending"
2333
2334             if value is not None:
2335                 value_string = format_satoshis(value, True)
2336             else:
2337                 value_string = '--'
2338
2339             if fee is not None:
2340                 fee_string = format_satoshis(fee, True)
2341             else:
2342                 fee_string = '0'
2343
2344             if tx_hash:
2345                 label, is_default_label = wallet.get_label(tx_hash)
2346                 label = label.encode('utf-8')
2347             else:
2348                 label = ""
2349
2350             balance_string = format_satoshis(balance, False)
2351             if is_csv:
2352                 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2353             else:
2354                 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2355
2356         with open(fileName, "w+") as f:
2357             if is_csv:
2358                 transaction = csv.writer(f)
2359                 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2360                 for line in lines:
2361                     transaction.writerow(line)
2362             else:
2363                 import json
2364                 f.write(json.dumps(lines, indent = 4))
2365
2366
2367     def sweep_key_dialog(self):
2368         d = QDialog(self)
2369         d.setWindowTitle(_('Sweep private keys'))
2370         d.setMinimumSize(600, 300)
2371
2372         vbox = QVBoxLayout(d)
2373         vbox.addWidget(QLabel(_("Enter private keys")))
2374
2375         keys_e = QTextEdit()
2376         keys_e.setTabChangesFocus(True)
2377         vbox.addWidget(keys_e)
2378
2379         h, address_e = address_field(self.wallet.addresses())
2380         vbox.addLayout(h)
2381
2382         vbox.addStretch(1)
2383         hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2384         vbox.addLayout(hbox)
2385         button.setEnabled(False)
2386
2387         def get_address():
2388             addr = str(address_e.text())
2389             if bitcoin.is_address(addr):
2390                 return addr
2391
2392         def get_pk():
2393             pk = str(keys_e.toPlainText()).strip()
2394             if Wallet.is_private_key(pk):
2395                 return pk.split()
2396
2397         f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2398         keys_e.textChanged.connect(f)
2399         address_e.textChanged.connect(f)
2400         if not d.exec_():
2401             return
2402
2403         fee = self.wallet.fee
2404         tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2405         self.show_transaction(tx)
2406
2407
2408     @protected
2409     def do_import_privkey(self, password):
2410         if not self.wallet.has_imported_keys():
2411             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2412                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2413                                          + _('Are you sure you understand what you are doing?'), 3, 4)
2414             if r == 4: return
2415
2416         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2417         if not text: return
2418
2419         text = str(text).split()
2420         badkeys = []
2421         addrlist = []
2422         for key in text:
2423             try:
2424                 addr = self.wallet.import_key(key, password)
2425             except Exception as e:
2426                 badkeys.append(key)
2427                 continue
2428             if not addr:
2429                 badkeys.append(key)
2430             else:
2431                 addrlist.append(addr)
2432         if addrlist:
2433             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2434         if badkeys:
2435             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2436         self.update_address_tab()
2437         self.update_history_tab()
2438
2439
2440     def settings_dialog(self):
2441         d = QDialog(self)
2442         d.setWindowTitle(_('Electrum Settings'))
2443         d.setModal(1)
2444         vbox = QVBoxLayout()
2445         grid = QGridLayout()
2446         grid.setColumnStretch(0,1)
2447
2448         nz_label = QLabel(_('Display zeros') + ':')
2449         grid.addWidget(nz_label, 0, 0)
2450         nz_e = AmountEdit(None,True)
2451         nz_e.setText("%d"% self.num_zeros)
2452         grid.addWidget(nz_e, 0, 1)
2453         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2454         grid.addWidget(HelpButton(msg), 0, 2)
2455         if not self.config.is_modifiable('num_zeros'):
2456             for w in [nz_e, nz_label]: w.setEnabled(False)
2457
2458         lang_label=QLabel(_('Language') + ':')
2459         grid.addWidget(lang_label, 1, 0)
2460         lang_combo = QComboBox()
2461         from electrum.i18n import languages
2462         lang_combo.addItems(languages.values())
2463         try:
2464             index = languages.keys().index(self.config.get("language",''))
2465         except Exception:
2466             index = 0
2467         lang_combo.setCurrentIndex(index)
2468         grid.addWidget(lang_combo, 1, 1)
2469         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2470         if not self.config.is_modifiable('language'):
2471             for w in [lang_combo, lang_label]: w.setEnabled(False)
2472
2473
2474         fee_label = QLabel(_('Transaction fee') + ':')
2475         grid.addWidget(fee_label, 2, 0)
2476         fee_e = BTCAmountEdit(self.get_decimal_point)
2477         fee_e.setAmount(self.wallet.fee)
2478         grid.addWidget(fee_e, 2, 1)
2479         msg = _('Fee per kilobyte of transaction.') + '\n' \
2480             + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2481         grid.addWidget(HelpButton(msg), 2, 2)
2482         if not self.config.is_modifiable('fee_per_kb'):
2483             for w in [fee_e, fee_label]: w.setEnabled(False)
2484
2485         units = ['BTC', 'mBTC']
2486         unit_label = QLabel(_('Base unit') + ':')
2487         grid.addWidget(unit_label, 3, 0)
2488         unit_combo = QComboBox()
2489         unit_combo.addItems(units)
2490         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2491         grid.addWidget(unit_combo, 3, 1)
2492         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2493                                              + '\n1BTC=1000mBTC.\n' \
2494                                              + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2495
2496         usechange_cb = QCheckBox(_('Use change addresses'))
2497         usechange_cb.setChecked(self.wallet.use_change)
2498         grid.addWidget(usechange_cb, 4, 0)
2499         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2500         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2501
2502         block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2503         block_ex_label = QLabel(_('Online Block Explorer') + ':')
2504         grid.addWidget(block_ex_label, 5, 0)
2505         block_ex_combo = QComboBox()
2506         block_ex_combo.addItems(block_explorers)
2507         block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2508         grid.addWidget(block_ex_combo, 5, 1)
2509         grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2510
2511         show_tx = self.config.get('show_before_broadcast', False)
2512         showtx_cb = QCheckBox(_('Show before broadcast'))
2513         showtx_cb.setChecked(show_tx)
2514         grid.addWidget(showtx_cb, 6, 0)
2515         grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2516
2517         vbox.addLayout(grid)
2518         vbox.addStretch(1)
2519         vbox.addLayout(ok_cancel_buttons(d))
2520         d.setLayout(vbox)
2521
2522         # run the dialog
2523         if not d.exec_(): return
2524
2525         fee = fee_e.get_amount()
2526         if fee is None:
2527             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2528             return
2529
2530         self.wallet.set_fee(fee)
2531
2532         nz = unicode(nz_e.text())
2533         try:
2534             nz = int( nz )
2535             if nz>8: nz=8
2536         except Exception:
2537             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2538             return
2539
2540         if self.num_zeros != nz:
2541             self.num_zeros = nz
2542             self.config.set_key('num_zeros', nz, True)
2543             self.update_history_tab()
2544             self.update_address_tab()
2545
2546         usechange_result = usechange_cb.isChecked()
2547         if self.wallet.use_change != usechange_result:
2548             self.wallet.use_change = usechange_result
2549             self.wallet.storage.put('use_change', self.wallet.use_change)
2550
2551         if showtx_cb.isChecked() != show_tx:
2552             self.config.set_key('show_before_broadcast', not show_tx)
2553
2554         unit_result = units[unit_combo.currentIndex()]
2555         if self.base_unit() != unit_result:
2556             self.decimal_point = 8 if unit_result == 'BTC' else 5
2557             self.config.set_key('decimal_point', self.decimal_point, True)
2558             self.update_history_tab()
2559             self.update_status()
2560
2561         need_restart = False
2562
2563         lang_request = languages.keys()[lang_combo.currentIndex()]
2564         if lang_request != self.config.get('language'):
2565             self.config.set_key("language", lang_request, True)
2566             need_restart = True
2567
2568         be_result = block_explorers[block_ex_combo.currentIndex()]
2569         self.config.set_key('block_explorer', be_result, True)
2570
2571         run_hook('close_settings_dialog')
2572
2573         if need_restart:
2574             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2575
2576
2577     def run_network_dialog(self):
2578         if not self.network:
2579             return
2580         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2581
2582     def closeEvent(self, event):
2583         self.tray.hide()
2584         self.config.set_key("is_maximized", self.isMaximized())
2585         if not self.isMaximized():
2586             g = self.geometry()
2587             self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2588         self.save_column_widths()
2589         self.config.set_key("console-history", self.console.history[-50:], True)
2590         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2591         event.accept()
2592
2593
2594     def plugins_dialog(self):
2595         from electrum.plugins import plugins
2596
2597         d = QDialog(self)
2598         d.setWindowTitle(_('Electrum Plugins'))
2599         d.setModal(1)
2600
2601         vbox = QVBoxLayout(d)
2602
2603         # plugins
2604         scroll = QScrollArea()
2605         scroll.setEnabled(True)
2606         scroll.setWidgetResizable(True)
2607         scroll.setMinimumSize(400,250)
2608         vbox.addWidget(scroll)
2609
2610         w = QWidget()
2611         scroll.setWidget(w)
2612         w.setMinimumHeight(len(plugins)*35)
2613
2614         grid = QGridLayout()
2615         grid.setColumnStretch(0,1)
2616         w.setLayout(grid)
2617
2618         def do_toggle(cb, p, w):
2619             r = p.toggle()
2620             cb.setChecked(r)
2621             if w: w.setEnabled(r)
2622
2623         def mk_toggle(cb, p, w):
2624             return lambda: do_toggle(cb,p,w)
2625
2626         for i, p in enumerate(plugins):
2627             try:
2628                 cb = QCheckBox(p.fullname())
2629                 cb.setDisabled(not p.is_available())
2630                 cb.setChecked(p.is_enabled())
2631                 grid.addWidget(cb, i, 0)
2632                 if p.requires_settings():
2633                     w = p.settings_widget(self)
2634                     w.setEnabled( p.is_enabled() )
2635                     grid.addWidget(w, i, 1)
2636                 else:
2637                     w = None
2638                 cb.clicked.connect(mk_toggle(cb,p,w))
2639                 grid.addWidget(HelpButton(p.description()), i, 2)
2640             except Exception:
2641                 print_msg(_("Error: cannot display plugin"), p)
2642                 traceback.print_exc(file=sys.stdout)
2643         grid.setRowStretch(i+1,1)
2644
2645         vbox.addLayout(close_button(d))
2646
2647         d.exec_()
2648
2649
2650     def show_account_details(self, k):
2651         account = self.wallet.accounts[k]
2652
2653         d = QDialog(self)
2654         d.setWindowTitle(_('Account Details'))
2655         d.setModal(1)
2656
2657         vbox = QVBoxLayout(d)
2658         name = self.wallet.get_account_name(k)
2659         label = QLabel('Name: ' + name)
2660         vbox.addWidget(label)
2661
2662         vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2663
2664         vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2665
2666         vbox.addWidget(QLabel(_('Master Public Key:')))
2667
2668         text = QTextEdit()
2669         text.setReadOnly(True)
2670         text.setMaximumHeight(170)
2671         vbox.addWidget(text)
2672
2673         mpk_text = '\n'.join( account.get_master_pubkeys() )
2674         text.setText(mpk_text)
2675
2676         vbox.addLayout(close_button(d))
2677         d.exec_()