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