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