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