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