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