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