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