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