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