3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re, threading
20 from electrum.i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
28 from PyQt4.QtGui import *
29 from PyQt4.QtCore import *
30 import PyQt4.QtCore as QtCore
32 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
33 from electrum.plugins import run_hook
37 from electrum.wallet import format_satoshis
38 from electrum import Transaction
39 from electrum import mnemonic
40 from electrum import util, bitcoin, commands, Interface, Wallet
41 from electrum import SimpleConfig, Wallet, WalletStorage
44 from electrum import bmp, pyqrnative
46 from amountedit import AmountEdit
47 from network_dialog import NetworkDialog
48 from qrcodewidget import QRCodeWidget
50 from decimal import Decimal
58 if platform.system() == 'Windows':
59 MONOSPACE_FONT = 'Lucida Console'
60 elif platform.system() == 'Darwin':
61 MONOSPACE_FONT = 'Monaco'
63 MONOSPACE_FONT = 'monospace'
65 from electrum import ELECTRUM_VERSION
75 class StatusBarButton(QPushButton):
76 def __init__(self, icon, tooltip, func):
77 QPushButton.__init__(self, icon, '')
78 self.setToolTip(tooltip)
80 self.setMaximumWidth(25)
81 self.clicked.connect(func)
83 self.setIconSize(QSize(25,25))
85 def keyPressEvent(self, e):
86 if e.key() == QtCore.Qt.Key_Return:
98 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive":[[370], [370,200,130]] }
100 class ElectrumWindow(QMainWindow):
101 def changeEvent(self, event):
102 flags = self.windowFlags();
103 if event and event.type() == QtCore.QEvent.WindowStateChange:
104 if self.windowState() & QtCore.Qt.WindowMinimized:
105 self.build_menu(True)
106 # The only way to toggle the icon in the window managers taskbar is to use the Qt.Tooltip flag
107 # The problem is that it somehow creates an (in)visible window that will stay active and prevent
108 # Electrum from closing.
109 # As for now I have no clue how to implement a proper 'hide to tray' functionality.
110 # self.setWindowFlags(flags & ~Qt.ToolTip)
111 elif event.oldState() & QtCore.Qt.WindowMinimized:
112 self.build_menu(False)
113 #self.setWindowFlags(flags | Qt.ToolTip)
115 def build_menu(self, is_hidden = False):
117 if self.isMinimized():
118 m.addAction(_("Show"), self.showNormal)
120 m.addAction(_("Hide"), self.showMinimized)
123 m.addAction(_("Exit Electrum"), self.close)
124 self.tray.setContextMenu(m)
126 def tray_activated(self, reason):
127 if reason == QSystemTrayIcon.DoubleClick:
131 def __init__(self, config, network):
132 QMainWindow.__init__(self)
135 self.network = network
137 self._close_electrum = False
139 self.current_account = self.config.get("current_account", None)
141 self.icon = QIcon(':icons/electrum.png')
142 self.tray = QSystemTrayIcon(self.icon, self)
143 self.tray.setToolTip('Electrum')
144 self.tray.activated.connect(self.tray_activated)
148 self.create_status_bar()
150 self.need_update = threading.Event()
152 self.expert_mode = config.get('classic_expert_mode', False)
153 self.decimal_point = config.get('decimal_point', 8)
154 self.num_zeros = int(config.get('num_zeros',0))
156 set_language(config.get('language'))
158 self.funds_error = False
159 self.completions = QStringListModel()
161 self.tabs = tabs = QTabWidget(self)
162 self.column_widths = self.config.get("column_widths", default_column_widths )
163 tabs.addTab(self.create_history_tab(), _('History') )
164 tabs.addTab(self.create_send_tab(), _('Send') )
165 tabs.addTab(self.create_receive_tab(), _('Receive') )
166 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
167 tabs.addTab(self.create_console_tab(), _('Console') )
168 tabs.setMinimumSize(600, 400)
169 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
170 self.setCentralWidget(tabs)
172 g = self.config.get("winpos-qt",[100, 100, 840, 400])
173 self.setGeometry(g[0], g[1], g[2], g[3])
177 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
178 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
179 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
180 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
181 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
183 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
184 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
185 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
187 self.history_list.setFocus(True)
190 self.network.register_callback('updated', lambda: self.need_update.set())
191 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
192 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
193 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
194 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
195 # set initial message
196 self.console.showMessage(self.network.banner)
198 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
199 if platform.system() == 'Windows':
200 n = 3 if self.wallet.seed else 2
201 tabs.setCurrentIndex (n)
202 tabs.setCurrentIndex (0)
209 self.config.set_key('lite_mode', False, True)
214 self.config.set_key('lite_mode', True, True)
221 if not self.check_qt_version():
222 if self.config.get('lite_mode') is True:
223 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
224 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
225 self.config.set_key('lite_mode', False, True)
230 actuator = lite_window.MiniActuator(self)
232 # Should probably not modify the current path but instead
233 # change the behaviour of rsrc(...)
234 old_path = QDir.currentPath()
235 actuator.load_theme()
237 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
239 driver = lite_window.MiniDriver(self, self.mini)
241 # Reset path back to original value now that loading the GUI
243 QDir.setCurrent(old_path)
245 if self.config.get('lite_mode') is True:
251 def check_qt_version(self):
252 qtVersion = qVersion()
253 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
257 def load_wallet(self, wallet):
261 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
262 if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
263 self.setWindowTitle( title )
265 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
266 self.notify_transactions()
269 accounts = self.wallet.get_account_names()
270 self.account_selector.clear()
271 if len(accounts) > 1:
272 self.account_selector.addItems([_("All accounts")] + accounts.values())
273 self.account_selector.setCurrentIndex(0)
274 self.account_selector.show()
276 self.account_selector.hide()
278 self.new_account.setEnabled(self.wallet.seed_version>4)
280 self.update_lock_icon()
281 self.update_buttons_on_seed()
282 self.update_console()
284 run_hook('load_wallet', wallet)
287 def select_wallet_file(self):
288 wallet_folder = self.wallet.storage.path
289 re.sub("(\/\w*.dat)$", "", wallet_folder)
290 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
294 def open_wallet(self):
296 filename = self.select_wallet_file()
300 storage = WalletStorage({'wallet_path': filename})
301 if not storage.file_exists:
302 self.show_message("file not found "+ filename)
305 self.wallet.stop_threads()
308 wallet = Wallet(storage)
309 wallet.start_threads(self.network)
311 self.load_wallet(wallet)
315 def backup_wallet(self):
317 path = self.wallet.storage.path
318 wallet_folder = os.path.dirname(path)
319 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
320 new_filename = unicode(new_filename)
321 if not ok or not new_filename:
324 new_path = os.path.join(wallet_folder, new_filename)
327 shutil.copy2(path, new_path)
328 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
329 except (IOError, os.error), reason:
330 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
333 def new_wallet(self):
336 wallet_folder = os.path.dirname(self.wallet.storage.path)
337 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
338 filename = unicode(filename)
339 if not ok or not filename:
341 filename = os.path.join(wallet_folder, filename)
343 storage = WalletStorage({'wallet_path': filename})
344 assert not storage.file_exists
346 wizard = installwizard.InstallWizard(self.config, self.network, storage)
347 wallet = wizard.run()
349 self.load_wallet(wallet)
353 def init_menubar(self):
356 file_menu = menubar.addMenu(_("&File"))
357 open_wallet_action = file_menu.addAction(_("&Open"))
358 open_wallet_action.triggered.connect(self.open_wallet)
360 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
361 new_wallet_action.triggered.connect(self.new_wallet)
363 wallet_backup = file_menu.addAction(_("&Copy"))
364 wallet_backup.triggered.connect(self.backup_wallet)
366 quit_item = file_menu.addAction(_("&Close"))
367 quit_item.triggered.connect(self.close)
369 wallet_menu = menubar.addMenu(_("&Wallet"))
371 new_contact = wallet_menu.addAction(_("&New contact"))
372 new_contact.triggered.connect(self.new_contact_dialog)
374 self.new_account = wallet_menu.addAction(_("&New account"))
375 self.new_account.triggered.connect(self.new_account_dialog)
377 wallet_menu.addSeparator()
379 #if self.wallet.seed:
380 show_seed = wallet_menu.addAction(_("&Seed"))
381 show_seed.triggered.connect(self.show_seed_dialog)
383 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
384 show_mpk.triggered.connect(self.show_master_public_key)
386 wallet_menu.addSeparator()
388 csv_transaction_menu = wallet_menu.addMenu(_("&Create transaction"))
390 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
391 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
393 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
394 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
396 raw_transaction_menu = wallet_menu.addMenu(_("&Load transaction"))
398 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
399 raw_transaction_file.triggered.connect(self.do_process_from_file)
401 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
402 raw_transaction_text.triggered.connect(self.do_process_from_text)
405 tools_menu = menubar.addMenu(_("&Tools"))
407 # Settings / Preferences are all reserved keywords in OSX using this as work around
408 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
409 preferences_menu = tools_menu.addAction(preferences_name)
410 preferences_menu.triggered.connect(self.settings_dialog)
412 plugins_labels = tools_menu.addAction(_("&Plugins"))
413 plugins_labels.triggered.connect(self.plugins_dialog)
415 wallet_menu.addSeparator()
417 labels_menu = tools_menu.addMenu(_("&Labels"))
418 import_labels = labels_menu.addAction(_("&Import"))
419 import_labels.triggered.connect(self.do_import_labels)
420 export_labels = labels_menu.addAction(_("&Export"))
421 export_labels.triggered.connect(self.do_export_labels)
423 keys_menu = tools_menu.addMenu(_("&Private keys"))
424 import_keys = keys_menu.addAction(_("&Import"))
425 import_keys.triggered.connect(self.do_import_privkey)
426 export_keys = keys_menu.addAction(_("&Export"))
427 export_keys.triggered.connect(self.do_export_privkeys)
429 ex_history = tools_menu.addAction(_("&Export History"))
430 ex_history.triggered.connect(self.do_export_history)
433 help_menu = menubar.addMenu(_("&Help"))
434 show_about = help_menu.addAction(_("&About"))
435 show_about.triggered.connect(self.show_about)
436 web_open = help_menu.addAction(_("&Official website"))
437 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
439 help_menu.addSeparator()
440 doc_open = help_menu.addAction(_("&Documentation"))
441 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
442 report_bug = help_menu.addAction(_("&Report Bug"))
443 report_bug.triggered.connect(self.show_report_bug)
445 self.setMenuBar(menubar)
447 def show_about(self):
448 QMessageBox.about(self, "Electrum",
449 _("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."))
451 def show_report_bug(self):
452 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
453 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
456 def notify_transactions(self):
457 print_error("Notifying GUI")
458 if len(self.network.interface.pending_transactions_for_notifications) > 0:
459 # Combine the transactions if there are more then three
460 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
463 for tx in self.network.interface.pending_transactions_for_notifications:
464 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
468 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
469 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
471 self.network.interface.pending_transactions_for_notifications = []
473 for tx in self.network.interface.pending_transactions_for_notifications:
475 self.network.interface.pending_transactions_for_notifications.remove(tx)
476 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
478 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
480 def notify(self, message):
481 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
485 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
486 def getOpenFileName(self, title, filter = ""):
487 directory = self.config.get('io_dir', os.path.expanduser('~'))
488 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
489 if fileName and directory != os.path.dirname(fileName):
490 self.config.set_key('io_dir', os.path.dirname(fileName), True)
493 def getSaveFileName(self, title, filename, filter = ""):
494 directory = self.config.get('io_dir', os.path.expanduser('~'))
495 path = os.path.join( directory, filename )
496 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
497 if fileName and directory != os.path.dirname(fileName):
498 self.config.set_key('io_dir', os.path.dirname(fileName), True)
502 QMainWindow.close(self)
503 run_hook('close_main_window')
505 def connect_slots(self, sender):
506 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
507 self.previous_payto_e=''
509 def timer_actions(self):
510 if self.need_update.is_set():
512 self.need_update.clear()
513 run_hook('timer_actions')
515 def format_amount(self, x, is_diff=False, whitespaces=False):
516 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
518 def read_amount(self, x):
519 if x in['.', '']: return None
520 p = pow(10, self.decimal_point)
521 return int( p * Decimal(x) )
524 assert self.decimal_point in [5,8]
525 return "BTC" if self.decimal_point == 8 else "mBTC"
528 def update_status(self):
529 if self.network.interface and self.network.interface.is_connected:
530 if not self.wallet.up_to_date:
531 text = _("Synchronizing...")
532 icon = QIcon(":icons/status_waiting.png")
534 c, u = self.wallet.get_account_balance(self.current_account)
535 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
536 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
539 run_hook('set_quote_text', c+u, r)
542 text += " (%s)"%quote
544 self.tray.setToolTip(text)
545 icon = QIcon(":icons/status_connected.png")
547 text = _("Not connected")
548 icon = QIcon(":icons/status_disconnected.png")
550 self.balance_label.setText(text)
551 self.status_button.setIcon( icon )
554 def update_wallet(self):
556 if self.wallet.up_to_date or not self.network.interface.is_connected:
557 self.update_history_tab()
558 self.update_receive_tab()
559 self.update_contacts_tab()
560 self.update_completions()
563 def create_history_tab(self):
564 self.history_list = l = MyTreeWidget(self)
566 for i,width in enumerate(self.column_widths['history']):
567 l.setColumnWidth(i, width)
568 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
569 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
570 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
572 l.customContextMenuRequested.connect(self.create_history_menu)
576 def create_history_menu(self, position):
577 self.history_list.selectedIndexes()
578 item = self.history_list.currentItem()
580 tx_hash = str(item.data(0, Qt.UserRole).toString())
581 if not tx_hash: return
583 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
584 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
585 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
586 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
589 def show_transaction(self, tx):
590 import transaction_dialog
591 d = transaction_dialog.TxDialog(tx, self)
594 def tx_label_clicked(self, item, column):
595 if column==2 and item.isSelected():
597 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
598 self.history_list.editItem( item, column )
599 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
602 def tx_label_changed(self, item, column):
606 tx_hash = str(item.data(0, Qt.UserRole).toString())
607 tx = self.wallet.transactions.get(tx_hash)
608 text = unicode( item.text(2) )
609 self.wallet.set_label(tx_hash, text)
611 item.setForeground(2, QBrush(QColor('black')))
613 text = self.wallet.get_default_label(tx_hash)
614 item.setText(2, text)
615 item.setForeground(2, QBrush(QColor('gray')))
619 def edit_label(self, is_recv):
620 l = self.receive_list if is_recv else self.contacts_list
621 item = l.currentItem()
622 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
623 l.editItem( item, 1 )
624 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
628 def address_label_clicked(self, item, column, l, column_addr, column_label):
629 if column == column_label and item.isSelected():
630 is_editable = item.data(0, 32).toBool()
633 addr = unicode( item.text(column_addr) )
634 label = unicode( item.text(column_label) )
635 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
636 l.editItem( item, column )
637 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
640 def address_label_changed(self, item, column, l, column_addr, column_label):
641 if column == column_label:
642 addr = unicode( item.text(column_addr) )
643 text = unicode( item.text(column_label) )
644 is_editable = item.data(0, 32).toBool()
648 changed = self.wallet.set_label(addr, text)
650 self.update_history_tab()
651 self.update_completions()
653 self.current_item_changed(item)
655 run_hook('item_changed', item, column)
658 def current_item_changed(self, a):
659 run_hook('current_item_changed', a)
663 def update_history_tab(self):
665 self.history_list.clear()
666 for item in self.wallet.get_tx_history(self.current_account):
667 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
670 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
672 time_str = _("unknown")
675 time_str = 'unverified'
676 icon = QIcon(":icons/unconfirmed.png")
679 icon = QIcon(":icons/unconfirmed.png")
681 icon = QIcon(":icons/clock%d.png"%conf)
683 icon = QIcon(":icons/confirmed.png")
685 if value is not None:
686 v_str = self.format_amount(value, True, whitespaces=True)
690 balance_str = self.format_amount(balance, whitespaces=True)
693 label, is_default_label = self.wallet.get_label(tx_hash)
695 label = _('Pruned transaction outputs')
696 is_default_label = False
698 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
699 item.setFont(2, QFont(MONOSPACE_FONT))
700 item.setFont(3, QFont(MONOSPACE_FONT))
701 item.setFont(4, QFont(MONOSPACE_FONT))
703 item.setForeground(3, QBrush(QColor("#BC1E1E")))
705 item.setData(0, Qt.UserRole, tx_hash)
706 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
708 item.setForeground(2, QBrush(QColor('grey')))
710 item.setIcon(0, icon)
711 self.history_list.insertTopLevelItem(0,item)
714 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
717 def create_send_tab(self):
722 grid.setColumnMinimumWidth(3,300)
723 grid.setColumnStretch(5,1)
726 self.payto_e = QLineEdit()
727 grid.addWidget(QLabel(_('Pay to')), 1, 0)
728 grid.addWidget(self.payto_e, 1, 1, 1, 3)
730 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)
732 completer = QCompleter()
733 completer.setCaseSensitivity(False)
734 self.payto_e.setCompleter(completer)
735 completer.setModel(self.completions)
737 self.message_e = QLineEdit()
738 grid.addWidget(QLabel(_('Description')), 2, 0)
739 grid.addWidget(self.message_e, 2, 1, 1, 3)
740 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)
742 self.amount_e = AmountEdit(self.base_unit)
743 grid.addWidget(QLabel(_('Amount')), 3, 0)
744 grid.addWidget(self.amount_e, 3, 1, 1, 2)
745 grid.addWidget(HelpButton(
746 _('Amount to be sent.') + '\n\n' \
747 + _('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.') \
748 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
750 self.fee_e = AmountEdit(self.base_unit)
751 grid.addWidget(QLabel(_('Fee')), 4, 0)
752 grid.addWidget(self.fee_e, 4, 1, 1, 2)
753 grid.addWidget(HelpButton(
754 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
755 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
756 + _('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)
759 self.send_button = EnterButton(_("Send"), self.do_send)
760 grid.addWidget(self.send_button, 6, 1)
762 b = EnterButton(_("Clear"),self.do_clear)
763 grid.addWidget(b, 6, 2)
765 self.payto_sig = QLabel('')
766 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
768 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
769 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
778 def entry_changed( is_fee ):
779 self.funds_error = False
781 if self.amount_e.is_shortcut:
782 self.amount_e.is_shortcut = False
783 c, u = self.wallet.get_account_balance(self.current_account)
784 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
785 fee = self.wallet.estimated_fee(inputs)
787 self.amount_e.setText( self.format_amount(amount) )
788 self.fee_e.setText( self.format_amount( fee ) )
791 amount = self.read_amount(str(self.amount_e.text()))
792 fee = self.read_amount(str(self.fee_e.text()))
794 if not is_fee: fee = None
797 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
799 self.fee_e.setText( self.format_amount( fee ) )
802 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
806 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
807 self.funds_error = True
808 text = _( "Not enough funds" )
809 c, u = self.wallet.get_frozen_balance()
810 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
812 self.statusBar().showMessage(text)
813 self.amount_e.setPalette(palette)
814 self.fee_e.setPalette(palette)
816 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
817 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
819 run_hook('create_send_tab', grid)
823 def update_completions(self):
825 for addr,label in self.wallet.labels.items():
826 if addr in self.wallet.addressbook:
827 l.append( label + ' <' + addr + '>')
829 run_hook('update_completions', l)
830 self.completions.setStringList(l)
834 return lambda s, *args: s.do_protect(func, args)
839 label = unicode( self.message_e.text() )
840 r = unicode( self.payto_e.text() )
843 # label or alias, with address in brackets
844 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
845 to_address = m.group(2) if m else r
847 if not is_valid(to_address):
848 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
852 amount = self.read_amount(unicode( self.amount_e.text()))
854 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
857 fee = self.read_amount(unicode( self.fee_e.text()))
859 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
862 confirm_amount = self.config.get('confirm_amount', 100000000)
863 if amount >= confirm_amount:
864 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
867 self.send_tx(to_address, amount, fee, label)
871 def send_tx(self, to_address, amount, fee, label, password):
874 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
875 except BaseException, e:
876 traceback.print_exc(file=sys.stdout)
877 self.show_message(str(e))
880 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
881 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
885 self.wallet.set_label(tx.hash(), label)
888 h = self.wallet.send_tx(tx)
889 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
890 status, msg = self.wallet.receive_tx( h )
892 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
894 self.update_contacts_tab()
896 QMessageBox.warning(self, _('Error'), msg, _('OK'))
898 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
900 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
901 with open(fileName,'w') as f:
902 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
903 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
905 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
907 # add recipient to addressbook
908 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
909 self.wallet.addressbook.append(to_address)
914 def set_url(self, url):
915 address, amount, label, message, signature, identity, url = util.parse_url(url)
917 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
920 self.mini.set_payment_fields(address, amount)
922 if label and self.wallet.labels.get(address) != label:
923 if self.question('Give label "%s" to address %s ?'%(label,address)):
924 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
925 self.wallet.addressbook.append(address)
926 self.wallet.set_label(address, label)
928 run_hook('set_url', url, self.show_message, self.question)
930 self.tabs.setCurrentIndex(1)
931 label = self.wallet.labels.get(address)
932 m_addr = label + ' <'+ address +'>' if label else address
933 self.payto_e.setText(m_addr)
935 self.message_e.setText(message)
937 self.amount_e.setText(amount)
940 self.set_frozen(self.payto_e,True)
941 self.set_frozen(self.amount_e,True)
942 self.set_frozen(self.message_e,True)
943 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
945 self.payto_sig.setVisible(False)
948 self.payto_sig.setVisible(False)
949 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
951 self.set_frozen(e,False)
954 def set_frozen(self,entry,frozen):
956 entry.setReadOnly(True)
957 entry.setFrame(False)
959 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
960 entry.setPalette(palette)
962 entry.setReadOnly(False)
965 palette.setColor(entry.backgroundRole(), QColor('white'))
966 entry.setPalette(palette)
969 def toggle_freeze(self,addr):
971 if addr in self.wallet.frozen_addresses:
972 self.wallet.unfreeze(addr)
974 self.wallet.freeze(addr)
975 self.update_receive_tab()
977 def toggle_priority(self,addr):
979 if addr in self.wallet.prioritized_addresses:
980 self.wallet.unprioritize(addr)
982 self.wallet.prioritize(addr)
983 self.update_receive_tab()
986 def create_list_tab(self, headers):
987 "generic tab creation method"
988 l = MyTreeWidget(self)
989 l.setColumnCount( len(headers) )
990 l.setHeaderLabels( headers )
1000 vbox.addWidget(buttons)
1002 hbox = QHBoxLayout()
1005 buttons.setLayout(hbox)
1010 def create_receive_tab(self):
1011 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1012 l.setContextMenuPolicy(Qt.CustomContextMenu)
1013 l.customContextMenuRequested.connect(self.create_receive_menu)
1014 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1015 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1016 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1017 self.receive_list = l
1018 self.receive_buttons_hbox = hbox
1023 def receive_tab_set_mode(self, i):
1024 self.save_column_widths()
1025 self.expert_mode = (i == 1)
1026 self.config.set_key('classic_expert_mode', self.expert_mode, True)
1027 self.update_receive_tab()
1030 def save_column_widths(self):
1031 if not self.expert_mode:
1032 widths = [ self.receive_list.columnWidth(0) ]
1035 for i in range(self.receive_list.columnCount() -1):
1036 widths.append(self.receive_list.columnWidth(i))
1037 self.column_widths["receive"][self.expert_mode] = widths
1039 self.column_widths["history"] = []
1040 for i in range(self.history_list.columnCount() - 1):
1041 self.column_widths["history"].append(self.history_list.columnWidth(i))
1043 self.column_widths["contacts"] = []
1044 for i in range(self.contacts_list.columnCount() - 1):
1045 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1047 self.config.set_key("column_widths", self.column_widths, True)
1050 def create_contacts_tab(self):
1051 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1052 l.setContextMenuPolicy(Qt.CustomContextMenu)
1053 l.customContextMenuRequested.connect(self.create_contact_menu)
1054 for i,width in enumerate(self.column_widths['contacts']):
1055 l.setColumnWidth(i, width)
1057 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1058 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1059 self.contacts_list = l
1060 self.contacts_buttons_hbox = hbox
1065 def delete_imported_key(self, addr):
1066 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1067 self.wallet.delete_imported_key(addr)
1068 self.update_receive_tab()
1069 self.update_history_tab()
1071 def edit_account_label(self, k):
1072 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':')
1074 label = unicode(text)
1075 self.wallet.set_label(k,label)
1076 self.update_receive_tab()
1078 def create_account_menu(self, position, k, item):
1080 if item.isExpanded():
1081 menu.addAction(_("Minimize"), lambda: item.setExpanded(False))
1083 menu.addAction(_("Maximize"), lambda: item.setExpanded(True))
1084 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1085 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1086 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1088 def create_receive_menu(self, position):
1089 # fixme: this function apparently has a side effect.
1090 # if it is not called the menu pops up several times
1091 #self.receive_list.selectedIndexes()
1093 item = self.receive_list.itemAt(position)
1096 addr = unicode(item.text(0))
1097 if not is_valid(addr):
1098 k = str(item.data(0,32).toString())
1100 self.create_account_menu(position, k, item)
1102 item.setExpanded(not item.isExpanded())
1106 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1107 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1108 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1109 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1110 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1111 if addr in self.wallet.imported_keys:
1112 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1114 if self.expert_mode:
1115 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1116 menu.addAction(t, lambda: self.toggle_freeze(addr))
1117 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1118 menu.addAction(t, lambda: self.toggle_priority(addr))
1120 run_hook('receive_menu', menu)
1121 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1124 def payto(self, addr):
1126 label = self.wallet.labels.get(addr)
1127 m_addr = label + ' <' + addr + '>' if label else addr
1128 self.tabs.setCurrentIndex(1)
1129 self.payto_e.setText(m_addr)
1130 self.amount_e.setFocus()
1133 def delete_contact(self, x):
1134 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1135 self.wallet.delete_contact(x)
1136 self.wallet.set_label(x, None)
1137 self.update_history_tab()
1138 self.update_contacts_tab()
1139 self.update_completions()
1142 def create_contact_menu(self, position):
1143 item = self.contacts_list.itemAt(position)
1145 addr = unicode(item.text(0))
1146 label = unicode(item.text(1))
1147 is_editable = item.data(0,32).toBool()
1148 payto_addr = item.data(0,33).toString()
1150 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1151 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1152 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1154 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1155 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1157 run_hook('create_contact_menu', menu, item)
1158 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1161 def update_receive_item(self, item):
1162 item.setFont(0, QFont(MONOSPACE_FONT))
1163 address = str(item.data(0,0).toString())
1164 label = self.wallet.labels.get(address,'')
1165 item.setData(1,0,label)
1166 item.setData(0,32, True) # is editable
1168 run_hook('update_receive_item', address, item)
1170 c, u = self.wallet.get_addr_balance(address)
1171 balance = self.format_amount(c + u)
1172 item.setData(2,0,balance)
1174 if self.expert_mode:
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, not self.expert_mode)
1186 l.setColumnHidden(3, not self.expert_mode)
1187 for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
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] if self.expert_mode else [0]):
1210 if self.expert_mode:
1211 name = _("Receiving") if not is_change else _("Change")
1212 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1213 account_item.addChild(seq_item)
1214 if not is_change: seq_item.setExpanded(True)
1216 seq_item = account_item
1220 for address in account.get_addresses(is_change):
1221 h = self.wallet.history.get(address,[])
1225 if gap > self.wallet.gap_limit:
1230 num_tx = '*' if h == ['*'] else "%d"%len(h)
1231 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1232 self.update_receive_item(item)
1234 item.setBackgroundColor(1, QColor('red'))
1235 seq_item.addChild(item)
1238 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1239 c,u = self.wallet.get_imported_balance()
1240 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1241 l.addTopLevelItem(account_item)
1242 account_item.setExpanded(True)
1243 for address in self.wallet.imported_keys.keys():
1244 item = QTreeWidgetItem( [ address, '', '', ''] )
1245 self.update_receive_item(item)
1246 account_item.addChild(item)
1249 # we use column 1 because column 0 may be hidden
1250 l.setCurrentItem(l.topLevelItem(0),1)
1253 def update_contacts_tab(self):
1254 l = self.contacts_list
1257 for address in self.wallet.addressbook:
1258 label = self.wallet.labels.get(address,'')
1259 n = self.wallet.get_num_tx(address)
1260 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1261 item.setFont(0, QFont(MONOSPACE_FONT))
1262 # 32 = label can be edited (bool)
1263 item.setData(0,32, True)
1265 item.setData(0,33, address)
1266 l.addTopLevelItem(item)
1268 run_hook('update_contacts_tab', l)
1269 l.setCurrentItem(l.topLevelItem(0))
1273 def create_console_tab(self):
1274 from console import Console
1275 self.console = console = Console()
1279 def update_console(self):
1280 console = self.console
1281 console.history = self.config.get("console-history",[])
1282 console.history_index = len(console.history)
1284 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1285 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1287 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1289 def mkfunc(f, method):
1290 return lambda *args: apply( f, (method, args, self.password_dialog ))
1292 if m[0]=='_' or m=='wallet' or m == 'interface': continue
1293 methods[m] = mkfunc(c._run, m)
1295 console.updateNamespace(methods)
1298 def change_account(self,s):
1299 if s == _("All accounts"):
1300 self.current_account = None
1302 accounts = self.wallet.get_account_names()
1303 for k, v in accounts.items():
1305 self.current_account = k
1306 self.update_history_tab()
1307 self.update_status()
1308 self.update_receive_tab()
1310 def create_status_bar(self):
1313 sb.setFixedHeight(35)
1314 qtVersion = qVersion()
1316 self.balance_label = QLabel("")
1317 sb.addWidget(self.balance_label)
1319 from version_getter import UpdateLabel
1320 self.updatelabel = UpdateLabel(self.config, sb)
1322 self.account_selector = QComboBox()
1323 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1324 sb.addPermanentWidget(self.account_selector)
1326 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1327 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1329 self.lock_icon = QIcon()
1330 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1331 sb.addPermanentWidget( self.password_button )
1333 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1334 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1335 sb.addPermanentWidget( self.seed_button )
1336 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1337 sb.addPermanentWidget( self.status_button )
1339 run_hook('create_status_bar', (sb,))
1341 self.setStatusBar(sb)
1344 def update_lock_icon(self):
1345 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1346 self.password_button.setIcon( icon )
1349 def update_buttons_on_seed(self):
1350 if self.wallet.seed:
1351 self.seed_button.show()
1352 self.password_button.show()
1353 self.send_button.setText(_("Send"))
1355 self.password_button.hide()
1356 self.seed_button.hide()
1357 self.send_button.setText(_("Create unsigned transaction"))
1360 def change_password_dialog(self):
1361 from password_dialog import PasswordDialog
1362 d = PasswordDialog(self.wallet, self)
1364 self.update_lock_icon()
1367 def new_contact_dialog(self):
1368 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1369 address = unicode(text)
1371 if is_valid(address):
1372 self.wallet.add_contact(address)
1373 self.update_contacts_tab()
1374 self.update_history_tab()
1375 self.update_completions()
1377 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1380 def new_account_dialog(self):
1382 dialog = QDialog(self)
1384 dialog.setWindowTitle(_("New Account"))
1386 addr = self.wallet.new_account_address()
1387 vbox = QVBoxLayout()
1388 msg = _("Electrum considers that an account exists only if it contains bitcoins.") + '\n' \
1389 + _("To create a new account, please send coins to the first address of that account.") + '\n' \
1390 + _("Note: you will need to wait for 2 confirmations before the account is created.")
1391 vbox.addWidget(QLabel(msg))
1392 vbox.addWidget(QLabel(_('Address')+':'))
1397 vbox.addLayout(ok_cancel_buttons(dialog))
1398 dialog.setLayout(vbox)
1405 def show_master_public_key_old(self):
1406 dialog = QDialog(self)
1408 dialog.setWindowTitle(_("Master Public Key"))
1410 main_text = QTextEdit()
1411 main_text.setText(self.wallet.get_master_public_key())
1412 main_text.setReadOnly(True)
1413 main_text.setMaximumHeight(170)
1414 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1416 ok_button = QPushButton(_("OK"))
1417 ok_button.setDefault(True)
1418 ok_button.clicked.connect(dialog.accept)
1420 main_layout = QGridLayout()
1421 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1423 main_layout.addWidget(main_text, 1, 0)
1424 main_layout.addWidget(qrw, 1, 1 )
1426 vbox.addLayout(close_button(dialog))
1427 dialog.setLayout(vbox)
1431 def show_master_public_key(self):
1433 if self.wallet.seed_version == 4:
1434 self.show_master_public_keys_old()
1437 dialog = QDialog(self)
1439 dialog.setWindowTitle(_("Master Public Keys"))
1441 chain_text = QTextEdit()
1442 chain_text.setReadOnly(True)
1443 chain_text.setMaximumHeight(170)
1444 chain_qrw = QRCodeWidget()
1446 mpk_text = QTextEdit()
1447 mpk_text.setReadOnly(True)
1448 mpk_text.setMaximumHeight(170)
1449 mpk_qrw = QRCodeWidget()
1451 main_layout = QGridLayout()
1453 main_layout.addWidget(QLabel(_('chain')), 1, 0)
1454 main_layout.addWidget(chain_text, 1, 1)
1455 main_layout.addWidget(chain_qrw, 1, 2)
1456 main_layout.addWidget(QLabel(_('public key')), 2, 0)
1457 main_layout.addWidget(mpk_text, 2, 1)
1458 main_layout.addWidget(mpk_qrw, 2, 2)
1461 c, K, cK = self.wallet.master_public_keys[str(key)]
1462 chain_text.setText(c)
1463 chain_qrw.set_addr(c)
1464 chain_qrw.update_qr()
1469 key_selector = QComboBox()
1470 keys = sorted(self.wallet.master_public_keys.keys())
1471 key_selector.addItems(keys)
1473 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1474 main_layout.addWidget(key_selector, 0, 1)
1475 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1479 vbox = QVBoxLayout()
1480 vbox.addLayout(main_layout)
1481 vbox.addLayout(close_button(dialog))
1483 dialog.setLayout(vbox)
1488 def show_seed_dialog(self, password):
1489 if not self.wallet.seed:
1490 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1493 seed = self.wallet.decode_seed(password)
1495 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1498 from seed_dialog import SeedDialog
1499 d = SeedDialog(self)
1500 d.show_seed(seed, self.wallet.imported_keys)
1504 def show_qrcode(self, data, title = _("QR code")):
1508 d.setWindowTitle(title)
1509 d.setMinimumSize(270, 300)
1510 vbox = QVBoxLayout()
1511 qrw = QRCodeWidget(data)
1512 vbox.addWidget(qrw, 1)
1513 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1514 hbox = QHBoxLayout()
1518 filename = "qrcode.bmp"
1519 bmp.save_qrcode(qrw.qr, filename)
1520 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1522 b = QPushButton(_("Save"))
1524 b.clicked.connect(print_qr)
1526 b = QPushButton(_("Close"))
1528 b.clicked.connect(d.accept)
1531 vbox.addLayout(hbox)
1536 def do_protect(self, func, args):
1537 if self.wallet.use_encryption:
1538 password = self.password_dialog()
1544 if args != (False,):
1545 args = (self,) + args + (password,)
1547 args = (self,password)
1552 def show_private_key(self, address, password):
1553 if not address: return
1555 pk_list = self.wallet.get_private_key(address, password)
1556 except BaseException, e:
1557 self.show_message(str(e))
1559 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1563 def do_sign(self, address, message, signature, password):
1564 message = unicode(message.toPlainText())
1565 message = message.encode('utf-8')
1567 sig = self.wallet.sign_message(str(address.text()), message, password)
1568 signature.setText(sig)
1569 except BaseException, e:
1570 self.show_message(str(e))
1572 def sign_message(self, address):
1573 if not address: return
1576 d.setWindowTitle(_('Sign Message'))
1577 d.setMinimumSize(410, 290)
1579 tab_widget = QTabWidget()
1581 layout = QGridLayout(tab)
1583 sign_address = QLineEdit()
1585 sign_address.setText(address)
1586 layout.addWidget(QLabel(_('Address')), 1, 0)
1587 layout.addWidget(sign_address, 1, 1)
1589 sign_message = QTextEdit()
1590 layout.addWidget(QLabel(_('Message')), 2, 0)
1591 layout.addWidget(sign_message, 2, 1)
1592 layout.setRowStretch(2,3)
1594 sign_signature = QTextEdit()
1595 layout.addWidget(QLabel(_('Signature')), 3, 0)
1596 layout.addWidget(sign_signature, 3, 1)
1597 layout.setRowStretch(3,1)
1600 hbox = QHBoxLayout()
1601 b = QPushButton(_("Sign"))
1603 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1604 b = QPushButton(_("Close"))
1605 b.clicked.connect(d.accept)
1607 layout.addLayout(hbox, 4, 1)
1608 tab_widget.addTab(tab, _("Sign"))
1612 layout = QGridLayout(tab)
1614 verify_address = QLineEdit()
1615 layout.addWidget(QLabel(_('Address')), 1, 0)
1616 layout.addWidget(verify_address, 1, 1)
1618 verify_message = QTextEdit()
1619 layout.addWidget(QLabel(_('Message')), 2, 0)
1620 layout.addWidget(verify_message, 2, 1)
1621 layout.setRowStretch(2,3)
1623 verify_signature = QTextEdit()
1624 layout.addWidget(QLabel(_('Signature')), 3, 0)
1625 layout.addWidget(verify_signature, 3, 1)
1626 layout.setRowStretch(3,1)
1629 message = unicode(verify_message.toPlainText())
1630 message = message.encode('utf-8')
1631 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1632 self.show_message(_("Signature verified"))
1634 self.show_message(_("Error: wrong signature"))
1636 hbox = QHBoxLayout()
1637 b = QPushButton(_("Verify"))
1638 b.clicked.connect(do_verify)
1640 b = QPushButton(_("Close"))
1641 b.clicked.connect(d.accept)
1643 layout.addLayout(hbox, 4, 1)
1644 tab_widget.addTab(tab, _("Verify"))
1646 vbox = QVBoxLayout()
1647 vbox.addWidget(tab_widget)
1654 def question(self, msg):
1655 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1657 def show_message(self, msg):
1658 QMessageBox.information(self, _('Message'), msg, _('OK'))
1660 def password_dialog(self ):
1667 vbox = QVBoxLayout()
1668 msg = _('Please enter your password')
1669 vbox.addWidget(QLabel(msg))
1671 grid = QGridLayout()
1673 grid.addWidget(QLabel(_('Password')), 1, 0)
1674 grid.addWidget(pw, 1, 1)
1675 vbox.addLayout(grid)
1677 vbox.addLayout(ok_cancel_buttons(d))
1680 run_hook('password_dialog', pw, grid, 1)
1681 if not d.exec_(): return
1682 return unicode(pw.text())
1691 def tx_from_text(self, txt):
1692 "json or raw hexadecimal"
1695 tx = Transaction(txt)
1701 tx_dict = json.loads(str(txt))
1702 assert "hex" in tx_dict.keys()
1703 assert "complete" in tx_dict.keys()
1704 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1705 if not tx_dict["complete"]:
1706 assert "input_info" in tx_dict.keys()
1707 input_info = json.loads(tx_dict['input_info'])
1708 tx.add_input_info(input_info)
1713 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1717 def read_tx_from_file(self):
1718 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1722 with open(fileName, "r") as f:
1723 file_content = f.read()
1724 except (ValueError, IOError, os.error), reason:
1725 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1727 return self.tx_from_text(file_content)
1731 def sign_raw_transaction(self, tx, input_info, password):
1732 self.wallet.signrawtransaction(tx, input_info, [], password)
1734 def do_process_from_text(self):
1735 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1738 tx = self.tx_from_text(text)
1740 self.show_transaction(tx)
1742 def do_process_from_file(self):
1743 tx = self.read_tx_from_file()
1745 self.show_transaction(tx)
1747 def do_process_from_csvReader(self, csvReader):
1750 for row in csvReader:
1752 amount = float(row[1])
1753 amount = int(100000000*amount)
1754 outputs.append((address, amount))
1755 except (ValueError, IOError, os.error), reason:
1756 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1760 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1761 except BaseException, e:
1762 self.show_message(str(e))
1765 self.show_transaction(tx)
1767 def do_process_from_csv_file(self):
1768 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1772 with open(fileName, "r") as f:
1773 csvReader = csv.reader(f)
1774 self.do_process_from_csvReader(csvReader)
1775 except (ValueError, IOError, os.error), reason:
1776 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1779 def do_process_from_csv_text(self):
1780 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1781 + _("Format: address, amount. One output per line"), _("Load CSV"))
1784 f = StringIO.StringIO(text)
1785 csvReader = csv.reader(f)
1786 self.do_process_from_csvReader(csvReader)
1791 def do_export_privkeys(self, password):
1792 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.")))
1795 select_export = _('Select file to export your private keys to')
1796 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1798 with open(fileName, "w+") as csvfile:
1799 transaction = csv.writer(csvfile)
1800 transaction.writerow(["address", "private_key"])
1802 addresses = self.wallet.addresses(True)
1804 for addr in addresses:
1805 pk = "".join(self.wallet.get_private_key(addr, password))
1806 transaction.writerow(["%34s"%addr,pk])
1808 self.show_message(_("Private keys exported."))
1810 except (IOError, os.error), reason:
1811 export_error_label = _("Electrum was unable to produce a private key-export.")
1812 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1814 except BaseException, e:
1815 self.show_message(str(e))
1819 def do_import_labels(self):
1820 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1821 if not labelsFile: return
1823 f = open(labelsFile, 'r')
1826 for key, value in json.loads(data).items():
1827 self.wallet.set_label(key, value)
1828 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1829 except (IOError, os.error), reason:
1830 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1833 def do_export_labels(self):
1834 labels = self.wallet.labels
1836 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1838 with open(fileName, 'w+') as f:
1839 json.dump(labels, f)
1840 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1841 except (IOError, os.error), reason:
1842 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1845 def do_export_history(self):
1846 from lite_window import csv_transaction
1847 csv_transaction(self.wallet)
1851 def do_import_privkey(self, password):
1852 if not self.wallet.imported_keys:
1853 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1854 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1855 + _('Are you sure you understand what you are doing?'), 3, 4)
1858 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1861 text = str(text).split()
1866 addr = self.wallet.import_key(key, password)
1867 except BaseException as e:
1873 addrlist.append(addr)
1875 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1877 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1878 self.update_receive_tab()
1879 self.update_history_tab()
1882 def settings_dialog(self):
1884 d.setWindowTitle(_('Electrum Settings'))
1886 vbox = QVBoxLayout()
1888 tabs = QTabWidget(self)
1889 self.settings_tab = tabs
1890 vbox.addWidget(tabs)
1893 grid_ui = QGridLayout(tab1)
1894 grid_ui.setColumnStretch(0,1)
1895 tabs.addTab(tab1, _('Display') )
1897 nz_label = QLabel(_('Display zeros'))
1898 grid_ui.addWidget(nz_label, 0, 0)
1899 nz_e = AmountEdit(None,True)
1900 nz_e.setText("%d"% self.num_zeros)
1901 grid_ui.addWidget(nz_e, 0, 1)
1902 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1903 grid_ui.addWidget(HelpButton(msg), 0, 2)
1904 if not self.config.is_modifiable('num_zeros'):
1905 for w in [nz_e, nz_label]: w.setEnabled(False)
1907 lang_label=QLabel(_('Language') + ':')
1908 grid_ui.addWidget(lang_label, 1, 0)
1909 lang_combo = QComboBox()
1910 from electrum.i18n import languages
1911 lang_combo.addItems(languages.values())
1913 index = languages.keys().index(self.config.get("language",''))
1916 lang_combo.setCurrentIndex(index)
1917 grid_ui.addWidget(lang_combo, 1, 1)
1918 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1919 if not self.config.is_modifiable('language'):
1920 for w in [lang_combo, lang_label]: w.setEnabled(False)
1922 expert_cb = QCheckBox(_('Expert mode'))
1923 expert_cb.setChecked(self.expert_mode)
1924 grid_ui.addWidget(expert_cb, 3, 0)
1925 hh = _('In expert mode, your client will:') + '\n' \
1926 + _(' - Show change addresses in the Receive tab') + '\n' \
1927 + _(' - Display the balance of each address') + '\n' \
1928 + _(' - Add freeze/prioritize actions to addresses.')
1929 grid_ui.addWidget(HelpButton(hh), 3, 2)
1930 grid_ui.setRowStretch(4,1)
1934 grid_wallet = QGridLayout(tab2)
1935 grid_wallet.setColumnStretch(0,1)
1936 tabs.addTab(tab2, _('Wallet') )
1938 fee_label = QLabel(_('Transaction fee'))
1939 grid_wallet.addWidget(fee_label, 0, 0)
1940 fee_e = AmountEdit(self.base_unit)
1941 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1942 grid_wallet.addWidget(fee_e, 0, 2)
1943 msg = _('Fee per kilobyte of transaction.') + ' ' \
1944 + _('Recommended value') + ': ' + self.format_amount(50000)
1945 grid_wallet.addWidget(HelpButton(msg), 0, 3)
1946 if not self.config.is_modifiable('fee_per_kb'):
1947 for w in [fee_e, fee_label]: w.setEnabled(False)
1949 usechange_cb = QCheckBox(_('Use change addresses'))
1950 usechange_cb.setChecked(self.wallet.use_change)
1951 grid_wallet.addWidget(usechange_cb, 1, 0)
1952 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1953 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1955 units = ['BTC', 'mBTC']
1956 unit_label = QLabel(_('Base unit'))
1957 grid_wallet.addWidget(unit_label, 3, 0)
1958 unit_combo = QComboBox()
1959 unit_combo.addItems(units)
1960 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1961 grid_wallet.addWidget(unit_combo, 3, 2)
1962 grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1963 + '\n1BTC=1000mBTC.\n' \
1964 + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1965 grid_wallet.setRowStretch(4,1)
1968 run_hook('create_settings_tab', tabs)
1970 vbox.addLayout(ok_cancel_buttons(d))
1974 if not d.exec_(): return
1976 fee = unicode(fee_e.text())
1978 fee = self.read_amount(fee)
1980 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1983 self.wallet.set_fee(fee)
1985 nz = unicode(nz_e.text())
1990 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1993 if self.num_zeros != nz:
1995 self.config.set_key('num_zeros', nz, True)
1996 self.update_history_tab()
1997 self.update_receive_tab()
1999 usechange_result = usechange_cb.isChecked()
2000 if self.wallet.use_change != usechange_result:
2001 self.wallet.use_change = usechange_result
2002 self.config.set_key('use_change', self.wallet.use_change, True)
2004 unit_result = units[unit_combo.currentIndex()]
2005 if self.base_unit() != unit_result:
2006 self.decimal_point = 8 if unit_result == 'BTC' else 5
2007 self.config.set_key('decimal_point', self.decimal_point, True)
2008 self.update_history_tab()
2009 self.update_status()
2011 need_restart = False
2013 lang_request = languages.keys()[lang_combo.currentIndex()]
2014 if lang_request != self.config.get('language'):
2015 self.config.set_key("language", lang_request, True)
2019 run_hook('close_settings_dialog')
2022 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2024 self.receive_tab_set_mode(expert_cb.isChecked())
2026 def run_network_dialog(self):
2027 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2029 def closeEvent(self, event):
2031 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2032 self.save_column_widths()
2033 self.config.set_key("console-history", self.console.history[-50:], True)
2038 def plugins_dialog(self):
2039 from electrum.plugins import plugins
2042 d.setWindowTitle(_('Electrum Plugins'))
2045 vbox = QVBoxLayout(d)
2048 scroll = QScrollArea()
2049 scroll.setEnabled(True)
2050 scroll.setWidgetResizable(True)
2051 scroll.setMinimumSize(400,250)
2052 vbox.addWidget(scroll)
2056 w.setMinimumHeight(len(plugins)*35)
2058 grid = QGridLayout()
2059 grid.setColumnStretch(0,1)
2062 def mk_toggle(cb, p):
2063 return lambda: cb.setChecked(p.toggle())
2064 for i, p in enumerate(plugins):
2066 cb = QCheckBox(p.fullname())
2067 cb.setDisabled(not p.is_available())
2068 cb.setChecked(p.is_enabled())
2069 cb.clicked.connect(mk_toggle(cb,p))
2070 grid.addWidget(cb, i, 0)
2071 if p.requires_settings():
2072 b = EnterButton(_('Settings'), p.settings_dialog)
2073 b.setEnabled( p.is_enabled() )
2074 grid.addWidget(b, i, 1)
2075 grid.addWidget(HelpButton(p.description()), i, 2)
2077 print_msg(_("Error: cannot display plugin"), p)
2078 traceback.print_exc(file=sys.stdout)
2079 grid.setRowStretch(i+1,1)
2081 vbox.addLayout(close_button(d))
2086 def show_account_details(self, k):
2088 d.setWindowTitle(_('Account Details'))
2091 vbox = QVBoxLayout(d)
2092 roots = self.wallet.get_roots(k)
2094 name = self.wallet.get_account_name(k)
2095 label = QLabel('Name: ' + name)
2096 vbox.addWidget(label)
2098 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2099 vbox.addWidget(QLabel('Type: ' + acctype))
2101 label = QLabel('Derivation: ' + k)
2102 vbox.addWidget(label)
2105 # mpk = self.wallet.master_public_keys[root]
2106 # text = QTextEdit()
2107 # text.setReadOnly(True)
2108 # text.setMaximumHeight(120)
2109 # text.setText(repr(mpk))
2110 # vbox.addWidget(text)
2112 vbox.addLayout(close_button(d))