3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re, threading
20 from electrum.i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
28 from PyQt4.QtGui import *
29 from PyQt4.QtCore import *
30 import PyQt4.QtCore as QtCore
32 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
33 from electrum.plugins import run_hook
37 from electrum.wallet import format_satoshis
38 from electrum import Transaction
39 from electrum import mnemonic
40 from electrum import util, bitcoin, commands, Interface, Wallet
41 from electrum import SimpleConfig, Wallet, WalletStorage
44 from electrum import bmp, pyqrnative
46 from amountedit import AmountEdit
47 from network_dialog import NetworkDialog
48 from qrcodewidget import QRCodeWidget
50 from decimal import Decimal
58 if platform.system() == 'Windows':
59 MONOSPACE_FONT = 'Lucida Console'
60 elif platform.system() == 'Darwin':
61 MONOSPACE_FONT = 'Monaco'
63 MONOSPACE_FONT = 'monospace'
65 from electrum import ELECTRUM_VERSION
75 class StatusBarButton(QPushButton):
76 def __init__(self, icon, tooltip, func):
77 QPushButton.__init__(self, icon, '')
78 self.setToolTip(tooltip)
80 self.setMaximumWidth(25)
81 self.clicked.connect(func)
83 self.setIconSize(QSize(25,25))
85 def keyPressEvent(self, e):
86 if e.key() == QtCore.Qt.Key_Return:
98 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
100 class ElectrumWindow(QMainWindow):
101 def changeEvent(self, event):
102 flags = self.windowFlags();
103 if event and event.type() == QtCore.QEvent.WindowStateChange:
104 if self.windowState() & QtCore.Qt.WindowMinimized:
105 self.build_menu(True)
106 # The only way to toggle the icon in the window managers taskbar is to use the Qt.Tooltip flag
107 # The problem is that it somehow creates an (in)visible window that will stay active and prevent
108 # Electrum from closing.
109 # As for now I have no clue how to implement a proper 'hide to tray' functionality.
110 # self.setWindowFlags(flags & ~Qt.ToolTip)
111 elif event.oldState() & QtCore.Qt.WindowMinimized:
112 self.build_menu(False)
113 #self.setWindowFlags(flags | Qt.ToolTip)
115 def build_menu(self, is_hidden = False):
117 if self.isMinimized():
118 m.addAction(_("Show"), self.showNormal)
120 m.addAction(_("Hide"), self.showMinimized)
123 m.addAction(_("Exit Electrum"), self.close)
124 self.tray.setContextMenu(m)
126 def tray_activated(self, reason):
127 if reason == QSystemTrayIcon.DoubleClick:
130 def showNormal(self):
131 self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
133 def __init__(self, config, network):
134 QMainWindow.__init__(self)
137 self.network = network
139 self._close_electrum = False
142 if sys.platform == 'darwin':
143 self.icon = QIcon(":icons/electrum_dark_icon.png")
144 #self.icon = QIcon(":icons/lock.png")
146 self.icon = QIcon(':icons/electrum_light_icon.png')
148 self.tray = QSystemTrayIcon(self.icon, self)
149 self.tray.setToolTip('Electrum')
150 self.tray.activated.connect(self.tray_activated)
154 self.create_status_bar()
156 self.need_update = threading.Event()
158 self.decimal_point = config.get('decimal_point', 8)
159 self.num_zeros = int(config.get('num_zeros',0))
161 set_language(config.get('language'))
163 self.funds_error = False
164 self.completions = QStringListModel()
166 self.tabs = tabs = QTabWidget(self)
167 self.column_widths = self.config.get("column_widths_2", default_column_widths )
168 tabs.addTab(self.create_history_tab(), _('History') )
169 tabs.addTab(self.create_send_tab(), _('Send') )
170 tabs.addTab(self.create_receive_tab(), _('Receive') )
171 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
172 tabs.addTab(self.create_console_tab(), _('Console') )
173 tabs.setMinimumSize(600, 400)
174 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
175 self.setCentralWidget(tabs)
177 g = self.config.get("winpos-qt",[100, 100, 840, 400])
178 self.setGeometry(g[0], g[1], g[2], g[3])
180 self.setWindowIcon(QIcon(":icons/electrum.png"))
183 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
184 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
185 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
186 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
187 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
189 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
190 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
191 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
193 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')))
203 # set initial message
204 self.console.showMessage(self.network.banner)
211 self.config.set_key('lite_mode', False, True)
216 self.config.set_key('lite_mode', True, True)
223 if not self.check_qt_version():
224 if self.config.get('lite_mode') is True:
225 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
226 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
227 self.config.set_key('lite_mode', False, True)
233 actuator = lite_window.MiniActuator(self)
235 # Should probably not modify the current path but instead
236 # change the behaviour of rsrc(...)
237 old_path = QDir.currentPath()
238 actuator.load_theme()
240 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
242 driver = lite_window.MiniDriver(self, self.mini)
244 # Reset path back to original value now that loading the GUI
246 QDir.setCurrent(old_path)
248 if self.config.get('lite_mode') is True:
254 def check_qt_version(self):
255 qtVersion = qVersion()
256 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
259 def update_account_selector(self):
261 accounts = self.wallet.get_account_names()
262 self.account_selector.clear()
263 if len(accounts) > 1:
264 self.account_selector.addItems([_("All accounts")] + accounts.values())
265 self.account_selector.setCurrentIndex(0)
266 self.account_selector.show()
268 self.account_selector.hide()
271 def load_wallet(self, wallet):
274 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
275 self.current_account = self.wallet.storage.get("current_account", None)
277 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
278 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
279 self.setWindowTitle( title )
281 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
282 self.notify_transactions()
283 self.update_account_selector()
284 self.new_account.setEnabled(self.wallet.seed_version>4)
285 self.update_lock_icon()
286 self.update_buttons_on_seed()
287 self.update_console()
289 run_hook('load_wallet', wallet)
292 def select_wallet_file(self):
293 wallet_folder = self.wallet.storage.path
294 re.sub("(\/\w*.dat)$", "", wallet_folder)
295 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
299 def open_wallet(self):
301 filename = self.select_wallet_file()
305 storage = WalletStorage({'wallet_path': filename})
306 if not storage.file_exists:
307 self.show_message("file not found "+ filename)
310 self.wallet.stop_threads()
313 wallet = Wallet(storage)
314 wallet.start_threads(self.network)
316 self.load_wallet(wallet)
320 def backup_wallet(self):
322 path = self.wallet.storage.path
323 wallet_folder = os.path.dirname(path)
324 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
325 new_filename = unicode(new_filename)
326 if not ok or not new_filename:
329 new_path = os.path.join(wallet_folder, new_filename)
332 shutil.copy2(path, new_path)
333 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
334 except (IOError, os.error), reason:
335 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
338 def new_wallet(self):
341 wallet_folder = os.path.dirname(self.wallet.storage.path)
342 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
343 filename = unicode(filename)
344 if not ok or not filename:
346 filename = os.path.join(wallet_folder, filename)
348 storage = WalletStorage({'wallet_path': filename})
349 assert not storage.file_exists
351 wizard = installwizard.InstallWizard(self.config, self.network, storage)
352 wallet = wizard.run()
354 self.load_wallet(wallet)
358 def init_menubar(self):
361 file_menu = menubar.addMenu(_("&File"))
362 open_wallet_action = file_menu.addAction(_("&Open"))
363 open_wallet_action.triggered.connect(self.open_wallet)
365 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
366 new_wallet_action.triggered.connect(self.new_wallet)
368 wallet_backup = file_menu.addAction(_("&Copy"))
369 wallet_backup.triggered.connect(self.backup_wallet)
371 quit_item = file_menu.addAction(_("&Close"))
372 quit_item.triggered.connect(self.close)
374 wallet_menu = menubar.addMenu(_("&Wallet"))
376 new_contact = wallet_menu.addAction(_("&New contact"))
377 new_contact.triggered.connect(self.new_contact_dialog)
379 self.new_account = wallet_menu.addAction(_("&New account"))
380 self.new_account.triggered.connect(self.new_account_dialog)
382 wallet_menu.addSeparator()
384 pw = wallet_menu.addAction(_("&Password"))
385 pw.triggered.connect(self.change_password_dialog)
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 labels_menu = wallet_menu.addMenu(_("&Labels"))
396 import_labels = labels_menu.addAction(_("&Import"))
397 import_labels.triggered.connect(self.do_import_labels)
398 export_labels = labels_menu.addAction(_("&Export"))
399 export_labels.triggered.connect(self.do_export_labels)
401 keys_menu = wallet_menu.addMenu(_("&Private keys"))
402 import_keys = keys_menu.addAction(_("&Import"))
403 import_keys.triggered.connect(self.do_import_privkey)
404 export_keys = keys_menu.addAction(_("&Export"))
405 export_keys.triggered.connect(self.do_export_privkeys)
407 ex_history = wallet_menu.addAction(_("&Export History"))
408 ex_history.triggered.connect(self.do_export_history)
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 network = tools_menu.addAction(_("&Network"))
420 network.triggered.connect(self.run_network_dialog)
422 plugins_labels = tools_menu.addAction(_("&Plugins"))
423 plugins_labels.triggered.connect(self.plugins_dialog)
425 tools_menu.addSeparator()
427 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
429 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
430 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
432 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
433 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
435 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
437 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
438 raw_transaction_file.triggered.connect(self.do_process_from_file)
440 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
441 raw_transaction_text.triggered.connect(self.do_process_from_text)
444 help_menu = menubar.addMenu(_("&Help"))
445 show_about = help_menu.addAction(_("&About"))
446 show_about.triggered.connect(self.show_about)
447 web_open = help_menu.addAction(_("&Official website"))
448 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
450 help_menu.addSeparator()
451 doc_open = help_menu.addAction(_("&Documentation"))
452 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
453 report_bug = help_menu.addAction(_("&Report Bug"))
454 report_bug.triggered.connect(self.show_report_bug)
456 self.setMenuBar(menubar)
458 def show_about(self):
459 QMessageBox.about(self, "Electrum",
460 _("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."))
462 def show_report_bug(self):
463 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
464 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
467 def notify_transactions(self):
468 if not self.network or not self.network.is_connected():
471 print_error("Notifying GUI")
472 if len(self.network.interface.pending_transactions_for_notifications) > 0:
473 # Combine the transactions if there are more then three
474 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
477 for tx in self.network.interface.pending_transactions_for_notifications:
478 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
482 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
483 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
485 self.network.interface.pending_transactions_for_notifications = []
487 for tx in self.network.interface.pending_transactions_for_notifications:
489 self.network.interface.pending_transactions_for_notifications.remove(tx)
490 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
492 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
494 def notify(self, message):
495 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
499 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
500 def getOpenFileName(self, title, filter = ""):
501 directory = self.config.get('io_dir', os.path.expanduser('~'))
502 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
503 if fileName and directory != os.path.dirname(fileName):
504 self.config.set_key('io_dir', os.path.dirname(fileName), True)
507 def getSaveFileName(self, title, filename, filter = ""):
508 directory = self.config.get('io_dir', os.path.expanduser('~'))
509 path = os.path.join( directory, filename )
510 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
511 if fileName and directory != os.path.dirname(fileName):
512 self.config.set_key('io_dir', os.path.dirname(fileName), True)
516 QMainWindow.close(self)
517 run_hook('close_main_window')
519 def connect_slots(self, sender):
520 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
521 self.previous_payto_e=''
523 def timer_actions(self):
524 if self.need_update.is_set():
526 self.need_update.clear()
527 run_hook('timer_actions')
529 def format_amount(self, x, is_diff=False, whitespaces=False):
530 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
532 def read_amount(self, x):
533 if x in['.', '']: return None
534 p = pow(10, self.decimal_point)
535 return int( p * Decimal(x) )
538 assert self.decimal_point in [5,8]
539 return "BTC" if self.decimal_point == 8 else "mBTC"
542 def update_status(self):
543 if self.network is None:
545 icon = QIcon(":icons/status_disconnected.png")
547 elif self.network.is_connected():
548 if not self.wallet.up_to_date:
549 text = _("Synchronizing...")
550 icon = QIcon(":icons/status_waiting.png")
551 elif self.network.server_lag > 1:
552 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
553 icon = QIcon(":icons/status_lagging.png")
555 c, u = self.wallet.get_account_balance(self.current_account)
556 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
557 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
560 run_hook('set_quote_text', c+u, r)
563 text += " (%s)"%quote
565 self.tray.setToolTip(text)
566 icon = QIcon(":icons/status_connected.png")
568 text = _("Not connected")
569 icon = QIcon(":icons/status_disconnected.png")
571 self.balance_label.setText(text)
572 self.status_button.setIcon( icon )
575 def update_wallet(self):
577 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
578 self.update_history_tab()
579 self.update_receive_tab()
580 self.update_contacts_tab()
581 self.update_completions()
584 def create_history_tab(self):
585 self.history_list = l = MyTreeWidget(self)
587 for i,width in enumerate(self.column_widths['history']):
588 l.setColumnWidth(i, width)
589 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
590 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
591 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
593 l.customContextMenuRequested.connect(self.create_history_menu)
597 def create_history_menu(self, position):
598 self.history_list.selectedIndexes()
599 item = self.history_list.currentItem()
601 tx_hash = str(item.data(0, Qt.UserRole).toString())
602 if not tx_hash: return
604 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
605 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
606 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
607 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
610 def show_transaction(self, tx):
611 import transaction_dialog
612 d = transaction_dialog.TxDialog(tx, self)
615 def tx_label_clicked(self, item, column):
616 if column==2 and item.isSelected():
618 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
619 self.history_list.editItem( item, column )
620 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
623 def tx_label_changed(self, item, column):
627 tx_hash = str(item.data(0, Qt.UserRole).toString())
628 tx = self.wallet.transactions.get(tx_hash)
629 text = unicode( item.text(2) )
630 self.wallet.set_label(tx_hash, text)
632 item.setForeground(2, QBrush(QColor('black')))
634 text = self.wallet.get_default_label(tx_hash)
635 item.setText(2, text)
636 item.setForeground(2, QBrush(QColor('gray')))
640 def edit_label(self, is_recv):
641 l = self.receive_list if is_recv else self.contacts_list
642 item = l.currentItem()
643 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
644 l.editItem( item, 1 )
645 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
649 def address_label_clicked(self, item, column, l, column_addr, column_label):
650 if column == column_label and item.isSelected():
651 is_editable = item.data(0, 32).toBool()
654 addr = unicode( item.text(column_addr) )
655 label = unicode( item.text(column_label) )
656 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
657 l.editItem( item, column )
658 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
661 def address_label_changed(self, item, column, l, column_addr, column_label):
662 if column == column_label:
663 addr = unicode( item.text(column_addr) )
664 text = unicode( item.text(column_label) )
665 is_editable = item.data(0, 32).toBool()
669 changed = self.wallet.set_label(addr, text)
671 self.update_history_tab()
672 self.update_completions()
674 self.current_item_changed(item)
676 run_hook('item_changed', item, column)
679 def current_item_changed(self, a):
680 run_hook('current_item_changed', a)
684 def update_history_tab(self):
686 self.history_list.clear()
687 for item in self.wallet.get_tx_history(self.current_account):
688 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
689 time_str = _("unknown")
692 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
694 time_str = _("error")
697 time_str = 'unverified'
698 icon = QIcon(":icons/unconfirmed.png")
701 icon = QIcon(":icons/unconfirmed.png")
703 icon = QIcon(":icons/clock%d.png"%conf)
705 icon = QIcon(":icons/confirmed.png")
707 if value is not None:
708 v_str = self.format_amount(value, True, whitespaces=True)
712 balance_str = self.format_amount(balance, whitespaces=True)
715 label, is_default_label = self.wallet.get_label(tx_hash)
717 label = _('Pruned transaction outputs')
718 is_default_label = False
720 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
721 item.setFont(2, QFont(MONOSPACE_FONT))
722 item.setFont(3, QFont(MONOSPACE_FONT))
723 item.setFont(4, QFont(MONOSPACE_FONT))
725 item.setForeground(3, QBrush(QColor("#BC1E1E")))
727 item.setData(0, Qt.UserRole, tx_hash)
728 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
730 item.setForeground(2, QBrush(QColor('grey')))
732 item.setIcon(0, icon)
733 self.history_list.insertTopLevelItem(0,item)
736 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
739 def create_send_tab(self):
744 grid.setColumnMinimumWidth(3,300)
745 grid.setColumnStretch(5,1)
748 self.payto_e = QLineEdit()
749 grid.addWidget(QLabel(_('Pay to')), 1, 0)
750 grid.addWidget(self.payto_e, 1, 1, 1, 3)
752 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)
754 completer = QCompleter()
755 completer.setCaseSensitivity(False)
756 self.payto_e.setCompleter(completer)
757 completer.setModel(self.completions)
759 self.message_e = QLineEdit()
760 grid.addWidget(QLabel(_('Description')), 2, 0)
761 grid.addWidget(self.message_e, 2, 1, 1, 3)
762 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)
764 self.amount_e = AmountEdit(self.base_unit)
765 grid.addWidget(QLabel(_('Amount')), 3, 0)
766 grid.addWidget(self.amount_e, 3, 1, 1, 2)
767 grid.addWidget(HelpButton(
768 _('Amount to be sent.') + '\n\n' \
769 + _('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.') \
770 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
772 self.fee_e = AmountEdit(self.base_unit)
773 grid.addWidget(QLabel(_('Fee')), 4, 0)
774 grid.addWidget(self.fee_e, 4, 1, 1, 2)
775 grid.addWidget(HelpButton(
776 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
777 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
778 + _('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)
781 self.send_button = EnterButton(_("Send"), self.do_send)
782 grid.addWidget(self.send_button, 6, 1)
784 b = EnterButton(_("Clear"),self.do_clear)
785 grid.addWidget(b, 6, 2)
787 self.payto_sig = QLabel('')
788 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
790 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
791 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
800 def entry_changed( is_fee ):
801 self.funds_error = False
803 if self.amount_e.is_shortcut:
804 self.amount_e.is_shortcut = False
805 c, u = self.wallet.get_account_balance(self.current_account)
806 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( c + u, 0, self.current_account)
807 fee = self.wallet.estimated_fee(inputs)
809 self.amount_e.setText( self.format_amount(amount) )
810 self.fee_e.setText( self.format_amount( fee ) )
813 amount = self.read_amount(str(self.amount_e.text()))
814 fee = self.read_amount(str(self.fee_e.text()))
816 if not is_fee: fee = None
819 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( amount, fee, self.current_account )
821 self.fee_e.setText( self.format_amount( fee ) )
824 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
828 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
829 self.funds_error = True
830 text = _( "Not enough funds" )
831 c, u = self.wallet.get_frozen_balance()
832 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
834 self.statusBar().showMessage(text)
835 self.amount_e.setPalette(palette)
836 self.fee_e.setPalette(palette)
838 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
839 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
841 run_hook('create_send_tab', grid)
845 def update_completions(self):
847 for addr,label in self.wallet.labels.items():
848 if addr in self.wallet.addressbook:
849 l.append( label + ' <' + addr + '>')
851 run_hook('update_completions', l)
852 self.completions.setStringList(l)
856 return lambda s, *args: s.do_protect(func, args)
861 label = unicode( self.message_e.text() )
862 r = unicode( self.payto_e.text() )
865 # label or alias, with address in brackets
866 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
867 to_address = m.group(2) if m else r
869 if not is_valid(to_address):
870 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
874 amount = self.read_amount(unicode( self.amount_e.text()))
876 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
879 fee = self.read_amount(unicode( self.fee_e.text()))
881 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
884 confirm_amount = self.config.get('confirm_amount', 100000000)
885 if amount >= confirm_amount:
886 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
889 self.send_tx(to_address, amount, fee, label)
893 def send_tx(self, to_address, amount, fee, label, password):
896 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
897 except Exception as e:
898 traceback.print_exc(file=sys.stdout)
899 self.show_message(str(e))
902 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
903 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
907 self.wallet.set_label(tx.hash(), label)
910 h = self.wallet.send_tx(tx)
911 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
912 status, msg = self.wallet.receive_tx( h )
914 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
916 self.update_contacts_tab()
918 QMessageBox.warning(self, _('Error'), msg, _('OK'))
920 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
922 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
923 with open(fileName,'w') as f:
924 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
925 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
927 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
929 # add recipient to addressbook
930 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
931 self.wallet.addressbook.append(to_address)
936 def set_url(self, url):
937 address, amount, label, message, signature, identity, url = util.parse_url(url)
939 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
942 self.mini.set_payment_fields(address, amount)
944 if label and self.wallet.labels.get(address) != label:
945 if self.question('Give label "%s" to address %s ?'%(label,address)):
946 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
947 self.wallet.addressbook.append(address)
948 self.wallet.set_label(address, label)
950 run_hook('set_url', url, self.show_message, self.question)
952 self.tabs.setCurrentIndex(1)
953 label = self.wallet.labels.get(address)
954 m_addr = label + ' <'+ address +'>' if label else address
955 self.payto_e.setText(m_addr)
957 self.message_e.setText(message)
959 self.amount_e.setText(amount)
962 self.set_frozen(self.payto_e,True)
963 self.set_frozen(self.amount_e,True)
964 self.set_frozen(self.message_e,True)
965 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
967 self.payto_sig.setVisible(False)
970 self.payto_sig.setVisible(False)
971 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
973 self.set_frozen(e,False)
976 def set_frozen(self,entry,frozen):
978 entry.setReadOnly(True)
979 entry.setFrame(False)
981 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
982 entry.setPalette(palette)
984 entry.setReadOnly(False)
987 palette.setColor(entry.backgroundRole(), QColor('white'))
988 entry.setPalette(palette)
991 def toggle_freeze(self,addr):
993 if addr in self.wallet.frozen_addresses:
994 self.wallet.unfreeze(addr)
996 self.wallet.freeze(addr)
997 self.update_receive_tab()
999 def toggle_priority(self,addr):
1001 if addr in self.wallet.prioritized_addresses:
1002 self.wallet.unprioritize(addr)
1004 self.wallet.prioritize(addr)
1005 self.update_receive_tab()
1008 def create_list_tab(self, headers):
1009 "generic tab creation method"
1010 l = MyTreeWidget(self)
1011 l.setColumnCount( len(headers) )
1012 l.setHeaderLabels( headers )
1015 vbox = QVBoxLayout()
1022 vbox.addWidget(buttons)
1024 hbox = QHBoxLayout()
1027 buttons.setLayout(hbox)
1032 def create_receive_tab(self):
1033 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1034 l.setContextMenuPolicy(Qt.CustomContextMenu)
1035 l.customContextMenuRequested.connect(self.create_receive_menu)
1036 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1037 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1038 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1039 self.receive_list = l
1040 self.receive_buttons_hbox = hbox
1047 def save_column_widths(self):
1048 self.column_widths["receive"] = []
1049 for i in range(self.receive_list.columnCount() -1):
1050 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1052 self.column_widths["history"] = []
1053 for i in range(self.history_list.columnCount() - 1):
1054 self.column_widths["history"].append(self.history_list.columnWidth(i))
1056 self.column_widths["contacts"] = []
1057 for i in range(self.contacts_list.columnCount() - 1):
1058 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1060 self.config.set_key("column_widths_2", self.column_widths, True)
1063 def create_contacts_tab(self):
1064 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1065 l.setContextMenuPolicy(Qt.CustomContextMenu)
1066 l.customContextMenuRequested.connect(self.create_contact_menu)
1067 for i,width in enumerate(self.column_widths['contacts']):
1068 l.setColumnWidth(i, width)
1070 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1071 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1072 self.contacts_list = l
1073 self.contacts_buttons_hbox = hbox
1078 def delete_imported_key(self, addr):
1079 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1080 self.wallet.delete_imported_key(addr)
1081 self.update_receive_tab()
1082 self.update_history_tab()
1084 def edit_account_label(self, k):
1085 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1087 label = unicode(text)
1088 self.wallet.set_label(k,label)
1089 self.update_receive_tab()
1091 def account_set_expanded(self, item, k, b):
1093 self.accounts_expanded[k] = b
1095 def create_account_menu(self, position, k, item):
1097 if item.isExpanded():
1098 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1100 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1101 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1102 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1103 if self.wallet.account_is_pending(k):
1104 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1105 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1107 def delete_pending_account(self, k):
1108 self.wallet.delete_pending_account(k)
1109 self.update_receive_tab()
1111 def create_receive_menu(self, position):
1112 # fixme: this function apparently has a side effect.
1113 # if it is not called the menu pops up several times
1114 #self.receive_list.selectedIndexes()
1116 item = self.receive_list.itemAt(position)
1119 addr = unicode(item.text(0))
1120 if not is_valid(addr):
1121 k = str(item.data(0,32).toString())
1123 self.create_account_menu(position, k, item)
1125 item.setExpanded(not item.isExpanded())
1129 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1130 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1131 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1132 if self.wallet.seed:
1133 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1134 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1135 if addr in self.wallet.imported_keys:
1136 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1138 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1139 menu.addAction(t, lambda: self.toggle_freeze(addr))
1140 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1141 menu.addAction(t, lambda: self.toggle_priority(addr))
1143 run_hook('receive_menu', menu)
1144 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1147 def payto(self, addr):
1149 label = self.wallet.labels.get(addr)
1150 m_addr = label + ' <' + addr + '>' if label else addr
1151 self.tabs.setCurrentIndex(1)
1152 self.payto_e.setText(m_addr)
1153 self.amount_e.setFocus()
1156 def delete_contact(self, x):
1157 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1158 self.wallet.delete_contact(x)
1159 self.wallet.set_label(x, None)
1160 self.update_history_tab()
1161 self.update_contacts_tab()
1162 self.update_completions()
1165 def create_contact_menu(self, position):
1166 item = self.contacts_list.itemAt(position)
1168 addr = unicode(item.text(0))
1169 label = unicode(item.text(1))
1170 is_editable = item.data(0,32).toBool()
1171 payto_addr = item.data(0,33).toString()
1173 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1174 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1175 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1177 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1178 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1180 run_hook('create_contact_menu', menu, item)
1181 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1184 def update_receive_item(self, item):
1185 item.setFont(0, QFont(MONOSPACE_FONT))
1186 address = str(item.data(0,0).toString())
1187 label = self.wallet.labels.get(address,'')
1188 item.setData(1,0,label)
1189 item.setData(0,32, True) # is editable
1191 run_hook('update_receive_item', address, item)
1193 if not self.wallet.is_mine(address): return
1195 c, u = self.wallet.get_addr_balance(address)
1196 balance = self.format_amount(c + u)
1197 item.setData(2,0,balance)
1199 if address in self.wallet.frozen_addresses:
1200 item.setBackgroundColor(0, QColor('lightblue'))
1201 elif address in self.wallet.prioritized_addresses:
1202 item.setBackgroundColor(0, QColor('lightgreen'))
1205 def update_receive_tab(self):
1206 l = self.receive_list
1209 l.setColumnHidden(2, False)
1210 l.setColumnHidden(3, False)
1211 for i,width in enumerate(self.column_widths['receive']):
1212 l.setColumnWidth(i, width)
1214 if self.current_account is None:
1215 account_items = self.wallet.accounts.items()
1216 elif self.current_account != -1:
1217 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1221 for k, account in account_items:
1222 name = self.wallet.get_account_name(k)
1223 c,u = self.wallet.get_account_balance(k)
1224 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1225 l.addTopLevelItem(account_item)
1226 account_item.setExpanded(self.accounts_expanded.get(k, True))
1227 account_item.setData(0, 32, k)
1229 if not self.wallet.is_seeded(k):
1230 icon = QIcon(":icons/key.png")
1231 account_item.setIcon(0, icon)
1233 for is_change in ([0,1]):
1234 name = _("Receiving") if not is_change else _("Change")
1235 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1236 account_item.addChild(seq_item)
1237 if not is_change: seq_item.setExpanded(True)
1242 for address in account.get_addresses(is_change):
1243 h = self.wallet.history.get(address,[])
1247 if gap > self.wallet.gap_limit:
1252 num_tx = '*' if h == ['*'] else "%d"%len(h)
1253 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1254 self.update_receive_item(item)
1256 item.setBackgroundColor(1, QColor('red'))
1257 seq_item.addChild(item)
1260 for k, addr in self.wallet.get_pending_accounts():
1261 name = self.wallet.labels.get(k,'')
1262 account_item = QTreeWidgetItem( [ name + " [ "+_('pending account')+" ]", '', '', ''] )
1263 self.update_receive_item(item)
1264 l.addTopLevelItem(account_item)
1265 account_item.setExpanded(True)
1266 account_item.setData(0, 32, k)
1267 item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1268 account_item.addChild(item)
1269 self.update_receive_item(item)
1272 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1273 c,u = self.wallet.get_imported_balance()
1274 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1275 l.addTopLevelItem(account_item)
1276 account_item.setExpanded(True)
1277 for address in self.wallet.imported_keys.keys():
1278 item = QTreeWidgetItem( [ address, '', '', ''] )
1279 self.update_receive_item(item)
1280 account_item.addChild(item)
1283 # we use column 1 because column 0 may be hidden
1284 l.setCurrentItem(l.topLevelItem(0),1)
1287 def update_contacts_tab(self):
1288 l = self.contacts_list
1291 for address in self.wallet.addressbook:
1292 label = self.wallet.labels.get(address,'')
1293 n = self.wallet.get_num_tx(address)
1294 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1295 item.setFont(0, QFont(MONOSPACE_FONT))
1296 # 32 = label can be edited (bool)
1297 item.setData(0,32, True)
1299 item.setData(0,33, address)
1300 l.addTopLevelItem(item)
1302 run_hook('update_contacts_tab', l)
1303 l.setCurrentItem(l.topLevelItem(0))
1307 def create_console_tab(self):
1308 from console import Console
1309 self.console = console = Console()
1313 def update_console(self):
1314 console = self.console
1315 console.history = self.config.get("console-history",[])
1316 console.history_index = len(console.history)
1318 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1319 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1321 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1323 def mkfunc(f, method):
1324 return lambda *args: apply( f, (method, args, self.password_dialog ))
1326 if m[0]=='_' or m in ['network','wallet']: continue
1327 methods[m] = mkfunc(c._run, m)
1329 console.updateNamespace(methods)
1332 def change_account(self,s):
1333 if s == _("All accounts"):
1334 self.current_account = None
1336 accounts = self.wallet.get_account_names()
1337 for k, v in accounts.items():
1339 self.current_account = k
1340 self.update_history_tab()
1341 self.update_status()
1342 self.update_receive_tab()
1344 def create_status_bar(self):
1347 sb.setFixedHeight(35)
1348 qtVersion = qVersion()
1350 self.balance_label = QLabel("")
1351 sb.addWidget(self.balance_label)
1353 from version_getter import UpdateLabel
1354 self.updatelabel = UpdateLabel(self.config, sb)
1356 self.account_selector = QComboBox()
1357 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1358 sb.addPermanentWidget(self.account_selector)
1360 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1361 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1363 self.lock_icon = QIcon()
1364 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1365 sb.addPermanentWidget( self.password_button )
1367 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1368 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1369 sb.addPermanentWidget( self.seed_button )
1370 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1371 sb.addPermanentWidget( self.status_button )
1373 run_hook('create_status_bar', (sb,))
1375 self.setStatusBar(sb)
1378 def update_lock_icon(self):
1379 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1380 self.password_button.setIcon( icon )
1383 def update_buttons_on_seed(self):
1384 if not self.wallet.is_watching_only():
1385 self.seed_button.show()
1386 self.password_button.show()
1387 self.send_button.setText(_("Send"))
1389 self.password_button.hide()
1390 self.seed_button.hide()
1391 self.send_button.setText(_("Create unsigned transaction"))
1394 def change_password_dialog(self):
1395 from password_dialog import PasswordDialog
1396 d = PasswordDialog(self.wallet, self)
1398 self.update_lock_icon()
1401 def new_contact_dialog(self):
1404 vbox = QVBoxLayout(d)
1405 vbox.addWidget(QLabel(_('New Contact')+':'))
1407 grid = QGridLayout()
1410 grid.addWidget(QLabel(_("Address")), 1, 0)
1411 grid.addWidget(line1, 1, 1)
1412 grid.addWidget(QLabel(_("Name")), 2, 0)
1413 grid.addWidget(line2, 2, 1)
1415 vbox.addLayout(grid)
1416 vbox.addLayout(ok_cancel_buttons(d))
1421 address = str(line1.text())
1422 label = unicode(line2.text())
1424 if not is_valid(address):
1425 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1428 self.wallet.add_contact(address)
1430 self.wallet.set_label(address, label)
1432 self.update_contacts_tab()
1433 self.update_history_tab()
1434 self.update_completions()
1435 self.tabs.setCurrentIndex(3)
1438 def new_account_dialog(self):
1440 dialog = QDialog(self)
1442 dialog.setWindowTitle(_("New Account"))
1444 vbox = QVBoxLayout()
1445 vbox.addWidget(QLabel(_('Account name')+':'))
1448 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1449 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1454 vbox.addLayout(ok_cancel_buttons(dialog))
1455 dialog.setLayout(vbox)
1459 name = str(e.text())
1462 self.wallet.create_pending_account('1', name)
1463 self.update_receive_tab()
1464 self.tabs.setCurrentIndex(2)
1468 def show_master_public_key_old(self):
1469 dialog = QDialog(self)
1471 dialog.setWindowTitle(_("Master Public Key"))
1473 main_text = QTextEdit()
1474 main_text.setText(self.wallet.get_master_public_key())
1475 main_text.setReadOnly(True)
1476 main_text.setMaximumHeight(170)
1477 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1479 ok_button = QPushButton(_("OK"))
1480 ok_button.setDefault(True)
1481 ok_button.clicked.connect(dialog.accept)
1483 main_layout = QGridLayout()
1484 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1486 main_layout.addWidget(main_text, 1, 0)
1487 main_layout.addWidget(qrw, 1, 1 )
1489 vbox = QVBoxLayout()
1490 vbox.addLayout(main_layout)
1491 vbox.addLayout(close_button(dialog))
1492 dialog.setLayout(vbox)
1496 def show_master_public_key(self):
1498 if self.wallet.seed_version == 4:
1499 self.show_master_public_key_old()
1502 dialog = QDialog(self)
1504 dialog.setWindowTitle(_("Master Public Keys"))
1506 chain_text = QTextEdit()
1507 chain_text.setReadOnly(True)
1508 chain_text.setMaximumHeight(170)
1509 chain_qrw = QRCodeWidget()
1511 mpk_text = QTextEdit()
1512 mpk_text.setReadOnly(True)
1513 mpk_text.setMaximumHeight(170)
1514 mpk_qrw = QRCodeWidget()
1516 main_layout = QGridLayout()
1518 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1519 main_layout.addWidget(mpk_text, 1, 1)
1520 main_layout.addWidget(mpk_qrw, 1, 2)
1522 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1523 main_layout.addWidget(chain_text, 2, 1)
1524 main_layout.addWidget(chain_qrw, 2, 2)
1527 c, K, cK = self.wallet.master_public_keys[str(key)]
1528 chain_text.setText(c)
1529 chain_qrw.set_addr(c)
1530 chain_qrw.update_qr()
1535 key_selector = QComboBox()
1536 keys = sorted(self.wallet.master_public_keys.keys())
1537 key_selector.addItems(keys)
1539 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1540 main_layout.addWidget(key_selector, 0, 1)
1541 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1545 vbox = QVBoxLayout()
1546 vbox.addLayout(main_layout)
1547 vbox.addLayout(close_button(dialog))
1549 dialog.setLayout(vbox)
1554 def show_seed_dialog(self, password):
1555 if self.wallet.is_watching_only():
1556 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1559 if self.wallet.seed:
1561 mnemonic = self.wallet.get_mnemonic(password)
1563 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1565 from seed_dialog import SeedDialog
1566 d = SeedDialog(self, mnemonic, self.wallet.imported_keys)
1570 for k in self.wallet.master_private_keys.keys():
1571 pk = self.wallet.get_master_private_key(k, password)
1573 from seed_dialog import PrivateKeysDialog
1574 d = PrivateKeysDialog(self,l)
1581 def show_qrcode(self, data, title = _("QR code")):
1585 d.setWindowTitle(title)
1586 d.setMinimumSize(270, 300)
1587 vbox = QVBoxLayout()
1588 qrw = QRCodeWidget(data)
1589 vbox.addWidget(qrw, 1)
1590 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1591 hbox = QHBoxLayout()
1594 filename = os.path.join(self.config.path, "qrcode.bmp")
1597 bmp.save_qrcode(qrw.qr, filename)
1598 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1600 def copy_to_clipboard():
1601 bmp.save_qrcode(qrw.qr, filename)
1602 self.app.clipboard().setImage(QImage(filename))
1603 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1605 b = QPushButton(_("Copy"))
1607 b.clicked.connect(copy_to_clipboard)
1609 b = QPushButton(_("Save"))
1611 b.clicked.connect(print_qr)
1613 b = QPushButton(_("Close"))
1615 b.clicked.connect(d.accept)
1618 vbox.addLayout(hbox)
1623 def do_protect(self, func, args):
1624 if self.wallet.use_encryption:
1625 password = self.password_dialog()
1631 if args != (False,):
1632 args = (self,) + args + (password,)
1634 args = (self,password)
1639 def show_private_key(self, address, password):
1640 if not address: return
1642 pk_list = self.wallet.get_private_key(address, password)
1643 except Exception as e:
1644 self.show_message(str(e))
1646 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1650 def do_sign(self, address, message, signature, password):
1651 message = unicode(message.toPlainText())
1652 message = message.encode('utf-8')
1654 sig = self.wallet.sign_message(str(address.text()), message, password)
1655 signature.setText(sig)
1656 except Exception as e:
1657 self.show_message(str(e))
1659 def sign_message(self, address):
1660 if not address: return
1663 d.setWindowTitle(_('Sign Message'))
1664 d.setMinimumSize(410, 290)
1666 tab_widget = QTabWidget()
1668 layout = QGridLayout(tab)
1670 sign_address = QLineEdit()
1672 sign_address.setText(address)
1673 layout.addWidget(QLabel(_('Address')), 1, 0)
1674 layout.addWidget(sign_address, 1, 1)
1676 sign_message = QTextEdit()
1677 layout.addWidget(QLabel(_('Message')), 2, 0)
1678 layout.addWidget(sign_message, 2, 1)
1679 layout.setRowStretch(2,3)
1681 sign_signature = QTextEdit()
1682 layout.addWidget(QLabel(_('Signature')), 3, 0)
1683 layout.addWidget(sign_signature, 3, 1)
1684 layout.setRowStretch(3,1)
1687 hbox = QHBoxLayout()
1688 b = QPushButton(_("Sign"))
1690 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1691 b = QPushButton(_("Close"))
1692 b.clicked.connect(d.accept)
1694 layout.addLayout(hbox, 4, 1)
1695 tab_widget.addTab(tab, _("Sign"))
1699 layout = QGridLayout(tab)
1701 verify_address = QLineEdit()
1702 layout.addWidget(QLabel(_('Address')), 1, 0)
1703 layout.addWidget(verify_address, 1, 1)
1705 verify_message = QTextEdit()
1706 layout.addWidget(QLabel(_('Message')), 2, 0)
1707 layout.addWidget(verify_message, 2, 1)
1708 layout.setRowStretch(2,3)
1710 verify_signature = QTextEdit()
1711 layout.addWidget(QLabel(_('Signature')), 3, 0)
1712 layout.addWidget(verify_signature, 3, 1)
1713 layout.setRowStretch(3,1)
1716 message = unicode(verify_message.toPlainText())
1717 message = message.encode('utf-8')
1718 if bitcoin.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1719 self.show_message(_("Signature verified"))
1721 self.show_message(_("Error: wrong signature"))
1723 hbox = QHBoxLayout()
1724 b = QPushButton(_("Verify"))
1725 b.clicked.connect(do_verify)
1727 b = QPushButton(_("Close"))
1728 b.clicked.connect(d.accept)
1730 layout.addLayout(hbox, 4, 1)
1731 tab_widget.addTab(tab, _("Verify"))
1733 vbox = QVBoxLayout()
1734 vbox.addWidget(tab_widget)
1741 def question(self, msg):
1742 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1744 def show_message(self, msg):
1745 QMessageBox.information(self, _('Message'), msg, _('OK'))
1747 def password_dialog(self ):
1754 vbox = QVBoxLayout()
1755 msg = _('Please enter your password')
1756 vbox.addWidget(QLabel(msg))
1758 grid = QGridLayout()
1760 grid.addWidget(QLabel(_('Password')), 1, 0)
1761 grid.addWidget(pw, 1, 1)
1762 vbox.addLayout(grid)
1764 vbox.addLayout(ok_cancel_buttons(d))
1767 run_hook('password_dialog', pw, grid, 1)
1768 if not d.exec_(): return
1769 return unicode(pw.text())
1778 def tx_from_text(self, txt):
1779 "json or raw hexadecimal"
1782 tx = Transaction(txt)
1788 tx_dict = json.loads(str(txt))
1789 assert "hex" in tx_dict.keys()
1790 assert "complete" in tx_dict.keys()
1791 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1792 if not tx_dict["complete"]:
1793 assert "input_info" in tx_dict.keys()
1794 input_info = json.loads(tx_dict['input_info'])
1795 tx.add_input_info(input_info)
1800 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1804 def read_tx_from_file(self):
1805 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1809 with open(fileName, "r") as f:
1810 file_content = f.read()
1811 except (ValueError, IOError, os.error), reason:
1812 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1814 return self.tx_from_text(file_content)
1818 def sign_raw_transaction(self, tx, input_info, password):
1819 self.wallet.signrawtransaction(tx, input_info, [], password)
1821 def do_process_from_text(self):
1822 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1825 tx = self.tx_from_text(text)
1827 self.show_transaction(tx)
1829 def do_process_from_file(self):
1830 tx = self.read_tx_from_file()
1832 self.show_transaction(tx)
1834 def do_process_from_csvReader(self, csvReader):
1837 for row in csvReader:
1839 amount = float(row[1])
1840 amount = int(100000000*amount)
1841 outputs.append((address, amount))
1842 except (ValueError, IOError, os.error), reason:
1843 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1847 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1848 except Exception as e:
1849 self.show_message(str(e))
1852 self.show_transaction(tx)
1854 def do_process_from_csv_file(self):
1855 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1859 with open(fileName, "r") as f:
1860 csvReader = csv.reader(f)
1861 self.do_process_from_csvReader(csvReader)
1862 except (ValueError, IOError, os.error), reason:
1863 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1866 def do_process_from_csv_text(self):
1867 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1868 + _("Format: address, amount. One output per line"), _("Load CSV"))
1871 f = StringIO.StringIO(text)
1872 csvReader = csv.reader(f)
1873 self.do_process_from_csvReader(csvReader)
1878 def do_export_privkeys(self, password):
1879 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.")))
1882 select_export = _('Select file to export your private keys to')
1883 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1885 with open(fileName, "w+") as csvfile:
1886 transaction = csv.writer(csvfile)
1887 transaction.writerow(["address", "private_key"])
1889 addresses = self.wallet.addresses(True)
1891 for addr in addresses:
1892 pk = "".join(self.wallet.get_private_key(addr, password))
1893 transaction.writerow(["%34s"%addr,pk])
1895 self.show_message(_("Private keys exported."))
1897 except (IOError, os.error), reason:
1898 export_error_label = _("Electrum was unable to produce a private key-export.")
1899 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1901 except Exception as e:
1902 self.show_message(str(e))
1906 def do_import_labels(self):
1907 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1908 if not labelsFile: return
1910 f = open(labelsFile, 'r')
1913 for key, value in json.loads(data).items():
1914 self.wallet.set_label(key, value)
1915 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1916 except (IOError, os.error), reason:
1917 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1920 def do_export_labels(self):
1921 labels = self.wallet.labels
1923 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1925 with open(fileName, 'w+') as f:
1926 json.dump(labels, f)
1927 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1928 except (IOError, os.error), reason:
1929 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1932 def do_export_history(self):
1933 from lite_window import csv_transaction
1934 csv_transaction(self.wallet)
1938 def do_import_privkey(self, password):
1939 if not self.wallet.imported_keys:
1940 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1941 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1942 + _('Are you sure you understand what you are doing?'), 3, 4)
1945 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1948 text = str(text).split()
1953 addr = self.wallet.import_key(key, password)
1954 except Exception as e:
1960 addrlist.append(addr)
1962 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1964 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1965 self.update_receive_tab()
1966 self.update_history_tab()
1969 def settings_dialog(self):
1971 d.setWindowTitle(_('Electrum Settings'))
1973 vbox = QVBoxLayout()
1974 grid = QGridLayout()
1975 grid.setColumnStretch(0,1)
1977 nz_label = QLabel(_('Display zeros') + ':')
1978 grid.addWidget(nz_label, 0, 0)
1979 nz_e = AmountEdit(None,True)
1980 nz_e.setText("%d"% self.num_zeros)
1981 grid.addWidget(nz_e, 0, 1)
1982 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1983 grid.addWidget(HelpButton(msg), 0, 2)
1984 if not self.config.is_modifiable('num_zeros'):
1985 for w in [nz_e, nz_label]: w.setEnabled(False)
1987 lang_label=QLabel(_('Language') + ':')
1988 grid.addWidget(lang_label, 1, 0)
1989 lang_combo = QComboBox()
1990 from electrum.i18n import languages
1991 lang_combo.addItems(languages.values())
1993 index = languages.keys().index(self.config.get("language",''))
1996 lang_combo.setCurrentIndex(index)
1997 grid.addWidget(lang_combo, 1, 1)
1998 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1999 if not self.config.is_modifiable('language'):
2000 for w in [lang_combo, lang_label]: w.setEnabled(False)
2003 fee_label = QLabel(_('Transaction fee') + ':')
2004 grid.addWidget(fee_label, 2, 0)
2005 fee_e = AmountEdit(self.base_unit)
2006 fee_e.setText(self.format_amount(self.wallet.fee).strip())
2007 grid.addWidget(fee_e, 2, 1)
2008 msg = _('Fee per kilobyte of transaction.') + ' ' \
2009 + _('Recommended value') + ': ' + self.format_amount(50000)
2010 grid.addWidget(HelpButton(msg), 2, 2)
2011 if not self.config.is_modifiable('fee_per_kb'):
2012 for w in [fee_e, fee_label]: w.setEnabled(False)
2014 units = ['BTC', 'mBTC']
2015 unit_label = QLabel(_('Base unit') + ':')
2016 grid.addWidget(unit_label, 3, 0)
2017 unit_combo = QComboBox()
2018 unit_combo.addItems(units)
2019 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2020 grid.addWidget(unit_combo, 3, 1)
2021 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2022 + '\n1BTC=1000mBTC.\n' \
2023 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
2025 usechange_cb = QCheckBox(_('Use change addresses'))
2026 usechange_cb.setChecked(self.wallet.use_change)
2027 grid.addWidget(usechange_cb, 4, 0)
2028 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2029 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2031 grid.setRowStretch(5,1)
2033 vbox.addLayout(grid)
2034 vbox.addLayout(ok_cancel_buttons(d))
2038 if not d.exec_(): return
2040 fee = unicode(fee_e.text())
2042 fee = self.read_amount(fee)
2044 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2047 self.wallet.set_fee(fee)
2049 nz = unicode(nz_e.text())
2054 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2057 if self.num_zeros != nz:
2059 self.config.set_key('num_zeros', nz, True)
2060 self.update_history_tab()
2061 self.update_receive_tab()
2063 usechange_result = usechange_cb.isChecked()
2064 if self.wallet.use_change != usechange_result:
2065 self.wallet.use_change = usechange_result
2066 self.wallet.storage.put('use_change', self.wallet.use_change)
2068 unit_result = units[unit_combo.currentIndex()]
2069 if self.base_unit() != unit_result:
2070 self.decimal_point = 8 if unit_result == 'BTC' else 5
2071 self.config.set_key('decimal_point', self.decimal_point, True)
2072 self.update_history_tab()
2073 self.update_status()
2075 need_restart = False
2077 lang_request = languages.keys()[lang_combo.currentIndex()]
2078 if lang_request != self.config.get('language'):
2079 self.config.set_key("language", lang_request, True)
2082 run_hook('close_settings_dialog')
2085 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2088 def run_network_dialog(self):
2089 if not self.network:
2091 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2093 def closeEvent(self, event):
2095 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2096 self.save_column_widths()
2097 self.config.set_key("console-history", self.console.history[-50:], True)
2098 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2103 def plugins_dialog(self):
2104 from electrum.plugins import plugins
2107 d.setWindowTitle(_('Electrum Plugins'))
2110 vbox = QVBoxLayout(d)
2113 scroll = QScrollArea()
2114 scroll.setEnabled(True)
2115 scroll.setWidgetResizable(True)
2116 scroll.setMinimumSize(400,250)
2117 vbox.addWidget(scroll)
2121 w.setMinimumHeight(len(plugins)*35)
2123 grid = QGridLayout()
2124 grid.setColumnStretch(0,1)
2127 def do_toggle(cb, p, w):
2130 if w: w.setEnabled(r)
2132 def mk_toggle(cb, p, w):
2133 return lambda: do_toggle(cb,p,w)
2135 for i, p in enumerate(plugins):
2137 cb = QCheckBox(p.fullname())
2138 cb.setDisabled(not p.is_available())
2139 cb.setChecked(p.is_enabled())
2140 grid.addWidget(cb, i, 0)
2141 if p.requires_settings():
2142 w = p.settings_widget(self)
2143 w.setEnabled( p.is_enabled() )
2144 grid.addWidget(w, i, 1)
2147 cb.clicked.connect(mk_toggle(cb,p,w))
2148 grid.addWidget(HelpButton(p.description()), i, 2)
2150 print_msg(_("Error: cannot display plugin"), p)
2151 traceback.print_exc(file=sys.stdout)
2152 grid.setRowStretch(i+1,1)
2154 vbox.addLayout(close_button(d))
2159 def show_account_details(self, k):
2161 d.setWindowTitle(_('Account Details'))
2164 vbox = QVBoxLayout(d)
2165 roots = self.wallet.get_roots(k)
2167 name = self.wallet.get_account_name(k)
2168 label = QLabel('Name: ' + name)
2169 vbox.addWidget(label)
2171 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2172 vbox.addWidget(QLabel('Type: ' + acctype))
2174 label = QLabel('Derivation: ' + k)
2175 vbox.addWidget(label)
2178 # mpk = self.wallet.master_public_keys[root]
2179 # text = QTextEdit()
2180 # text.setReadOnly(True)
2181 # text.setMaximumHeight(120)
2182 # text.setText(repr(mpk))
2183 # vbox.addWidget(text)
2185 vbox.addLayout(close_button(d))