fix: missing import
[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         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         run_hook('exchange_rate_button', grid)
698
699         self.send_button = EnterButton(_("Send"), self.do_send)
700         grid.addWidget(self.send_button, 6, 1)
701
702         b = EnterButton(_("Clear"), self.do_clear)
703         grid.addWidget(b, 6, 2)
704
705         self.payto_sig = QLabel('')
706         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
707
708         #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
709         #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
710         w.setLayout(grid)
711
712         def entry_changed( is_fee ):
713             self.funds_error = False
714
715             if self.amount_e.is_shortcut:
716                 self.amount_e.is_shortcut = False
717                 sendable = self.get_sendable_balance()
718                 # there is only one output because we are completely spending inputs
719                 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
720                 fee = self.wallet.estimated_fee(inputs, 1)
721                 amount = total - fee
722                 self.amount_e.setText( self.format_amount(amount) )
723                 self.fee_e.setText( self.format_amount( fee ) )
724                 return
725
726             amount = self.amount_e.get_amount()
727             fee = self.fee_e.get_amount()
728
729             if not is_fee: fee = None
730             if amount is None:
731                 return
732             # assume that there will be 2 outputs (one for change)
733             inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
734             if not is_fee:
735                 self.fee_e.setText( self.format_amount( fee ) )
736             if inputs:
737                 palette = QPalette()
738                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
739                 text = ""
740             else:
741                 palette = QPalette()
742                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
743                 self.funds_error = True
744                 text = _( "Not enough funds" )
745                 c, u = self.wallet.get_frozen_balance()
746                 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
747
748             self.statusBar().showMessage(text)
749             self.amount_e.setPalette(palette)
750             self.fee_e.setPalette(palette)
751
752         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
753         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
754
755         run_hook('create_send_tab', grid)
756         return w
757
758     def from_list_delete(self, item):
759         i = self.from_list.indexOfTopLevelItem(item)
760         self.pay_from.pop(i)
761         self.redraw_from_list()
762
763     def from_list_menu(self, position):
764         item = self.from_list.itemAt(position)
765         menu = QMenu()
766         menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
767         menu.exec_(self.from_list.viewport().mapToGlobal(position))
768
769     def set_pay_from(self, domain = None):
770         self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
771         self.redraw_from_list()
772
773     def redraw_from_list(self):
774         self.from_list.clear()
775         self.from_label.setHidden(len(self.pay_from) == 0)
776         self.from_list.setHidden(len(self.pay_from) == 0)
777
778         def format(x):
779             h = x.get('prevout_hash')
780             return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
781
782         for item in self.pay_from:
783             self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
784
785     def update_completions(self):
786         l = []
787         for addr,label in self.wallet.labels.items():
788             if addr in self.wallet.addressbook:
789                 l.append( label + '  <' + addr + '>')
790
791         run_hook('update_completions', l)
792         self.completions.setStringList(l)
793
794
795     def protected(func):
796         return lambda s, *args: s.do_protect(func, args)
797
798
799     def do_send(self):
800         label = unicode( self.message_e.text() )
801
802         if self.gui_object.payment_request:
803             outputs = self.gui_object.payment_request.outputs
804         else:
805             outputs = self.payto_e.get_outputs()
806
807         if not outputs:
808             QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
809             return
810
811         for addr, x in outputs:
812             if addr is None or not bitcoin.is_address(addr):
813                 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
814                 return
815             if x is None:
816                 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
817                 return
818
819         amount = sum(map(lambda x:x[1], outputs))
820
821         try:
822             fee = self.fee_e.get_amount()
823         except Exception:
824             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
825             return
826
827         confirm_amount = self.config.get('confirm_amount', 100000000)
828         if amount >= confirm_amount:
829             o = '\n'.join(map(lambda x:x[0], outputs))
830             if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
831                 return
832             
833         confirm_fee = self.config.get('confirm_fee', 100000)
834         if fee >= confirm_fee:
835             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()}):
836                 return
837
838         self.send_tx(outputs, fee, label)
839
840
841
842     @protected
843     def send_tx(self, outputs, fee, label, password):
844         self.send_button.setDisabled(True)
845
846         # first, create an unsigned tx 
847         coins = self.get_coins()
848         try:
849             tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
850             tx.error = None
851         except Exception as e:
852             traceback.print_exc(file=sys.stdout)
853             self.show_message(str(e))
854             self.send_button.setDisabled(False)
855             return
856
857         # call hook to see if plugin needs gui interaction
858         run_hook('send_tx', tx)
859
860         # sign the tx
861         def sign_thread():
862             time.sleep(0.1)
863             keypairs = {}
864             self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
865             self.wallet.sign_transaction(tx, keypairs, password)
866             return tx, fee, label
867
868         def sign_done(tx, fee, label):
869             if tx.error:
870                 self.show_message(tx.error)
871                 self.send_button.setDisabled(False)
872                 return
873             if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
874                 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
875                 self.send_button.setDisabled(False)
876                 return
877             if label:
878                 self.wallet.set_label(tx.hash(), label)
879
880             if not tx.is_complete() or self.config.get('show_before_broadcast'):
881                 self.show_transaction(tx)
882                 self.do_clear()
883                 self.send_button.setDisabled(False)
884                 return
885
886             self.broadcast_transaction(tx)
887
888         self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
889         self.waiting_dialog.start()
890
891
892
893     def broadcast_transaction(self, tx):
894
895         def broadcast_thread():
896             if self.gui_object.payment_request:
897                 refund_address = self.wallet.addresses()[0]
898                 status, msg = self.gui_object.payment_request.send_ack(str(tx), refund_address)
899                 self.gui_object.payment_request = None
900             else:
901                 status, msg =  self.wallet.sendtx(tx)
902             return status, msg
903
904         def broadcast_done(status, msg):
905             if status:
906                 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
907                 self.do_clear()
908             else:
909                 QMessageBox.warning(self, _('Error'), msg, _('OK'))
910             self.send_button.setDisabled(False)
911
912         self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
913         self.waiting_dialog.start()
914
915
916
917     def prepare_for_payment_request(self):
918         self.tabs.setCurrentIndex(1)
919         self.payto_e.is_pr = True
920         for e in [self.payto_e, self.amount_e, self.message_e]:
921             e.setFrozen(True)
922         for h in [self.payto_help, self.amount_help, self.message_help]:
923             h.hide()
924         self.payto_e.setText(_("please wait..."))
925         return True
926
927     def payment_request_ok(self):
928         pr = self.gui_object.payment_request
929         pr_id = pr.get_id()
930         # save it
931         self.invoices[pr_id] = (pr.get_domain(), pr.get_amount())
932         self.wallet.storage.put('invoices', self.invoices)
933         self.update_invoices_tab()
934
935         self.payto_help.show()
936         self.payto_help.set_alt(lambda: self.show_pr_details(pr))
937
938         self.payto_e.setGreen()
939         self.payto_e.setText(pr.domain)
940         self.amount_e.setText(self.format_amount(pr.get_amount()))
941         self.message_e.setText(pr.memo)
942
943     def payment_request_error(self):
944         self.do_clear()
945         self.show_message(self.gui_object.payment_request.error)
946         self.gui_object.payment_request = None
947
948     def set_send(self, address, amount, label, message):
949
950         if label and self.wallet.labels.get(address) != label:
951             if self.question('Give label "%s" to address %s ?'%(label,address)):
952                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
953                     self.wallet.addressbook.append(address)
954                 self.wallet.set_label(address, label)
955
956         self.tabs.setCurrentIndex(1)
957         label = self.wallet.labels.get(address)
958         m_addr = label + '  <'+ address +'>' if label else address
959         self.payto_e.setText(m_addr)
960
961         self.message_e.setText(message)
962         if amount:
963             self.amount_e.setText(amount)
964
965
966     def do_clear(self):
967         self.payto_e.is_pr = False
968         self.payto_sig.setVisible(False)
969         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
970             e.setText('')
971             e.setFrozen(False)
972
973         for h in [self.payto_help, self.amount_help, self.message_help]:
974             h.show()
975
976         self.payto_help.set_alt(None)
977         self.set_pay_from([])
978         self.update_status()
979
980
981
982     def set_addrs_frozen(self,addrs,freeze):
983         for addr in addrs:
984             if not addr: continue
985             if addr in self.wallet.frozen_addresses and not freeze:
986                 self.wallet.unfreeze(addr)
987             elif addr not in self.wallet.frozen_addresses and freeze:
988                 self.wallet.freeze(addr)
989         self.update_receive_tab()
990
991
992
993     def create_list_tab(self, headers):
994         "generic tab creation method"
995         l = MyTreeWidget(self)
996         l.setColumnCount( len(headers) )
997         l.setHeaderLabels( headers )
998
999         w = QWidget()
1000         vbox = QVBoxLayout()
1001         w.setLayout(vbox)
1002
1003         vbox.setMargin(0)
1004         vbox.setSpacing(0)
1005         vbox.addWidget(l)
1006         buttons = QWidget()
1007         vbox.addWidget(buttons)
1008
1009         return l, w
1010
1011
1012     def create_receive_tab(self):
1013         l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1014         l.setContextMenuPolicy(Qt.CustomContextMenu)
1015         l.customContextMenuRequested.connect(self.create_receive_menu)
1016         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1017         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1018         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1019         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1020         self.receive_list = l
1021         return w
1022
1023
1024
1025
1026     def save_column_widths(self):
1027         self.column_widths["receive"] = []
1028         for i in range(self.receive_list.columnCount() -1):
1029             self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1030
1031         self.column_widths["history"] = []
1032         for i in range(self.history_list.columnCount() - 1):
1033             self.column_widths["history"].append(self.history_list.columnWidth(i))
1034
1035         self.column_widths["contacts"] = []
1036         for i in range(self.contacts_list.columnCount() - 1):
1037             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1038
1039         self.config.set_key("column_widths_2", self.column_widths, True)
1040
1041
1042     def create_contacts_tab(self):
1043         l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1044         l.setContextMenuPolicy(Qt.CustomContextMenu)
1045         l.customContextMenuRequested.connect(self.create_contact_menu)
1046         for i,width in enumerate(self.column_widths['contacts']):
1047             l.setColumnWidth(i, width)
1048
1049         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1050         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1051         self.contacts_list = l
1052         return w
1053
1054
1055     def create_invoices_tab(self):
1056         l, w = self.create_list_tab([_('Requestor'), _('Amount'), _('Status')])
1057         l.setContextMenuPolicy(Qt.CustomContextMenu)
1058         l.customContextMenuRequested.connect(self.create_invoice_menu)
1059         self.invoices_list = l
1060         return w
1061
1062     def update_invoices_tab(self):
1063         invoices = self.wallet.storage.get('invoices', {})
1064         l = self.invoices_list
1065         l.clear()
1066
1067         for item, value in invoices.items():
1068             domain, amount = value
1069             item = QTreeWidgetItem( [ domain, self.format_amount(amount), ""] )
1070             l.addTopLevelItem(item)
1071
1072         l.setCurrentItem(l.topLevelItem(0))
1073
1074
1075
1076     def delete_imported_key(self, addr):
1077         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1078             self.wallet.delete_imported_key(addr)
1079             self.update_receive_tab()
1080             self.update_history_tab()
1081
1082     def edit_account_label(self, k):
1083         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1084         if ok:
1085             label = unicode(text)
1086             self.wallet.set_label(k,label)
1087             self.update_receive_tab()
1088
1089     def account_set_expanded(self, item, k, b):
1090         item.setExpanded(b)
1091         self.accounts_expanded[k] = b
1092
1093     def create_account_menu(self, position, k, item):
1094         menu = QMenu()
1095         if item.isExpanded():
1096             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1097         else:
1098             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1099         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1100         if self.wallet.seed_version > 4:
1101             menu.addAction(_("View details"), lambda: self.show_account_details(k))
1102         if self.wallet.account_is_pending(k):
1103             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1104         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1105
1106     def delete_pending_account(self, k):
1107         self.wallet.delete_pending_account(k)
1108         self.update_receive_tab()
1109
1110     def create_receive_menu(self, position):
1111         # fixme: this function apparently has a side effect.
1112         # if it is not called the menu pops up several times
1113         #self.receive_list.selectedIndexes()
1114
1115         selected = self.receive_list.selectedItems()
1116         multi_select = len(selected) > 1
1117         addrs = [unicode(item.text(0)) for item in selected]
1118         if not multi_select:
1119             item = self.receive_list.itemAt(position)
1120             if not item: return
1121
1122             addr = addrs[0]
1123             if not is_valid(addr):
1124                 k = str(item.data(0,32).toString())
1125                 if k:
1126                     self.create_account_menu(position, k, item)
1127                 else:
1128                     item.setExpanded(not item.isExpanded())
1129                 return
1130
1131         menu = QMenu()
1132         if not multi_select:
1133             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1134             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1135             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1136             menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1137             if not self.wallet.is_watching_only():
1138                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1139                 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1140                 #menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1141             if self.wallet.is_imported(addr):
1142                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1143
1144         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1145             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1146         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1147             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1148
1149         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1150             menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1151
1152         run_hook('receive_menu', menu, addrs)
1153         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1154
1155
1156     def get_sendable_balance(self):
1157         return sum(map(lambda x:x['value'], self.get_coins()))
1158
1159
1160     def get_coins(self):
1161         if self.pay_from:
1162             return self.pay_from
1163         else:
1164             domain = self.wallet.get_account_addresses(self.current_account)
1165             for i in self.wallet.frozen_addresses:
1166                 if i in domain: domain.remove(i)
1167             return self.wallet.get_unspent_coins(domain)
1168
1169
1170     def send_from_addresses(self, addrs):
1171         self.set_pay_from( addrs )
1172         self.tabs.setCurrentIndex(1)
1173
1174
1175     def payto(self, addr):
1176         if not addr: return
1177         label = self.wallet.labels.get(addr)
1178         m_addr = label + '  <' + addr + '>' if label else addr
1179         self.tabs.setCurrentIndex(1)
1180         self.payto_e.setText(m_addr)
1181         self.amount_e.setFocus()
1182
1183
1184     def delete_contact(self, x):
1185         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1186             self.wallet.delete_contact(x)
1187             self.wallet.set_label(x, None)
1188             self.update_history_tab()
1189             self.update_contacts_tab()
1190             self.update_completions()
1191
1192
1193     def create_contact_menu(self, position):
1194         item = self.contacts_list.itemAt(position)
1195         menu = QMenu()
1196         if not item:
1197             menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1198         else:
1199             addr = unicode(item.text(0))
1200             label = unicode(item.text(1))
1201             is_editable = item.data(0,32).toBool()
1202             payto_addr = item.data(0,33).toString()
1203             menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1204             menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1205             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1206             if is_editable:
1207                 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1208                 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1209
1210         run_hook('create_contact_menu', menu, item)
1211         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1212
1213     def delete_invoice(self, item):
1214         self.invoices.pop(key)
1215         self.wallet.storage.put('invoices', self.invoices)
1216         self.update_invoices_tab()
1217
1218     def show_invoice(self, key):
1219         from electrum.paymentrequest import PaymentRequest
1220         domain, value = self.invoices[key]
1221         pr = PaymentRequest(self.config)
1222         pr.read_file(key)
1223         pr.domain = domain
1224         pr.verify()
1225         self.show_pr_details(pr)
1226
1227     def show_pr_details(self, pr):
1228         msg = 'Domain: ' + pr.domain
1229         msg += '\nStatus: ' + pr.get_status()
1230         msg += '\nMemo: ' + pr.memo
1231         msg += '\nPayment URL: ' + pr.payment_url
1232         msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1233         QMessageBox.information(self, 'Invoice', msg , 'OK')
1234
1235     def create_invoice_menu(self, position):
1236         item = self.invoices_list.itemAt(position)
1237         if not item:
1238             return
1239         k = self.invoices_list.indexOfTopLevelItem(item)
1240         key = self.invoices.keys()[k]
1241         menu = QMenu()
1242         menu.addAction(_("Details"), lambda: self.show_invoice(key))
1243         menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1244         menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1245
1246
1247     def update_receive_item(self, item):
1248         item.setFont(0, QFont(MONOSPACE_FONT))
1249         address = str(item.data(0,0).toString())
1250         label = self.wallet.labels.get(address,'')
1251         item.setData(1,0,label)
1252         item.setData(0,32, True) # is editable
1253
1254         run_hook('update_receive_item', address, item)
1255
1256         if not self.wallet.is_mine(address): return
1257
1258         c, u = self.wallet.get_addr_balance(address)
1259         balance = self.format_amount(c + u)
1260         item.setData(2,0,balance)
1261
1262         if address in self.wallet.frozen_addresses:
1263             item.setBackgroundColor(0, QColor('lightblue'))
1264
1265
1266     def update_receive_tab(self):
1267         l = self.receive_list
1268         # extend the syntax for consistency
1269         l.addChild = l.addTopLevelItem
1270         l.insertChild = l.insertTopLevelItem
1271
1272         l.clear()
1273         for i,width in enumerate(self.column_widths['receive']):
1274             l.setColumnWidth(i, width)
1275
1276         accounts = self.wallet.get_accounts()
1277         if self.current_account is None:
1278             account_items = sorted(accounts.items())
1279         else:
1280             account_items = [(self.current_account, accounts.get(self.current_account))]
1281
1282
1283         for k, account in account_items:
1284
1285             if len(accounts) > 1:
1286                 name = self.wallet.get_account_name(k)
1287                 c,u = self.wallet.get_account_balance(k)
1288                 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1289                 l.addTopLevelItem(account_item)
1290                 account_item.setExpanded(self.accounts_expanded.get(k, True))
1291                 account_item.setData(0, 32, k)
1292             else:
1293                 account_item = l
1294
1295             sequences = [0,1] if account.has_change() else [0]
1296             for is_change in sequences:
1297                 if len(sequences) > 1:
1298                     name = _("Receiving") if not is_change else _("Change")
1299                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1300                     account_item.addChild(seq_item)
1301                     if not is_change: 
1302                         seq_item.setExpanded(True)
1303                 else:
1304                     seq_item = account_item
1305                     
1306                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1307                 used_flag = False
1308
1309                 is_red = False
1310                 gap = 0
1311
1312                 for address in account.get_addresses(is_change):
1313
1314                     num, is_used = self.wallet.is_used(address)
1315                     if num == 0:
1316                         gap += 1
1317                         if gap > self.wallet.gap_limit:
1318                             is_red = True
1319                     else:
1320                         gap = 0
1321
1322                     item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1323                     self.update_receive_item(item)
1324                     if is_red:
1325                         item.setBackgroundColor(1, QColor('red'))
1326
1327                     if is_used:
1328                         if not used_flag:
1329                             seq_item.insertChild(0,used_item)
1330                             used_flag = True
1331                         used_item.addChild(item)
1332                     else:
1333                         seq_item.addChild(item)
1334
1335         # we use column 1 because column 0 may be hidden
1336         l.setCurrentItem(l.topLevelItem(0),1)
1337
1338
1339     def update_contacts_tab(self):
1340         l = self.contacts_list
1341         l.clear()
1342
1343         for address in self.wallet.addressbook:
1344             label = self.wallet.labels.get(address,'')
1345             n = self.wallet.get_num_tx(address)
1346             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1347             item.setFont(0, QFont(MONOSPACE_FONT))
1348             # 32 = label can be edited (bool)
1349             item.setData(0,32, True)
1350             # 33 = payto string
1351             item.setData(0,33, address)
1352             l.addTopLevelItem(item)
1353
1354         run_hook('update_contacts_tab', l)
1355         l.setCurrentItem(l.topLevelItem(0))
1356
1357
1358
1359     def create_console_tab(self):
1360         from console import Console
1361         self.console = console = Console()
1362         return console
1363
1364
1365     def update_console(self):
1366         console = self.console
1367         console.history = self.config.get("console-history",[])
1368         console.history_index = len(console.history)
1369
1370         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1371         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1372
1373         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1374         methods = {}
1375         def mkfunc(f, method):
1376             return lambda *args: apply( f, (method, args, self.password_dialog ))
1377         for m in dir(c):
1378             if m[0]=='_' or m in ['network','wallet']: continue
1379             methods[m] = mkfunc(c._run, m)
1380
1381         console.updateNamespace(methods)
1382
1383
1384     def change_account(self,s):
1385         if s == _("All accounts"):
1386             self.current_account = None
1387         else:
1388             accounts = self.wallet.get_account_names()
1389             for k, v in accounts.items():
1390                 if v == s:
1391                     self.current_account = k
1392         self.update_history_tab()
1393         self.update_status()
1394         self.update_receive_tab()
1395
1396     def create_status_bar(self):
1397
1398         sb = QStatusBar()
1399         sb.setFixedHeight(35)
1400         qtVersion = qVersion()
1401
1402         self.balance_label = QLabel("")
1403         sb.addWidget(self.balance_label)
1404
1405         from version_getter import UpdateLabel
1406         self.updatelabel = UpdateLabel(self.config, sb)
1407
1408         self.account_selector = QComboBox()
1409         self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1410         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1411         sb.addPermanentWidget(self.account_selector)
1412
1413         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1414             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1415
1416         self.lock_icon = QIcon()
1417         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1418         sb.addPermanentWidget( self.password_button )
1419
1420         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1421         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1422         sb.addPermanentWidget( self.seed_button )
1423         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1424         sb.addPermanentWidget( self.status_button )
1425
1426         run_hook('create_status_bar', (sb,))
1427
1428         self.setStatusBar(sb)
1429
1430
1431     def update_lock_icon(self):
1432         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1433         self.password_button.setIcon( icon )
1434
1435
1436     def update_buttons_on_seed(self):
1437         if self.wallet.has_seed():
1438            self.seed_button.show()
1439         else:
1440            self.seed_button.hide()
1441
1442         if not self.wallet.is_watching_only():
1443            self.password_button.show()
1444            self.send_button.setText(_("Send"))
1445         else:
1446            self.password_button.hide()
1447            self.send_button.setText(_("Create unsigned transaction"))
1448
1449
1450     def change_password_dialog(self):
1451         from password_dialog import PasswordDialog
1452         d = PasswordDialog(self.wallet, self)
1453         d.run()
1454         self.update_lock_icon()
1455
1456
1457     def new_contact_dialog(self):
1458
1459         d = QDialog(self)
1460         d.setWindowTitle(_("New Contact"))
1461         vbox = QVBoxLayout(d)
1462         vbox.addWidget(QLabel(_('New Contact')+':'))
1463
1464         grid = QGridLayout()
1465         line1 = QLineEdit()
1466         line2 = QLineEdit()
1467         grid.addWidget(QLabel(_("Address")), 1, 0)
1468         grid.addWidget(line1, 1, 1)
1469         grid.addWidget(QLabel(_("Name")), 2, 0)
1470         grid.addWidget(line2, 2, 1)
1471
1472         vbox.addLayout(grid)
1473         vbox.addLayout(ok_cancel_buttons(d))
1474
1475         if not d.exec_():
1476             return
1477
1478         address = str(line1.text())
1479         label = unicode(line2.text())
1480
1481         if not is_valid(address):
1482             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1483             return
1484
1485         self.wallet.add_contact(address)
1486         if label:
1487             self.wallet.set_label(address, label)
1488
1489         self.update_contacts_tab()
1490         self.update_history_tab()
1491         self.update_completions()
1492         self.tabs.setCurrentIndex(3)
1493
1494
1495     @protected
1496     def new_account_dialog(self, password):
1497
1498         dialog = QDialog(self)
1499         dialog.setModal(1)
1500         dialog.setWindowTitle(_("New Account"))
1501
1502         vbox = QVBoxLayout()
1503         vbox.addWidget(QLabel(_('Account name')+':'))
1504         e = QLineEdit()
1505         vbox.addWidget(e)
1506         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1507             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1508         l = QLabel(msg)
1509         l.setWordWrap(True)
1510         vbox.addWidget(l)
1511
1512         vbox.addLayout(ok_cancel_buttons(dialog))
1513         dialog.setLayout(vbox)
1514         r = dialog.exec_()
1515         if not r: return
1516
1517         name = str(e.text())
1518         if not name: return
1519
1520         self.wallet.create_pending_account(name, password)
1521         self.update_receive_tab()
1522         self.tabs.setCurrentIndex(2)
1523
1524
1525
1526
1527     def show_master_public_keys(self):
1528
1529         dialog = QDialog(self)
1530         dialog.setModal(1)
1531         dialog.setWindowTitle(_("Master Public Keys"))
1532
1533         main_layout = QGridLayout()
1534         mpk_dict = self.wallet.get_master_public_keys()
1535         i = 0
1536         for key, value in mpk_dict.items():
1537             main_layout.addWidget(QLabel(key), i, 0)
1538             mpk_text = QTextEdit()
1539             mpk_text.setReadOnly(True)
1540             mpk_text.setMaximumHeight(170)
1541             mpk_text.setText(value)
1542             main_layout.addWidget(mpk_text, i + 1, 0)
1543             i += 2
1544
1545         vbox = QVBoxLayout()
1546         vbox.addLayout(main_layout)
1547         vbox.addLayout(close_button(dialog))
1548
1549         dialog.setLayout(vbox)
1550         dialog.exec_()
1551
1552
1553     @protected
1554     def show_seed_dialog(self, password):
1555         if not self.wallet.has_seed():
1556             QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1557             return
1558
1559         try:
1560             mnemonic = self.wallet.get_mnemonic(password)
1561         except Exception:
1562             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1563             return
1564         from seed_dialog import SeedDialog
1565         d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1566         d.exec_()
1567
1568
1569
1570     def show_qrcode(self, data, title = _("QR code")):
1571         if not data: return
1572         d = QDialog(self)
1573         d.setModal(1)
1574         d.setWindowTitle(title)
1575         d.setMinimumSize(270, 300)
1576         vbox = QVBoxLayout()
1577         qrw = QRCodeWidget(data)
1578         vbox.addWidget(qrw, 1)
1579         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1580         hbox = QHBoxLayout()
1581         hbox.addStretch(1)
1582
1583         filename = os.path.join(self.config.path, "qrcode.bmp")
1584
1585         def print_qr():
1586             bmp.save_qrcode(qrw.qr, filename)
1587             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1588
1589         def copy_to_clipboard():
1590             bmp.save_qrcode(qrw.qr, filename)
1591             self.app.clipboard().setImage(QImage(filename))
1592             QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1593
1594         b = QPushButton(_("Copy"))
1595         hbox.addWidget(b)
1596         b.clicked.connect(copy_to_clipboard)
1597
1598         b = QPushButton(_("Save"))
1599         hbox.addWidget(b)
1600         b.clicked.connect(print_qr)
1601
1602         b = QPushButton(_("Close"))
1603         hbox.addWidget(b)
1604         b.clicked.connect(d.accept)
1605         b.setDefault(True)
1606
1607         vbox.addLayout(hbox)
1608         d.setLayout(vbox)
1609         d.exec_()
1610
1611
1612     def do_protect(self, func, args):
1613         if self.wallet.use_encryption:
1614             password = self.password_dialog()
1615             if not password:
1616                 return
1617         else:
1618             password = None
1619
1620         if args != (False,):
1621             args = (self,) + args + (password,)
1622         else:
1623             args = (self,password)
1624         apply( func, args)
1625
1626
1627     def show_public_keys(self, address):
1628         if not address: return
1629         try:
1630             pubkey_list = self.wallet.get_public_keys(address)
1631         except Exception as e:
1632             traceback.print_exc(file=sys.stdout)
1633             self.show_message(str(e))
1634             return
1635
1636         d = QDialog(self)
1637         d.setMinimumSize(600, 200)
1638         d.setModal(1)
1639         vbox = QVBoxLayout()
1640         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1641         vbox.addWidget( QLabel(_("Public key") + ':'))
1642         keys = QTextEdit()
1643         keys.setReadOnly(True)
1644         keys.setText('\n'.join(pubkey_list))
1645         vbox.addWidget(keys)
1646         #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1647         vbox.addLayout(close_button(d))
1648         d.setLayout(vbox)
1649         d.exec_()
1650
1651     @protected
1652     def show_private_key(self, address, password):
1653         if not address: return
1654         try:
1655             pk_list = self.wallet.get_private_key(address, password)
1656         except Exception as e:
1657             traceback.print_exc(file=sys.stdout)
1658             self.show_message(str(e))
1659             return
1660
1661         d = QDialog(self)
1662         d.setMinimumSize(600, 200)
1663         d.setModal(1)
1664         vbox = QVBoxLayout()
1665         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1666         vbox.addWidget( QLabel(_("Private key") + ':'))
1667         keys = QTextEdit()
1668         keys.setReadOnly(True)
1669         keys.setText('\n'.join(pk_list))
1670         vbox.addWidget(keys)
1671         vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1672         vbox.addLayout(close_button(d))
1673         d.setLayout(vbox)
1674         d.exec_()
1675
1676
1677     @protected
1678     def do_sign(self, address, message, signature, password):
1679         message = unicode(message.toPlainText())
1680         message = message.encode('utf-8')
1681         try:
1682             sig = self.wallet.sign_message(str(address.text()), message, password)
1683             signature.setText(sig)
1684         except Exception as e:
1685             self.show_message(str(e))
1686
1687     def do_verify(self, address, message, signature):
1688         message = unicode(message.toPlainText())
1689         message = message.encode('utf-8')
1690         if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1691             self.show_message(_("Signature verified"))
1692         else:
1693             self.show_message(_("Error: wrong signature"))
1694
1695
1696     def sign_verify_message(self, address=''):
1697         d = QDialog(self)
1698         d.setModal(1)
1699         d.setWindowTitle(_('Sign/verify Message'))
1700         d.setMinimumSize(410, 290)
1701
1702         layout = QGridLayout(d)
1703
1704         message_e = QTextEdit()
1705         layout.addWidget(QLabel(_('Message')), 1, 0)
1706         layout.addWidget(message_e, 1, 1)
1707         layout.setRowStretch(2,3)
1708
1709         address_e = QLineEdit()
1710         address_e.setText(address)
1711         layout.addWidget(QLabel(_('Address')), 2, 0)
1712         layout.addWidget(address_e, 2, 1)
1713
1714         signature_e = QTextEdit()
1715         layout.addWidget(QLabel(_('Signature')), 3, 0)
1716         layout.addWidget(signature_e, 3, 1)
1717         layout.setRowStretch(3,1)
1718
1719         hbox = QHBoxLayout()
1720
1721         b = QPushButton(_("Sign"))
1722         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1723         hbox.addWidget(b)
1724
1725         b = QPushButton(_("Verify"))
1726         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1727         hbox.addWidget(b)
1728
1729         b = QPushButton(_("Close"))
1730         b.clicked.connect(d.accept)
1731         hbox.addWidget(b)
1732         layout.addLayout(hbox, 4, 1)
1733         d.exec_()
1734
1735
1736     @protected
1737     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1738         try:
1739             decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1740             message_e.setText(decrypted)
1741         except Exception as e:
1742             self.show_message(str(e))
1743
1744
1745     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1746         message = unicode(message_e.toPlainText())
1747         message = message.encode('utf-8')
1748         try:
1749             encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1750             encrypted_e.setText(encrypted)
1751         except Exception as e:
1752             self.show_message(str(e))
1753
1754
1755
1756     def encrypt_message(self, address = ''):
1757         d = QDialog(self)
1758         d.setModal(1)
1759         d.setWindowTitle(_('Encrypt/decrypt Message'))
1760         d.setMinimumSize(610, 490)
1761
1762         layout = QGridLayout(d)
1763
1764         message_e = QTextEdit()
1765         layout.addWidget(QLabel(_('Message')), 1, 0)
1766         layout.addWidget(message_e, 1, 1)
1767         layout.setRowStretch(2,3)
1768
1769         pubkey_e = QLineEdit()
1770         if address:
1771             pubkey = self.wallet.getpubkeys(address)[0]
1772             pubkey_e.setText(pubkey)
1773         layout.addWidget(QLabel(_('Public key')), 2, 0)
1774         layout.addWidget(pubkey_e, 2, 1)
1775
1776         encrypted_e = QTextEdit()
1777         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1778         layout.addWidget(encrypted_e, 3, 1)
1779         layout.setRowStretch(3,1)
1780
1781         hbox = QHBoxLayout()
1782         b = QPushButton(_("Encrypt"))
1783         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1784         hbox.addWidget(b)
1785
1786         b = QPushButton(_("Decrypt"))
1787         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1788         hbox.addWidget(b)
1789
1790         b = QPushButton(_("Close"))
1791         b.clicked.connect(d.accept)
1792         hbox.addWidget(b)
1793
1794         layout.addLayout(hbox, 4, 1)
1795         d.exec_()
1796
1797
1798     def question(self, msg):
1799         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1800
1801     def show_message(self, msg):
1802         QMessageBox.information(self, _('Message'), msg, _('OK'))
1803
1804     def password_dialog(self, msg=None):
1805         d = QDialog(self)
1806         d.setModal(1)
1807         d.setWindowTitle(_("Enter Password"))
1808
1809         pw = QLineEdit()
1810         pw.setEchoMode(2)
1811
1812         vbox = QVBoxLayout()
1813         if not msg:
1814             msg = _('Please enter your password')
1815         vbox.addWidget(QLabel(msg))
1816
1817         grid = QGridLayout()
1818         grid.setSpacing(8)
1819         grid.addWidget(QLabel(_('Password')), 1, 0)
1820         grid.addWidget(pw, 1, 1)
1821         vbox.addLayout(grid)
1822
1823         vbox.addLayout(ok_cancel_buttons(d))
1824         d.setLayout(vbox)
1825
1826         run_hook('password_dialog', pw, grid, 1)
1827         if not d.exec_(): return
1828         return unicode(pw.text())
1829
1830
1831
1832
1833
1834
1835
1836
1837     def tx_from_text(self, txt):
1838         "json or raw hexadecimal"
1839         try:
1840             txt.decode('hex')
1841             tx = Transaction(txt)
1842             return tx
1843         except Exception:
1844             pass
1845
1846         try:
1847             tx_dict = json.loads(str(txt))
1848             assert "hex" in tx_dict.keys()
1849             tx = Transaction(tx_dict["hex"])
1850             if tx_dict.has_key("input_info"):
1851                 input_info = json.loads(tx_dict['input_info'])
1852                 tx.add_input_info(input_info)
1853             return tx
1854         except Exception:
1855             traceback.print_exc(file=sys.stdout)
1856             pass
1857
1858         QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1859
1860
1861
1862     def read_tx_from_file(self):
1863         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1864         if not fileName:
1865             return
1866         try:
1867             with open(fileName, "r") as f:
1868                 file_content = f.read()
1869         except (ValueError, IOError, os.error), reason:
1870             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1871
1872         return self.tx_from_text(file_content)
1873
1874
1875     @protected
1876     def sign_raw_transaction(self, tx, input_info, password):
1877         self.wallet.signrawtransaction(tx, input_info, [], password)
1878
1879     def do_process_from_text(self):
1880         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1881         if not text:
1882             return
1883         tx = self.tx_from_text(text)
1884         if tx:
1885             self.show_transaction(tx)
1886
1887     def do_process_from_file(self):
1888         tx = self.read_tx_from_file()
1889         if tx:
1890             self.show_transaction(tx)
1891
1892     def do_process_from_txid(self):
1893         from electrum import transaction
1894         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1895         if ok and txid:
1896             r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1897             if r:
1898                 tx = transaction.Transaction(r)
1899                 if tx:
1900                     self.show_transaction(tx)
1901                 else:
1902                     self.show_message("unknown transaction")
1903
1904     def do_process_from_csvReader(self, csvReader):
1905         outputs = []
1906         errors = []
1907         errtext = ""
1908         try:
1909             for position, row in enumerate(csvReader):
1910                 address = row[0]
1911                 if not is_valid(address):
1912                     errors.append((position, address))
1913                     continue
1914                 amount = Decimal(row[1])
1915                 amount = int(100000000*amount)
1916                 outputs.append((address, amount))
1917         except (ValueError, IOError, os.error), reason:
1918             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1919             return
1920         if errors != []:
1921             for x in errors:
1922                 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1923             QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1924             return
1925
1926         try:
1927             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1928         except Exception as e:
1929             self.show_message(str(e))
1930             return
1931
1932         self.show_transaction(tx)
1933
1934     def do_process_from_csv_file(self):
1935         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1936         if not fileName:
1937             return
1938         try:
1939             with open(fileName, "r") as f:
1940                 csvReader = csv.reader(f)
1941                 self.do_process_from_csvReader(csvReader)
1942         except (ValueError, IOError, os.error), reason:
1943             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1944             return
1945
1946     def do_process_from_csv_text(self):
1947         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1948                                + _("Format: address, amount. One output per line"), _("Load CSV"))
1949         if not text:
1950             return
1951         f = StringIO.StringIO(text)
1952         csvReader = csv.reader(f)
1953         self.do_process_from_csvReader(csvReader)
1954
1955
1956
1957     @protected
1958     def export_privkeys_dialog(self, password):
1959         if self.wallet.is_watching_only():
1960             self.show_message(_("This is a watching-only wallet"))
1961             return
1962
1963         d = QDialog(self)
1964         d.setWindowTitle(_('Private keys'))
1965         d.setMinimumSize(850, 300)
1966         vbox = QVBoxLayout(d)
1967
1968         msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."), 
1969                               _("Exposing a single private key can compromise your entire wallet!"), 
1970                               _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
1971         vbox.addWidget(QLabel(msg))
1972
1973         e = QTextEdit()
1974         e.setReadOnly(True)
1975         vbox.addWidget(e)
1976
1977         defaultname = 'electrum-private-keys.csv'
1978         select_msg = _('Select file to export your private keys to')
1979         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
1980         vbox.addLayout(hbox)
1981
1982         h, b = ok_cancel_buttons2(d, _('Export'))
1983         b.setEnabled(False)
1984         vbox.addLayout(h)
1985
1986         private_keys = {}
1987         addresses = self.wallet.addresses(True)
1988         done = False
1989         def privkeys_thread():
1990             for addr in addresses:
1991                 time.sleep(0.1)
1992                 if done: 
1993                     break
1994                 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
1995                 d.emit(SIGNAL('computing_privkeys'))
1996             d.emit(SIGNAL('show_privkeys'))
1997
1998         def show_privkeys():
1999             s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2000             e.setText(s)
2001             b.setEnabled(True)
2002
2003         d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2004         d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2005         threading.Thread(target=privkeys_thread).start()
2006
2007         if not d.exec_():
2008             done = True
2009             return
2010
2011         filename = filename_e.text()
2012         if not filename:
2013             return
2014
2015         try:
2016             self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2017         except (IOError, os.error), reason:
2018             export_error_label = _("Electrum was unable to produce a private key-export.")
2019             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2020
2021         except Exception as e:
2022             self.show_message(str(e))
2023             return
2024
2025         self.show_message(_("Private keys exported."))
2026
2027
2028     def do_export_privkeys(self, fileName, pklist, is_csv):
2029         with open(fileName, "w+") as f:
2030             if is_csv:
2031                 transaction = csv.writer(f)
2032                 transaction.writerow(["address", "private_key"])
2033                 for addr, pk in pklist.items():
2034                     transaction.writerow(["%34s"%addr,pk])
2035             else:
2036                 import json
2037                 f.write(json.dumps(pklist, indent = 4))
2038
2039
2040     def do_import_labels(self):
2041         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2042         if not labelsFile: return
2043         try:
2044             f = open(labelsFile, 'r')
2045             data = f.read()
2046             f.close()
2047             for key, value in json.loads(data).items():
2048                 self.wallet.set_label(key, value)
2049             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2050         except (IOError, os.error), reason:
2051             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2052
2053
2054     def do_export_labels(self):
2055         labels = self.wallet.labels
2056         try:
2057             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2058             if fileName:
2059                 with open(fileName, 'w+') as f:
2060                     json.dump(labels, f)
2061                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2062         except (IOError, os.error), reason:
2063             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2064
2065
2066     def export_history_dialog(self):
2067
2068         d = QDialog(self)
2069         d.setWindowTitle(_('Export History'))
2070         d.setMinimumSize(400, 200)
2071         vbox = QVBoxLayout(d)
2072
2073         defaultname = os.path.expanduser('~/electrum-history.csv')
2074         select_msg = _('Select file to export your wallet transactions to')
2075
2076         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2077         vbox.addLayout(hbox)
2078
2079         vbox.addStretch(1)
2080
2081         h, b = ok_cancel_buttons2(d, _('Export'))
2082         vbox.addLayout(h)
2083         if not d.exec_():
2084             return
2085
2086         filename = filename_e.text()
2087         if not filename:
2088             return
2089
2090         try:
2091             self.do_export_history(self.wallet, filename, csv_button.isChecked())
2092         except (IOError, os.error), reason:
2093             export_error_label = _("Electrum was unable to produce a transaction export.")
2094             QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2095             return
2096
2097         QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2098
2099
2100     def do_export_history(self, wallet, fileName, is_csv):
2101         history = wallet.get_tx_history()
2102         lines = []
2103         for item in history:
2104             tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2105             if confirmations:
2106                 if timestamp is not None:
2107                     try:
2108                         time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2109                     except [RuntimeError, TypeError, NameError] as reason:
2110                         time_string = "unknown"
2111                         pass
2112                 else:
2113                     time_string = "unknown"
2114             else:
2115                 time_string = "pending"
2116
2117             if value is not None:
2118                 value_string = format_satoshis(value, True)
2119             else:
2120                 value_string = '--'
2121
2122             if fee is not None:
2123                 fee_string = format_satoshis(fee, True)
2124             else:
2125                 fee_string = '0'
2126
2127             if tx_hash:
2128                 label, is_default_label = wallet.get_label(tx_hash)
2129                 label = label.encode('utf-8')
2130             else:
2131                 label = ""
2132
2133             balance_string = format_satoshis(balance, False)
2134             if is_csv:
2135                 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2136             else:
2137                 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2138
2139         with open(fileName, "w+") as f:
2140             if is_csv:
2141                 transaction = csv.writer(f)
2142                 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2143                 for line in lines:
2144                     transaction.writerow(line)
2145             else:
2146                 import json
2147                 f.write(json.dumps(lines, indent = 4))
2148
2149
2150     def sweep_key_dialog(self):
2151         d = QDialog(self)
2152         d.setWindowTitle(_('Sweep private keys'))
2153         d.setMinimumSize(600, 300)
2154
2155         vbox = QVBoxLayout(d)
2156         vbox.addWidget(QLabel(_("Enter private keys")))
2157
2158         keys_e = QTextEdit()
2159         keys_e.setTabChangesFocus(True)
2160         vbox.addWidget(keys_e)
2161
2162         h, address_e = address_field(self.wallet.addresses())
2163         vbox.addLayout(h)
2164
2165         vbox.addStretch(1)
2166         hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2167         vbox.addLayout(hbox)
2168         button.setEnabled(False)
2169
2170         def get_address():
2171             addr = str(address_e.text())
2172             if bitcoin.is_address(addr):
2173                 return addr
2174
2175         def get_pk():
2176             pk = str(keys_e.toPlainText()).strip()
2177             if Wallet.is_private_key(pk):
2178                 return pk.split()
2179
2180         f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2181         keys_e.textChanged.connect(f)
2182         address_e.textChanged.connect(f)
2183         if not d.exec_():
2184             return
2185
2186         fee = self.wallet.fee
2187         tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2188         self.show_transaction(tx)
2189
2190
2191     @protected
2192     def do_import_privkey(self, password):
2193         if not self.wallet.has_imported_keys():
2194             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2195                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2196                                          + _('Are you sure you understand what you are doing?'), 3, 4)
2197             if r == 4: return
2198
2199         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2200         if not text: return
2201
2202         text = str(text).split()
2203         badkeys = []
2204         addrlist = []
2205         for key in text:
2206             try:
2207                 addr = self.wallet.import_key(key, password)
2208             except Exception as e:
2209                 badkeys.append(key)
2210                 continue
2211             if not addr:
2212                 badkeys.append(key)
2213             else:
2214                 addrlist.append(addr)
2215         if addrlist:
2216             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2217         if badkeys:
2218             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2219         self.update_receive_tab()
2220         self.update_history_tab()
2221
2222
2223     def settings_dialog(self):
2224         d = QDialog(self)
2225         d.setWindowTitle(_('Electrum Settings'))
2226         d.setModal(1)
2227         vbox = QVBoxLayout()
2228         grid = QGridLayout()
2229         grid.setColumnStretch(0,1)
2230
2231         nz_label = QLabel(_('Display zeros') + ':')
2232         grid.addWidget(nz_label, 0, 0)
2233         nz_e = AmountEdit(None,True)
2234         nz_e.setText("%d"% self.num_zeros)
2235         grid.addWidget(nz_e, 0, 1)
2236         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2237         grid.addWidget(HelpButton(msg), 0, 2)
2238         if not self.config.is_modifiable('num_zeros'):
2239             for w in [nz_e, nz_label]: w.setEnabled(False)
2240
2241         lang_label=QLabel(_('Language') + ':')
2242         grid.addWidget(lang_label, 1, 0)
2243         lang_combo = QComboBox()
2244         from electrum.i18n import languages
2245         lang_combo.addItems(languages.values())
2246         try:
2247             index = languages.keys().index(self.config.get("language",''))
2248         except Exception:
2249             index = 0
2250         lang_combo.setCurrentIndex(index)
2251         grid.addWidget(lang_combo, 1, 1)
2252         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2253         if not self.config.is_modifiable('language'):
2254             for w in [lang_combo, lang_label]: w.setEnabled(False)
2255
2256
2257         fee_label = QLabel(_('Transaction fee') + ':')
2258         grid.addWidget(fee_label, 2, 0)
2259         fee_e = BTCAmountEdit(self.get_decimal_point)
2260         fee_e.setText(self.format_amount(self.wallet.fee).strip())
2261         grid.addWidget(fee_e, 2, 1)
2262         msg = _('Fee per kilobyte of transaction.') + ' ' \
2263             + _('Recommended value') + ': ' + self.format_amount(20000)
2264         grid.addWidget(HelpButton(msg), 2, 2)
2265         if not self.config.is_modifiable('fee_per_kb'):
2266             for w in [fee_e, fee_label]: w.setEnabled(False)
2267
2268         units = ['BTC', 'mBTC']
2269         unit_label = QLabel(_('Base unit') + ':')
2270         grid.addWidget(unit_label, 3, 0)
2271         unit_combo = QComboBox()
2272         unit_combo.addItems(units)
2273         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2274         grid.addWidget(unit_combo, 3, 1)
2275         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2276                                              + '\n1BTC=1000mBTC.\n' \
2277                                              + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2278
2279         usechange_cb = QCheckBox(_('Use change addresses'))
2280         usechange_cb.setChecked(self.wallet.use_change)
2281         grid.addWidget(usechange_cb, 4, 0)
2282         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2283         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2284
2285         block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2286         block_ex_label = QLabel(_('Online Block Explorer') + ':')
2287         grid.addWidget(block_ex_label, 5, 0)
2288         block_ex_combo = QComboBox()
2289         block_ex_combo.addItems(block_explorers)
2290         block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2291         grid.addWidget(block_ex_combo, 5, 1)
2292         grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2293
2294         show_tx = self.config.get('show_before_broadcast', False)
2295         showtx_cb = QCheckBox(_('Show before broadcast'))
2296         showtx_cb.setChecked(show_tx)
2297         grid.addWidget(showtx_cb, 6, 0)
2298         grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2299
2300         vbox.addLayout(grid)
2301         vbox.addStretch(1)
2302         vbox.addLayout(ok_cancel_buttons(d))
2303         d.setLayout(vbox)
2304
2305         # run the dialog
2306         if not d.exec_(): return
2307
2308         try:
2309             fee = self.fee_e.get_amount()
2310         except Exception:
2311             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2312             return
2313
2314         self.wallet.set_fee(fee)
2315
2316         nz = unicode(nz_e.text())
2317         try:
2318             nz = int( nz )
2319             if nz>8: nz=8
2320         except Exception:
2321             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2322             return
2323
2324         if self.num_zeros != nz:
2325             self.num_zeros = nz
2326             self.config.set_key('num_zeros', nz, True)
2327             self.update_history_tab()
2328             self.update_receive_tab()
2329
2330         usechange_result = usechange_cb.isChecked()
2331         if self.wallet.use_change != usechange_result:
2332             self.wallet.use_change = usechange_result
2333             self.wallet.storage.put('use_change', self.wallet.use_change)
2334
2335         if showtx_cb.isChecked() != show_tx:
2336             self.config.set_key('show_before_broadcast', not show_tx)
2337
2338         unit_result = units[unit_combo.currentIndex()]
2339         if self.base_unit() != unit_result:
2340             self.decimal_point = 8 if unit_result == 'BTC' else 5
2341             self.config.set_key('decimal_point', self.decimal_point, True)
2342             self.update_history_tab()
2343             self.update_status()
2344
2345         need_restart = False
2346
2347         lang_request = languages.keys()[lang_combo.currentIndex()]
2348         if lang_request != self.config.get('language'):
2349             self.config.set_key("language", lang_request, True)
2350             need_restart = True
2351
2352         be_result = block_explorers[block_ex_combo.currentIndex()]
2353         self.config.set_key('block_explorer', be_result, True)
2354
2355         run_hook('close_settings_dialog')
2356
2357         if need_restart:
2358             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2359
2360
2361     def run_network_dialog(self):
2362         if not self.network:
2363             return
2364         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2365
2366     def closeEvent(self, event):
2367         self.tray.hide()
2368         self.config.set_key("is_maximized", self.isMaximized())
2369         if not self.isMaximized():
2370             g = self.geometry()
2371             self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2372         self.save_column_widths()
2373         self.config.set_key("console-history", self.console.history[-50:], True)
2374         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2375         event.accept()
2376
2377
2378     def plugins_dialog(self):
2379         from electrum.plugins import plugins
2380
2381         d = QDialog(self)
2382         d.setWindowTitle(_('Electrum Plugins'))
2383         d.setModal(1)
2384
2385         vbox = QVBoxLayout(d)
2386
2387         # plugins
2388         scroll = QScrollArea()
2389         scroll.setEnabled(True)
2390         scroll.setWidgetResizable(True)
2391         scroll.setMinimumSize(400,250)
2392         vbox.addWidget(scroll)
2393
2394         w = QWidget()
2395         scroll.setWidget(w)
2396         w.setMinimumHeight(len(plugins)*35)
2397
2398         grid = QGridLayout()
2399         grid.setColumnStretch(0,1)
2400         w.setLayout(grid)
2401
2402         def do_toggle(cb, p, w):
2403             r = p.toggle()
2404             cb.setChecked(r)
2405             if w: w.setEnabled(r)
2406
2407         def mk_toggle(cb, p, w):
2408             return lambda: do_toggle(cb,p,w)
2409
2410         for i, p in enumerate(plugins):
2411             try:
2412                 cb = QCheckBox(p.fullname())
2413                 cb.setDisabled(not p.is_available())
2414                 cb.setChecked(p.is_enabled())
2415                 grid.addWidget(cb, i, 0)
2416                 if p.requires_settings():
2417                     w = p.settings_widget(self)
2418                     w.setEnabled( p.is_enabled() )
2419                     grid.addWidget(w, i, 1)
2420                 else:
2421                     w = None
2422                 cb.clicked.connect(mk_toggle(cb,p,w))
2423                 grid.addWidget(HelpButton(p.description()), i, 2)
2424             except Exception:
2425                 print_msg(_("Error: cannot display plugin"), p)
2426                 traceback.print_exc(file=sys.stdout)
2427         grid.setRowStretch(i+1,1)
2428
2429         vbox.addLayout(close_button(d))
2430
2431         d.exec_()
2432
2433
2434     def show_account_details(self, k):
2435         account = self.wallet.accounts[k]
2436
2437         d = QDialog(self)
2438         d.setWindowTitle(_('Account Details'))
2439         d.setModal(1)
2440
2441         vbox = QVBoxLayout(d)
2442         name = self.wallet.get_account_name(k)
2443         label = QLabel('Name: ' + name)
2444         vbox.addWidget(label)
2445
2446         vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2447
2448         vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2449
2450         vbox.addWidget(QLabel(_('Master Public Key:')))
2451
2452         text = QTextEdit()
2453         text.setReadOnly(True)
2454         text.setMaximumHeight(170)
2455         vbox.addWidget(text)
2456
2457         mpk_text = '\n'.join( account.get_master_pubkeys() )
2458         text.setText(mpk_text)
2459
2460         vbox.addLayout(close_button(d))
2461         d.exec_()