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