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