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