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