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