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