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