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