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