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