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
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
33 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
34 from electrum.plugins import run_hook
38 from electrum.wallet import format_satoshis
39 from electrum import Transaction
40 from electrum import mnemonic
41 from electrum import util, bitcoin, commands, Interface, Wallet
42 from electrum import SimpleConfig, Wallet, WalletStorage
44 from electrum.paymentrequest import PR_UNPAID, PR_PAID
47 from electrum import bmp, pyqrnative
49 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
50 from network_dialog import NetworkDialog
51 from qrcodewidget import QRCodeWidget
53 from decimal import Decimal
61 if platform.system() == 'Windows':
62 MONOSPACE_FONT = 'Lucida Console'
63 elif platform.system() == 'Darwin':
64 MONOSPACE_FONT = 'Monaco'
66 MONOSPACE_FONT = 'monospace'
68 from electrum import ELECTRUM_VERSION
81 class StatusBarButton(QPushButton):
82 def __init__(self, icon, tooltip, func):
83 QPushButton.__init__(self, icon, '')
84 self.setToolTip(tooltip)
86 self.setMaximumWidth(25)
87 self.clicked.connect(func)
89 self.setIconSize(QSize(25,25))
91 def keyPressEvent(self, e):
92 if e.key() == QtCore.Qt.Key_Return:
104 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
106 class ElectrumWindow(QMainWindow):
110 def __init__(self, config, network, gui_object):
111 QMainWindow.__init__(self)
114 self.network = network
115 self.gui_object = gui_object
116 self.tray = gui_object.tray
117 self.go_lite = gui_object.go_lite
120 self.create_status_bar()
121 self.need_update = threading.Event()
123 self.decimal_point = config.get('decimal_point', 5)
124 self.num_zeros = int(config.get('num_zeros',0))
127 set_language(config.get('language'))
129 self.funds_error = False
130 self.completions = QStringListModel()
132 self.tabs = tabs = QTabWidget(self)
133 self.column_widths = self.config.get("column_widths_2", default_column_widths )
134 tabs.addTab(self.create_history_tab(), _('History') )
135 tabs.addTab(self.create_send_tab(), _('Send') )
136 tabs.addTab(self.create_receive_tab(), _('Receive') )
137 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
138 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
139 tabs.addTab(self.create_console_tab(), _('Console') )
140 tabs.setMinimumSize(600, 400)
141 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
142 self.setCentralWidget(tabs)
144 g = self.config.get("winpos-qt",[100, 100, 840, 400])
145 self.setGeometry(g[0], g[1], g[2], g[3])
146 if self.config.get("is_maximized"):
149 self.setWindowIcon(QIcon(":icons/electrum.png"))
152 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
153 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
154 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
155 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
156 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
158 for i in range(tabs.count()):
159 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
161 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
162 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
163 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
164 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
165 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
167 self.history_list.setFocus(True)
171 self.network.register_callback('updated', lambda: self.need_update.set())
172 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
173 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
174 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
175 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
177 # set initial message
178 self.console.showMessage(self.network.banner)
183 def update_account_selector(self):
185 accounts = self.wallet.get_account_names()
186 self.account_selector.clear()
187 if len(accounts) > 1:
188 self.account_selector.addItems([_("All accounts")] + accounts.values())
189 self.account_selector.setCurrentIndex(0)
190 self.account_selector.show()
192 self.account_selector.hide()
195 def load_wallet(self, wallet):
199 self.update_wallet_format()
201 self.invoices = self.wallet.storage.get('invoices', {})
202 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
203 self.current_account = self.wallet.storage.get("current_account", None)
204 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
205 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
206 self.setWindowTitle( title )
208 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
209 self.notify_transactions()
210 self.update_account_selector()
212 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
213 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
214 self.password_menu.setEnabled(not self.wallet.is_watching_only())
215 self.seed_menu.setEnabled(self.wallet.has_seed())
216 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
217 self.import_menu.setEnabled(self.wallet.can_import())
219 self.update_lock_icon()
220 self.update_buttons_on_seed()
221 self.update_console()
223 run_hook('load_wallet', wallet)
226 def update_wallet_format(self):
227 # convert old-format imported keys
228 if self.wallet.imported_keys:
229 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
231 self.wallet.convert_imported_keys(password)
233 self.show_message("error")
236 def open_wallet(self):
237 wallet_folder = self.wallet.storage.path
238 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
242 storage = WalletStorage({'wallet_path': filename})
243 if not storage.file_exists:
244 self.show_message("file not found "+ filename)
247 self.wallet.stop_threads()
250 wallet = Wallet(storage)
251 wallet.start_threads(self.network)
253 self.load_wallet(wallet)
257 def backup_wallet(self):
259 path = self.wallet.storage.path
260 wallet_folder = os.path.dirname(path)
261 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
265 new_path = os.path.join(wallet_folder, filename)
268 shutil.copy2(path, new_path)
269 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
270 except (IOError, os.error), reason:
271 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
274 def new_wallet(self):
277 wallet_folder = os.path.dirname(self.wallet.storage.path)
278 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
281 filename = os.path.join(wallet_folder, filename)
283 storage = WalletStorage({'wallet_path': filename})
284 if storage.file_exists:
285 QMessageBox.critical(None, "Error", _("File exists"))
288 wizard = installwizard.InstallWizard(self.config, self.network, storage)
289 wallet = wizard.run('new')
291 self.load_wallet(wallet)
295 def init_menubar(self):
298 file_menu = menubar.addMenu(_("&File"))
299 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
300 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
301 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
302 file_menu.addAction(_("&Quit"), self.close)
304 wallet_menu = menubar.addMenu(_("&Wallet"))
305 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
306 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
308 wallet_menu.addSeparator()
310 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
311 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
312 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
314 wallet_menu.addSeparator()
315 labels_menu = wallet_menu.addMenu(_("&Labels"))
316 labels_menu.addAction(_("&Import"), self.do_import_labels)
317 labels_menu.addAction(_("&Export"), self.do_export_labels)
319 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
320 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
321 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
322 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
323 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
325 tools_menu = menubar.addMenu(_("&Tools"))
327 # Settings / Preferences are all reserved keywords in OSX using this as work around
328 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
329 tools_menu.addAction(_("&Network"), self.run_network_dialog)
330 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
331 tools_menu.addSeparator()
332 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
333 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
334 tools_menu.addSeparator()
336 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
337 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
338 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
340 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
341 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
342 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
343 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
345 help_menu = menubar.addMenu(_("&Help"))
346 help_menu.addAction(_("&About"), self.show_about)
347 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
348 help_menu.addSeparator()
349 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
350 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
352 self.setMenuBar(menubar)
354 def show_about(self):
355 QMessageBox.about(self, "Electrum",
356 _("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."))
358 def show_report_bug(self):
359 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
360 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
363 def notify_transactions(self):
364 if not self.network or not self.network.is_connected():
367 print_error("Notifying GUI")
368 if len(self.network.pending_transactions_for_notifications) > 0:
369 # Combine the transactions if there are more then three
370 tx_amount = len(self.network.pending_transactions_for_notifications)
373 for tx in self.network.pending_transactions_for_notifications:
374 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
378 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
379 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
381 self.network.pending_transactions_for_notifications = []
383 for tx in self.network.pending_transactions_for_notifications:
385 self.network.pending_transactions_for_notifications.remove(tx)
386 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
388 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
390 def notify(self, message):
391 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
395 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
396 def getOpenFileName(self, title, filter = ""):
397 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
398 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
399 if fileName and directory != os.path.dirname(fileName):
400 self.config.set_key('io_dir', os.path.dirname(fileName), True)
403 def getSaveFileName(self, title, filename, filter = ""):
404 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
405 path = os.path.join( directory, filename )
406 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
407 if fileName and directory != os.path.dirname(fileName):
408 self.config.set_key('io_dir', os.path.dirname(fileName), True)
412 QMainWindow.close(self)
413 run_hook('close_main_window')
415 def connect_slots(self, sender):
416 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
417 self.previous_payto_e=''
419 def timer_actions(self):
420 if self.need_update.is_set():
422 self.need_update.clear()
423 run_hook('timer_actions')
425 def format_amount(self, x, is_diff=False, whitespaces=False):
426 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
429 def get_decimal_point(self):
430 return self.decimal_point
434 assert self.decimal_point in [5,8]
435 return "BTC" if self.decimal_point == 8 else "mBTC"
438 def update_status(self):
439 if self.network is None or not self.network.is_running():
441 icon = QIcon(":icons/status_disconnected.png")
443 elif self.network.is_connected():
444 if not self.wallet.up_to_date:
445 text = _("Synchronizing...")
446 icon = QIcon(":icons/status_waiting.png")
447 elif self.network.server_lag > 1:
448 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
449 icon = QIcon(":icons/status_lagging.png")
451 c, u = self.wallet.get_account_balance(self.current_account)
452 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
453 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
455 # append fiat balance and price from exchange rate plugin
457 run_hook('get_fiat_status_text', c+u, r)
462 self.tray.setToolTip(text)
463 icon = QIcon(":icons/status_connected.png")
465 text = _("Not connected")
466 icon = QIcon(":icons/status_disconnected.png")
468 self.balance_label.setText(text)
469 self.status_button.setIcon( icon )
472 def update_wallet(self):
474 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
475 self.update_history_tab()
476 self.update_receive_tab()
477 self.update_contacts_tab()
478 self.update_completions()
479 self.update_invoices_tab()
482 def create_history_tab(self):
483 self.history_list = l = MyTreeWidget(self)
485 for i,width in enumerate(self.column_widths['history']):
486 l.setColumnWidth(i, width)
487 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
488 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
489 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
491 l.customContextMenuRequested.connect(self.create_history_menu)
495 def create_history_menu(self, position):
496 self.history_list.selectedIndexes()
497 item = self.history_list.currentItem()
498 be = self.config.get('block_explorer', 'Blockchain.info')
499 if be == 'Blockchain.info':
500 block_explorer = 'https://blockchain.info/tx/'
501 elif be == 'Blockr.io':
502 block_explorer = 'https://blockr.io/tx/info/'
503 elif be == 'Insight.is':
504 block_explorer = 'http://live.insight.is/tx/'
506 tx_hash = str(item.data(0, Qt.UserRole).toString())
507 if not tx_hash: return
509 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
510 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
511 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
512 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
513 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
516 def show_transaction(self, tx):
517 import transaction_dialog
518 d = transaction_dialog.TxDialog(tx, self)
521 def tx_label_clicked(self, item, column):
522 if column==2 and item.isSelected():
524 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
525 self.history_list.editItem( item, column )
526 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
529 def tx_label_changed(self, item, column):
533 tx_hash = str(item.data(0, Qt.UserRole).toString())
534 tx = self.wallet.transactions.get(tx_hash)
535 text = unicode( item.text(2) )
536 self.wallet.set_label(tx_hash, text)
538 item.setForeground(2, QBrush(QColor('black')))
540 text = self.wallet.get_default_label(tx_hash)
541 item.setText(2, text)
542 item.setForeground(2, QBrush(QColor('gray')))
546 def edit_label(self, is_recv):
547 l = self.receive_list if is_recv else self.contacts_list
548 item = l.currentItem()
549 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
550 l.editItem( item, 1 )
551 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
555 def address_label_clicked(self, item, column, l, column_addr, column_label):
556 if column == column_label and item.isSelected():
557 is_editable = item.data(0, 32).toBool()
560 addr = unicode( item.text(column_addr) )
561 label = unicode( item.text(column_label) )
562 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
563 l.editItem( item, column )
564 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
567 def address_label_changed(self, item, column, l, column_addr, column_label):
568 if column == column_label:
569 addr = unicode( item.text(column_addr) )
570 text = unicode( item.text(column_label) )
571 is_editable = item.data(0, 32).toBool()
575 changed = self.wallet.set_label(addr, text)
577 self.update_history_tab()
578 self.update_completions()
580 self.current_item_changed(item)
582 run_hook('item_changed', item, column)
585 def current_item_changed(self, a):
586 run_hook('current_item_changed', a)
590 def update_history_tab(self):
592 self.history_list.clear()
593 for item in self.wallet.get_tx_history(self.current_account):
594 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
595 time_str = _("unknown")
598 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
600 time_str = _("error")
603 time_str = 'unverified'
604 icon = QIcon(":icons/unconfirmed.png")
607 icon = QIcon(":icons/unconfirmed.png")
609 icon = QIcon(":icons/clock%d.png"%conf)
611 icon = QIcon(":icons/confirmed.png")
613 if value is not None:
614 v_str = self.format_amount(value, True, whitespaces=True)
618 balance_str = self.format_amount(balance, whitespaces=True)
621 label, is_default_label = self.wallet.get_label(tx_hash)
623 label = _('Pruned transaction outputs')
624 is_default_label = False
626 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
627 item.setFont(2, QFont(MONOSPACE_FONT))
628 item.setFont(3, QFont(MONOSPACE_FONT))
629 item.setFont(4, QFont(MONOSPACE_FONT))
631 item.setForeground(3, QBrush(QColor("#BC1E1E")))
633 item.setData(0, Qt.UserRole, tx_hash)
634 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
636 item.setForeground(2, QBrush(QColor('grey')))
638 item.setIcon(0, icon)
639 self.history_list.insertTopLevelItem(0,item)
642 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
643 run_hook('history_tab_update')
646 def create_send_tab(self):
649 self.send_grid = grid = QGridLayout(w)
651 grid.setColumnMinimumWidth(3,300)
652 grid.setColumnStretch(5,1)
653 grid.setRowStretch(8, 1)
655 from paytoedit import PayToEdit
656 self.amount_e = BTCAmountEdit(self.get_decimal_point)
657 self.payto_e = PayToEdit(self.amount_e)
658 self.payto_help = 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)'))
659 grid.addWidget(QLabel(_('Pay to')), 1, 0)
660 grid.addWidget(self.payto_e, 1, 1, 1, 3)
661 grid.addWidget(self.payto_help, 1, 4)
663 completer = QCompleter()
664 completer.setCaseSensitivity(False)
665 self.payto_e.setCompleter(completer)
666 completer.setModel(self.completions)
668 self.message_e = MyLineEdit()
669 self.message_help = 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.'))
670 grid.addWidget(QLabel(_('Description')), 2, 0)
671 grid.addWidget(self.message_e, 2, 1, 1, 3)
672 grid.addWidget(self.message_help, 2, 4)
674 self.from_label = QLabel(_('From'))
675 grid.addWidget(self.from_label, 3, 0)
676 self.from_list = MyTreeWidget(self)
677 self.from_list.setColumnCount(2)
678 self.from_list.setColumnWidth(0, 350)
679 self.from_list.setColumnWidth(1, 50)
680 self.from_list.setHeaderHidden(True)
681 self.from_list.setMaximumHeight(80)
682 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
683 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
684 grid.addWidget(self.from_list, 3, 1, 1, 3)
685 self.set_pay_from([])
687 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
688 + _('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.') \
689 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
690 grid.addWidget(QLabel(_('Amount')), 4, 0)
691 grid.addWidget(self.amount_e, 4, 1, 1, 2)
692 grid.addWidget(self.amount_help, 4, 3)
694 self.fee_e = BTCAmountEdit(self.get_decimal_point)
695 grid.addWidget(QLabel(_('Fee')), 5, 0)
696 grid.addWidget(self.fee_e, 5, 1, 1, 2)
697 grid.addWidget(HelpButton(
698 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
699 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
700 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 5, 3)
702 self.send_button = EnterButton(_("Send"), self.do_send)
703 grid.addWidget(self.send_button, 6, 1)
705 b = EnterButton(_("Clear"), self.do_clear)
706 grid.addWidget(b, 6, 2)
708 self.payto_sig = QLabel('')
709 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
711 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
712 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
715 def entry_changed( is_fee ):
716 self.funds_error = False
718 if self.amount_e.is_shortcut:
719 self.amount_e.is_shortcut = False
720 sendable = self.get_sendable_balance()
721 # there is only one output because we are completely spending inputs
722 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
723 fee = self.wallet.estimated_fee(inputs, 1)
725 self.amount_e.setAmount(amount)
726 self.fee_e.setAmount(fee)
729 amount = self.amount_e.get_amount()
730 fee = self.fee_e.get_amount()
732 if not is_fee: fee = None
735 # assume that there will be 2 outputs (one for change)
736 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
738 self.fee_e.setAmount(fee)
741 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
745 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
746 self.funds_error = True
747 text = _( "Not enough funds" )
748 c, u = self.wallet.get_frozen_balance()
749 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
751 self.statusBar().showMessage(text)
752 self.amount_e.setPalette(palette)
753 self.fee_e.setPalette(palette)
755 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
756 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
758 run_hook('create_send_tab', grid)
761 def from_list_delete(self, item):
762 i = self.from_list.indexOfTopLevelItem(item)
764 self.redraw_from_list()
766 def from_list_menu(self, position):
767 item = self.from_list.itemAt(position)
769 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
770 menu.exec_(self.from_list.viewport().mapToGlobal(position))
772 def set_pay_from(self, domain = None):
773 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
774 self.redraw_from_list()
776 def redraw_from_list(self):
777 self.from_list.clear()
778 self.from_label.setHidden(len(self.pay_from) == 0)
779 self.from_list.setHidden(len(self.pay_from) == 0)
782 h = x.get('prevout_hash')
783 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
785 for item in self.pay_from:
786 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
788 def update_completions(self):
790 for addr,label in self.wallet.labels.items():
791 if addr in self.wallet.addressbook:
792 l.append( label + ' <' + addr + '>')
794 run_hook('update_completions', l)
795 self.completions.setStringList(l)
799 return lambda s, *args: s.do_protect(func, args)
803 label = unicode( self.message_e.text() )
805 if self.gui_object.payment_request:
806 outputs = self.gui_object.payment_request.get_outputs()
808 outputs = self.payto_e.get_outputs()
811 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
814 for addr, x in outputs:
815 if addr is None or not bitcoin.is_address(addr):
816 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
819 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
822 amount = sum(map(lambda x:x[1], outputs))
824 fee = self.fee_e.get_amount()
826 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
829 confirm_amount = self.config.get('confirm_amount', 100000000)
830 if amount >= confirm_amount:
831 o = '\n'.join(map(lambda x:x[0], outputs))
832 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
835 confirm_fee = self.config.get('confirm_fee', 100000)
836 if fee >= confirm_fee:
837 if not self.question(_("The fee for this transaction seems unusually high.\nAre you really sure you want to pay %(fee)s in fees?")%{ 'fee' : self.format_amount(fee) + ' '+ self.base_unit()}):
840 self.send_tx(outputs, fee, label)
845 def send_tx(self, outputs, fee, label, password):
846 self.send_button.setDisabled(True)
848 # first, create an unsigned tx
849 coins = self.get_coins()
851 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
853 except Exception as e:
854 traceback.print_exc(file=sys.stdout)
855 self.show_message(str(e))
856 self.send_button.setDisabled(False)
859 # call hook to see if plugin needs gui interaction
860 run_hook('send_tx', tx)
866 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
867 self.wallet.sign_transaction(tx, keypairs, password)
868 return tx, fee, label
870 def sign_done(tx, fee, label):
872 self.show_message(tx.error)
873 self.send_button.setDisabled(False)
875 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
876 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
877 self.send_button.setDisabled(False)
880 self.wallet.set_label(tx.hash(), label)
882 if not tx.is_complete() or self.config.get('show_before_broadcast'):
883 self.show_transaction(tx)
885 self.send_button.setDisabled(False)
888 self.broadcast_transaction(tx)
890 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
891 self.waiting_dialog.start()
895 def broadcast_transaction(self, tx):
897 def broadcast_thread():
898 if self.gui_object.payment_request:
899 refund_address = self.wallet.addresses()[0]
900 status, msg = self.gui_object.payment_request.send_ack(str(tx), refund_address)
902 pr = self.gui_object.payment_request
904 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), PR_PAID)
905 self.wallet.storage.put('invoices', self.invoices)
906 self.update_invoices_tab()
907 self.gui_object.payment_request = None
909 status, msg = self.wallet.sendtx(tx)
912 def broadcast_done(status, msg):
914 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
917 QMessageBox.warning(self, _('Error'), msg, _('OK'))
918 self.send_button.setDisabled(False)
920 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
921 self.waiting_dialog.start()
925 def prepare_for_payment_request(self):
926 self.tabs.setCurrentIndex(1)
927 self.payto_e.is_pr = True
928 for e in [self.payto_e, self.amount_e, self.message_e]:
930 for h in [self.payto_help, self.amount_help, self.message_help]:
932 self.payto_e.setText(_("please wait..."))
935 def payment_request_ok(self):
936 pr = self.gui_object.payment_request
938 if pr_id not in self.invoices:
939 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), PR_UNPAID)
940 self.wallet.storage.put('invoices', self.invoices)
941 self.update_invoices_tab()
943 print_error('invoice already in list')
945 status = self.invoices[pr_id][3]
946 if status == PR_PAID:
948 self.show_message("invoice already paid")
949 self.gui_object.payment_request = None
952 self.payto_help.show()
953 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
955 self.payto_e.setGreen()
956 self.payto_e.setText(pr.domain)
957 self.amount_e.setText(self.format_amount(pr.get_amount()))
958 self.message_e.setText(pr.get_memo())
960 def payment_request_error(self):
962 self.show_message(self.gui_object.payment_request.error)
963 self.gui_object.payment_request = None
965 def set_send(self, address, amount, label, message):
967 if label and self.wallet.labels.get(address) != label:
968 if self.question('Give label "%s" to address %s ?'%(label,address)):
969 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
970 self.wallet.addressbook.append(address)
971 self.wallet.set_label(address, label)
973 self.tabs.setCurrentIndex(1)
974 label = self.wallet.labels.get(address)
975 m_addr = label + ' <'+ address +'>' if label else address
976 self.payto_e.setText(m_addr)
978 self.message_e.setText(message)
980 self.amount_e.setText(amount)
984 self.payto_e.is_pr = False
985 self.payto_sig.setVisible(False)
986 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
990 for h in [self.payto_help, self.amount_help, self.message_help]:
993 self.payto_help.set_alt(None)
994 self.set_pay_from([])
999 def set_addrs_frozen(self,addrs,freeze):
1001 if not addr: continue
1002 if addr in self.wallet.frozen_addresses and not freeze:
1003 self.wallet.unfreeze(addr)
1004 elif addr not in self.wallet.frozen_addresses and freeze:
1005 self.wallet.freeze(addr)
1006 self.update_receive_tab()
1010 def create_list_tab(self, headers):
1011 "generic tab creation method"
1012 l = MyTreeWidget(self)
1013 l.setColumnCount( len(headers) )
1014 l.setHeaderLabels( headers )
1017 vbox = QVBoxLayout()
1024 vbox.addWidget(buttons)
1029 def create_receive_tab(self):
1030 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1031 l.setContextMenuPolicy(Qt.CustomContextMenu)
1032 l.customContextMenuRequested.connect(self.create_receive_menu)
1033 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1034 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1035 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1036 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1037 self.receive_list = l
1043 def save_column_widths(self):
1044 self.column_widths["receive"] = []
1045 for i in range(self.receive_list.columnCount() -1):
1046 self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1048 self.column_widths["history"] = []
1049 for i in range(self.history_list.columnCount() - 1):
1050 self.column_widths["history"].append(self.history_list.columnWidth(i))
1052 self.column_widths["contacts"] = []
1053 for i in range(self.contacts_list.columnCount() - 1):
1054 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1056 self.config.set_key("column_widths_2", self.column_widths, True)
1059 def create_contacts_tab(self):
1060 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1061 l.setContextMenuPolicy(Qt.CustomContextMenu)
1062 l.customContextMenuRequested.connect(self.create_contact_menu)
1063 for i,width in enumerate(self.column_widths['contacts']):
1064 l.setColumnWidth(i, width)
1066 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1067 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1068 self.contacts_list = l
1072 def create_invoices_tab(self):
1073 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1075 h.setStretchLastSection(False)
1076 h.setResizeMode(1, QHeaderView.Stretch)
1077 l.setContextMenuPolicy(Qt.CustomContextMenu)
1078 l.customContextMenuRequested.connect(self.create_invoice_menu)
1079 self.invoices_list = l
1082 def update_invoices_tab(self):
1083 invoices = self.wallet.storage.get('invoices', {})
1084 l = self.invoices_list
1086 for key, value in invoices.items():
1088 domain, memo, amount, status = value
1092 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1093 l.addTopLevelItem(item)
1095 l.setCurrentItem(l.topLevelItem(0))
1099 def delete_imported_key(self, addr):
1100 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1101 self.wallet.delete_imported_key(addr)
1102 self.update_receive_tab()
1103 self.update_history_tab()
1105 def edit_account_label(self, k):
1106 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1108 label = unicode(text)
1109 self.wallet.set_label(k,label)
1110 self.update_receive_tab()
1112 def account_set_expanded(self, item, k, b):
1114 self.accounts_expanded[k] = b
1116 def create_account_menu(self, position, k, item):
1118 if item.isExpanded():
1119 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1121 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1122 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1123 if self.wallet.seed_version > 4:
1124 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1125 if self.wallet.account_is_pending(k):
1126 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1127 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1129 def delete_pending_account(self, k):
1130 self.wallet.delete_pending_account(k)
1131 self.update_receive_tab()
1133 def create_receive_menu(self, position):
1134 # fixme: this function apparently has a side effect.
1135 # if it is not called the menu pops up several times
1136 #self.receive_list.selectedIndexes()
1138 selected = self.receive_list.selectedItems()
1139 multi_select = len(selected) > 1
1140 addrs = [unicode(item.text(0)) for item in selected]
1141 if not multi_select:
1142 item = self.receive_list.itemAt(position)
1146 if not is_valid(addr):
1147 k = str(item.data(0,32).toString())
1149 self.create_account_menu(position, k, item)
1151 item.setExpanded(not item.isExpanded())
1155 if not multi_select:
1156 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1157 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1158 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1159 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1160 if not self.wallet.is_watching_only():
1161 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1162 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1163 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1164 if self.wallet.is_imported(addr):
1165 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1167 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1168 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1169 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1170 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1172 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1173 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1175 run_hook('receive_menu', menu, addrs)
1176 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1179 def get_sendable_balance(self):
1180 return sum(map(lambda x:x['value'], self.get_coins()))
1183 def get_coins(self):
1185 return self.pay_from
1187 domain = self.wallet.get_account_addresses(self.current_account)
1188 for i in self.wallet.frozen_addresses:
1189 if i in domain: domain.remove(i)
1190 return self.wallet.get_unspent_coins(domain)
1193 def send_from_addresses(self, addrs):
1194 self.set_pay_from( addrs )
1195 self.tabs.setCurrentIndex(1)
1198 def payto(self, addr):
1200 label = self.wallet.labels.get(addr)
1201 m_addr = label + ' <' + addr + '>' if label else addr
1202 self.tabs.setCurrentIndex(1)
1203 self.payto_e.setText(m_addr)
1204 self.amount_e.setFocus()
1207 def delete_contact(self, x):
1208 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1209 self.wallet.delete_contact(x)
1210 self.wallet.set_label(x, None)
1211 self.update_history_tab()
1212 self.update_contacts_tab()
1213 self.update_completions()
1216 def create_contact_menu(self, position):
1217 item = self.contacts_list.itemAt(position)
1220 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1222 addr = unicode(item.text(0))
1223 label = unicode(item.text(1))
1224 is_editable = item.data(0,32).toBool()
1225 payto_addr = item.data(0,33).toString()
1226 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1227 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1228 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1230 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1231 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1233 run_hook('create_contact_menu', menu, item)
1234 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1236 def delete_invoice(self, key):
1237 self.invoices.pop(key)
1238 self.wallet.storage.put('invoices', self.invoices)
1239 self.update_invoices_tab()
1241 def show_invoice(self, key):
1242 from electrum.paymentrequest import PaymentRequest
1243 domain, memo, value, status = self.invoices[key]
1244 pr = PaymentRequest(self.config)
1248 self.show_pr_details(pr)
1250 def show_pr_details(self, pr):
1251 msg = 'Domain: ' + pr.domain
1252 msg += '\nStatus: ' + pr.get_status()
1253 msg += '\nMemo: ' + pr.get_memo()
1254 msg += '\nPayment URL: ' + pr.payment_url
1255 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1256 QMessageBox.information(self, 'Invoice', msg , 'OK')
1258 def create_invoice_menu(self, position):
1259 item = self.invoices_list.itemAt(position)
1262 k = self.invoices_list.indexOfTopLevelItem(item)
1263 key = self.invoices.keys()[k]
1265 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1266 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1267 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1270 def update_receive_item(self, item):
1271 item.setFont(0, QFont(MONOSPACE_FONT))
1272 address = str(item.data(0,0).toString())
1273 label = self.wallet.labels.get(address,'')
1274 item.setData(1,0,label)
1275 item.setData(0,32, True) # is editable
1277 run_hook('update_receive_item', address, item)
1279 if not self.wallet.is_mine(address): return
1281 c, u = self.wallet.get_addr_balance(address)
1282 balance = self.format_amount(c + u)
1283 item.setData(2,0,balance)
1285 if address in self.wallet.frozen_addresses:
1286 item.setBackgroundColor(0, QColor('lightblue'))
1289 def update_receive_tab(self):
1290 l = self.receive_list
1291 # extend the syntax for consistency
1292 l.addChild = l.addTopLevelItem
1293 l.insertChild = l.insertTopLevelItem
1296 for i,width in enumerate(self.column_widths['receive']):
1297 l.setColumnWidth(i, width)
1299 accounts = self.wallet.get_accounts()
1300 if self.current_account is None:
1301 account_items = sorted(accounts.items())
1303 account_items = [(self.current_account, accounts.get(self.current_account))]
1306 for k, account in account_items:
1308 if len(accounts) > 1:
1309 name = self.wallet.get_account_name(k)
1310 c,u = self.wallet.get_account_balance(k)
1311 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1312 l.addTopLevelItem(account_item)
1313 account_item.setExpanded(self.accounts_expanded.get(k, True))
1314 account_item.setData(0, 32, k)
1318 sequences = [0,1] if account.has_change() else [0]
1319 for is_change in sequences:
1320 if len(sequences) > 1:
1321 name = _("Receiving") if not is_change else _("Change")
1322 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1323 account_item.addChild(seq_item)
1325 seq_item.setExpanded(True)
1327 seq_item = account_item
1329 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1335 for address in account.get_addresses(is_change):
1337 num, is_used = self.wallet.is_used(address)
1340 if gap > self.wallet.gap_limit:
1345 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1346 self.update_receive_item(item)
1348 item.setBackgroundColor(1, QColor('red'))
1352 seq_item.insertChild(0,used_item)
1354 used_item.addChild(item)
1356 seq_item.addChild(item)
1358 # we use column 1 because column 0 may be hidden
1359 l.setCurrentItem(l.topLevelItem(0),1)
1362 def update_contacts_tab(self):
1363 l = self.contacts_list
1366 for address in self.wallet.addressbook:
1367 label = self.wallet.labels.get(address,'')
1368 n = self.wallet.get_num_tx(address)
1369 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1370 item.setFont(0, QFont(MONOSPACE_FONT))
1371 # 32 = label can be edited (bool)
1372 item.setData(0,32, True)
1374 item.setData(0,33, address)
1375 l.addTopLevelItem(item)
1377 run_hook('update_contacts_tab', l)
1378 l.setCurrentItem(l.topLevelItem(0))
1382 def create_console_tab(self):
1383 from console import Console
1384 self.console = console = Console()
1388 def update_console(self):
1389 console = self.console
1390 console.history = self.config.get("console-history",[])
1391 console.history_index = len(console.history)
1393 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1394 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1396 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1398 def mkfunc(f, method):
1399 return lambda *args: apply( f, (method, args, self.password_dialog ))
1401 if m[0]=='_' or m in ['network','wallet']: continue
1402 methods[m] = mkfunc(c._run, m)
1404 console.updateNamespace(methods)
1407 def change_account(self,s):
1408 if s == _("All accounts"):
1409 self.current_account = None
1411 accounts = self.wallet.get_account_names()
1412 for k, v in accounts.items():
1414 self.current_account = k
1415 self.update_history_tab()
1416 self.update_status()
1417 self.update_receive_tab()
1419 def create_status_bar(self):
1422 sb.setFixedHeight(35)
1423 qtVersion = qVersion()
1425 self.balance_label = QLabel("")
1426 sb.addWidget(self.balance_label)
1428 from version_getter import UpdateLabel
1429 self.updatelabel = UpdateLabel(self.config, sb)
1431 self.account_selector = QComboBox()
1432 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1433 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1434 sb.addPermanentWidget(self.account_selector)
1436 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1437 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1439 self.lock_icon = QIcon()
1440 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1441 sb.addPermanentWidget( self.password_button )
1443 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1444 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1445 sb.addPermanentWidget( self.seed_button )
1446 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1447 sb.addPermanentWidget( self.status_button )
1449 run_hook('create_status_bar', (sb,))
1451 self.setStatusBar(sb)
1454 def update_lock_icon(self):
1455 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1456 self.password_button.setIcon( icon )
1459 def update_buttons_on_seed(self):
1460 if self.wallet.has_seed():
1461 self.seed_button.show()
1463 self.seed_button.hide()
1465 if not self.wallet.is_watching_only():
1466 self.password_button.show()
1467 self.send_button.setText(_("Send"))
1469 self.password_button.hide()
1470 self.send_button.setText(_("Create unsigned transaction"))
1473 def change_password_dialog(self):
1474 from password_dialog import PasswordDialog
1475 d = PasswordDialog(self.wallet, self)
1477 self.update_lock_icon()
1480 def new_contact_dialog(self):
1483 d.setWindowTitle(_("New Contact"))
1484 vbox = QVBoxLayout(d)
1485 vbox.addWidget(QLabel(_('New Contact')+':'))
1487 grid = QGridLayout()
1490 grid.addWidget(QLabel(_("Address")), 1, 0)
1491 grid.addWidget(line1, 1, 1)
1492 grid.addWidget(QLabel(_("Name")), 2, 0)
1493 grid.addWidget(line2, 2, 1)
1495 vbox.addLayout(grid)
1496 vbox.addLayout(ok_cancel_buttons(d))
1501 address = str(line1.text())
1502 label = unicode(line2.text())
1504 if not is_valid(address):
1505 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1508 self.wallet.add_contact(address)
1510 self.wallet.set_label(address, label)
1512 self.update_contacts_tab()
1513 self.update_history_tab()
1514 self.update_completions()
1515 self.tabs.setCurrentIndex(3)
1519 def new_account_dialog(self, password):
1521 dialog = QDialog(self)
1523 dialog.setWindowTitle(_("New Account"))
1525 vbox = QVBoxLayout()
1526 vbox.addWidget(QLabel(_('Account name')+':'))
1529 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1530 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1535 vbox.addLayout(ok_cancel_buttons(dialog))
1536 dialog.setLayout(vbox)
1540 name = str(e.text())
1543 self.wallet.create_pending_account(name, password)
1544 self.update_receive_tab()
1545 self.tabs.setCurrentIndex(2)
1550 def show_master_public_keys(self):
1552 dialog = QDialog(self)
1554 dialog.setWindowTitle(_("Master Public Keys"))
1556 main_layout = QGridLayout()
1557 mpk_dict = self.wallet.get_master_public_keys()
1559 for key, value in mpk_dict.items():
1560 main_layout.addWidget(QLabel(key), i, 0)
1561 mpk_text = QTextEdit()
1562 mpk_text.setReadOnly(True)
1563 mpk_text.setMaximumHeight(170)
1564 mpk_text.setText(value)
1565 main_layout.addWidget(mpk_text, i + 1, 0)
1568 vbox = QVBoxLayout()
1569 vbox.addLayout(main_layout)
1570 vbox.addLayout(close_button(dialog))
1572 dialog.setLayout(vbox)
1577 def show_seed_dialog(self, password):
1578 if not self.wallet.has_seed():
1579 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1583 mnemonic = self.wallet.get_mnemonic(password)
1585 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1587 from seed_dialog import SeedDialog
1588 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1593 def show_qrcode(self, data, title = _("QR code")):
1597 d.setWindowTitle(title)
1598 d.setMinimumSize(270, 300)
1599 vbox = QVBoxLayout()
1600 qrw = QRCodeWidget(data)
1601 vbox.addWidget(qrw, 1)
1602 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1603 hbox = QHBoxLayout()
1606 filename = os.path.join(self.config.path, "qrcode.bmp")
1609 bmp.save_qrcode(qrw.qr, filename)
1610 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1612 def copy_to_clipboard():
1613 bmp.save_qrcode(qrw.qr, filename)
1614 self.app.clipboard().setImage(QImage(filename))
1615 QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1617 b = QPushButton(_("Copy"))
1619 b.clicked.connect(copy_to_clipboard)
1621 b = QPushButton(_("Save"))
1623 b.clicked.connect(print_qr)
1625 b = QPushButton(_("Close"))
1627 b.clicked.connect(d.accept)
1630 vbox.addLayout(hbox)
1635 def do_protect(self, func, args):
1636 if self.wallet.use_encryption:
1637 password = self.password_dialog()
1643 if args != (False,):
1644 args = (self,) + args + (password,)
1646 args = (self,password)
1650 def show_public_keys(self, address):
1651 if not address: return
1653 pubkey_list = self.wallet.get_public_keys(address)
1654 except Exception as e:
1655 traceback.print_exc(file=sys.stdout)
1656 self.show_message(str(e))
1660 d.setMinimumSize(600, 200)
1662 vbox = QVBoxLayout()
1663 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1664 vbox.addWidget( QLabel(_("Public key") + ':'))
1666 keys.setReadOnly(True)
1667 keys.setText('\n'.join(pubkey_list))
1668 vbox.addWidget(keys)
1669 #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1670 vbox.addLayout(close_button(d))
1675 def show_private_key(self, address, password):
1676 if not address: return
1678 pk_list = self.wallet.get_private_key(address, password)
1679 except Exception as e:
1680 traceback.print_exc(file=sys.stdout)
1681 self.show_message(str(e))
1685 d.setMinimumSize(600, 200)
1687 vbox = QVBoxLayout()
1688 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1689 vbox.addWidget( QLabel(_("Private key") + ':'))
1691 keys.setReadOnly(True)
1692 keys.setText('\n'.join(pk_list))
1693 vbox.addWidget(keys)
1694 vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1695 vbox.addLayout(close_button(d))
1701 def do_sign(self, address, message, signature, password):
1702 message = unicode(message.toPlainText())
1703 message = message.encode('utf-8')
1705 sig = self.wallet.sign_message(str(address.text()), message, password)
1706 signature.setText(sig)
1707 except Exception as e:
1708 self.show_message(str(e))
1710 def do_verify(self, address, message, signature):
1711 message = unicode(message.toPlainText())
1712 message = message.encode('utf-8')
1713 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1714 self.show_message(_("Signature verified"))
1716 self.show_message(_("Error: wrong signature"))
1719 def sign_verify_message(self, address=''):
1722 d.setWindowTitle(_('Sign/verify Message'))
1723 d.setMinimumSize(410, 290)
1725 layout = QGridLayout(d)
1727 message_e = QTextEdit()
1728 layout.addWidget(QLabel(_('Message')), 1, 0)
1729 layout.addWidget(message_e, 1, 1)
1730 layout.setRowStretch(2,3)
1732 address_e = QLineEdit()
1733 address_e.setText(address)
1734 layout.addWidget(QLabel(_('Address')), 2, 0)
1735 layout.addWidget(address_e, 2, 1)
1737 signature_e = QTextEdit()
1738 layout.addWidget(QLabel(_('Signature')), 3, 0)
1739 layout.addWidget(signature_e, 3, 1)
1740 layout.setRowStretch(3,1)
1742 hbox = QHBoxLayout()
1744 b = QPushButton(_("Sign"))
1745 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1748 b = QPushButton(_("Verify"))
1749 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1752 b = QPushButton(_("Close"))
1753 b.clicked.connect(d.accept)
1755 layout.addLayout(hbox, 4, 1)
1760 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1762 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1763 message_e.setText(decrypted)
1764 except Exception as e:
1765 self.show_message(str(e))
1768 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1769 message = unicode(message_e.toPlainText())
1770 message = message.encode('utf-8')
1772 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1773 encrypted_e.setText(encrypted)
1774 except Exception as e:
1775 self.show_message(str(e))
1779 def encrypt_message(self, address = ''):
1782 d.setWindowTitle(_('Encrypt/decrypt Message'))
1783 d.setMinimumSize(610, 490)
1785 layout = QGridLayout(d)
1787 message_e = QTextEdit()
1788 layout.addWidget(QLabel(_('Message')), 1, 0)
1789 layout.addWidget(message_e, 1, 1)
1790 layout.setRowStretch(2,3)
1792 pubkey_e = QLineEdit()
1794 pubkey = self.wallet.getpubkeys(address)[0]
1795 pubkey_e.setText(pubkey)
1796 layout.addWidget(QLabel(_('Public key')), 2, 0)
1797 layout.addWidget(pubkey_e, 2, 1)
1799 encrypted_e = QTextEdit()
1800 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1801 layout.addWidget(encrypted_e, 3, 1)
1802 layout.setRowStretch(3,1)
1804 hbox = QHBoxLayout()
1805 b = QPushButton(_("Encrypt"))
1806 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1809 b = QPushButton(_("Decrypt"))
1810 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1813 b = QPushButton(_("Close"))
1814 b.clicked.connect(d.accept)
1817 layout.addLayout(hbox, 4, 1)
1821 def question(self, msg):
1822 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1824 def show_message(self, msg):
1825 QMessageBox.information(self, _('Message'), msg, _('OK'))
1827 def password_dialog(self, msg=None):
1830 d.setWindowTitle(_("Enter Password"))
1835 vbox = QVBoxLayout()
1837 msg = _('Please enter your password')
1838 vbox.addWidget(QLabel(msg))
1840 grid = QGridLayout()
1842 grid.addWidget(QLabel(_('Password')), 1, 0)
1843 grid.addWidget(pw, 1, 1)
1844 vbox.addLayout(grid)
1846 vbox.addLayout(ok_cancel_buttons(d))
1849 run_hook('password_dialog', pw, grid, 1)
1850 if not d.exec_(): return
1851 return unicode(pw.text())
1860 def tx_from_text(self, txt):
1861 "json or raw hexadecimal"
1864 tx = Transaction(txt)
1870 tx_dict = json.loads(str(txt))
1871 assert "hex" in tx_dict.keys()
1872 tx = Transaction(tx_dict["hex"])
1873 if tx_dict.has_key("input_info"):
1874 input_info = json.loads(tx_dict['input_info'])
1875 tx.add_input_info(input_info)
1878 traceback.print_exc(file=sys.stdout)
1881 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1885 def read_tx_from_file(self):
1886 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1890 with open(fileName, "r") as f:
1891 file_content = f.read()
1892 except (ValueError, IOError, os.error), reason:
1893 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1895 return self.tx_from_text(file_content)
1899 def sign_raw_transaction(self, tx, input_info, password):
1900 self.wallet.signrawtransaction(tx, input_info, [], password)
1902 def do_process_from_text(self):
1903 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1906 tx = self.tx_from_text(text)
1908 self.show_transaction(tx)
1910 def do_process_from_file(self):
1911 tx = self.read_tx_from_file()
1913 self.show_transaction(tx)
1915 def do_process_from_txid(self):
1916 from electrum import transaction
1917 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1919 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1921 tx = transaction.Transaction(r)
1923 self.show_transaction(tx)
1925 self.show_message("unknown transaction")
1927 def do_process_from_csvReader(self, csvReader):
1932 for position, row in enumerate(csvReader):
1934 if not is_valid(address):
1935 errors.append((position, address))
1937 amount = Decimal(row[1])
1938 amount = int(100000000*amount)
1939 outputs.append((address, amount))
1940 except (ValueError, IOError, os.error), reason:
1941 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1945 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1946 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1950 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1951 except Exception as e:
1952 self.show_message(str(e))
1955 self.show_transaction(tx)
1957 def do_process_from_csv_file(self):
1958 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1962 with open(fileName, "r") as f:
1963 csvReader = csv.reader(f)
1964 self.do_process_from_csvReader(csvReader)
1965 except (ValueError, IOError, os.error), reason:
1966 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1969 def do_process_from_csv_text(self):
1970 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1971 + _("Format: address, amount. One output per line"), _("Load CSV"))
1974 f = StringIO.StringIO(text)
1975 csvReader = csv.reader(f)
1976 self.do_process_from_csvReader(csvReader)
1981 def export_privkeys_dialog(self, password):
1982 if self.wallet.is_watching_only():
1983 self.show_message(_("This is a watching-only wallet"))
1987 d.setWindowTitle(_('Private keys'))
1988 d.setMinimumSize(850, 300)
1989 vbox = QVBoxLayout(d)
1991 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
1992 _("Exposing a single private key can compromise your entire wallet!"),
1993 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
1994 vbox.addWidget(QLabel(msg))
2000 defaultname = 'electrum-private-keys.csv'
2001 select_msg = _('Select file to export your private keys to')
2002 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2003 vbox.addLayout(hbox)
2005 h, b = ok_cancel_buttons2(d, _('Export'))
2010 addresses = self.wallet.addresses(True)
2012 def privkeys_thread():
2013 for addr in addresses:
2017 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2018 d.emit(SIGNAL('computing_privkeys'))
2019 d.emit(SIGNAL('show_privkeys'))
2021 def show_privkeys():
2022 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2026 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2027 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2028 threading.Thread(target=privkeys_thread).start()
2034 filename = filename_e.text()
2039 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2040 except (IOError, os.error), reason:
2041 export_error_label = _("Electrum was unable to produce a private key-export.")
2042 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2044 except Exception as e:
2045 self.show_message(str(e))
2048 self.show_message(_("Private keys exported."))
2051 def do_export_privkeys(self, fileName, pklist, is_csv):
2052 with open(fileName, "w+") as f:
2054 transaction = csv.writer(f)
2055 transaction.writerow(["address", "private_key"])
2056 for addr, pk in pklist.items():
2057 transaction.writerow(["%34s"%addr,pk])
2060 f.write(json.dumps(pklist, indent = 4))
2063 def do_import_labels(self):
2064 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2065 if not labelsFile: return
2067 f = open(labelsFile, 'r')
2070 for key, value in json.loads(data).items():
2071 self.wallet.set_label(key, value)
2072 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2073 except (IOError, os.error), reason:
2074 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2077 def do_export_labels(self):
2078 labels = self.wallet.labels
2080 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2082 with open(fileName, 'w+') as f:
2083 json.dump(labels, f)
2084 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2085 except (IOError, os.error), reason:
2086 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2089 def export_history_dialog(self):
2092 d.setWindowTitle(_('Export History'))
2093 d.setMinimumSize(400, 200)
2094 vbox = QVBoxLayout(d)
2096 defaultname = os.path.expanduser('~/electrum-history.csv')
2097 select_msg = _('Select file to export your wallet transactions to')
2099 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2100 vbox.addLayout(hbox)
2104 h, b = ok_cancel_buttons2(d, _('Export'))
2109 filename = filename_e.text()
2114 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2115 except (IOError, os.error), reason:
2116 export_error_label = _("Electrum was unable to produce a transaction export.")
2117 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2120 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2123 def do_export_history(self, wallet, fileName, is_csv):
2124 history = wallet.get_tx_history()
2126 for item in history:
2127 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2129 if timestamp is not None:
2131 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2132 except [RuntimeError, TypeError, NameError] as reason:
2133 time_string = "unknown"
2136 time_string = "unknown"
2138 time_string = "pending"
2140 if value is not None:
2141 value_string = format_satoshis(value, True)
2146 fee_string = format_satoshis(fee, True)
2151 label, is_default_label = wallet.get_label(tx_hash)
2152 label = label.encode('utf-8')
2156 balance_string = format_satoshis(balance, False)
2158 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2160 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2162 with open(fileName, "w+") as f:
2164 transaction = csv.writer(f)
2165 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2167 transaction.writerow(line)
2170 f.write(json.dumps(lines, indent = 4))
2173 def sweep_key_dialog(self):
2175 d.setWindowTitle(_('Sweep private keys'))
2176 d.setMinimumSize(600, 300)
2178 vbox = QVBoxLayout(d)
2179 vbox.addWidget(QLabel(_("Enter private keys")))
2181 keys_e = QTextEdit()
2182 keys_e.setTabChangesFocus(True)
2183 vbox.addWidget(keys_e)
2185 h, address_e = address_field(self.wallet.addresses())
2189 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2190 vbox.addLayout(hbox)
2191 button.setEnabled(False)
2194 addr = str(address_e.text())
2195 if bitcoin.is_address(addr):
2199 pk = str(keys_e.toPlainText()).strip()
2200 if Wallet.is_private_key(pk):
2203 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2204 keys_e.textChanged.connect(f)
2205 address_e.textChanged.connect(f)
2209 fee = self.wallet.fee
2210 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2211 self.show_transaction(tx)
2215 def do_import_privkey(self, password):
2216 if not self.wallet.has_imported_keys():
2217 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2218 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2219 + _('Are you sure you understand what you are doing?'), 3, 4)
2222 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2225 text = str(text).split()
2230 addr = self.wallet.import_key(key, password)
2231 except Exception as e:
2237 addrlist.append(addr)
2239 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2241 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2242 self.update_receive_tab()
2243 self.update_history_tab()
2246 def settings_dialog(self):
2248 d.setWindowTitle(_('Electrum Settings'))
2250 vbox = QVBoxLayout()
2251 grid = QGridLayout()
2252 grid.setColumnStretch(0,1)
2254 nz_label = QLabel(_('Display zeros') + ':')
2255 grid.addWidget(nz_label, 0, 0)
2256 nz_e = AmountEdit(None,True)
2257 nz_e.setText("%d"% self.num_zeros)
2258 grid.addWidget(nz_e, 0, 1)
2259 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2260 grid.addWidget(HelpButton(msg), 0, 2)
2261 if not self.config.is_modifiable('num_zeros'):
2262 for w in [nz_e, nz_label]: w.setEnabled(False)
2264 lang_label=QLabel(_('Language') + ':')
2265 grid.addWidget(lang_label, 1, 0)
2266 lang_combo = QComboBox()
2267 from electrum.i18n import languages
2268 lang_combo.addItems(languages.values())
2270 index = languages.keys().index(self.config.get("language",''))
2273 lang_combo.setCurrentIndex(index)
2274 grid.addWidget(lang_combo, 1, 1)
2275 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2276 if not self.config.is_modifiable('language'):
2277 for w in [lang_combo, lang_label]: w.setEnabled(False)
2280 fee_label = QLabel(_('Transaction fee') + ':')
2281 grid.addWidget(fee_label, 2, 0)
2282 fee_e = BTCAmountEdit(self.get_decimal_point)
2283 fee_e.setAmount(self.wallet.fee)
2284 grid.addWidget(fee_e, 2, 1)
2285 msg = _('Fee per kilobyte of transaction.') + '\n' \
2286 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2287 grid.addWidget(HelpButton(msg), 2, 2)
2288 if not self.config.is_modifiable('fee_per_kb'):
2289 for w in [fee_e, fee_label]: w.setEnabled(False)
2291 units = ['BTC', 'mBTC']
2292 unit_label = QLabel(_('Base unit') + ':')
2293 grid.addWidget(unit_label, 3, 0)
2294 unit_combo = QComboBox()
2295 unit_combo.addItems(units)
2296 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2297 grid.addWidget(unit_combo, 3, 1)
2298 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2299 + '\n1BTC=1000mBTC.\n' \
2300 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2302 usechange_cb = QCheckBox(_('Use change addresses'))
2303 usechange_cb.setChecked(self.wallet.use_change)
2304 grid.addWidget(usechange_cb, 4, 0)
2305 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2306 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2308 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2309 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2310 grid.addWidget(block_ex_label, 5, 0)
2311 block_ex_combo = QComboBox()
2312 block_ex_combo.addItems(block_explorers)
2313 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2314 grid.addWidget(block_ex_combo, 5, 1)
2315 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2317 show_tx = self.config.get('show_before_broadcast', False)
2318 showtx_cb = QCheckBox(_('Show before broadcast'))
2319 showtx_cb.setChecked(show_tx)
2320 grid.addWidget(showtx_cb, 6, 0)
2321 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2323 vbox.addLayout(grid)
2325 vbox.addLayout(ok_cancel_buttons(d))
2329 if not d.exec_(): return
2331 fee = fee_e.get_amount()
2333 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2336 self.wallet.set_fee(fee)
2338 nz = unicode(nz_e.text())
2343 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2346 if self.num_zeros != nz:
2348 self.config.set_key('num_zeros', nz, True)
2349 self.update_history_tab()
2350 self.update_receive_tab()
2352 usechange_result = usechange_cb.isChecked()
2353 if self.wallet.use_change != usechange_result:
2354 self.wallet.use_change = usechange_result
2355 self.wallet.storage.put('use_change', self.wallet.use_change)
2357 if showtx_cb.isChecked() != show_tx:
2358 self.config.set_key('show_before_broadcast', not show_tx)
2360 unit_result = units[unit_combo.currentIndex()]
2361 if self.base_unit() != unit_result:
2362 self.decimal_point = 8 if unit_result == 'BTC' else 5
2363 self.config.set_key('decimal_point', self.decimal_point, True)
2364 self.update_history_tab()
2365 self.update_status()
2367 need_restart = False
2369 lang_request = languages.keys()[lang_combo.currentIndex()]
2370 if lang_request != self.config.get('language'):
2371 self.config.set_key("language", lang_request, True)
2374 be_result = block_explorers[block_ex_combo.currentIndex()]
2375 self.config.set_key('block_explorer', be_result, True)
2377 run_hook('close_settings_dialog')
2380 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2383 def run_network_dialog(self):
2384 if not self.network:
2386 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2388 def closeEvent(self, event):
2390 self.config.set_key("is_maximized", self.isMaximized())
2391 if not self.isMaximized():
2393 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2394 self.save_column_widths()
2395 self.config.set_key("console-history", self.console.history[-50:], True)
2396 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2400 def plugins_dialog(self):
2401 from electrum.plugins import plugins
2404 d.setWindowTitle(_('Electrum Plugins'))
2407 vbox = QVBoxLayout(d)
2410 scroll = QScrollArea()
2411 scroll.setEnabled(True)
2412 scroll.setWidgetResizable(True)
2413 scroll.setMinimumSize(400,250)
2414 vbox.addWidget(scroll)
2418 w.setMinimumHeight(len(plugins)*35)
2420 grid = QGridLayout()
2421 grid.setColumnStretch(0,1)
2424 def do_toggle(cb, p, w):
2427 if w: w.setEnabled(r)
2429 def mk_toggle(cb, p, w):
2430 return lambda: do_toggle(cb,p,w)
2432 for i, p in enumerate(plugins):
2434 cb = QCheckBox(p.fullname())
2435 cb.setDisabled(not p.is_available())
2436 cb.setChecked(p.is_enabled())
2437 grid.addWidget(cb, i, 0)
2438 if p.requires_settings():
2439 w = p.settings_widget(self)
2440 w.setEnabled( p.is_enabled() )
2441 grid.addWidget(w, i, 1)
2444 cb.clicked.connect(mk_toggle(cb,p,w))
2445 grid.addWidget(HelpButton(p.description()), i, 2)
2447 print_msg(_("Error: cannot display plugin"), p)
2448 traceback.print_exc(file=sys.stdout)
2449 grid.setRowStretch(i+1,1)
2451 vbox.addLayout(close_button(d))
2456 def show_account_details(self, k):
2457 account = self.wallet.accounts[k]
2460 d.setWindowTitle(_('Account Details'))
2463 vbox = QVBoxLayout(d)
2464 name = self.wallet.get_account_name(k)
2465 label = QLabel('Name: ' + name)
2466 vbox.addWidget(label)
2468 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2470 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2472 vbox.addWidget(QLabel(_('Master Public Key:')))
2475 text.setReadOnly(True)
2476 text.setMaximumHeight(170)
2477 vbox.addWidget(text)
2479 mpk_text = '\n'.join( account.get_master_pubkeys() )
2480 text.setText(mpk_text)
2482 vbox.addLayout(close_button(d))