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