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,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 if not self.network.is_connected():
465 print_error("Notifying GUI")
466 if len(self.network.interface.pending_transactions_for_notifications) > 0:
467 # Combine the transactions if there are more then three
468 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
471 for tx in self.network.interface.pending_transactions_for_notifications:
472 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
476 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
477 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
479 self.network.interface.pending_transactions_for_notifications = []
481 for tx in self.network.interface.pending_transactions_for_notifications:
483 self.network.interface.pending_transactions_for_notifications.remove(tx)
484 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
486 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
488 def notify(self, message):
489 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
493 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
494 def getOpenFileName(self, title, filter = ""):
495 directory = self.config.get('io_dir', os.path.expanduser('~'))
496 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
497 if fileName and directory != os.path.dirname(fileName):
498 self.config.set_key('io_dir', os.path.dirname(fileName), True)
501 def getSaveFileName(self, title, filename, filter = ""):
502 directory = self.config.get('io_dir', os.path.expanduser('~'))
503 path = os.path.join( directory, filename )
504 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
505 if fileName and directory != os.path.dirname(fileName):
506 self.config.set_key('io_dir', os.path.dirname(fileName), True)
510 QMainWindow.close(self)
511 run_hook('close_main_window')
513 def connect_slots(self, sender):
514 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
515 self.previous_payto_e=''
517 def timer_actions(self):
518 if self.need_update.is_set():
520 self.need_update.clear()
521 run_hook('timer_actions')
523 def format_amount(self, x, is_diff=False, whitespaces=False):
524 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
526 def read_amount(self, x):
527 if x in['.', '']: return None
528 p = pow(10, self.decimal_point)
529 return int( p * Decimal(x) )
532 assert self.decimal_point in [5,8]
533 return "BTC" if self.decimal_point == 8 else "mBTC"
536 def update_status(self):
537 if self.network.is_connected():
538 if not self.wallet.up_to_date:
539 text = _("Synchronizing...")
540 icon = QIcon(":icons/status_waiting.png")
541 elif self.network.server_lag > 1:
542 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
543 icon = QIcon(":icons/status_lagging.png")
545 c, u = self.wallet.get_account_balance(self.current_account)
546 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
547 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
550 run_hook('set_quote_text', c+u, r)
553 text += " (%s)"%quote
555 self.tray.setToolTip(text)
556 icon = QIcon(":icons/status_connected.png")
558 text = _("Not connected")
559 icon = QIcon(":icons/status_disconnected.png")
561 self.balance_label.setText(text)
562 self.status_button.setIcon( icon )
565 def update_wallet(self):
567 if self.wallet.up_to_date or not self.network.is_connected():
568 self.update_history_tab()
569 self.update_receive_tab()
570 self.update_contacts_tab()
571 self.update_completions()
574 def create_history_tab(self):
575 self.history_list = l = MyTreeWidget(self)
577 for i,width in enumerate(self.column_widths['history']):
578 l.setColumnWidth(i, width)
579 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
580 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
581 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
583 l.customContextMenuRequested.connect(self.create_history_menu)
587 def create_history_menu(self, position):
588 self.history_list.selectedIndexes()
589 item = self.history_list.currentItem()
591 tx_hash = str(item.data(0, Qt.UserRole).toString())
592 if not tx_hash: return
594 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
595 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
596 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
597 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
600 def show_transaction(self, tx):
601 import transaction_dialog
602 d = transaction_dialog.TxDialog(tx, self)
605 def tx_label_clicked(self, item, column):
606 if column==2 and item.isSelected():
608 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
609 self.history_list.editItem( item, column )
610 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
613 def tx_label_changed(self, item, column):
617 tx_hash = str(item.data(0, Qt.UserRole).toString())
618 tx = self.wallet.transactions.get(tx_hash)
619 text = unicode( item.text(2) )
620 self.wallet.set_label(tx_hash, text)
622 item.setForeground(2, QBrush(QColor('black')))
624 text = self.wallet.get_default_label(tx_hash)
625 item.setText(2, text)
626 item.setForeground(2, QBrush(QColor('gray')))
630 def edit_label(self, is_recv):
631 l = self.receive_list if is_recv else self.contacts_list
632 item = l.currentItem()
633 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
634 l.editItem( item, 1 )
635 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
639 def address_label_clicked(self, item, column, l, column_addr, column_label):
640 if column == column_label and item.isSelected():
641 is_editable = item.data(0, 32).toBool()
644 addr = unicode( item.text(column_addr) )
645 label = unicode( item.text(column_label) )
646 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
647 l.editItem( item, column )
648 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
651 def address_label_changed(self, item, column, l, column_addr, column_label):
652 if column == column_label:
653 addr = unicode( item.text(column_addr) )
654 text = unicode( item.text(column_label) )
655 is_editable = item.data(0, 32).toBool()
659 changed = self.wallet.set_label(addr, text)
661 self.update_history_tab()
662 self.update_completions()
664 self.current_item_changed(item)
666 run_hook('item_changed', item, column)
669 def current_item_changed(self, a):
670 run_hook('current_item_changed', a)
674 def update_history_tab(self):
676 self.history_list.clear()
677 for item in self.wallet.get_tx_history(self.current_account):
678 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
681 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
683 time_str = _("unknown")
686 time_str = 'unverified'
687 icon = QIcon(":icons/unconfirmed.png")
690 icon = QIcon(":icons/unconfirmed.png")
692 icon = QIcon(":icons/clock%d.png"%conf)
694 icon = QIcon(":icons/confirmed.png")
696 if value is not None:
697 v_str = self.format_amount(value, True, whitespaces=True)
701 balance_str = self.format_amount(balance, whitespaces=True)
704 label, is_default_label = self.wallet.get_label(tx_hash)
706 label = _('Pruned transaction outputs')
707 is_default_label = False
709 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
710 item.setFont(2, QFont(MONOSPACE_FONT))
711 item.setFont(3, QFont(MONOSPACE_FONT))
712 item.setFont(4, QFont(MONOSPACE_FONT))
714 item.setForeground(3, QBrush(QColor("#BC1E1E")))
716 item.setData(0, Qt.UserRole, tx_hash)
717 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
719 item.setForeground(2, QBrush(QColor('grey')))
721 item.setIcon(0, icon)
722 self.history_list.insertTopLevelItem(0,item)
725 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
728 def create_send_tab(self):
733 grid.setColumnMinimumWidth(3,300)
734 grid.setColumnStretch(5,1)
737 self.payto_e = QLineEdit()
738 grid.addWidget(QLabel(_('Pay to')), 1, 0)
739 grid.addWidget(self.payto_e, 1, 1, 1, 3)
741 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)
743 completer = QCompleter()
744 completer.setCaseSensitivity(False)
745 self.payto_e.setCompleter(completer)
746 completer.setModel(self.completions)
748 self.message_e = QLineEdit()
749 grid.addWidget(QLabel(_('Description')), 2, 0)
750 grid.addWidget(self.message_e, 2, 1, 1, 3)
751 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)
753 self.amount_e = AmountEdit(self.base_unit)
754 grid.addWidget(QLabel(_('Amount')), 3, 0)
755 grid.addWidget(self.amount_e, 3, 1, 1, 2)
756 grid.addWidget(HelpButton(
757 _('Amount to be sent.') + '\n\n' \
758 + _('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.') \
759 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
761 self.fee_e = AmountEdit(self.base_unit)
762 grid.addWidget(QLabel(_('Fee')), 4, 0)
763 grid.addWidget(self.fee_e, 4, 1, 1, 2)
764 grid.addWidget(HelpButton(
765 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
766 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
767 + _('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)
770 self.send_button = EnterButton(_("Send"), self.do_send)
771 grid.addWidget(self.send_button, 6, 1)
773 b = EnterButton(_("Clear"),self.do_clear)
774 grid.addWidget(b, 6, 2)
776 self.payto_sig = QLabel('')
777 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
779 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
780 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
789 def entry_changed( is_fee ):
790 self.funds_error = False
792 if self.amount_e.is_shortcut:
793 self.amount_e.is_shortcut = False
794 c, u = self.wallet.get_account_balance(self.current_account)
795 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
796 fee = self.wallet.estimated_fee(inputs)
798 self.amount_e.setText( self.format_amount(amount) )
799 self.fee_e.setText( self.format_amount( fee ) )
802 amount = self.read_amount(str(self.amount_e.text()))
803 fee = self.read_amount(str(self.fee_e.text()))
805 if not is_fee: fee = None
808 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
810 self.fee_e.setText( self.format_amount( fee ) )
813 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
817 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
818 self.funds_error = True
819 text = _( "Not enough funds" )
820 c, u = self.wallet.get_frozen_balance()
821 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
823 self.statusBar().showMessage(text)
824 self.amount_e.setPalette(palette)
825 self.fee_e.setPalette(palette)
827 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
828 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
830 run_hook('create_send_tab', grid)
834 def update_completions(self):
836 for addr,label in self.wallet.labels.items():
837 if addr in self.wallet.addressbook:
838 l.append( label + ' <' + addr + '>')
840 run_hook('update_completions', l)
841 self.completions.setStringList(l)
845 return lambda s, *args: s.do_protect(func, args)
850 label = unicode( self.message_e.text() )
851 r = unicode( self.payto_e.text() )
854 # label or alias, with address in brackets
855 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
856 to_address = m.group(2) if m else r
858 if not is_valid(to_address):
859 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
863 amount = self.read_amount(unicode( self.amount_e.text()))
865 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
868 fee = self.read_amount(unicode( self.fee_e.text()))
870 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
873 confirm_amount = self.config.get('confirm_amount', 100000000)
874 if amount >= confirm_amount:
875 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
878 self.send_tx(to_address, amount, fee, label)
882 def send_tx(self, to_address, amount, fee, label, password):
885 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
886 except BaseException, e:
887 traceback.print_exc(file=sys.stdout)
888 self.show_message(str(e))
891 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
892 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
896 self.wallet.set_label(tx.hash(), label)
899 h = self.wallet.send_tx(tx)
900 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
901 status, msg = self.wallet.receive_tx( h )
903 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
905 self.update_contacts_tab()
907 QMessageBox.warning(self, _('Error'), msg, _('OK'))
909 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
911 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
912 with open(fileName,'w') as f:
913 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
914 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
916 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
918 # add recipient to addressbook
919 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
920 self.wallet.addressbook.append(to_address)
925 def set_url(self, url):
926 address, amount, label, message, signature, identity, url = util.parse_url(url)
928 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
931 self.mini.set_payment_fields(address, amount)
933 if label and self.wallet.labels.get(address) != label:
934 if self.question('Give label "%s" to address %s ?'%(label,address)):
935 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
936 self.wallet.addressbook.append(address)
937 self.wallet.set_label(address, label)
939 run_hook('set_url', url, self.show_message, self.question)
941 self.tabs.setCurrentIndex(1)
942 label = self.wallet.labels.get(address)
943 m_addr = label + ' <'+ address +'>' if label else address
944 self.payto_e.setText(m_addr)
946 self.message_e.setText(message)
948 self.amount_e.setText(amount)
951 self.set_frozen(self.payto_e,True)
952 self.set_frozen(self.amount_e,True)
953 self.set_frozen(self.message_e,True)
954 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
956 self.payto_sig.setVisible(False)
959 self.payto_sig.setVisible(False)
960 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
962 self.set_frozen(e,False)
965 def set_frozen(self,entry,frozen):
967 entry.setReadOnly(True)
968 entry.setFrame(False)
970 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
971 entry.setPalette(palette)
973 entry.setReadOnly(False)
976 palette.setColor(entry.backgroundRole(), QColor('white'))
977 entry.setPalette(palette)
980 def toggle_freeze(self,addr):
982 if addr in self.wallet.frozen_addresses:
983 self.wallet.unfreeze(addr)
985 self.wallet.freeze(addr)
986 self.update_receive_tab()
988 def toggle_priority(self,addr):
990 if addr in self.wallet.prioritized_addresses:
991 self.wallet.unprioritize(addr)
993 self.wallet.prioritize(addr)
994 self.update_receive_tab()
997 def create_list_tab(self, headers):
998 "generic tab creation method"
999 l = MyTreeWidget(self)
1000 l.setColumnCount( len(headers) )
1001 l.setHeaderLabels( headers )
1004 vbox = QVBoxLayout()
1011 vbox.addWidget(buttons)
1013 hbox = QHBoxLayout()
1016 buttons.setLayout(hbox)
1021 def create_receive_tab(self):
1022 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1023 l.setContextMenuPolicy(Qt.CustomContextMenu)
1024 l.customContextMenuRequested.connect(self.create_receive_menu)
1025 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1026 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1027 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1028 self.receive_list = l
1029 self.receive_buttons_hbox = hbox
1036 def save_column_widths(self):
1037 self.column_widths["receive"] = []
1038 for i in range(self.receive_list.columnCount() -1):
1039 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1041 self.column_widths["history"] = []
1042 for i in range(self.history_list.columnCount() - 1):
1043 self.column_widths["history"].append(self.history_list.columnWidth(i))
1045 self.column_widths["contacts"] = []
1046 for i in range(self.contacts_list.columnCount() - 1):
1047 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1049 self.config.set_key("column_widths", self.column_widths, True)
1052 def create_contacts_tab(self):
1053 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1054 l.setContextMenuPolicy(Qt.CustomContextMenu)
1055 l.customContextMenuRequested.connect(self.create_contact_menu)
1056 for i,width in enumerate(self.column_widths['contacts']):
1057 l.setColumnWidth(i, width)
1059 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1060 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1061 self.contacts_list = l
1062 self.contacts_buttons_hbox = hbox
1067 def delete_imported_key(self, addr):
1068 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1069 self.wallet.delete_imported_key(addr)
1070 self.update_receive_tab()
1071 self.update_history_tab()
1073 def edit_account_label(self, k):
1074 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1076 label = unicode(text)
1077 self.wallet.set_label(k,label)
1078 self.update_receive_tab()
1080 def create_account_menu(self, position, k, item):
1082 if item.isExpanded():
1083 menu.addAction(_("Minimize"), lambda: item.setExpanded(False))
1085 menu.addAction(_("Maximize"), lambda: item.setExpanded(True))
1086 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1087 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1088 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1090 def create_receive_menu(self, position):
1091 # fixme: this function apparently has a side effect.
1092 # if it is not called the menu pops up several times
1093 #self.receive_list.selectedIndexes()
1095 item = self.receive_list.itemAt(position)
1098 addr = unicode(item.text(0))
1099 if not is_valid(addr):
1100 k = str(item.data(0,32).toString())
1102 self.create_account_menu(position, k, item)
1104 item.setExpanded(not item.isExpanded())
1108 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1109 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1110 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1111 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1112 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1113 if addr in self.wallet.imported_keys:
1114 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1116 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1117 menu.addAction(t, lambda: self.toggle_freeze(addr))
1118 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1119 menu.addAction(t, lambda: self.toggle_priority(addr))
1121 run_hook('receive_menu', menu)
1122 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1125 def payto(self, addr):
1127 label = self.wallet.labels.get(addr)
1128 m_addr = label + ' <' + addr + '>' if label else addr
1129 self.tabs.setCurrentIndex(1)
1130 self.payto_e.setText(m_addr)
1131 self.amount_e.setFocus()
1134 def delete_contact(self, x):
1135 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1136 self.wallet.delete_contact(x)
1137 self.wallet.set_label(x, None)
1138 self.update_history_tab()
1139 self.update_contacts_tab()
1140 self.update_completions()
1143 def create_contact_menu(self, position):
1144 item = self.contacts_list.itemAt(position)
1146 addr = unicode(item.text(0))
1147 label = unicode(item.text(1))
1148 is_editable = item.data(0,32).toBool()
1149 payto_addr = item.data(0,33).toString()
1151 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1152 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1153 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1155 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1156 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1158 run_hook('create_contact_menu', menu, item)
1159 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1162 def update_receive_item(self, item):
1163 item.setFont(0, QFont(MONOSPACE_FONT))
1164 address = str(item.data(0,0).toString())
1165 label = self.wallet.labels.get(address,'')
1166 item.setData(1,0,label)
1167 item.setData(0,32, True) # is editable
1169 run_hook('update_receive_item', address, item)
1171 c, u = self.wallet.get_addr_balance(address)
1172 balance = self.format_amount(c + u)
1173 item.setData(2,0,balance)
1175 if address in self.wallet.frozen_addresses:
1176 item.setBackgroundColor(0, QColor('lightblue'))
1177 elif address in self.wallet.prioritized_addresses:
1178 item.setBackgroundColor(0, QColor('lightgreen'))
1181 def update_receive_tab(self):
1182 l = self.receive_list
1185 l.setColumnHidden(2, False)
1186 l.setColumnHidden(3, False)
1187 for i,width in enumerate(self.column_widths['receive']):
1188 l.setColumnWidth(i, width)
1190 if self.current_account is None:
1191 account_items = self.wallet.accounts.items()
1192 elif self.current_account != -1:
1193 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1197 for k, account in account_items:
1198 name = self.wallet.get_account_name(k)
1199 c,u = self.wallet.get_account_balance(k)
1200 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1201 l.addTopLevelItem(account_item)
1202 account_item.setExpanded(True)
1203 account_item.setData(0, 32, k)
1205 if not self.wallet.is_seeded(k):
1206 icon = QIcon(":icons/key.png")
1207 account_item.setIcon(0, icon)
1209 for is_change in ([0,1]):
1210 name = _("Receiving") if not is_change else _("Change")
1211 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1212 account_item.addChild(seq_item)
1213 if not is_change: seq_item.setExpanded(True)
1218 for address in account.get_addresses(is_change):
1219 h = self.wallet.history.get(address,[])
1223 if gap > self.wallet.gap_limit:
1228 num_tx = '*' if h == ['*'] else "%d"%len(h)
1229 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1230 self.update_receive_item(item)
1232 item.setBackgroundColor(1, QColor('red'))
1233 seq_item.addChild(item)
1236 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1237 c,u = self.wallet.get_imported_balance()
1238 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1239 l.addTopLevelItem(account_item)
1240 account_item.setExpanded(True)
1241 for address in self.wallet.imported_keys.keys():
1242 item = QTreeWidgetItem( [ address, '', '', ''] )
1243 self.update_receive_item(item)
1244 account_item.addChild(item)
1247 # we use column 1 because column 0 may be hidden
1248 l.setCurrentItem(l.topLevelItem(0),1)
1251 def update_contacts_tab(self):
1252 l = self.contacts_list
1255 for address in self.wallet.addressbook:
1256 label = self.wallet.labels.get(address,'')
1257 n = self.wallet.get_num_tx(address)
1258 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1259 item.setFont(0, QFont(MONOSPACE_FONT))
1260 # 32 = label can be edited (bool)
1261 item.setData(0,32, True)
1263 item.setData(0,33, address)
1264 l.addTopLevelItem(item)
1266 run_hook('update_contacts_tab', l)
1267 l.setCurrentItem(l.topLevelItem(0))
1271 def create_console_tab(self):
1272 from console import Console
1273 self.console = console = Console()
1277 def update_console(self):
1278 console = self.console
1279 console.history = self.config.get("console-history",[])
1280 console.history_index = len(console.history)
1282 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1283 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1285 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1287 def mkfunc(f, method):
1288 return lambda *args: apply( f, (method, args, self.password_dialog ))
1290 if m[0]=='_' or m in ['network','wallet']: continue
1291 methods[m] = mkfunc(c._run, m)
1293 console.updateNamespace(methods)
1296 def change_account(self,s):
1297 if s == _("All accounts"):
1298 self.current_account = None
1300 accounts = self.wallet.get_account_names()
1301 for k, v in accounts.items():
1303 self.current_account = k
1304 self.update_history_tab()
1305 self.update_status()
1306 self.update_receive_tab()
1308 def create_status_bar(self):
1311 sb.setFixedHeight(35)
1312 qtVersion = qVersion()
1314 self.balance_label = QLabel("")
1315 sb.addWidget(self.balance_label)
1317 from version_getter import UpdateLabel
1318 self.updatelabel = UpdateLabel(self.config, sb)
1320 self.account_selector = QComboBox()
1321 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1322 sb.addPermanentWidget(self.account_selector)
1324 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1325 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1327 self.lock_icon = QIcon()
1328 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1329 sb.addPermanentWidget( self.password_button )
1331 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1332 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1333 sb.addPermanentWidget( self.seed_button )
1334 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1335 sb.addPermanentWidget( self.status_button )
1337 run_hook('create_status_bar', (sb,))
1339 self.setStatusBar(sb)
1342 def update_lock_icon(self):
1343 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1344 self.password_button.setIcon( icon )
1347 def update_buttons_on_seed(self):
1348 if not self.wallet.is_watching_only():
1349 self.seed_button.show()
1350 self.password_button.show()
1351 self.send_button.setText(_("Send"))
1353 self.password_button.hide()
1354 self.seed_button.hide()
1355 self.send_button.setText(_("Create unsigned transaction"))
1358 def change_password_dialog(self):
1359 from password_dialog import PasswordDialog
1360 d = PasswordDialog(self.wallet, self)
1362 self.update_lock_icon()
1365 def new_contact_dialog(self):
1366 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1367 address = unicode(text)
1369 if is_valid(address):
1370 self.wallet.add_contact(address)
1371 self.update_contacts_tab()
1372 self.update_history_tab()
1373 self.update_completions()
1375 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1378 def new_account_dialog(self):
1380 dialog = QDialog(self)
1382 dialog.setWindowTitle(_("New Account"))
1384 addr = self.wallet.new_account_address()
1385 vbox = QVBoxLayout()
1386 msg = _("Electrum considers that an account exists only if it contains bitcoins.") + '\n' \
1387 + _("To create a new account, please send coins to the first address of that account.") + '\n' \
1388 + _("Note: you will need to wait for 2 confirmations before the account is created.")
1389 vbox.addWidget(QLabel(msg))
1390 vbox.addWidget(QLabel(_('Address')+':'))
1395 vbox.addLayout(ok_cancel_buttons(dialog))
1396 dialog.setLayout(vbox)
1403 def show_master_public_key_old(self):
1404 dialog = QDialog(self)
1406 dialog.setWindowTitle(_("Master Public Key"))
1408 main_text = QTextEdit()
1409 main_text.setText(self.wallet.get_master_public_key())
1410 main_text.setReadOnly(True)
1411 main_text.setMaximumHeight(170)
1412 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1414 ok_button = QPushButton(_("OK"))
1415 ok_button.setDefault(True)
1416 ok_button.clicked.connect(dialog.accept)
1418 main_layout = QGridLayout()
1419 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1421 main_layout.addWidget(main_text, 1, 0)
1422 main_layout.addWidget(qrw, 1, 1 )
1424 vbox.addLayout(close_button(dialog))
1425 dialog.setLayout(vbox)
1429 def show_master_public_key(self):
1431 if self.wallet.seed_version == 4:
1432 self.show_master_public_keys_old()
1435 dialog = QDialog(self)
1437 dialog.setWindowTitle(_("Master Public Keys"))
1439 chain_text = QTextEdit()
1440 chain_text.setReadOnly(True)
1441 chain_text.setMaximumHeight(170)
1442 chain_qrw = QRCodeWidget()
1444 mpk_text = QTextEdit()
1445 mpk_text.setReadOnly(True)
1446 mpk_text.setMaximumHeight(170)
1447 mpk_qrw = QRCodeWidget()
1449 main_layout = QGridLayout()
1451 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1452 main_layout.addWidget(mpk_text, 1, 1)
1453 main_layout.addWidget(mpk_qrw, 1, 2)
1455 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1456 main_layout.addWidget(chain_text, 2, 1)
1457 main_layout.addWidget(chain_qrw, 2, 2)
1460 c, K, cK = self.wallet.master_public_keys[str(key)]
1461 chain_text.setText(c)
1462 chain_qrw.set_addr(c)
1463 chain_qrw.update_qr()
1468 key_selector = QComboBox()
1469 keys = sorted(self.wallet.master_public_keys.keys())
1470 key_selector.addItems(keys)
1472 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1473 main_layout.addWidget(key_selector, 0, 1)
1474 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1478 vbox = QVBoxLayout()
1479 vbox.addLayout(main_layout)
1480 vbox.addLayout(close_button(dialog))
1482 dialog.setLayout(vbox)
1487 def show_seed_dialog(self, password):
1488 if self.wallet.is_watching_only():
1489 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1492 if self.wallet.seed:
1494 seed = self.wallet.decode_seed(password)
1496 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1498 from seed_dialog import SeedDialog
1499 d = SeedDialog(self, seed, self.wallet.imported_keys)
1503 for k in self.wallet.master_private_keys.keys():
1504 pk = self.wallet.get_master_private_key(k, password)
1506 from seed_dialog import PrivateKeysDialog
1507 d = PrivateKeysDialog(self,l)
1514 def show_qrcode(self, data, title = _("QR code")):
1518 d.setWindowTitle(title)
1519 d.setMinimumSize(270, 300)
1520 vbox = QVBoxLayout()
1521 qrw = QRCodeWidget(data)
1522 vbox.addWidget(qrw, 1)
1523 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1524 hbox = QHBoxLayout()
1528 filename = "qrcode.bmp"
1529 bmp.save_qrcode(qrw.qr, filename)
1530 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1532 b = QPushButton(_("Save"))
1534 b.clicked.connect(print_qr)
1536 b = QPushButton(_("Close"))
1538 b.clicked.connect(d.accept)
1541 vbox.addLayout(hbox)
1546 def do_protect(self, func, args):
1547 if self.wallet.use_encryption:
1548 password = self.password_dialog()
1554 if args != (False,):
1555 args = (self,) + args + (password,)
1557 args = (self,password)
1562 def show_private_key(self, address, password):
1563 if not address: return
1565 pk_list = self.wallet.get_private_key(address, password)
1566 except BaseException, e:
1567 self.show_message(str(e))
1569 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1573 def do_sign(self, address, message, signature, password):
1574 message = unicode(message.toPlainText())
1575 message = message.encode('utf-8')
1577 sig = self.wallet.sign_message(str(address.text()), message, password)
1578 signature.setText(sig)
1579 except BaseException, e:
1580 self.show_message(str(e))
1582 def sign_message(self, address):
1583 if not address: return
1586 d.setWindowTitle(_('Sign Message'))
1587 d.setMinimumSize(410, 290)
1589 tab_widget = QTabWidget()
1591 layout = QGridLayout(tab)
1593 sign_address = QLineEdit()
1595 sign_address.setText(address)
1596 layout.addWidget(QLabel(_('Address')), 1, 0)
1597 layout.addWidget(sign_address, 1, 1)
1599 sign_message = QTextEdit()
1600 layout.addWidget(QLabel(_('Message')), 2, 0)
1601 layout.addWidget(sign_message, 2, 1)
1602 layout.setRowStretch(2,3)
1604 sign_signature = QTextEdit()
1605 layout.addWidget(QLabel(_('Signature')), 3, 0)
1606 layout.addWidget(sign_signature, 3, 1)
1607 layout.setRowStretch(3,1)
1610 hbox = QHBoxLayout()
1611 b = QPushButton(_("Sign"))
1613 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1614 b = QPushButton(_("Close"))
1615 b.clicked.connect(d.accept)
1617 layout.addLayout(hbox, 4, 1)
1618 tab_widget.addTab(tab, _("Sign"))
1622 layout = QGridLayout(tab)
1624 verify_address = QLineEdit()
1625 layout.addWidget(QLabel(_('Address')), 1, 0)
1626 layout.addWidget(verify_address, 1, 1)
1628 verify_message = QTextEdit()
1629 layout.addWidget(QLabel(_('Message')), 2, 0)
1630 layout.addWidget(verify_message, 2, 1)
1631 layout.setRowStretch(2,3)
1633 verify_signature = QTextEdit()
1634 layout.addWidget(QLabel(_('Signature')), 3, 0)
1635 layout.addWidget(verify_signature, 3, 1)
1636 layout.setRowStretch(3,1)
1639 message = unicode(verify_message.toPlainText())
1640 message = message.encode('utf-8')
1641 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1642 self.show_message(_("Signature verified"))
1644 self.show_message(_("Error: wrong signature"))
1646 hbox = QHBoxLayout()
1647 b = QPushButton(_("Verify"))
1648 b.clicked.connect(do_verify)
1650 b = QPushButton(_("Close"))
1651 b.clicked.connect(d.accept)
1653 layout.addLayout(hbox, 4, 1)
1654 tab_widget.addTab(tab, _("Verify"))
1656 vbox = QVBoxLayout()
1657 vbox.addWidget(tab_widget)
1664 def question(self, msg):
1665 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1667 def show_message(self, msg):
1668 QMessageBox.information(self, _('Message'), msg, _('OK'))
1670 def password_dialog(self ):
1677 vbox = QVBoxLayout()
1678 msg = _('Please enter your password')
1679 vbox.addWidget(QLabel(msg))
1681 grid = QGridLayout()
1683 grid.addWidget(QLabel(_('Password')), 1, 0)
1684 grid.addWidget(pw, 1, 1)
1685 vbox.addLayout(grid)
1687 vbox.addLayout(ok_cancel_buttons(d))
1690 run_hook('password_dialog', pw, grid, 1)
1691 if not d.exec_(): return
1692 return unicode(pw.text())
1701 def tx_from_text(self, txt):
1702 "json or raw hexadecimal"
1705 tx = Transaction(txt)
1711 tx_dict = json.loads(str(txt))
1712 assert "hex" in tx_dict.keys()
1713 assert "complete" in tx_dict.keys()
1714 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1715 if not tx_dict["complete"]:
1716 assert "input_info" in tx_dict.keys()
1717 input_info = json.loads(tx_dict['input_info'])
1718 tx.add_input_info(input_info)
1723 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1727 def read_tx_from_file(self):
1728 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1732 with open(fileName, "r") as f:
1733 file_content = f.read()
1734 except (ValueError, IOError, os.error), reason:
1735 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1737 return self.tx_from_text(file_content)
1741 def sign_raw_transaction(self, tx, input_info, password):
1742 self.wallet.signrawtransaction(tx, input_info, [], password)
1744 def do_process_from_text(self):
1745 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1748 tx = self.tx_from_text(text)
1750 self.show_transaction(tx)
1752 def do_process_from_file(self):
1753 tx = self.read_tx_from_file()
1755 self.show_transaction(tx)
1757 def do_process_from_csvReader(self, csvReader):
1760 for row in csvReader:
1762 amount = float(row[1])
1763 amount = int(100000000*amount)
1764 outputs.append((address, amount))
1765 except (ValueError, IOError, os.error), reason:
1766 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1770 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1771 except BaseException, e:
1772 self.show_message(str(e))
1775 self.show_transaction(tx)
1777 def do_process_from_csv_file(self):
1778 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1782 with open(fileName, "r") as f:
1783 csvReader = csv.reader(f)
1784 self.do_process_from_csvReader(csvReader)
1785 except (ValueError, IOError, os.error), reason:
1786 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1789 def do_process_from_csv_text(self):
1790 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1791 + _("Format: address, amount. One output per line"), _("Load CSV"))
1794 f = StringIO.StringIO(text)
1795 csvReader = csv.reader(f)
1796 self.do_process_from_csvReader(csvReader)
1801 def do_export_privkeys(self, password):
1802 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.")))
1805 select_export = _('Select file to export your private keys to')
1806 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1808 with open(fileName, "w+") as csvfile:
1809 transaction = csv.writer(csvfile)
1810 transaction.writerow(["address", "private_key"])
1812 addresses = self.wallet.addresses(True)
1814 for addr in addresses:
1815 pk = "".join(self.wallet.get_private_key(addr, password))
1816 transaction.writerow(["%34s"%addr,pk])
1818 self.show_message(_("Private keys exported."))
1820 except (IOError, os.error), reason:
1821 export_error_label = _("Electrum was unable to produce a private key-export.")
1822 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1824 except BaseException, e:
1825 self.show_message(str(e))
1829 def do_import_labels(self):
1830 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1831 if not labelsFile: return
1833 f = open(labelsFile, 'r')
1836 for key, value in json.loads(data).items():
1837 self.wallet.set_label(key, value)
1838 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1839 except (IOError, os.error), reason:
1840 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1843 def do_export_labels(self):
1844 labels = self.wallet.labels
1846 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1848 with open(fileName, 'w+') as f:
1849 json.dump(labels, f)
1850 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1851 except (IOError, os.error), reason:
1852 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1855 def do_export_history(self):
1856 from lite_window import csv_transaction
1857 csv_transaction(self.wallet)
1861 def do_import_privkey(self, password):
1862 if not self.wallet.imported_keys:
1863 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1864 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1865 + _('Are you sure you understand what you are doing?'), 3, 4)
1868 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1871 text = str(text).split()
1876 addr = self.wallet.import_key(key, password)
1877 except BaseException as e:
1883 addrlist.append(addr)
1885 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1887 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1888 self.update_receive_tab()
1889 self.update_history_tab()
1892 def settings_dialog(self):
1894 d.setWindowTitle(_('Electrum Settings'))
1896 vbox = QVBoxLayout()
1897 grid = QGridLayout()
1898 grid.setColumnStretch(0,1)
1900 nz_label = QLabel(_('Display zeros') + ':')
1901 grid.addWidget(nz_label, 0, 0)
1902 nz_e = AmountEdit(None,True)
1903 nz_e.setText("%d"% self.num_zeros)
1904 grid.addWidget(nz_e, 0, 1)
1905 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1906 grid.addWidget(HelpButton(msg), 0, 2)
1907 if not self.config.is_modifiable('num_zeros'):
1908 for w in [nz_e, nz_label]: w.setEnabled(False)
1910 lang_label=QLabel(_('Language') + ':')
1911 grid.addWidget(lang_label, 1, 0)
1912 lang_combo = QComboBox()
1913 from electrum.i18n import languages
1914 lang_combo.addItems(languages.values())
1916 index = languages.keys().index(self.config.get("language",''))
1919 lang_combo.setCurrentIndex(index)
1920 grid.addWidget(lang_combo, 1, 1)
1921 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1922 if not self.config.is_modifiable('language'):
1923 for w in [lang_combo, lang_label]: w.setEnabled(False)
1926 fee_label = QLabel(_('Transaction fee') + ':')
1927 grid.addWidget(fee_label, 2, 0)
1928 fee_e = AmountEdit(self.base_unit)
1929 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1930 grid.addWidget(fee_e, 2, 1)
1931 msg = _('Fee per kilobyte of transaction.') + ' ' \
1932 + _('Recommended value') + ': ' + self.format_amount(50000)
1933 grid.addWidget(HelpButton(msg), 2, 2)
1934 if not self.config.is_modifiable('fee_per_kb'):
1935 for w in [fee_e, fee_label]: w.setEnabled(False)
1937 units = ['BTC', 'mBTC']
1938 unit_label = QLabel(_('Base unit') + ':')
1939 grid.addWidget(unit_label, 3, 0)
1940 unit_combo = QComboBox()
1941 unit_combo.addItems(units)
1942 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1943 grid.addWidget(unit_combo, 3, 1)
1944 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
1945 + '\n1BTC=1000mBTC.\n' \
1946 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
1948 usechange_cb = QCheckBox(_('Use change addresses'))
1949 usechange_cb.setChecked(self.wallet.use_change)
1950 grid.addWidget(usechange_cb, 4, 0)
1951 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
1952 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1954 grid.setRowStretch(5,1)
1956 vbox.addLayout(grid)
1957 vbox.addLayout(ok_cancel_buttons(d))
1961 if not d.exec_(): return
1963 fee = unicode(fee_e.text())
1965 fee = self.read_amount(fee)
1967 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1970 self.wallet.set_fee(fee)
1972 nz = unicode(nz_e.text())
1977 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1980 if self.num_zeros != nz:
1982 self.config.set_key('num_zeros', nz, True)
1983 self.update_history_tab()
1984 self.update_receive_tab()
1986 usechange_result = usechange_cb.isChecked()
1987 if self.wallet.use_change != usechange_result:
1988 self.wallet.use_change = usechange_result
1989 self.config.set_key('use_change', self.wallet.use_change, True)
1991 unit_result = units[unit_combo.currentIndex()]
1992 if self.base_unit() != unit_result:
1993 self.decimal_point = 8 if unit_result == 'BTC' else 5
1994 self.config.set_key('decimal_point', self.decimal_point, True)
1995 self.update_history_tab()
1996 self.update_status()
1998 need_restart = False
2000 lang_request = languages.keys()[lang_combo.currentIndex()]
2001 if lang_request != self.config.get('language'):
2002 self.config.set_key("language", lang_request, True)
2005 run_hook('close_settings_dialog')
2008 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2011 def run_network_dialog(self):
2012 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2014 def closeEvent(self, event):
2016 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2017 self.save_column_widths()
2018 self.config.set_key("console-history", self.console.history[-50:], True)
2023 def plugins_dialog(self):
2024 from electrum.plugins import plugins
2027 d.setWindowTitle(_('Electrum Plugins'))
2030 vbox = QVBoxLayout(d)
2033 scroll = QScrollArea()
2034 scroll.setEnabled(True)
2035 scroll.setWidgetResizable(True)
2036 scroll.setMinimumSize(400,250)
2037 vbox.addWidget(scroll)
2041 w.setMinimumHeight(len(plugins)*35)
2043 grid = QGridLayout()
2044 grid.setColumnStretch(0,1)
2047 def mk_toggle(cb, p):
2048 return lambda: cb.setChecked(p.toggle())
2049 for i, p in enumerate(plugins):
2051 cb = QCheckBox(p.fullname())
2052 cb.setDisabled(not p.is_available())
2053 cb.setChecked(p.is_enabled())
2054 cb.clicked.connect(mk_toggle(cb,p))
2055 grid.addWidget(cb, i, 0)
2056 if p.requires_settings():
2057 b = EnterButton(_('Settings'), p.settings_dialog)
2058 b.setEnabled( p.is_enabled() )
2059 grid.addWidget(b, i, 1)
2060 grid.addWidget(HelpButton(p.description()), i, 2)
2062 print_msg(_("Error: cannot display plugin"), p)
2063 traceback.print_exc(file=sys.stdout)
2064 grid.setRowStretch(i+1,1)
2066 vbox.addLayout(close_button(d))
2071 def show_account_details(self, k):
2073 d.setWindowTitle(_('Account Details'))
2076 vbox = QVBoxLayout(d)
2077 roots = self.wallet.get_roots(k)
2079 name = self.wallet.get_account_name(k)
2080 label = QLabel('Name: ' + name)
2081 vbox.addWidget(label)
2083 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2084 vbox.addWidget(QLabel('Type: ' + acctype))
2086 label = QLabel('Derivation: ' + k)
2087 vbox.addWidget(label)
2090 # mpk = self.wallet.master_public_keys[root]
2091 # text = QTextEdit()
2092 # text.setReadOnly(True)
2093 # text.setMaximumHeight(120)
2094 # text.setText(repr(mpk))
2095 # vbox.addWidget(text)
2097 vbox.addLayout(close_button(d))