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