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