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