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