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)
190 self.network.register_callback('updated', lambda: self.need_update.set())
191 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
192 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
193 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
194 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
195 # set initial message
196 self.console.showMessage(self.network.banner)
198 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
199 if platform.system() == 'Windows':
200 n = 3 if self.wallet.seed else 2
201 tabs.setCurrentIndex (n)
202 tabs.setCurrentIndex (0)
209 self.config.set_key('lite_mode', False, True)
214 self.config.set_key('lite_mode', True, True)
221 if not self.check_qt_version():
222 if self.config.get('lite_mode') is True:
223 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
224 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
225 self.config.set_key('lite_mode', False, True)
230 actuator = lite_window.MiniActuator(self)
232 # Should probably not modify the current path but instead
233 # change the behaviour of rsrc(...)
234 old_path = QDir.currentPath()
235 actuator.load_theme()
237 self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
239 driver = lite_window.MiniDriver(self, self.mini)
241 # Reset path back to original value now that loading the GUI
243 QDir.setCurrent(old_path)
245 if self.config.get('lite_mode') is True:
251 def check_qt_version(self):
252 qtVersion = qVersion()
253 return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
256 def update_account_selector(self):
258 accounts = self.wallet.get_account_names()
259 self.account_selector.clear()
260 if len(accounts) > 1:
261 self.account_selector.addItems([_("All accounts")] + accounts.values())
262 self.account_selector.setCurrentIndex(0)
263 self.account_selector.show()
265 self.account_selector.hide()
268 def load_wallet(self, wallet):
271 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
272 self.current_account = self.wallet.storage.get("current_account", None)
274 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
275 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
276 self.setWindowTitle( title )
278 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
279 self.notify_transactions()
280 self.update_account_selector()
281 self.new_account.setEnabled(self.wallet.seed_version>4)
282 self.update_lock_icon()
283 self.update_buttons_on_seed()
284 self.update_console()
286 run_hook('load_wallet', wallet)
289 def select_wallet_file(self):
290 wallet_folder = self.wallet.storage.path
291 re.sub("(\/\w*.dat)$", "", wallet_folder)
292 file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
296 def open_wallet(self):
298 filename = self.select_wallet_file()
302 storage = WalletStorage({'wallet_path': filename})
303 if not storage.file_exists:
304 self.show_message("file not found "+ filename)
307 self.wallet.stop_threads()
310 wallet = Wallet(storage)
311 wallet.start_threads(self.network)
313 self.load_wallet(wallet)
317 def backup_wallet(self):
319 path = self.wallet.storage.path
320 wallet_folder = os.path.dirname(path)
321 new_filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n' + _('Enter a filename for the copy of your wallet') + ':')
322 new_filename = unicode(new_filename)
323 if not ok or not new_filename:
326 new_path = os.path.join(wallet_folder, new_filename)
329 shutil.copy2(path, new_path)
330 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
331 except (IOError, os.error), reason:
332 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
335 def new_wallet(self):
338 wallet_folder = os.path.dirname(self.wallet.storage.path)
339 filename, ok = QInputDialog.getText(self, _('Filename'), _('Current directory') + ': ' + wallet_folder + '\n'+_('Enter a new file name') + ':')
340 filename = unicode(filename)
341 if not ok or not filename:
343 filename = os.path.join(wallet_folder, filename)
345 storage = WalletStorage({'wallet_path': filename})
346 assert not storage.file_exists
348 wizard = installwizard.InstallWizard(self.config, self.network, storage)
349 wallet = wizard.run()
351 self.load_wallet(wallet)
355 def init_menubar(self):
358 file_menu = menubar.addMenu(_("&File"))
359 open_wallet_action = file_menu.addAction(_("&Open"))
360 open_wallet_action.triggered.connect(self.open_wallet)
362 new_wallet_action = file_menu.addAction(_("&Create/Restore"))
363 new_wallet_action.triggered.connect(self.new_wallet)
365 wallet_backup = file_menu.addAction(_("&Copy"))
366 wallet_backup.triggered.connect(self.backup_wallet)
368 quit_item = file_menu.addAction(_("&Close"))
369 quit_item.triggered.connect(self.close)
371 wallet_menu = menubar.addMenu(_("&Wallet"))
373 new_contact = wallet_menu.addAction(_("&New contact"))
374 new_contact.triggered.connect(self.new_contact_dialog)
376 self.new_account = wallet_menu.addAction(_("&New account"))
377 self.new_account.triggered.connect(self.new_account_dialog)
379 wallet_menu.addSeparator()
381 pw = wallet_menu.addAction(_("&Password"))
382 pw.triggered.connect(self.change_password_dialog)
384 show_seed = wallet_menu.addAction(_("&Seed"))
385 show_seed.triggered.connect(self.show_seed_dialog)
387 show_mpk = wallet_menu.addAction(_("&Master Public Key"))
388 show_mpk.triggered.connect(self.show_master_public_key)
390 wallet_menu.addSeparator()
392 labels_menu = wallet_menu.addMenu(_("&Labels"))
393 import_labels = labels_menu.addAction(_("&Import"))
394 import_labels.triggered.connect(self.do_import_labels)
395 export_labels = labels_menu.addAction(_("&Export"))
396 export_labels.triggered.connect(self.do_export_labels)
398 keys_menu = wallet_menu.addMenu(_("&Private keys"))
399 import_keys = keys_menu.addAction(_("&Import"))
400 import_keys.triggered.connect(self.do_import_privkey)
401 export_keys = keys_menu.addAction(_("&Export"))
402 export_keys.triggered.connect(self.do_export_privkeys)
404 ex_history = wallet_menu.addAction(_("&Export History"))
405 ex_history.triggered.connect(self.do_export_history)
409 tools_menu = menubar.addMenu(_("&Tools"))
411 # Settings / Preferences are all reserved keywords in OSX using this as work around
412 preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
413 preferences_menu = tools_menu.addAction(preferences_name)
414 preferences_menu.triggered.connect(self.settings_dialog)
416 network = tools_menu.addAction(_("&Network"))
417 network.triggered.connect(self.run_network_dialog)
419 plugins_labels = tools_menu.addAction(_("&Plugins"))
420 plugins_labels.triggered.connect(self.plugins_dialog)
422 tools_menu.addSeparator()
424 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
426 csv_transaction_file = csv_transaction_menu.addAction(_("&From CSV file"))
427 csv_transaction_file.triggered.connect(self.do_process_from_csv_file)
429 csv_transaction_text = csv_transaction_menu.addAction(_("&From CSV text"))
430 csv_transaction_text.triggered.connect(self.do_process_from_csv_text)
432 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
434 raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
435 raw_transaction_file.triggered.connect(self.do_process_from_file)
437 raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
438 raw_transaction_text.triggered.connect(self.do_process_from_text)
441 help_menu = menubar.addMenu(_("&Help"))
442 show_about = help_menu.addAction(_("&About"))
443 show_about.triggered.connect(self.show_about)
444 web_open = help_menu.addAction(_("&Official website"))
445 web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
447 help_menu.addSeparator()
448 doc_open = help_menu.addAction(_("&Documentation"))
449 doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
450 report_bug = help_menu.addAction(_("&Report Bug"))
451 report_bug.triggered.connect(self.show_report_bug)
453 self.setMenuBar(menubar)
455 def show_about(self):
456 QMessageBox.about(self, "Electrum",
457 _("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."))
459 def show_report_bug(self):
460 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
461 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
464 def notify_transactions(self):
465 if not self.network.is_connected():
468 print_error("Notifying GUI")
469 if len(self.network.interface.pending_transactions_for_notifications) > 0:
470 # Combine the transactions if there are more then three
471 tx_amount = len(self.network.interface.pending_transactions_for_notifications)
474 for tx in self.network.interface.pending_transactions_for_notifications:
475 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
479 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
480 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
482 self.network.interface.pending_transactions_for_notifications = []
484 for tx in self.network.interface.pending_transactions_for_notifications:
486 self.network.interface.pending_transactions_for_notifications.remove(tx)
487 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
489 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
491 def notify(self, message):
492 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
496 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
497 def getOpenFileName(self, title, filter = ""):
498 directory = self.config.get('io_dir', os.path.expanduser('~'))
499 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
500 if fileName and directory != os.path.dirname(fileName):
501 self.config.set_key('io_dir', os.path.dirname(fileName), True)
504 def getSaveFileName(self, title, filename, filter = ""):
505 directory = self.config.get('io_dir', os.path.expanduser('~'))
506 path = os.path.join( directory, filename )
507 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
508 if fileName and directory != os.path.dirname(fileName):
509 self.config.set_key('io_dir', os.path.dirname(fileName), True)
513 QMainWindow.close(self)
514 run_hook('close_main_window')
516 def connect_slots(self, sender):
517 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
518 self.previous_payto_e=''
520 def timer_actions(self):
521 if self.need_update.is_set():
523 self.need_update.clear()
524 run_hook('timer_actions')
526 def format_amount(self, x, is_diff=False, whitespaces=False):
527 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
529 def read_amount(self, x):
530 if x in['.', '']: return None
531 p = pow(10, self.decimal_point)
532 return int( p * Decimal(x) )
535 assert self.decimal_point in [5,8]
536 return "BTC" if self.decimal_point == 8 else "mBTC"
539 def update_status(self):
540 if 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.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 BaseException, 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'))
913 filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
915 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
916 with open(fileName,'w') as f:
917 f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
918 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
920 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
922 # add recipient to addressbook
923 if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
924 self.wallet.addressbook.append(to_address)
929 def set_url(self, url):
930 address, amount, label, message, signature, identity, url = util.parse_url(url)
932 if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
935 self.mini.set_payment_fields(address, amount)
937 if label and self.wallet.labels.get(address) != label:
938 if self.question('Give label "%s" to address %s ?'%(label,address)):
939 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
940 self.wallet.addressbook.append(address)
941 self.wallet.set_label(address, label)
943 run_hook('set_url', url, self.show_message, self.question)
945 self.tabs.setCurrentIndex(1)
946 label = self.wallet.labels.get(address)
947 m_addr = label + ' <'+ address +'>' if label else address
948 self.payto_e.setText(m_addr)
950 self.message_e.setText(message)
952 self.amount_e.setText(amount)
955 self.set_frozen(self.payto_e,True)
956 self.set_frozen(self.amount_e,True)
957 self.set_frozen(self.message_e,True)
958 self.payto_sig.setText( ' '+_('The bitcoin URI was signed by')+' ' + identity )
960 self.payto_sig.setVisible(False)
963 self.payto_sig.setVisible(False)
964 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
966 self.set_frozen(e,False)
969 def set_frozen(self,entry,frozen):
971 entry.setReadOnly(True)
972 entry.setFrame(False)
974 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
975 entry.setPalette(palette)
977 entry.setReadOnly(False)
980 palette.setColor(entry.backgroundRole(), QColor('white'))
981 entry.setPalette(palette)
984 def toggle_freeze(self,addr):
986 if addr in self.wallet.frozen_addresses:
987 self.wallet.unfreeze(addr)
989 self.wallet.freeze(addr)
990 self.update_receive_tab()
992 def toggle_priority(self,addr):
994 if addr in self.wallet.prioritized_addresses:
995 self.wallet.unprioritize(addr)
997 self.wallet.prioritize(addr)
998 self.update_receive_tab()
1001 def create_list_tab(self, headers):
1002 "generic tab creation method"
1003 l = MyTreeWidget(self)
1004 l.setColumnCount( len(headers) )
1005 l.setHeaderLabels( headers )
1008 vbox = QVBoxLayout()
1015 vbox.addWidget(buttons)
1017 hbox = QHBoxLayout()
1020 buttons.setLayout(hbox)
1025 def create_receive_tab(self):
1026 l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1027 l.setContextMenuPolicy(Qt.CustomContextMenu)
1028 l.customContextMenuRequested.connect(self.create_receive_menu)
1029 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1030 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1031 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1032 self.receive_list = l
1033 self.receive_buttons_hbox = hbox
1040 def save_column_widths(self):
1041 self.column_widths["receive"] = []
1042 for i in range(self.receive_list.columnCount() -1):
1043 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1045 self.column_widths["history"] = []
1046 for i in range(self.history_list.columnCount() - 1):
1047 self.column_widths["history"].append(self.history_list.columnWidth(i))
1049 self.column_widths["contacts"] = []
1050 for i in range(self.contacts_list.columnCount() - 1):
1051 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1053 self.config.set_key("column_widths_2", self.column_widths, True)
1056 def create_contacts_tab(self):
1057 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1058 l.setContextMenuPolicy(Qt.CustomContextMenu)
1059 l.customContextMenuRequested.connect(self.create_contact_menu)
1060 for i,width in enumerate(self.column_widths['contacts']):
1061 l.setColumnWidth(i, width)
1063 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1064 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1065 self.contacts_list = l
1066 self.contacts_buttons_hbox = hbox
1071 def delete_imported_key(self, addr):
1072 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1073 self.wallet.delete_imported_key(addr)
1074 self.update_receive_tab()
1075 self.update_history_tab()
1077 def edit_account_label(self, k):
1078 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1080 label = unicode(text)
1081 self.wallet.set_label(k,label)
1082 self.update_receive_tab()
1084 def account_set_expanded(self, item, k, b):
1086 self.accounts_expanded[k] = b
1088 def create_account_menu(self, position, k, item):
1090 if item.isExpanded():
1091 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1093 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1094 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1095 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1096 if self.wallet.account_is_pending(k):
1097 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1098 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1100 def delete_pending_account(self, k):
1101 self.wallet.delete_pending_account(k)
1102 self.update_receive_tab()
1104 def create_receive_menu(self, position):
1105 # fixme: this function apparently has a side effect.
1106 # if it is not called the menu pops up several times
1107 #self.receive_list.selectedIndexes()
1109 item = self.receive_list.itemAt(position)
1112 addr = unicode(item.text(0))
1113 if not is_valid(addr):
1114 k = str(item.data(0,32).toString())
1116 self.create_account_menu(position, k, item)
1118 item.setExpanded(not item.isExpanded())
1122 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1123 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1124 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
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):
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 address in self.wallet.frozen_addresses:
1192 item.setBackgroundColor(0, QColor('lightblue'))
1193 elif address in self.wallet.prioritized_addresses:
1194 item.setBackgroundColor(0, QColor('lightgreen'))
1197 def update_receive_tab(self):
1198 l = self.receive_list
1201 l.setColumnHidden(2, False)
1202 l.setColumnHidden(3, False)
1203 for i,width in enumerate(self.column_widths['receive']):
1204 l.setColumnWidth(i, width)
1206 if self.current_account is None:
1207 account_items = self.wallet.accounts.items()
1208 elif self.current_account != -1:
1209 account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1213 for k, account in account_items:
1214 name = self.wallet.get_account_name(k)
1215 c,u = self.wallet.get_account_balance(k)
1216 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1217 l.addTopLevelItem(account_item)
1218 account_item.setExpanded(self.accounts_expanded.get(k, True))
1219 account_item.setData(0, 32, k)
1221 if not self.wallet.is_seeded(k):
1222 icon = QIcon(":icons/key.png")
1223 account_item.setIcon(0, icon)
1225 for is_change in ([0,1]):
1226 name = _("Receiving") if not is_change else _("Change")
1227 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1228 account_item.addChild(seq_item)
1229 if not is_change: seq_item.setExpanded(True)
1234 for address in account.get_addresses(is_change):
1235 h = self.wallet.history.get(address,[])
1239 if gap > self.wallet.gap_limit:
1244 num_tx = '*' if h == ['*'] else "%d"%len(h)
1245 item = QTreeWidgetItem( [ address, '', '', num_tx] )
1246 self.update_receive_item(item)
1248 item.setBackgroundColor(1, QColor('red'))
1249 seq_item.addChild(item)
1252 for k, addr in self.wallet.get_pending_accounts():
1253 name = self.wallet.labels.get(k,'')
1254 account_item = QTreeWidgetItem( [ name + " [ "+_('pending account')+" ]", '', '', ''] )
1255 self.update_receive_item(item)
1256 l.addTopLevelItem(account_item)
1257 account_item.setExpanded(True)
1258 account_item.setData(0, 32, k)
1259 item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1260 account_item.addChild(item)
1261 self.update_receive_item(item)
1264 if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1265 c,u = self.wallet.get_imported_balance()
1266 account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1267 l.addTopLevelItem(account_item)
1268 account_item.setExpanded(True)
1269 for address in self.wallet.imported_keys.keys():
1270 item = QTreeWidgetItem( [ address, '', '', ''] )
1271 self.update_receive_item(item)
1272 account_item.addChild(item)
1275 # we use column 1 because column 0 may be hidden
1276 l.setCurrentItem(l.topLevelItem(0),1)
1279 def update_contacts_tab(self):
1280 l = self.contacts_list
1283 for address in self.wallet.addressbook:
1284 label = self.wallet.labels.get(address,'')
1285 n = self.wallet.get_num_tx(address)
1286 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1287 item.setFont(0, QFont(MONOSPACE_FONT))
1288 # 32 = label can be edited (bool)
1289 item.setData(0,32, True)
1291 item.setData(0,33, address)
1292 l.addTopLevelItem(item)
1294 run_hook('update_contacts_tab', l)
1295 l.setCurrentItem(l.topLevelItem(0))
1299 def create_console_tab(self):
1300 from console import Console
1301 self.console = console = Console()
1305 def update_console(self):
1306 console = self.console
1307 console.history = self.config.get("console-history",[])
1308 console.history_index = len(console.history)
1310 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1311 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1313 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1315 def mkfunc(f, method):
1316 return lambda *args: apply( f, (method, args, self.password_dialog ))
1318 if m[0]=='_' or m in ['network','wallet']: continue
1319 methods[m] = mkfunc(c._run, m)
1321 console.updateNamespace(methods)
1324 def change_account(self,s):
1325 if s == _("All accounts"):
1326 self.current_account = None
1328 accounts = self.wallet.get_account_names()
1329 for k, v in accounts.items():
1331 self.current_account = k
1332 self.update_history_tab()
1333 self.update_status()
1334 self.update_receive_tab()
1336 def create_status_bar(self):
1339 sb.setFixedHeight(35)
1340 qtVersion = qVersion()
1342 self.balance_label = QLabel("")
1343 sb.addWidget(self.balance_label)
1345 from version_getter import UpdateLabel
1346 self.updatelabel = UpdateLabel(self.config, sb)
1348 self.account_selector = QComboBox()
1349 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1350 sb.addPermanentWidget(self.account_selector)
1352 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1353 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1355 self.lock_icon = QIcon()
1356 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1357 sb.addPermanentWidget( self.password_button )
1359 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1360 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1361 sb.addPermanentWidget( self.seed_button )
1362 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1363 sb.addPermanentWidget( self.status_button )
1365 run_hook('create_status_bar', (sb,))
1367 self.setStatusBar(sb)
1370 def update_lock_icon(self):
1371 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1372 self.password_button.setIcon( icon )
1375 def update_buttons_on_seed(self):
1376 if not self.wallet.is_watching_only():
1377 self.seed_button.show()
1378 self.password_button.show()
1379 self.send_button.setText(_("Send"))
1381 self.password_button.hide()
1382 self.seed_button.hide()
1383 self.send_button.setText(_("Create unsigned transaction"))
1386 def change_password_dialog(self):
1387 from password_dialog import PasswordDialog
1388 d = PasswordDialog(self.wallet, self)
1390 self.update_lock_icon()
1393 def new_contact_dialog(self):
1396 vbox = QVBoxLayout(d)
1397 vbox.addWidget(QLabel(_('New Contact')+':'))
1399 grid = QGridLayout()
1402 grid.addWidget(QLabel(_("Address")), 1, 0)
1403 grid.addWidget(line1, 1, 1)
1404 grid.addWidget(QLabel(_("Name")), 2, 0)
1405 grid.addWidget(line2, 2, 1)
1407 vbox.addLayout(grid)
1408 vbox.addLayout(ok_cancel_buttons(d))
1413 address = str(line1.text())
1414 label = unicode(line2.text())
1416 if not is_valid(address):
1417 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1420 self.wallet.add_contact(address)
1422 self.wallet.set_label(address, label)
1424 self.update_contacts_tab()
1425 self.update_history_tab()
1426 self.update_completions()
1427 self.tabs.setCurrentIndex(3)
1430 def new_account_dialog(self):
1432 dialog = QDialog(self)
1434 dialog.setWindowTitle(_("New Account"))
1436 vbox = QVBoxLayout()
1437 vbox.addWidget(QLabel(_('Account name')+':'))
1440 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1441 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1446 vbox.addLayout(ok_cancel_buttons(dialog))
1447 dialog.setLayout(vbox)
1451 name = str(e.text())
1454 self.wallet.create_pending_account('1', name)
1455 self.update_receive_tab()
1456 self.tabs.setCurrentIndex(2)
1460 def show_master_public_key_old(self):
1461 dialog = QDialog(self)
1463 dialog.setWindowTitle(_("Master Public Key"))
1465 main_text = QTextEdit()
1466 main_text.setText(self.wallet.get_master_public_key())
1467 main_text.setReadOnly(True)
1468 main_text.setMaximumHeight(170)
1469 qrw = QRCodeWidget(self.wallet.get_master_public_key())
1471 ok_button = QPushButton(_("OK"))
1472 ok_button.setDefault(True)
1473 ok_button.clicked.connect(dialog.accept)
1475 main_layout = QGridLayout()
1476 main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1478 main_layout.addWidget(main_text, 1, 0)
1479 main_layout.addWidget(qrw, 1, 1 )
1481 vbox.addLayout(close_button(dialog))
1482 dialog.setLayout(vbox)
1486 def show_master_public_key(self):
1488 if self.wallet.seed_version == 4:
1489 self.show_master_public_keys_old()
1492 dialog = QDialog(self)
1494 dialog.setWindowTitle(_("Master Public Keys"))
1496 chain_text = QTextEdit()
1497 chain_text.setReadOnly(True)
1498 chain_text.setMaximumHeight(170)
1499 chain_qrw = QRCodeWidget()
1501 mpk_text = QTextEdit()
1502 mpk_text.setReadOnly(True)
1503 mpk_text.setMaximumHeight(170)
1504 mpk_qrw = QRCodeWidget()
1506 main_layout = QGridLayout()
1508 main_layout.addWidget(QLabel(_('Key')), 1, 0)
1509 main_layout.addWidget(mpk_text, 1, 1)
1510 main_layout.addWidget(mpk_qrw, 1, 2)
1512 main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1513 main_layout.addWidget(chain_text, 2, 1)
1514 main_layout.addWidget(chain_qrw, 2, 2)
1517 c, K, cK = self.wallet.master_public_keys[str(key)]
1518 chain_text.setText(c)
1519 chain_qrw.set_addr(c)
1520 chain_qrw.update_qr()
1525 key_selector = QComboBox()
1526 keys = sorted(self.wallet.master_public_keys.keys())
1527 key_selector.addItems(keys)
1529 main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1530 main_layout.addWidget(key_selector, 0, 1)
1531 dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1535 vbox = QVBoxLayout()
1536 vbox.addLayout(main_layout)
1537 vbox.addLayout(close_button(dialog))
1539 dialog.setLayout(vbox)
1544 def show_seed_dialog(self, password):
1545 if self.wallet.is_watching_only():
1546 QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1549 if self.wallet.seed:
1551 seed = self.wallet.decode_seed(password)
1553 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1555 from seed_dialog import SeedDialog
1556 d = SeedDialog(self, seed, self.wallet.imported_keys)
1560 for k in self.wallet.master_private_keys.keys():
1561 pk = self.wallet.get_master_private_key(k, password)
1563 from seed_dialog import PrivateKeysDialog
1564 d = PrivateKeysDialog(self,l)
1571 def show_qrcode(self, data, title = _("QR code")):
1575 d.setWindowTitle(title)
1576 d.setMinimumSize(270, 300)
1577 vbox = QVBoxLayout()
1578 qrw = QRCodeWidget(data)
1579 vbox.addWidget(qrw, 1)
1580 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1581 hbox = QHBoxLayout()
1584 filename = os.path.join(self.config.path, "qrcode.bmp")
1587 bmp.save_qrcode(qrw.qr, filename)
1588 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1590 def copy_to_clipboard():
1591 bmp.save_qrcode(qrw.qr, filename)
1592 self.app.clipboard().setImage(QImage(filename))
1593 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1595 b = QPushButton(_("Copy"))
1597 b.clicked.connect(copy_to_clipboard)
1599 b = QPushButton(_("Save"))
1601 b.clicked.connect(print_qr)
1603 b = QPushButton(_("Close"))
1605 b.clicked.connect(d.accept)
1608 vbox.addLayout(hbox)
1613 def do_protect(self, func, args):
1614 if self.wallet.use_encryption:
1615 password = self.password_dialog()
1621 if args != (False,):
1622 args = (self,) + args + (password,)
1624 args = (self,password)
1629 def show_private_key(self, address, password):
1630 if not address: return
1632 pk_list = self.wallet.get_private_key(address, password)
1633 except BaseException, e:
1634 self.show_message(str(e))
1636 QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1640 def do_sign(self, address, message, signature, password):
1641 message = unicode(message.toPlainText())
1642 message = message.encode('utf-8')
1644 sig = self.wallet.sign_message(str(address.text()), message, password)
1645 signature.setText(sig)
1646 except BaseException, e:
1647 self.show_message(str(e))
1649 def sign_message(self, address):
1650 if not address: return
1653 d.setWindowTitle(_('Sign Message'))
1654 d.setMinimumSize(410, 290)
1656 tab_widget = QTabWidget()
1658 layout = QGridLayout(tab)
1660 sign_address = QLineEdit()
1662 sign_address.setText(address)
1663 layout.addWidget(QLabel(_('Address')), 1, 0)
1664 layout.addWidget(sign_address, 1, 1)
1666 sign_message = QTextEdit()
1667 layout.addWidget(QLabel(_('Message')), 2, 0)
1668 layout.addWidget(sign_message, 2, 1)
1669 layout.setRowStretch(2,3)
1671 sign_signature = QTextEdit()
1672 layout.addWidget(QLabel(_('Signature')), 3, 0)
1673 layout.addWidget(sign_signature, 3, 1)
1674 layout.setRowStretch(3,1)
1677 hbox = QHBoxLayout()
1678 b = QPushButton(_("Sign"))
1680 b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1681 b = QPushButton(_("Close"))
1682 b.clicked.connect(d.accept)
1684 layout.addLayout(hbox, 4, 1)
1685 tab_widget.addTab(tab, _("Sign"))
1689 layout = QGridLayout(tab)
1691 verify_address = QLineEdit()
1692 layout.addWidget(QLabel(_('Address')), 1, 0)
1693 layout.addWidget(verify_address, 1, 1)
1695 verify_message = QTextEdit()
1696 layout.addWidget(QLabel(_('Message')), 2, 0)
1697 layout.addWidget(verify_message, 2, 1)
1698 layout.setRowStretch(2,3)
1700 verify_signature = QTextEdit()
1701 layout.addWidget(QLabel(_('Signature')), 3, 0)
1702 layout.addWidget(verify_signature, 3, 1)
1703 layout.setRowStretch(3,1)
1706 message = unicode(verify_message.toPlainText())
1707 message = message.encode('utf-8')
1708 if bitcoin.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1709 self.show_message(_("Signature verified"))
1711 self.show_message(_("Error: wrong signature"))
1713 hbox = QHBoxLayout()
1714 b = QPushButton(_("Verify"))
1715 b.clicked.connect(do_verify)
1717 b = QPushButton(_("Close"))
1718 b.clicked.connect(d.accept)
1720 layout.addLayout(hbox, 4, 1)
1721 tab_widget.addTab(tab, _("Verify"))
1723 vbox = QVBoxLayout()
1724 vbox.addWidget(tab_widget)
1731 def question(self, msg):
1732 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1734 def show_message(self, msg):
1735 QMessageBox.information(self, _('Message'), msg, _('OK'))
1737 def password_dialog(self ):
1744 vbox = QVBoxLayout()
1745 msg = _('Please enter your password')
1746 vbox.addWidget(QLabel(msg))
1748 grid = QGridLayout()
1750 grid.addWidget(QLabel(_('Password')), 1, 0)
1751 grid.addWidget(pw, 1, 1)
1752 vbox.addLayout(grid)
1754 vbox.addLayout(ok_cancel_buttons(d))
1757 run_hook('password_dialog', pw, grid, 1)
1758 if not d.exec_(): return
1759 return unicode(pw.text())
1768 def tx_from_text(self, txt):
1769 "json or raw hexadecimal"
1772 tx = Transaction(txt)
1778 tx_dict = json.loads(str(txt))
1779 assert "hex" in tx_dict.keys()
1780 assert "complete" in tx_dict.keys()
1781 tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1782 if not tx_dict["complete"]:
1783 assert "input_info" in tx_dict.keys()
1784 input_info = json.loads(tx_dict['input_info'])
1785 tx.add_input_info(input_info)
1790 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1794 def read_tx_from_file(self):
1795 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1799 with open(fileName, "r") as f:
1800 file_content = f.read()
1801 except (ValueError, IOError, os.error), reason:
1802 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1804 return self.tx_from_text(file_content)
1808 def sign_raw_transaction(self, tx, input_info, password):
1809 self.wallet.signrawtransaction(tx, input_info, [], password)
1811 def do_process_from_text(self):
1812 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1815 tx = self.tx_from_text(text)
1817 self.show_transaction(tx)
1819 def do_process_from_file(self):
1820 tx = self.read_tx_from_file()
1822 self.show_transaction(tx)
1824 def do_process_from_csvReader(self, csvReader):
1827 for row in csvReader:
1829 amount = float(row[1])
1830 amount = int(100000000*amount)
1831 outputs.append((address, amount))
1832 except (ValueError, IOError, os.error), reason:
1833 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1837 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1838 except BaseException, e:
1839 self.show_message(str(e))
1842 self.show_transaction(tx)
1844 def do_process_from_csv_file(self):
1845 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1849 with open(fileName, "r") as f:
1850 csvReader = csv.reader(f)
1851 self.do_process_from_csvReader(csvReader)
1852 except (ValueError, IOError, os.error), reason:
1853 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1856 def do_process_from_csv_text(self):
1857 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1858 + _("Format: address, amount. One output per line"), _("Load CSV"))
1861 f = StringIO.StringIO(text)
1862 csvReader = csv.reader(f)
1863 self.do_process_from_csvReader(csvReader)
1868 def do_export_privkeys(self, password):
1869 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.")))
1872 select_export = _('Select file to export your private keys to')
1873 fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1875 with open(fileName, "w+") as csvfile:
1876 transaction = csv.writer(csvfile)
1877 transaction.writerow(["address", "private_key"])
1879 addresses = self.wallet.addresses(True)
1881 for addr in addresses:
1882 pk = "".join(self.wallet.get_private_key(addr, password))
1883 transaction.writerow(["%34s"%addr,pk])
1885 self.show_message(_("Private keys exported."))
1887 except (IOError, os.error), reason:
1888 export_error_label = _("Electrum was unable to produce a private key-export.")
1889 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1891 except BaseException, e:
1892 self.show_message(str(e))
1896 def do_import_labels(self):
1897 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1898 if not labelsFile: return
1900 f = open(labelsFile, 'r')
1903 for key, value in json.loads(data).items():
1904 self.wallet.set_label(key, value)
1905 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1906 except (IOError, os.error), reason:
1907 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1910 def do_export_labels(self):
1911 labels = self.wallet.labels
1913 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1915 with open(fileName, 'w+') as f:
1916 json.dump(labels, f)
1917 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1918 except (IOError, os.error), reason:
1919 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1922 def do_export_history(self):
1923 from lite_window import csv_transaction
1924 csv_transaction(self.wallet)
1928 def do_import_privkey(self, password):
1929 if not self.wallet.imported_keys:
1930 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1931 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1932 + _('Are you sure you understand what you are doing?'), 3, 4)
1935 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1938 text = str(text).split()
1943 addr = self.wallet.import_key(key, password)
1944 except BaseException as e:
1950 addrlist.append(addr)
1952 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1954 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1955 self.update_receive_tab()
1956 self.update_history_tab()
1959 def settings_dialog(self):
1961 d.setWindowTitle(_('Electrum Settings'))
1963 vbox = QVBoxLayout()
1964 grid = QGridLayout()
1965 grid.setColumnStretch(0,1)
1967 nz_label = QLabel(_('Display zeros') + ':')
1968 grid.addWidget(nz_label, 0, 0)
1969 nz_e = AmountEdit(None,True)
1970 nz_e.setText("%d"% self.num_zeros)
1971 grid.addWidget(nz_e, 0, 1)
1972 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1973 grid.addWidget(HelpButton(msg), 0, 2)
1974 if not self.config.is_modifiable('num_zeros'):
1975 for w in [nz_e, nz_label]: w.setEnabled(False)
1977 lang_label=QLabel(_('Language') + ':')
1978 grid.addWidget(lang_label, 1, 0)
1979 lang_combo = QComboBox()
1980 from electrum.i18n import languages
1981 lang_combo.addItems(languages.values())
1983 index = languages.keys().index(self.config.get("language",''))
1986 lang_combo.setCurrentIndex(index)
1987 grid.addWidget(lang_combo, 1, 1)
1988 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1989 if not self.config.is_modifiable('language'):
1990 for w in [lang_combo, lang_label]: w.setEnabled(False)
1993 fee_label = QLabel(_('Transaction fee') + ':')
1994 grid.addWidget(fee_label, 2, 0)
1995 fee_e = AmountEdit(self.base_unit)
1996 fee_e.setText(self.format_amount(self.wallet.fee).strip())
1997 grid.addWidget(fee_e, 2, 1)
1998 msg = _('Fee per kilobyte of transaction.') + ' ' \
1999 + _('Recommended value') + ': ' + self.format_amount(50000)
2000 grid.addWidget(HelpButton(msg), 2, 2)
2001 if not self.config.is_modifiable('fee_per_kb'):
2002 for w in [fee_e, fee_label]: w.setEnabled(False)
2004 units = ['BTC', 'mBTC']
2005 unit_label = QLabel(_('Base unit') + ':')
2006 grid.addWidget(unit_label, 3, 0)
2007 unit_combo = QComboBox()
2008 unit_combo.addItems(units)
2009 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2010 grid.addWidget(unit_combo, 3, 1)
2011 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2012 + '\n1BTC=1000mBTC.\n' \
2013 + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
2015 usechange_cb = QCheckBox(_('Use change addresses'))
2016 usechange_cb.setChecked(self.wallet.use_change)
2017 grid.addWidget(usechange_cb, 4, 0)
2018 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2019 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2021 grid.setRowStretch(5,1)
2023 vbox.addLayout(grid)
2024 vbox.addLayout(ok_cancel_buttons(d))
2028 if not d.exec_(): return
2030 fee = unicode(fee_e.text())
2032 fee = self.read_amount(fee)
2034 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2037 self.wallet.set_fee(fee)
2039 nz = unicode(nz_e.text())
2044 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2047 if self.num_zeros != nz:
2049 self.config.set_key('num_zeros', nz, True)
2050 self.update_history_tab()
2051 self.update_receive_tab()
2053 usechange_result = usechange_cb.isChecked()
2054 if self.wallet.use_change != usechange_result:
2055 self.wallet.use_change = usechange_result
2056 self.config.set_key('use_change', self.wallet.use_change, True)
2058 unit_result = units[unit_combo.currentIndex()]
2059 if self.base_unit() != unit_result:
2060 self.decimal_point = 8 if unit_result == 'BTC' else 5
2061 self.config.set_key('decimal_point', self.decimal_point, True)
2062 self.update_history_tab()
2063 self.update_status()
2065 need_restart = False
2067 lang_request = languages.keys()[lang_combo.currentIndex()]
2068 if lang_request != self.config.get('language'):
2069 self.config.set_key("language", lang_request, True)
2072 run_hook('close_settings_dialog')
2075 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2078 def run_network_dialog(self):
2079 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2081 def closeEvent(self, event):
2083 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2084 self.save_column_widths()
2085 self.config.set_key("console-history", self.console.history[-50:], True)
2086 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2091 def plugins_dialog(self):
2092 from electrum.plugins import plugins
2095 d.setWindowTitle(_('Electrum Plugins'))
2098 vbox = QVBoxLayout(d)
2101 scroll = QScrollArea()
2102 scroll.setEnabled(True)
2103 scroll.setWidgetResizable(True)
2104 scroll.setMinimumSize(400,250)
2105 vbox.addWidget(scroll)
2109 w.setMinimumHeight(len(plugins)*35)
2111 grid = QGridLayout()
2112 grid.setColumnStretch(0,1)
2115 def do_toggle(cb, p, w):
2118 if w: w.setEnabled(r)
2120 def mk_toggle(cb, p, w):
2121 return lambda: do_toggle(cb,p,w)
2123 for i, p in enumerate(plugins):
2125 cb = QCheckBox(p.fullname())
2126 cb.setDisabled(not p.is_available())
2127 cb.setChecked(p.is_enabled())
2128 grid.addWidget(cb, i, 0)
2129 if p.requires_settings():
2130 w = p.settings_widget(self)
2131 w.setEnabled( p.is_enabled() )
2132 grid.addWidget(w, i, 1)
2135 cb.clicked.connect(mk_toggle(cb,p,w))
2136 grid.addWidget(HelpButton(p.description()), i, 2)
2138 print_msg(_("Error: cannot display plugin"), p)
2139 traceback.print_exc(file=sys.stdout)
2140 grid.setRowStretch(i+1,1)
2142 vbox.addLayout(close_button(d))
2147 def show_account_details(self, k):
2149 d.setWindowTitle(_('Account Details'))
2152 vbox = QVBoxLayout(d)
2153 roots = self.wallet.get_roots(k)
2155 name = self.wallet.get_account_name(k)
2156 label = QLabel('Name: ' + name)
2157 vbox.addWidget(label)
2159 acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2160 vbox.addWidget(QLabel('Type: ' + acctype))
2162 label = QLabel('Derivation: ' + k)
2163 vbox.addWidget(label)
2166 # mpk = self.wallet.master_public_keys[root]
2167 # text = QTextEdit()
2168 # text.setReadOnly(True)
2169 # text.setMaximumHeight(120)
2170 # text.setText(repr(mpk))
2171 # vbox.addWidget(text)
2173 vbox.addLayout(close_button(d))