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