3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
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.
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.
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/>.
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
28 from PyQt4.QtGui import *
29 from PyQt4.QtCore import *
30 import PyQt4.QtCore as QtCore
32 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
33 from electrum.plugins import run_hook
37 from electrum.wallet import format_satoshis
38 from electrum import Transaction
39 from electrum import mnemonic
40 from electrum import util, bitcoin, commands, Interface, Wallet
41 from electrum import SimpleConfig, Wallet, WalletStorage
44 from electrum import bmp, pyqrnative
46 from amountedit import AmountEdit
47 from network_dialog import NetworkDialog
48 from qrcodewidget import QRCodeWidget
50 from decimal import Decimal
58 if platform.system() == 'Windows':
59 MONOSPACE_FONT = 'Lucida Console'
60 elif platform.system() == 'Darwin':
61 MONOSPACE_FONT = 'Monaco'
63 MONOSPACE_FONT = 'monospace'
65 from electrum import ELECTRUM_VERSION
75 class StatusBarButton(QPushButton):
76 def __init__(self, icon, tooltip, func):
77 QPushButton.__init__(self, icon, '')
78 self.setToolTip(tooltip)
80 self.setMaximumWidth(25)
81 self.clicked.connect(func)
83 self.setIconSize(QSize(25,25))
85 def keyPressEvent(self, e):
86 if e.key() == QtCore.Qt.Key_Return:
98 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive":[[370], [370,200,130]] }
100 class ElectrumWindow(QMainWindow):
101 def changeEvent(self, event):
102 flags = self.windowFlags();
103 if event and event.type() == QtCore.QEvent.WindowStateChange:
104 if self.windowState() & QtCore.Qt.WindowMinimized:
105 self.build_menu(True)
106 # The only way to toggle the icon in the window managers taskbar is to use the Qt.Tooltip flag
107 # The problem is that it somehow creates an (in)visible window that will stay active and prevent
108 # Electrum from closing.
109 # As for now I have no clue how to implement a proper 'hide to tray' functionality.
110 # self.setWindowFlags(flags & ~Qt.ToolTip)
111 elif event.oldState() & QtCore.Qt.WindowMinimized:
112 self.build_menu(False)
113 #self.setWindowFlags(flags | Qt.ToolTip)
115 def build_menu(self, is_hidden = False):
117 if self.isMinimized():
118 m.addAction(_("Show"), self.showNormal)
120 m.addAction(_("Hide"), self.showMinimized)
123 m.addAction(_("Exit Electrum"), self.close)
124 self.tray.setContextMenu(m)
126 def tray_activated(self, reason):
127 if reason == QSystemTrayIcon.DoubleClick:
131 def __init__(self, config, network):
132 QMainWindow.__init__(self)
135 self.network = network
137 self._close_electrum = False
139 self.current_account = self.config.get("current_account", None)
141 self.icon = QIcon(':icons/electrum.png')
142 self.tray = QSystemTrayIcon(self.icon, self)
143 self.tray.setToolTip('Electrum')
144 self.tray.activated.connect(self.tray_activated)
148 self.create_status_bar()
150 self.need_update = threading.Event()
152 self.expert_mode = config.get('classic_expert_mode', False)
153 self.decimal_point = config.get('decimal_point', 8)
154 self.num_zeros = int(config.get('num_zeros',0))
156 set_language(config.get('language'))
158 self.funds_error = False
159 self.completions = QStringListModel()
161 self.tabs = tabs = QTabWidget(self)
162 self.column_widths = self.config.get("column_widths", default_column_widths )
163 tabs.addTab(self.create_history_tab(), _('History') )
164 tabs.addTab(self.create_send_tab(), _('Send') )
165 tabs.addTab(self.create_receive_tab(), _('Receive') )
166 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
167 tabs.addTab(self.create_console_tab(), _('Console') )
168 tabs.setMinimumSize(600, 400)
169 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
170 self.setCentralWidget(tabs)
172 g = self.config.get("winpos-qt",[100, 100, 840, 400])
173 self.setGeometry(g[0], g[1], g[2], g[3])
177 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
178 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
179 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
180 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
181 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
183 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
184 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
185 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
187 self.history_list.setFocus(True)
190 self.network.register_callback('updated', lambda: self.need_update.set())
191 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
192 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
193 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
194 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
195 # set initial message
196 self.console.showMessage(self.network.banner)
198 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
199 if platform.system() == 'Windows':
200 n = 3 if self.wallet.seed else 2
201 tabs.setCurrentIndex (n)
202 tabs.setCurrentIndex (0)
209 self.config.set_key('lite_mode', False, True)
214 self.config.set_key('lite_mode', True, True)
221 if not self.check_qt_version():
222 if self.config.get('lite_mode') is True:
223 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
224 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
225 self.config.set_key('lite_mode', False, True)
230 actuator = lite_window.MiniActuator(self)
232 # Should probably not modify the current path but instead
233 # change the behaviour of rsrc(...)
234 old_path = QDir.currentPath()
235 actuator.load_theme()
237 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
239 driver = lite_window.MiniDriver(self, self.mini)
241 # Reset path back to original value now that loading the GUI
243 QDir.setCurrent(old_path)
245 if self.config.get('lite_mode') is True:
251 def check_qt_version(self):
252 qtVersion = qVersion()
253 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
257 def load_wallet(self, wallet):
261 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
262 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
263 self.setWindowTitle( title )
265 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
266 self.notify_transactions()
269 accounts = self.wallet.get_account_names()
270 self.account_selector.clear()
271 if len(accounts) > 1:
272 self.account_selector.addItems([_("All accounts")] + accounts.values())
273 self.account_selector.setCurrentIndex(0)
274 self.account_selector.show()
276 self.account_selector.hide()
278 self.new_account.setEnabled(self.wallet.seed_version>4)
280 self.update_lock_icon()
281 self.update_buttons_on_seed()
282 self.update_console()
284 run_hook('load_wallet', wallet)
287 def select_wallet_file(self):
288 wallet_folder = self.wallet.storage.path
289 re.sub("(\/\w*.dat)$", "", wallet_folder)
290 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
294 def open_wallet(self):
296 filename = self.select_wallet_file()
300 storage = WalletStorage({'wallet_path': filename})
301 if not storage.file_exists:
302 self.show_message("file not found "+ filename)
305 self.wallet.stop_threads()
308 wallet = Wallet(storage)
309 wallet.start_threads(self.network)
311 self.load_wallet(wallet)
315 def backup_wallet(self):
317 path = self.wallet.storage.path
318 wallet_folder = os.path.dirname(path)
319 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
320 new_filename = unicode(new_filename)
321 if not ok or not new_filename:
324 new_path = os.path.join(wallet_folder, new_filename)
327 shutil.copy2(path, new_path)
328 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
329 except (IOError, os.error), reason:
330 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
333 def new_wallet(self):
336 wallet_folder = os.path.dirname(self.wallet.storage.path)
337 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
338 filename = unicode(filename)
339 if not ok or not filename:
341 filename = os.path.join(wallet_folder, filename)
343 storage = WalletStorage({'wallet_path': filename})
344 assert not storage.file_exists
346 wizard = installwizard.InstallWizard(self.config, self.network, storage)
347 wallet = wizard.run()
349 self.load_wallet(wallet)
353 def init_menubar(self):
356 file_menu = menubar.addMenu(_("&File"))
357 open_wallet_action = file_menu.addAction(_("&Open"))
358 open_wallet_action.triggered.connect(self.open_wallet)
360 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
361 new_wallet_action.triggered.connect(self.new_wallet)
363 wallet_backup = file_menu.addAction(_("&Copy"))
364 wallet_backup.triggered.connect(self.backup_wallet)
366 quit_item = file_menu.addAction(_("&Close"))
367 quit_item.triggered.connect(self.close)
369 wallet_menu = menubar.addMenu(_("&Wallet"))
371 new_contact = wallet_menu.addAction(_("&New contact"))
372 new_contact.triggered.connect(self.new_contact_dialog)
374 self.new_account = wallet_menu.addAction(_("&New account"))
375 self.new_account.triggered.connect(self.new_account_dialog)
377 wallet_menu.addSeparator()
379 pw = wallet_menu.addAction(_("&Password"))
380 pw.triggered.connect(self.change_password_dialog)
382 show_seed = wallet_menu.addAction(_("&Seed"))
383 show_seed.triggered.connect(self.show_seed_dialog)
385 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
386 show_mpk.triggered.connect(self.show_master_public_key)
388 wallet_menu.addSeparator()
390 labels_menu = wallet_menu.addMenu(_("&Labels"))
391 import_labels = labels_menu.addAction(_("&Import"))
392 import_labels.triggered.connect(self.do_import_labels)
393 export_labels = labels_menu.addAction(_("&Export"))
394 export_labels.triggered.connect(self.do_export_labels)
396 keys_menu = wallet_menu.addMenu(_("&Private keys"))
397 import_keys = keys_menu.addAction(_("&Import"))
398 import_keys.triggered.connect(self.do_import_privkey)
399 export_keys = keys_menu.addAction(_("&Export"))
400 export_keys.triggered.connect(self.do_export_privkeys)
402 ex_history = wallet_menu.addAction(_("&Export History"))
403 ex_history.triggered.connect(self.do_export_history)
407 tools_menu = menubar.addMenu(_("&Tools"))
409 # Settings / Preferences are all reserved keywords in OSX using this as work around
410 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
411 preferences_menu = tools_menu.addAction(preferences_name)
412 preferences_menu.triggered.connect(self.settings_dialog)
414 network = tools_menu.addAction(_("&Network"))
415 network.triggered.connect(self.run_network_dialog)
417 plugins_labels = tools_menu.addAction(_("&Plugins"))
418 plugins_labels.triggered.connect(self.plugins_dialog)
420 tools_menu.addSeparator()
422 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
424 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
425 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
427 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
428 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
430 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
432 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
433 raw_transaction_file.triggered.connect(self.do_process_from_file)
435 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
436 raw_transaction_text.triggered.connect(self.do_process_from_text)
439 help_menu = menubar.addMenu(_("&Help"))
440 show_about = help_menu.addAction(_("&About"))
441 show_about.triggered.connect(self.show_about)
442 web_open = help_menu.addAction(_("&Official website"))
443 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
445 help_menu.addSeparator()
446 doc_open = help_menu.addAction(_("&Documentation"))
447 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
448 report_bug = help_menu.addAction(_("&Report Bug"))
449 report_bug.triggered.connect(self.show_report_bug)
451 self.setMenuBar(menubar)
453 def show_about(self):
454 QMessageBox.about(self, "Electrum",
455 _("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."))
457 def show_report_bug(self):
458 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
459 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
462 def notify_transactions(self):
463 print_error("Notifying GUI")
464 if len(self.network.interface.pending_transactions_for_notifications) > 0:
465 # Combine the transactions if there are more then three
466 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
469 for tx in self.network.interface.pending_transactions_for_notifications:
470 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
474 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
475 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
477 self.network.interface.pending_transactions_for_notifications = []
479 for tx in self.network.interface.pending_transactions_for_notifications:
481 self.network.interface.pending_transactions_for_notifications.remove(tx)
482 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
484 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
486 def notify(self, message):
487 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
491 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
492 def getOpenFileName(self, title, filter = ""):
493 directory = self.config.get('io_dir', os.path.expanduser('~'))
494 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
495 if fileName and directory != os.path.dirname(fileName):
496 self.config.set_key('io_dir', os.path.dirname(fileName), True)
499 def getSaveFileName(self, title, filename, filter = ""):
500 directory = self.config.get('io_dir', os.path.expanduser('~'))
501 path = os.path.join( directory, filename )
502 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
503 if fileName and directory != os.path.dirname(fileName):
504 self.config.set_key('io_dir', os.path.dirname(fileName), True)
508 QMainWindow.close(self)
509 run_hook('close_main_window')
511 def connect_slots(self, sender):
512 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
513 self.previous_payto_e=''
515 def timer_actions(self):
516 if self.need_update.is_set():
518 self.need_update.clear()
519 run_hook('timer_actions')
521 def format_amount(self, x, is_diff=False, whitespaces=False):
522 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
524 def read_amount(self, x):
525 if x in['.', '']: return None
526 p = pow(10, self.decimal_point)
527 return int( p * Decimal(x) )
530 assert self.decimal_point in [5,8]
531 return "BTC" if self.decimal_point == 8 else "mBTC"
534 def update_status(self):
535 if self.network.interface and self.network.interface.is_connected:
536 if not self.wallet.up_to_date:
537 text = _("Synchronizing...")
538 icon = QIcon(":icons/status_waiting.png")
540 c, u = self.wallet.get_account_balance(self.current_account)
541 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
542 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
545 run_hook('set_quote_text', c+u, r)
548 text += " (%s)"%quote
550 self.tray.setToolTip(text)
551 icon = QIcon(":icons/status_connected.png")
553 text = _("Not connected")
554 icon = QIcon(":icons/status_disconnected.png")
556 self.balance_label.setText(text)
557 self.status_button.setIcon( icon )
560 def update_wallet(self):
562 if self.wallet.up_to_date or not self.network.interface.is_connected:
563 self.update_history_tab()
564 self.update_receive_tab()
565 self.update_contacts_tab()
566 self.update_completions()
569 def create_history_tab(self):
570 self.history_list = l = MyTreeWidget(self)
572 for i,width in enumerate(self.column_widths['history']):
573 l.setColumnWidth(i, width)
574 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
575 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
576 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
578 l.customContextMenuRequested.connect(self.create_history_menu)
582 def create_history_menu(self, position):
583 self.history_list.selectedIndexes()
584 item = self.history_list.currentItem()
586 tx_hash = str(item.data(0, Qt.UserRole).toString())
587 if not tx_hash: return
589 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
590 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
591 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
592 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
595 def show_transaction(self, tx):
596 import transaction_dialog
597 d = transaction_dialog.TxDialog(tx, self)
600 def tx_label_clicked(self, item, column):
601 if column==2 and item.isSelected():
603 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
604 self.history_list.editItem( item, column )
605 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
608 def tx_label_changed(self, item, column):
612 tx_hash = str(item.data(0, Qt.UserRole).toString())
613 tx = self.wallet.transactions.get(tx_hash)
614 text = unicode( item.text(2) )
615 self.wallet.set_label(tx_hash, text)
617 item.setForeground(2, QBrush(QColor('black')))
619 text = self.wallet.get_default_label(tx_hash)
620 item.setText(2, text)
621 item.setForeground(2, QBrush(QColor('gray')))
625 def edit_label(self, is_recv):
626 l = self.receive_list if is_recv else self.contacts_list
627 item = l.currentItem()
628 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
629 l.editItem( item, 1 )
630 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
634 def address_label_clicked(self, item, column, l, column_addr, column_label):
635 if column == column_label and item.isSelected():
636 is_editable = item.data(0, 32).toBool()
639 addr = unicode( item.text(column_addr) )
640 label = unicode( item.text(column_label) )
641 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
642 l.editItem( item, column )
643 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
646 def address_label_changed(self, item, column, l, column_addr, column_label):
647 if column == column_label:
648 addr = unicode( item.text(column_addr) )
649 text = unicode( item.text(column_label) )
650 is_editable = item.data(0, 32).toBool()
654 changed = self.wallet.set_label(addr, text)
656 self.update_history_tab()
657 self.update_completions()
659 self.current_item_changed(item)
661 run_hook('item_changed', item, column)
664 def current_item_changed(self, a):
665 run_hook('current_item_changed', a)
669 def update_history_tab(self):
671 self.history_list.clear()
672 for item in self.wallet.get_tx_history(self.current_account):
673 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
676 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
678 time_str = _("unknown")
681 time_str = 'unverified'
682 icon = QIcon(":icons/unconfirmed.png")
685 icon = QIcon(":icons/unconfirmed.png")
687 icon = QIcon(":icons/clock%d.png"%conf)
689 icon = QIcon(":icons/confirmed.png")
691 if value is not None:
692 v_str = self.format_amount(value, True, whitespaces=True)
696 balance_str = self.format_amount(balance, whitespaces=True)
699 label, is_default_label = self.wallet.get_label(tx_hash)
701 label = _('Pruned transaction outputs')
702 is_default_label = False
704 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
705 item.setFont(2, QFont(MONOSPACE_FONT))
706 item.setFont(3, QFont(MONOSPACE_FONT))
707 item.setFont(4, QFont(MONOSPACE_FONT))
709 item.setForeground(3, QBrush(QColor("#BC1E1E")))
711 item.setData(0, Qt.UserRole, tx_hash)
712 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
714 item.setForeground(2, QBrush(QColor('grey')))
716 item.setIcon(0, icon)
717 self.history_list.insertTopLevelItem(0,item)
720 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
723 def create_send_tab(self):
728 grid.setColumnMinimumWidth(3,300)
729 grid.setColumnStretch(5,1)
732 self.payto_e = QLineEdit()
733 grid.addWidget(QLabel(_('Pay to')), 1, 0)
734 grid.addWidget(self.payto_e, 1, 1, 1, 3)
736 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)
738 completer = QCompleter()
739 completer.setCaseSensitivity(False)
740 self.payto_e.setCompleter(completer)
741 completer.setModel(self.completions)
743 self.message_e = QLineEdit()
744 grid.addWidget(QLabel(_('Description')), 2, 0)
745 grid.addWidget(self.message_e, 2, 1, 1, 3)
746 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)
748 self.amount_e = AmountEdit(self.base_unit)
749 grid.addWidget(QLabel(_('Amount')), 3, 0)
750 grid.addWidget(self.amount_e, 3, 1, 1, 2)
751 grid.addWidget(HelpButton(
752 _('Amount to be sent.') + '\n\n' \
753 + _('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.') \
754 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
756 self.fee_e = AmountEdit(self.base_unit)
757 grid.addWidget(QLabel(_('Fee')), 4, 0)
758 grid.addWidget(self.fee_e, 4, 1, 1, 2)
759 grid.addWidget(HelpButton(
760 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
761 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
762 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
765 self.send_button = EnterButton(_("Send"), self.do_send)
766 grid.addWidget(self.send_button, 6, 1)
768 b = EnterButton(_("Clear"),self.do_clear)
769 grid.addWidget(b, 6, 2)
771 self.payto_sig = QLabel('')
772 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
774 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
775 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
784 def entry_changed( is_fee ):
785 self.funds_error = False
787 if self.amount_e.is_shortcut:
788 self.amount_e.is_shortcut = False
789 c, u = self.wallet.get_account_balance(self.current_account)
790 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
791 fee = self.wallet.estimated_fee(inputs)
793 self.amount_e.setText( self.format_amount(amount) )
794 self.fee_e.setText( self.format_amount( fee ) )
797 amount = self.read_amount(str(self.amount_e.text()))
798 fee = self.read_amount(str(self.fee_e.text()))
800 if not is_fee: fee = None
803 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
805 self.fee_e.setText( self.format_amount( fee ) )
808 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
812 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
813 self.funds_error = True
814 text = _( "Not enough funds" )
815 c, u = self.wallet.get_frozen_balance()
816 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
818 self.statusBar().showMessage(text)
819 self.amount_e.setPalette(palette)
820 self.fee_e.setPalette(palette)
822 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
823 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
825 run_hook('create_send_tab', grid)
829 def update_completions(self):
831 for addr,label in self.wallet.labels.items():
832 if addr in self.wallet.addressbook:
833 l.append( label + ' <' + addr + '>')
835 run_hook('update_completions', l)
836 self.completions.setStringList(l)
840 return lambda s, *args: s.do_protect(func, args)
845 label = unicode( self.message_e.text() )
846 r = unicode( self.payto_e.text() )
849 # label or alias, with address in brackets
850 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
851 to_address = m.group(2) if m else r
853 if not is_valid(to_address):
854 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
858 amount = self.read_amount(unicode( self.amount_e.text()))
860 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
863 fee = self.read_amount(unicode( self.fee_e.text()))
865 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
868 confirm_amount = self.config.get('confirm_amount', 100000000)
869 if amount >= confirm_amount:
870 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
873 self.send_tx(to_address, amount, fee, label)
877 def send_tx(self, to_address, amount, fee, label, password):
880 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
881 except BaseException, e:
882 traceback.print_exc(file=sys.stdout)
883 self.show_message(str(e))
886 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
887 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
891 self.wallet.set_label(tx.hash(), label)
894 h = self.wallet.send_tx(tx)
895 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
896 status, msg = self.wallet.receive_tx( h )
898 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
900 self.update_contacts_tab()
902 QMessageBox.warning(self, _('Error'), msg, _('OK'))
904 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
906 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
907 with open(fileName,'w') as f:
908 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
909 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
911 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
913 # add recipient to addressbook
914 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
915 self.wallet.addressbook.append(to_address)
920 def set_url(self, url):
921 address, amount, label, message, signature, identity, url = util.parse_url(url)
923 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
926 self.mini.set_payment_fields(address, amount)
928 if label and self.wallet.labels.get(address) != label:
929 if self.question('Give label "%s" to address %s ?'%(label,address)):
930 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
931 self.wallet.addressbook.append(address)
932 self.wallet.set_label(address, label)
934 run_hook('set_url', url, self.show_message, self.question)
936 self.tabs.setCurrentIndex(1)
937 label = self.wallet.labels.get(address)
938 m_addr = label + ' <'+ address +'>' if label else address
939 self.payto_e.setText(m_addr)
941 self.message_e.setText(message)
943 self.amount_e.setText(amount)
946 self.set_frozen(self.payto_e,True)
947 self.set_frozen(self.amount_e,True)
948 self.set_frozen(self.message_e,True)
949 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
951 self.payto_sig.setVisible(False)
954 self.payto_sig.setVisible(False)
955 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
957 self.set_frozen(e,False)
960 def set_frozen(self,entry,frozen):
962 entry.setReadOnly(True)
963 entry.setFrame(False)
965 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
966 entry.setPalette(palette)
968 entry.setReadOnly(False)
971 palette.setColor(entry.backgroundRole(), QColor('white'))
972 entry.setPalette(palette)
975 def toggle_freeze(self,addr):
977 if addr in self.wallet.frozen_addresses:
978 self.wallet.unfreeze(addr)
980 self.wallet.freeze(addr)
981 self.update_receive_tab()
983 def toggle_priority(self,addr):
985 if addr in self.wallet.prioritized_addresses:
986 self.wallet.unprioritize(addr)
988 self.wallet.prioritize(addr)
989 self.update_receive_tab()
992 def create_list_tab(self, headers):
993 "generic tab creation method"
994 l = MyTreeWidget(self)
995 l.setColumnCount( len(headers) )
996 l.setHeaderLabels( headers )
1006 vbox.addWidget(buttons)
1008 hbox = QHBoxLayout()
1011 buttons.setLayout(hbox)
1016 def create_receive_tab(self):
1017 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1018 l.setContextMenuPolicy(Qt.CustomContextMenu)
1019 l.customContextMenuRequested.connect(self.create_receive_menu)
1020 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1021 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1022 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1023 self.receive_list = l
1024 self.receive_buttons_hbox = hbox
1029 def receive_tab_set_mode(self, i):
1030 self.save_column_widths()
1031 self.expert_mode = (i == 1)
1032 self.config.set_key('classic_expert_mode', self.expert_mode, True)
1033 self.update_receive_tab()
1036 def save_column_widths(self):
1037 if not self.expert_mode:
1038 widths = [ self.receive_list.columnWidth(0) ]
1041 for i in range(self.receive_list.columnCount() -1):
1042 widths.append(self.receive_list.columnWidth(i))
1043 self.column_widths["receive"][self.expert_mode] = widths
1045 self.column_widths["history"] = []
1046 for i in range(self.history_list.columnCount() - 1):
1047 self.column_widths["history"].append(self.history_list.columnWidth(i))
1049 self.column_widths["contacts"] = []
1050 for i in range(self.contacts_list.columnCount() - 1):
1051 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1053 self.config.set_key("column_widths", self.column_widths, True)
1056 def create_contacts_tab(self):
1057 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1058 l.setContextMenuPolicy(Qt.CustomContextMenu)
1059 l.customContextMenuRequested.connect(self.create_contact_menu)
1060 for i,width in enumerate(self.column_widths['contacts']):
1061 l.setColumnWidth(i, width)
1063 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1064 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1065 self.contacts_list = l
1066 self.contacts_buttons_hbox = hbox
1071 def delete_imported_key(self, addr):
1072 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1073 self.wallet.delete_imported_key(addr)
1074 self.update_receive_tab()
1075 self.update_history_tab()
1077 def edit_account_label(self, k):
1078 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':')
1080 label = unicode(text)
1081 self.wallet.set_label(k,label)
1082 self.update_receive_tab()
1084 def create_account_menu(self, position, k, item):
1086 if item.isExpanded():
1087 menu.addAction(_("Minimize"), lambda: item.setExpanded(False))
1089 menu.addAction(_("Maximize"), lambda: item.setExpanded(True))
1090 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1091 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1092 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1094 def create_receive_menu(self, position):
1095 # fixme: this function apparently has a side effect.
1096 # if it is not called the menu pops up several times
1097 #self.receive_list.selectedIndexes()
1099 item = self.receive_list.itemAt(position)
1102 addr = unicode(item.text(0))
1103 if not is_valid(addr):
1104 k = str(item.data(0,32).toString())
1106 self.create_account_menu(position, k, item)
1108 item.setExpanded(not item.isExpanded())
1112 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1113 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1114 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1115 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1116 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1117 if addr in self.wallet.imported_keys:
1118 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1120 if self.expert_mode:
1121 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1122 menu.addAction(t, lambda: self.toggle_freeze(addr))
1123 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1124 menu.addAction(t, lambda: self.toggle_priority(addr))
1126 run_hook('receive_menu', menu)
1127 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1130 def payto(self, addr):
1132 label = self.wallet.labels.get(addr)
1133 m_addr = label + ' <' + addr + '>' if label else addr
1134 self.tabs.setCurrentIndex(1)
1135 self.payto_e.setText(m_addr)
1136 self.amount_e.setFocus()
1139 def delete_contact(self, x):
1140 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1141 self.wallet.delete_contact(x)
1142 self.wallet.set_label(x, None)
1143 self.update_history_tab()
1144 self.update_contacts_tab()
1145 self.update_completions()
1148 def create_contact_menu(self, position):
1149 item = self.contacts_list.itemAt(position)
1151 addr = unicode(item.text(0))
1152 label = unicode(item.text(1))
1153 is_editable = item.data(0,32).toBool()
1154 payto_addr = item.data(0,33).toString()
1156 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1157 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1158 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1160 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1161 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1163 run_hook('create_contact_menu', menu, item)
1164 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1167 def update_receive_item(self, item):
1168 item.setFont(0, QFont(MONOSPACE_FONT))
1169 address = str(item.data(0,0).toString())
1170 label = self.wallet.labels.get(address,'')
1171 item.setData(1,0,label)
1172 item.setData(0,32, True) # is editable
1174 run_hook('update_receive_item', address, item)
1176 c, u = self.wallet.get_addr_balance(address)
1177 balance = self.format_amount(c + u)
1178 item.setData(2,0,balance)
1180 if self.expert_mode:
1181 if address in self.wallet.frozen_addresses:
1182 item.setBackgroundColor(0, QColor('lightblue'))
1183 elif address in self.wallet.prioritized_addresses:
1184 item.setBackgroundColor(0, QColor('lightgreen'))
1187 def update_receive_tab(self):
1188 l = self.receive_list
1191 l.setColumnHidden(2, not self.expert_mode)
1192 l.setColumnHidden(3, not self.expert_mode)
1193 for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1194 l.setColumnWidth(i, width)
1196 if self.current_account is None:
1197 account_items = self.wallet.accounts.items()
1198 elif self.current_account != -1:
1199 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1203 for k, account in account_items:
1204 name = self.wallet.get_account_name(k)
1205 c,u = self.wallet.get_account_balance(k)
1206 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1207 l.addTopLevelItem(account_item)
1208 account_item.setExpanded(True)
1209 account_item.setData(0, 32, k)
1211 if not self.wallet.is_seeded(k):
1212 icon = QIcon(":icons/key.png")
1213 account_item.setIcon(0, icon)
1215 for is_change in ([0,1] if self.expert_mode else [0]):
1216 if self.expert_mode:
1217 name = _("Receiving") if not is_change else _("Change")
1218 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1219 account_item.addChild(seq_item)
1220 if not is_change: seq_item.setExpanded(True)
1222 seq_item = account_item
1226 for address in account.get_addresses(is_change):
1227 h = self.wallet.history.get(address,[])
1231 if gap > self.wallet.gap_limit:
1236 num_tx = '*' if h == ['*'] else "%d"%len(h)
1237 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1238 self.update_receive_item(item)
1240 item.setBackgroundColor(1, QColor('red'))
1241 seq_item.addChild(item)
1244 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1245 c,u = self.wallet.get_imported_balance()
1246 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1247 l.addTopLevelItem(account_item)
1248 account_item.setExpanded(True)
1249 for address in self.wallet.imported_keys.keys():
1250 item = QTreeWidgetItem( [ address, '', '', ''] )
1251 self.update_receive_item(item)
1252 account_item.addChild(item)
1255 # we use column 1 because column 0 may be hidden
1256 l.setCurrentItem(l.topLevelItem(0),1)
1259 def update_contacts_tab(self):
1260 l = self.contacts_list
1263 for address in self.wallet.addressbook:
1264 label = self.wallet.labels.get(address,'')
1265 n = self.wallet.get_num_tx(address)
1266 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1267 item.setFont(0, QFont(MONOSPACE_FONT))
1268 # 32 = label can be edited (bool)
1269 item.setData(0,32, True)
1271 item.setData(0,33, address)
1272 l.addTopLevelItem(item)
1274 run_hook('update_contacts_tab', l)
1275 l.setCurrentItem(l.topLevelItem(0))
1279 def create_console_tab(self):
1280 from console import Console
1281 self.console = console = Console()
1285 def update_console(self):
1286 console = self.console
1287 console.history = self.config.get("console-history",[])
1288 console.history_index = len(console.history)
1290 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1291 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1293 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1295 def mkfunc(f, method):
1296 return lambda *args: apply( f, (method, args, self.password_dialog ))
1298 if m[0]=='_' or m=='wallet' or m == 'interface': continue
1299 methods[m] = mkfunc(c._run, m)
1301 console.updateNamespace(methods)
1304 def change_account(self,s):
1305 if s == _("All accounts"):
1306 self.current_account = None
1308 accounts = self.wallet.get_account_names()
1309 for k, v in accounts.items():
1311 self.current_account = k
1312 self.update_history_tab()
1313 self.update_status()
1314 self.update_receive_tab()
1316 def create_status_bar(self):
1319 sb.setFixedHeight(35)
1320 qtVersion = qVersion()
1322 self.balance_label = QLabel("")
1323 sb.addWidget(self.balance_label)
1325 from version_getter import UpdateLabel
1326 self.updatelabel = UpdateLabel(self.config, sb)
1328 self.account_selector = QComboBox()
1329 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1330 sb.addPermanentWidget(self.account_selector)
1332 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1333 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1335 self.lock_icon = QIcon()
1336 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1337 sb.addPermanentWidget( self.password_button )
1339 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1340 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1341 sb.addPermanentWidget( self.seed_button )
1342 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1343 sb.addPermanentWidget( self.status_button )
1345 run_hook('create_status_bar', (sb,))
1347 self.setStatusBar(sb)
1350 def update_lock_icon(self):
1351 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1352 self.password_button.setIcon( icon )
1355 def update_buttons_on_seed(self):
1356 if not self.wallet.is_watching_only():
1357 self.seed_button.show()
1358 self.password_button.show()
1359 self.send_button.setText(_("Send"))
1361 self.password_button.hide()
1362 self.seed_button.hide()
1363 self.send_button.setText(_("Create unsigned transaction"))
1366 def change_password_dialog(self):
1367 from password_dialog import PasswordDialog
1368 d = PasswordDialog(self.wallet, self)
1370 self.update_lock_icon()
1373 def new_contact_dialog(self):
1374 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1375 address = unicode(text)
1377 if is_valid(address):
1378 self.wallet.add_contact(address)
1379 self.update_contacts_tab()
1380 self.update_history_tab()
1381 self.update_completions()
1383 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1386 def new_account_dialog(self):
1388 dialog = QDialog(self)
1390 dialog.setWindowTitle(_("New Account"))
1392 addr = self.wallet.new_account_address()
1393 vbox = QVBoxLayout()
1394 msg = _("Electrum considers that an account exists only if it contains bitcoins.") + '\n' \
1395 + _("To create a new account, please send coins to the first address of that account.") + '\n' \
1396 + _("Note: you will need to wait for 2 confirmations before the account is created.")
1397 vbox.addWidget(QLabel(msg))
1398 vbox.addWidget(QLabel(_('Address')+':'))
1403 vbox.addLayout(ok_cancel_buttons(dialog))
1404 dialog.setLayout(vbox)
1411 def show_master_public_key_old(self):
1412 dialog = QDialog(self)
1414 dialog.setWindowTitle(_("Master Public Key"))
1416 main_text = QTextEdit()
1417 main_text.setText(self.wallet.get_master_public_key())
1418 main_text.setReadOnly(True)
1419 main_text.setMaximumHeight(170)
1420 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1422 ok_button = QPushButton(_("OK"))
1423 ok_button.setDefault(True)
1424 ok_button.clicked.connect(dialog.accept)
1426 main_layout = QGridLayout()
1427 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1429 main_layout.addWidget(main_text, 1, 0)
1430 main_layout.addWidget(qrw, 1, 1 )
1432 vbox.addLayout(close_button(dialog))
1433 dialog.setLayout(vbox)
1437 def show_master_public_key(self):
1439 if self.wallet.seed_version == 4:
1440 self.show_master_public_keys_old()
1443 dialog = QDialog(self)
1445 dialog.setWindowTitle(_("Master Public Keys"))
1447 chain_text = QTextEdit()
1448 chain_text.setReadOnly(True)
1449 chain_text.setMaximumHeight(170)
1450 chain_qrw = QRCodeWidget()
1452 mpk_text = QTextEdit()
1453 mpk_text.setReadOnly(True)
1454 mpk_text.setMaximumHeight(170)
1455 mpk_qrw = QRCodeWidget()
1457 main_layout = QGridLayout()
1459 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1460 main_layout.addWidget(mpk_text, 1, 1)
1461 main_layout.addWidget(mpk_qrw, 1, 2)
1463 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1464 main_layout.addWidget(chain_text, 2, 1)
1465 main_layout.addWidget(chain_qrw, 2, 2)
1468 c, K, cK = self.wallet.master_public_keys[str(key)]
1469 chain_text.setText(c)
1470 chain_qrw.set_addr(c)
1471 chain_qrw.update_qr()
1476 key_selector = QComboBox()
1477 keys = sorted(self.wallet.master_public_keys.keys())
1478 key_selector.addItems(keys)
1480 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1481 main_layout.addWidget(key_selector, 0, 1)
1482 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1486 vbox = QVBoxLayout()
1487 vbox.addLayout(main_layout)
1488 vbox.addLayout(close_button(dialog))
1490 dialog.setLayout(vbox)
1495 def show_seed_dialog(self, password):
1496 if self.wallet.is_watching_only():
1497 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1500 if self.wallet.seed:
1502 seed = self.wallet.decode_seed(password)
1504 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1506 from seed_dialog import SeedDialog
1507 d = SeedDialog(self)
1508 d.show_seed(seed, self.wallet.imported_keys)
1511 for k in self.wallet.master_private_keys.keys():
1512 pk = self.wallet.get_master_private_key(k, password)
1514 from seed_dialog import PrivateKeysDialog
1515 d = PrivateKeysDialog(self,l)
1522 def show_qrcode(self, data, title = _("QR code")):
1526 d.setWindowTitle(title)
1527 d.setMinimumSize(270, 300)
1528 vbox = QVBoxLayout()
1529 qrw = QRCodeWidget(data)
1530 vbox.addWidget(qrw, 1)
1531 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1532 hbox = QHBoxLayout()
1536 filename = "qrcode.bmp"
1537 bmp.save_qrcode(qrw.qr, filename)
1538 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1540 b = QPushButton(_("Save"))
1542 b.clicked.connect(print_qr)
1544 b = QPushButton(_("Close"))
1546 b.clicked.connect(d.accept)
1549 vbox.addLayout(hbox)
1554 def do_protect(self, func, args):
1555 if self.wallet.use_encryption:
1556 password = self.password_dialog()
1562 if args != (False,):
1563 args = (self,) + args + (password,)
1565 args = (self,password)
1570 def show_private_key(self, address, password):
1571 if not address: return
1573 pk_list = self.wallet.get_private_key(address, password)
1574 except BaseException, e:
1575 self.show_message(str(e))
1577 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1581 def do_sign(self, address, message, signature, password):
1582 message = unicode(message.toPlainText())
1583 message = message.encode('utf-8')
1585 sig = self.wallet.sign_message(str(address.text()), message, password)
1586 signature.setText(sig)
1587 except BaseException, e:
1588 self.show_message(str(e))
1590 def sign_message(self, address):
1591 if not address: return
1594 d.setWindowTitle(_('Sign Message'))
1595 d.setMinimumSize(410, 290)
1597 tab_widget = QTabWidget()
1599 layout = QGridLayout(tab)
1601 sign_address = QLineEdit()
1603 sign_address.setText(address)
1604 layout.addWidget(QLabel(_('Address')), 1, 0)
1605 layout.addWidget(sign_address, 1, 1)
1607 sign_message = QTextEdit()
1608 layout.addWidget(QLabel(_('Message')), 2, 0)
1609 layout.addWidget(sign_message, 2, 1)
1610 layout.setRowStretch(2,3)
1612 sign_signature = QTextEdit()
1613 layout.addWidget(QLabel(_('Signature')), 3, 0)
1614 layout.addWidget(sign_signature, 3, 1)
1615 layout.setRowStretch(3,1)
1618 hbox = QHBoxLayout()
1619 b = QPushButton(_("Sign"))
1621 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1622 b = QPushButton(_("Close"))
1623 b.clicked.connect(d.accept)
1625 layout.addLayout(hbox, 4, 1)
1626 tab_widget.addTab(tab, _("Sign"))
1630 layout = QGridLayout(tab)
1632 verify_address = QLineEdit()
1633 layout.addWidget(QLabel(_('Address')), 1, 0)
1634 layout.addWidget(verify_address, 1, 1)
1636 verify_message = QTextEdit()
1637 layout.addWidget(QLabel(_('Message')), 2, 0)
1638 layout.addWidget(verify_message, 2, 1)
1639 layout.setRowStretch(2,3)
1641 verify_signature = QTextEdit()
1642 layout.addWidget(QLabel(_('Signature')), 3, 0)
1643 layout.addWidget(verify_signature, 3, 1)
1644 layout.setRowStretch(3,1)
1647 message = unicode(verify_message.toPlainText())
1648 message = message.encode('utf-8')
1649 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1650 self.show_message(_("Signature verified"))
1652 self.show_message(_("Error: wrong signature"))
1654 hbox = QHBoxLayout()
1655 b = QPushButton(_("Verify"))
1656 b.clicked.connect(do_verify)
1658 b = QPushButton(_("Close"))
1659 b.clicked.connect(d.accept)
1661 layout.addLayout(hbox, 4, 1)
1662 tab_widget.addTab(tab, _("Verify"))
1664 vbox = QVBoxLayout()
1665 vbox.addWidget(tab_widget)
1672 def question(self, msg):
1673 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1675 def show_message(self, msg):
1676 QMessageBox.information(self, _('Message'), msg, _('OK'))
1678 def password_dialog(self ):
1685 vbox = QVBoxLayout()
1686 msg = _('Please enter your password')
1687 vbox.addWidget(QLabel(msg))
1689 grid = QGridLayout()
1691 grid.addWidget(QLabel(_('Password')), 1, 0)
1692 grid.addWidget(pw, 1, 1)
1693 vbox.addLayout(grid)
1695 vbox.addLayout(ok_cancel_buttons(d))
1698 run_hook('password_dialog', pw, grid, 1)
1699 if not d.exec_(): return
1700 return unicode(pw.text())
1709 def tx_from_text(self, txt):
1710 "json or raw hexadecimal"
1713 tx = Transaction(txt)
1719 tx_dict = json.loads(str(txt))
1720 assert "hex" in tx_dict.keys()
1721 assert "complete" in tx_dict.keys()
1722 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1723 if not tx_dict["complete"]:
1724 assert "input_info" in tx_dict.keys()
1725 input_info = json.loads(tx_dict['input_info'])
1726 tx.add_input_info(input_info)
1731 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1735 def read_tx_from_file(self):
1736 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1740 with open(fileName, "r") as f:
1741 file_content = f.read()
1742 except (ValueError, IOError, os.error), reason:
1743 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1745 return self.tx_from_text(file_content)
1749 def sign_raw_transaction(self, tx, input_info, password):
1750 self.wallet.signrawtransaction(tx, input_info, [], password)
1752 def do_process_from_text(self):
1753 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1756 tx = self.tx_from_text(text)
1758 self.show_transaction(tx)
1760 def do_process_from_file(self):
1761 tx = self.read_tx_from_file()
1763 self.show_transaction(tx)
1765 def do_process_from_csvReader(self, csvReader):
1768 for row in csvReader:
1770 amount = float(row[1])
1771 amount = int(100000000*amount)
1772 outputs.append((address, amount))
1773 except (ValueError, IOError, os.error), reason:
1774 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1778 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1779 except BaseException, e:
1780 self.show_message(str(e))
1783 self.show_transaction(tx)
1785 def do_process_from_csv_file(self):
1786 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1790 with open(fileName, "r") as f:
1791 csvReader = csv.reader(f)
1792 self.do_process_from_csvReader(csvReader)
1793 except (ValueError, IOError, os.error), reason:
1794 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1797 def do_process_from_csv_text(self):
1798 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1799 + _("Format: address, amount. One output per line"), _("Load CSV"))
1802 f = StringIO.StringIO(text)
1803 csvReader = csv.reader(f)
1804 self.do_process_from_csvReader(csvReader)
1809 def do_export_privkeys(self, password):
1810 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.")))
1813 select_export = _('Select file to export your private keys to')
1814 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1816 with open(fileName, "w+") as csvfile:
1817 transaction = csv.writer(csvfile)
1818 transaction.writerow(["address", "private_key"])
1820 addresses = self.wallet.addresses(True)
1822 for addr in addresses:
1823 pk = "".join(self.wallet.get_private_key(addr, password))
1824 transaction.writerow(["%34s"%addr,pk])
1826 self.show_message(_("Private keys exported."))
1828 except (IOError, os.error), reason:
1829 export_error_label = _("Electrum was unable to produce a private key-export.")
1830 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1832 except BaseException, e:
1833 self.show_message(str(e))
1837 def do_import_labels(self):
1838 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1839 if not labelsFile: return
1841 f = open(labelsFile, 'r')
1844 for key, value in json.loads(data).items():
1845 self.wallet.set_label(key, value)
1846 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1847 except (IOError, os.error), reason:
1848 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1851 def do_export_labels(self):
1852 labels = self.wallet.labels
1854 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1856 with open(fileName, 'w+') as f:
1857 json.dump(labels, f)
1858 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1859 except (IOError, os.error), reason:
1860 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1863 def do_export_history(self):
1864 from lite_window import csv_transaction
1865 csv_transaction(self.wallet)
1869 def do_import_privkey(self, password):
1870 if not self.wallet.imported_keys:
1871 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1872 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1873 + _('Are you sure you understand what you are doing?'), 3, 4)
1876 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1879 text = str(text).split()
1884 addr = self.wallet.import_key(key, password)
1885 except BaseException as e:
1891 addrlist.append(addr)
1893 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1895 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1896 self.update_receive_tab()
1897 self.update_history_tab()
1900 def settings_dialog(self):
1902 d.setWindowTitle(_('Electrum Settings'))
1904 vbox = QVBoxLayout()
1906 tabs = QTabWidget(self)
1907 self.settings_tab = tabs
1908 vbox.addWidget(tabs)
1911 grid_ui = QGridLayout(tab1)
1912 grid_ui.setColumnStretch(0,1)
1913 tabs.addTab(tab1, _('Display') )
1915 nz_label = QLabel(_('Display zeros'))
1916 grid_ui.addWidget(nz_label, 0, 0)
1917 nz_e = AmountEdit(None,True)
1918 nz_e.setText("%d"% self.num_zeros)
1919 grid_ui.addWidget(nz_e, 0, 1)
1920 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1921 grid_ui.addWidget(HelpButton(msg), 0, 2)
1922 if not self.config.is_modifiable('num_zeros'):
1923 for w in [nz_e, nz_label]: w.setEnabled(False)
1925 lang_label=QLabel(_('Language') + ':')
1926 grid_ui.addWidget(lang_label, 1, 0)
1927 lang_combo = QComboBox()
1928 from electrum.i18n import languages
1929 lang_combo.addItems(languages.values())
1931 index = languages.keys().index(self.config.get("language",''))
1934 lang_combo.setCurrentIndex(index)
1935 grid_ui.addWidget(lang_combo, 1, 1)
1936 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1937 if not self.config.is_modifiable('language'):
1938 for w in [lang_combo, lang_label]: w.setEnabled(False)
1940 expert_cb = QCheckBox(_('Expert mode'))
1941 expert_cb.setChecked(self.expert_mode)
1942 grid_ui.addWidget(expert_cb, 3, 0)
1943 hh = _('In expert mode, your client will:') + '\n' \
1944 + _(' - Show change addresses in the Receive tab') + '\n' \
1945 + _(' - Display the balance of each address') + '\n' \
1946 + _(' - Add freeze/prioritize actions to addresses.')
1947 grid_ui.addWidget(HelpButton(hh), 3, 2)
1948 grid_ui.setRowStretch(4,1)
1952 grid_wallet = QGridLayout(tab2)
1953 grid_wallet.setColumnStretch(0,1)
1954 tabs.addTab(tab2, _('Wallet') )
1956 fee_label = QLabel(_('Transaction fee'))
1957 grid_wallet.addWidget(fee_label, 0, 0)
1958 fee_e = AmountEdit(self.base_unit)
1959 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1960 grid_wallet.addWidget(fee_e, 0, 2)
1961 msg = _('Fee per kilobyte of transaction.') + ' ' \
1962 + _('Recommended value') + ': ' + self.format_amount(50000)
1963 grid_wallet.addWidget(HelpButton(msg), 0, 3)
1964 if not self.config.is_modifiable('fee_per_kb'):
1965 for w in [fee_e, fee_label]: w.setEnabled(False)
1967 usechange_cb = QCheckBox(_('Use change addresses'))
1968 usechange_cb.setChecked(self.wallet.use_change)
1969 grid_wallet.addWidget(usechange_cb, 1, 0)
1970 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1971 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1973 units = ['BTC', 'mBTC']
1974 unit_label = QLabel(_('Base unit'))
1975 grid_wallet.addWidget(unit_label, 3, 0)
1976 unit_combo = QComboBox()
1977 unit_combo.addItems(units)
1978 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1979 grid_wallet.addWidget(unit_combo, 3, 2)
1980 grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1981 + '\n1BTC=1000mBTC.\n' \
1982 + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1983 grid_wallet.setRowStretch(4,1)
1986 run_hook('create_settings_tab', tabs)
1988 vbox.addLayout(ok_cancel_buttons(d))
1992 if not d.exec_(): return
1994 fee = unicode(fee_e.text())
1996 fee = self.read_amount(fee)
1998 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2001 self.wallet.set_fee(fee)
2003 nz = unicode(nz_e.text())
2008 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2011 if self.num_zeros != nz:
2013 self.config.set_key('num_zeros', nz, True)
2014 self.update_history_tab()
2015 self.update_receive_tab()
2017 usechange_result = usechange_cb.isChecked()
2018 if self.wallet.use_change != usechange_result:
2019 self.wallet.use_change = usechange_result
2020 self.config.set_key('use_change', self.wallet.use_change, True)
2022 unit_result = units[unit_combo.currentIndex()]
2023 if self.base_unit() != unit_result:
2024 self.decimal_point = 8 if unit_result == 'BTC' else 5
2025 self.config.set_key('decimal_point', self.decimal_point, True)
2026 self.update_history_tab()
2027 self.update_status()
2029 need_restart = False
2031 lang_request = languages.keys()[lang_combo.currentIndex()]
2032 if lang_request != self.config.get('language'):
2033 self.config.set_key("language", lang_request, True)
2037 run_hook('close_settings_dialog')
2040 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2042 self.receive_tab_set_mode(expert_cb.isChecked())
2044 def run_network_dialog(self):
2045 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2047 def closeEvent(self, event):
2049 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2050 self.save_column_widths()
2051 self.config.set_key("console-history", self.console.history[-50:], True)
2056 def plugins_dialog(self):
2057 from electrum.plugins import plugins
2060 d.setWindowTitle(_('Electrum Plugins'))
2063 vbox = QVBoxLayout(d)
2066 scroll = QScrollArea()
2067 scroll.setEnabled(True)
2068 scroll.setWidgetResizable(True)
2069 scroll.setMinimumSize(400,250)
2070 vbox.addWidget(scroll)
2074 w.setMinimumHeight(len(plugins)*35)
2076 grid = QGridLayout()
2077 grid.setColumnStretch(0,1)
2080 def mk_toggle(cb, p):
2081 return lambda: cb.setChecked(p.toggle())
2082 for i, p in enumerate(plugins):
2084 cb = QCheckBox(p.fullname())
2085 cb.setDisabled(not p.is_available())
2086 cb.setChecked(p.is_enabled())
2087 cb.clicked.connect(mk_toggle(cb,p))
2088 grid.addWidget(cb, i, 0)
2089 if p.requires_settings():
2090 b = EnterButton(_('Settings'), p.settings_dialog)
2091 b.setEnabled( p.is_enabled() )
2092 grid.addWidget(b, i, 1)
2093 grid.addWidget(HelpButton(p.description()), i, 2)
2095 print_msg(_("Error: cannot display plugin"), p)
2096 traceback.print_exc(file=sys.stdout)
2097 grid.setRowStretch(i+1,1)
2099 vbox.addLayout(close_button(d))
2104 def show_account_details(self, k):
2106 d.setWindowTitle(_('Account Details'))
2109 vbox = QVBoxLayout(d)
2110 roots = self.wallet.get_roots(k)
2112 name = self.wallet.get_account_name(k)
2113 label = QLabel('Name: ' + name)
2114 vbox.addWidget(label)
2116 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2117 vbox.addWidget(QLabel('Type: ' + acctype))
2119 label = QLabel('Derivation: ' + k)
2120 vbox.addWidget(label)
2123 # mpk = self.wallet.master_public_keys[root]
2124 # text = QTextEdit()
2125 # text.setReadOnly(True)
2126 # text.setMaximumHeight(120)
2127 # text.setText(repr(mpk))
2128 # vbox.addWidget(text)
2130 vbox.addLayout(close_button(d))