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(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 = QVBoxLayout()
1427 vbox.addLayout(main_layout)
1428 hbox = QHBoxLayout()
1430 hbox.addWidget(ok_button)
1431 vbox.addLayout(hbox)
1433 dialog.setLayout(vbox)
1438 def show_seed_dialog(self, password):
1439 if not self.wallet.seed:
1440 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1443 seed = self.wallet.decode_seed(password)
1445 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1448 from seed_dialog import SeedDialog
1449 d = SeedDialog(self)
1450 d.show_seed(seed, self.wallet.imported_keys)
1454 def show_qrcode(self, data, title = _("QR code")):
1458 d.setWindowTitle(title)
1459 d.setMinimumSize(270, 300)
1460 vbox = QVBoxLayout()
1461 qrw = QRCodeWidget(data)
1462 vbox.addWidget(qrw, 1)
1463 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1464 hbox = QHBoxLayout()
1468 filename = "qrcode.bmp"
1469 bmp.save_qrcode(qrw.qr, filename)
1470 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1472 b = QPushButton(_("Save"))
1474 b.clicked.connect(print_qr)
1476 b = QPushButton(_("Close"))
1478 b.clicked.connect(d.accept)
1481 vbox.addLayout(hbox)
1486 def do_protect(self, func, args):
1487 if self.wallet.use_encryption:
1488 password = self.password_dialog()
1494 if args != (False,):
1495 args = (self,) + args + (password,)
1497 args = (self,password)
1502 def show_private_key(self, address, password):
1503 if not address: return
1505 pk_list = self.wallet.get_private_key(address, password)
1506 except BaseException, e:
1507 self.show_message(str(e))
1509 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1513 def do_sign(self, address, message, signature, password):
1514 message = unicode(message.toPlainText())
1515 message = message.encode('utf-8')
1517 sig = self.wallet.sign_message(str(address.text()), message, password)
1518 signature.setText(sig)
1519 except BaseException, e:
1520 self.show_message(str(e))
1522 def sign_message(self, address):
1523 if not address: return
1526 d.setWindowTitle(_('Sign Message'))
1527 d.setMinimumSize(410, 290)
1529 tab_widget = QTabWidget()
1531 layout = QGridLayout(tab)
1533 sign_address = QLineEdit()
1535 sign_address.setText(address)
1536 layout.addWidget(QLabel(_('Address')), 1, 0)
1537 layout.addWidget(sign_address, 1, 1)
1539 sign_message = QTextEdit()
1540 layout.addWidget(QLabel(_('Message')), 2, 0)
1541 layout.addWidget(sign_message, 2, 1)
1542 layout.setRowStretch(2,3)
1544 sign_signature = QTextEdit()
1545 layout.addWidget(QLabel(_('Signature')), 3, 0)
1546 layout.addWidget(sign_signature, 3, 1)
1547 layout.setRowStretch(3,1)
1550 hbox = QHBoxLayout()
1551 b = QPushButton(_("Sign"))
1553 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1554 b = QPushButton(_("Close"))
1555 b.clicked.connect(d.accept)
1557 layout.addLayout(hbox, 4, 1)
1558 tab_widget.addTab(tab, _("Sign"))
1562 layout = QGridLayout(tab)
1564 verify_address = QLineEdit()
1565 layout.addWidget(QLabel(_('Address')), 1, 0)
1566 layout.addWidget(verify_address, 1, 1)
1568 verify_message = QTextEdit()
1569 layout.addWidget(QLabel(_('Message')), 2, 0)
1570 layout.addWidget(verify_message, 2, 1)
1571 layout.setRowStretch(2,3)
1573 verify_signature = QTextEdit()
1574 layout.addWidget(QLabel(_('Signature')), 3, 0)
1575 layout.addWidget(verify_signature, 3, 1)
1576 layout.setRowStretch(3,1)
1579 message = unicode(verify_message.toPlainText())
1580 message = message.encode('utf-8')
1581 if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1582 self.show_message(_("Signature verified"))
1584 self.show_message(_("Error: wrong signature"))
1586 hbox = QHBoxLayout()
1587 b = QPushButton(_("Verify"))
1588 b.clicked.connect(do_verify)
1590 b = QPushButton(_("Close"))
1591 b.clicked.connect(d.accept)
1593 layout.addLayout(hbox, 4, 1)
1594 tab_widget.addTab(tab, _("Verify"))
1596 vbox = QVBoxLayout()
1597 vbox.addWidget(tab_widget)
1604 def question(self, msg):
1605 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1607 def show_message(self, msg):
1608 QMessageBox.information(self, _('Message'), msg, _('OK'))
1610 def password_dialog(self ):
1617 vbox = QVBoxLayout()
1618 msg = _('Please enter your password')
1619 vbox.addWidget(QLabel(msg))
1621 grid = QGridLayout()
1623 grid.addWidget(QLabel(_('Password')), 1, 0)
1624 grid.addWidget(pw, 1, 1)
1625 vbox.addLayout(grid)
1627 vbox.addLayout(ok_cancel_buttons(d))
1630 run_hook('password_dialog', pw, grid, 1)
1631 if not d.exec_(): return
1632 return unicode(pw.text())
1641 def tx_from_text(self, txt):
1642 "json or raw hexadecimal"
1645 tx = Transaction(txt)
1651 tx_dict = json.loads(str(txt))
1652 assert "hex" in tx_dict.keys()
1653 assert "complete" in tx_dict.keys()
1654 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1655 if not tx_dict["complete"]:
1656 assert "input_info" in tx_dict.keys()
1657 input_info = json.loads(tx_dict['input_info'])
1658 tx.add_input_info(input_info)
1663 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1667 def read_tx_from_file(self):
1668 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1672 with open(fileName, "r") as f:
1673 file_content = f.read()
1674 except (ValueError, IOError, os.error), reason:
1675 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1677 return self.tx_from_text(file_content)
1681 def sign_raw_transaction(self, tx, input_info, password):
1682 self.wallet.signrawtransaction(tx, input_info, [], password)
1684 def do_process_from_text(self):
1685 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1688 tx = self.tx_from_text(text)
1690 self.show_transaction(tx)
1692 def do_process_from_file(self):
1693 tx = self.read_tx_from_file()
1695 self.show_transaction(tx)
1697 def do_process_from_csvReader(self, csvReader):
1700 for row in csvReader:
1702 amount = float(row[1])
1703 amount = int(100000000*amount)
1704 outputs.append((address, amount))
1705 except (ValueError, IOError, os.error), reason:
1706 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1710 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1711 except BaseException, e:
1712 self.show_message(str(e))
1715 self.show_transaction(tx)
1717 def do_process_from_csv_file(self):
1718 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1722 with open(fileName, "r") as f:
1723 csvReader = csv.reader(f)
1724 self.do_process_from_csvReader(csvReader)
1725 except (ValueError, IOError, os.error), reason:
1726 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1729 def do_process_from_csv_text(self):
1730 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1731 + _("Format: address, amount. One output per line"), _("Load CSV"))
1734 f = StringIO.StringIO(text)
1735 csvReader = csv.reader(f)
1736 self.do_process_from_csvReader(csvReader)
1741 def do_export_privkeys(self, password):
1742 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.")))
1745 select_export = _('Select file to export your private keys to')
1746 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1748 with open(fileName, "w+") as csvfile:
1749 transaction = csv.writer(csvfile)
1750 transaction.writerow(["address", "private_key"])
1752 addresses = self.wallet.addresses(True)
1754 for addr in addresses:
1755 pk = "".join(self.wallet.get_private_key(addr, password))
1756 transaction.writerow(["%34s"%addr,pk])
1758 self.show_message(_("Private keys exported."))
1760 except (IOError, os.error), reason:
1761 export_error_label = _("Electrum was unable to produce a private key-export.")
1762 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1764 except BaseException, e:
1765 self.show_message(str(e))
1769 def do_import_labels(self):
1770 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1771 if not labelsFile: return
1773 f = open(labelsFile, 'r')
1776 for key, value in json.loads(data).items():
1777 self.wallet.set_label(key, value)
1778 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1779 except (IOError, os.error), reason:
1780 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1783 def do_export_labels(self):
1784 labels = self.wallet.labels
1786 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1788 with open(fileName, 'w+') as f:
1789 json.dump(labels, f)
1790 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1791 except (IOError, os.error), reason:
1792 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1795 def do_export_history(self):
1796 from lite_window import csv_transaction
1797 csv_transaction(self.wallet)
1801 def do_import_privkey(self, password):
1802 if not self.wallet.imported_keys:
1803 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1804 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1805 + _('Are you sure you understand what you are doing?'), 3, 4)
1808 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1811 text = str(text).split()
1816 addr = self.wallet.import_key(key, password)
1817 except BaseException as e:
1823 addrlist.append(addr)
1825 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1827 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1828 self.update_receive_tab()
1829 self.update_history_tab()
1832 def settings_dialog(self):
1834 d.setWindowTitle(_('Electrum Settings'))
1836 vbox = QVBoxLayout()
1838 tabs = QTabWidget(self)
1839 self.settings_tab = tabs
1840 vbox.addWidget(tabs)
1843 grid_ui = QGridLayout(tab1)
1844 grid_ui.setColumnStretch(0,1)
1845 tabs.addTab(tab1, _('Display') )
1847 nz_label = QLabel(_('Display zeros'))
1848 grid_ui.addWidget(nz_label, 0, 0)
1849 nz_e = AmountEdit(None,True)
1850 nz_e.setText("%d"% self.num_zeros)
1851 grid_ui.addWidget(nz_e, 0, 1)
1852 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1853 grid_ui.addWidget(HelpButton(msg), 0, 2)
1854 if not self.config.is_modifiable('num_zeros'):
1855 for w in [nz_e, nz_label]: w.setEnabled(False)
1857 lang_label=QLabel(_('Language') + ':')
1858 grid_ui.addWidget(lang_label, 1, 0)
1859 lang_combo = QComboBox()
1860 from electrum.i18n import languages
1861 lang_combo.addItems(languages.values())
1863 index = languages.keys().index(self.config.get("language",''))
1866 lang_combo.setCurrentIndex(index)
1867 grid_ui.addWidget(lang_combo, 1, 1)
1868 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1869 if not self.config.is_modifiable('language'):
1870 for w in [lang_combo, lang_label]: w.setEnabled(False)
1872 expert_cb = QCheckBox(_('Expert mode'))
1873 expert_cb.setChecked(self.expert_mode)
1874 grid_ui.addWidget(expert_cb, 3, 0)
1875 hh = _('In expert mode, your client will:') + '\n' \
1876 + _(' - Show change addresses in the Receive tab') + '\n' \
1877 + _(' - Display the balance of each address') + '\n' \
1878 + _(' - Add freeze/prioritize actions to addresses.')
1879 grid_ui.addWidget(HelpButton(hh), 3, 2)
1880 grid_ui.setRowStretch(4,1)
1884 grid_wallet = QGridLayout(tab2)
1885 grid_wallet.setColumnStretch(0,1)
1886 tabs.addTab(tab2, _('Wallet') )
1888 fee_label = QLabel(_('Transaction fee'))
1889 grid_wallet.addWidget(fee_label, 0, 0)
1890 fee_e = AmountEdit(self.base_unit)
1891 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1892 grid_wallet.addWidget(fee_e, 0, 2)
1893 msg = _('Fee per kilobyte of transaction.') + ' ' \
1894 + _('Recommended value') + ': ' + self.format_amount(50000)
1895 grid_wallet.addWidget(HelpButton(msg), 0, 3)
1896 if not self.config.is_modifiable('fee_per_kb'):
1897 for w in [fee_e, fee_label]: w.setEnabled(False)
1899 usechange_cb = QCheckBox(_('Use change addresses'))
1900 usechange_cb.setChecked(self.wallet.use_change)
1901 grid_wallet.addWidget(usechange_cb, 1, 0)
1902 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1903 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
1905 units = ['BTC', 'mBTC']
1906 unit_label = QLabel(_('Base unit'))
1907 grid_wallet.addWidget(unit_label, 3, 0)
1908 unit_combo = QComboBox()
1909 unit_combo.addItems(units)
1910 unit_combo.setCurrentIndex(units.index(self.base_unit()))
1911 grid_wallet.addWidget(unit_combo, 3, 2)
1912 grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
1913 + '\n1BTC=1000mBTC.\n' \
1914 + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
1915 grid_wallet.setRowStretch(4,1)
1918 run_hook('create_settings_tab', tabs)
1920 vbox.addLayout(ok_cancel_buttons(d))
1924 if not d.exec_(): return
1926 fee = unicode(fee_e.text())
1928 fee = self.read_amount(fee)
1930 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1933 self.wallet.set_fee(fee)
1935 nz = unicode(nz_e.text())
1940 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1943 if self.num_zeros != nz:
1945 self.config.set_key('num_zeros', nz, True)
1946 self.update_history_tab()
1947 self.update_receive_tab()
1949 usechange_result = usechange_cb.isChecked()
1950 if self.wallet.use_change != usechange_result:
1951 self.wallet.use_change = usechange_result
1952 self.config.set_key('use_change', self.wallet.use_change, True)
1954 unit_result = units[unit_combo.currentIndex()]
1955 if self.base_unit() != unit_result:
1956 self.decimal_point = 8 if unit_result == 'BTC' else 5
1957 self.config.set_key('decimal_point', self.decimal_point, True)
1958 self.update_history_tab()
1959 self.update_status()
1961 need_restart = False
1963 lang_request = languages.keys()[lang_combo.currentIndex()]
1964 if lang_request != self.config.get('language'):
1965 self.config.set_key("language", lang_request, True)
1969 run_hook('close_settings_dialog')
1972 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
1974 self.receive_tab_set_mode(expert_cb.isChecked())
1976 def run_network_dialog(self):
1977 NetworkDialog(self.wallet.network, self.config, self).do_exec()
1979 def closeEvent(self, event):
1981 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1982 self.save_column_widths()
1983 self.config.set_key("console-history", self.console.history[-50:], True)
1988 def plugins_dialog(self):
1989 from electrum.plugins import plugins
1992 d.setWindowTitle(_('Electrum Plugins'))
1995 vbox = QVBoxLayout(d)
1998 scroll = QScrollArea()
1999 scroll.setEnabled(True)
2000 scroll.setWidgetResizable(True)
2001 scroll.setMinimumSize(400,250)
2002 vbox.addWidget(scroll)
2006 w.setMinimumHeight(len(plugins)*35)
2008 grid = QGridLayout()
2009 grid.setColumnStretch(0,1)
2012 def mk_toggle(cb, p):
2013 return lambda: cb.setChecked(p.toggle())
2014 for i, p in enumerate(plugins):
2016 cb = QCheckBox(p.fullname())
2017 cb.setDisabled(not p.is_available())
2018 cb.setChecked(p.is_enabled())
2019 cb.clicked.connect(mk_toggle(cb,p))
2020 grid.addWidget(cb, i, 0)
2021 if p.requires_settings():
2022 grid.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
2023 grid.addWidget(HelpButton(p.description()), i, 2)
2025 print_msg(_("Error: cannot display plugin"), p)
2026 traceback.print_exc(file=sys.stdout)
2027 grid.setRowStretch(i+1,1)
2029 vbox.addLayout(close_button(d))
2034 def show_account_details(self, k):
2036 d.setWindowTitle(_('Account Details'))
2039 vbox = QVBoxLayout(d)
2040 roots = self.wallet.get_roots(k)
2042 name = self.wallet.get_account_name(k)
2043 label = QLabel('Name: ' + name)
2044 vbox.addWidget(label)
2046 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2047 vbox.addWidget(QLabel('Type: ' + acctype))
2049 label = QLabel('Derivation: ' + k)
2050 vbox.addWidget(label)
2053 # mpk = self.wallet.master_public_keys[root]
2054 # text = QTextEdit()
2055 # text.setReadOnly(True)
2056 # text.setMaximumHeight(120)
2057 # text.setText(repr(mpk))
2058 # vbox.addWidget(text)
2060 vbox.addLayout(close_button(d))