51c35aabbba22282ff77f5417cb38e8f4d9a986f
[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         pr = self.gui_object.payment_request
895         self.payto_help.show()
896         self.payto_help.set_alt(pr.status)
897         self.payto_e.setGreen()
898         self.payto_e.setText(pr.domain)
899         self.amount_e.setText(self.format_amount(pr.get_amount()))
900         self.message_e.setText(pr.memo)
901
902     def payment_request_error(self):
903         self.do_clear()
904         self.show_message(self.gui_object.payment_request.error)
905         self.gui_object.payment_request = None
906
907     def set_send(self, address, amount, label, message):
908
909         if label and self.wallet.labels.get(address) != label:
910             if self.question('Give label "%s" to address %s ?'%(label,address)):
911                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
912                     self.wallet.addressbook.append(address)
913                 self.wallet.set_label(address, label)
914
915         self.tabs.setCurrentIndex(1)
916         label = self.wallet.labels.get(address)
917         m_addr = label + '  <'+ address +'>' if label else address
918         self.payto_e.setText(m_addr)
919
920         self.message_e.setText(message)
921         if amount:
922             self.amount_e.setText(amount)
923
924
925     def do_clear(self):
926         self.payto_sig.setVisible(False)
927         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
928             e.setText('')
929             e.setFrozen(False)
930
931         for h in [self.payto_help, self.amount_help, self.message_help]:
932             h.show()
933
934         self.payto_help.set_alt(None)
935
936         self.set_pay_from([])
937         self.update_status()
938
939
940
941     def set_addrs_frozen(self,addrs,freeze):
942         for addr in addrs:
943             if not addr: continue
944             if addr in self.wallet.frozen_addresses and not freeze:
945                 self.wallet.unfreeze(addr)
946             elif addr not in self.wallet.frozen_addresses and freeze:
947                 self.wallet.freeze(addr)
948         self.update_receive_tab()
949
950
951
952     def create_list_tab(self, headers):
953         "generic tab creation method"
954         l = MyTreeWidget(self)
955         l.setColumnCount( len(headers) )
956         l.setHeaderLabels( headers )
957
958         w = QWidget()
959         vbox = QVBoxLayout()
960         w.setLayout(vbox)
961
962         vbox.setMargin(0)
963         vbox.setSpacing(0)
964         vbox.addWidget(l)
965         buttons = QWidget()
966         vbox.addWidget(buttons)
967
968         hbox = QHBoxLayout()
969         hbox.setMargin(0)
970         hbox.setSpacing(0)
971         buttons.setLayout(hbox)
972
973         return l,w,hbox
974
975
976     def create_receive_tab(self):
977         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
978         l.setContextMenuPolicy(Qt.CustomContextMenu)
979         l.customContextMenuRequested.connect(self.create_receive_menu)
980         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
981         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
982         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
983         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
984         self.receive_list = l
985         self.receive_buttons_hbox = hbox
986         hbox.addStretch(1)
987         return w
988
989
990
991
992     def save_column_widths(self):
993         self.column_widths["receive"] = []
994         for i in range(self.receive_list.columnCount() -1):
995             self.column_widths["receive"].append(self.receive_list.columnWidth(i))
996
997         self.column_widths["history"] = []
998         for i in range(self.history_list.columnCount() - 1):
999             self.column_widths["history"].append(self.history_list.columnWidth(i))
1000
1001         self.column_widths["contacts"] = []
1002         for i in range(self.contacts_list.columnCount() - 1):
1003             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1004
1005         self.config.set_key("column_widths_2", self.column_widths, True)
1006
1007
1008     def create_contacts_tab(self):
1009         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1010         l.setContextMenuPolicy(Qt.CustomContextMenu)
1011         l.customContextMenuRequested.connect(self.create_contact_menu)
1012         for i,width in enumerate(self.column_widths['contacts']):
1013             l.setColumnWidth(i, width)
1014
1015         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1016         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1017         self.contacts_list = l
1018         self.contacts_buttons_hbox = hbox
1019         hbox.addStretch(1)
1020         return w
1021
1022
1023     def delete_imported_key(self, addr):
1024         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1025             self.wallet.delete_imported_key(addr)
1026             self.update_receive_tab()
1027             self.update_history_tab()
1028
1029     def edit_account_label(self, k):
1030         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1031         if ok:
1032             label = unicode(text)
1033             self.wallet.set_label(k,label)
1034             self.update_receive_tab()
1035
1036     def account_set_expanded(self, item, k, b):
1037         item.setExpanded(b)
1038         self.accounts_expanded[k] = b
1039
1040     def create_account_menu(self, position, k, item):
1041         menu = QMenu()
1042         if item.isExpanded():
1043             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1044         else:
1045             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1046         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1047         if self.wallet.seed_version > 4:
1048             menu.addAction(_("View details"), lambda: self.show_account_details(k))
1049         if self.wallet.account_is_pending(k):
1050             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1051         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1052
1053     def delete_pending_account(self, k):
1054         self.wallet.delete_pending_account(k)
1055         self.update_receive_tab()
1056
1057     def create_receive_menu(self, position):
1058         # fixme: this function apparently has a side effect.
1059         # if it is not called the menu pops up several times
1060         #self.receive_list.selectedIndexes()
1061
1062         selected = self.receive_list.selectedItems()
1063         multi_select = len(selected) > 1
1064         addrs = [unicode(item.text(0)) for item in selected]
1065         if not multi_select:
1066             item = self.receive_list.itemAt(position)
1067             if not item: return
1068
1069             addr = addrs[0]
1070             if not is_valid(addr):
1071                 k = str(item.data(0,32).toString())
1072                 if k:
1073                     self.create_account_menu(position, k, item)
1074                 else:
1075                     item.setExpanded(not item.isExpanded())
1076                 return
1077
1078         menu = QMenu()
1079         if not multi_select:
1080             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1081             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1082             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1083             menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1084             if not self.wallet.is_watching_only():
1085                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1086                 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1087                 #menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1088             if self.wallet.is_imported(addr):
1089                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1090
1091         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1092             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1093         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1094             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1095
1096         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1097             menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1098
1099         run_hook('receive_menu', menu, addrs)
1100         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1101
1102
1103     def get_sendable_balance(self):
1104         return sum(sum(self.wallet.get_addr_balance(a)) for a in self.get_payment_sources())
1105
1106
1107     def get_payment_sources(self):
1108         if self.pay_from:
1109             return self.pay_from
1110         else:
1111             return self.wallet.get_account_addresses(self.current_account)
1112
1113
1114     def send_from_addresses(self, addrs):
1115         self.set_pay_from( addrs )
1116         self.tabs.setCurrentIndex(1)
1117
1118
1119     def payto(self, addr):
1120         if not addr: return
1121         label = self.wallet.labels.get(addr)
1122         m_addr = label + '  <' + addr + '>' if label else addr
1123         self.tabs.setCurrentIndex(1)
1124         self.payto_e.setText(m_addr)
1125         self.amount_e.setFocus()
1126
1127
1128     def delete_contact(self, x):
1129         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1130             self.wallet.delete_contact(x)
1131             self.wallet.set_label(x, None)
1132             self.update_history_tab()
1133             self.update_contacts_tab()
1134             self.update_completions()
1135
1136
1137     def create_contact_menu(self, position):
1138         item = self.contacts_list.itemAt(position)
1139         menu = QMenu()
1140         if not item:
1141             menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1142         else:
1143             addr = unicode(item.text(0))
1144             label = unicode(item.text(1))
1145             is_editable = item.data(0,32).toBool()
1146             payto_addr = item.data(0,33).toString()
1147             menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1148             menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1149             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1150             if is_editable:
1151                 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1152                 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1153
1154         run_hook('create_contact_menu', menu, item)
1155         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1156
1157
1158     def update_receive_item(self, item):
1159         item.setFont(0, QFont(MONOSPACE_FONT))
1160         address = str(item.data(0,0).toString())
1161         label = self.wallet.labels.get(address,'')
1162         item.setData(1,0,label)
1163         item.setData(0,32, True) # is editable
1164
1165         run_hook('update_receive_item', address, item)
1166
1167         if not self.wallet.is_mine(address): return
1168
1169         c, u = self.wallet.get_addr_balance(address)
1170         balance = self.format_amount(c + u)
1171         item.setData(2,0,balance)
1172
1173         if address in self.wallet.frozen_addresses:
1174             item.setBackgroundColor(0, QColor('lightblue'))
1175
1176
1177     def update_receive_tab(self):
1178         l = self.receive_list
1179         # extend the syntax for consistency
1180         l.addChild = l.addTopLevelItem
1181         l.insertChild = l.insertTopLevelItem
1182
1183         l.clear()
1184         for i,width in enumerate(self.column_widths['receive']):
1185             l.setColumnWidth(i, width)
1186
1187         accounts = self.wallet.get_accounts()
1188         if self.current_account is None:
1189             account_items = sorted(accounts.items())
1190         else:
1191             account_items = [(self.current_account, accounts.get(self.current_account))]
1192
1193
1194         for k, account in account_items:
1195
1196             if len(accounts) > 1:
1197                 name = self.wallet.get_account_name(k)
1198                 c,u = self.wallet.get_account_balance(k)
1199                 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1200                 l.addTopLevelItem(account_item)
1201                 account_item.setExpanded(self.accounts_expanded.get(k, True))
1202                 account_item.setData(0, 32, k)
1203             else:
1204                 account_item = l
1205
1206             sequences = [0,1] if account.has_change() else [0]
1207             for is_change in sequences:
1208                 if len(sequences) > 1:
1209                     name = _("Receiving") if not is_change else _("Change")
1210                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1211                     account_item.addChild(seq_item)
1212                     if not is_change: 
1213                         seq_item.setExpanded(True)
1214                 else:
1215                     seq_item = account_item
1216                     
1217                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1218                 used_flag = False
1219
1220                 is_red = False
1221                 gap = 0
1222
1223                 for address in account.get_addresses(is_change):
1224
1225                     num, is_used = self.wallet.is_used(address)
1226                     if num == 0:
1227                         gap += 1
1228                         if gap > self.wallet.gap_limit:
1229                             is_red = True
1230                     else:
1231                         gap = 0
1232
1233                     item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1234                     self.update_receive_item(item)
1235                     if is_red:
1236                         item.setBackgroundColor(1, QColor('red'))
1237
1238                     if is_used:
1239                         if not used_flag:
1240                             seq_item.insertChild(0,used_item)
1241                             used_flag = True
1242                         used_item.addChild(item)
1243                     else:
1244                         seq_item.addChild(item)
1245
1246         # we use column 1 because column 0 may be hidden
1247         l.setCurrentItem(l.topLevelItem(0),1)
1248
1249
1250     def update_contacts_tab(self):
1251         l = self.contacts_list
1252         l.clear()
1253
1254         for address in self.wallet.addressbook:
1255             label = self.wallet.labels.get(address,'')
1256             n = self.wallet.get_num_tx(address)
1257             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1258             item.setFont(0, QFont(MONOSPACE_FONT))
1259             # 32 = label can be edited (bool)
1260             item.setData(0,32, True)
1261             # 33 = payto string
1262             item.setData(0,33, address)
1263             l.addTopLevelItem(item)
1264
1265         run_hook('update_contacts_tab', l)
1266         l.setCurrentItem(l.topLevelItem(0))
1267
1268
1269
1270     def create_console_tab(self):
1271         from console import Console
1272         self.console = console = Console()
1273         return console
1274
1275
1276     def update_console(self):
1277         console = self.console
1278         console.history = self.config.get("console-history",[])
1279         console.history_index = len(console.history)
1280
1281         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1282         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1283
1284         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1285         methods = {}
1286         def mkfunc(f, method):
1287             return lambda *args: apply( f, (method, args, self.password_dialog ))
1288         for m in dir(c):
1289             if m[0]=='_' or m in ['network','wallet']: continue
1290             methods[m] = mkfunc(c._run, m)
1291
1292         console.updateNamespace(methods)
1293
1294
1295     def change_account(self,s):
1296         if s == _("All accounts"):
1297             self.current_account = None
1298         else:
1299             accounts = self.wallet.get_account_names()
1300             for k, v in accounts.items():
1301                 if v == s:
1302                     self.current_account = k
1303         self.update_history_tab()
1304         self.update_status()
1305         self.update_receive_tab()
1306
1307     def create_status_bar(self):
1308
1309         sb = QStatusBar()
1310         sb.setFixedHeight(35)
1311         qtVersion = qVersion()
1312
1313         self.balance_label = QLabel("")
1314         sb.addWidget(self.balance_label)
1315
1316         from version_getter import UpdateLabel
1317         self.updatelabel = UpdateLabel(self.config, sb)
1318
1319         self.account_selector = QComboBox()
1320         self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1321         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1322         sb.addPermanentWidget(self.account_selector)
1323
1324         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1325             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1326
1327         self.lock_icon = QIcon()
1328         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1329         sb.addPermanentWidget( self.password_button )
1330
1331         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1332         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1333         sb.addPermanentWidget( self.seed_button )
1334         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1335         sb.addPermanentWidget( self.status_button )
1336
1337         run_hook('create_status_bar', (sb,))
1338
1339         self.setStatusBar(sb)
1340
1341
1342     def update_lock_icon(self):
1343         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1344         self.password_button.setIcon( icon )
1345
1346
1347     def update_buttons_on_seed(self):
1348         if self.wallet.has_seed():
1349            self.seed_button.show()
1350         else:
1351            self.seed_button.hide()
1352
1353         if not self.wallet.is_watching_only():
1354            self.password_button.show()
1355            self.send_button.setText(_("Send"))
1356         else:
1357            self.password_button.hide()
1358            self.send_button.setText(_("Create unsigned transaction"))
1359
1360
1361     def change_password_dialog(self):
1362         from password_dialog import PasswordDialog
1363         d = PasswordDialog(self.wallet, self)
1364         d.run()
1365         self.update_lock_icon()
1366
1367
1368     def new_contact_dialog(self):
1369
1370         d = QDialog(self)
1371         d.setWindowTitle(_("New Contact"))
1372         vbox = QVBoxLayout(d)
1373         vbox.addWidget(QLabel(_('New Contact')+':'))
1374
1375         grid = QGridLayout()
1376         line1 = QLineEdit()
1377         line2 = QLineEdit()
1378         grid.addWidget(QLabel(_("Address")), 1, 0)
1379         grid.addWidget(line1, 1, 1)
1380         grid.addWidget(QLabel(_("Name")), 2, 0)
1381         grid.addWidget(line2, 2, 1)
1382
1383         vbox.addLayout(grid)
1384         vbox.addLayout(ok_cancel_buttons(d))
1385
1386         if not d.exec_():
1387             return
1388
1389         address = str(line1.text())
1390         label = unicode(line2.text())
1391
1392         if not is_valid(address):
1393             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1394             return
1395
1396         self.wallet.add_contact(address)
1397         if label:
1398             self.wallet.set_label(address, label)
1399
1400         self.update_contacts_tab()
1401         self.update_history_tab()
1402         self.update_completions()
1403         self.tabs.setCurrentIndex(3)
1404
1405
1406     @protected
1407     def new_account_dialog(self, password):
1408
1409         dialog = QDialog(self)
1410         dialog.setModal(1)
1411         dialog.setWindowTitle(_("New Account"))
1412
1413         vbox = QVBoxLayout()
1414         vbox.addWidget(QLabel(_('Account name')+':'))
1415         e = QLineEdit()
1416         vbox.addWidget(e)
1417         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1418             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1419         l = QLabel(msg)
1420         l.setWordWrap(True)
1421         vbox.addWidget(l)
1422
1423         vbox.addLayout(ok_cancel_buttons(dialog))
1424         dialog.setLayout(vbox)
1425         r = dialog.exec_()
1426         if not r: return
1427
1428         name = str(e.text())
1429         if not name: return
1430
1431         self.wallet.create_pending_account(name, password)
1432         self.update_receive_tab()
1433         self.tabs.setCurrentIndex(2)
1434
1435
1436
1437
1438     def show_master_public_keys(self):
1439
1440         dialog = QDialog(self)
1441         dialog.setModal(1)
1442         dialog.setWindowTitle(_("Master Public Keys"))
1443
1444         main_layout = QGridLayout()
1445         mpk_dict = self.wallet.get_master_public_keys()
1446         i = 0
1447         for key, value in mpk_dict.items():
1448             main_layout.addWidget(QLabel(key), i, 0)
1449             mpk_text = QTextEdit()
1450             mpk_text.setReadOnly(True)
1451             mpk_text.setMaximumHeight(170)
1452             mpk_text.setText(value)
1453             main_layout.addWidget(mpk_text, i + 1, 0)
1454             i += 2
1455
1456         vbox = QVBoxLayout()
1457         vbox.addLayout(main_layout)
1458         vbox.addLayout(close_button(dialog))
1459
1460         dialog.setLayout(vbox)
1461         dialog.exec_()
1462
1463
1464     @protected
1465     def show_seed_dialog(self, password):
1466         if not self.wallet.has_seed():
1467             QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1468             return
1469
1470         try:
1471             mnemonic = self.wallet.get_mnemonic(password)
1472         except Exception:
1473             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1474             return
1475         from seed_dialog import SeedDialog
1476         d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1477         d.exec_()
1478
1479
1480
1481     def show_qrcode(self, data, title = _("QR code")):
1482         if not data: return
1483         d = QDialog(self)
1484         d.setModal(1)
1485         d.setWindowTitle(title)
1486         d.setMinimumSize(270, 300)
1487         vbox = QVBoxLayout()
1488         qrw = QRCodeWidget(data)
1489         vbox.addWidget(qrw, 1)
1490         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1491         hbox = QHBoxLayout()
1492         hbox.addStretch(1)
1493
1494         filename = os.path.join(self.config.path, "qrcode.bmp")
1495
1496         def print_qr():
1497             bmp.save_qrcode(qrw.qr, filename)
1498             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1499
1500         def copy_to_clipboard():
1501             bmp.save_qrcode(qrw.qr, filename)
1502             self.app.clipboard().setImage(QImage(filename))
1503             QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1504
1505         b = QPushButton(_("Copy"))
1506         hbox.addWidget(b)
1507         b.clicked.connect(copy_to_clipboard)
1508
1509         b = QPushButton(_("Save"))
1510         hbox.addWidget(b)
1511         b.clicked.connect(print_qr)
1512
1513         b = QPushButton(_("Close"))
1514         hbox.addWidget(b)
1515         b.clicked.connect(d.accept)
1516         b.setDefault(True)
1517
1518         vbox.addLayout(hbox)
1519         d.setLayout(vbox)
1520         d.exec_()
1521
1522
1523     def do_protect(self, func, args):
1524         if self.wallet.use_encryption:
1525             password = self.password_dialog()
1526             if not password:
1527                 return
1528         else:
1529             password = None
1530
1531         if args != (False,):
1532             args = (self,) + args + (password,)
1533         else:
1534             args = (self,password)
1535         apply( func, args)
1536
1537
1538     def show_public_keys(self, address):
1539         if not address: return
1540         try:
1541             pubkey_list = self.wallet.get_public_keys(address)
1542         except Exception as e:
1543             traceback.print_exc(file=sys.stdout)
1544             self.show_message(str(e))
1545             return
1546
1547         d = QDialog(self)
1548         d.setMinimumSize(600, 200)
1549         d.setModal(1)
1550         vbox = QVBoxLayout()
1551         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1552         vbox.addWidget( QLabel(_("Public key") + ':'))
1553         keys = QTextEdit()
1554         keys.setReadOnly(True)
1555         keys.setText('\n'.join(pubkey_list))
1556         vbox.addWidget(keys)
1557         #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1558         vbox.addLayout(close_button(d))
1559         d.setLayout(vbox)
1560         d.exec_()
1561
1562     @protected
1563     def show_private_key(self, address, password):
1564         if not address: return
1565         try:
1566             pk_list = self.wallet.get_private_key(address, password)
1567         except Exception as e:
1568             traceback.print_exc(file=sys.stdout)
1569             self.show_message(str(e))
1570             return
1571
1572         d = QDialog(self)
1573         d.setMinimumSize(600, 200)
1574         d.setModal(1)
1575         vbox = QVBoxLayout()
1576         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1577         vbox.addWidget( QLabel(_("Private key") + ':'))
1578         keys = QTextEdit()
1579         keys.setReadOnly(True)
1580         keys.setText('\n'.join(pk_list))
1581         vbox.addWidget(keys)
1582         vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1583         vbox.addLayout(close_button(d))
1584         d.setLayout(vbox)
1585         d.exec_()
1586
1587
1588     @protected
1589     def do_sign(self, address, message, signature, password):
1590         message = unicode(message.toPlainText())
1591         message = message.encode('utf-8')
1592         try:
1593             sig = self.wallet.sign_message(str(address.text()), message, password)
1594             signature.setText(sig)
1595         except Exception as e:
1596             self.show_message(str(e))
1597
1598     def do_verify(self, address, message, signature):
1599         message = unicode(message.toPlainText())
1600         message = message.encode('utf-8')
1601         if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1602             self.show_message(_("Signature verified"))
1603         else:
1604             self.show_message(_("Error: wrong signature"))
1605
1606
1607     def sign_verify_message(self, address=''):
1608         d = QDialog(self)
1609         d.setModal(1)
1610         d.setWindowTitle(_('Sign/verify Message'))
1611         d.setMinimumSize(410, 290)
1612
1613         layout = QGridLayout(d)
1614
1615         message_e = QTextEdit()
1616         layout.addWidget(QLabel(_('Message')), 1, 0)
1617         layout.addWidget(message_e, 1, 1)
1618         layout.setRowStretch(2,3)
1619
1620         address_e = QLineEdit()
1621         address_e.setText(address)
1622         layout.addWidget(QLabel(_('Address')), 2, 0)
1623         layout.addWidget(address_e, 2, 1)
1624
1625         signature_e = QTextEdit()
1626         layout.addWidget(QLabel(_('Signature')), 3, 0)
1627         layout.addWidget(signature_e, 3, 1)
1628         layout.setRowStretch(3,1)
1629
1630         hbox = QHBoxLayout()
1631
1632         b = QPushButton(_("Sign"))
1633         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1634         hbox.addWidget(b)
1635
1636         b = QPushButton(_("Verify"))
1637         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1638         hbox.addWidget(b)
1639
1640         b = QPushButton(_("Close"))
1641         b.clicked.connect(d.accept)
1642         hbox.addWidget(b)
1643         layout.addLayout(hbox, 4, 1)
1644         d.exec_()
1645
1646
1647     @protected
1648     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1649         try:
1650             decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1651             message_e.setText(decrypted)
1652         except Exception as e:
1653             self.show_message(str(e))
1654
1655
1656     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1657         message = unicode(message_e.toPlainText())
1658         message = message.encode('utf-8')
1659         try:
1660             encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1661             encrypted_e.setText(encrypted)
1662         except Exception as e:
1663             self.show_message(str(e))
1664
1665
1666
1667     def encrypt_message(self, address = ''):
1668         d = QDialog(self)
1669         d.setModal(1)
1670         d.setWindowTitle(_('Encrypt/decrypt Message'))
1671         d.setMinimumSize(610, 490)
1672
1673         layout = QGridLayout(d)
1674
1675         message_e = QTextEdit()
1676         layout.addWidget(QLabel(_('Message')), 1, 0)
1677         layout.addWidget(message_e, 1, 1)
1678         layout.setRowStretch(2,3)
1679
1680         pubkey_e = QLineEdit()
1681         if address:
1682             pubkey = self.wallet.getpubkeys(address)[0]
1683             pubkey_e.setText(pubkey)
1684         layout.addWidget(QLabel(_('Public key')), 2, 0)
1685         layout.addWidget(pubkey_e, 2, 1)
1686
1687         encrypted_e = QTextEdit()
1688         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1689         layout.addWidget(encrypted_e, 3, 1)
1690         layout.setRowStretch(3,1)
1691
1692         hbox = QHBoxLayout()
1693         b = QPushButton(_("Encrypt"))
1694         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1695         hbox.addWidget(b)
1696
1697         b = QPushButton(_("Decrypt"))
1698         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1699         hbox.addWidget(b)
1700
1701         b = QPushButton(_("Close"))
1702         b.clicked.connect(d.accept)
1703         hbox.addWidget(b)
1704
1705         layout.addLayout(hbox, 4, 1)
1706         d.exec_()
1707
1708
1709     def question(self, msg):
1710         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1711
1712     def show_message(self, msg):
1713         QMessageBox.information(self, _('Message'), msg, _('OK'))
1714
1715     def password_dialog(self, msg=None):
1716         d = QDialog(self)
1717         d.setModal(1)
1718         d.setWindowTitle(_("Enter Password"))
1719
1720         pw = QLineEdit()
1721         pw.setEchoMode(2)
1722
1723         vbox = QVBoxLayout()
1724         if not msg:
1725             msg = _('Please enter your password')
1726         vbox.addWidget(QLabel(msg))
1727
1728         grid = QGridLayout()
1729         grid.setSpacing(8)
1730         grid.addWidget(QLabel(_('Password')), 1, 0)
1731         grid.addWidget(pw, 1, 1)
1732         vbox.addLayout(grid)
1733
1734         vbox.addLayout(ok_cancel_buttons(d))
1735         d.setLayout(vbox)
1736
1737         run_hook('password_dialog', pw, grid, 1)
1738         if not d.exec_(): return
1739         return unicode(pw.text())
1740
1741
1742
1743
1744
1745
1746
1747
1748     def tx_from_text(self, txt):
1749         "json or raw hexadecimal"
1750         try:
1751             txt.decode('hex')
1752             tx = Transaction(txt)
1753             return tx
1754         except Exception:
1755             pass
1756
1757         try:
1758             tx_dict = json.loads(str(txt))
1759             assert "hex" in tx_dict.keys()
1760             tx = Transaction(tx_dict["hex"])
1761             if tx_dict.has_key("input_info"):
1762                 input_info = json.loads(tx_dict['input_info'])
1763                 tx.add_input_info(input_info)
1764             return tx
1765         except Exception:
1766             traceback.print_exc(file=sys.stdout)
1767             pass
1768
1769         QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1770
1771
1772
1773     def read_tx_from_file(self):
1774         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1775         if not fileName:
1776             return
1777         try:
1778             with open(fileName, "r") as f:
1779                 file_content = f.read()
1780         except (ValueError, IOError, os.error), reason:
1781             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1782
1783         return self.tx_from_text(file_content)
1784
1785
1786     @protected
1787     def sign_raw_transaction(self, tx, input_info, password):
1788         self.wallet.signrawtransaction(tx, input_info, [], password)
1789
1790     def do_process_from_text(self):
1791         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1792         if not text:
1793             return
1794         tx = self.tx_from_text(text)
1795         if tx:
1796             self.show_transaction(tx)
1797
1798     def do_process_from_file(self):
1799         tx = self.read_tx_from_file()
1800         if tx:
1801             self.show_transaction(tx)
1802
1803     def do_process_from_txid(self):
1804         from electrum import transaction
1805         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1806         if ok and txid:
1807             r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1808             if r:
1809                 tx = transaction.Transaction(r)
1810                 if tx:
1811                     self.show_transaction(tx)
1812                 else:
1813                     self.show_message("unknown transaction")
1814
1815     def do_process_from_csvReader(self, csvReader):
1816         outputs = []
1817         errors = []
1818         errtext = ""
1819         try:
1820             for position, row in enumerate(csvReader):
1821                 address = row[0]
1822                 if not is_valid(address):
1823                     errors.append((position, address))
1824                     continue
1825                 amount = Decimal(row[1])
1826                 amount = int(100000000*amount)
1827                 outputs.append((address, amount))
1828         except (ValueError, IOError, os.error), reason:
1829             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1830             return
1831         if errors != []:
1832             for x in errors:
1833                 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1834             QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1835             return
1836
1837         try:
1838             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1839         except Exception as e:
1840             self.show_message(str(e))
1841             return
1842
1843         self.show_transaction(tx)
1844
1845     def do_process_from_csv_file(self):
1846         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1847         if not fileName:
1848             return
1849         try:
1850             with open(fileName, "r") as f:
1851                 csvReader = csv.reader(f)
1852                 self.do_process_from_csvReader(csvReader)
1853         except (ValueError, IOError, os.error), reason:
1854             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1855             return
1856
1857     def do_process_from_csv_text(self):
1858         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1859                                + _("Format: address, amount. One output per line"), _("Load CSV"))
1860         if not text:
1861             return
1862         f = StringIO.StringIO(text)
1863         csvReader = csv.reader(f)
1864         self.do_process_from_csvReader(csvReader)
1865
1866
1867
1868     @protected
1869     def export_privkeys_dialog(self, password):
1870         if self.wallet.is_watching_only():
1871             self.show_message(_("This is a watching-only wallet"))
1872             return
1873
1874         d = QDialog(self)
1875         d.setWindowTitle(_('Private keys'))
1876         d.setMinimumSize(850, 300)
1877         vbox = QVBoxLayout(d)
1878
1879         msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."), 
1880                               _("Exposing a single private key can compromise your entire wallet!"), 
1881                               _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
1882         vbox.addWidget(QLabel(msg))
1883
1884         e = QTextEdit()
1885         e.setReadOnly(True)
1886         vbox.addWidget(e)
1887
1888         defaultname = 'electrum-private-keys.csv'
1889         select_msg = _('Select file to export your private keys to')
1890         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
1891         vbox.addLayout(hbox)
1892
1893         h, b = ok_cancel_buttons2(d, _('Export'))
1894         b.setEnabled(False)
1895         vbox.addLayout(h)
1896
1897         private_keys = {}
1898         addresses = self.wallet.addresses(True)
1899         done = False
1900         def privkeys_thread():
1901             for addr in addresses:
1902                 time.sleep(0.1)
1903                 if done: 
1904                     break
1905                 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
1906                 d.emit(SIGNAL('computing_privkeys'))
1907             d.emit(SIGNAL('show_privkeys'))
1908
1909         def show_privkeys():
1910             s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
1911             e.setText(s)
1912             b.setEnabled(True)
1913
1914         d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
1915         d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
1916         threading.Thread(target=privkeys_thread).start()
1917
1918         if not d.exec_():
1919             done = True
1920             return
1921
1922         filename = filename_e.text()
1923         if not filename:
1924             return
1925
1926         try:
1927             self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
1928         except (IOError, os.error), reason:
1929             export_error_label = _("Electrum was unable to produce a private key-export.")
1930             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1931
1932         except Exception as e:
1933             self.show_message(str(e))
1934             return
1935
1936         self.show_message(_("Private keys exported."))
1937
1938
1939     def do_export_privkeys(self, fileName, pklist, is_csv):
1940         with open(fileName, "w+") as f:
1941             if is_csv:
1942                 transaction = csv.writer(f)
1943                 transaction.writerow(["address", "private_key"])
1944                 for addr, pk in pklist.items():
1945                     transaction.writerow(["%34s"%addr,pk])
1946             else:
1947                 import json
1948                 f.write(json.dumps(pklist, indent = 4))
1949
1950
1951     def do_import_labels(self):
1952         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1953         if not labelsFile: return
1954         try:
1955             f = open(labelsFile, 'r')
1956             data = f.read()
1957             f.close()
1958             for key, value in json.loads(data).items():
1959                 self.wallet.set_label(key, value)
1960             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1961         except (IOError, os.error), reason:
1962             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1963
1964
1965     def do_export_labels(self):
1966         labels = self.wallet.labels
1967         try:
1968             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1969             if fileName:
1970                 with open(fileName, 'w+') as f:
1971                     json.dump(labels, f)
1972                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1973         except (IOError, os.error), reason:
1974             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1975
1976
1977     def export_history_dialog(self):
1978
1979         d = QDialog(self)
1980         d.setWindowTitle(_('Export History'))
1981         d.setMinimumSize(400, 200)
1982         vbox = QVBoxLayout(d)
1983
1984         defaultname = os.path.expanduser('~/electrum-history.csv')
1985         select_msg = _('Select file to export your wallet transactions to')
1986
1987         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
1988         vbox.addLayout(hbox)
1989
1990         vbox.addStretch(1)
1991
1992         h, b = ok_cancel_buttons2(d, _('Export'))
1993         vbox.addLayout(h)
1994         if not d.exec_():
1995             return
1996
1997         filename = filename_e.text()
1998         if not filename:
1999             return
2000
2001         try:
2002             self.do_export_history(self.wallet, filename, csv_button.isChecked())
2003         except (IOError, os.error), reason:
2004             export_error_label = _("Electrum was unable to produce a transaction export.")
2005             QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2006             return
2007
2008         QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2009
2010
2011     def do_export_history(self, wallet, fileName, is_csv):
2012         history = wallet.get_tx_history()
2013         lines = []
2014         for item in history:
2015             tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2016             if confirmations:
2017                 if timestamp is not None:
2018                     try:
2019                         time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2020                     except [RuntimeError, TypeError, NameError] as reason:
2021                         time_string = "unknown"
2022                         pass
2023                 else:
2024                     time_string = "unknown"
2025             else:
2026                 time_string = "pending"
2027
2028             if value is not None:
2029                 value_string = format_satoshis(value, True)
2030             else:
2031                 value_string = '--'
2032
2033             if fee is not None:
2034                 fee_string = format_satoshis(fee, True)
2035             else:
2036                 fee_string = '0'
2037
2038             if tx_hash:
2039                 label, is_default_label = wallet.get_label(tx_hash)
2040                 label = label.encode('utf-8')
2041             else:
2042                 label = ""
2043
2044             balance_string = format_satoshis(balance, False)
2045             if is_csv:
2046                 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2047             else:
2048                 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2049
2050         with open(fileName, "w+") as f:
2051             if is_csv:
2052                 transaction = csv.writer(f)
2053                 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2054                 for line in lines:
2055                     transaction.writerow(line)
2056             else:
2057                 import json
2058                 f.write(json.dumps(lines, indent = 4))
2059
2060
2061     def sweep_key_dialog(self):
2062         d = QDialog(self)
2063         d.setWindowTitle(_('Sweep private keys'))
2064         d.setMinimumSize(600, 300)
2065
2066         vbox = QVBoxLayout(d)
2067         vbox.addWidget(QLabel(_("Enter private keys")))
2068
2069         keys_e = QTextEdit()
2070         keys_e.setTabChangesFocus(True)
2071         vbox.addWidget(keys_e)
2072
2073         h, address_e = address_field(self.wallet.addresses())
2074         vbox.addLayout(h)
2075
2076         vbox.addStretch(1)
2077         hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2078         vbox.addLayout(hbox)
2079         button.setEnabled(False)
2080
2081         def get_address():
2082             addr = str(address_e.text())
2083             if bitcoin.is_address(addr):
2084                 return addr
2085
2086         def get_pk():
2087             pk = str(keys_e.toPlainText()).strip()
2088             if Wallet.is_private_key(pk):
2089                 return pk.split()
2090
2091         f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2092         keys_e.textChanged.connect(f)
2093         address_e.textChanged.connect(f)
2094         if not d.exec_():
2095             return
2096
2097         fee = self.wallet.fee
2098         tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2099         self.show_transaction(tx)
2100
2101
2102     @protected
2103     def do_import_privkey(self, password):
2104         if not self.wallet.has_imported_keys():
2105             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2106                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2107                                          + _('Are you sure you understand what you are doing?'), 3, 4)
2108             if r == 4: return
2109
2110         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2111         if not text: return
2112
2113         text = str(text).split()
2114         badkeys = []
2115         addrlist = []
2116         for key in text:
2117             try:
2118                 addr = self.wallet.import_key(key, password)
2119             except Exception as e:
2120                 badkeys.append(key)
2121                 continue
2122             if not addr:
2123                 badkeys.append(key)
2124             else:
2125                 addrlist.append(addr)
2126         if addrlist:
2127             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2128         if badkeys:
2129             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2130         self.update_receive_tab()
2131         self.update_history_tab()
2132
2133
2134     def settings_dialog(self):
2135         d = QDialog(self)
2136         d.setWindowTitle(_('Electrum Settings'))
2137         d.setModal(1)
2138         vbox = QVBoxLayout()
2139         grid = QGridLayout()
2140         grid.setColumnStretch(0,1)
2141
2142         nz_label = QLabel(_('Display zeros') + ':')
2143         grid.addWidget(nz_label, 0, 0)
2144         nz_e = AmountEdit(None,True)
2145         nz_e.setText("%d"% self.num_zeros)
2146         grid.addWidget(nz_e, 0, 1)
2147         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2148         grid.addWidget(HelpButton(msg), 0, 2)
2149         if not self.config.is_modifiable('num_zeros'):
2150             for w in [nz_e, nz_label]: w.setEnabled(False)
2151
2152         lang_label=QLabel(_('Language') + ':')
2153         grid.addWidget(lang_label, 1, 0)
2154         lang_combo = QComboBox()
2155         from electrum.i18n import languages
2156         lang_combo.addItems(languages.values())
2157         try:
2158             index = languages.keys().index(self.config.get("language",''))
2159         except Exception:
2160             index = 0
2161         lang_combo.setCurrentIndex(index)
2162         grid.addWidget(lang_combo, 1, 1)
2163         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2164         if not self.config.is_modifiable('language'):
2165             for w in [lang_combo, lang_label]: w.setEnabled(False)
2166
2167
2168         fee_label = QLabel(_('Transaction fee') + ':')
2169         grid.addWidget(fee_label, 2, 0)
2170         fee_e = AmountEdit(self.get_decimal_point)
2171         fee_e.setText(self.format_amount(self.wallet.fee).strip())
2172         grid.addWidget(fee_e, 2, 1)
2173         msg = _('Fee per kilobyte of transaction.') + ' ' \
2174             + _('Recommended value') + ': ' + self.format_amount(20000)
2175         grid.addWidget(HelpButton(msg), 2, 2)
2176         if not self.config.is_modifiable('fee_per_kb'):
2177             for w in [fee_e, fee_label]: w.setEnabled(False)
2178
2179         units = ['BTC', 'mBTC']
2180         unit_label = QLabel(_('Base unit') + ':')
2181         grid.addWidget(unit_label, 3, 0)
2182         unit_combo = QComboBox()
2183         unit_combo.addItems(units)
2184         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2185         grid.addWidget(unit_combo, 3, 1)
2186         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2187                                              + '\n1BTC=1000mBTC.\n' \
2188                                              + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2189
2190         usechange_cb = QCheckBox(_('Use change addresses'))
2191         usechange_cb.setChecked(self.wallet.use_change)
2192         grid.addWidget(usechange_cb, 4, 0)
2193         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2194         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2195
2196         block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2197         block_ex_label = QLabel(_('Online Block Explorer') + ':')
2198         grid.addWidget(block_ex_label, 5, 0)
2199         block_ex_combo = QComboBox()
2200         block_ex_combo.addItems(block_explorers)
2201         block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2202         grid.addWidget(block_ex_combo, 5, 1)
2203         grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2204
2205         show_tx = self.config.get('show_before_broadcast', False)
2206         showtx_cb = QCheckBox(_('Show before broadcast'))
2207         showtx_cb.setChecked(show_tx)
2208         grid.addWidget(showtx_cb, 6, 0)
2209         grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2210
2211         vbox.addLayout(grid)
2212         vbox.addStretch(1)
2213         vbox.addLayout(ok_cancel_buttons(d))
2214         d.setLayout(vbox)
2215
2216         # run the dialog
2217         if not d.exec_(): return
2218
2219         try:
2220             fee = self.fee_e.get_amount()
2221         except Exception:
2222             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2223             return
2224
2225         self.wallet.set_fee(fee)
2226
2227         nz = unicode(nz_e.text())
2228         try:
2229             nz = int( nz )
2230             if nz>8: nz=8
2231         except Exception:
2232             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2233             return
2234
2235         if self.num_zeros != nz:
2236             self.num_zeros = nz
2237             self.config.set_key('num_zeros', nz, True)
2238             self.update_history_tab()
2239             self.update_receive_tab()
2240
2241         usechange_result = usechange_cb.isChecked()
2242         if self.wallet.use_change != usechange_result:
2243             self.wallet.use_change = usechange_result
2244             self.wallet.storage.put('use_change', self.wallet.use_change)
2245
2246         if showtx_cb.isChecked() != show_tx:
2247             self.config.set_key('show_before_broadcast', not show_tx)
2248
2249         unit_result = units[unit_combo.currentIndex()]
2250         if self.base_unit() != unit_result:
2251             self.decimal_point = 8 if unit_result == 'BTC' else 5
2252             self.config.set_key('decimal_point', self.decimal_point, True)
2253             self.update_history_tab()
2254             self.update_status()
2255
2256         need_restart = False
2257
2258         lang_request = languages.keys()[lang_combo.currentIndex()]
2259         if lang_request != self.config.get('language'):
2260             self.config.set_key("language", lang_request, True)
2261             need_restart = True
2262
2263         be_result = block_explorers[block_ex_combo.currentIndex()]
2264         self.config.set_key('block_explorer', be_result, True)
2265
2266         run_hook('close_settings_dialog')
2267
2268         if need_restart:
2269             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2270
2271
2272     def run_network_dialog(self):
2273         if not self.network:
2274             return
2275         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2276
2277     def closeEvent(self, event):
2278         self.tray.hide()
2279         self.config.set_key("is_maximized", self.isMaximized())
2280         if not self.isMaximized():
2281             g = self.geometry()
2282             self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2283         self.save_column_widths()
2284         self.config.set_key("console-history", self.console.history[-50:], True)
2285         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2286         event.accept()
2287
2288
2289     def plugins_dialog(self):
2290         from electrum.plugins import plugins
2291
2292         d = QDialog(self)
2293         d.setWindowTitle(_('Electrum Plugins'))
2294         d.setModal(1)
2295
2296         vbox = QVBoxLayout(d)
2297
2298         # plugins
2299         scroll = QScrollArea()
2300         scroll.setEnabled(True)
2301         scroll.setWidgetResizable(True)
2302         scroll.setMinimumSize(400,250)
2303         vbox.addWidget(scroll)
2304
2305         w = QWidget()
2306         scroll.setWidget(w)
2307         w.setMinimumHeight(len(plugins)*35)
2308
2309         grid = QGridLayout()
2310         grid.setColumnStretch(0,1)
2311         w.setLayout(grid)
2312
2313         def do_toggle(cb, p, w):
2314             r = p.toggle()
2315             cb.setChecked(r)
2316             if w: w.setEnabled(r)
2317
2318         def mk_toggle(cb, p, w):
2319             return lambda: do_toggle(cb,p,w)
2320
2321         for i, p in enumerate(plugins):
2322             try:
2323                 cb = QCheckBox(p.fullname())
2324                 cb.setDisabled(not p.is_available())
2325                 cb.setChecked(p.is_enabled())
2326                 grid.addWidget(cb, i, 0)
2327                 if p.requires_settings():
2328                     w = p.settings_widget(self)
2329                     w.setEnabled( p.is_enabled() )
2330                     grid.addWidget(w, i, 1)
2331                 else:
2332                     w = None
2333                 cb.clicked.connect(mk_toggle(cb,p,w))
2334                 grid.addWidget(HelpButton(p.description()), i, 2)
2335             except Exception:
2336                 print_msg(_("Error: cannot display plugin"), p)
2337                 traceback.print_exc(file=sys.stdout)
2338         grid.setRowStretch(i+1,1)
2339
2340         vbox.addLayout(close_button(d))
2341
2342         d.exec_()
2343
2344
2345     def show_account_details(self, k):
2346         account = self.wallet.accounts[k]
2347
2348         d = QDialog(self)
2349         d.setWindowTitle(_('Account Details'))
2350         d.setModal(1)
2351
2352         vbox = QVBoxLayout(d)
2353         name = self.wallet.get_account_name(k)
2354         label = QLabel('Name: ' + name)
2355         vbox.addWidget(label)
2356
2357         vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2358
2359         vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2360
2361         vbox.addWidget(QLabel(_('Master Public Key:')))
2362
2363         text = QTextEdit()
2364         text.setReadOnly(True)
2365         text.setMaximumHeight(170)
2366         vbox.addWidget(text)
2367
2368         mpk_text = '\n'.join( account.get_master_pubkeys() )
2369         text.setText(mpk_text)
2370
2371         vbox.addLayout(close_button(d))
2372         d.exec_()