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 open_wallet(self):
293 wallet_folder = self.wallet.storage.path
294 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
298 storage = WalletStorage({'wallet_path': filename})
299 if not storage.file_exists:
300 self.show_message("file not found "+ filename)
303 self.wallet.stop_threads()
306 wallet = Wallet(storage)
307 wallet.start_threads(self.network)
309 self.load_wallet(wallet)
313 def backup_wallet(self):
315 path = self.wallet.storage.path
316 wallet_folder = os.path.dirname(path)
317 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
321 new_path = os.path.join(wallet_folder, filename)
324 shutil.copy2(path, new_path)
325 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
326 except (IOError, os.error), reason:
327 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
330 def new_wallet(self):
333 wallet_folder = os.path.dirname(self.wallet.storage.path)
334 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
337 filename = os.path.join(wallet_folder, filename)
339 storage = WalletStorage({'wallet_path': filename})
340 if storage.file_exists:
341 QMessageBox.critical(None, "Error", _("File exists"))
344 wizard = installwizard.InstallWizard(self.config, self.network, storage)
345 wallet = wizard.run()
347 self.load_wallet(wallet)
351 def init_menubar(self):
354 file_menu = menubar.addMenu(_("&File"))
355 open_wallet_action = file_menu.addAction(_("&Open"))
356 open_wallet_action.triggered.connect(self.open_wallet)
358 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
359 new_wallet_action.triggered.connect(self.new_wallet)
361 wallet_backup = file_menu.addAction(_("&Copy"))
362 wallet_backup.triggered.connect(self.backup_wallet)
364 quit_item = file_menu.addAction(_("&Close"))
365 quit_item.triggered.connect(self.close)
367 wallet_menu = menubar.addMenu(_("&Wallet"))
369 new_contact = wallet_menu.addAction(_("&New contact"))
370 new_contact.triggered.connect(self.new_contact_dialog)
372 self.new_account = wallet_menu.addAction(_("&New account"))
373 self.new_account.triggered.connect(self.new_account_dialog)
375 wallet_menu.addSeparator()
377 pw = wallet_menu.addAction(_("&Password"))
378 pw.triggered.connect(self.change_password_dialog)
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 labels_menu = wallet_menu.addMenu(_("&Labels"))
389 import_labels = labels_menu.addAction(_("&Import"))
390 import_labels.triggered.connect(self.do_import_labels)
391 export_labels = labels_menu.addAction(_("&Export"))
392 export_labels.triggered.connect(self.do_export_labels)
394 keys_menu = wallet_menu.addMenu(_("&Private keys"))
395 import_keys = keys_menu.addAction(_("&Import"))
396 import_keys.triggered.connect(self.do_import_privkey)
397 export_keys = keys_menu.addAction(_("&Export"))
398 export_keys.triggered.connect(self.do_export_privkeys)
400 ex_history = wallet_menu.addAction(_("&Export History"))
401 ex_history.triggered.connect(self.do_export_history)
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 network = tools_menu.addAction(_("&Network"))
413 network.triggered.connect(self.run_network_dialog)
415 plugins_labels = tools_menu.addAction(_("&Plugins"))
416 plugins_labels.triggered.connect(self.plugins_dialog)
418 tools_menu.addSeparator()
420 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
422 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
423 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
425 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
426 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
428 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
430 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
431 raw_transaction_file.triggered.connect(self.do_process_from_file)
433 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
434 raw_transaction_text.triggered.connect(self.do_process_from_text)
437 help_menu = menubar.addMenu(_("&Help"))
438 show_about = help_menu.addAction(_("&About"))
439 show_about.triggered.connect(self.show_about)
440 web_open = help_menu.addAction(_("&Official website"))
441 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
443 help_menu.addSeparator()
444 doc_open = help_menu.addAction(_("&Documentation"))
445 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
446 report_bug = help_menu.addAction(_("&Report Bug"))
447 report_bug.triggered.connect(self.show_report_bug)
449 self.setMenuBar(menubar)
451 def show_about(self):
452 QMessageBox.about(self, "Electrum",
453 _("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."))
455 def show_report_bug(self):
456 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
457 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
460 def notify_transactions(self):
461 if not self.network or not self.network.is_connected():
464 print_error("Notifying GUI")
465 if len(self.network.interface.pending_transactions_for_notifications) > 0:
466 # Combine the transactions if there are more then three
467 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
470 for tx in self.network.interface.pending_transactions_for_notifications:
471 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
475 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
476 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
478 self.network.interface.pending_transactions_for_notifications = []
480 for tx in self.network.interface.pending_transactions_for_notifications:
482 self.network.interface.pending_transactions_for_notifications.remove(tx)
483 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
485 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
487 def notify(self, message):
488 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
492 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
493 def getOpenFileName(self, title, filter = ""):
494 directory = self.config.get('io_dir', os.path.expanduser('~'))
495 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
496 if fileName and directory != os.path.dirname(fileName):
497 self.config.set_key('io_dir', os.path.dirname(fileName), True)
500 def getSaveFileName(self, title, filename, filter = ""):
501 directory = self.config.get('io_dir', os.path.expanduser('~'))
502 path = os.path.join( directory, filename )
503 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
504 if fileName and directory != os.path.dirname(fileName):
505 self.config.set_key('io_dir', os.path.dirname(fileName), True)
509 QMainWindow.close(self)
510 run_hook('close_main_window')
512 def connect_slots(self, sender):
513 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
514 self.previous_payto_e=''
516 def timer_actions(self):
517 if self.need_update.is_set():
519 self.need_update.clear()
520 run_hook('timer_actions')
522 def format_amount(self, x, is_diff=False, whitespaces=False):
523 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
525 def read_amount(self, x):
526 if x in['.', '']: return None
527 p = pow(10, self.decimal_point)
528 return int( p * Decimal(x) )
531 assert self.decimal_point in [5,8]
532 return "BTC" if self.decimal_point == 8 else "mBTC"
535 def update_status(self):
536 if self.network is None:
538 icon = QIcon(":icons/status_disconnected.png")
540 elif self.network.is_connected():
541 if not self.wallet.up_to_date:
542 text = _("Synchronizing...")
543 icon = QIcon(":icons/status_waiting.png")
544 elif self.network.server_lag > 1:
545 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
546 icon = QIcon(":icons/status_lagging.png")
548 c, u = self.wallet.get_account_balance(self.current_account)
549 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
550 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
553 run_hook('set_quote_text', c+u, r)
556 text += " (%s)"%quote
558 self.tray.setToolTip(text)
559 icon = QIcon(":icons/status_connected.png")
561 text = _("Not connected")
562 icon = QIcon(":icons/status_disconnected.png")
564 self.balance_label.setText(text)
565 self.status_button.setIcon( icon )
568 def update_wallet(self):
570 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
571 self.update_history_tab()
572 self.update_receive_tab()
573 self.update_contacts_tab()
574 self.update_completions()
577 def create_history_tab(self):
578 self.history_list = l = MyTreeWidget(self)
580 for i,width in enumerate(self.column_widths['history']):
581 l.setColumnWidth(i, width)
582 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
583 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
584 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
586 l.customContextMenuRequested.connect(self.create_history_menu)
590 def create_history_menu(self, position):
591 self.history_list.selectedIndexes()
592 item = self.history_list.currentItem()
594 tx_hash = str(item.data(0, Qt.UserRole).toString())
595 if not tx_hash: return
597 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
598 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
599 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
600 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
603 def show_transaction(self, tx):
604 import transaction_dialog
605 d = transaction_dialog.TxDialog(tx, self)
608 def tx_label_clicked(self, item, column):
609 if column==2 and item.isSelected():
611 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
612 self.history_list.editItem( item, column )
613 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
616 def tx_label_changed(self, item, column):
620 tx_hash = str(item.data(0, Qt.UserRole).toString())
621 tx = self.wallet.transactions.get(tx_hash)
622 text = unicode( item.text(2) )
623 self.wallet.set_label(tx_hash, text)
625 item.setForeground(2, QBrush(QColor('black')))
627 text = self.wallet.get_default_label(tx_hash)
628 item.setText(2, text)
629 item.setForeground(2, QBrush(QColor('gray')))
633 def edit_label(self, is_recv):
634 l = self.receive_list if is_recv else self.contacts_list
635 item = l.currentItem()
636 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
637 l.editItem( item, 1 )
638 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
642 def address_label_clicked(self, item, column, l, column_addr, column_label):
643 if column == column_label and item.isSelected():
644 is_editable = item.data(0, 32).toBool()
647 addr = unicode( item.text(column_addr) )
648 label = unicode( item.text(column_label) )
649 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
650 l.editItem( item, column )
651 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
654 def address_label_changed(self, item, column, l, column_addr, column_label):
655 if column == column_label:
656 addr = unicode( item.text(column_addr) )
657 text = unicode( item.text(column_label) )
658 is_editable = item.data(0, 32).toBool()
662 changed = self.wallet.set_label(addr, text)
664 self.update_history_tab()
665 self.update_completions()
667 self.current_item_changed(item)
669 run_hook('item_changed', item, column)
672 def current_item_changed(self, a):
673 run_hook('current_item_changed', a)
677 def update_history_tab(self):
679 self.history_list.clear()
680 for item in self.wallet.get_tx_history(self.current_account):
681 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
682 time_str = _("unknown")
685 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
687 time_str = _("error")
690 time_str = 'unverified'
691 icon = QIcon(":icons/unconfirmed.png")
694 icon = QIcon(":icons/unconfirmed.png")
696 icon = QIcon(":icons/clock%d.png"%conf)
698 icon = QIcon(":icons/confirmed.png")
700 if value is not None:
701 v_str = self.format_amount(value, True, whitespaces=True)
705 balance_str = self.format_amount(balance, whitespaces=True)
708 label, is_default_label = self.wallet.get_label(tx_hash)
710 label = _('Pruned transaction outputs')
711 is_default_label = False
713 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
714 item.setFont(2, QFont(MONOSPACE_FONT))
715 item.setFont(3, QFont(MONOSPACE_FONT))
716 item.setFont(4, QFont(MONOSPACE_FONT))
718 item.setForeground(3, QBrush(QColor("#BC1E1E")))
720 item.setData(0, Qt.UserRole, tx_hash)
721 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
723 item.setForeground(2, QBrush(QColor('grey')))
725 item.setIcon(0, icon)
726 self.history_list.insertTopLevelItem(0,item)
729 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
732 def create_send_tab(self):
737 grid.setColumnMinimumWidth(3,300)
738 grid.setColumnStretch(5,1)
741 self.payto_e = QLineEdit()
742 grid.addWidget(QLabel(_('Pay to')), 1, 0)
743 grid.addWidget(self.payto_e, 1, 1, 1, 3)
745 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)
747 completer = QCompleter()
748 completer.setCaseSensitivity(False)
749 self.payto_e.setCompleter(completer)
750 completer.setModel(self.completions)
752 self.message_e = QLineEdit()
753 grid.addWidget(QLabel(_('Description')), 2, 0)
754 grid.addWidget(self.message_e, 2, 1, 1, 3)
755 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)
757 self.amount_e = AmountEdit(self.base_unit)
758 grid.addWidget(QLabel(_('Amount')), 3, 0)
759 grid.addWidget(self.amount_e, 3, 1, 1, 2)
760 grid.addWidget(HelpButton(
761 _('Amount to be sent.') + '\n\n' \
762 + _('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.') \
763 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
765 self.fee_e = AmountEdit(self.base_unit)
766 grid.addWidget(QLabel(_('Fee')), 4, 0)
767 grid.addWidget(self.fee_e, 4, 1, 1, 2)
768 grid.addWidget(HelpButton(
769 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
770 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
771 + _('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)
774 self.send_button = EnterButton(_("Send"), self.do_send)
775 grid.addWidget(self.send_button, 6, 1)
777 b = EnterButton(_("Clear"),self.do_clear)
778 grid.addWidget(b, 6, 2)
780 self.payto_sig = QLabel('')
781 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
783 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
784 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
793 def entry_changed( is_fee ):
794 self.funds_error = False
796 if self.amount_e.is_shortcut:
797 self.amount_e.is_shortcut = False
798 c, u = self.wallet.get_account_balance(self.current_account)
799 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( c + u, 0, self.current_account)
800 fee = self.wallet.estimated_fee(inputs)
802 self.amount_e.setText( self.format_amount(amount) )
803 self.fee_e.setText( self.format_amount( fee ) )
806 amount = self.read_amount(str(self.amount_e.text()))
807 fee = self.read_amount(str(self.fee_e.text()))
809 if not is_fee: fee = None
812 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( amount, fee, self.current_account )
814 self.fee_e.setText( self.format_amount( fee ) )
817 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
821 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
822 self.funds_error = True
823 text = _( "Not enough funds" )
824 c, u = self.wallet.get_frozen_balance()
825 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
827 self.statusBar().showMessage(text)
828 self.amount_e.setPalette(palette)
829 self.fee_e.setPalette(palette)
831 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
832 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
834 run_hook('create_send_tab', grid)
838 def update_completions(self):
840 for addr,label in self.wallet.labels.items():
841 if addr in self.wallet.addressbook:
842 l.append( label + ' <' + addr + '>')
844 run_hook('update_completions', l)
845 self.completions.setStringList(l)
849 return lambda s, *args: s.do_protect(func, args)
854 label = unicode( self.message_e.text() )
855 r = unicode( self.payto_e.text() )
858 # label or alias, with address in brackets
859 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
860 to_address = m.group(2) if m else r
862 if not is_valid(to_address):
863 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
867 amount = self.read_amount(unicode( self.amount_e.text()))
869 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
872 fee = self.read_amount(unicode( self.fee_e.text()))
874 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
877 confirm_amount = self.config.get('confirm_amount', 100000000)
878 if amount >= confirm_amount:
879 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
882 self.send_tx(to_address, amount, fee, label)
886 def send_tx(self, to_address, amount, fee, label, password):
889 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
890 except Exception as e:
891 traceback.print_exc(file=sys.stdout)
892 self.show_message(str(e))
895 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
896 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
900 self.wallet.set_label(tx.hash(), label)
903 h = self.wallet.send_tx(tx)
904 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
905 status, msg = self.wallet.receive_tx( h )
907 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
909 self.update_contacts_tab()
911 QMessageBox.warning(self, _('Error'), msg, _('OK'))
914 self.show_transaction(tx)
916 # add recipient to addressbook
917 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
918 self.wallet.addressbook.append(to_address)
923 def set_url(self, url):
924 address, amount, label, message, signature, identity, url = util.parse_url(url)
927 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
928 elif amount: amount = str(Decimal(amount))
931 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
934 self.mini.set_payment_fields(address, amount)
936 if label and self.wallet.labels.get(address) != label:
937 if self.question('Give label "%s" to address %s ?'%(label,address)):
938 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
939 self.wallet.addressbook.append(address)
940 self.wallet.set_label(address, label)
942 run_hook('set_url', url, self.show_message, self.question)
944 self.tabs.setCurrentIndex(1)
945 label = self.wallet.labels.get(address)
946 m_addr = label + ' <'+ address +'>' if label else address
947 self.payto_e.setText(m_addr)
949 self.message_e.setText(message)
951 self.amount_e.setText(amount)
954 self.set_frozen(self.payto_e,True)
955 self.set_frozen(self.amount_e,True)
956 self.set_frozen(self.message_e,True)
957 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
959 self.payto_sig.setVisible(False)
962 self.payto_sig.setVisible(False)
963 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
965 self.set_frozen(e,False)
968 def set_frozen(self,entry,frozen):
970 entry.setReadOnly(True)
971 entry.setFrame(False)
973 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
974 entry.setPalette(palette)
976 entry.setReadOnly(False)
979 palette.setColor(entry.backgroundRole(), QColor('white'))
980 entry.setPalette(palette)
983 def toggle_freeze(self,addr):
985 if addr in self.wallet.frozen_addresses:
986 self.wallet.unfreeze(addr)
988 self.wallet.freeze(addr)
989 self.update_receive_tab()
991 def toggle_priority(self,addr):
993 if addr in self.wallet.prioritized_addresses:
994 self.wallet.unprioritize(addr)
996 self.wallet.prioritize(addr)
997 self.update_receive_tab()
1000 def create_list_tab(self, headers):
1001 "generic tab creation method"
1002 l = MyTreeWidget(self)
1003 l.setColumnCount( len(headers) )
1004 l.setHeaderLabels( headers )
1007 vbox = QVBoxLayout()
1014 vbox.addWidget(buttons)
1016 hbox = QHBoxLayout()
1019 buttons.setLayout(hbox)
1024 def create_receive_tab(self):
1025 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1026 l.setContextMenuPolicy(Qt.CustomContextMenu)
1027 l.customContextMenuRequested.connect(self.create_receive_menu)
1028 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1029 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1030 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1031 self.receive_list = l
1032 self.receive_buttons_hbox = hbox
1039 def save_column_widths(self):
1040 self.column_widths["receive"] = []
1041 for i in range(self.receive_list.columnCount() -1):
1042 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1044 self.column_widths["history"] = []
1045 for i in range(self.history_list.columnCount() - 1):
1046 self.column_widths["history"].append(self.history_list.columnWidth(i))
1048 self.column_widths["contacts"] = []
1049 for i in range(self.contacts_list.columnCount() - 1):
1050 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1052 self.config.set_key("column_widths_2", self.column_widths, True)
1055 def create_contacts_tab(self):
1056 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1057 l.setContextMenuPolicy(Qt.CustomContextMenu)
1058 l.customContextMenuRequested.connect(self.create_contact_menu)
1059 for i,width in enumerate(self.column_widths['contacts']):
1060 l.setColumnWidth(i, width)
1062 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1063 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1064 self.contacts_list = l
1065 self.contacts_buttons_hbox = hbox
1070 def delete_imported_key(self, addr):
1071 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1072 self.wallet.delete_imported_key(addr)
1073 self.update_receive_tab()
1074 self.update_history_tab()
1076 def edit_account_label(self, k):
1077 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1079 label = unicode(text)
1080 self.wallet.set_label(k,label)
1081 self.update_receive_tab()
1083 def account_set_expanded(self, item, k, b):
1085 self.accounts_expanded[k] = b
1087 def create_account_menu(self, position, k, item):
1089 if item.isExpanded():
1090 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1092 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1093 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1094 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1095 if self.wallet.account_is_pending(k):
1096 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1097 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1099 def delete_pending_account(self, k):
1100 self.wallet.delete_pending_account(k)
1101 self.update_receive_tab()
1103 def create_receive_menu(self, position):
1104 # fixme: this function apparently has a side effect.
1105 # if it is not called the menu pops up several times
1106 #self.receive_list.selectedIndexes()
1108 item = self.receive_list.itemAt(position)
1111 addr = unicode(item.text(0))
1112 if not is_valid(addr):
1113 k = str(item.data(0,32).toString())
1115 self.create_account_menu(position, k, item)
1117 item.setExpanded(not item.isExpanded())
1121 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1122 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1123 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1124 if self.wallet.seed:
1125 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1126 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1127 if addr in self.wallet.imported_keys:
1128 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1130 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1131 menu.addAction(t, lambda: self.toggle_freeze(addr))
1132 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1133 menu.addAction(t, lambda: self.toggle_priority(addr))
1135 run_hook('receive_menu', menu)
1136 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1139 def payto(self, addr):
1141 label = self.wallet.labels.get(addr)
1142 m_addr = label + ' <' + addr + '>' if label else addr
1143 self.tabs.setCurrentIndex(1)
1144 self.payto_e.setText(m_addr)
1145 self.amount_e.setFocus()
1148 def delete_contact(self, x):
1149 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1150 self.wallet.delete_contact(x)
1151 self.wallet.set_label(x, None)
1152 self.update_history_tab()
1153 self.update_contacts_tab()
1154 self.update_completions()
1157 def create_contact_menu(self, position):
1158 item = self.contacts_list.itemAt(position)
1160 addr = unicode(item.text(0))
1161 label = unicode(item.text(1))
1162 is_editable = item.data(0,32).toBool()
1163 payto_addr = item.data(0,33).toString()
1165 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1166 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1167 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1169 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1170 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1172 run_hook('create_contact_menu', menu, item)
1173 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1176 def update_receive_item(self, item, num_tx = 0):
1177 item.setFont(0, QFont(MONOSPACE_FONT))
1178 address = str(item.data(0,0).toString())
1179 label = self.wallet.labels.get(address,'')
1180 item.setData(1,0,label)
1181 item.setData(0,32, True) # is editable
1183 run_hook('update_receive_item', address, item)
1185 if not self.wallet.is_mine(address): return
1187 c, u = self.wallet.get_addr_balance(address)
1188 balance = self.format_amount(c + u)
1189 item.setData(2,0,balance)
1191 if (num_tx > 1) and (c == -u):
1192 item.setForeground(0,QColor('lightgray'))
1193 item.setForeground(1,QColor('gray'))
1194 item.setForeground(2,QColor('gray'))
1195 item.setForeground(3,QColor('gray'))
1196 elif address in self.wallet.frozen_addresses:
1197 item.setBackgroundColor(0, QColor('lightblue'))
1198 elif address in self.wallet.prioritized_addresses:
1199 item.setBackgroundColor(0, QColor('lightgreen'))
1202 def update_receive_tab(self):
1203 l = self.receive_list
1206 l.setColumnHidden(2, False)
1207 l.setColumnHidden(3, False)
1208 for i,width in enumerate(self.column_widths['receive']):
1209 l.setColumnWidth(i, width)
1211 if self.current_account is None:
1212 account_items = self.wallet.accounts.items()
1213 elif self.current_account != -1:
1214 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1218 for k, account in account_items:
1219 name = self.wallet.get_account_name(k)
1220 c,u = self.wallet.get_account_balance(k)
1221 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1222 l.addTopLevelItem(account_item)
1223 account_item.setExpanded(self.accounts_expanded.get(k, True))
1224 account_item.setData(0, 32, k)
1226 if not self.wallet.is_seeded(k):
1227 icon = QIcon(":icons/key.png")
1228 account_item.setIcon(0, icon)
1230 for is_change in ([0,1]):
1231 name = _("Receiving") if not is_change else _("Change")
1232 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1233 account_item.addChild(seq_item)
1234 if not is_change: seq_item.setExpanded(True)
1239 for address in account.get_addresses(is_change):
1240 h = self.wallet.history.get(address,[])
1244 if gap > self.wallet.gap_limit:
1249 num_tx = '*' if h == ['*'] else "%d"%len(h)
1250 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1251 self.update_receive_item(item, len(h))
1253 item.setBackgroundColor(1, QColor('red'))
1254 seq_item.addChild(item)
1257 for k, addr in self.wallet.get_pending_accounts():
1258 name = self.wallet.labels.get(k,'')
1259 account_item = QTreeWidgetItem( [ name + " [ "+_('pending account')+" ]", '', '', ''] )
1260 self.update_receive_item(item)
1261 l.addTopLevelItem(account_item)
1262 account_item.setExpanded(True)
1263 account_item.setData(0, 32, k)
1264 item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1265 account_item.addChild(item)
1266 self.update_receive_item(item)
1269 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1270 c,u = self.wallet.get_imported_balance()
1271 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1272 l.addTopLevelItem(account_item)
1273 account_item.setExpanded(True)
1274 for address in self.wallet.imported_keys.keys():
1275 item = QTreeWidgetItem( [ address, '', '', ''] )
1276 self.update_receive_item(item)
1277 account_item.addChild(item)
1280 # we use column 1 because column 0 may be hidden
1281 l.setCurrentItem(l.topLevelItem(0),1)
1284 def update_contacts_tab(self):
1285 l = self.contacts_list
1288 for address in self.wallet.addressbook:
1289 label = self.wallet.labels.get(address,'')
1290 n = self.wallet.get_num_tx(address)
1291 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1292 item.setFont(0, QFont(MONOSPACE_FONT))
1293 # 32 = label can be edited (bool)
1294 item.setData(0,32, True)
1296 item.setData(0,33, address)
1297 l.addTopLevelItem(item)
1299 run_hook('update_contacts_tab', l)
1300 l.setCurrentItem(l.topLevelItem(0))
1304 def create_console_tab(self):
1305 from console import Console
1306 self.console = console = Console()
1310 def update_console(self):
1311 console = self.console
1312 console.history = self.config.get("console-history",[])
1313 console.history_index = len(console.history)
1315 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1316 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1318 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1320 def mkfunc(f, method):
1321 return lambda *args: apply( f, (method, args, self.password_dialog ))
1323 if m[0]=='_' or m in ['network','wallet']: continue
1324 methods[m] = mkfunc(c._run, m)
1326 console.updateNamespace(methods)
1329 def change_account(self,s):
1330 if s == _("All accounts"):
1331 self.current_account = None
1333 accounts = self.wallet.get_account_names()
1334 for k, v in accounts.items():
1336 self.current_account = k
1337 self.update_history_tab()
1338 self.update_status()
1339 self.update_receive_tab()
1341 def create_status_bar(self):
1344 sb.setFixedHeight(35)
1345 qtVersion = qVersion()
1347 self.balance_label = QLabel("")
1348 sb.addWidget(self.balance_label)
1350 from version_getter import UpdateLabel
1351 self.updatelabel = UpdateLabel(self.config, sb)
1353 self.account_selector = QComboBox()
1354 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1355 sb.addPermanentWidget(self.account_selector)
1357 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1358 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1360 self.lock_icon = QIcon()
1361 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1362 sb.addPermanentWidget( self.password_button )
1364 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1365 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1366 sb.addPermanentWidget( self.seed_button )
1367 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1368 sb.addPermanentWidget( self.status_button )
1370 run_hook('create_status_bar', (sb,))
1372 self.setStatusBar(sb)
1375 def update_lock_icon(self):
1376 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1377 self.password_button.setIcon( icon )
1380 def update_buttons_on_seed(self):
1381 if not self.wallet.is_watching_only():
1382 self.seed_button.show()
1383 self.password_button.show()
1384 self.send_button.setText(_("Send"))
1386 self.password_button.hide()
1387 self.seed_button.hide()
1388 self.send_button.setText(_("Create unsigned transaction"))
1391 def change_password_dialog(self):
1392 from password_dialog import PasswordDialog
1393 d = PasswordDialog(self.wallet, self)
1395 self.update_lock_icon()
1398 def new_contact_dialog(self):
1401 vbox = QVBoxLayout(d)
1402 vbox.addWidget(QLabel(_('New Contact')+':'))
1404 grid = QGridLayout()
1407 grid.addWidget(QLabel(_("Address")), 1, 0)
1408 grid.addWidget(line1, 1, 1)
1409 grid.addWidget(QLabel(_("Name")), 2, 0)
1410 grid.addWidget(line2, 2, 1)
1412 vbox.addLayout(grid)
1413 vbox.addLayout(ok_cancel_buttons(d))
1418 address = str(line1.text())
1419 label = unicode(line2.text())
1421 if not is_valid(address):
1422 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1425 self.wallet.add_contact(address)
1427 self.wallet.set_label(address, label)
1429 self.update_contacts_tab()
1430 self.update_history_tab()
1431 self.update_completions()
1432 self.tabs.setCurrentIndex(3)
1435 def new_account_dialog(self):
1437 dialog = QDialog(self)
1439 dialog.setWindowTitle(_("New Account"))
1441 vbox = QVBoxLayout()
1442 vbox.addWidget(QLabel(_('Account name')+':'))
1445 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1446 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1451 vbox.addLayout(ok_cancel_buttons(dialog))
1452 dialog.setLayout(vbox)
1456 name = str(e.text())
1459 self.wallet.create_pending_account('1', name)
1460 self.update_receive_tab()
1461 self.tabs.setCurrentIndex(2)
1465 def show_master_public_key_old(self):
1466 dialog = QDialog(self)
1468 dialog.setWindowTitle(_("Master Public Key"))
1470 main_text = QTextEdit()
1471 main_text.setText(self.wallet.get_master_public_key())
1472 main_text.setReadOnly(True)
1473 main_text.setMaximumHeight(170)
1474 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1476 ok_button = QPushButton(_("OK"))
1477 ok_button.setDefault(True)
1478 ok_button.clicked.connect(dialog.accept)
1480 main_layout = QGridLayout()
1481 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1483 main_layout.addWidget(main_text, 1, 0)
1484 main_layout.addWidget(qrw, 1, 1 )
1486 vbox = QVBoxLayout()
1487 vbox.addLayout(main_layout)
1488 vbox.addLayout(close_button(dialog))
1489 dialog.setLayout(vbox)
1493 def show_master_public_key(self):
1495 if self.wallet.seed_version == 4:
1496 self.show_master_public_key_old()
1499 dialog = QDialog(self)
1501 dialog.setWindowTitle(_("Master Public Keys"))
1503 chain_text = QTextEdit()
1504 chain_text.setReadOnly(True)
1505 chain_text.setMaximumHeight(170)
1506 chain_qrw = QRCodeWidget()
1508 mpk_text = QTextEdit()
1509 mpk_text.setReadOnly(True)
1510 mpk_text.setMaximumHeight(170)
1511 mpk_qrw = QRCodeWidget()
1513 main_layout = QGridLayout()
1515 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1516 main_layout.addWidget(mpk_text, 1, 1)
1517 main_layout.addWidget(mpk_qrw, 1, 2)
1519 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1520 main_layout.addWidget(chain_text, 2, 1)
1521 main_layout.addWidget(chain_qrw, 2, 2)
1524 c, K, cK = self.wallet.master_public_keys[str(key)]
1525 chain_text.setText(c)
1526 chain_qrw.set_addr(c)
1527 chain_qrw.update_qr()
1532 key_selector = QComboBox()
1533 keys = sorted(self.wallet.master_public_keys.keys())
1534 key_selector.addItems(keys)
1536 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1537 main_layout.addWidget(key_selector, 0, 1)
1538 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1542 vbox = QVBoxLayout()
1543 vbox.addLayout(main_layout)
1544 vbox.addLayout(close_button(dialog))
1546 dialog.setLayout(vbox)
1551 def show_seed_dialog(self, password):
1552 if self.wallet.is_watching_only():
1553 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1556 if self.wallet.seed:
1558 mnemonic = self.wallet.get_mnemonic(password)
1560 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1562 from seed_dialog import SeedDialog
1563 d = SeedDialog(self, mnemonic, self.wallet.imported_keys)
1567 for k in self.wallet.master_private_keys.keys():
1568 pk = self.wallet.get_master_private_key(k, password)
1570 from seed_dialog import PrivateKeysDialog
1571 d = PrivateKeysDialog(self,l)
1578 def show_qrcode(self, data, title = _("QR code")):
1582 d.setWindowTitle(title)
1583 d.setMinimumSize(270, 300)
1584 vbox = QVBoxLayout()
1585 qrw = QRCodeWidget(data)
1586 vbox.addWidget(qrw, 1)
1587 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1588 hbox = QHBoxLayout()
1591 filename = os.path.join(self.config.path, "qrcode.bmp")
1594 bmp.save_qrcode(qrw.qr, filename)
1595 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1597 def copy_to_clipboard():
1598 bmp.save_qrcode(qrw.qr, filename)
1599 self.app.clipboard().setImage(QImage(filename))
1600 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1602 b = QPushButton(_("Copy"))
1604 b.clicked.connect(copy_to_clipboard)
1606 b = QPushButton(_("Save"))
1608 b.clicked.connect(print_qr)
1610 b = QPushButton(_("Close"))
1612 b.clicked.connect(d.accept)
1615 vbox.addLayout(hbox)
1620 def do_protect(self, func, args):
1621 if self.wallet.use_encryption:
1622 password = self.password_dialog()
1628 if args != (False,):
1629 args = (self,) + args + (password,)
1631 args = (self,password)
1636 def show_private_key(self, address, password):
1637 if not address: return
1639 pk_list = self.wallet.get_private_key(address, password)
1640 except Exception as e:
1641 self.show_message(str(e))
1643 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1647 def do_sign(self, address, message, signature, password):
1648 message = unicode(message.toPlainText())
1649 message = message.encode('utf-8')
1651 sig = self.wallet.sign_message(str(address.text()), message, password)
1652 signature.setText(sig)
1653 except Exception as e:
1654 self.show_message(str(e))
1656 def sign_message(self, address):
1657 if not address: return
1660 d.setWindowTitle(_('Sign Message'))
1661 d.setMinimumSize(410, 290)
1663 tab_widget = QTabWidget()
1665 layout = QGridLayout(tab)
1667 sign_address = QLineEdit()
1669 sign_address.setText(address)
1670 layout.addWidget(QLabel(_('Address')), 1, 0)
1671 layout.addWidget(sign_address, 1, 1)
1673 sign_message = QTextEdit()
1674 layout.addWidget(QLabel(_('Message')), 2, 0)
1675 layout.addWidget(sign_message, 2, 1)
1676 layout.setRowStretch(2,3)
1678 sign_signature = QTextEdit()
1679 layout.addWidget(QLabel(_('Signature')), 3, 0)
1680 layout.addWidget(sign_signature, 3, 1)
1681 layout.setRowStretch(3,1)
1684 hbox = QHBoxLayout()
1685 b = QPushButton(_("Sign"))
1687 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1688 b = QPushButton(_("Close"))
1689 b.clicked.connect(d.accept)
1691 layout.addLayout(hbox, 4, 1)
1692 tab_widget.addTab(tab, _("Sign"))
1696 layout = QGridLayout(tab)
1698 verify_address = QLineEdit()
1699 layout.addWidget(QLabel(_('Address')), 1, 0)
1700 layout.addWidget(verify_address, 1, 1)
1702 verify_message = QTextEdit()
1703 layout.addWidget(QLabel(_('Message')), 2, 0)
1704 layout.addWidget(verify_message, 2, 1)
1705 layout.setRowStretch(2,3)
1707 verify_signature = QTextEdit()
1708 layout.addWidget(QLabel(_('Signature')), 3, 0)
1709 layout.addWidget(verify_signature, 3, 1)
1710 layout.setRowStretch(3,1)
1713 message = unicode(verify_message.toPlainText())
1714 message = message.encode('utf-8')
1715 if bitcoin.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1716 self.show_message(_("Signature verified"))
1718 self.show_message(_("Error: wrong signature"))
1720 hbox = QHBoxLayout()
1721 b = QPushButton(_("Verify"))
1722 b.clicked.connect(do_verify)
1724 b = QPushButton(_("Close"))
1725 b.clicked.connect(d.accept)
1727 layout.addLayout(hbox, 4, 1)
1728 tab_widget.addTab(tab, _("Verify"))
1730 vbox = QVBoxLayout()
1731 vbox.addWidget(tab_widget)
1738 def question(self, msg):
1739 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1741 def show_message(self, msg):
1742 QMessageBox.information(self, _('Message'), msg, _('OK'))
1744 def password_dialog(self ):
1751 vbox = QVBoxLayout()
1752 msg = _('Please enter your password')
1753 vbox.addWidget(QLabel(msg))
1755 grid = QGridLayout()
1757 grid.addWidget(QLabel(_('Password')), 1, 0)
1758 grid.addWidget(pw, 1, 1)
1759 vbox.addLayout(grid)
1761 vbox.addLayout(ok_cancel_buttons(d))
1764 run_hook('password_dialog', pw, grid, 1)
1765 if not d.exec_(): return
1766 return unicode(pw.text())
1775 def tx_from_text(self, txt):
1776 "json or raw hexadecimal"
1779 tx = Transaction(txt)
1785 tx_dict = json.loads(str(txt))
1786 assert "hex" in tx_dict.keys()
1787 assert "complete" in tx_dict.keys()
1788 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1789 if not tx_dict["complete"]:
1790 assert "input_info" in tx_dict.keys()
1791 input_info = json.loads(tx_dict['input_info'])
1792 tx.add_input_info(input_info)
1797 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1801 def read_tx_from_file(self):
1802 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1806 with open(fileName, "r") as f:
1807 file_content = f.read()
1808 except (ValueError, IOError, os.error), reason:
1809 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1811 return self.tx_from_text(file_content)
1815 def sign_raw_transaction(self, tx, input_info, password):
1816 self.wallet.signrawtransaction(tx, input_info, [], password)
1818 def do_process_from_text(self):
1819 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1822 tx = self.tx_from_text(text)
1824 self.show_transaction(tx)
1826 def do_process_from_file(self):
1827 tx = self.read_tx_from_file()
1829 self.show_transaction(tx)
1831 def do_process_from_csvReader(self, csvReader):
1834 for row in csvReader:
1836 amount = float(row[1])
1837 amount = int(100000000*amount)
1838 outputs.append((address, amount))
1839 except (ValueError, IOError, os.error), reason:
1840 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1844 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1845 except Exception as e:
1846 self.show_message(str(e))
1849 self.show_transaction(tx)
1851 def do_process_from_csv_file(self):
1852 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1856 with open(fileName, "r") as f:
1857 csvReader = csv.reader(f)
1858 self.do_process_from_csvReader(csvReader)
1859 except (ValueError, IOError, os.error), reason:
1860 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1863 def do_process_from_csv_text(self):
1864 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1865 + _("Format: address, amount. One output per line"), _("Load CSV"))
1868 f = StringIO.StringIO(text)
1869 csvReader = csv.reader(f)
1870 self.do_process_from_csvReader(csvReader)
1875 def do_export_privkeys(self, password):
1876 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.")))
1879 select_export = _('Select file to export your private keys to')
1880 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1882 with open(fileName, "w+") as csvfile:
1883 transaction = csv.writer(csvfile)
1884 transaction.writerow(["address", "private_key"])
1886 addresses = self.wallet.addresses(True)
1888 for addr in addresses:
1889 pk = "".join(self.wallet.get_private_key(addr, password))
1890 transaction.writerow(["%34s"%addr,pk])
1892 self.show_message(_("Private keys exported."))
1894 except (IOError, os.error), reason:
1895 export_error_label = _("Electrum was unable to produce a private key-export.")
1896 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1898 except Exception as e:
1899 self.show_message(str(e))
1903 def do_import_labels(self):
1904 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1905 if not labelsFile: return
1907 f = open(labelsFile, 'r')
1910 for key, value in json.loads(data).items():
1911 self.wallet.set_label(key, value)
1912 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1913 except (IOError, os.error), reason:
1914 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1917 def do_export_labels(self):
1918 labels = self.wallet.labels
1920 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1922 with open(fileName, 'w+') as f:
1923 json.dump(labels, f)
1924 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1925 except (IOError, os.error), reason:
1926 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1929 def do_export_history(self):
1930 from lite_window import csv_transaction
1931 csv_transaction(self.wallet)
1935 def do_import_privkey(self, password):
1936 if not self.wallet.imported_keys:
1937 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1938 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1939 + _('Are you sure you understand what you are doing?'), 3, 4)
1942 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1945 text = str(text).split()
1950 addr = self.wallet.import_key(key, password)
1951 except Exception as e:
1957 addrlist.append(addr)
1959 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1961 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1962 self.update_receive_tab()
1963 self.update_history_tab()
1966 def settings_dialog(self):
1968 d.setWindowTitle(_('Electrum Settings'))
1970 vbox = QVBoxLayout()
1971 grid = QGridLayout()
1972 grid.setColumnStretch(0,1)
1974 nz_label = QLabel(_('Display zeros') + ':')
1975 grid.addWidget(nz_label, 0, 0)
1976 nz_e = AmountEdit(None,True)
1977 nz_e.setText("%d"% self.num_zeros)
1978 grid.addWidget(nz_e, 0, 1)
1979 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1980 grid.addWidget(HelpButton(msg), 0, 2)
1981 if not self.config.is_modifiable('num_zeros'):
1982 for w in [nz_e, nz_label]: w.setEnabled(False)
1984 lang_label=QLabel(_('Language') + ':')
1985 grid.addWidget(lang_label, 1, 0)
1986 lang_combo = QComboBox()
1987 from electrum.i18n import languages
1988 lang_combo.addItems(languages.values())
1990 index = languages.keys().index(self.config.get("language",''))
1993 lang_combo.setCurrentIndex(index)
1994 grid.addWidget(lang_combo, 1, 1)
1995 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1996 if not self.config.is_modifiable('language'):
1997 for w in [lang_combo, lang_label]: w.setEnabled(False)
2000 fee_label = QLabel(_('Transaction fee') + ':')
2001 grid.addWidget(fee_label, 2, 0)
2002 fee_e = AmountEdit(self.base_unit)
2003 fee_e.setText(self.format_amount(self.wallet.fee).strip())
2004 grid.addWidget(fee_e, 2, 1)
2005 msg = _('Fee per kilobyte of transaction.') + ' ' \
2006 + _('Recommended value') + ': ' + self.format_amount(20000)
2007 grid.addWidget(HelpButton(msg), 2, 2)
2008 if not self.config.is_modifiable('fee_per_kb'):
2009 for w in [fee_e, fee_label]: w.setEnabled(False)
2011 units = ['BTC', 'mBTC']
2012 unit_label = QLabel(_('Base unit') + ':')
2013 grid.addWidget(unit_label, 3, 0)
2014 unit_combo = QComboBox()
2015 unit_combo.addItems(units)
2016 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2017 grid.addWidget(unit_combo, 3, 1)
2018 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2019 + '\n1BTC=1000mBTC.\n' \
2020 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
2022 usechange_cb = QCheckBox(_('Use change addresses'))
2023 usechange_cb.setChecked(self.wallet.use_change)
2024 grid.addWidget(usechange_cb, 4, 0)
2025 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2026 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2028 grid.setRowStretch(5,1)
2030 vbox.addLayout(grid)
2031 vbox.addLayout(ok_cancel_buttons(d))
2035 if not d.exec_(): return
2037 fee = unicode(fee_e.text())
2039 fee = self.read_amount(fee)
2041 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2044 self.wallet.set_fee(fee)
2046 nz = unicode(nz_e.text())
2051 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2054 if self.num_zeros != nz:
2056 self.config.set_key('num_zeros', nz, True)
2057 self.update_history_tab()
2058 self.update_receive_tab()
2060 usechange_result = usechange_cb.isChecked()
2061 if self.wallet.use_change != usechange_result:
2062 self.wallet.use_change = usechange_result
2063 self.wallet.storage.put('use_change', self.wallet.use_change)
2065 unit_result = units[unit_combo.currentIndex()]
2066 if self.base_unit() != unit_result:
2067 self.decimal_point = 8 if unit_result == 'BTC' else 5
2068 self.config.set_key('decimal_point', self.decimal_point, True)
2069 self.update_history_tab()
2070 self.update_status()
2072 need_restart = False
2074 lang_request = languages.keys()[lang_combo.currentIndex()]
2075 if lang_request != self.config.get('language'):
2076 self.config.set_key("language", lang_request, True)
2079 run_hook('close_settings_dialog')
2082 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2085 def run_network_dialog(self):
2086 if not self.network:
2088 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2090 def closeEvent(self, event):
2092 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2093 self.save_column_widths()
2094 self.config.set_key("console-history", self.console.history[-50:], True)
2095 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2100 def plugins_dialog(self):
2101 from electrum.plugins import plugins
2104 d.setWindowTitle(_('Electrum Plugins'))
2107 vbox = QVBoxLayout(d)
2110 scroll = QScrollArea()
2111 scroll.setEnabled(True)
2112 scroll.setWidgetResizable(True)
2113 scroll.setMinimumSize(400,250)
2114 vbox.addWidget(scroll)
2118 w.setMinimumHeight(len(plugins)*35)
2120 grid = QGridLayout()
2121 grid.setColumnStretch(0,1)
2124 def do_toggle(cb, p, w):
2127 if w: w.setEnabled(r)
2129 def mk_toggle(cb, p, w):
2130 return lambda: do_toggle(cb,p,w)
2132 for i, p in enumerate(plugins):
2134 cb = QCheckBox(p.fullname())
2135 cb.setDisabled(not p.is_available())
2136 cb.setChecked(p.is_enabled())
2137 grid.addWidget(cb, i, 0)
2138 if p.requires_settings():
2139 w = p.settings_widget(self)
2140 w.setEnabled( p.is_enabled() )
2141 grid.addWidget(w, i, 1)
2144 cb.clicked.connect(mk_toggle(cb,p,w))
2145 grid.addWidget(HelpButton(p.description()), i, 2)
2147 print_msg(_("Error: cannot display plugin"), p)
2148 traceback.print_exc(file=sys.stdout)
2149 grid.setRowStretch(i+1,1)
2151 vbox.addLayout(close_button(d))
2156 def show_account_details(self, k):
2158 d.setWindowTitle(_('Account Details'))
2161 vbox = QVBoxLayout(d)
2162 roots = self.wallet.get_roots(k)
2164 name = self.wallet.get_account_name(k)
2165 label = QLabel('Name: ' + name)
2166 vbox.addWidget(label)
2168 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2169 vbox.addWidget(QLabel('Type: ' + acctype))
2171 label = QLabel('Derivation: ' + k)
2172 vbox.addWidget(label)
2175 # mpk = self.wallet.master_public_keys[root]
2176 # text = QTextEdit()
2177 # text.setReadOnly(True)
2178 # text.setMaximumHeight(120)
2179 # text.setText(repr(mpk))
2180 # vbox.addWidget(text)
2182 vbox.addLayout(close_button(d))