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 self.icon = QIcon(':icons/electrum_light_icon.png')
143 self.tray = QSystemTrayIcon(self.icon, self)
144 self.tray.setToolTip('Electrum')
145 self.tray.activated.connect(self.tray_activated)
149 self.create_status_bar()
151 self.need_update = threading.Event()
153 self.decimal_point = config.get('decimal_point', 8)
154 self.num_zeros = int(config.get('num_zeros',0))
156 set_language(config.get('language'))
158 self.funds_error = False
159 self.completions = QStringListModel()
161 self.tabs = tabs = QTabWidget(self)
162 self.column_widths = self.config.get("column_widths_2", default_column_widths )
163 tabs.addTab(self.create_history_tab(), _('History') )
164 tabs.addTab(self.create_send_tab(), _('Send') )
165 tabs.addTab(self.create_receive_tab(), _('Receive') )
166 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
167 tabs.addTab(self.create_console_tab(), _('Console') )
168 tabs.setMinimumSize(600, 400)
169 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
170 self.setCentralWidget(tabs)
172 g = self.config.get("winpos-qt",[100, 100, 840, 400])
173 self.setGeometry(g[0], g[1], g[2], g[3])
177 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
178 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
179 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
180 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
181 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
183 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
184 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
185 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
187 self.history_list.setFocus(True)
191 self.network.register_callback('updated', lambda: self.need_update.set())
192 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
193 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
194 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
195 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
197 # set initial message
198 self.console.showMessage(self.network.banner)
205 self.config.set_key('lite_mode', False, True)
210 self.config.set_key('lite_mode', True, True)
217 if not self.check_qt_version():
218 if self.config.get('lite_mode') is True:
219 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
220 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
221 self.config.set_key('lite_mode', False, True)
227 actuator = lite_window.MiniActuator(self)
229 # Should probably not modify the current path but instead
230 # change the behaviour of rsrc(...)
231 old_path = QDir.currentPath()
232 actuator.load_theme()
234 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
236 driver = lite_window.MiniDriver(self, self.mini)
238 # Reset path back to original value now that loading the GUI
240 QDir.setCurrent(old_path)
242 if self.config.get('lite_mode') is True:
248 def check_qt_version(self):
249 qtVersion = qVersion()
250 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
253 def update_account_selector(self):
255 accounts = self.wallet.get_account_names()
256 self.account_selector.clear()
257 if len(accounts) > 1:
258 self.account_selector.addItems([_("All accounts")] + accounts.values())
259 self.account_selector.setCurrentIndex(0)
260 self.account_selector.show()
262 self.account_selector.hide()
265 def load_wallet(self, wallet):
268 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
269 self.current_account = self.wallet.storage.get("current_account", None)
271 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
272 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
273 self.setWindowTitle( title )
275 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
276 self.notify_transactions()
277 self.update_account_selector()
278 self.new_account.setEnabled(self.wallet.seed_version>4)
279 self.update_lock_icon()
280 self.update_buttons_on_seed()
281 self.update_console()
283 run_hook('load_wallet', wallet)
286 def select_wallet_file(self):
287 wallet_folder = self.wallet.storage.path
288 re.sub("(\/\w*.dat)$", "", wallet_folder)
289 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
293 def open_wallet(self):
295 filename = self.select_wallet_file()
299 storage = WalletStorage({'wallet_path': filename})
300 if not storage.file_exists:
301 self.show_message("file not found "+ filename)
304 self.wallet.stop_threads()
307 wallet = Wallet(storage)
308 wallet.start_threads(self.network)
310 self.load_wallet(wallet)
314 def backup_wallet(self):
316 path = self.wallet.storage.path
317 wallet_folder = os.path.dirname(path)
318 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
319 new_filename = unicode(new_filename)
320 if not ok or not new_filename:
323 new_path = os.path.join(wallet_folder, new_filename)
326 shutil.copy2(path, new_path)
327 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
328 except (IOError, os.error), reason:
329 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
332 def new_wallet(self):
335 wallet_folder = os.path.dirname(self.wallet.storage.path)
336 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
337 filename = unicode(filename)
338 if not ok or not filename:
340 filename = os.path.join(wallet_folder, filename)
342 storage = WalletStorage({'wallet_path': filename})
343 assert not storage.file_exists
345 wizard = installwizard.InstallWizard(self.config, self.network, storage)
346 wallet = wizard.run()
348 self.load_wallet(wallet)
352 def init_menubar(self):
355 file_menu = menubar.addMenu(_("&File"))
356 open_wallet_action = file_menu.addAction(_("&Open"))
357 open_wallet_action.triggered.connect(self.open_wallet)
359 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
360 new_wallet_action.triggered.connect(self.new_wallet)
362 wallet_backup = file_menu.addAction(_("&Copy"))
363 wallet_backup.triggered.connect(self.backup_wallet)
365 quit_item = file_menu.addAction(_("&Close"))
366 quit_item.triggered.connect(self.close)
368 wallet_menu = menubar.addMenu(_("&Wallet"))
370 new_contact = wallet_menu.addAction(_("&New contact"))
371 new_contact.triggered.connect(self.new_contact_dialog)
373 self.new_account = wallet_menu.addAction(_("&New account"))
374 self.new_account.triggered.connect(self.new_account_dialog)
376 wallet_menu.addSeparator()
378 pw = wallet_menu.addAction(_("&Password"))
379 pw.triggered.connect(self.change_password_dialog)
381 show_seed = wallet_menu.addAction(_("&Seed"))
382 show_seed.triggered.connect(self.show_seed_dialog)
384 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
385 show_mpk.triggered.connect(self.show_master_public_key)
387 wallet_menu.addSeparator()
389 labels_menu = wallet_menu.addMenu(_("&Labels"))
390 import_labels = labels_menu.addAction(_("&Import"))
391 import_labels.triggered.connect(self.do_import_labels)
392 export_labels = labels_menu.addAction(_("&Export"))
393 export_labels.triggered.connect(self.do_export_labels)
395 keys_menu = wallet_menu.addMenu(_("&Private keys"))
396 import_keys = keys_menu.addAction(_("&Import"))
397 import_keys.triggered.connect(self.do_import_privkey)
398 export_keys = keys_menu.addAction(_("&Export"))
399 export_keys.triggered.connect(self.do_export_privkeys)
401 ex_history = wallet_menu.addAction(_("&Export History"))
402 ex_history.triggered.connect(self.do_export_history)
406 tools_menu = menubar.addMenu(_("&Tools"))
408 # Settings / Preferences are all reserved keywords in OSX using this as work around
409 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
410 preferences_menu = tools_menu.addAction(preferences_name)
411 preferences_menu.triggered.connect(self.settings_dialog)
413 network = tools_menu.addAction(_("&Network"))
414 network.triggered.connect(self.run_network_dialog)
416 plugins_labels = tools_menu.addAction(_("&Plugins"))
417 plugins_labels.triggered.connect(self.plugins_dialog)
419 tools_menu.addSeparator()
421 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
423 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
424 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
426 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
427 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
429 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
431 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
432 raw_transaction_file.triggered.connect(self.do_process_from_file)
434 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
435 raw_transaction_text.triggered.connect(self.do_process_from_text)
438 help_menu = menubar.addMenu(_("&Help"))
439 show_about = help_menu.addAction(_("&About"))
440 show_about.triggered.connect(self.show_about)
441 web_open = help_menu.addAction(_("&Official website"))
442 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
444 help_menu.addSeparator()
445 doc_open = help_menu.addAction(_("&Documentation"))
446 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
447 report_bug = help_menu.addAction(_("&Report Bug"))
448 report_bug.triggered.connect(self.show_report_bug)
450 self.setMenuBar(menubar)
452 def show_about(self):
453 QMessageBox.about(self, "Electrum",
454 _("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."))
456 def show_report_bug(self):
457 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
458 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
461 def notify_transactions(self):
462 if not self.network or not self.network.is_connected():
465 print_error("Notifying GUI")
466 if len(self.network.interface.pending_transactions_for_notifications) > 0:
467 # Combine the transactions if there are more then three
468 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
471 for tx in self.network.interface.pending_transactions_for_notifications:
472 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
476 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
477 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
479 self.network.interface.pending_transactions_for_notifications = []
481 for tx in self.network.interface.pending_transactions_for_notifications:
483 self.network.interface.pending_transactions_for_notifications.remove(tx)
484 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
486 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
488 def notify(self, message):
489 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
493 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
494 def getOpenFileName(self, title, filter = ""):
495 directory = self.config.get('io_dir', os.path.expanduser('~'))
496 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
497 if fileName and directory != os.path.dirname(fileName):
498 self.config.set_key('io_dir', os.path.dirname(fileName), True)
501 def getSaveFileName(self, title, filename, filter = ""):
502 directory = self.config.get('io_dir', os.path.expanduser('~'))
503 path = os.path.join( directory, filename )
504 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
505 if fileName and directory != os.path.dirname(fileName):
506 self.config.set_key('io_dir', os.path.dirname(fileName), True)
510 QMainWindow.close(self)
511 run_hook('close_main_window')
513 def connect_slots(self, sender):
514 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
515 self.previous_payto_e=''
517 def timer_actions(self):
518 if self.need_update.is_set():
520 self.need_update.clear()
521 run_hook('timer_actions')
523 def format_amount(self, x, is_diff=False, whitespaces=False):
524 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
526 def read_amount(self, x):
527 if x in['.', '']: return None
528 p = pow(10, self.decimal_point)
529 return int( p * Decimal(x) )
532 assert self.decimal_point in [5,8]
533 return "BTC" if self.decimal_point == 8 else "mBTC"
536 def update_status(self):
537 if self.network is None:
539 icon = QIcon(":icons/status_disconnected.png")
541 elif self.network.is_connected():
542 if not self.wallet.up_to_date:
543 text = _("Synchronizing...")
544 icon = QIcon(":icons/status_waiting.png")
545 elif self.network.server_lag > 1:
546 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
547 icon = QIcon(":icons/status_lagging.png")
549 c, u = self.wallet.get_account_balance(self.current_account)
550 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
551 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
554 run_hook('set_quote_text', c+u, r)
557 text += " (%s)"%quote
559 self.tray.setToolTip(text)
560 icon = QIcon(":icons/status_connected.png")
562 text = _("Not connected")
563 icon = QIcon(":icons/status_disconnected.png")
565 self.balance_label.setText(text)
566 self.status_button.setIcon( icon )
569 def update_wallet(self):
571 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
572 self.update_history_tab()
573 self.update_receive_tab()
574 self.update_contacts_tab()
575 self.update_completions()
578 def create_history_tab(self):
579 self.history_list = l = MyTreeWidget(self)
581 for i,width in enumerate(self.column_widths['history']):
582 l.setColumnWidth(i, width)
583 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
584 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
585 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
587 l.customContextMenuRequested.connect(self.create_history_menu)
591 def create_history_menu(self, position):
592 self.history_list.selectedIndexes()
593 item = self.history_list.currentItem()
595 tx_hash = str(item.data(0, Qt.UserRole).toString())
596 if not tx_hash: return
598 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
599 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
600 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
601 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
604 def show_transaction(self, tx):
605 import transaction_dialog
606 d = transaction_dialog.TxDialog(tx, self)
609 def tx_label_clicked(self, item, column):
610 if column==2 and item.isSelected():
612 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
613 self.history_list.editItem( item, column )
614 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
617 def tx_label_changed(self, item, column):
621 tx_hash = str(item.data(0, Qt.UserRole).toString())
622 tx = self.wallet.transactions.get(tx_hash)
623 text = unicode( item.text(2) )
624 self.wallet.set_label(tx_hash, text)
626 item.setForeground(2, QBrush(QColor('black')))
628 text = self.wallet.get_default_label(tx_hash)
629 item.setText(2, text)
630 item.setForeground(2, QBrush(QColor('gray')))
634 def edit_label(self, is_recv):
635 l = self.receive_list if is_recv else self.contacts_list
636 item = l.currentItem()
637 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
638 l.editItem( item, 1 )
639 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
643 def address_label_clicked(self, item, column, l, column_addr, column_label):
644 if column == column_label and item.isSelected():
645 is_editable = item.data(0, 32).toBool()
648 addr = unicode( item.text(column_addr) )
649 label = unicode( item.text(column_label) )
650 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
651 l.editItem( item, column )
652 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
655 def address_label_changed(self, item, column, l, column_addr, column_label):
656 if column == column_label:
657 addr = unicode( item.text(column_addr) )
658 text = unicode( item.text(column_label) )
659 is_editable = item.data(0, 32).toBool()
663 changed = self.wallet.set_label(addr, text)
665 self.update_history_tab()
666 self.update_completions()
668 self.current_item_changed(item)
670 run_hook('item_changed', item, column)
673 def current_item_changed(self, a):
674 run_hook('current_item_changed', a)
678 def update_history_tab(self):
680 self.history_list.clear()
681 for item in self.wallet.get_tx_history(self.current_account):
682 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
683 time_str = _("unknown")
686 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
688 time_str = _("error")
691 time_str = 'unverified'
692 icon = QIcon(":icons/unconfirmed.png")
695 icon = QIcon(":icons/unconfirmed.png")
697 icon = QIcon(":icons/clock%d.png"%conf)
699 icon = QIcon(":icons/confirmed.png")
701 if value is not None:
702 v_str = self.format_amount(value, True, whitespaces=True)
706 balance_str = self.format_amount(balance, whitespaces=True)
709 label, is_default_label = self.wallet.get_label(tx_hash)
711 label = _('Pruned transaction outputs')
712 is_default_label = False
714 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
715 item.setFont(2, QFont(MONOSPACE_FONT))
716 item.setFont(3, QFont(MONOSPACE_FONT))
717 item.setFont(4, QFont(MONOSPACE_FONT))
719 item.setForeground(3, QBrush(QColor("#BC1E1E")))
721 item.setData(0, Qt.UserRole, tx_hash)
722 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
724 item.setForeground(2, QBrush(QColor('grey')))
726 item.setIcon(0, icon)
727 self.history_list.insertTopLevelItem(0,item)
730 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
733 def create_send_tab(self):
738 grid.setColumnMinimumWidth(3,300)
739 grid.setColumnStretch(5,1)
742 self.payto_e = QLineEdit()
743 grid.addWidget(QLabel(_('Pay to')), 1, 0)
744 grid.addWidget(self.payto_e, 1, 1, 1, 3)
746 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)
748 completer = QCompleter()
749 completer.setCaseSensitivity(False)
750 self.payto_e.setCompleter(completer)
751 completer.setModel(self.completions)
753 self.message_e = QLineEdit()
754 grid.addWidget(QLabel(_('Description')), 2, 0)
755 grid.addWidget(self.message_e, 2, 1, 1, 3)
756 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)
758 self.amount_e = AmountEdit(self.base_unit)
759 grid.addWidget(QLabel(_('Amount')), 3, 0)
760 grid.addWidget(self.amount_e, 3, 1, 1, 2)
761 grid.addWidget(HelpButton(
762 _('Amount to be sent.') + '\n\n' \
763 + _('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.') \
764 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
766 self.fee_e = AmountEdit(self.base_unit)
767 grid.addWidget(QLabel(_('Fee')), 4, 0)
768 grid.addWidget(self.fee_e, 4, 1, 1, 2)
769 grid.addWidget(HelpButton(
770 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
771 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
772 + _('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)
775 self.send_button = EnterButton(_("Send"), self.do_send)
776 grid.addWidget(self.send_button, 6, 1)
778 b = EnterButton(_("Clear"),self.do_clear)
779 grid.addWidget(b, 6, 2)
781 self.payto_sig = QLabel('')
782 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
784 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
785 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
794 def entry_changed( is_fee ):
795 self.funds_error = False
797 if self.amount_e.is_shortcut:
798 self.amount_e.is_shortcut = False
799 c, u = self.wallet.get_account_balance(self.current_account)
800 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( c + u, 0, self.current_account)
801 fee = self.wallet.estimated_fee(inputs)
803 self.amount_e.setText( self.format_amount(amount) )
804 self.fee_e.setText( self.format_amount( fee ) )
807 amount = self.read_amount(str(self.amount_e.text()))
808 fee = self.read_amount(str(self.fee_e.text()))
810 if not is_fee: fee = None
813 inputs, total, fee = self.wallet.choose_tx_inputs_from_account( amount, fee, self.current_account )
815 self.fee_e.setText( self.format_amount( fee ) )
818 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
822 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
823 self.funds_error = True
824 text = _( "Not enough funds" )
825 c, u = self.wallet.get_frozen_balance()
826 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
828 self.statusBar().showMessage(text)
829 self.amount_e.setPalette(palette)
830 self.fee_e.setPalette(palette)
832 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
833 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
835 run_hook('create_send_tab', grid)
839 def update_completions(self):
841 for addr,label in self.wallet.labels.items():
842 if addr in self.wallet.addressbook:
843 l.append( label + ' <' + addr + '>')
845 run_hook('update_completions', l)
846 self.completions.setStringList(l)
850 return lambda s, *args: s.do_protect(func, args)
855 label = unicode( self.message_e.text() )
856 r = unicode( self.payto_e.text() )
859 # label or alias, with address in brackets
860 m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
861 to_address = m.group(2) if m else r
863 if not is_valid(to_address):
864 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
868 amount = self.read_amount(unicode( self.amount_e.text()))
870 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
873 fee = self.read_amount(unicode( self.fee_e.text()))
875 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
878 confirm_amount = self.config.get('confirm_amount', 100000000)
879 if amount >= confirm_amount:
880 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
883 self.send_tx(to_address, amount, fee, label)
887 def send_tx(self, to_address, amount, fee, label, password):
890 tx = self.wallet.mktx_from_account( [(to_address, amount)], password, fee, self.current_account)
891 except BaseException, e:
892 traceback.print_exc(file=sys.stdout)
893 self.show_message(str(e))
896 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
897 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
901 self.wallet.set_label(tx.hash(), label)
904 h = self.wallet.send_tx(tx)
905 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
906 status, msg = self.wallet.receive_tx( h )
908 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
910 self.update_contacts_tab()
912 QMessageBox.warning(self, _('Error'), msg, _('OK'))
914 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
916 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
917 with open(fileName,'w') as f:
918 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
919 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
921 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
923 # add recipient to addressbook
924 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
925 self.wallet.addressbook.append(to_address)
930 def set_url(self, url):
931 address, amount, label, message, signature, identity, url = util.parse_url(url)
933 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
936 self.mini.set_payment_fields(address, amount)
938 if label and self.wallet.labels.get(address) != label:
939 if self.question('Give label "%s" to address %s ?'%(label,address)):
940 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
941 self.wallet.addressbook.append(address)
942 self.wallet.set_label(address, label)
944 run_hook('set_url', url, self.show_message, self.question)
946 self.tabs.setCurrentIndex(1)
947 label = self.wallet.labels.get(address)
948 m_addr = label + ' <'+ address +'>' if label else address
949 self.payto_e.setText(m_addr)
951 self.message_e.setText(message)
953 self.amount_e.setText(amount)
956 self.set_frozen(self.payto_e,True)
957 self.set_frozen(self.amount_e,True)
958 self.set_frozen(self.message_e,True)
959 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
961 self.payto_sig.setVisible(False)
964 self.payto_sig.setVisible(False)
965 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
967 self.set_frozen(e,False)
970 def set_frozen(self,entry,frozen):
972 entry.setReadOnly(True)
973 entry.setFrame(False)
975 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
976 entry.setPalette(palette)
978 entry.setReadOnly(False)
981 palette.setColor(entry.backgroundRole(), QColor('white'))
982 entry.setPalette(palette)
985 def toggle_freeze(self,addr):
987 if addr in self.wallet.frozen_addresses:
988 self.wallet.unfreeze(addr)
990 self.wallet.freeze(addr)
991 self.update_receive_tab()
993 def toggle_priority(self,addr):
995 if addr in self.wallet.prioritized_addresses:
996 self.wallet.unprioritize(addr)
998 self.wallet.prioritize(addr)
999 self.update_receive_tab()
1002 def create_list_tab(self, headers):
1003 "generic tab creation method"
1004 l = MyTreeWidget(self)
1005 l.setColumnCount( len(headers) )
1006 l.setHeaderLabels( headers )
1009 vbox = QVBoxLayout()
1016 vbox.addWidget(buttons)
1018 hbox = QHBoxLayout()
1021 buttons.setLayout(hbox)
1026 def create_receive_tab(self):
1027 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1028 l.setContextMenuPolicy(Qt.CustomContextMenu)
1029 l.customContextMenuRequested.connect(self.create_receive_menu)
1030 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1031 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1032 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1033 self.receive_list = l
1034 self.receive_buttons_hbox = hbox
1041 def save_column_widths(self):
1042 self.column_widths["receive"] = []
1043 for i in range(self.receive_list.columnCount() -1):
1044 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1046 self.column_widths["history"] = []
1047 for i in range(self.history_list.columnCount() - 1):
1048 self.column_widths["history"].append(self.history_list.columnWidth(i))
1050 self.column_widths["contacts"] = []
1051 for i in range(self.contacts_list.columnCount() - 1):
1052 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1054 self.config.set_key("column_widths_2", self.column_widths, True)
1057 def create_contacts_tab(self):
1058 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1059 l.setContextMenuPolicy(Qt.CustomContextMenu)
1060 l.customContextMenuRequested.connect(self.create_contact_menu)
1061 for i,width in enumerate(self.column_widths['contacts']):
1062 l.setColumnWidth(i, width)
1064 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1065 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1066 self.contacts_list = l
1067 self.contacts_buttons_hbox = hbox
1072 def delete_imported_key(self, addr):
1073 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1074 self.wallet.delete_imported_key(addr)
1075 self.update_receive_tab()
1076 self.update_history_tab()
1078 def edit_account_label(self, k):
1079 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1081 label = unicode(text)
1082 self.wallet.set_label(k,label)
1083 self.update_receive_tab()
1085 def account_set_expanded(self, item, k, b):
1087 self.accounts_expanded[k] = b
1089 def create_account_menu(self, position, k, item):
1091 if item.isExpanded():
1092 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1094 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1095 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1096 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1097 if self.wallet.account_is_pending(k):
1098 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1099 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1101 def delete_pending_account(self, k):
1102 self.wallet.delete_pending_account(k)
1103 self.update_receive_tab()
1105 def create_receive_menu(self, position):
1106 # fixme: this function apparently has a side effect.
1107 # if it is not called the menu pops up several times
1108 #self.receive_list.selectedIndexes()
1110 item = self.receive_list.itemAt(position)
1113 addr = unicode(item.text(0))
1114 if not is_valid(addr):
1115 k = str(item.data(0,32).toString())
1117 self.create_account_menu(position, k, item)
1119 item.setExpanded(not item.isExpanded())
1123 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1124 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1125 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1126 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1127 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1128 if addr in self.wallet.imported_keys:
1129 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1131 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1132 menu.addAction(t, lambda: self.toggle_freeze(addr))
1133 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1134 menu.addAction(t, lambda: self.toggle_priority(addr))
1136 run_hook('receive_menu', menu)
1137 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1140 def payto(self, addr):
1142 label = self.wallet.labels.get(addr)
1143 m_addr = label + ' <' + addr + '>' if label else addr
1144 self.tabs.setCurrentIndex(1)
1145 self.payto_e.setText(m_addr)
1146 self.amount_e.setFocus()
1149 def delete_contact(self, x):
1150 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1151 self.wallet.delete_contact(x)
1152 self.wallet.set_label(x, None)
1153 self.update_history_tab()
1154 self.update_contacts_tab()
1155 self.update_completions()
1158 def create_contact_menu(self, position):
1159 item = self.contacts_list.itemAt(position)
1161 addr = unicode(item.text(0))
1162 label = unicode(item.text(1))
1163 is_editable = item.data(0,32).toBool()
1164 payto_addr = item.data(0,33).toString()
1166 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1167 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1168 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1170 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1171 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1173 run_hook('create_contact_menu', menu, item)
1174 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1177 def update_receive_item(self, item):
1178 item.setFont(0, QFont(MONOSPACE_FONT))
1179 address = str(item.data(0,0).toString())
1180 label = self.wallet.labels.get(address,'')
1181 item.setData(1,0,label)
1182 item.setData(0,32, True) # is editable
1184 run_hook('update_receive_item', address, item)
1186 if not self.wallet.is_mine(address): return
1188 c, u = self.wallet.get_addr_balance(address)
1189 balance = self.format_amount(c + u)
1190 item.setData(2,0,balance)
1192 if address in self.wallet.frozen_addresses:
1193 item.setBackgroundColor(0, QColor('lightblue'))
1194 elif address in self.wallet.prioritized_addresses:
1195 item.setBackgroundColor(0, QColor('lightgreen'))
1198 def update_receive_tab(self):
1199 l = self.receive_list
1202 l.setColumnHidden(2, False)
1203 l.setColumnHidden(3, False)
1204 for i,width in enumerate(self.column_widths['receive']):
1205 l.setColumnWidth(i, width)
1207 if self.current_account is None:
1208 account_items = self.wallet.accounts.items()
1209 elif self.current_account != -1:
1210 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1214 for k, account in account_items:
1215 name = self.wallet.get_account_name(k)
1216 c,u = self.wallet.get_account_balance(k)
1217 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1218 l.addTopLevelItem(account_item)
1219 account_item.setExpanded(self.accounts_expanded.get(k, True))
1220 account_item.setData(0, 32, k)
1222 if not self.wallet.is_seeded(k):
1223 icon = QIcon(":icons/key.png")
1224 account_item.setIcon(0, icon)
1226 for is_change in ([0,1]):
1227 name = _("Receiving") if not is_change else _("Change")
1228 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1229 account_item.addChild(seq_item)
1230 if not is_change: seq_item.setExpanded(True)
1235 for address in account.get_addresses(is_change):
1236 h = self.wallet.history.get(address,[])
1240 if gap > self.wallet.gap_limit:
1245 num_tx = '*' if h == ['*'] else "%d"%len(h)
1246 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1247 self.update_receive_item(item)
1249 item.setBackgroundColor(1, QColor('red'))
1250 seq_item.addChild(item)
1253 for k, addr in self.wallet.get_pending_accounts():
1254 name = self.wallet.labels.get(k,'')
1255 account_item = QTreeWidgetItem( [ name + " [ "+_('pending account')+" ]", '', '', ''] )
1256 self.update_receive_item(item)
1257 l.addTopLevelItem(account_item)
1258 account_item.setExpanded(True)
1259 account_item.setData(0, 32, k)
1260 item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1261 account_item.addChild(item)
1262 self.update_receive_item(item)
1265 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1266 c,u = self.wallet.get_imported_balance()
1267 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1268 l.addTopLevelItem(account_item)
1269 account_item.setExpanded(True)
1270 for address in self.wallet.imported_keys.keys():
1271 item = QTreeWidgetItem( [ address, '', '', ''] )
1272 self.update_receive_item(item)
1273 account_item.addChild(item)
1276 # we use column 1 because column 0 may be hidden
1277 l.setCurrentItem(l.topLevelItem(0),1)
1280 def update_contacts_tab(self):
1281 l = self.contacts_list
1284 for address in self.wallet.addressbook:
1285 label = self.wallet.labels.get(address,'')
1286 n = self.wallet.get_num_tx(address)
1287 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1288 item.setFont(0, QFont(MONOSPACE_FONT))
1289 # 32 = label can be edited (bool)
1290 item.setData(0,32, True)
1292 item.setData(0,33, address)
1293 l.addTopLevelItem(item)
1295 run_hook('update_contacts_tab', l)
1296 l.setCurrentItem(l.topLevelItem(0))
1300 def create_console_tab(self):
1301 from console import Console
1302 self.console = console = Console()
1306 def update_console(self):
1307 console = self.console
1308 console.history = self.config.get("console-history",[])
1309 console.history_index = len(console.history)
1311 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1312 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1314 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1316 def mkfunc(f, method):
1317 return lambda *args: apply( f, (method, args, self.password_dialog ))
1319 if m[0]=='_' or m in ['network','wallet']: continue
1320 methods[m] = mkfunc(c._run, m)
1322 console.updateNamespace(methods)
1325 def change_account(self,s):
1326 if s == _("All accounts"):
1327 self.current_account = None
1329 accounts = self.wallet.get_account_names()
1330 for k, v in accounts.items():
1332 self.current_account = k
1333 self.update_history_tab()
1334 self.update_status()
1335 self.update_receive_tab()
1337 def create_status_bar(self):
1340 sb.setFixedHeight(35)
1341 qtVersion = qVersion()
1343 self.balance_label = QLabel("")
1344 sb.addWidget(self.balance_label)
1346 from version_getter import UpdateLabel
1347 self.updatelabel = UpdateLabel(self.config, sb)
1349 self.account_selector = QComboBox()
1350 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1351 sb.addPermanentWidget(self.account_selector)
1353 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1354 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1356 self.lock_icon = QIcon()
1357 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1358 sb.addPermanentWidget( self.password_button )
1360 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1361 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1362 sb.addPermanentWidget( self.seed_button )
1363 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1364 sb.addPermanentWidget( self.status_button )
1366 run_hook('create_status_bar', (sb,))
1368 self.setStatusBar(sb)
1371 def update_lock_icon(self):
1372 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1373 self.password_button.setIcon( icon )
1376 def update_buttons_on_seed(self):
1377 if not self.wallet.is_watching_only():
1378 self.seed_button.show()
1379 self.password_button.show()
1380 self.send_button.setText(_("Send"))
1382 self.password_button.hide()
1383 self.seed_button.hide()
1384 self.send_button.setText(_("Create unsigned transaction"))
1387 def change_password_dialog(self):
1388 from password_dialog import PasswordDialog
1389 d = PasswordDialog(self.wallet, self)
1391 self.update_lock_icon()
1394 def new_contact_dialog(self):
1397 vbox = QVBoxLayout(d)
1398 vbox.addWidget(QLabel(_('New Contact')+':'))
1400 grid = QGridLayout()
1403 grid.addWidget(QLabel(_("Address")), 1, 0)
1404 grid.addWidget(line1, 1, 1)
1405 grid.addWidget(QLabel(_("Name")), 2, 0)
1406 grid.addWidget(line2, 2, 1)
1408 vbox.addLayout(grid)
1409 vbox.addLayout(ok_cancel_buttons(d))
1414 address = str(line1.text())
1415 label = unicode(line2.text())
1417 if not is_valid(address):
1418 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1421 self.wallet.add_contact(address)
1423 self.wallet.set_label(address, label)
1425 self.update_contacts_tab()
1426 self.update_history_tab()
1427 self.update_completions()
1428 self.tabs.setCurrentIndex(3)
1431 def new_account_dialog(self):
1433 dialog = QDialog(self)
1435 dialog.setWindowTitle(_("New Account"))
1437 vbox = QVBoxLayout()
1438 vbox.addWidget(QLabel(_('Account name')+':'))
1441 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1442 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1447 vbox.addLayout(ok_cancel_buttons(dialog))
1448 dialog.setLayout(vbox)
1452 name = str(e.text())
1455 self.wallet.create_pending_account('1', name)
1456 self.update_receive_tab()
1457 self.tabs.setCurrentIndex(2)
1461 def show_master_public_key_old(self):
1462 dialog = QDialog(self)
1464 dialog.setWindowTitle(_("Master Public Key"))
1466 main_text = QTextEdit()
1467 main_text.setText(self.wallet.get_master_public_key())
1468 main_text.setReadOnly(True)
1469 main_text.setMaximumHeight(170)
1470 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1472 ok_button = QPushButton(_("OK"))
1473 ok_button.setDefault(True)
1474 ok_button.clicked.connect(dialog.accept)
1476 main_layout = QGridLayout()
1477 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1479 main_layout.addWidget(main_text, 1, 0)
1480 main_layout.addWidget(qrw, 1, 1 )
1482 vbox = QVBoxLayout()
1483 vbox.addLayout(main_layout)
1484 vbox.addLayout(close_button(dialog))
1485 dialog.setLayout(vbox)
1489 def show_master_public_key(self):
1491 if self.wallet.seed_version == 4:
1492 self.show_master_public_key_old()
1495 dialog = QDialog(self)
1497 dialog.setWindowTitle(_("Master Public Keys"))
1499 chain_text = QTextEdit()
1500 chain_text.setReadOnly(True)
1501 chain_text.setMaximumHeight(170)
1502 chain_qrw = QRCodeWidget()
1504 mpk_text = QTextEdit()
1505 mpk_text.setReadOnly(True)
1506 mpk_text.setMaximumHeight(170)
1507 mpk_qrw = QRCodeWidget()
1509 main_layout = QGridLayout()
1511 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1512 main_layout.addWidget(mpk_text, 1, 1)
1513 main_layout.addWidget(mpk_qrw, 1, 2)
1515 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1516 main_layout.addWidget(chain_text, 2, 1)
1517 main_layout.addWidget(chain_qrw, 2, 2)
1520 c, K, cK = self.wallet.master_public_keys[str(key)]
1521 chain_text.setText(c)
1522 chain_qrw.set_addr(c)
1523 chain_qrw.update_qr()
1528 key_selector = QComboBox()
1529 keys = sorted(self.wallet.master_public_keys.keys())
1530 key_selector.addItems(keys)
1532 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1533 main_layout.addWidget(key_selector, 0, 1)
1534 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1538 vbox = QVBoxLayout()
1539 vbox.addLayout(main_layout)
1540 vbox.addLayout(close_button(dialog))
1542 dialog.setLayout(vbox)
1547 def show_seed_dialog(self, password):
1548 if self.wallet.is_watching_only():
1549 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1552 if self.wallet.seed:
1554 mnemonic = self.wallet.get_mnemonic(password)
1556 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1558 from seed_dialog import SeedDialog
1559 d = SeedDialog(self, mnemonic, self.wallet.imported_keys)
1563 for k in self.wallet.master_private_keys.keys():
1564 pk = self.wallet.get_master_private_key(k, password)
1566 from seed_dialog import PrivateKeysDialog
1567 d = PrivateKeysDialog(self,l)
1574 def show_qrcode(self, data, title = _("QR code")):
1578 d.setWindowTitle(title)
1579 d.setMinimumSize(270, 300)
1580 vbox = QVBoxLayout()
1581 qrw = QRCodeWidget(data)
1582 vbox.addWidget(qrw, 1)
1583 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1584 hbox = QHBoxLayout()
1587 filename = os.path.join(self.config.path, "qrcode.bmp")
1590 bmp.save_qrcode(qrw.qr, filename)
1591 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1593 def copy_to_clipboard():
1594 bmp.save_qrcode(qrw.qr, filename)
1595 self.app.clipboard().setImage(QImage(filename))
1596 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1598 b = QPushButton(_("Copy"))
1600 b.clicked.connect(copy_to_clipboard)
1602 b = QPushButton(_("Save"))
1604 b.clicked.connect(print_qr)
1606 b = QPushButton(_("Close"))
1608 b.clicked.connect(d.accept)
1611 vbox.addLayout(hbox)
1616 def do_protect(self, func, args):
1617 if self.wallet.use_encryption:
1618 password = self.password_dialog()
1624 if args != (False,):
1625 args = (self,) + args + (password,)
1627 args = (self,password)
1632 def show_private_key(self, address, password):
1633 if not address: return
1635 pk_list = self.wallet.get_private_key(address, password)
1636 except BaseException, e:
1637 self.show_message(str(e))
1639 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1643 def do_sign(self, address, message, signature, password):
1644 message = unicode(message.toPlainText())
1645 message = message.encode('utf-8')
1647 sig = self.wallet.sign_message(str(address.text()), message, password)
1648 signature.setText(sig)
1649 except BaseException, e:
1650 self.show_message(str(e))
1652 def sign_message(self, address):
1653 if not address: return
1656 d.setWindowTitle(_('Sign Message'))
1657 d.setMinimumSize(410, 290)
1659 tab_widget = QTabWidget()
1661 layout = QGridLayout(tab)
1663 sign_address = QLineEdit()
1665 sign_address.setText(address)
1666 layout.addWidget(QLabel(_('Address')), 1, 0)
1667 layout.addWidget(sign_address, 1, 1)
1669 sign_message = QTextEdit()
1670 layout.addWidget(QLabel(_('Message')), 2, 0)
1671 layout.addWidget(sign_message, 2, 1)
1672 layout.setRowStretch(2,3)
1674 sign_signature = QTextEdit()
1675 layout.addWidget(QLabel(_('Signature')), 3, 0)
1676 layout.addWidget(sign_signature, 3, 1)
1677 layout.setRowStretch(3,1)
1680 hbox = QHBoxLayout()
1681 b = QPushButton(_("Sign"))
1683 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1684 b = QPushButton(_("Close"))
1685 b.clicked.connect(d.accept)
1687 layout.addLayout(hbox, 4, 1)
1688 tab_widget.addTab(tab, _("Sign"))
1692 layout = QGridLayout(tab)
1694 verify_address = QLineEdit()
1695 layout.addWidget(QLabel(_('Address')), 1, 0)
1696 layout.addWidget(verify_address, 1, 1)
1698 verify_message = QTextEdit()
1699 layout.addWidget(QLabel(_('Message')), 2, 0)
1700 layout.addWidget(verify_message, 2, 1)
1701 layout.setRowStretch(2,3)
1703 verify_signature = QTextEdit()
1704 layout.addWidget(QLabel(_('Signature')), 3, 0)
1705 layout.addWidget(verify_signature, 3, 1)
1706 layout.setRowStretch(3,1)
1709 message = unicode(verify_message.toPlainText())
1710 message = message.encode('utf-8')
1711 if bitcoin.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1712 self.show_message(_("Signature verified"))
1714 self.show_message(_("Error: wrong signature"))
1716 hbox = QHBoxLayout()
1717 b = QPushButton(_("Verify"))
1718 b.clicked.connect(do_verify)
1720 b = QPushButton(_("Close"))
1721 b.clicked.connect(d.accept)
1723 layout.addLayout(hbox, 4, 1)
1724 tab_widget.addTab(tab, _("Verify"))
1726 vbox = QVBoxLayout()
1727 vbox.addWidget(tab_widget)
1734 def question(self, msg):
1735 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1737 def show_message(self, msg):
1738 QMessageBox.information(self, _('Message'), msg, _('OK'))
1740 def password_dialog(self ):
1747 vbox = QVBoxLayout()
1748 msg = _('Please enter your password')
1749 vbox.addWidget(QLabel(msg))
1751 grid = QGridLayout()
1753 grid.addWidget(QLabel(_('Password')), 1, 0)
1754 grid.addWidget(pw, 1, 1)
1755 vbox.addLayout(grid)
1757 vbox.addLayout(ok_cancel_buttons(d))
1760 run_hook('password_dialog', pw, grid, 1)
1761 if not d.exec_(): return
1762 return unicode(pw.text())
1771 def tx_from_text(self, txt):
1772 "json or raw hexadecimal"
1775 tx = Transaction(txt)
1781 tx_dict = json.loads(str(txt))
1782 assert "hex" in tx_dict.keys()
1783 assert "complete" in tx_dict.keys()
1784 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1785 if not tx_dict["complete"]:
1786 assert "input_info" in tx_dict.keys()
1787 input_info = json.loads(tx_dict['input_info'])
1788 tx.add_input_info(input_info)
1793 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1797 def read_tx_from_file(self):
1798 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1802 with open(fileName, "r") as f:
1803 file_content = f.read()
1804 except (ValueError, IOError, os.error), reason:
1805 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1807 return self.tx_from_text(file_content)
1811 def sign_raw_transaction(self, tx, input_info, password):
1812 self.wallet.signrawtransaction(tx, input_info, [], password)
1814 def do_process_from_text(self):
1815 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1818 tx = self.tx_from_text(text)
1820 self.show_transaction(tx)
1822 def do_process_from_file(self):
1823 tx = self.read_tx_from_file()
1825 self.show_transaction(tx)
1827 def do_process_from_csvReader(self, csvReader):
1830 for row in csvReader:
1832 amount = float(row[1])
1833 amount = int(100000000*amount)
1834 outputs.append((address, amount))
1835 except (ValueError, IOError, os.error), reason:
1836 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1840 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1841 except BaseException, e:
1842 self.show_message(str(e))
1845 self.show_transaction(tx)
1847 def do_process_from_csv_file(self):
1848 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1852 with open(fileName, "r") as f:
1853 csvReader = csv.reader(f)
1854 self.do_process_from_csvReader(csvReader)
1855 except (ValueError, IOError, os.error), reason:
1856 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1859 def do_process_from_csv_text(self):
1860 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1861 + _("Format: address, amount. One output per line"), _("Load CSV"))
1864 f = StringIO.StringIO(text)
1865 csvReader = csv.reader(f)
1866 self.do_process_from_csvReader(csvReader)
1871 def do_export_privkeys(self, password):
1872 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.")))
1875 select_export = _('Select file to export your private keys to')
1876 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1878 with open(fileName, "w+") as csvfile:
1879 transaction = csv.writer(csvfile)
1880 transaction.writerow(["address", "private_key"])
1882 addresses = self.wallet.addresses(True)
1884 for addr in addresses:
1885 pk = "".join(self.wallet.get_private_key(addr, password))
1886 transaction.writerow(["%34s"%addr,pk])
1888 self.show_message(_("Private keys exported."))
1890 except (IOError, os.error), reason:
1891 export_error_label = _("Electrum was unable to produce a private key-export.")
1892 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1894 except BaseException, e:
1895 self.show_message(str(e))
1899 def do_import_labels(self):
1900 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1901 if not labelsFile: return
1903 f = open(labelsFile, 'r')
1906 for key, value in json.loads(data).items():
1907 self.wallet.set_label(key, value)
1908 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1909 except (IOError, os.error), reason:
1910 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1913 def do_export_labels(self):
1914 labels = self.wallet.labels
1916 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1918 with open(fileName, 'w+') as f:
1919 json.dump(labels, f)
1920 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1921 except (IOError, os.error), reason:
1922 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1925 def do_export_history(self):
1926 from lite_window import csv_transaction
1927 csv_transaction(self.wallet)
1931 def do_import_privkey(self, password):
1932 if not self.wallet.imported_keys:
1933 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1934 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1935 + _('Are you sure you understand what you are doing?'), 3, 4)
1938 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1941 text = str(text).split()
1946 addr = self.wallet.import_key(key, password)
1947 except BaseException as e:
1953 addrlist.append(addr)
1955 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1957 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1958 self.update_receive_tab()
1959 self.update_history_tab()
1962 def settings_dialog(self):
1964 d.setWindowTitle(_('Electrum Settings'))
1966 vbox = QVBoxLayout()
1967 grid = QGridLayout()
1968 grid.setColumnStretch(0,1)
1970 nz_label = QLabel(_('Display zeros') + ':')
1971 grid.addWidget(nz_label, 0, 0)
1972 nz_e = AmountEdit(None,True)
1973 nz_e.setText("%d"% self.num_zeros)
1974 grid.addWidget(nz_e, 0, 1)
1975 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1976 grid.addWidget(HelpButton(msg), 0, 2)
1977 if not self.config.is_modifiable('num_zeros'):
1978 for w in [nz_e, nz_label]: w.setEnabled(False)
1980 lang_label=QLabel(_('Language') + ':')
1981 grid.addWidget(lang_label, 1, 0)
1982 lang_combo = QComboBox()
1983 from electrum.i18n import languages
1984 lang_combo.addItems(languages.values())
1986 index = languages.keys().index(self.config.get("language",''))
1989 lang_combo.setCurrentIndex(index)
1990 grid.addWidget(lang_combo, 1, 1)
1991 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1992 if not self.config.is_modifiable('language'):
1993 for w in [lang_combo, lang_label]: w.setEnabled(False)
1996 fee_label = QLabel(_('Transaction fee') + ':')
1997 grid.addWidget(fee_label, 2, 0)
1998 fee_e = AmountEdit(self.base_unit)
1999 fee_e.setText(self.format_amount(self.wallet.fee).strip())
2000 grid.addWidget(fee_e, 2, 1)
2001 msg = _('Fee per kilobyte of transaction.') + ' ' \
2002 + _('Recommended value') + ': ' + self.format_amount(50000)
2003 grid.addWidget(HelpButton(msg), 2, 2)
2004 if not self.config.is_modifiable('fee_per_kb'):
2005 for w in [fee_e, fee_label]: w.setEnabled(False)
2007 units = ['BTC', 'mBTC']
2008 unit_label = QLabel(_('Base unit') + ':')
2009 grid.addWidget(unit_label, 3, 0)
2010 unit_combo = QComboBox()
2011 unit_combo.addItems(units)
2012 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2013 grid.addWidget(unit_combo, 3, 1)
2014 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2015 + '\n1BTC=1000mBTC.\n' \
2016 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
2018 usechange_cb = QCheckBox(_('Use change addresses'))
2019 usechange_cb.setChecked(self.wallet.use_change)
2020 grid.addWidget(usechange_cb, 4, 0)
2021 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2022 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2024 grid.setRowStretch(5,1)
2026 vbox.addLayout(grid)
2027 vbox.addLayout(ok_cancel_buttons(d))
2031 if not d.exec_(): return
2033 fee = unicode(fee_e.text())
2035 fee = self.read_amount(fee)
2037 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2040 self.wallet.set_fee(fee)
2042 nz = unicode(nz_e.text())
2047 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2050 if self.num_zeros != nz:
2052 self.config.set_key('num_zeros', nz, True)
2053 self.update_history_tab()
2054 self.update_receive_tab()
2056 usechange_result = usechange_cb.isChecked()
2057 if self.wallet.use_change != usechange_result:
2058 self.wallet.use_change = usechange_result
2059 self.config.set_key('use_change', self.wallet.use_change, True)
2061 unit_result = units[unit_combo.currentIndex()]
2062 if self.base_unit() != unit_result:
2063 self.decimal_point = 8 if unit_result == 'BTC' else 5
2064 self.config.set_key('decimal_point', self.decimal_point, True)
2065 self.update_history_tab()
2066 self.update_status()
2068 need_restart = False
2070 lang_request = languages.keys()[lang_combo.currentIndex()]
2071 if lang_request != self.config.get('language'):
2072 self.config.set_key("language", lang_request, True)
2075 run_hook('close_settings_dialog')
2078 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2081 def run_network_dialog(self):
2082 if not self.network:
2084 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2086 def closeEvent(self, event):
2088 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2089 self.save_column_widths()
2090 self.config.set_key("console-history", self.console.history[-50:], True)
2091 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2096 def plugins_dialog(self):
2097 from electrum.plugins import plugins
2100 d.setWindowTitle(_('Electrum Plugins'))
2103 vbox = QVBoxLayout(d)
2106 scroll = QScrollArea()
2107 scroll.setEnabled(True)
2108 scroll.setWidgetResizable(True)
2109 scroll.setMinimumSize(400,250)
2110 vbox.addWidget(scroll)
2114 w.setMinimumHeight(len(plugins)*35)
2116 grid = QGridLayout()
2117 grid.setColumnStretch(0,1)
2120 def do_toggle(cb, p, w):
2123 if w: w.setEnabled(r)
2125 def mk_toggle(cb, p, w):
2126 return lambda: do_toggle(cb,p,w)
2128 for i, p in enumerate(plugins):
2130 cb = QCheckBox(p.fullname())
2131 cb.setDisabled(not p.is_available())
2132 cb.setChecked(p.is_enabled())
2133 grid.addWidget(cb, i, 0)
2134 if p.requires_settings():
2135 w = p.settings_widget(self)
2136 w.setEnabled( p.is_enabled() )
2137 grid.addWidget(w, i, 1)
2140 cb.clicked.connect(mk_toggle(cb,p,w))
2141 grid.addWidget(HelpButton(p.description()), i, 2)
2143 print_msg(_("Error: cannot display plugin"), p)
2144 traceback.print_exc(file=sys.stdout)
2145 grid.setRowStretch(i+1,1)
2147 vbox.addLayout(close_button(d))
2152 def show_account_details(self, k):
2154 d.setWindowTitle(_('Account Details'))
2157 vbox = QVBoxLayout(d)
2158 roots = self.wallet.get_roots(k)
2160 name = self.wallet.get_account_name(k)
2161 label = QLabel('Name: ' + name)
2162 vbox.addWidget(label)
2164 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2165 vbox.addWidget(QLabel('Type: ' + acctype))
2167 label = QLabel('Derivation: ' + k)
2168 vbox.addWidget(label)
2171 # mpk = self.wallet.master_public_keys[root]
2172 # text = QTextEdit()
2173 # text.setReadOnly(True)
2174 # text.setMaximumHeight(120)
2175 # text.setText(repr(mpk))
2176 # vbox.addWidget(text)
2178 vbox.addLayout(close_button(d))