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