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