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