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.decimal_point = config.get('decimal_point', 8)
153 self.num_zeros = int(config.get('num_zeros',0))
155 set_language(config.get('language'))
157 self.funds_error = False
158 self.completions = QStringListModel()
160 self.tabs = tabs = QTabWidget(self)
161 self.column_widths = self.config.get("column_widths", default_column_widths )
162 tabs.addTab(self.create_history_tab(), _('History') )
163 tabs.addTab(self.create_send_tab(), _('Send') )
164 tabs.addTab(self.create_receive_tab(), _('Receive') )
165 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
166 tabs.addTab(self.create_console_tab(), _('Console') )
167 tabs.setMinimumSize(600, 400)
168 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
169 self.setCentralWidget(tabs)
171 g = self.config.get("winpos-qt",[100, 100, 840, 400])
172 self.setGeometry(g[0], g[1], g[2], g[3])
176 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
177 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
178 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
179 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
180 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
182 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
183 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
184 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
186 self.history_list.setFocus(True)
189 self.network.register_callback('updated', lambda: self.need_update.set())
190 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
191 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
192 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
193 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
194 # set initial message
195 self.console.showMessage(self.network.banner)
197 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
198 if platform.system() == 'Windows':
199 n = 3 if self.wallet.seed else 2
200 tabs.setCurrentIndex (n)
201 tabs.setCurrentIndex (0)
208 self.config.set_key('lite_mode', False, True)
213 self.config.set_key('lite_mode', True, True)
220 if not self.check_qt_version():
221 if self.config.get('lite_mode') is True:
222 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
223 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
224 self.config.set_key('lite_mode', False, True)
229 actuator = lite_window.MiniActuator(self)
231 # Should probably not modify the current path but instead
232 # change the behaviour of rsrc(...)
233 old_path = QDir.currentPath()
234 actuator.load_theme()
236 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
238 driver = lite_window.MiniDriver(self, self.mini)
240 # Reset path back to original value now that loading the GUI
242 QDir.setCurrent(old_path)
244 if self.config.get('lite_mode') is True:
250 def check_qt_version(self):
251 qtVersion = qVersion()
252 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
256 def load_wallet(self, wallet):
260 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
261 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
262 self.setWindowTitle( title )
264 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
265 self.notify_transactions()
268 accounts = self.wallet.get_account_names()
269 self.account_selector.clear()
270 if len(accounts) > 1:
271 self.account_selector.addItems([_("All accounts")] + accounts.values())
272 self.account_selector.setCurrentIndex(0)
273 self.account_selector.show()
275 self.account_selector.hide()
277 self.new_account.setEnabled(self.wallet.seed_version>4)
279 self.update_lock_icon()
280 self.update_buttons_on_seed()
281 self.update_console()
283 run_hook('load_wallet', wallet)
286 def select_wallet_file(self):
287 wallet_folder = self.wallet.storage.path
288 re.sub("(\/\w*.dat)$", "", wallet_folder)
289 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
293 def open_wallet(self):
295 filename = self.select_wallet_file()
299 storage = WalletStorage({'wallet_path': filename})
300 if not storage.file_exists:
301 self.show_message("file not found "+ filename)
304 self.wallet.stop_threads()
307 wallet = Wallet(storage)
308 wallet.start_threads(self.network)
310 self.load_wallet(wallet)
314 def backup_wallet(self):
316 path = self.wallet.storage.path
317 wallet_folder = os.path.dirname(path)
318 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
319 new_filename = unicode(new_filename)
320 if not ok or not new_filename:
323 new_path = os.path.join(wallet_folder, new_filename)
326 shutil.copy2(path, new_path)
327 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
328 except (IOError, os.error), reason:
329 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
332 def new_wallet(self):
335 wallet_folder = os.path.dirname(self.wallet.storage.path)
336 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
337 filename = unicode(filename)
338 if not ok or not filename:
340 filename = os.path.join(wallet_folder, filename)
342 storage = WalletStorage({'wallet_path': filename})
343 assert not storage.file_exists
345 wizard = installwizard.InstallWizard(self.config, self.network, storage)
346 wallet = wizard.run()
348 self.load_wallet(wallet)
352 def init_menubar(self):
355 file_menu = menubar.addMenu(_("&File"))
356 open_wallet_action = file_menu.addAction(_("&Open"))
357 open_wallet_action.triggered.connect(self.open_wallet)
359 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
360 new_wallet_action.triggered.connect(self.new_wallet)
362 wallet_backup = file_menu.addAction(_("&Copy"))
363 wallet_backup.triggered.connect(self.backup_wallet)
365 quit_item = file_menu.addAction(_("&Close"))
366 quit_item.triggered.connect(self.close)
368 wallet_menu = menubar.addMenu(_("&Wallet"))
370 new_contact = wallet_menu.addAction(_("&New contact"))
371 new_contact.triggered.connect(self.new_contact_dialog)
373 self.new_account = wallet_menu.addAction(_("&New account"))
374 self.new_account.triggered.connect(self.new_account_dialog)
376 wallet_menu.addSeparator()
378 pw = wallet_menu.addAction(_("&Password"))
379 pw.triggered.connect(self.change_password_dialog)
381 show_seed = wallet_menu.addAction(_("&Seed"))
382 show_seed.triggered.connect(self.show_seed_dialog)
384 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
385 show_mpk.triggered.connect(self.show_master_public_key)
387 wallet_menu.addSeparator()
389 labels_menu = wallet_menu.addMenu(_("&Labels"))
390 import_labels = labels_menu.addAction(_("&Import"))
391 import_labels.triggered.connect(self.do_import_labels)
392 export_labels = labels_menu.addAction(_("&Export"))
393 export_labels.triggered.connect(self.do_export_labels)
395 keys_menu = wallet_menu.addMenu(_("&Private keys"))
396 import_keys = keys_menu.addAction(_("&Import"))
397 import_keys.triggered.connect(self.do_import_privkey)
398 export_keys = keys_menu.addAction(_("&Export"))
399 export_keys.triggered.connect(self.do_export_privkeys)
401 ex_history = wallet_menu.addAction(_("&Export History"))
402 ex_history.triggered.connect(self.do_export_history)
406 tools_menu = menubar.addMenu(_("&Tools"))
408 # Settings / Preferences are all reserved keywords in OSX using this as work around
409 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
410 preferences_menu = tools_menu.addAction(preferences_name)
411 preferences_menu.triggered.connect(self.settings_dialog)
413 network = tools_menu.addAction(_("&Network"))
414 network.triggered.connect(self.run_network_dialog)
416 plugins_labels = tools_menu.addAction(_("&Plugins"))
417 plugins_labels.triggered.connect(self.plugins_dialog)
419 tools_menu.addSeparator()
421 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
423 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
424 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
426 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
427 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
429 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
431 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
432 raw_transaction_file.triggered.connect(self.do_process_from_file)
434 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
435 raw_transaction_text.triggered.connect(self.do_process_from_text)
438 help_menu = menubar.addMenu(_("&Help"))
439 show_about = help_menu.addAction(_("&About"))
440 show_about.triggered.connect(self.show_about)
441 web_open = help_menu.addAction(_("&Official website"))
442 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
444 help_menu.addSeparator()
445 doc_open = help_menu.addAction(_("&Documentation"))
446 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
447 report_bug = help_menu.addAction(_("&Report Bug"))
448 report_bug.triggered.connect(self.show_report_bug)
450 self.setMenuBar(menubar)
452 def show_about(self):
453 QMessageBox.about(self, "Electrum",
454 _("Version")+" %s" % (self.wallet.electrum_version) + "\n\n" + _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin. You do not need to perform regular backups, because your wallet can be recovered from a secret phrase that you can memorize or write on paper. Startup times are instant because it operates in conjunction with high-performance servers that handle the most complicated parts of the Bitcoin system."))
456 def show_report_bug(self):
457 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
458 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
461 def notify_transactions(self):
462 print_error("Notifying GUI")
463 if len(self.network.interface.pending_transactions_for_notifications) > 0:
464 # Combine the transactions if there are more then three
465 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
468 for tx in self.network.interface.pending_transactions_for_notifications:
469 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
473 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
474 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
476 self.network.interface.pending_transactions_for_notifications = []
478 for tx in self.network.interface.pending_transactions_for_notifications:
480 self.network.interface.pending_transactions_for_notifications.remove(tx)
481 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
483 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
485 def notify(self, message):
486 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
490 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
491 def getOpenFileName(self, title, filter = ""):
492 directory = self.config.get('io_dir', os.path.expanduser('~'))
493 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
494 if fileName and directory != os.path.dirname(fileName):
495 self.config.set_key('io_dir', os.path.dirname(fileName), True)
498 def getSaveFileName(self, title, filename, filter = ""):
499 directory = self.config.get('io_dir', os.path.expanduser('~'))
500 path = os.path.join( directory, filename )
501 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
502 if fileName and directory != os.path.dirname(fileName):
503 self.config.set_key('io_dir', os.path.dirname(fileName), True)
507 QMainWindow.close(self)
508 run_hook('close_main_window')
510 def connect_slots(self, sender):
511 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
512 self.previous_payto_e=''
514 def timer_actions(self):
515 if self.need_update.is_set():
517 self.need_update.clear()
518 run_hook('timer_actions')
520 def format_amount(self, x, is_diff=False, whitespaces=False):
521 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
523 def read_amount(self, x):
524 if x in['.', '']: return None
525 p = pow(10, self.decimal_point)
526 return int( p * Decimal(x) )
529 assert self.decimal_point in [5,8]
530 return "BTC" if self.decimal_point == 8 else "mBTC"
533 def update_status(self):
534 if self.network.interface and self.network.interface.is_connected:
535 if not self.wallet.up_to_date:
536 text = _("Synchronizing...")
537 icon = QIcon(":icons/status_waiting.png")
538 elif self.network.is_lagging:
539 text = _("Server is lagging")
540 icon = QIcon(":icons/status_lagging.png")
542 c, u = self.wallet.get_account_balance(self.current_account)
543 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
544 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
547 run_hook('set_quote_text', c+u, r)
550 text += " (%s)"%quote
552 self.tray.setToolTip(text)
553 icon = QIcon(":icons/status_connected.png")
555 text = _("Not connected")
556 icon = QIcon(":icons/status_disconnected.png")
558 self.balance_label.setText(text)
559 self.status_button.setIcon( icon )
562 def update_wallet(self):
564 if self.wallet.up_to_date or not self.network.interface.is_connected:
565 self.update_history_tab()
566 self.update_receive_tab()
567 self.update_contacts_tab()
568 self.update_completions()
571 def create_history_tab(self):
572 self.history_list = l = MyTreeWidget(self)
574 for i,width in enumerate(self.column_widths['history']):
575 l.setColumnWidth(i, width)
576 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
577 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
578 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
580 l.customContextMenuRequested.connect(self.create_history_menu)
584 def create_history_menu(self, position):
585 self.history_list.selectedIndexes()
586 item = self.history_list.currentItem()
588 tx_hash = str(item.data(0, Qt.UserRole).toString())
589 if not tx_hash: return
591 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
592 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
593 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
594 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
597 def show_transaction(self, tx):
598 import transaction_dialog
599 d = transaction_dialog.TxDialog(tx, self)
602 def tx_label_clicked(self, item, column):
603 if column==2 and item.isSelected():
605 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
606 self.history_list.editItem( item, column )
607 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
610 def tx_label_changed(self, item, column):
614 tx_hash = str(item.data(0, Qt.UserRole).toString())
615 tx = self.wallet.transactions.get(tx_hash)
616 text = unicode( item.text(2) )
617 self.wallet.set_label(tx_hash, text)
619 item.setForeground(2, QBrush(QColor('black')))
621 text = self.wallet.get_default_label(tx_hash)
622 item.setText(2, text)
623 item.setForeground(2, QBrush(QColor('gray')))
627 def edit_label(self, is_recv):
628 l = self.receive_list if is_recv else self.contacts_list
629 item = l.currentItem()
630 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
631 l.editItem( item, 1 )
632 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
636 def address_label_clicked(self, item, column, l, column_addr, column_label):
637 if column == column_label and item.isSelected():
638 is_editable = item.data(0, 32).toBool()
641 addr = unicode( item.text(column_addr) )
642 label = unicode( item.text(column_label) )
643 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
644 l.editItem( item, column )
645 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
648 def address_label_changed(self, item, column, l, column_addr, column_label):
649 if column == column_label:
650 addr = unicode( item.text(column_addr) )
651 text = unicode( item.text(column_label) )
652 is_editable = item.data(0, 32).toBool()
656 changed = self.wallet.set_label(addr, text)
658 self.update_history_tab()
659 self.update_completions()
661 self.current_item_changed(item)
663 run_hook('item_changed', item, column)
666 def current_item_changed(self, a):
667 run_hook('current_item_changed', a)
671 def update_history_tab(self):
673 self.history_list.clear()
674 for item in self.wallet.get_tx_history(self.current_account):
675 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
678 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
680 time_str = _("unknown")
683 time_str = 'unverified'
684 icon = QIcon(":icons/unconfirmed.png")
687 icon = QIcon(":icons/unconfirmed.png")
689 icon = QIcon(":icons/clock%d.png"%conf)
691 icon = QIcon(":icons/confirmed.png")
693 if value is not None:
694 v_str = self.format_amount(value, True, whitespaces=True)
698 balance_str = self.format_amount(balance, whitespaces=True)
701 label, is_default_label = self.wallet.get_label(tx_hash)
703 label = _('Pruned transaction outputs')
704 is_default_label = False
706 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
707 item.setFont(2, QFont(MONOSPACE_FONT))
708 item.setFont(3, QFont(MONOSPACE_FONT))
709 item.setFont(4, QFont(MONOSPACE_FONT))
711 item.setForeground(3, QBrush(QColor("#BC1E1E")))
713 item.setData(0, Qt.UserRole, tx_hash)
714 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
716 item.setForeground(2, QBrush(QColor('grey')))
718 item.setIcon(0, icon)
719 self.history_list.insertTopLevelItem(0,item)
722 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
725 def create_send_tab(self):
730 grid.setColumnMinimumWidth(3,300)
731 grid.setColumnStretch(5,1)
734 self.payto_e = QLineEdit()
735 grid.addWidget(QLabel(_('Pay to')), 1, 0)
736 grid.addWidget(self.payto_e, 1, 1, 1, 3)
738 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)
740 completer = QCompleter()
741 completer.setCaseSensitivity(False)
742 self.payto_e.setCompleter(completer)
743 completer.setModel(self.completions)
745 self.message_e = QLineEdit()
746 grid.addWidget(QLabel(_('Description')), 2, 0)
747 grid.addWidget(self.message_e, 2, 1, 1, 3)
748 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)
750 self.amount_e = AmountEdit(self.base_unit)
751 grid.addWidget(QLabel(_('Amount')), 3, 0)
752 grid.addWidget(self.amount_e, 3, 1, 1, 2)
753 grid.addWidget(HelpButton(
754 _('Amount to be sent.') + '\n\n' \
755 + _('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.') \
756 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
758 self.fee_e = AmountEdit(self.base_unit)
759 grid.addWidget(QLabel(_('Fee')), 4, 0)
760 grid.addWidget(self.fee_e, 4, 1, 1, 2)
761 grid.addWidget(HelpButton(
762 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
763 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
764 + _('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)
767 self.send_button = EnterButton(_("Send"), self.do_send)
768 grid.addWidget(self.send_button, 6, 1)
770 b = EnterButton(_("Clear"),self.do_clear)
771 grid.addWidget(b, 6, 2)
773 self.payto_sig = QLabel('')
774 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
776 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
777 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
786 def entry_changed( is_fee ):
787 self.funds_error = False
789 if self.amount_e.is_shortcut:
790 self.amount_e.is_shortcut = False
791 c, u = self.wallet.get_account_balance(self.current_account)
792 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
793 fee = self.wallet.estimated_fee(inputs)
795 self.amount_e.setText( self.format_amount(amount) )
796 self.fee_e.setText( self.format_amount( fee ) )
799 amount = self.read_amount(str(self.amount_e.text()))
800 fee = self.read_amount(str(self.fee_e.text()))
802 if not is_fee: fee = None
805 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
807 self.fee_e.setText( self.format_amount( fee ) )
810 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
814 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
815 self.funds_error = True
816 text = _( "Not enough funds" )
817 c, u = self.wallet.get_frozen_balance()
818 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
820 self.statusBar().showMessage(text)
821 self.amount_e.setPalette(palette)
822 self.fee_e.setPalette(palette)
824 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
825 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
827 run_hook('create_send_tab', grid)
831 def update_completions(self):
833 for addr,label in self.wallet.labels.items():
834 if addr in self.wallet.addressbook:
835 l.append( label + ' <' + addr + '>')
837 run_hook('update_completions', l)
838 self.completions.setStringList(l)
842 return lambda s, *args: s.do_protect(func, args)
847 label = unicode( self.message_e.text() )
848 r = unicode( self.payto_e.text() )
851 # label or alias, with address in brackets
852 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
853 to_address = m.group(2) if m else r
855 if not is_valid(to_address):
856 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
860 amount = self.read_amount(unicode( self.amount_e.text()))
862 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
865 fee = self.read_amount(unicode( self.fee_e.text()))
867 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
870 confirm_amount = self.config.get('confirm_amount', 100000000)
871 if amount >= confirm_amount:
872 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
875 self.send_tx(to_address, amount, fee, label)
879 def send_tx(self, to_address, amount, fee, label, password):
882 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
883 except BaseException, e:
884 traceback.print_exc(file=sys.stdout)
885 self.show_message(str(e))
888 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
889 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
893 self.wallet.set_label(tx.hash(), label)
896 h = self.wallet.send_tx(tx)
897 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
898 status, msg = self.wallet.receive_tx( h )
900 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
902 self.update_contacts_tab()
904 QMessageBox.warning(self, _('Error'), msg, _('OK'))
906 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
908 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
909 with open(fileName,'w') as f:
910 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
911 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
913 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
915 # add recipient to addressbook
916 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
917 self.wallet.addressbook.append(to_address)
922 def set_url(self, url):
923 address, amount, label, message, signature, identity, url = util.parse_url(url)
925 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
928 self.mini.set_payment_fields(address, amount)
930 if label and self.wallet.labels.get(address) != label:
931 if self.question('Give label "%s" to address %s ?'%(label,address)):
932 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
933 self.wallet.addressbook.append(address)
934 self.wallet.set_label(address, label)
936 run_hook('set_url', url, self.show_message, self.question)
938 self.tabs.setCurrentIndex(1)
939 label = self.wallet.labels.get(address)
940 m_addr = label + ' <'+ address +'>' if label else address
941 self.payto_e.setText(m_addr)
943 self.message_e.setText(message)
945 self.amount_e.setText(amount)
948 self.set_frozen(self.payto_e,True)
949 self.set_frozen(self.amount_e,True)
950 self.set_frozen(self.message_e,True)
951 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
953 self.payto_sig.setVisible(False)
956 self.payto_sig.setVisible(False)
957 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
959 self.set_frozen(e,False)
962 def set_frozen(self,entry,frozen):
964 entry.setReadOnly(True)
965 entry.setFrame(False)
967 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
968 entry.setPalette(palette)
970 entry.setReadOnly(False)
973 palette.setColor(entry.backgroundRole(), QColor('white'))
974 entry.setPalette(palette)
977 def toggle_freeze(self,addr):
979 if addr in self.wallet.frozen_addresses:
980 self.wallet.unfreeze(addr)
982 self.wallet.freeze(addr)
983 self.update_receive_tab()
985 def toggle_priority(self,addr):
987 if addr in self.wallet.prioritized_addresses:
988 self.wallet.unprioritize(addr)
990 self.wallet.prioritize(addr)
991 self.update_receive_tab()
994 def create_list_tab(self, headers):
995 "generic tab creation method"
996 l = MyTreeWidget(self)
997 l.setColumnCount( len(headers) )
998 l.setHeaderLabels( headers )
1001 vbox = QVBoxLayout()
1008 vbox.addWidget(buttons)
1010 hbox = QHBoxLayout()
1013 buttons.setLayout(hbox)
1018 def create_receive_tab(self):
1019 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1020 l.setContextMenuPolicy(Qt.CustomContextMenu)
1021 l.customContextMenuRequested.connect(self.create_receive_menu)
1022 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1023 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1024 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1025 self.receive_list = l
1026 self.receive_buttons_hbox = hbox
1033 def save_column_widths(self):
1035 for i in range(self.receive_list.columnCount() -1):
1036 widths.append(self.receive_list.columnWidth(i))
1037 self.column_widths["receive"][1] = widths
1039 self.column_widths["history"] = []
1040 for i in range(self.history_list.columnCount() - 1):
1041 self.column_widths["history"].append(self.history_list.columnWidth(i))
1043 self.column_widths["contacts"] = []
1044 for i in range(self.contacts_list.columnCount() - 1):
1045 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1047 self.config.set_key("column_widths", self.column_widths, True)
1050 def create_contacts_tab(self):
1051 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1052 l.setContextMenuPolicy(Qt.CustomContextMenu)
1053 l.customContextMenuRequested.connect(self.create_contact_menu)
1054 for i,width in enumerate(self.column_widths['contacts']):
1055 l.setColumnWidth(i, width)
1057 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1058 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1059 self.contacts_list = l
1060 self.contacts_buttons_hbox = hbox
1065 def delete_imported_key(self, addr):
1066 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1067 self.wallet.delete_imported_key(addr)
1068 self.update_receive_tab()
1069 self.update_history_tab()
1071 def edit_account_label(self, k):
1072 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':')
1074 label = unicode(text)
1075 self.wallet.set_label(k,label)
1076 self.update_receive_tab()
1078 def create_account_menu(self, position, k, item):
1080 if item.isExpanded():
1081 menu.addAction(_("Minimize"), lambda: item.setExpanded(False))
1083 menu.addAction(_("Maximize"), lambda: item.setExpanded(True))
1084 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1085 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1086 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1088 def create_receive_menu(self, position):
1089 # fixme: this function apparently has a side effect.
1090 # if it is not called the menu pops up several times
1091 #self.receive_list.selectedIndexes()
1093 item = self.receive_list.itemAt(position)
1096 addr = unicode(item.text(0))
1097 if not is_valid(addr):
1098 k = str(item.data(0,32).toString())
1100 self.create_account_menu(position, k, item)
1102 item.setExpanded(not item.isExpanded())
1106 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1107 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1108 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1109 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1110 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1111 if addr in self.wallet.imported_keys:
1112 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1114 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1115 menu.addAction(t, lambda: self.toggle_freeze(addr))
1116 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1117 menu.addAction(t, lambda: self.toggle_priority(addr))
1119 run_hook('receive_menu', menu)
1120 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1123 def payto(self, addr):
1125 label = self.wallet.labels.get(addr)
1126 m_addr = label + ' <' + addr + '>' if label else addr
1127 self.tabs.setCurrentIndex(1)
1128 self.payto_e.setText(m_addr)
1129 self.amount_e.setFocus()
1132 def delete_contact(self, x):
1133 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1134 self.wallet.delete_contact(x)
1135 self.wallet.set_label(x, None)
1136 self.update_history_tab()
1137 self.update_contacts_tab()
1138 self.update_completions()
1141 def create_contact_menu(self, position):
1142 item = self.contacts_list.itemAt(position)
1144 addr = unicode(item.text(0))
1145 label = unicode(item.text(1))
1146 is_editable = item.data(0,32).toBool()
1147 payto_addr = item.data(0,33).toString()
1149 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1150 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1151 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1153 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1154 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1156 run_hook('create_contact_menu', menu, item)
1157 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1160 def update_receive_item(self, item):
1161 item.setFont(0, QFont(MONOSPACE_FONT))
1162 address = str(item.data(0,0).toString())
1163 label = self.wallet.labels.get(address,'')
1164 item.setData(1,0,label)
1165 item.setData(0,32, True) # is editable
1167 run_hook('update_receive_item', address, item)
1169 c, u = self.wallet.get_addr_balance(address)
1170 balance = self.format_amount(c + u)
1171 item.setData(2,0,balance)
1173 if address in self.wallet.frozen_addresses:
1174 item.setBackgroundColor(0, QColor('lightblue'))
1175 elif address in self.wallet.prioritized_addresses:
1176 item.setBackgroundColor(0, QColor('lightgreen'))
1179 def update_receive_tab(self):
1180 l = self.receive_list
1183 l.setColumnHidden(2, False)
1184 l.setColumnHidden(3, False)
1185 for i,width in enumerate(self.column_widths['receive'][1]):
1186 l.setColumnWidth(i, width)
1188 if self.current_account is None:
1189 account_items = self.wallet.accounts.items()
1190 elif self.current_account != -1:
1191 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1195 for k, account in account_items:
1196 name = self.wallet.get_account_name(k)
1197 c,u = self.wallet.get_account_balance(k)
1198 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1199 l.addTopLevelItem(account_item)
1200 account_item.setExpanded(True)
1201 account_item.setData(0, 32, k)
1203 if not self.wallet.is_seeded(k):
1204 icon = QIcon(":icons/key.png")
1205 account_item.setIcon(0, icon)
1207 for is_change in ([0,1]):
1208 name = _("Receiving") if not is_change else _("Change")
1209 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1210 account_item.addChild(seq_item)
1211 if not is_change: seq_item.setExpanded(True)
1216 for address in account.get_addresses(is_change):
1217 h = self.wallet.history.get(address,[])
1221 if gap > self.wallet.gap_limit:
1226 num_tx = '*' if h == ['*'] else "%d"%len(h)
1227 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1228 self.update_receive_item(item)
1230 item.setBackgroundColor(1, QColor('red'))
1231 seq_item.addChild(item)
1234 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1235 c,u = self.wallet.get_imported_balance()
1236 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1237 l.addTopLevelItem(account_item)
1238 account_item.setExpanded(True)
1239 for address in self.wallet.imported_keys.keys():
1240 item = QTreeWidgetItem( [ address, '', '', ''] )
1241 self.update_receive_item(item)
1242 account_item.addChild(item)
1245 # we use column 1 because column 0 may be hidden
1246 l.setCurrentItem(l.topLevelItem(0),1)
1249 def update_contacts_tab(self):
1250 l = self.contacts_list
1253 for address in self.wallet.addressbook:
1254 label = self.wallet.labels.get(address,'')
1255 n = self.wallet.get_num_tx(address)
1256 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1257 item.setFont(0, QFont(MONOSPACE_FONT))
1258 # 32 = label can be edited (bool)
1259 item.setData(0,32, True)
1261 item.setData(0,33, address)
1262 l.addTopLevelItem(item)
1264 run_hook('update_contacts_tab', l)
1265 l.setCurrentItem(l.topLevelItem(0))
1269 def create_console_tab(self):
1270 from console import Console
1271 self.console = console = Console()
1275 def update_console(self):
1276 console = self.console
1277 console.history = self.config.get("console-history",[])
1278 console.history_index = len(console.history)
1280 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1281 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1283 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1285 def mkfunc(f, method):
1286 return lambda *args: apply( f, (method, args, self.password_dialog ))
1288 if m[0]=='_' or m=='wallet' or m == 'interface': continue
1289 methods[m] = mkfunc(c._run, m)
1291 console.updateNamespace(methods)
1294 def change_account(self,s):
1295 if s == _("All accounts"):
1296 self.current_account = None
1298 accounts = self.wallet.get_account_names()
1299 for k, v in accounts.items():
1301 self.current_account = k
1302 self.update_history_tab()
1303 self.update_status()
1304 self.update_receive_tab()
1306 def create_status_bar(self):
1309 sb.setFixedHeight(35)
1310 qtVersion = qVersion()
1312 self.balance_label = QLabel("")
1313 sb.addWidget(self.balance_label)
1315 from version_getter import UpdateLabel
1316 self.updatelabel = UpdateLabel(self.config, sb)
1318 self.account_selector = QComboBox()
1319 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1320 sb.addPermanentWidget(self.account_selector)
1322 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1323 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1325 self.lock_icon = QIcon()
1326 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1327 sb.addPermanentWidget( self.password_button )
1329 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1330 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1331 sb.addPermanentWidget( self.seed_button )
1332 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1333 sb.addPermanentWidget( self.status_button )
1335 run_hook('create_status_bar', (sb,))
1337 self.setStatusBar(sb)
1340 def update_lock_icon(self):
1341 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1342 self.password_button.setIcon( icon )
1345 def update_buttons_on_seed(self):
1346 if not self.wallet.is_watching_only():
1347 self.seed_button.show()
1348 self.password_button.show()
1349 self.send_button.setText(_("Send"))
1351 self.password_button.hide()
1352 self.seed_button.hide()
1353 self.send_button.setText(_("Create unsigned transaction"))
1356 def change_password_dialog(self):
1357 from password_dialog import PasswordDialog
1358 d = PasswordDialog(self.wallet, self)
1360 self.update_lock_icon()
1363 def new_contact_dialog(self):
1364 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1365 address = unicode(text)
1367 if is_valid(address):
1368 self.wallet.add_contact(address)
1369 self.update_contacts_tab()
1370 self.update_history_tab()
1371 self.update_completions()
1373 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1376 def new_account_dialog(self):
1378 dialog = QDialog(self)
1380 dialog.setWindowTitle(_("New Account"))
1382 addr = self.wallet.new_account_address()
1383 vbox = QVBoxLayout()
1384 msg = _("Electrum considers that an account exists only if it contains bitcoins.") + '\n' \
1385 + _("To create a new account, please send coins to the first address of that account.") + '\n' \
1386 + _("Note: you will need to wait for 2 confirmations before the account is created.")
1387 vbox.addWidget(QLabel(msg))
1388 vbox.addWidget(QLabel(_('Address')+':'))
1393 vbox.addLayout(ok_cancel_buttons(dialog))
1394 dialog.setLayout(vbox)
1401 def show_master_public_key_old(self):
1402 dialog = QDialog(self)
1404 dialog.setWindowTitle(_("Master Public Key"))
1406 main_text = QTextEdit()
1407 main_text.setText(self.wallet.get_master_public_key())
1408 main_text.setReadOnly(True)
1409 main_text.setMaximumHeight(170)
1410 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1412 ok_button = QPushButton(_("OK"))
1413 ok_button.setDefault(True)
1414 ok_button.clicked.connect(dialog.accept)
1416 main_layout = QGridLayout()
1417 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1419 main_layout.addWidget(main_text, 1, 0)
1420 main_layout.addWidget(qrw, 1, 1 )
1422 vbox.addLayout(close_button(dialog))
1423 dialog.setLayout(vbox)
1427 def show_master_public_key(self):
1429 if self.wallet.seed_version == 4:
1430 self.show_master_public_keys_old()
1433 dialog = QDialog(self)
1435 dialog.setWindowTitle(_("Master Public Keys"))
1437 chain_text = QTextEdit()
1438 chain_text.setReadOnly(True)
1439 chain_text.setMaximumHeight(170)
1440 chain_qrw = QRCodeWidget()
1442 mpk_text = QTextEdit()
1443 mpk_text.setReadOnly(True)
1444 mpk_text.setMaximumHeight(170)
1445 mpk_qrw = QRCodeWidget()
1447 main_layout = QGridLayout()
1449 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1450 main_layout.addWidget(mpk_text, 1, 1)
1451 main_layout.addWidget(mpk_qrw, 1, 2)
1453 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1454 main_layout.addWidget(chain_text, 2, 1)
1455 main_layout.addWidget(chain_qrw, 2, 2)
1458 c, K, cK = self.wallet.master_public_keys[str(key)]
1459 chain_text.setText(c)
1460 chain_qrw.set_addr(c)
1461 chain_qrw.update_qr()
1466 key_selector = QComboBox()
1467 keys = sorted(self.wallet.master_public_keys.keys())
1468 key_selector.addItems(keys)
1470 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1471 main_layout.addWidget(key_selector, 0, 1)
1472 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1476 vbox = QVBoxLayout()
1477 vbox.addLayout(main_layout)
1478 vbox.addLayout(close_button(dialog))
1480 dialog.setLayout(vbox)
1485 def show_seed_dialog(self, password):
1486 if self.wallet.is_watching_only():
1487 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1490 if self.wallet.seed:
1492 seed = self.wallet.decode_seed(password)
1494 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1496 from seed_dialog import SeedDialog
1497 d = SeedDialog(self, seed, self.wallet.imported_keys)
1501 for k in self.wallet.master_private_keys.keys():
1502 pk = self.wallet.get_master_private_key(k, password)
1504 from seed_dialog import PrivateKeysDialog
1505 d = PrivateKeysDialog(self,l)
1512 def show_qrcode(self, data, title = _("QR code")):
1516 d.setWindowTitle(title)
1517 d.setMinimumSize(270, 300)
1518 vbox = QVBoxLayout()
1519 qrw = QRCodeWidget(data)
1520 vbox.addWidget(qrw, 1)
1521 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1522 hbox = QHBoxLayout()
1526 filename = "qrcode.bmp"
1527 bmp.save_qrcode(qrw.qr, filename)
1528 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1530 b = QPushButton(_("Save"))
1532 b.clicked.connect(print_qr)
1534 b = QPushButton(_("Close"))
1536 b.clicked.connect(d.accept)
1539 vbox.addLayout(hbox)
1544 def do_protect(self, func, args):
1545 if self.wallet.use_encryption:
1546 password = self.password_dialog()
1552 if args != (False,):
1553 args = (self,) + args + (password,)
1555 args = (self,password)
1560 def show_private_key(self, address, password):
1561 if not address: return
1563 pk_list = self.wallet.get_private_key(address, password)
1564 except BaseException, e:
1565 self.show_message(str(e))
1567 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1571 def do_sign(self, address, message, signature, password):
1572 message = unicode(message.toPlainText())
1573 message = message.encode('utf-8')
1575 sig = self.wallet.sign_message(str(address.text()), message, password)
1576 signature.setText(sig)
1577 except BaseException, e:
1578 self.show_message(str(e))
1580 def sign_message(self, address):
1581 if not address: return
1584 d.setWindowTitle(_('Sign Message'))
1585 d.setMinimumSize(410, 290)
1587 tab_widget = QTabWidget()
1589 layout = QGridLayout(tab)
1591 sign_address = QLineEdit()
1593 sign_address.setText(address)
1594 layout.addWidget(QLabel(_('Address')), 1, 0)
1595 layout.addWidget(sign_address, 1, 1)
1597 sign_message = QTextEdit()
1598 layout.addWidget(QLabel(_('Message')), 2, 0)
1599 layout.addWidget(sign_message, 2, 1)
1600 layout.setRowStretch(2,3)
1602 sign_signature = QTextEdit()
1603 layout.addWidget(QLabel(_('Signature')), 3, 0)
1604 layout.addWidget(sign_signature, 3, 1)
1605 layout.setRowStretch(3,1)
1608 hbox = QHBoxLayout()
1609 b = QPushButton(_("Sign"))
1611 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1612 b = QPushButton(_("Close"))
1613 b.clicked.connect(d.accept)
1615 layout.addLayout(hbox, 4, 1)
1616 tab_widget.addTab(tab, _("Sign"))
1620 layout = QGridLayout(tab)
1622 verify_address = QLineEdit()
1623 layout.addWidget(QLabel(_('Address')), 1, 0)
1624 layout.addWidget(verify_address, 1, 1)
1626 verify_message = QTextEdit()
1627 layout.addWidget(QLabel(_('Message')), 2, 0)
1628 layout.addWidget(verify_message, 2, 1)
1629 layout.setRowStretch(2,3)
1631 verify_signature = QTextEdit()
1632 layout.addWidget(QLabel(_('Signature')), 3, 0)
1633 layout.addWidget(verify_signature, 3, 1)
1634 layout.setRowStretch(3,1)
1637 message = unicode(verify_message.toPlainText())
1638 message = message.encode('utf-8')
1639 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1640 self.show_message(_("Signature verified"))
1642 self.show_message(_("Error: wrong signature"))
1644 hbox = QHBoxLayout()
1645 b = QPushButton(_("Verify"))
1646 b.clicked.connect(do_verify)
1648 b = QPushButton(_("Close"))
1649 b.clicked.connect(d.accept)
1651 layout.addLayout(hbox, 4, 1)
1652 tab_widget.addTab(tab, _("Verify"))
1654 vbox = QVBoxLayout()
1655 vbox.addWidget(tab_widget)
1662 def question(self, msg):
1663 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1665 def show_message(self, msg):
1666 QMessageBox.information(self, _('Message'), msg, _('OK'))
1668 def password_dialog(self ):
1675 vbox = QVBoxLayout()
1676 msg = _('Please enter your password')
1677 vbox.addWidget(QLabel(msg))
1679 grid = QGridLayout()
1681 grid.addWidget(QLabel(_('Password')), 1, 0)
1682 grid.addWidget(pw, 1, 1)
1683 vbox.addLayout(grid)
1685 vbox.addLayout(ok_cancel_buttons(d))
1688 run_hook('password_dialog', pw, grid, 1)
1689 if not d.exec_(): return
1690 return unicode(pw.text())
1699 def tx_from_text(self, txt):
1700 "json or raw hexadecimal"
1703 tx = Transaction(txt)
1709 tx_dict = json.loads(str(txt))
1710 assert "hex" in tx_dict.keys()
1711 assert "complete" in tx_dict.keys()
1712 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1713 if not tx_dict["complete"]:
1714 assert "input_info" in tx_dict.keys()
1715 input_info = json.loads(tx_dict['input_info'])
1716 tx.add_input_info(input_info)
1721 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1725 def read_tx_from_file(self):
1726 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1730 with open(fileName, "r") as f:
1731 file_content = f.read()
1732 except (ValueError, IOError, os.error), reason:
1733 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1735 return self.tx_from_text(file_content)
1739 def sign_raw_transaction(self, tx, input_info, password):
1740 self.wallet.signrawtransaction(tx, input_info, [], password)
1742 def do_process_from_text(self):
1743 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1746 tx = self.tx_from_text(text)
1748 self.show_transaction(tx)
1750 def do_process_from_file(self):
1751 tx = self.read_tx_from_file()
1753 self.show_transaction(tx)
1755 def do_process_from_csvReader(self, csvReader):
1758 for row in csvReader:
1760 amount = float(row[1])
1761 amount = int(100000000*amount)
1762 outputs.append((address, amount))
1763 except (ValueError, IOError, os.error), reason:
1764 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1768 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1769 except BaseException, e:
1770 self.show_message(str(e))
1773 self.show_transaction(tx)
1775 def do_process_from_csv_file(self):
1776 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1780 with open(fileName, "r") as f:
1781 csvReader = csv.reader(f)
1782 self.do_process_from_csvReader(csvReader)
1783 except (ValueError, IOError, os.error), reason:
1784 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1787 def do_process_from_csv_text(self):
1788 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1789 + _("Format: address, amount. One output per line"), _("Load CSV"))
1792 f = StringIO.StringIO(text)
1793 csvReader = csv.reader(f)
1794 self.do_process_from_csvReader(csvReader)
1799 def do_export_privkeys(self, password):
1800 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.")))
1803 select_export = _('Select file to export your private keys to')
1804 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1806 with open(fileName, "w+") as csvfile:
1807 transaction = csv.writer(csvfile)
1808 transaction.writerow(["address", "private_key"])
1810 addresses = self.wallet.addresses(True)
1812 for addr in addresses:
1813 pk = "".join(self.wallet.get_private_key(addr, password))
1814 transaction.writerow(["%34s"%addr,pk])
1816 self.show_message(_("Private keys exported."))
1818 except (IOError, os.error), reason:
1819 export_error_label = _("Electrum was unable to produce a private key-export.")
1820 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1822 except BaseException, e:
1823 self.show_message(str(e))
1827 def do_import_labels(self):
1828 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1829 if not labelsFile: return
1831 f = open(labelsFile, 'r')
1834 for key, value in json.loads(data).items():
1835 self.wallet.set_label(key, value)
1836 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1837 except (IOError, os.error), reason:
1838 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1841 def do_export_labels(self):
1842 labels = self.wallet.labels
1844 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1846 with open(fileName, 'w+') as f:
1847 json.dump(labels, f)
1848 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1849 except (IOError, os.error), reason:
1850 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1853 def do_export_history(self):
1854 from lite_window import csv_transaction
1855 csv_transaction(self.wallet)
1859 def do_import_privkey(self, password):
1860 if not self.wallet.imported_keys:
1861 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1862 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1863 + _('Are you sure you understand what you are doing?'), 3, 4)
1866 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1869 text = str(text).split()
1874 addr = self.wallet.import_key(key, password)
1875 except BaseException as e:
1881 addrlist.append(addr)
1883 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1885 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1886 self.update_receive_tab()
1887 self.update_history_tab()
1890 def settings_dialog(self):
1892 d.setWindowTitle(_('Electrum Settings'))
1894 vbox = QVBoxLayout()
1895 grid = QGridLayout()
1896 grid.setColumnStretch(0,1)
1898 nz_label = QLabel(_('Display zeros') + ':')
1899 grid.addWidget(nz_label, 0, 0)
1900 nz_e = AmountEdit(None,True)
1901 nz_e.setText("%d"% self.num_zeros)
1902 grid.addWidget(nz_e, 0, 1)
1903 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1904 grid.addWidget(HelpButton(msg), 0, 2)
1905 if not self.config.is_modifiable('num_zeros'):
1906 for w in [nz_e, nz_label]: w.setEnabled(False)
1908 lang_label=QLabel(_('Language') + ':')
1909 grid.addWidget(lang_label, 1, 0)
1910 lang_combo = QComboBox()
1911 from electrum.i18n import languages
1912 lang_combo.addItems(languages.values())
1914 index = languages.keys().index(self.config.get("language",''))
1917 lang_combo.setCurrentIndex(index)
1918 grid.addWidget(lang_combo, 1, 1)
1919 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1920 if not self.config.is_modifiable('language'):
1921 for w in [lang_combo, lang_label]: w.setEnabled(False)
1924 fee_label = QLabel(_('Transaction fee') + ':')
1925 grid.addWidget(fee_label, 2, 0)
1926 fee_e = AmountEdit(self.base_unit)
1927 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1928 grid.addWidget(fee_e, 2, 1)
1929 msg = _('Fee per kilobyte of transaction.') + ' ' \
1930 + _('Recommended value') + ': ' + self.format_amount(50000)
1931 grid.addWidget(HelpButton(msg), 2, 2)
1932 if not self.config.is_modifiable('fee_per_kb'):
1933 for w in [fee_e, fee_label]: w.setEnabled(False)
1935 units = ['BTC', 'mBTC']
1936 unit_label = QLabel(_('Base unit') + ':')
1937 grid.addWidget(unit_label, 3, 0)
1938 unit_combo = QComboBox()
1939 unit_combo.addItems(units)
1940 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1941 grid.addWidget(unit_combo, 3, 1)
1942 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
1943 + '\n1BTC=1000mBTC.\n' \
1944 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
1946 usechange_cb = QCheckBox(_('Use change addresses'))
1947 usechange_cb.setChecked(self.wallet.use_change)
1948 grid.addWidget(usechange_cb, 4, 0)
1949 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
1950 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1952 grid.setRowStretch(5,1)
1954 vbox.addLayout(grid)
1955 vbox.addLayout(ok_cancel_buttons(d))
1959 if not d.exec_(): return
1961 fee = unicode(fee_e.text())
1963 fee = self.read_amount(fee)
1965 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1968 self.wallet.set_fee(fee)
1970 nz = unicode(nz_e.text())
1975 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1978 if self.num_zeros != nz:
1980 self.config.set_key('num_zeros', nz, True)
1981 self.update_history_tab()
1982 self.update_receive_tab()
1984 usechange_result = usechange_cb.isChecked()
1985 if self.wallet.use_change != usechange_result:
1986 self.wallet.use_change = usechange_result
1987 self.config.set_key('use_change', self.wallet.use_change, True)
1989 unit_result = units[unit_combo.currentIndex()]
1990 if self.base_unit() != unit_result:
1991 self.decimal_point = 8 if unit_result == 'BTC' else 5
1992 self.config.set_key('decimal_point', self.decimal_point, True)
1993 self.update_history_tab()
1994 self.update_status()
1996 need_restart = False
1998 lang_request = languages.keys()[lang_combo.currentIndex()]
1999 if lang_request != self.config.get('language'):
2000 self.config.set_key("language", lang_request, True)
2003 run_hook('close_settings_dialog')
2006 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2009 def run_network_dialog(self):
2010 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2012 def closeEvent(self, event):
2014 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2015 self.save_column_widths()
2016 self.config.set_key("console-history", self.console.history[-50:], True)
2021 def plugins_dialog(self):
2022 from electrum.plugins import plugins
2025 d.setWindowTitle(_('Electrum Plugins'))
2028 vbox = QVBoxLayout(d)
2031 scroll = QScrollArea()
2032 scroll.setEnabled(True)
2033 scroll.setWidgetResizable(True)
2034 scroll.setMinimumSize(400,250)
2035 vbox.addWidget(scroll)
2039 w.setMinimumHeight(len(plugins)*35)
2041 grid = QGridLayout()
2042 grid.setColumnStretch(0,1)
2045 def mk_toggle(cb, p):
2046 return lambda: cb.setChecked(p.toggle())
2047 for i, p in enumerate(plugins):
2049 cb = QCheckBox(p.fullname())
2050 cb.setDisabled(not p.is_available())
2051 cb.setChecked(p.is_enabled())
2052 cb.clicked.connect(mk_toggle(cb,p))
2053 grid.addWidget(cb, i, 0)
2054 if p.requires_settings():
2055 b = EnterButton(_('Settings'), p.settings_dialog)
2056 b.setEnabled( p.is_enabled() )
2057 grid.addWidget(b, i, 1)
2058 grid.addWidget(HelpButton(p.description()), i, 2)
2060 print_msg(_("Error: cannot display plugin"), p)
2061 traceback.print_exc(file=sys.stdout)
2062 grid.setRowStretch(i+1,1)
2064 vbox.addLayout(close_button(d))
2069 def show_account_details(self, k):
2071 d.setWindowTitle(_('Account Details'))
2074 vbox = QVBoxLayout(d)
2075 roots = self.wallet.get_roots(k)
2077 name = self.wallet.get_account_name(k)
2078 label = QLabel('Name: ' + name)
2079 vbox.addWidget(label)
2081 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2082 vbox.addWidget(QLabel('Type: ' + acctype))
2084 label = QLabel('Derivation: ' + k)
2085 vbox.addWidget(label)
2088 # mpk = self.wallet.master_public_keys[root]
2089 # text = QTextEdit()
2090 # text.setReadOnly(True)
2091 # text.setMaximumHeight(120)
2092 # text.setText(repr(mpk))
2093 # vbox.addWidget(text)
2095 vbox.addLayout(close_button(d))