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