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