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