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
30 sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
32 from PyQt4.QtGui import *
33 from PyQt4.QtCore import *
34 import PyQt4.QtCore as QtCore
36 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
37 from electrum.plugins import run_hook
42 sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o gui/icons_rc.py'")
44 from electrum.wallet import format_satoshis
45 from electrum import Transaction
46 from electrum import mnemonic
47 from electrum import util, bitcoin, commands, Interface, Wallet
48 from electrum import SimpleConfig, Wallet, WalletStorage
51 from electrum import bmp, pyqrnative
53 from amountedit import AmountEdit
54 from network_dialog import NetworkDialog
55 from qrcodewidget import QRCodeWidget
57 from decimal import Decimal
65 if platform.system() == 'Windows':
66 MONOSPACE_FONT = 'Lucida Console'
67 elif platform.system() == 'Darwin':
68 MONOSPACE_FONT = 'Monaco'
70 MONOSPACE_FONT = 'monospace'
72 from electrum import ELECTRUM_VERSION
82 class StatusBarButton(QPushButton):
83 def __init__(self, icon, tooltip, func):
84 QPushButton.__init__(self, icon, '')
85 self.setToolTip(tooltip)
87 self.setMaximumWidth(25)
88 self.clicked.connect(func)
90 self.setIconSize(QSize(25,25))
92 def keyPressEvent(self, e):
93 if e.key() == QtCore.Qt.Key_Return:
105 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive":[[370], [370,200,130]] }
107 class ElectrumWindow(QMainWindow):
108 def changeEvent(self, event):
109 flags = self.windowFlags();
110 if event and event.type() == QtCore.QEvent.WindowStateChange:
111 if self.windowState() & QtCore.Qt.WindowMinimized:
112 self.build_menu(True)
113 # The only way to toggle the icon in the window managers taskbar is to use the Qt.Tooltip flag
114 # The problem is that it somehow creates an (in)visible window that will stay active and prevent
115 # Electrum from closing.
116 # As for now I have no clue how to implement a proper 'hide to tray' functionality.
117 # self.setWindowFlags(flags & ~Qt.ToolTip)
118 elif event.oldState() & QtCore.Qt.WindowMinimized:
119 self.build_menu(False)
120 #self.setWindowFlags(flags | Qt.ToolTip)
122 def build_menu(self, is_hidden = False):
124 if self.isMinimized():
125 m.addAction(_("Show"), self.showNormal)
127 m.addAction(_("Hide"), self.showMinimized)
130 m.addAction(_("Exit Electrum"), self.close)
131 self.tray.setContextMenu(m)
133 def tray_activated(self, reason):
134 if reason == QSystemTrayIcon.DoubleClick:
138 def __init__(self, config, network):
139 QMainWindow.__init__(self)
142 self.network = network
144 self._close_electrum = False
146 self.current_account = self.config.get("current_account", None)
148 self.icon = QIcon(':icons/electrum.png')
149 self.tray = QSystemTrayIcon(self.icon, self)
150 self.tray.setToolTip('Electrum')
151 self.tray.activated.connect(self.tray_activated)
155 self.create_status_bar()
157 self.need_update = threading.Event()
159 self.expert_mode = config.get('classic_expert_mode', False)
160 self.decimal_point = config.get('decimal_point', 8)
161 self.num_zeros = int(config.get('num_zeros',0))
163 set_language(config.get('language'))
165 self.funds_error = False
166 self.completions = QStringListModel()
168 self.tabs = tabs = QTabWidget(self)
169 self.column_widths = self.config.get("column_widths", default_column_widths )
170 tabs.addTab(self.create_history_tab(), _('History') )
171 tabs.addTab(self.create_send_tab(), _('Send') )
172 tabs.addTab(self.create_receive_tab(), _('Receive') )
173 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
174 tabs.addTab(self.create_console_tab(), _('Console') )
175 tabs.setMinimumSize(600, 400)
176 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
177 self.setCentralWidget(tabs)
179 g = self.config.get("winpos-qt",[100, 100, 840, 400])
180 self.setGeometry(g[0], g[1], g[2], g[3])
184 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
185 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
186 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
187 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
188 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
190 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
191 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
192 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
194 self.history_list.setFocus(True)
197 self.network.register_callback('updated', lambda: self.need_update.set())
198 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
199 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
200 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
201 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
202 # set initial message
203 self.console.showMessage(self.network.banner)
205 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
206 if platform.system() == 'Windows':
207 n = 3 if self.wallet.seed else 2
208 tabs.setCurrentIndex (n)
209 tabs.setCurrentIndex (0)
216 self.config.set_key('lite_mode', False, True)
221 self.config.set_key('lite_mode', True, True)
228 if not self.check_qt_version():
229 if self.config.get('lite_mode') is True:
230 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
231 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
232 self.config.set_key('lite_mode', False, True)
237 actuator = lite_window.MiniActuator(self)
239 # Should probably not modify the current path but instead
240 # change the behaviour of rsrc(...)
241 old_path = QDir.currentPath()
242 actuator.load_theme()
244 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
246 driver = lite_window.MiniDriver(self, self.mini)
248 # Reset path back to original value now that loading the GUI
250 QDir.setCurrent(old_path)
252 if self.config.get('lite_mode') is True:
258 def check_qt_version(self):
259 qtVersion = qVersion()
260 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
264 def load_wallet(self, wallet):
268 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
269 if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
270 self.setWindowTitle( title )
272 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
273 self.notify_transactions()
276 accounts = self.wallet.get_account_names()
277 self.account_selector.clear()
278 if len(accounts) > 1:
279 self.account_selector.addItems([_("All accounts")] + accounts.values())
280 self.account_selector.setCurrentIndex(0)
281 self.account_selector.show()
283 self.account_selector.hide()
285 self.new_account.setEnabled(self.wallet.seed_version>4)
287 self.update_lock_icon()
288 self.update_buttons_on_seed()
289 self.update_console()
291 run_hook('load_wallet', wallet)
294 def select_wallet_file(self):
295 wallet_folder = self.wallet.storage.path
296 re.sub("(\/\w*.dat)$", "", wallet_folder)
297 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
301 def open_wallet(self):
303 filename = self.select_wallet_file()
307 storage = WalletStorage({'wallet_path': filename})
308 if not storage.file_exists:
309 self.show_message("file not found "+ filename)
312 self.wallet.stop_threads()
315 wallet = Wallet(storage)
316 wallet.start_threads(self.network)
318 self.load_wallet(wallet)
322 def backup_wallet(self):
324 path = self.wallet.storage.path
325 wallet_folder = os.path.dirname(path)
326 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
327 new_filename = unicode(new_filename)
328 if not ok or not new_filename:
331 new_path = os.path.join(wallet_folder, new_filename)
334 shutil.copy2(path, new_path)
335 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
336 except (IOError, os.error), reason:
337 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
340 def new_wallet(self):
343 wallet_folder = os.path.dirname(self.wallet.storage.path)
344 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
345 filename = unicode(filename)
346 if not ok or not filename:
348 filename = os.path.join(wallet_folder, filename)
350 storage = WalletStorage({'wallet_path': filename})
351 assert not storage.file_exists
353 wizard = installwizard.InstallWizard(self.config, self.network, storage)
354 wallet = wizard.run()
356 self.load_wallet(wallet)
360 def init_menubar(self):
363 file_menu = menubar.addMenu(_("&File"))
364 open_wallet_action = file_menu.addAction(_("&Open"))
365 open_wallet_action.triggered.connect(self.open_wallet)
367 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
368 new_wallet_action.triggered.connect(self.new_wallet)
370 wallet_backup = file_menu.addAction(_("&Copy"))
371 wallet_backup.triggered.connect(self.backup_wallet)
373 quit_item = file_menu.addAction(_("&Close"))
374 quit_item.triggered.connect(self.close)
376 wallet_menu = menubar.addMenu(_("&Wallet"))
378 new_contact = wallet_menu.addAction(_("&New contact"))
379 new_contact.triggered.connect(self.new_contact_dialog)
381 self.new_account = wallet_menu.addAction(_("&New account"))
382 self.new_account.triggered.connect(self.new_account_dialog)
384 wallet_menu.addSeparator()
386 #if self.wallet.seed:
387 show_seed = wallet_menu.addAction(_("&Seed"))
388 show_seed.triggered.connect(self.show_seed_dialog)
390 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
391 show_mpk.triggered.connect(self.show_master_public_key)
393 wallet_menu.addSeparator()
395 csv_transaction_menu = wallet_menu.addMenu(_("&Create transaction"))
397 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
398 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
400 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
401 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
403 raw_transaction_menu = wallet_menu.addMenu(_("&Load transaction"))
405 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
406 raw_transaction_file.triggered.connect(self.do_process_from_file)
408 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
409 raw_transaction_text.triggered.connect(self.do_process_from_text)
412 tools_menu = menubar.addMenu(_("&Tools"))
414 # Settings / Preferences are all reserved keywords in OSX using this as work around
415 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
416 preferences_menu = tools_menu.addAction(preferences_name)
417 preferences_menu.triggered.connect(self.settings_dialog)
419 plugins_labels = tools_menu.addAction(_("&Plugins"))
420 plugins_labels.triggered.connect(self.plugins_dialog)
422 wallet_menu.addSeparator()
424 labels_menu = tools_menu.addMenu(_("&Labels"))
425 import_labels = labels_menu.addAction(_("&Import"))
426 import_labels.triggered.connect(self.do_import_labels)
427 export_labels = labels_menu.addAction(_("&Export"))
428 export_labels.triggered.connect(self.do_export_labels)
430 keys_menu = tools_menu.addMenu(_("&Private keys"))
431 import_keys = keys_menu.addAction(_("&Import"))
432 import_keys.triggered.connect(self.do_import_privkey)
433 export_keys = keys_menu.addAction(_("&Export"))
434 export_keys.triggered.connect(self.do_export_privkeys)
436 ex_history = tools_menu.addAction(_("&Export History"))
437 ex_history.triggered.connect(self.do_export_history)
440 help_menu = menubar.addMenu(_("&Help"))
441 show_about = help_menu.addAction(_("&About"))
442 show_about.triggered.connect(self.show_about)
443 web_open = help_menu.addAction(_("&Official website"))
444 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
446 help_menu.addSeparator()
447 doc_open = help_menu.addAction(_("&Documentation"))
448 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
449 report_bug = help_menu.addAction(_("&Report Bug"))
450 report_bug.triggered.connect(self.show_report_bug)
452 self.setMenuBar(menubar)
454 def show_about(self):
455 QMessageBox.about(self, "Electrum",
456 _("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."))
458 def show_report_bug(self):
459 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
460 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
463 def notify_transactions(self):
464 print_error("Notifying GUI")
465 if len(self.network.interface.pending_transactions_for_notifications) > 0:
466 # Combine the transactions if there are more then three
467 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
470 for tx in self.network.interface.pending_transactions_for_notifications:
471 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
475 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
476 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
478 self.network.interface.pending_transactions_for_notifications = []
480 for tx in self.network.interface.pending_transactions_for_notifications:
482 self.network.interface.pending_transactions_for_notifications.remove(tx)
483 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
485 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
487 def notify(self, message):
488 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
492 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
493 def getOpenFileName(self, title, filter = ""):
494 directory = self.config.get('io_dir', os.path.expanduser('~'))
495 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
496 if fileName and directory != os.path.dirname(fileName):
497 self.config.set_key('io_dir', os.path.dirname(fileName), True)
500 def getSaveFileName(self, title, filename, filter = ""):
501 directory = self.config.get('io_dir', os.path.expanduser('~'))
502 path = os.path.join( directory, filename )
503 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
504 if fileName and directory != os.path.dirname(fileName):
505 self.config.set_key('io_dir', os.path.dirname(fileName), True)
509 QMainWindow.close(self)
510 run_hook('close_main_window')
512 def connect_slots(self, sender):
513 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
514 self.previous_payto_e=''
516 def timer_actions(self):
517 if self.need_update.is_set():
519 self.need_update.clear()
520 run_hook('timer_actions')
522 def format_amount(self, x, is_diff=False, whitespaces=False):
523 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
525 def read_amount(self, x):
526 if x in['.', '']: return None
527 p = pow(10, self.decimal_point)
528 return int( p * Decimal(x) )
531 assert self.decimal_point in [5,8]
532 return "BTC" if self.decimal_point == 8 else "mBTC"
535 def update_status(self):
536 if self.network.interface and self.network.interface.is_connected:
537 if not self.wallet.up_to_date:
538 text = _("Synchronizing...")
539 icon = QIcon(":icons/status_waiting.png")
541 c, u = self.wallet.get_account_balance(self.current_account)
542 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
543 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
546 run_hook('set_quote_text', c+u, r)
549 text += " (%s)"%quote
551 self.tray.setToolTip(text)
552 icon = QIcon(":icons/status_connected.png")
554 text = _("Not connected")
555 icon = QIcon(":icons/status_disconnected.png")
557 self.balance_label.setText(text)
558 self.status_button.setIcon( icon )
561 def update_wallet(self):
563 if self.wallet.up_to_date or not self.network.interface.is_connected:
564 self.update_history_tab()
565 self.update_receive_tab()
566 self.update_contacts_tab()
567 self.update_completions()
570 def create_history_tab(self):
571 self.history_list = l = MyTreeWidget(self)
573 for i,width in enumerate(self.column_widths['history']):
574 l.setColumnWidth(i, width)
575 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
576 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
577 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
579 l.customContextMenuRequested.connect(self.create_history_menu)
583 def create_history_menu(self, position):
584 self.history_list.selectedIndexes()
585 item = self.history_list.currentItem()
587 tx_hash = str(item.data(0, Qt.UserRole).toString())
588 if not tx_hash: return
590 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
591 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
592 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
593 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
596 def show_transaction(self, tx):
597 import transaction_dialog
598 d = transaction_dialog.TxDialog(tx, self)
601 def tx_label_clicked(self, item, column):
602 if column==2 and item.isSelected():
604 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
605 self.history_list.editItem( item, column )
606 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
609 def tx_label_changed(self, item, column):
613 tx_hash = str(item.data(0, Qt.UserRole).toString())
614 tx = self.wallet.transactions.get(tx_hash)
615 text = unicode( item.text(2) )
616 self.wallet.set_label(tx_hash, text)
618 item.setForeground(2, QBrush(QColor('black')))
620 text = self.wallet.get_default_label(tx_hash)
621 item.setText(2, text)
622 item.setForeground(2, QBrush(QColor('gray')))
626 def edit_label(self, is_recv):
627 l = self.receive_list if is_recv else self.contacts_list
628 item = l.currentItem()
629 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
630 l.editItem( item, 1 )
631 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
635 def address_label_clicked(self, item, column, l, column_addr, column_label):
636 if column == column_label and item.isSelected():
637 is_editable = item.data(0, 32).toBool()
640 addr = unicode( item.text(column_addr) )
641 label = unicode( item.text(column_label) )
642 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
643 l.editItem( item, column )
644 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
647 def address_label_changed(self, item, column, l, column_addr, column_label):
648 if column == column_label:
649 addr = unicode( item.text(column_addr) )
650 text = unicode( item.text(column_label) )
651 is_editable = item.data(0, 32).toBool()
655 changed = self.wallet.set_label(addr, text)
657 self.update_history_tab()
658 self.update_completions()
660 self.current_item_changed(item)
662 run_hook('item_changed', item, column)
665 def current_item_changed(self, a):
666 run_hook('current_item_changed', a)
670 def update_history_tab(self):
672 self.history_list.clear()
673 for item in self.wallet.get_tx_history(self.current_account):
674 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
677 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
679 time_str = _("unknown")
682 time_str = 'unverified'
683 icon = QIcon(":icons/unconfirmed.png")
686 icon = QIcon(":icons/unconfirmed.png")
688 icon = QIcon(":icons/clock%d.png"%conf)
690 icon = QIcon(":icons/confirmed.png")
692 if value is not None:
693 v_str = self.format_amount(value, True, whitespaces=True)
697 balance_str = self.format_amount(balance, whitespaces=True)
700 label, is_default_label = self.wallet.get_label(tx_hash)
702 label = _('Pruned transaction outputs')
703 is_default_label = False
705 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
706 item.setFont(2, QFont(MONOSPACE_FONT))
707 item.setFont(3, QFont(MONOSPACE_FONT))
708 item.setFont(4, QFont(MONOSPACE_FONT))
710 item.setForeground(3, QBrush(QColor("#BC1E1E")))
712 item.setData(0, Qt.UserRole, tx_hash)
713 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
715 item.setForeground(2, QBrush(QColor('grey')))
717 item.setIcon(0, icon)
718 self.history_list.insertTopLevelItem(0,item)
721 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
724 def create_send_tab(self):
729 grid.setColumnMinimumWidth(3,300)
730 grid.setColumnStretch(5,1)
733 self.payto_e = QLineEdit()
734 grid.addWidget(QLabel(_('Pay to')), 1, 0)
735 grid.addWidget(self.payto_e, 1, 1, 1, 3)
737 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)
739 completer = QCompleter()
740 completer.setCaseSensitivity(False)
741 self.payto_e.setCompleter(completer)
742 completer.setModel(self.completions)
744 self.message_e = QLineEdit()
745 grid.addWidget(QLabel(_('Description')), 2, 0)
746 grid.addWidget(self.message_e, 2, 1, 1, 3)
747 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)
749 self.amount_e = AmountEdit(self.base_unit)
750 grid.addWidget(QLabel(_('Amount')), 3, 0)
751 grid.addWidget(self.amount_e, 3, 1, 1, 2)
752 grid.addWidget(HelpButton(
753 _('Amount to be sent.') + '\n\n' \
754 + _('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.') \
755 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
757 self.fee_e = AmountEdit(self.base_unit)
758 grid.addWidget(QLabel(_('Fee')), 4, 0)
759 grid.addWidget(self.fee_e, 4, 1, 1, 2)
760 grid.addWidget(HelpButton(
761 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
762 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
763 + _('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)
766 self.send_button = EnterButton(_("Send"), self.do_send)
767 grid.addWidget(self.send_button, 6, 1)
769 b = EnterButton(_("Clear"),self.do_clear)
770 grid.addWidget(b, 6, 2)
772 self.payto_sig = QLabel('')
773 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
775 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
776 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
785 def entry_changed( is_fee ):
786 self.funds_error = False
788 if self.amount_e.is_shortcut:
789 self.amount_e.is_shortcut = False
790 c, u = self.wallet.get_account_balance(self.current_account)
791 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
792 fee = self.wallet.estimated_fee(inputs)
794 self.amount_e.setText( self.format_amount(amount) )
795 self.fee_e.setText( self.format_amount( fee ) )
798 amount = self.read_amount(str(self.amount_e.text()))
799 fee = self.read_amount(str(self.fee_e.text()))
801 if not is_fee: fee = None
804 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
806 self.fee_e.setText( self.format_amount( fee ) )
809 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
813 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
814 self.funds_error = True
815 text = _( "Not enough funds" )
816 c, u = self.wallet.get_frozen_balance()
817 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
819 self.statusBar().showMessage(text)
820 self.amount_e.setPalette(palette)
821 self.fee_e.setPalette(palette)
823 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
824 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
826 run_hook('create_send_tab', grid)
830 def update_completions(self):
832 for addr,label in self.wallet.labels.items():
833 if addr in self.wallet.addressbook:
834 l.append( label + ' <' + addr + '>')
836 run_hook('update_completions', l)
837 self.completions.setStringList(l)
841 return lambda s, *args: s.do_protect(func, args)
846 label = unicode( self.message_e.text() )
847 r = unicode( self.payto_e.text() )
850 # label or alias, with address in brackets
851 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
852 to_address = m.group(2) if m else r
854 if not is_valid(to_address):
855 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
859 amount = self.read_amount(unicode( self.amount_e.text()))
861 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
864 fee = self.read_amount(unicode( self.fee_e.text()))
866 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
869 confirm_amount = self.config.get('confirm_amount', 100000000)
870 if amount >= confirm_amount:
871 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
874 self.send_tx(to_address, amount, fee, label)
878 def send_tx(self, to_address, amount, fee, label, password):
881 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
882 except BaseException, e:
883 traceback.print_exc(file=sys.stdout)
884 self.show_message(str(e))
887 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
888 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
892 self.wallet.set_label(tx.hash(), label)
895 h = self.wallet.send_tx(tx)
896 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
897 status, msg = self.wallet.receive_tx( h )
899 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
901 self.update_contacts_tab()
903 QMessageBox.warning(self, _('Error'), msg, _('OK'))
905 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
907 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
908 with open(fileName,'w') as f:
909 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
910 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
912 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
914 # add recipient to addressbook
915 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
916 self.wallet.addressbook.append(to_address)
921 def set_url(self, url):
922 address, amount, label, message, signature, identity, url = util.parse_url(url)
924 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
927 self.mini.set_payment_fields(address, amount)
929 if label and self.wallet.labels.get(address) != label:
930 if self.question('Give label "%s" to address %s ?'%(label,address)):
931 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
932 self.wallet.addressbook.append(address)
933 self.wallet.set_label(address, label)
935 run_hook('set_url', url, self.show_message, self.question)
937 self.tabs.setCurrentIndex(1)
938 label = self.wallet.labels.get(address)
939 m_addr = label + ' <'+ address +'>' if label else address
940 self.payto_e.setText(m_addr)
942 self.message_e.setText(message)
944 self.amount_e.setText(amount)
947 self.set_frozen(self.payto_e,True)
948 self.set_frozen(self.amount_e,True)
949 self.set_frozen(self.message_e,True)
950 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
952 self.payto_sig.setVisible(False)
955 self.payto_sig.setVisible(False)
956 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
958 self.set_frozen(e,False)
961 def set_frozen(self,entry,frozen):
963 entry.setReadOnly(True)
964 entry.setFrame(False)
966 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
967 entry.setPalette(palette)
969 entry.setReadOnly(False)
972 palette.setColor(entry.backgroundRole(), QColor('white'))
973 entry.setPalette(palette)
976 def toggle_freeze(self,addr):
978 if addr in self.wallet.frozen_addresses:
979 self.wallet.unfreeze(addr)
981 self.wallet.freeze(addr)
982 self.update_receive_tab()
984 def toggle_priority(self,addr):
986 if addr in self.wallet.prioritized_addresses:
987 self.wallet.unprioritize(addr)
989 self.wallet.prioritize(addr)
990 self.update_receive_tab()
993 def create_list_tab(self, headers):
994 "generic tab creation method"
995 l = MyTreeWidget(self)
996 l.setColumnCount( len(headers) )
997 l.setHeaderLabels( headers )
1000 vbox = QVBoxLayout()
1007 vbox.addWidget(buttons)
1009 hbox = QHBoxLayout()
1012 buttons.setLayout(hbox)
1017 def create_receive_tab(self):
1018 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1019 l.setContextMenuPolicy(Qt.CustomContextMenu)
1020 l.customContextMenuRequested.connect(self.create_receive_menu)
1021 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1022 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1023 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1024 self.receive_list = l
1025 self.receive_buttons_hbox = hbox
1030 def receive_tab_set_mode(self, i):
1031 self.save_column_widths()
1032 self.expert_mode = (i == 1)
1033 self.config.set_key('classic_expert_mode', self.expert_mode, True)
1034 self.update_receive_tab()
1037 def save_column_widths(self):
1038 if not self.expert_mode:
1039 widths = [ self.receive_list.columnWidth(0) ]
1042 for i in range(self.receive_list.columnCount() -1):
1043 widths.append(self.receive_list.columnWidth(i))
1044 self.column_widths["receive"][self.expert_mode] = widths
1046 self.column_widths["history"] = []
1047 for i in range(self.history_list.columnCount() - 1):
1048 self.column_widths["history"].append(self.history_list.columnWidth(i))
1050 self.column_widths["contacts"] = []
1051 for i in range(self.contacts_list.columnCount() - 1):
1052 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1054 self.config.set_key("column_widths", self.column_widths, True)
1057 def create_contacts_tab(self):
1058 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1059 l.setContextMenuPolicy(Qt.CustomContextMenu)
1060 l.customContextMenuRequested.connect(self.create_contact_menu)
1061 for i,width in enumerate(self.column_widths['contacts']):
1062 l.setColumnWidth(i, width)
1064 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1065 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1066 self.contacts_list = l
1067 self.contacts_buttons_hbox = hbox
1072 def delete_imported_key(self, addr):
1073 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1074 self.wallet.delete_imported_key(addr)
1075 self.update_receive_tab()
1076 self.update_history_tab()
1078 def edit_account_label(self, k):
1079 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':')
1081 label = unicode(text)
1082 self.wallet.set_label(k,label)
1083 self.update_receive_tab()
1085 def create_account_menu(self, position, k, item):
1087 if item.isExpanded():
1088 menu.addAction(_("Minimize"), lambda: item.setExpanded(False))
1090 menu.addAction(_("Maximize"), lambda: item.setExpanded(True))
1091 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1092 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1093 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1095 def create_receive_menu(self, position):
1096 # fixme: this function apparently has a side effect.
1097 # if it is not called the menu pops up several times
1098 #self.receive_list.selectedIndexes()
1100 item = self.receive_list.itemAt(position)
1103 addr = unicode(item.text(0))
1104 if not is_valid(addr):
1105 k = str(item.data(0,32).toString())
1107 self.create_account_menu(position, k, item)
1109 item.setExpanded(not item.isExpanded())
1113 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1114 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1115 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1116 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1117 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1118 if addr in self.wallet.imported_keys:
1119 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1121 if self.expert_mode:
1122 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1123 menu.addAction(t, lambda: self.toggle_freeze(addr))
1124 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1125 menu.addAction(t, lambda: self.toggle_priority(addr))
1127 run_hook('receive_menu', menu)
1128 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1131 def payto(self, addr):
1133 label = self.wallet.labels.get(addr)
1134 m_addr = label + ' <' + addr + '>' if label else addr
1135 self.tabs.setCurrentIndex(1)
1136 self.payto_e.setText(m_addr)
1137 self.amount_e.setFocus()
1140 def delete_contact(self, x):
1141 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1142 self.wallet.delete_contact(x)
1143 self.wallet.set_label(x, None)
1144 self.update_history_tab()
1145 self.update_contacts_tab()
1146 self.update_completions()
1149 def create_contact_menu(self, position):
1150 item = self.contacts_list.itemAt(position)
1152 addr = unicode(item.text(0))
1153 label = unicode(item.text(1))
1154 is_editable = item.data(0,32).toBool()
1155 payto_addr = item.data(0,33).toString()
1157 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1158 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1159 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1161 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1162 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1164 run_hook('create_contact_menu', menu, item)
1165 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1168 def update_receive_item(self, item):
1169 item.setFont(0, QFont(MONOSPACE_FONT))
1170 address = str(item.data(0,0).toString())
1171 label = self.wallet.labels.get(address,'')
1172 item.setData(1,0,label)
1173 item.setData(0,32, True) # is editable
1175 run_hook('update_receive_item', address, item)
1177 c, u = self.wallet.get_addr_balance(address)
1178 balance = self.format_amount(c + u)
1179 item.setData(2,0,balance)
1181 if self.expert_mode:
1182 if address in self.wallet.frozen_addresses:
1183 item.setBackgroundColor(0, QColor('lightblue'))
1184 elif address in self.wallet.prioritized_addresses:
1185 item.setBackgroundColor(0, QColor('lightgreen'))
1188 def update_receive_tab(self):
1189 l = self.receive_list
1192 l.setColumnHidden(2, not self.expert_mode)
1193 l.setColumnHidden(3, not self.expert_mode)
1194 for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1195 l.setColumnWidth(i, width)
1197 if self.current_account is None:
1198 account_items = self.wallet.accounts.items()
1199 elif self.current_account != -1:
1200 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1204 for k, account in account_items:
1205 name = self.wallet.get_account_name(k)
1206 c,u = self.wallet.get_account_balance(k)
1207 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1208 l.addTopLevelItem(account_item)
1209 account_item.setExpanded(True)
1210 account_item.setData(0, 32, k)
1212 if not self.wallet.is_seeded(k):
1213 icon = QIcon(":icons/key.png")
1214 account_item.setIcon(0, icon)
1216 for is_change in ([0,1] if self.expert_mode else [0]):
1217 if self.expert_mode:
1218 name = _("Receiving") if not is_change else _("Change")
1219 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1220 account_item.addChild(seq_item)
1221 if not is_change: seq_item.setExpanded(True)
1223 seq_item = account_item
1227 for address in account.get_addresses(is_change):
1228 h = self.wallet.history.get(address,[])
1232 if gap > self.wallet.gap_limit:
1237 num_tx = '*' if h == ['*'] else "%d"%len(h)
1238 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1239 self.update_receive_item(item)
1241 item.setBackgroundColor(1, QColor('red'))
1242 seq_item.addChild(item)
1245 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1246 c,u = self.wallet.get_imported_balance()
1247 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1248 l.addTopLevelItem(account_item)
1249 account_item.setExpanded(True)
1250 for address in self.wallet.imported_keys.keys():
1251 item = QTreeWidgetItem( [ address, '', '', ''] )
1252 self.update_receive_item(item)
1253 account_item.addChild(item)
1256 # we use column 1 because column 0 may be hidden
1257 l.setCurrentItem(l.topLevelItem(0),1)
1260 def update_contacts_tab(self):
1261 l = self.contacts_list
1264 for address in self.wallet.addressbook:
1265 label = self.wallet.labels.get(address,'')
1266 n = self.wallet.get_num_tx(address)
1267 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1268 item.setFont(0, QFont(MONOSPACE_FONT))
1269 # 32 = label can be edited (bool)
1270 item.setData(0,32, True)
1272 item.setData(0,33, address)
1273 l.addTopLevelItem(item)
1275 run_hook('update_contacts_tab', l)
1276 l.setCurrentItem(l.topLevelItem(0))
1280 def create_console_tab(self):
1281 from console import Console
1282 self.console = console = Console()
1286 def update_console(self):
1287 console = self.console
1288 console.history = self.config.get("console-history",[])
1289 console.history_index = len(console.history)
1291 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1292 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1294 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1296 def mkfunc(f, method):
1297 return lambda *args: apply( f, (method, args, self.password_dialog ))
1299 if m[0]=='_' or m=='wallet' or m == 'interface': continue
1300 methods[m] = mkfunc(c._run, m)
1302 console.updateNamespace(methods)
1305 def change_account(self,s):
1306 if s == _("All accounts"):
1307 self.current_account = None
1309 accounts = self.wallet.get_account_names()
1310 for k, v in accounts.items():
1312 self.current_account = k
1313 self.update_history_tab()
1314 self.update_status()
1315 self.update_receive_tab()
1317 def create_status_bar(self):
1320 sb.setFixedHeight(35)
1321 qtVersion = qVersion()
1323 self.balance_label = QLabel("")
1324 sb.addWidget(self.balance_label)
1326 from version_getter import UpdateLabel
1327 self.updatelabel = UpdateLabel(self.config, sb)
1329 self.account_selector = QComboBox()
1330 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1331 sb.addPermanentWidget(self.account_selector)
1333 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1334 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1336 self.lock_icon = QIcon()
1337 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1338 sb.addPermanentWidget( self.password_button )
1340 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1341 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1342 sb.addPermanentWidget( self.seed_button )
1343 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1344 sb.addPermanentWidget( self.status_button )
1346 run_hook('create_status_bar', (sb,))
1348 self.setStatusBar(sb)
1351 def update_lock_icon(self):
1352 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1353 self.password_button.setIcon( icon )
1356 def update_buttons_on_seed(self):
1357 if self.wallet.seed:
1358 self.seed_button.show()
1359 self.password_button.show()
1360 self.send_button.setText(_("Send"))
1362 self.password_button.hide()
1363 self.seed_button.hide()
1364 self.send_button.setText(_("Create unsigned transaction"))
1367 def change_password_dialog(self):
1368 from password_dialog import PasswordDialog
1369 d = PasswordDialog(self.wallet, self)
1371 self.update_lock_icon()
1374 def new_contact_dialog(self):
1375 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1376 address = unicode(text)
1378 if is_valid(address):
1379 self.wallet.add_contact(address)
1380 self.update_contacts_tab()
1381 self.update_history_tab()
1382 self.update_completions()
1384 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1387 def new_account_dialog(self):
1389 dialog = QDialog(self)
1391 dialog.setWindowTitle(_("New Account"))
1393 addr = self.wallet.new_account_address()
1394 vbox = QVBoxLayout()
1395 msg = _("Electrum considers that an account exists only if it contains bitcoins.") + '\n' \
1396 + _("To create a new account, please send coins to the first address of that account.") + '\n' \
1397 + _("Note: you will need to wait for 2 confirmations before the account is created.")
1398 vbox.addWidget(QLabel(msg))
1399 vbox.addWidget(QLabel(_('Address')+':'))
1404 vbox.addLayout(ok_cancel_buttons(dialog))
1405 dialog.setLayout(vbox)
1412 def show_master_public_key(self):
1413 dialog = QDialog(self)
1415 dialog.setWindowTitle(_("Master Public Key"))
1417 main_text = QTextEdit()
1418 main_text.setText(self.wallet.get_master_public_key())
1419 main_text.setReadOnly(True)
1420 main_text.setMaximumHeight(170)
1421 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1423 ok_button = QPushButton(_("OK"))
1424 ok_button.setDefault(True)
1425 ok_button.clicked.connect(dialog.accept)
1427 main_layout = QGridLayout()
1428 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1430 main_layout.addWidget(main_text, 1, 0)
1431 main_layout.addWidget(qrw, 1, 1 )
1433 vbox = QVBoxLayout()
1434 vbox.addLayout(main_layout)
1435 hbox = QHBoxLayout()
1437 hbox.addWidget(ok_button)
1438 vbox.addLayout(hbox)
1440 dialog.setLayout(vbox)
1445 def show_seed_dialog(self, password):
1446 if not self.wallet.seed:
1447 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1450 seed = self.wallet.decode_seed(password)
1452 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1455 from seed_dialog import SeedDialog
1456 d = SeedDialog(self)
1457 d.show_seed(seed, self.wallet.imported_keys)
1461 def show_qrcode(self, data, title = _("QR code")):
1465 d.setWindowTitle(title)
1466 d.setMinimumSize(270, 300)
1467 vbox = QVBoxLayout()
1468 qrw = QRCodeWidget(data)
1469 vbox.addWidget(qrw, 1)
1470 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1471 hbox = QHBoxLayout()
1475 filename = "qrcode.bmp"
1476 bmp.save_qrcode(qrw.qr, filename)
1477 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1479 b = QPushButton(_("Save"))
1481 b.clicked.connect(print_qr)
1483 b = QPushButton(_("Close"))
1485 b.clicked.connect(d.accept)
1488 vbox.addLayout(hbox)
1493 def do_protect(self, func, args):
1494 if self.wallet.use_encryption:
1495 password = self.password_dialog()
1501 if args != (False,):
1502 args = (self,) + args + (password,)
1504 args = (self,password)
1509 def show_private_key(self, address, password):
1510 if not address: return
1512 pk_list = self.wallet.get_private_key(address, password)
1513 except BaseException, e:
1514 self.show_message(str(e))
1516 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1520 def do_sign(self, address, message, signature, password):
1521 message = unicode(message.toPlainText())
1522 message = message.encode('utf-8')
1524 sig = self.wallet.sign_message(str(address.text()), message, password)
1525 signature.setText(sig)
1526 except BaseException, e:
1527 self.show_message(str(e))
1529 def sign_message(self, address):
1530 if not address: return
1533 d.setWindowTitle(_('Sign Message'))
1534 d.setMinimumSize(410, 290)
1536 tab_widget = QTabWidget()
1538 layout = QGridLayout(tab)
1540 sign_address = QLineEdit()
1542 sign_address.setText(address)
1543 layout.addWidget(QLabel(_('Address')), 1, 0)
1544 layout.addWidget(sign_address, 1, 1)
1546 sign_message = QTextEdit()
1547 layout.addWidget(QLabel(_('Message')), 2, 0)
1548 layout.addWidget(sign_message, 2, 1)
1549 layout.setRowStretch(2,3)
1551 sign_signature = QTextEdit()
1552 layout.addWidget(QLabel(_('Signature')), 3, 0)
1553 layout.addWidget(sign_signature, 3, 1)
1554 layout.setRowStretch(3,1)
1557 hbox = QHBoxLayout()
1558 b = QPushButton(_("Sign"))
1560 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1561 b = QPushButton(_("Close"))
1562 b.clicked.connect(d.accept)
1564 layout.addLayout(hbox, 4, 1)
1565 tab_widget.addTab(tab, _("Sign"))
1569 layout = QGridLayout(tab)
1571 verify_address = QLineEdit()
1572 layout.addWidget(QLabel(_('Address')), 1, 0)
1573 layout.addWidget(verify_address, 1, 1)
1575 verify_message = QTextEdit()
1576 layout.addWidget(QLabel(_('Message')), 2, 0)
1577 layout.addWidget(verify_message, 2, 1)
1578 layout.setRowStretch(2,3)
1580 verify_signature = QTextEdit()
1581 layout.addWidget(QLabel(_('Signature')), 3, 0)
1582 layout.addWidget(verify_signature, 3, 1)
1583 layout.setRowStretch(3,1)
1586 message = unicode(verify_message.toPlainText())
1587 message = message.encode('utf-8')
1588 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1589 self.show_message(_("Signature verified"))
1591 self.show_message(_("Error: wrong signature"))
1593 hbox = QHBoxLayout()
1594 b = QPushButton(_("Verify"))
1595 b.clicked.connect(do_verify)
1597 b = QPushButton(_("Close"))
1598 b.clicked.connect(d.accept)
1600 layout.addLayout(hbox, 4, 1)
1601 tab_widget.addTab(tab, _("Verify"))
1603 vbox = QVBoxLayout()
1604 vbox.addWidget(tab_widget)
1611 def question(self, msg):
1612 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1614 def show_message(self, msg):
1615 QMessageBox.information(self, _('Message'), msg, _('OK'))
1617 def password_dialog(self ):
1624 vbox = QVBoxLayout()
1625 msg = _('Please enter your password')
1626 vbox.addWidget(QLabel(msg))
1628 grid = QGridLayout()
1630 grid.addWidget(QLabel(_('Password')), 1, 0)
1631 grid.addWidget(pw, 1, 1)
1632 vbox.addLayout(grid)
1634 vbox.addLayout(ok_cancel_buttons(d))
1637 run_hook('password_dialog', pw, grid, 1)
1638 if not d.exec_(): return
1639 return unicode(pw.text())
1648 def tx_from_text(self, txt):
1649 "json or raw hexadecimal"
1652 tx = Transaction(txt)
1658 tx_dict = json.loads(str(txt))
1659 assert "hex" in tx_dict.keys()
1660 assert "complete" in tx_dict.keys()
1661 if not tx_dict["complete"]:
1662 assert "input_info" in tx_dict.keys()
1663 tx = Transaction(tx_dict["hex"])
1668 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1672 def read_tx_from_file(self):
1673 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1677 with open(fileName, "r") as f:
1678 file_content = f.read()
1679 except (ValueError, IOError, os.error), reason:
1680 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1682 return self.tx_from_text(file_content)
1686 def sign_raw_transaction(self, tx, input_info, password):
1687 self.wallet.signrawtransaction(tx, input_info, [], password)
1689 def do_process_from_text(self):
1690 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1693 tx = self.tx_from_text(text)
1695 self.show_transaction(tx)
1697 def do_process_from_file(self):
1698 tx = self.read_tx_from_file()
1700 self.show_transaction(tx)
1702 def do_process_from_csvReader(self, csvReader):
1705 for row in csvReader:
1707 amount = float(row[1])
1708 amount = int(100000000*amount)
1709 outputs.append((address, amount))
1710 except (ValueError, IOError, os.error), reason:
1711 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1715 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1716 except BaseException, e:
1717 self.show_message(str(e))
1720 self.show_transaction(tx)
1722 def do_process_from_csv_file(self):
1723 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1727 with open(fileName, "r") as f:
1728 csvReader = csv.reader(f)
1729 self.do_process_from_csvReader(csvReader)
1730 except (ValueError, IOError, os.error), reason:
1731 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1734 def do_process_from_csv_text(self):
1735 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1736 + _("Format: address, amount. One output per line"), _("Load CSV"))
1739 f = StringIO.StringIO(text)
1740 csvReader = csv.reader(f)
1741 self.do_process_from_csvReader(csvReader)
1746 def do_export_privkeys(self, password):
1747 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.")))
1750 select_export = _('Select file to export your private keys to')
1751 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1753 with open(fileName, "w+") as csvfile:
1754 transaction = csv.writer(csvfile)
1755 transaction.writerow(["address", "private_key"])
1757 addresses = self.wallet.addresses(True)
1759 for addr in addresses:
1760 pk = "".join(self.wallet.get_private_key(addr, password))
1761 transaction.writerow(["%34s"%addr,pk])
1763 self.show_message(_("Private keys exported."))
1765 except (IOError, os.error), reason:
1766 export_error_label = _("Electrum was unable to produce a private key-export.")
1767 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1769 except BaseException, e:
1770 self.show_message(str(e))
1774 def do_import_labels(self):
1775 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1776 if not labelsFile: return
1778 f = open(labelsFile, 'r')
1781 for key, value in json.loads(data).items():
1782 self.wallet.set_label(key, value)
1783 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1784 except (IOError, os.error), reason:
1785 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1788 def do_export_labels(self):
1789 labels = self.wallet.labels
1791 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1793 with open(fileName, 'w+') as f:
1794 json.dump(labels, f)
1795 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1796 except (IOError, os.error), reason:
1797 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1800 def do_export_history(self):
1801 from lite_window import csv_transaction
1802 csv_transaction(self.wallet)
1806 def do_import_privkey(self, password):
1807 if not self.wallet.imported_keys:
1808 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1809 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1810 + _('Are you sure you understand what you are doing?'), 3, 4)
1813 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1816 text = str(text).split()
1821 addr = self.wallet.import_key(key, password)
1822 except BaseException as e:
1828 addrlist.append(addr)
1830 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1832 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1833 self.update_receive_tab()
1834 self.update_history_tab()
1837 def settings_dialog(self):
1839 d.setWindowTitle(_('Electrum Settings'))
1841 vbox = QVBoxLayout()
1843 tabs = QTabWidget(self)
1844 self.settings_tab = tabs
1845 vbox.addWidget(tabs)
1848 grid_ui = QGridLayout(tab1)
1849 grid_ui.setColumnStretch(0,1)
1850 tabs.addTab(tab1, _('Display') )
1852 nz_label = QLabel(_('Display zeros'))
1853 grid_ui.addWidget(nz_label, 0, 0)
1854 nz_e = AmountEdit(None,True)
1855 nz_e.setText("%d"% self.num_zeros)
1856 grid_ui.addWidget(nz_e, 0, 1)
1857 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1858 grid_ui.addWidget(HelpButton(msg), 0, 2)
1859 if not self.config.is_modifiable('num_zeros'):
1860 for w in [nz_e, nz_label]: w.setEnabled(False)
1862 lang_label=QLabel(_('Language') + ':')
1863 grid_ui.addWidget(lang_label, 1, 0)
1864 lang_combo = QComboBox()
1865 from electrum.i18n import languages
1866 lang_combo.addItems(languages.values())
1868 index = languages.keys().index(self.config.get("language",''))
1871 lang_combo.setCurrentIndex(index)
1872 grid_ui.addWidget(lang_combo, 1, 1)
1873 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1874 if not self.config.is_modifiable('language'):
1875 for w in [lang_combo, lang_label]: w.setEnabled(False)
1877 expert_cb = QCheckBox(_('Expert mode'))
1878 expert_cb.setChecked(self.expert_mode)
1879 grid_ui.addWidget(expert_cb, 3, 0)
1880 hh = _('In expert mode, your client will:') + '\n' \
1881 + _(' - Show change addresses in the Receive tab') + '\n' \
1882 + _(' - Display the balance of each address') + '\n' \
1883 + _(' - Add freeze/prioritize actions to addresses.')
1884 grid_ui.addWidget(HelpButton(hh), 3, 2)
1885 grid_ui.setRowStretch(4,1)
1889 grid_wallet = QGridLayout(tab2)
1890 grid_wallet.setColumnStretch(0,1)
1891 tabs.addTab(tab2, _('Wallet') )
1893 fee_label = QLabel(_('Transaction fee'))
1894 grid_wallet.addWidget(fee_label, 0, 0)
1895 fee_e = AmountEdit(self.base_unit)
1896 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1897 grid_wallet.addWidget(fee_e, 0, 2)
1898 msg = _('Fee per kilobyte of transaction.') + ' ' \
1899 + _('Recommended value') + ': ' + self.format_amount(50000)
1900 grid_wallet.addWidget(HelpButton(msg), 0, 3)
1901 if not self.config.is_modifiable('fee_per_kb'):
1902 for w in [fee_e, fee_label]: w.setEnabled(False)
1904 usechange_cb = QCheckBox(_('Use change addresses'))
1905 usechange_cb.setChecked(self.wallet.use_change)
1906 grid_wallet.addWidget(usechange_cb, 1, 0)
1907 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1908 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1910 units = ['BTC', 'mBTC']
1911 unit_label = QLabel(_('Base unit'))
1912 grid_wallet.addWidget(unit_label, 3, 0)
1913 unit_combo = QComboBox()
1914 unit_combo.addItems(units)
1915 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1916 grid_wallet.addWidget(unit_combo, 3, 2)
1917 grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1918 + '\n1BTC=1000mBTC.\n' \
1919 + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1920 grid_wallet.setRowStretch(4,1)
1923 run_hook('create_settings_tab', tabs)
1925 vbox.addLayout(ok_cancel_buttons(d))
1929 if not d.exec_(): return
1931 fee = unicode(fee_e.text())
1933 fee = self.read_amount(fee)
1935 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1938 self.wallet.set_fee(fee)
1940 nz = unicode(nz_e.text())
1945 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1948 if self.num_zeros != nz:
1950 self.config.set_key('num_zeros', nz, True)
1951 self.update_history_tab()
1952 self.update_receive_tab()
1954 usechange_result = usechange_cb.isChecked()
1955 if self.wallet.use_change != usechange_result:
1956 self.wallet.use_change = usechange_result
1957 self.config.set_key('use_change', self.wallet.use_change, True)
1959 unit_result = units[unit_combo.currentIndex()]
1960 if self.base_unit() != unit_result:
1961 self.decimal_point = 8 if unit_result == 'BTC' else 5
1962 self.config.set_key('decimal_point', self.decimal_point, True)
1963 self.update_history_tab()
1964 self.update_status()
1966 need_restart = False
1968 lang_request = languages.keys()[lang_combo.currentIndex()]
1969 if lang_request != self.config.get('language'):
1970 self.config.set_key("language", lang_request, True)
1974 run_hook('close_settings_dialog')
1977 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
1979 self.receive_tab_set_mode(expert_cb.isChecked())
1981 def run_network_dialog(self):
1982 NetworkDialog(self.wallet.network, self.config, self).do_exec()
1984 def closeEvent(self, event):
1986 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1987 self.save_column_widths()
1988 self.config.set_key("console-history", self.console.history[-50:], True)
1993 def plugins_dialog(self):
1994 from electrum.plugins import plugins
1997 d.setWindowTitle(_('Electrum Plugins'))
2000 vbox = QVBoxLayout(d)
2003 scroll = QScrollArea()
2004 scroll.setEnabled(True)
2005 scroll.setWidgetResizable(True)
2006 scroll.setMinimumSize(400,250)
2007 vbox.addWidget(scroll)
2011 w.setMinimumHeight(len(plugins)*35)
2013 grid = QGridLayout()
2014 grid.setColumnStretch(0,1)
2017 def mk_toggle(cb, p):
2018 return lambda: cb.setChecked(p.toggle())
2019 for i, p in enumerate(plugins):
2021 cb = QCheckBox(p.fullname())
2022 cb.setDisabled(not p.is_available())
2023 cb.setChecked(p.is_enabled())
2024 cb.clicked.connect(mk_toggle(cb,p))
2025 grid.addWidget(cb, i, 0)
2026 if p.requires_settings():
2027 grid.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
2028 grid.addWidget(HelpButton(p.description()), i, 2)
2030 print_msg(_("Error: cannot display plugin"), p)
2031 traceback.print_exc(file=sys.stdout)
2032 grid.setRowStretch(i+1,1)
2034 vbox.addLayout(close_button(d))
2039 def show_account_details(self, k):
2041 d.setWindowTitle(_('Account Details'))
2044 vbox = QVBoxLayout(d)
2045 roots = self.wallet.get_roots(k)
2047 name = self.wallet.get_account_name(k)
2048 label = QLabel('Name: ' + name)
2049 vbox.addWidget(label)
2051 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2052 vbox.addWidget(QLabel('Type: ' + acctype))
2054 label = QLabel('Derivation: ' + k)
2055 vbox.addWidget(label)
2058 # mpk = self.wallet.master_public_keys[root]
2059 # text = QTextEdit()
2060 # text.setReadOnly(True)
2061 # text.setMaximumHeight(120)
2062 # text.setText(repr(mpk))
2063 # vbox.addWidget(text)
2065 vbox.addLayout(close_button(d))