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