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