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