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")
539 c, u = self.wallet.get_account_balance(self.current_account)
540 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
541 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
544 run_hook('set_quote_text', c+u, r)
547 text += " (%s)"%quote
549 self.tray.setToolTip(text)
550 icon = QIcon(":icons/status_connected.png")
552 text = _("Not connected")
553 icon = QIcon(":icons/status_disconnected.png")
555 self.balance_label.setText(text)
556 self.status_button.setIcon( icon )
559 def update_wallet(self):
561 if self.wallet.up_to_date or not self.network.interface.is_connected:
562 self.update_history_tab()
563 self.update_receive_tab()
564 self.update_contacts_tab()
565 self.update_completions()
568 def create_history_tab(self):
569 self.history_list = l = MyTreeWidget(self)
571 for i,width in enumerate(self.column_widths['history']):
572 l.setColumnWidth(i, width)
573 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
574 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
575 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
577 l.customContextMenuRequested.connect(self.create_history_menu)
581 def create_history_menu(self, position):
582 self.history_list.selectedIndexes()
583 item = self.history_list.currentItem()
585 tx_hash = str(item.data(0, Qt.UserRole).toString())
586 if not tx_hash: return
588 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
589 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
590 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
591 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
594 def show_transaction(self, tx):
595 import transaction_dialog
596 d = transaction_dialog.TxDialog(tx, self)
599 def tx_label_clicked(self, item, column):
600 if column==2 and item.isSelected():
602 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
603 self.history_list.editItem( item, column )
604 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
607 def tx_label_changed(self, item, column):
611 tx_hash = str(item.data(0, Qt.UserRole).toString())
612 tx = self.wallet.transactions.get(tx_hash)
613 text = unicode( item.text(2) )
614 self.wallet.set_label(tx_hash, text)
616 item.setForeground(2, QBrush(QColor('black')))
618 text = self.wallet.get_default_label(tx_hash)
619 item.setText(2, text)
620 item.setForeground(2, QBrush(QColor('gray')))
624 def edit_label(self, is_recv):
625 l = self.receive_list if is_recv else self.contacts_list
626 item = l.currentItem()
627 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
628 l.editItem( item, 1 )
629 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
633 def address_label_clicked(self, item, column, l, column_addr, column_label):
634 if column == column_label and item.isSelected():
635 is_editable = item.data(0, 32).toBool()
638 addr = unicode( item.text(column_addr) )
639 label = unicode( item.text(column_label) )
640 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
641 l.editItem( item, column )
642 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
645 def address_label_changed(self, item, column, l, column_addr, column_label):
646 if column == column_label:
647 addr = unicode( item.text(column_addr) )
648 text = unicode( item.text(column_label) )
649 is_editable = item.data(0, 32).toBool()
653 changed = self.wallet.set_label(addr, text)
655 self.update_history_tab()
656 self.update_completions()
658 self.current_item_changed(item)
660 run_hook('item_changed', item, column)
663 def current_item_changed(self, a):
664 run_hook('current_item_changed', a)
668 def update_history_tab(self):
670 self.history_list.clear()
671 for item in self.wallet.get_tx_history(self.current_account):
672 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
675 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
677 time_str = _("unknown")
680 time_str = 'unverified'
681 icon = QIcon(":icons/unconfirmed.png")
684 icon = QIcon(":icons/unconfirmed.png")
686 icon = QIcon(":icons/clock%d.png"%conf)
688 icon = QIcon(":icons/confirmed.png")
690 if value is not None:
691 v_str = self.format_amount(value, True, whitespaces=True)
695 balance_str = self.format_amount(balance, whitespaces=True)
698 label, is_default_label = self.wallet.get_label(tx_hash)
700 label = _('Pruned transaction outputs')
701 is_default_label = False
703 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
704 item.setFont(2, QFont(MONOSPACE_FONT))
705 item.setFont(3, QFont(MONOSPACE_FONT))
706 item.setFont(4, QFont(MONOSPACE_FONT))
708 item.setForeground(3, QBrush(QColor("#BC1E1E")))
710 item.setData(0, Qt.UserRole, tx_hash)
711 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
713 item.setForeground(2, QBrush(QColor('grey')))
715 item.setIcon(0, icon)
716 self.history_list.insertTopLevelItem(0,item)
719 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
722 def create_send_tab(self):
727 grid.setColumnMinimumWidth(3,300)
728 grid.setColumnStretch(5,1)
731 self.payto_e = QLineEdit()
732 grid.addWidget(QLabel(_('Pay to')), 1, 0)
733 grid.addWidget(self.payto_e, 1, 1, 1, 3)
735 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)
737 completer = QCompleter()
738 completer.setCaseSensitivity(False)
739 self.payto_e.setCompleter(completer)
740 completer.setModel(self.completions)
742 self.message_e = QLineEdit()
743 grid.addWidget(QLabel(_('Description')), 2, 0)
744 grid.addWidget(self.message_e, 2, 1, 1, 3)
745 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)
747 self.amount_e = AmountEdit(self.base_unit)
748 grid.addWidget(QLabel(_('Amount')), 3, 0)
749 grid.addWidget(self.amount_e, 3, 1, 1, 2)
750 grid.addWidget(HelpButton(
751 _('Amount to be sent.') + '\n\n' \
752 + _('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.') \
753 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
755 self.fee_e = AmountEdit(self.base_unit)
756 grid.addWidget(QLabel(_('Fee')), 4, 0)
757 grid.addWidget(self.fee_e, 4, 1, 1, 2)
758 grid.addWidget(HelpButton(
759 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
760 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
761 + _('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)
764 self.send_button = EnterButton(_("Send"), self.do_send)
765 grid.addWidget(self.send_button, 6, 1)
767 b = EnterButton(_("Clear"),self.do_clear)
768 grid.addWidget(b, 6, 2)
770 self.payto_sig = QLabel('')
771 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
773 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
774 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
783 def entry_changed( is_fee ):
784 self.funds_error = False
786 if self.amount_e.is_shortcut:
787 self.amount_e.is_shortcut = False
788 c, u = self.wallet.get_account_balance(self.current_account)
789 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
790 fee = self.wallet.estimated_fee(inputs)
792 self.amount_e.setText( self.format_amount(amount) )
793 self.fee_e.setText( self.format_amount( fee ) )
796 amount = self.read_amount(str(self.amount_e.text()))
797 fee = self.read_amount(str(self.fee_e.text()))
799 if not is_fee: fee = None
802 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
804 self.fee_e.setText( self.format_amount( fee ) )
807 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
811 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
812 self.funds_error = True
813 text = _( "Not enough funds" )
814 c, u = self.wallet.get_frozen_balance()
815 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
817 self.statusBar().showMessage(text)
818 self.amount_e.setPalette(palette)
819 self.fee_e.setPalette(palette)
821 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
822 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
824 run_hook('create_send_tab', grid)
828 def update_completions(self):
830 for addr,label in self.wallet.labels.items():
831 if addr in self.wallet.addressbook:
832 l.append( label + ' <' + addr + '>')
834 run_hook('update_completions', l)
835 self.completions.setStringList(l)
839 return lambda s, *args: s.do_protect(func, args)
844 label = unicode( self.message_e.text() )
845 r = unicode( self.payto_e.text() )
848 # label or alias, with address in brackets
849 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
850 to_address = m.group(2) if m else r
852 if not is_valid(to_address):
853 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
857 amount = self.read_amount(unicode( self.amount_e.text()))
859 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
862 fee = self.read_amount(unicode( self.fee_e.text()))
864 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
867 confirm_amount = self.config.get('confirm_amount', 100000000)
868 if amount >= confirm_amount:
869 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
872 self.send_tx(to_address, amount, fee, label)
876 def send_tx(self, to_address, amount, fee, label, password):
879 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
880 except BaseException, e:
881 traceback.print_exc(file=sys.stdout)
882 self.show_message(str(e))
885 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
886 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
890 self.wallet.set_label(tx.hash(), label)
893 h = self.wallet.send_tx(tx)
894 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
895 status, msg = self.wallet.receive_tx( h )
897 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
899 self.update_contacts_tab()
901 QMessageBox.warning(self, _('Error'), msg, _('OK'))
903 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
905 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
906 with open(fileName,'w') as f:
907 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
908 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
910 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
912 # add recipient to addressbook
913 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
914 self.wallet.addressbook.append(to_address)
919 def set_url(self, url):
920 address, amount, label, message, signature, identity, url = util.parse_url(url)
922 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
925 self.mini.set_payment_fields(address, amount)
927 if label and self.wallet.labels.get(address) != label:
928 if self.question('Give label "%s" to address %s ?'%(label,address)):
929 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
930 self.wallet.addressbook.append(address)
931 self.wallet.set_label(address, label)
933 run_hook('set_url', url, self.show_message, self.question)
935 self.tabs.setCurrentIndex(1)
936 label = self.wallet.labels.get(address)
937 m_addr = label + ' <'+ address +'>' if label else address
938 self.payto_e.setText(m_addr)
940 self.message_e.setText(message)
942 self.amount_e.setText(amount)
945 self.set_frozen(self.payto_e,True)
946 self.set_frozen(self.amount_e,True)
947 self.set_frozen(self.message_e,True)
948 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
950 self.payto_sig.setVisible(False)
953 self.payto_sig.setVisible(False)
954 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
956 self.set_frozen(e,False)
959 def set_frozen(self,entry,frozen):
961 entry.setReadOnly(True)
962 entry.setFrame(False)
964 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
965 entry.setPalette(palette)
967 entry.setReadOnly(False)
970 palette.setColor(entry.backgroundRole(), QColor('white'))
971 entry.setPalette(palette)
974 def toggle_freeze(self,addr):
976 if addr in self.wallet.frozen_addresses:
977 self.wallet.unfreeze(addr)
979 self.wallet.freeze(addr)
980 self.update_receive_tab()
982 def toggle_priority(self,addr):
984 if addr in self.wallet.prioritized_addresses:
985 self.wallet.unprioritize(addr)
987 self.wallet.prioritize(addr)
988 self.update_receive_tab()
991 def create_list_tab(self, headers):
992 "generic tab creation method"
993 l = MyTreeWidget(self)
994 l.setColumnCount( len(headers) )
995 l.setHeaderLabels( headers )
1005 vbox.addWidget(buttons)
1007 hbox = QHBoxLayout()
1010 buttons.setLayout(hbox)
1015 def create_receive_tab(self):
1016 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1017 l.setContextMenuPolicy(Qt.CustomContextMenu)
1018 l.customContextMenuRequested.connect(self.create_receive_menu)
1019 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1020 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1021 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1022 self.receive_list = l
1023 self.receive_buttons_hbox = hbox
1030 def save_column_widths(self):
1032 for i in range(self.receive_list.columnCount() -1):
1033 widths.append(self.receive_list.columnWidth(i))
1034 self.column_widths["receive"][1] = widths
1036 self.column_widths["history"] = []
1037 for i in range(self.history_list.columnCount() - 1):
1038 self.column_widths["history"].append(self.history_list.columnWidth(i))
1040 self.column_widths["contacts"] = []
1041 for i in range(self.contacts_list.columnCount() - 1):
1042 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1044 self.config.set_key("column_widths", self.column_widths, True)
1047 def create_contacts_tab(self):
1048 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1049 l.setContextMenuPolicy(Qt.CustomContextMenu)
1050 l.customContextMenuRequested.connect(self.create_contact_menu)
1051 for i,width in enumerate(self.column_widths['contacts']):
1052 l.setColumnWidth(i, width)
1054 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1055 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1056 self.contacts_list = l
1057 self.contacts_buttons_hbox = hbox
1062 def delete_imported_key(self, addr):
1063 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1064 self.wallet.delete_imported_key(addr)
1065 self.update_receive_tab()
1066 self.update_history_tab()
1068 def edit_account_label(self, k):
1069 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':')
1071 label = unicode(text)
1072 self.wallet.set_label(k,label)
1073 self.update_receive_tab()
1075 def create_account_menu(self, position, k, item):
1077 if item.isExpanded():
1078 menu.addAction(_("Minimize"), lambda: item.setExpanded(False))
1080 menu.addAction(_("Maximize"), lambda: item.setExpanded(True))
1081 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1082 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1083 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1085 def create_receive_menu(self, position):
1086 # fixme: this function apparently has a side effect.
1087 # if it is not called the menu pops up several times
1088 #self.receive_list.selectedIndexes()
1090 item = self.receive_list.itemAt(position)
1093 addr = unicode(item.text(0))
1094 if not is_valid(addr):
1095 k = str(item.data(0,32).toString())
1097 self.create_account_menu(position, k, item)
1099 item.setExpanded(not item.isExpanded())
1103 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1104 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1105 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1106 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1107 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1108 if addr in self.wallet.imported_keys:
1109 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1111 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1112 menu.addAction(t, lambda: self.toggle_freeze(addr))
1113 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1114 menu.addAction(t, lambda: self.toggle_priority(addr))
1116 run_hook('receive_menu', menu)
1117 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1120 def payto(self, addr):
1122 label = self.wallet.labels.get(addr)
1123 m_addr = label + ' <' + addr + '>' if label else addr
1124 self.tabs.setCurrentIndex(1)
1125 self.payto_e.setText(m_addr)
1126 self.amount_e.setFocus()
1129 def delete_contact(self, x):
1130 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1131 self.wallet.delete_contact(x)
1132 self.wallet.set_label(x, None)
1133 self.update_history_tab()
1134 self.update_contacts_tab()
1135 self.update_completions()
1138 def create_contact_menu(self, position):
1139 item = self.contacts_list.itemAt(position)
1141 addr = unicode(item.text(0))
1142 label = unicode(item.text(1))
1143 is_editable = item.data(0,32).toBool()
1144 payto_addr = item.data(0,33).toString()
1146 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1147 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1148 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1150 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1151 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1153 run_hook('create_contact_menu', menu, item)
1154 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1157 def update_receive_item(self, item):
1158 item.setFont(0, QFont(MONOSPACE_FONT))
1159 address = str(item.data(0,0).toString())
1160 label = self.wallet.labels.get(address,'')
1161 item.setData(1,0,label)
1162 item.setData(0,32, True) # is editable
1164 run_hook('update_receive_item', address, item)
1166 c, u = self.wallet.get_addr_balance(address)
1167 balance = self.format_amount(c + u)
1168 item.setData(2,0,balance)
1170 if address in self.wallet.frozen_addresses:
1171 item.setBackgroundColor(0, QColor('lightblue'))
1172 elif address in self.wallet.prioritized_addresses:
1173 item.setBackgroundColor(0, QColor('lightgreen'))
1176 def update_receive_tab(self):
1177 l = self.receive_list
1180 l.setColumnHidden(2, False)
1181 l.setColumnHidden(3, False)
1182 for i,width in enumerate(self.column_widths['receive'][1]):
1183 l.setColumnWidth(i, width)
1185 if self.current_account is None:
1186 account_items = self.wallet.accounts.items()
1187 elif self.current_account != -1:
1188 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1192 for k, account in account_items:
1193 name = self.wallet.get_account_name(k)
1194 c,u = self.wallet.get_account_balance(k)
1195 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1196 l.addTopLevelItem(account_item)
1197 account_item.setExpanded(True)
1198 account_item.setData(0, 32, k)
1200 if not self.wallet.is_seeded(k):
1201 icon = QIcon(":icons/key.png")
1202 account_item.setIcon(0, icon)
1204 for is_change in ([0,1]):
1205 name = _("Receiving") if not is_change else _("Change")
1206 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1207 account_item.addChild(seq_item)
1208 if not is_change: seq_item.setExpanded(True)
1213 for address in account.get_addresses(is_change):
1214 h = self.wallet.history.get(address,[])
1218 if gap > self.wallet.gap_limit:
1223 num_tx = '*' if h == ['*'] else "%d"%len(h)
1224 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1225 self.update_receive_item(item)
1227 item.setBackgroundColor(1, QColor('red'))
1228 seq_item.addChild(item)
1231 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1232 c,u = self.wallet.get_imported_balance()
1233 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1234 l.addTopLevelItem(account_item)
1235 account_item.setExpanded(True)
1236 for address in self.wallet.imported_keys.keys():
1237 item = QTreeWidgetItem( [ address, '', '', ''] )
1238 self.update_receive_item(item)
1239 account_item.addChild(item)
1242 # we use column 1 because column 0 may be hidden
1243 l.setCurrentItem(l.topLevelItem(0),1)
1246 def update_contacts_tab(self):
1247 l = self.contacts_list
1250 for address in self.wallet.addressbook:
1251 label = self.wallet.labels.get(address,'')
1252 n = self.wallet.get_num_tx(address)
1253 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1254 item.setFont(0, QFont(MONOSPACE_FONT))
1255 # 32 = label can be edited (bool)
1256 item.setData(0,32, True)
1258 item.setData(0,33, address)
1259 l.addTopLevelItem(item)
1261 run_hook('update_contacts_tab', l)
1262 l.setCurrentItem(l.topLevelItem(0))
1266 def create_console_tab(self):
1267 from console import Console
1268 self.console = console = Console()
1272 def update_console(self):
1273 console = self.console
1274 console.history = self.config.get("console-history",[])
1275 console.history_index = len(console.history)
1277 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1278 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1280 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1282 def mkfunc(f, method):
1283 return lambda *args: apply( f, (method, args, self.password_dialog ))
1285 if m[0]=='_' or m=='wallet' or m == 'interface': continue
1286 methods[m] = mkfunc(c._run, m)
1288 console.updateNamespace(methods)
1291 def change_account(self,s):
1292 if s == _("All accounts"):
1293 self.current_account = None
1295 accounts = self.wallet.get_account_names()
1296 for k, v in accounts.items():
1298 self.current_account = k
1299 self.update_history_tab()
1300 self.update_status()
1301 self.update_receive_tab()
1303 def create_status_bar(self):
1306 sb.setFixedHeight(35)
1307 qtVersion = qVersion()
1309 self.balance_label = QLabel("")
1310 sb.addWidget(self.balance_label)
1312 from version_getter import UpdateLabel
1313 self.updatelabel = UpdateLabel(self.config, sb)
1315 self.account_selector = QComboBox()
1316 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1317 sb.addPermanentWidget(self.account_selector)
1319 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1320 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1322 self.lock_icon = QIcon()
1323 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1324 sb.addPermanentWidget( self.password_button )
1326 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1327 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1328 sb.addPermanentWidget( self.seed_button )
1329 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1330 sb.addPermanentWidget( self.status_button )
1332 run_hook('create_status_bar', (sb,))
1334 self.setStatusBar(sb)
1337 def update_lock_icon(self):
1338 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1339 self.password_button.setIcon( icon )
1342 def update_buttons_on_seed(self):
1343 if not self.wallet.is_watching_only():
1344 self.seed_button.show()
1345 self.password_button.show()
1346 self.send_button.setText(_("Send"))
1348 self.password_button.hide()
1349 self.seed_button.hide()
1350 self.send_button.setText(_("Create unsigned transaction"))
1353 def change_password_dialog(self):
1354 from password_dialog import PasswordDialog
1355 d = PasswordDialog(self.wallet, self)
1357 self.update_lock_icon()
1360 def new_contact_dialog(self):
1361 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1362 address = unicode(text)
1364 if is_valid(address):
1365 self.wallet.add_contact(address)
1366 self.update_contacts_tab()
1367 self.update_history_tab()
1368 self.update_completions()
1370 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1373 def new_account_dialog(self):
1375 dialog = QDialog(self)
1377 dialog.setWindowTitle(_("New Account"))
1379 addr = self.wallet.new_account_address()
1380 vbox = QVBoxLayout()
1381 msg = _("Electrum considers that an account exists only if it contains bitcoins.") + '\n' \
1382 + _("To create a new account, please send coins to the first address of that account.") + '\n' \
1383 + _("Note: you will need to wait for 2 confirmations before the account is created.")
1384 vbox.addWidget(QLabel(msg))
1385 vbox.addWidget(QLabel(_('Address')+':'))
1390 vbox.addLayout(ok_cancel_buttons(dialog))
1391 dialog.setLayout(vbox)
1398 def show_master_public_key_old(self):
1399 dialog = QDialog(self)
1401 dialog.setWindowTitle(_("Master Public Key"))
1403 main_text = QTextEdit()
1404 main_text.setText(self.wallet.get_master_public_key())
1405 main_text.setReadOnly(True)
1406 main_text.setMaximumHeight(170)
1407 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1409 ok_button = QPushButton(_("OK"))
1410 ok_button.setDefault(True)
1411 ok_button.clicked.connect(dialog.accept)
1413 main_layout = QGridLayout()
1414 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1416 main_layout.addWidget(main_text, 1, 0)
1417 main_layout.addWidget(qrw, 1, 1 )
1419 vbox.addLayout(close_button(dialog))
1420 dialog.setLayout(vbox)
1424 def show_master_public_key(self):
1426 if self.wallet.seed_version == 4:
1427 self.show_master_public_keys_old()
1430 dialog = QDialog(self)
1432 dialog.setWindowTitle(_("Master Public Keys"))
1434 chain_text = QTextEdit()
1435 chain_text.setReadOnly(True)
1436 chain_text.setMaximumHeight(170)
1437 chain_qrw = QRCodeWidget()
1439 mpk_text = QTextEdit()
1440 mpk_text.setReadOnly(True)
1441 mpk_text.setMaximumHeight(170)
1442 mpk_qrw = QRCodeWidget()
1444 main_layout = QGridLayout()
1446 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1447 main_layout.addWidget(mpk_text, 1, 1)
1448 main_layout.addWidget(mpk_qrw, 1, 2)
1450 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1451 main_layout.addWidget(chain_text, 2, 1)
1452 main_layout.addWidget(chain_qrw, 2, 2)
1455 c, K, cK = self.wallet.master_public_keys[str(key)]
1456 chain_text.setText(c)
1457 chain_qrw.set_addr(c)
1458 chain_qrw.update_qr()
1463 key_selector = QComboBox()
1464 keys = sorted(self.wallet.master_public_keys.keys())
1465 key_selector.addItems(keys)
1467 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1468 main_layout.addWidget(key_selector, 0, 1)
1469 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1473 vbox = QVBoxLayout()
1474 vbox.addLayout(main_layout)
1475 vbox.addLayout(close_button(dialog))
1477 dialog.setLayout(vbox)
1482 def show_seed_dialog(self, password):
1483 if self.wallet.is_watching_only():
1484 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1487 if self.wallet.seed:
1489 seed = self.wallet.decode_seed(password)
1491 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1493 from seed_dialog import SeedDialog
1494 d = SeedDialog(self, seed, self.wallet.imported_keys)
1498 for k in self.wallet.master_private_keys.keys():
1499 pk = self.wallet.get_master_private_key(k, password)
1501 from seed_dialog import PrivateKeysDialog
1502 d = PrivateKeysDialog(self,l)
1509 def show_qrcode(self, data, title = _("QR code")):
1513 d.setWindowTitle(title)
1514 d.setMinimumSize(270, 300)
1515 vbox = QVBoxLayout()
1516 qrw = QRCodeWidget(data)
1517 vbox.addWidget(qrw, 1)
1518 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1519 hbox = QHBoxLayout()
1523 filename = "qrcode.bmp"
1524 bmp.save_qrcode(qrw.qr, filename)
1525 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1527 b = QPushButton(_("Save"))
1529 b.clicked.connect(print_qr)
1531 b = QPushButton(_("Close"))
1533 b.clicked.connect(d.accept)
1536 vbox.addLayout(hbox)
1541 def do_protect(self, func, args):
1542 if self.wallet.use_encryption:
1543 password = self.password_dialog()
1549 if args != (False,):
1550 args = (self,) + args + (password,)
1552 args = (self,password)
1557 def show_private_key(self, address, password):
1558 if not address: return
1560 pk_list = self.wallet.get_private_key(address, password)
1561 except BaseException, e:
1562 self.show_message(str(e))
1564 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1568 def do_sign(self, address, message, signature, password):
1569 message = unicode(message.toPlainText())
1570 message = message.encode('utf-8')
1572 sig = self.wallet.sign_message(str(address.text()), message, password)
1573 signature.setText(sig)
1574 except BaseException, e:
1575 self.show_message(str(e))
1577 def sign_message(self, address):
1578 if not address: return
1581 d.setWindowTitle(_('Sign Message'))
1582 d.setMinimumSize(410, 290)
1584 tab_widget = QTabWidget()
1586 layout = QGridLayout(tab)
1588 sign_address = QLineEdit()
1590 sign_address.setText(address)
1591 layout.addWidget(QLabel(_('Address')), 1, 0)
1592 layout.addWidget(sign_address, 1, 1)
1594 sign_message = QTextEdit()
1595 layout.addWidget(QLabel(_('Message')), 2, 0)
1596 layout.addWidget(sign_message, 2, 1)
1597 layout.setRowStretch(2,3)
1599 sign_signature = QTextEdit()
1600 layout.addWidget(QLabel(_('Signature')), 3, 0)
1601 layout.addWidget(sign_signature, 3, 1)
1602 layout.setRowStretch(3,1)
1605 hbox = QHBoxLayout()
1606 b = QPushButton(_("Sign"))
1608 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1609 b = QPushButton(_("Close"))
1610 b.clicked.connect(d.accept)
1612 layout.addLayout(hbox, 4, 1)
1613 tab_widget.addTab(tab, _("Sign"))
1617 layout = QGridLayout(tab)
1619 verify_address = QLineEdit()
1620 layout.addWidget(QLabel(_('Address')), 1, 0)
1621 layout.addWidget(verify_address, 1, 1)
1623 verify_message = QTextEdit()
1624 layout.addWidget(QLabel(_('Message')), 2, 0)
1625 layout.addWidget(verify_message, 2, 1)
1626 layout.setRowStretch(2,3)
1628 verify_signature = QTextEdit()
1629 layout.addWidget(QLabel(_('Signature')), 3, 0)
1630 layout.addWidget(verify_signature, 3, 1)
1631 layout.setRowStretch(3,1)
1634 message = unicode(verify_message.toPlainText())
1635 message = message.encode('utf-8')
1636 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1637 self.show_message(_("Signature verified"))
1639 self.show_message(_("Error: wrong signature"))
1641 hbox = QHBoxLayout()
1642 b = QPushButton(_("Verify"))
1643 b.clicked.connect(do_verify)
1645 b = QPushButton(_("Close"))
1646 b.clicked.connect(d.accept)
1648 layout.addLayout(hbox, 4, 1)
1649 tab_widget.addTab(tab, _("Verify"))
1651 vbox = QVBoxLayout()
1652 vbox.addWidget(tab_widget)
1659 def question(self, msg):
1660 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1662 def show_message(self, msg):
1663 QMessageBox.information(self, _('Message'), msg, _('OK'))
1665 def password_dialog(self ):
1672 vbox = QVBoxLayout()
1673 msg = _('Please enter your password')
1674 vbox.addWidget(QLabel(msg))
1676 grid = QGridLayout()
1678 grid.addWidget(QLabel(_('Password')), 1, 0)
1679 grid.addWidget(pw, 1, 1)
1680 vbox.addLayout(grid)
1682 vbox.addLayout(ok_cancel_buttons(d))
1685 run_hook('password_dialog', pw, grid, 1)
1686 if not d.exec_(): return
1687 return unicode(pw.text())
1696 def tx_from_text(self, txt):
1697 "json or raw hexadecimal"
1700 tx = Transaction(txt)
1706 tx_dict = json.loads(str(txt))
1707 assert "hex" in tx_dict.keys()
1708 assert "complete" in tx_dict.keys()
1709 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1710 if not tx_dict["complete"]:
1711 assert "input_info" in tx_dict.keys()
1712 input_info = json.loads(tx_dict['input_info'])
1713 tx.add_input_info(input_info)
1718 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1722 def read_tx_from_file(self):
1723 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1727 with open(fileName, "r") as f:
1728 file_content = f.read()
1729 except (ValueError, IOError, os.error), reason:
1730 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1732 return self.tx_from_text(file_content)
1736 def sign_raw_transaction(self, tx, input_info, password):
1737 self.wallet.signrawtransaction(tx, input_info, [], password)
1739 def do_process_from_text(self):
1740 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1743 tx = self.tx_from_text(text)
1745 self.show_transaction(tx)
1747 def do_process_from_file(self):
1748 tx = self.read_tx_from_file()
1750 self.show_transaction(tx)
1752 def do_process_from_csvReader(self, csvReader):
1755 for row in csvReader:
1757 amount = float(row[1])
1758 amount = int(100000000*amount)
1759 outputs.append((address, amount))
1760 except (ValueError, IOError, os.error), reason:
1761 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1765 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1766 except BaseException, e:
1767 self.show_message(str(e))
1770 self.show_transaction(tx)
1772 def do_process_from_csv_file(self):
1773 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1777 with open(fileName, "r") as f:
1778 csvReader = csv.reader(f)
1779 self.do_process_from_csvReader(csvReader)
1780 except (ValueError, IOError, os.error), reason:
1781 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1784 def do_process_from_csv_text(self):
1785 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1786 + _("Format: address, amount. One output per line"), _("Load CSV"))
1789 f = StringIO.StringIO(text)
1790 csvReader = csv.reader(f)
1791 self.do_process_from_csvReader(csvReader)
1796 def do_export_privkeys(self, password):
1797 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.")))
1800 select_export = _('Select file to export your private keys to')
1801 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1803 with open(fileName, "w+") as csvfile:
1804 transaction = csv.writer(csvfile)
1805 transaction.writerow(["address", "private_key"])
1807 addresses = self.wallet.addresses(True)
1809 for addr in addresses:
1810 pk = "".join(self.wallet.get_private_key(addr, password))
1811 transaction.writerow(["%34s"%addr,pk])
1813 self.show_message(_("Private keys exported."))
1815 except (IOError, os.error), reason:
1816 export_error_label = _("Electrum was unable to produce a private key-export.")
1817 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1819 except BaseException, e:
1820 self.show_message(str(e))
1824 def do_import_labels(self):
1825 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1826 if not labelsFile: return
1828 f = open(labelsFile, 'r')
1831 for key, value in json.loads(data).items():
1832 self.wallet.set_label(key, value)
1833 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1834 except (IOError, os.error), reason:
1835 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1838 def do_export_labels(self):
1839 labels = self.wallet.labels
1841 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1843 with open(fileName, 'w+') as f:
1844 json.dump(labels, f)
1845 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1846 except (IOError, os.error), reason:
1847 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1850 def do_export_history(self):
1851 from lite_window import csv_transaction
1852 csv_transaction(self.wallet)
1856 def do_import_privkey(self, password):
1857 if not self.wallet.imported_keys:
1858 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1859 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1860 + _('Are you sure you understand what you are doing?'), 3, 4)
1863 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1866 text = str(text).split()
1871 addr = self.wallet.import_key(key, password)
1872 except BaseException as e:
1878 addrlist.append(addr)
1880 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1882 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1883 self.update_receive_tab()
1884 self.update_history_tab()
1887 def settings_dialog(self):
1889 d.setWindowTitle(_('Electrum Settings'))
1891 vbox = QVBoxLayout()
1892 grid = QGridLayout()
1893 grid.setColumnStretch(0,1)
1895 nz_label = QLabel(_('Display zeros') + ':')
1896 grid.addWidget(nz_label, 0, 0)
1897 nz_e = AmountEdit(None,True)
1898 nz_e.setText("%d"% self.num_zeros)
1899 grid.addWidget(nz_e, 0, 1)
1900 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1901 grid.addWidget(HelpButton(msg), 0, 2)
1902 if not self.config.is_modifiable('num_zeros'):
1903 for w in [nz_e, nz_label]: w.setEnabled(False)
1905 lang_label=QLabel(_('Language') + ':')
1906 grid.addWidget(lang_label, 1, 0)
1907 lang_combo = QComboBox()
1908 from electrum.i18n import languages
1909 lang_combo.addItems(languages.values())
1911 index = languages.keys().index(self.config.get("language",''))
1914 lang_combo.setCurrentIndex(index)
1915 grid.addWidget(lang_combo, 1, 1)
1916 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1917 if not self.config.is_modifiable('language'):
1918 for w in [lang_combo, lang_label]: w.setEnabled(False)
1921 fee_label = QLabel(_('Transaction fee') + ':')
1922 grid.addWidget(fee_label, 2, 0)
1923 fee_e = AmountEdit(self.base_unit)
1924 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1925 grid.addWidget(fee_e, 2, 1)
1926 msg = _('Fee per kilobyte of transaction.') + ' ' \
1927 + _('Recommended value') + ': ' + self.format_amount(50000)
1928 grid.addWidget(HelpButton(msg), 2, 2)
1929 if not self.config.is_modifiable('fee_per_kb'):
1930 for w in [fee_e, fee_label]: w.setEnabled(False)
1932 units = ['BTC', 'mBTC']
1933 unit_label = QLabel(_('Base unit') + ':')
1934 grid.addWidget(unit_label, 3, 0)
1935 unit_combo = QComboBox()
1936 unit_combo.addItems(units)
1937 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1938 grid.addWidget(unit_combo, 3, 1)
1939 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
1940 + '\n1BTC=1000mBTC.\n' \
1941 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
1943 usechange_cb = QCheckBox(_('Use change addresses'))
1944 usechange_cb.setChecked(self.wallet.use_change)
1945 grid.addWidget(usechange_cb, 4, 0)
1946 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
1947 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1949 grid.setRowStretch(5,1)
1951 vbox.addLayout(grid)
1952 vbox.addLayout(ok_cancel_buttons(d))
1956 if not d.exec_(): return
1958 fee = unicode(fee_e.text())
1960 fee = self.read_amount(fee)
1962 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1965 self.wallet.set_fee(fee)
1967 nz = unicode(nz_e.text())
1972 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1975 if self.num_zeros != nz:
1977 self.config.set_key('num_zeros', nz, True)
1978 self.update_history_tab()
1979 self.update_receive_tab()
1981 usechange_result = usechange_cb.isChecked()
1982 if self.wallet.use_change != usechange_result:
1983 self.wallet.use_change = usechange_result
1984 self.config.set_key('use_change', self.wallet.use_change, True)
1986 unit_result = units[unit_combo.currentIndex()]
1987 if self.base_unit() != unit_result:
1988 self.decimal_point = 8 if unit_result == 'BTC' else 5
1989 self.config.set_key('decimal_point', self.decimal_point, True)
1990 self.update_history_tab()
1991 self.update_status()
1993 need_restart = False
1995 lang_request = languages.keys()[lang_combo.currentIndex()]
1996 if lang_request != self.config.get('language'):
1997 self.config.set_key("language", lang_request, True)
2000 run_hook('close_settings_dialog')
2003 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2006 def run_network_dialog(self):
2007 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2009 def closeEvent(self, event):
2011 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2012 self.save_column_widths()
2013 self.config.set_key("console-history", self.console.history[-50:], True)
2018 def plugins_dialog(self):
2019 from electrum.plugins import plugins
2022 d.setWindowTitle(_('Electrum Plugins'))
2025 vbox = QVBoxLayout(d)
2028 scroll = QScrollArea()
2029 scroll.setEnabled(True)
2030 scroll.setWidgetResizable(True)
2031 scroll.setMinimumSize(400,250)
2032 vbox.addWidget(scroll)
2036 w.setMinimumHeight(len(plugins)*35)
2038 grid = QGridLayout()
2039 grid.setColumnStretch(0,1)
2042 def mk_toggle(cb, p):
2043 return lambda: cb.setChecked(p.toggle())
2044 for i, p in enumerate(plugins):
2046 cb = QCheckBox(p.fullname())
2047 cb.setDisabled(not p.is_available())
2048 cb.setChecked(p.is_enabled())
2049 cb.clicked.connect(mk_toggle(cb,p))
2050 grid.addWidget(cb, i, 0)
2051 if p.requires_settings():
2052 b = EnterButton(_('Settings'), p.settings_dialog)
2053 b.setEnabled( p.is_enabled() )
2054 grid.addWidget(b, i, 1)
2055 grid.addWidget(HelpButton(p.description()), i, 2)
2057 print_msg(_("Error: cannot display plugin"), p)
2058 traceback.print_exc(file=sys.stdout)
2059 grid.setRowStretch(i+1,1)
2061 vbox.addLayout(close_button(d))
2066 def show_account_details(self, k):
2068 d.setWindowTitle(_('Account Details'))
2071 vbox = QVBoxLayout(d)
2072 roots = self.wallet.get_roots(k)
2074 name = self.wallet.get_account_name(k)
2075 label = QLabel('Name: ' + name)
2076 vbox.addWidget(label)
2078 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2079 vbox.addWidget(QLabel('Type: ' + acctype))
2081 label = QLabel('Derivation: ' + k)
2082 vbox.addWidget(label)
2085 # mpk = self.wallet.master_public_keys[root]
2086 # text = QTextEdit()
2087 # text.setReadOnly(True)
2088 # text.setMaximumHeight(120)
2089 # text.setText(repr(mpk))
2090 # vbox.addWidget(text)
2092 vbox.addLayout(close_button(d))