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.util 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 amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
45 from network_dialog import NetworkDialog
46 from qrcodewidget import QRCodeWidget, QRDialog
47 from qrtextedit import QRTextEdit
49 from decimal import Decimal
57 if platform.system() == 'Windows':
58 MONOSPACE_FONT = 'Lucida Console'
59 elif platform.system() == 'Darwin':
60 MONOSPACE_FONT = 'Monaco'
62 MONOSPACE_FONT = 'monospace'
66 # status of payment requests
69 PR_SENT = 2 # sent but not propagated
70 PR_PAID = 3 # send and propagated
71 PR_ERROR = 4 # could not parse
74 from electrum import ELECTRUM_VERSION
89 class StatusBarButton(QPushButton):
90 def __init__(self, icon, tooltip, func):
91 QPushButton.__init__(self, icon, '')
92 self.setToolTip(tooltip)
94 self.setMaximumWidth(25)
95 self.clicked.connect(func)
97 self.setIconSize(QSize(25,25))
99 def keyPressEvent(self, e):
100 if e.key() == QtCore.Qt.Key_Return:
112 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
114 class ElectrumWindow(QMainWindow):
118 def __init__(self, config, network, gui_object):
119 QMainWindow.__init__(self)
122 self.network = network
123 self.gui_object = gui_object
124 self.tray = gui_object.tray
125 self.go_lite = gui_object.go_lite
128 self.create_status_bar()
129 self.need_update = threading.Event()
131 self.decimal_point = config.get('decimal_point', 5)
132 self.num_zeros = int(config.get('num_zeros',0))
135 set_language(config.get('language'))
137 self.completions = QStringListModel()
139 self.tabs = tabs = QTabWidget(self)
140 self.column_widths = self.config.get("column_widths_2", default_column_widths )
141 tabs.addTab(self.create_history_tab(), _('History') )
142 tabs.addTab(self.create_send_tab(), _('Send') )
143 tabs.addTab(self.create_receive_tab(), _('Receive') )
144 tabs.addTab(self.create_addresses_tab(), _('Addresses') )
145 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
146 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
147 tabs.addTab(self.create_console_tab(), _('Console') )
148 tabs.setMinimumSize(600, 400)
149 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
150 self.setCentralWidget(tabs)
152 g = self.config.get("winpos-qt",[100, 100, 840, 400])
153 self.setGeometry(g[0], g[1], g[2], g[3])
154 if self.config.get("is_maximized"):
157 self.setWindowIcon(QIcon(":icons/electrum.png"))
160 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
161 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
162 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
163 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
164 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
166 for i in range(tabs.count()):
167 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
169 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
170 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
171 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
172 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
173 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
175 self.history_list.setFocus(True)
179 self.network.register_callback('updated', lambda: self.need_update.set())
180 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
181 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
182 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
183 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
185 # set initial message
186 self.console.showMessage(self.network.banner)
189 self.payment_request = None
191 def update_account_selector(self):
193 accounts = self.wallet.get_account_names()
194 self.account_selector.clear()
195 if len(accounts) > 1:
196 self.account_selector.addItems([_("All accounts")] + accounts.values())
197 self.account_selector.setCurrentIndex(0)
198 self.account_selector.show()
200 self.account_selector.hide()
203 def load_wallet(self, wallet):
207 self.update_wallet_format()
209 self.invoices = self.wallet.storage.get('invoices', {})
210 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
211 self.current_account = self.wallet.storage.get("current_account", None)
212 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
213 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
214 self.setWindowTitle( title )
216 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
217 self.notify_transactions()
218 self.update_account_selector()
220 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
221 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
222 self.password_menu.setEnabled(not self.wallet.is_watching_only())
223 self.seed_menu.setEnabled(self.wallet.has_seed())
224 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
225 self.import_menu.setEnabled(self.wallet.can_import())
227 self.update_lock_icon()
228 self.update_buttons_on_seed()
229 self.update_console()
231 self.clear_receive_tab()
232 self.update_receive_tab()
233 run_hook('load_wallet', wallet)
236 def update_wallet_format(self):
237 # convert old-format imported keys
238 if self.wallet.imported_keys:
239 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
241 self.wallet.convert_imported_keys(password)
243 self.show_message("error")
246 def open_wallet(self):
247 wallet_folder = self.wallet.storage.path
248 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
252 storage = WalletStorage({'wallet_path': filename})
253 if not storage.file_exists:
254 self.show_message("file not found "+ filename)
257 self.wallet.stop_threads()
260 wallet = Wallet(storage)
261 wallet.start_threads(self.network)
263 self.load_wallet(wallet)
267 def backup_wallet(self):
269 path = self.wallet.storage.path
270 wallet_folder = os.path.dirname(path)
271 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
275 new_path = os.path.join(wallet_folder, filename)
278 shutil.copy2(path, new_path)
279 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
280 except (IOError, os.error), reason:
281 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
284 def new_wallet(self):
287 wallet_folder = os.path.dirname(self.wallet.storage.path)
288 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
291 filename = os.path.join(wallet_folder, filename)
293 storage = WalletStorage({'wallet_path': filename})
294 if storage.file_exists:
295 QMessageBox.critical(None, "Error", _("File exists"))
298 wizard = installwizard.InstallWizard(self.config, self.network, storage)
299 wallet = wizard.run('new')
301 self.load_wallet(wallet)
305 def init_menubar(self):
308 file_menu = menubar.addMenu(_("&File"))
309 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
310 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
311 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
312 file_menu.addAction(_("&Quit"), self.close)
314 wallet_menu = menubar.addMenu(_("&Wallet"))
315 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
316 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
318 wallet_menu.addSeparator()
320 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
321 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
322 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
324 wallet_menu.addSeparator()
325 labels_menu = wallet_menu.addMenu(_("&Labels"))
326 labels_menu.addAction(_("&Import"), self.do_import_labels)
327 labels_menu.addAction(_("&Export"), self.do_export_labels)
329 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
330 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
331 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
332 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
333 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
335 tools_menu = menubar.addMenu(_("&Tools"))
337 # Settings / Preferences are all reserved keywords in OSX using this as work around
338 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
339 tools_menu.addAction(_("&Network"), self.run_network_dialog)
340 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
341 tools_menu.addSeparator()
342 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
343 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
344 tools_menu.addSeparator()
346 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
347 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
348 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
350 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
351 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
352 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
353 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
354 self.raw_transaction_menu = raw_transaction_menu
356 help_menu = menubar.addMenu(_("&Help"))
357 help_menu.addAction(_("&About"), self.show_about)
358 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
359 help_menu.addSeparator()
360 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
361 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
363 self.setMenuBar(menubar)
365 def show_about(self):
366 QMessageBox.about(self, "Electrum",
367 _("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."))
369 def show_report_bug(self):
370 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
371 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
374 def notify_transactions(self):
375 if not self.network or not self.network.is_connected():
378 print_error("Notifying GUI")
379 if len(self.network.pending_transactions_for_notifications) > 0:
380 # Combine the transactions if there are more then three
381 tx_amount = len(self.network.pending_transactions_for_notifications)
384 for tx in self.network.pending_transactions_for_notifications:
385 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
389 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
390 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
392 self.network.pending_transactions_for_notifications = []
394 for tx in self.network.pending_transactions_for_notifications:
396 self.network.pending_transactions_for_notifications.remove(tx)
397 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
399 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
401 def notify(self, message):
402 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
406 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
407 def getOpenFileName(self, title, filter = ""):
408 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
409 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
410 if fileName and directory != os.path.dirname(fileName):
411 self.config.set_key('io_dir', os.path.dirname(fileName), True)
414 def getSaveFileName(self, title, filename, filter = ""):
415 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
416 path = os.path.join( directory, filename )
417 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
418 if fileName and directory != os.path.dirname(fileName):
419 self.config.set_key('io_dir', os.path.dirname(fileName), True)
423 QMainWindow.close(self)
424 run_hook('close_main_window')
426 def connect_slots(self, sender):
427 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
428 self.previous_payto_e=''
430 def timer_actions(self):
431 if self.need_update.is_set():
433 self.need_update.clear()
435 run_hook('timer_actions')
437 def format_amount(self, x, is_diff=False, whitespaces=False):
438 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
441 def get_decimal_point(self):
442 return self.decimal_point
446 assert self.decimal_point in [5,8]
447 return "BTC" if self.decimal_point == 8 else "mBTC"
450 def update_status(self):
451 if self.network is None or not self.network.is_running():
453 icon = QIcon(":icons/status_disconnected.png")
455 elif self.network.is_connected():
456 if not self.wallet.up_to_date:
457 text = _("Synchronizing...")
458 icon = QIcon(":icons/status_waiting.png")
459 elif self.network.server_lag > 1:
460 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
461 icon = QIcon(":icons/status_lagging.png")
463 c, u = self.wallet.get_account_balance(self.current_account)
464 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
465 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
467 # append fiat balance and price from exchange rate plugin
469 run_hook('get_fiat_status_text', c+u, r)
474 self.tray.setToolTip(text)
475 icon = QIcon(":icons/status_connected.png")
477 text = _("Not connected")
478 icon = QIcon(":icons/status_disconnected.png")
480 self.balance_label.setText(text)
481 self.status_button.setIcon( icon )
484 def update_wallet(self):
486 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
487 self.update_history_tab()
488 self.update_receive_tab()
489 self.update_address_tab()
490 self.update_contacts_tab()
491 self.update_completions()
492 self.update_invoices_tab()
495 def create_history_tab(self):
496 self.history_list = l = MyTreeWidget(self)
498 for i,width in enumerate(self.column_widths['history']):
499 l.setColumnWidth(i, width)
500 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
501 l.itemDoubleClicked.connect(self.tx_label_clicked)
502 l.itemChanged.connect(self.tx_label_changed)
503 l.customContextMenuRequested.connect(self.create_history_menu)
507 def create_history_menu(self, position):
508 self.history_list.selectedIndexes()
509 item = self.history_list.currentItem()
510 be = self.config.get('block_explorer', 'Blockchain.info')
511 if be == 'Blockchain.info':
512 block_explorer = 'https://blockchain.info/tx/'
513 elif be == 'Blockr.io':
514 block_explorer = 'https://blockr.io/tx/info/'
515 elif be == 'Insight.is':
516 block_explorer = 'http://live.insight.is/tx/'
518 tx_hash = str(item.data(0, Qt.UserRole).toString())
519 if not tx_hash: return
521 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
522 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
523 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
524 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
525 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
528 def show_transaction(self, tx):
529 import transaction_dialog
530 d = transaction_dialog.TxDialog(tx, self)
533 def tx_label_clicked(self, item, column):
534 if column==2 and item.isSelected():
536 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
537 self.history_list.editItem( item, column )
538 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
541 def tx_label_changed(self, item, column):
545 tx_hash = str(item.data(0, Qt.UserRole).toString())
546 tx = self.wallet.transactions.get(tx_hash)
547 text = unicode( item.text(2) )
548 self.wallet.set_label(tx_hash, text)
550 item.setForeground(2, QBrush(QColor('black')))
552 text = self.wallet.get_default_label(tx_hash)
553 item.setText(2, text)
554 item.setForeground(2, QBrush(QColor('gray')))
558 def edit_label(self, is_recv):
559 l = self.address_list if is_recv else self.contacts_list
560 item = l.currentItem()
561 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
562 l.editItem( item, 1 )
563 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
567 def address_label_clicked(self, item, column, l, column_addr, column_label):
568 if column == column_label and item.isSelected():
569 is_editable = item.data(0, 32).toBool()
572 addr = unicode( item.text(column_addr) )
573 label = unicode( item.text(column_label) )
574 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
575 l.editItem( item, column )
576 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
579 def address_label_changed(self, item, column, l, column_addr, column_label):
580 if column == column_label:
581 addr = unicode( item.text(column_addr) )
582 text = unicode( item.text(column_label) )
583 is_editable = item.data(0, 32).toBool()
587 changed = self.wallet.set_label(addr, text)
589 self.update_history_tab()
590 self.update_completions()
592 self.current_item_changed(item)
594 run_hook('item_changed', item, column)
597 def current_item_changed(self, a):
598 run_hook('current_item_changed', a)
602 def update_history_tab(self):
604 self.history_list.clear()
605 for item in self.wallet.get_tx_history(self.current_account):
606 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
607 time_str = _("unknown")
610 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
612 time_str = _("error")
615 time_str = 'unverified'
616 icon = QIcon(":icons/unconfirmed.png")
619 icon = QIcon(":icons/unconfirmed.png")
621 icon = QIcon(":icons/clock%d.png"%conf)
623 icon = QIcon(":icons/confirmed.png")
625 if value is not None:
626 v_str = self.format_amount(value, True, whitespaces=True)
630 balance_str = self.format_amount(balance, whitespaces=True)
633 label, is_default_label = self.wallet.get_label(tx_hash)
635 label = _('Pruned transaction outputs')
636 is_default_label = False
638 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
639 item.setFont(2, QFont(MONOSPACE_FONT))
640 item.setFont(3, QFont(MONOSPACE_FONT))
641 item.setFont(4, QFont(MONOSPACE_FONT))
643 item.setForeground(3, QBrush(QColor("#BC1E1E")))
645 item.setData(0, Qt.UserRole, tx_hash)
646 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
648 item.setForeground(2, QBrush(QColor('grey')))
650 item.setIcon(0, icon)
651 self.history_list.insertTopLevelItem(0,item)
654 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
655 run_hook('history_tab_update')
658 def create_receive_tab(self):
660 grid = QGridLayout(w)
661 grid.setColumnMinimumWidth(3, 300)
662 grid.setColumnStretch(5, 1)
664 self.receive_address_e = QLineEdit()
665 self.receive_address_e.setReadOnly(True)
666 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
667 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
668 self.receive_address_e.textChanged.connect(self.update_receive_qr)
670 self.receive_message_e = QLineEdit()
671 grid.addWidget(QLabel(_('Message')), 1, 0)
672 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
673 self.receive_message_e.textChanged.connect(self.update_receive_qr)
675 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
676 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
677 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
678 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
680 self.save_request_button = QPushButton(_('Save'))
681 self.save_request_button.clicked.connect(self.save_payment_request)
682 grid.addWidget(self.save_request_button, 3, 1)
683 clear_button = QPushButton(_('New'))
684 clear_button.clicked.connect(self.clear_receive_tab)
685 grid.addWidget(clear_button, 3, 2)
686 grid.setRowStretch(4, 1)
688 self.receive_qr = QRCodeWidget(fixedSize=200)
689 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
691 grid.setRowStretch(5, 1)
693 self.receive_requests_label = QLabel(_('Saved Requests'))
694 self.receive_list = MyTreeWidget(self)
695 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
696 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
697 self.receive_list.itemClicked.connect(self.receive_item_changed)
698 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount')] )
699 self.receive_list.setColumnWidth(0, 340)
700 h = self.receive_list.header()
701 h.setStretchLastSection(False)
702 h.setResizeMode(1, QHeaderView.Stretch)
704 grid.addWidget(self.receive_requests_label, 6, 0)
705 grid.addWidget(self.receive_list, 7, 0, 1, 6)
708 def receive_item_changed(self, item):
711 addr = str(item.text(0))
712 amount, message = self.receive_requests[addr]
713 self.receive_address_e.setText(addr)
714 self.receive_message_e.setText(message)
715 self.receive_amount_e.setAmount(amount)
718 def receive_list_delete(self, item):
719 addr = str(item.text(0))
720 self.receive_requests.pop(addr)
721 self.update_receive_tab()
722 self.clear_receive_tab()
724 def receive_list_menu(self, position):
725 item = self.receive_list.itemAt(position)
727 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
728 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
730 def save_payment_request(self):
731 addr = str(self.receive_address_e.text())
732 amount = self.receive_amount_e.get_amount()
733 message = str(self.receive_message_e.text())
734 if not message and not amount:
735 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
737 self.receive_requests = self.wallet.storage.get('receive_requests',{})
738 self.receive_requests[addr] = (amount, message)
739 self.wallet.storage.put('receive_requests', self.receive_requests)
740 self.update_receive_tab()
742 def clear_receive_tab(self):
743 self.receive_requests = self.wallet.storage.get('receive_requests',{})
744 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
746 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
750 self.receive_address_e.setText(addr)
751 self.receive_message_e.setText('')
752 self.receive_amount_e.setAmount(None)
754 def receive_at(self, addr):
755 if not bitcoin.is_address(addr):
757 self.tabs.setCurrentIndex(2)
758 self.receive_address_e.setText(addr)
760 def update_receive_tab(self):
761 self.receive_requests = self.wallet.storage.get('receive_requests',{})
762 b = len(self.receive_requests) > 0
763 self.receive_list.setVisible(b)
764 self.receive_requests_label.setVisible(b)
766 self.receive_list.clear()
767 for address, v in self.receive_requests.items():
769 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else ""] )
770 item.setFont(0, QFont(MONOSPACE_FONT))
771 self.receive_list.addTopLevelItem(item)
774 def update_receive_qr(self):
775 import urlparse, urllib
776 addr = str(self.receive_address_e.text())
777 amount = self.receive_amount_e.get_amount()
778 message = unicode(self.receive_message_e.text()).encode('utf8')
779 self.save_request_button.setEnabled((amount is not None) or (message != ""))
783 query.append('amount=%s'%format_satoshis(amount))
785 query.append('message=%s'%urllib.quote(message))
786 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
787 url = urlparse.urlunparse(p)
790 self.receive_qr.setData(url)
791 run_hook('update_receive_qr', addr, amount, message, url)
794 def create_send_tab(self):
797 self.send_grid = grid = QGridLayout(w)
799 grid.setColumnMinimumWidth(3,300)
800 grid.setColumnStretch(5,1)
801 grid.setRowStretch(8, 1)
803 from paytoedit import PayToEdit
804 self.amount_e = BTCAmountEdit(self.get_decimal_point)
805 self.payto_e = PayToEdit(self)
806 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)'))
807 grid.addWidget(QLabel(_('Pay to')), 1, 0)
808 grid.addWidget(self.payto_e, 1, 1, 1, 3)
809 grid.addWidget(self.payto_help, 1, 4)
811 completer = QCompleter()
812 completer.setCaseSensitivity(False)
813 self.payto_e.setCompleter(completer)
814 completer.setModel(self.completions)
816 self.message_e = MyLineEdit()
817 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.'))
818 grid.addWidget(QLabel(_('Description')), 2, 0)
819 grid.addWidget(self.message_e, 2, 1, 1, 3)
820 grid.addWidget(self.message_help, 2, 4)
822 self.from_label = QLabel(_('From'))
823 grid.addWidget(self.from_label, 3, 0)
824 self.from_list = MyTreeWidget(self)
825 self.from_list.setColumnCount(2)
826 self.from_list.setColumnWidth(0, 350)
827 self.from_list.setColumnWidth(1, 50)
828 self.from_list.setHeaderHidden(True)
829 self.from_list.setMaximumHeight(80)
830 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
831 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
832 grid.addWidget(self.from_list, 3, 1, 1, 3)
833 self.set_pay_from([])
835 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
836 + _('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.') \
837 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
838 grid.addWidget(QLabel(_('Amount')), 4, 0)
839 grid.addWidget(self.amount_e, 4, 1, 1, 2)
840 grid.addWidget(self.amount_help, 4, 3)
842 self.fee_e = BTCAmountEdit(self.get_decimal_point)
843 grid.addWidget(QLabel(_('Fee')), 5, 0)
844 grid.addWidget(self.fee_e, 5, 1, 1, 2)
845 grid.addWidget(HelpButton(
846 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
847 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
848 + _('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)
850 self.send_button = EnterButton(_("Send"), self.do_send)
851 grid.addWidget(self.send_button, 6, 1)
853 b = EnterButton(_("Clear"), self.do_clear)
854 grid.addWidget(b, 6, 2)
856 self.payto_sig = QLabel('')
857 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
859 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
860 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
863 def entry_changed( is_fee ):
865 if self.amount_e.is_shortcut:
866 self.amount_e.is_shortcut = False
867 sendable = self.get_sendable_balance()
868 # there is only one output because we are completely spending inputs
869 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
870 fee = self.wallet.estimated_fee(inputs, 1)
872 self.amount_e.setAmount(amount)
873 self.amount_e.textEdited.emit("")
874 self.fee_e.setAmount(fee)
877 amount = self.amount_e.get_amount()
878 fee = self.fee_e.get_amount()
880 if not is_fee: fee = None
882 self.fee_e.setAmount(None)
884 # assume that there will be 2 outputs (one for change)
885 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
887 self.fee_e.setAmount(fee)
890 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
894 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
895 text = _( "Not enough funds" )
896 c, u = self.wallet.get_frozen_balance()
897 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
899 self.statusBar().showMessage(text)
900 self.amount_e.setPalette(palette)
901 self.fee_e.setPalette(palette)
903 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
904 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
906 run_hook('create_send_tab', grid)
909 def from_list_delete(self, item):
910 i = self.from_list.indexOfTopLevelItem(item)
912 self.redraw_from_list()
914 def from_list_menu(self, position):
915 item = self.from_list.itemAt(position)
917 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
918 menu.exec_(self.from_list.viewport().mapToGlobal(position))
920 def set_pay_from(self, domain = None):
921 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
922 self.redraw_from_list()
924 def redraw_from_list(self):
925 self.from_list.clear()
926 self.from_label.setHidden(len(self.pay_from) == 0)
927 self.from_list.setHidden(len(self.pay_from) == 0)
930 h = x.get('prevout_hash')
931 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
933 for item in self.pay_from:
934 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
936 def update_completions(self):
938 for addr,label in self.wallet.labels.items():
939 if addr in self.wallet.addressbook:
940 l.append( label + ' <' + addr + '>')
942 run_hook('update_completions', l)
943 self.completions.setStringList(l)
947 return lambda s, *args: s.do_protect(func, args)
950 def read_send_tab(self):
952 if self.payment_request and self.payment_request.has_expired():
953 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
956 label = unicode( self.message_e.text() )
958 if self.payment_request:
959 outputs = self.payment_request.get_outputs()
961 outputs = self.payto_e.get_outputs()
964 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
967 for addr, x in outputs:
968 if addr is None or not bitcoin.is_address(addr):
969 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
972 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
975 amount = sum(map(lambda x:x[1], outputs))
977 fee = self.fee_e.get_amount()
979 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
982 confirm_amount = self.config.get('confirm_amount', 100000000)
983 if amount >= confirm_amount:
984 o = '\n'.join(map(lambda x:x[0], outputs))
985 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
988 confirm_fee = self.config.get('confirm_fee', 100000)
989 if fee >= confirm_fee:
990 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()}):
993 coins = self.get_coins()
994 return outputs, fee, label, coins
998 r = self.read_send_tab()
1001 outputs, fee, label, coins = r
1002 self.send_tx(outputs, fee, label, coins)
1006 def send_tx(self, outputs, fee, label, coins, password):
1007 self.send_button.setDisabled(True)
1009 # first, create an unsigned tx
1011 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1013 except Exception as e:
1014 traceback.print_exc(file=sys.stdout)
1015 self.show_message(str(e))
1016 self.send_button.setDisabled(False)
1019 # call hook to see if plugin needs gui interaction
1020 run_hook('send_tx', tx)
1026 self.wallet.add_keypairs(tx, keypairs, password)
1027 self.wallet.sign_transaction(tx, keypairs, password)
1028 except Exception as e:
1034 self.show_message(tx.error)
1035 self.send_button.setDisabled(False)
1037 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1038 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1039 self.send_button.setDisabled(False)
1042 self.wallet.set_label(tx.hash(), label)
1044 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1045 self.show_transaction(tx)
1047 self.send_button.setDisabled(False)
1050 self.broadcast_transaction(tx)
1052 # keep a reference to WaitingDialog or the gui might crash
1053 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1054 self.waiting_dialog.start()
1058 def broadcast_transaction(self, tx):
1060 def broadcast_thread():
1061 pr = self.payment_request
1063 return self.wallet.sendtx(tx)
1065 if pr.has_expired():
1066 self.payment_request = None
1067 return False, _("Payment request has expired")
1069 status, msg = self.wallet.sendtx(tx)
1073 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1074 self.wallet.storage.put('invoices', self.invoices)
1075 self.update_invoices_tab()
1076 self.payment_request = None
1077 refund_address = self.wallet.addresses()[0]
1078 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1084 def broadcast_done(status, msg):
1086 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1089 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1090 self.send_button.setDisabled(False)
1092 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1093 self.waiting_dialog.start()
1097 def prepare_for_payment_request(self):
1098 self.tabs.setCurrentIndex(1)
1099 self.payto_e.is_pr = True
1100 for e in [self.payto_e, self.amount_e, self.message_e]:
1102 for h in [self.payto_help, self.amount_help, self.message_help]:
1104 self.payto_e.setText(_("please wait..."))
1107 def payment_request_ok(self):
1108 pr = self.payment_request
1110 if pr_id not in self.invoices:
1111 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1112 self.wallet.storage.put('invoices', self.invoices)
1113 self.update_invoices_tab()
1115 print_error('invoice already in list')
1117 status = self.invoices[pr_id][4]
1118 if status == PR_PAID:
1120 self.show_message("invoice already paid")
1121 self.payment_request = None
1124 self.payto_help.show()
1125 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1127 if not pr.has_expired():
1128 self.payto_e.setGreen()
1130 self.payto_e.setExpired()
1132 self.payto_e.setText(pr.domain)
1133 self.amount_e.setText(self.format_amount(pr.get_amount()))
1134 self.message_e.setText(pr.get_memo())
1136 def payment_request_error(self):
1138 self.show_message(self.payment_request.error)
1139 self.payment_request = None
1141 def pay_from_URI(self,URI):
1144 address, amount, label, message, request_url = util.parse_URI(URI)
1146 address, amount, label, message, request_url = util.parse_URI(URI)
1147 except Exception as e:
1148 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1151 self.tabs.setCurrentIndex(1)
1155 if self.wallet.labels.get(address) != label:
1156 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1157 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1158 self.wallet.addressbook.append(address)
1159 self.wallet.set_label(address, label)
1161 label = self.wallet.labels.get(address)
1163 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1165 self.message_e.setText(message)
1167 self.amount_e.setAmount(amount)
1170 from electrum import paymentrequest
1171 def payment_request():
1172 self.payment_request = paymentrequest.PaymentRequest(self.config)
1173 self.payment_request.read(request_url)
1174 if self.payment_request.verify():
1175 self.emit(SIGNAL('payment_request_ok'))
1177 self.emit(SIGNAL('payment_request_error'))
1179 self.pr_thread = threading.Thread(target=payment_request).start()
1180 self.prepare_for_payment_request()
1185 self.payto_e.is_pr = False
1186 self.payto_sig.setVisible(False)
1187 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1191 for h in [self.payto_help, self.amount_help, self.message_help]:
1194 self.payto_help.set_alt(None)
1195 self.set_pay_from([])
1196 self.update_status()
1200 def set_addrs_frozen(self,addrs,freeze):
1202 if not addr: continue
1203 if addr in self.wallet.frozen_addresses and not freeze:
1204 self.wallet.unfreeze(addr)
1205 elif addr not in self.wallet.frozen_addresses and freeze:
1206 self.wallet.freeze(addr)
1207 self.update_address_tab()
1211 def create_list_tab(self, headers):
1212 "generic tab creation method"
1213 l = MyTreeWidget(self)
1214 l.setColumnCount( len(headers) )
1215 l.setHeaderLabels( headers )
1218 vbox = QVBoxLayout()
1225 vbox.addWidget(buttons)
1230 def create_addresses_tab(self):
1231 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1232 for i,width in enumerate(self.column_widths['receive']):
1233 l.setColumnWidth(i, width)
1234 l.setContextMenuPolicy(Qt.CustomContextMenu)
1235 l.customContextMenuRequested.connect(self.create_receive_menu)
1236 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1237 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1238 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1239 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1240 self.address_list = l
1246 def save_column_widths(self):
1247 self.column_widths["receive"] = []
1248 for i in range(self.address_list.columnCount() -1):
1249 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1251 self.column_widths["history"] = []
1252 for i in range(self.history_list.columnCount() - 1):
1253 self.column_widths["history"].append(self.history_list.columnWidth(i))
1255 self.column_widths["contacts"] = []
1256 for i in range(self.contacts_list.columnCount() - 1):
1257 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1259 self.config.set_key("column_widths_2", self.column_widths, True)
1262 def create_contacts_tab(self):
1263 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1264 l.setContextMenuPolicy(Qt.CustomContextMenu)
1265 l.customContextMenuRequested.connect(self.create_contact_menu)
1266 for i,width in enumerate(self.column_widths['contacts']):
1267 l.setColumnWidth(i, width)
1268 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1269 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1270 self.contacts_list = l
1274 def create_invoices_tab(self):
1275 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1276 l.setColumnWidth(0, 150)
1278 h.setStretchLastSection(False)
1279 h.setResizeMode(1, QHeaderView.Stretch)
1280 l.setContextMenuPolicy(Qt.CustomContextMenu)
1281 l.customContextMenuRequested.connect(self.create_invoice_menu)
1282 self.invoices_list = l
1285 def update_invoices_tab(self):
1286 invoices = self.wallet.storage.get('invoices', {})
1287 l = self.invoices_list
1289 for key, value in invoices.items():
1291 domain, memo, amount, expiration_date, status, tx_hash = value
1295 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1297 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1298 l.addTopLevelItem(item)
1300 l.setCurrentItem(l.topLevelItem(0))
1304 def delete_imported_key(self, addr):
1305 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1306 self.wallet.delete_imported_key(addr)
1307 self.update_address_tab()
1308 self.update_history_tab()
1310 def edit_account_label(self, k):
1311 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1313 label = unicode(text)
1314 self.wallet.set_label(k,label)
1315 self.update_address_tab()
1317 def account_set_expanded(self, item, k, b):
1319 self.accounts_expanded[k] = b
1321 def create_account_menu(self, position, k, item):
1323 if item.isExpanded():
1324 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1326 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1327 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1328 if self.wallet.seed_version > 4:
1329 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1330 if self.wallet.account_is_pending(k):
1331 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1332 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1334 def delete_pending_account(self, k):
1335 self.wallet.delete_pending_account(k)
1336 self.update_address_tab()
1338 def create_receive_menu(self, position):
1339 # fixme: this function apparently has a side effect.
1340 # if it is not called the menu pops up several times
1341 #self.address_list.selectedIndexes()
1343 selected = self.address_list.selectedItems()
1344 multi_select = len(selected) > 1
1345 addrs = [unicode(item.text(0)) for item in selected]
1346 if not multi_select:
1347 item = self.address_list.itemAt(position)
1351 if not is_valid(addr):
1352 k = str(item.data(0,32).toString())
1354 self.create_account_menu(position, k, item)
1356 item.setExpanded(not item.isExpanded())
1360 if not multi_select:
1361 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1362 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1363 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1364 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1365 if not self.wallet.is_watching_only():
1366 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1367 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1368 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1369 if self.wallet.is_imported(addr):
1370 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1372 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1373 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1374 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1375 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1378 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1379 if any(can_send(addr) for addr in addrs):
1380 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1382 run_hook('receive_menu', menu, addrs)
1383 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1386 def get_sendable_balance(self):
1387 return sum(map(lambda x:x['value'], self.get_coins()))
1390 def get_coins(self):
1392 return self.pay_from
1394 domain = self.wallet.get_account_addresses(self.current_account)
1395 for i in self.wallet.frozen_addresses:
1396 if i in domain: domain.remove(i)
1397 return self.wallet.get_unspent_coins(domain)
1400 def send_from_addresses(self, addrs):
1401 self.set_pay_from( addrs )
1402 self.tabs.setCurrentIndex(1)
1405 def payto(self, addr):
1407 label = self.wallet.labels.get(addr)
1408 m_addr = label + ' <' + addr + '>' if label else addr
1409 self.tabs.setCurrentIndex(1)
1410 self.payto_e.setText(m_addr)
1411 self.amount_e.setFocus()
1414 def delete_contact(self, x):
1415 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1416 self.wallet.delete_contact(x)
1417 self.wallet.set_label(x, None)
1418 self.update_history_tab()
1419 self.update_contacts_tab()
1420 self.update_completions()
1423 def create_contact_menu(self, position):
1424 item = self.contacts_list.itemAt(position)
1427 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1429 addr = unicode(item.text(0))
1430 label = unicode(item.text(1))
1431 is_editable = item.data(0,32).toBool()
1432 payto_addr = item.data(0,33).toString()
1433 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1434 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1435 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1437 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1438 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1440 run_hook('create_contact_menu', menu, item)
1441 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1443 def delete_invoice(self, key):
1444 self.invoices.pop(key)
1445 self.wallet.storage.put('invoices', self.invoices)
1446 self.update_invoices_tab()
1448 def show_invoice(self, key):
1449 from electrum.paymentrequest import PaymentRequest
1450 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1451 pr = PaymentRequest(self.config)
1455 self.show_pr_details(pr)
1457 def show_pr_details(self, pr):
1458 msg = 'Domain: ' + pr.domain
1459 msg += '\nStatus: ' + pr.get_status()
1460 msg += '\nMemo: ' + pr.get_memo()
1461 msg += '\nPayment URL: ' + pr.payment_url
1462 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1463 QMessageBox.information(self, 'Invoice', msg , 'OK')
1465 def do_pay_invoice(self, key):
1466 from electrum.paymentrequest import PaymentRequest
1467 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1468 pr = PaymentRequest(self.config)
1471 self.payment_request = pr
1472 self.prepare_for_payment_request()
1474 self.payment_request_ok()
1476 self.payment_request_error()
1479 def create_invoice_menu(self, position):
1480 item = self.invoices_list.itemAt(position)
1483 k = self.invoices_list.indexOfTopLevelItem(item)
1484 key = self.invoices.keys()[k]
1485 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1487 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1488 if status == PR_UNPAID:
1489 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1490 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1491 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1495 def update_address_tab(self):
1496 l = self.address_list
1497 # extend the syntax for consistency
1498 l.addChild = l.addTopLevelItem
1499 l.insertChild = l.insertTopLevelItem
1503 accounts = self.wallet.get_accounts()
1504 if self.current_account is None:
1505 account_items = sorted(accounts.items())
1507 account_items = [(self.current_account, accounts.get(self.current_account))]
1510 for k, account in account_items:
1512 if len(accounts) > 1:
1513 name = self.wallet.get_account_name(k)
1514 c,u = self.wallet.get_account_balance(k)
1515 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1516 l.addTopLevelItem(account_item)
1517 account_item.setExpanded(self.accounts_expanded.get(k, True))
1518 account_item.setData(0, 32, k)
1522 sequences = [0,1] if account.has_change() else [0]
1523 for is_change in sequences:
1524 if len(sequences) > 1:
1525 name = _("Receiving") if not is_change else _("Change")
1526 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1527 account_item.addChild(seq_item)
1529 seq_item.setExpanded(True)
1531 seq_item = account_item
1533 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1536 addr_list = account.get_addresses(is_change)
1537 for address in addr_list:
1538 num, is_used = self.wallet.is_used(address)
1539 label = self.wallet.labels.get(address,'')
1540 c, u = self.wallet.get_addr_balance(address)
1541 balance = self.format_amount(c + u)
1542 item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1543 item.setFont(0, QFont(MONOSPACE_FONT))
1544 item.setData(0, 32, True) # label can be edited
1545 if address in self.wallet.frozen_addresses:
1546 item.setBackgroundColor(0, QColor('lightblue'))
1547 if self.wallet.is_beyond_limit(address, account, is_change):
1548 item.setBackgroundColor(0, QColor('red'))
1551 seq_item.insertChild(0, used_item)
1553 used_item.addChild(item)
1555 seq_item.addChild(item)
1557 # we use column 1 because column 0 may be hidden
1558 l.setCurrentItem(l.topLevelItem(0),1)
1561 def update_contacts_tab(self):
1562 l = self.contacts_list
1565 for address in self.wallet.addressbook:
1566 label = self.wallet.labels.get(address,'')
1567 n = self.wallet.get_num_tx(address)
1568 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1569 item.setFont(0, QFont(MONOSPACE_FONT))
1570 # 32 = label can be edited (bool)
1571 item.setData(0,32, True)
1573 item.setData(0,33, address)
1574 l.addTopLevelItem(item)
1576 run_hook('update_contacts_tab', l)
1577 l.setCurrentItem(l.topLevelItem(0))
1580 def create_console_tab(self):
1581 from console import Console
1582 self.console = console = Console()
1586 def update_console(self):
1587 console = self.console
1588 console.history = self.config.get("console-history",[])
1589 console.history_index = len(console.history)
1591 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1592 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1594 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1596 def mkfunc(f, method):
1597 return lambda *args: apply( f, (method, args, self.password_dialog ))
1599 if m[0]=='_' or m in ['network','wallet']: continue
1600 methods[m] = mkfunc(c._run, m)
1602 console.updateNamespace(methods)
1605 def change_account(self,s):
1606 if s == _("All accounts"):
1607 self.current_account = None
1609 accounts = self.wallet.get_account_names()
1610 for k, v in accounts.items():
1612 self.current_account = k
1613 self.update_history_tab()
1614 self.update_status()
1615 self.update_address_tab()
1616 self.update_receive_tab()
1618 def create_status_bar(self):
1621 sb.setFixedHeight(35)
1622 qtVersion = qVersion()
1624 self.balance_label = QLabel("")
1625 sb.addWidget(self.balance_label)
1627 from version_getter import UpdateLabel
1628 self.updatelabel = UpdateLabel(self.config, sb)
1630 self.account_selector = QComboBox()
1631 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1632 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1633 sb.addPermanentWidget(self.account_selector)
1635 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1636 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1638 self.lock_icon = QIcon()
1639 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1640 sb.addPermanentWidget( self.password_button )
1642 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1643 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1644 sb.addPermanentWidget( self.seed_button )
1645 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1646 sb.addPermanentWidget( self.status_button )
1648 run_hook('create_status_bar', (sb,))
1650 self.setStatusBar(sb)
1653 def update_lock_icon(self):
1654 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1655 self.password_button.setIcon( icon )
1658 def update_buttons_on_seed(self):
1659 if self.wallet.has_seed():
1660 self.seed_button.show()
1662 self.seed_button.hide()
1664 if not self.wallet.is_watching_only():
1665 self.password_button.show()
1666 self.send_button.setText(_("Send"))
1668 self.password_button.hide()
1669 self.send_button.setText(_("Create unsigned transaction"))
1672 def change_password_dialog(self):
1673 from password_dialog import PasswordDialog
1674 d = PasswordDialog(self.wallet, self)
1676 self.update_lock_icon()
1679 def new_contact_dialog(self):
1682 d.setWindowTitle(_("New Contact"))
1683 vbox = QVBoxLayout(d)
1684 vbox.addWidget(QLabel(_('New Contact')+':'))
1686 grid = QGridLayout()
1689 grid.addWidget(QLabel(_("Address")), 1, 0)
1690 grid.addWidget(line1, 1, 1)
1691 grid.addWidget(QLabel(_("Name")), 2, 0)
1692 grid.addWidget(line2, 2, 1)
1694 vbox.addLayout(grid)
1695 vbox.addLayout(ok_cancel_buttons(d))
1700 address = str(line1.text())
1701 label = unicode(line2.text())
1703 if not is_valid(address):
1704 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1707 self.wallet.add_contact(address)
1709 self.wallet.set_label(address, label)
1711 self.update_contacts_tab()
1712 self.update_history_tab()
1713 self.update_completions()
1714 self.tabs.setCurrentIndex(3)
1718 def new_account_dialog(self, password):
1720 dialog = QDialog(self)
1722 dialog.setWindowTitle(_("New Account"))
1724 vbox = QVBoxLayout()
1725 vbox.addWidget(QLabel(_('Account name')+':'))
1728 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1729 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1734 vbox.addLayout(ok_cancel_buttons(dialog))
1735 dialog.setLayout(vbox)
1739 name = str(e.text())
1742 self.wallet.create_pending_account(name, password)
1743 self.update_address_tab()
1744 self.tabs.setCurrentIndex(2)
1749 def show_master_public_keys(self):
1751 dialog = QDialog(self)
1753 dialog.setWindowTitle(_("Master Public Keys"))
1755 main_layout = QGridLayout()
1756 mpk_dict = self.wallet.get_master_public_keys()
1758 for key, value in mpk_dict.items():
1759 main_layout.addWidget(QLabel(key), i, 0)
1760 mpk_text = QTextEdit()
1761 mpk_text.setReadOnly(True)
1762 mpk_text.setMaximumHeight(170)
1763 mpk_text.setText(value)
1764 main_layout.addWidget(mpk_text, i + 1, 0)
1767 vbox = QVBoxLayout()
1768 vbox.addLayout(main_layout)
1769 vbox.addLayout(close_button(dialog))
1771 dialog.setLayout(vbox)
1776 def show_seed_dialog(self, password):
1777 if not self.wallet.has_seed():
1778 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1782 mnemonic = self.wallet.get_mnemonic(password)
1784 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1786 from seed_dialog import SeedDialog
1787 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1792 def show_qrcode(self, data, title = _("QR code")):
1795 d = QRDialog(data, self, title)
1799 def do_protect(self, func, args):
1800 if self.wallet.use_encryption:
1801 password = self.password_dialog()
1807 if args != (False,):
1808 args = (self,) + args + (password,)
1810 args = (self,password)
1814 def show_public_keys(self, address):
1815 if not address: return
1817 pubkey_list = self.wallet.get_public_keys(address)
1818 except Exception as e:
1819 traceback.print_exc(file=sys.stdout)
1820 self.show_message(str(e))
1824 d.setMinimumSize(600, 200)
1826 vbox = QVBoxLayout()
1827 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1828 vbox.addWidget( QLabel(_("Public key") + ':'))
1830 keys.setReadOnly(True)
1831 keys.setText('\n'.join(pubkey_list))
1832 vbox.addWidget(keys)
1833 vbox.addLayout(close_button(d))
1838 def show_private_key(self, address, password):
1839 if not address: return
1841 pk_list = self.wallet.get_private_key(address, password)
1842 except Exception as e:
1843 traceback.print_exc(file=sys.stdout)
1844 self.show_message(str(e))
1848 d.setMinimumSize(600, 200)
1850 vbox = QVBoxLayout()
1851 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1852 vbox.addWidget( QLabel(_("Private key") + ':'))
1854 keys.setReadOnly(True)
1855 keys.setText('\n'.join(pk_list))
1856 vbox.addWidget(keys)
1857 vbox.addLayout(close_button(d))
1863 def do_sign(self, address, message, signature, password):
1864 message = unicode(message.toPlainText())
1865 message = message.encode('utf-8')
1867 sig = self.wallet.sign_message(str(address.text()), message, password)
1868 signature.setText(sig)
1869 except Exception as e:
1870 self.show_message(str(e))
1872 def do_verify(self, address, message, signature):
1873 message = unicode(message.toPlainText())
1874 message = message.encode('utf-8')
1875 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1876 self.show_message(_("Signature verified"))
1878 self.show_message(_("Error: wrong signature"))
1881 def sign_verify_message(self, address=''):
1884 d.setWindowTitle(_('Sign/verify Message'))
1885 d.setMinimumSize(410, 290)
1887 layout = QGridLayout(d)
1889 message_e = QTextEdit()
1890 layout.addWidget(QLabel(_('Message')), 1, 0)
1891 layout.addWidget(message_e, 1, 1)
1892 layout.setRowStretch(2,3)
1894 address_e = QLineEdit()
1895 address_e.setText(address)
1896 layout.addWidget(QLabel(_('Address')), 2, 0)
1897 layout.addWidget(address_e, 2, 1)
1899 signature_e = QTextEdit()
1900 layout.addWidget(QLabel(_('Signature')), 3, 0)
1901 layout.addWidget(signature_e, 3, 1)
1902 layout.setRowStretch(3,1)
1904 hbox = QHBoxLayout()
1906 b = QPushButton(_("Sign"))
1907 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1910 b = QPushButton(_("Verify"))
1911 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1914 b = QPushButton(_("Close"))
1915 b.clicked.connect(d.accept)
1917 layout.addLayout(hbox, 4, 1)
1922 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1924 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1925 message_e.setText(decrypted)
1926 except Exception as e:
1927 self.show_message(str(e))
1930 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1931 message = unicode(message_e.toPlainText())
1932 message = message.encode('utf-8')
1934 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1935 encrypted_e.setText(encrypted)
1936 except Exception as e:
1937 self.show_message(str(e))
1941 def encrypt_message(self, address = ''):
1944 d.setWindowTitle(_('Encrypt/decrypt Message'))
1945 d.setMinimumSize(610, 490)
1947 layout = QGridLayout(d)
1949 message_e = QTextEdit()
1950 layout.addWidget(QLabel(_('Message')), 1, 0)
1951 layout.addWidget(message_e, 1, 1)
1952 layout.setRowStretch(2,3)
1954 pubkey_e = QLineEdit()
1956 pubkey = self.wallet.getpubkeys(address)[0]
1957 pubkey_e.setText(pubkey)
1958 layout.addWidget(QLabel(_('Public key')), 2, 0)
1959 layout.addWidget(pubkey_e, 2, 1)
1961 encrypted_e = QTextEdit()
1962 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1963 layout.addWidget(encrypted_e, 3, 1)
1964 layout.setRowStretch(3,1)
1966 hbox = QHBoxLayout()
1967 b = QPushButton(_("Encrypt"))
1968 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1971 b = QPushButton(_("Decrypt"))
1972 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1975 b = QPushButton(_("Close"))
1976 b.clicked.connect(d.accept)
1979 layout.addLayout(hbox, 4, 1)
1983 def question(self, msg):
1984 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1986 def show_message(self, msg):
1987 QMessageBox.information(self, _('Message'), msg, _('OK'))
1989 def password_dialog(self, msg=None):
1992 d.setWindowTitle(_("Enter Password"))
1997 vbox = QVBoxLayout()
1999 msg = _('Please enter your password')
2000 vbox.addWidget(QLabel(msg))
2002 grid = QGridLayout()
2004 grid.addWidget(QLabel(_('Password')), 1, 0)
2005 grid.addWidget(pw, 1, 1)
2006 vbox.addLayout(grid)
2008 vbox.addLayout(ok_cancel_buttons(d))
2011 run_hook('password_dialog', pw, grid, 1)
2012 if not d.exec_(): return
2013 return unicode(pw.text())
2022 def tx_from_text(self, txt):
2023 "json or raw hexadecimal"
2032 return Transaction(txt)
2034 traceback.print_exc(file=sys.stdout)
2035 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2039 tx_dict = json.loads(str(txt))
2040 assert "hex" in tx_dict.keys()
2041 tx = Transaction(tx_dict["hex"])
2042 #if tx_dict.has_key("input_info"):
2043 # input_info = json.loads(tx_dict['input_info'])
2044 # tx.add_input_info(input_info)
2047 traceback.print_exc(file=sys.stdout)
2048 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2052 def read_tx_from_file(self):
2053 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2057 with open(fileName, "r") as f:
2058 file_content = f.read()
2059 except (ValueError, IOError, os.error), reason:
2060 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2062 return self.tx_from_text(file_content)
2066 def sign_raw_transaction(self, tx, password):
2068 self.wallet.signrawtransaction(tx, [], password)
2069 except Exception as e:
2070 traceback.print_exc(file=sys.stdout)
2071 QMessageBox.warning(self, _("Error"), str(e))
2073 def do_process_from_text(self):
2074 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2077 tx = self.tx_from_text(text)
2079 self.show_transaction(tx)
2081 def do_process_from_file(self):
2082 tx = self.read_tx_from_file()
2084 self.show_transaction(tx)
2086 def do_process_from_txid(self):
2087 from electrum import transaction
2088 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2090 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2092 tx = transaction.Transaction(r)
2094 self.show_transaction(tx)
2096 self.show_message("unknown transaction")
2098 def do_process_from_csvReader(self, csvReader):
2103 for position, row in enumerate(csvReader):
2105 if not is_valid(address):
2106 errors.append((position, address))
2108 amount = Decimal(row[1])
2109 amount = int(100000000*amount)
2110 outputs.append((address, amount))
2111 except (ValueError, IOError, os.error), reason:
2112 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2116 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2117 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2121 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2122 except Exception as e:
2123 self.show_message(str(e))
2126 self.show_transaction(tx)
2128 def do_process_from_csv_file(self):
2129 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2133 with open(fileName, "r") as f:
2134 csvReader = csv.reader(f)
2135 self.do_process_from_csvReader(csvReader)
2136 except (ValueError, IOError, os.error), reason:
2137 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2140 def do_process_from_csv_text(self):
2141 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2142 + _("Format: address, amount. One output per line"), _("Load CSV"))
2145 f = StringIO.StringIO(text)
2146 csvReader = csv.reader(f)
2147 self.do_process_from_csvReader(csvReader)
2152 def export_privkeys_dialog(self, password):
2153 if self.wallet.is_watching_only():
2154 self.show_message(_("This is a watching-only wallet"))
2158 d.setWindowTitle(_('Private keys'))
2159 d.setMinimumSize(850, 300)
2160 vbox = QVBoxLayout(d)
2162 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2163 _("Exposing a single private key can compromise your entire wallet!"),
2164 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2165 vbox.addWidget(QLabel(msg))
2171 defaultname = 'electrum-private-keys.csv'
2172 select_msg = _('Select file to export your private keys to')
2173 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2174 vbox.addLayout(hbox)
2176 h, b = ok_cancel_buttons2(d, _('Export'))
2181 addresses = self.wallet.addresses(True)
2183 def privkeys_thread():
2184 for addr in addresses:
2188 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2189 d.emit(SIGNAL('computing_privkeys'))
2190 d.emit(SIGNAL('show_privkeys'))
2192 def show_privkeys():
2193 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2197 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2198 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2199 threading.Thread(target=privkeys_thread).start()
2205 filename = filename_e.text()
2210 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2211 except (IOError, os.error), reason:
2212 export_error_label = _("Electrum was unable to produce a private key-export.")
2213 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2215 except Exception as e:
2216 self.show_message(str(e))
2219 self.show_message(_("Private keys exported."))
2222 def do_export_privkeys(self, fileName, pklist, is_csv):
2223 with open(fileName, "w+") as f:
2225 transaction = csv.writer(f)
2226 transaction.writerow(["address", "private_key"])
2227 for addr, pk in pklist.items():
2228 transaction.writerow(["%34s"%addr,pk])
2231 f.write(json.dumps(pklist, indent = 4))
2234 def do_import_labels(self):
2235 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2236 if not labelsFile: return
2238 f = open(labelsFile, 'r')
2241 for key, value in json.loads(data).items():
2242 self.wallet.set_label(key, value)
2243 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2244 except (IOError, os.error), reason:
2245 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2248 def do_export_labels(self):
2249 labels = self.wallet.labels
2251 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2253 with open(fileName, 'w+') as f:
2254 json.dump(labels, f)
2255 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2256 except (IOError, os.error), reason:
2257 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2260 def export_history_dialog(self):
2263 d.setWindowTitle(_('Export History'))
2264 d.setMinimumSize(400, 200)
2265 vbox = QVBoxLayout(d)
2267 defaultname = os.path.expanduser('~/electrum-history.csv')
2268 select_msg = _('Select file to export your wallet transactions to')
2270 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2271 vbox.addLayout(hbox)
2275 h, b = ok_cancel_buttons2(d, _('Export'))
2280 filename = filename_e.text()
2285 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2286 except (IOError, os.error), reason:
2287 export_error_label = _("Electrum was unable to produce a transaction export.")
2288 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2291 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2294 def do_export_history(self, wallet, fileName, is_csv):
2295 history = wallet.get_tx_history()
2297 for item in history:
2298 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2300 if timestamp is not None:
2302 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2303 except [RuntimeError, TypeError, NameError] as reason:
2304 time_string = "unknown"
2307 time_string = "unknown"
2309 time_string = "pending"
2311 if value is not None:
2312 value_string = format_satoshis(value, True)
2317 fee_string = format_satoshis(fee, True)
2322 label, is_default_label = wallet.get_label(tx_hash)
2323 label = label.encode('utf-8')
2327 balance_string = format_satoshis(balance, False)
2329 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2331 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2333 with open(fileName, "w+") as f:
2335 transaction = csv.writer(f)
2336 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2338 transaction.writerow(line)
2341 f.write(json.dumps(lines, indent = 4))
2344 def sweep_key_dialog(self):
2346 d.setWindowTitle(_('Sweep private keys'))
2347 d.setMinimumSize(600, 300)
2349 vbox = QVBoxLayout(d)
2350 vbox.addWidget(QLabel(_("Enter private keys")))
2352 keys_e = QTextEdit()
2353 keys_e.setTabChangesFocus(True)
2354 vbox.addWidget(keys_e)
2356 h, address_e = address_field(self.wallet.addresses())
2360 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2361 vbox.addLayout(hbox)
2362 button.setEnabled(False)
2365 addr = str(address_e.text())
2366 if bitcoin.is_address(addr):
2370 pk = str(keys_e.toPlainText()).strip()
2371 if Wallet.is_private_key(pk):
2374 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2375 keys_e.textChanged.connect(f)
2376 address_e.textChanged.connect(f)
2380 fee = self.wallet.fee
2381 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2382 self.show_transaction(tx)
2386 def do_import_privkey(self, password):
2387 if not self.wallet.has_imported_keys():
2388 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2389 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2390 + _('Are you sure you understand what you are doing?'), 3, 4)
2393 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2396 text = str(text).split()
2401 addr = self.wallet.import_key(key, password)
2402 except Exception as e:
2408 addrlist.append(addr)
2410 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2412 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2413 self.update_address_tab()
2414 self.update_history_tab()
2417 def settings_dialog(self):
2419 d.setWindowTitle(_('Electrum Settings'))
2421 vbox = QVBoxLayout()
2422 grid = QGridLayout()
2423 grid.setColumnStretch(0,1)
2425 nz_label = QLabel(_('Display zeros') + ':')
2426 grid.addWidget(nz_label, 0, 0)
2427 nz_e = AmountEdit(None,True)
2428 nz_e.setText("%d"% self.num_zeros)
2429 grid.addWidget(nz_e, 0, 1)
2430 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2431 grid.addWidget(HelpButton(msg), 0, 2)
2432 if not self.config.is_modifiable('num_zeros'):
2433 for w in [nz_e, nz_label]: w.setEnabled(False)
2435 lang_label=QLabel(_('Language') + ':')
2436 grid.addWidget(lang_label, 1, 0)
2437 lang_combo = QComboBox()
2438 from electrum.i18n import languages
2439 lang_combo.addItems(languages.values())
2441 index = languages.keys().index(self.config.get("language",''))
2444 lang_combo.setCurrentIndex(index)
2445 grid.addWidget(lang_combo, 1, 1)
2446 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2447 if not self.config.is_modifiable('language'):
2448 for w in [lang_combo, lang_label]: w.setEnabled(False)
2451 fee_label = QLabel(_('Transaction fee') + ':')
2452 grid.addWidget(fee_label, 2, 0)
2453 fee_e = BTCAmountEdit(self.get_decimal_point)
2454 fee_e.setAmount(self.wallet.fee)
2455 grid.addWidget(fee_e, 2, 1)
2456 msg = _('Fee per kilobyte of transaction.') + '\n' \
2457 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2458 grid.addWidget(HelpButton(msg), 2, 2)
2459 if not self.config.is_modifiable('fee_per_kb'):
2460 for w in [fee_e, fee_label]: w.setEnabled(False)
2462 units = ['BTC', 'mBTC']
2463 unit_label = QLabel(_('Base unit') + ':')
2464 grid.addWidget(unit_label, 3, 0)
2465 unit_combo = QComboBox()
2466 unit_combo.addItems(units)
2467 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2468 grid.addWidget(unit_combo, 3, 1)
2469 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2470 + '\n1BTC=1000mBTC.\n' \
2471 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2473 usechange_cb = QCheckBox(_('Use change addresses'))
2474 usechange_cb.setChecked(self.wallet.use_change)
2475 grid.addWidget(usechange_cb, 4, 0)
2476 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2477 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2479 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2480 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2481 grid.addWidget(block_ex_label, 5, 0)
2482 block_ex_combo = QComboBox()
2483 block_ex_combo.addItems(block_explorers)
2484 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2485 grid.addWidget(block_ex_combo, 5, 1)
2486 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2488 show_tx = self.config.get('show_before_broadcast', False)
2489 showtx_cb = QCheckBox(_('Show before broadcast'))
2490 showtx_cb.setChecked(show_tx)
2491 grid.addWidget(showtx_cb, 6, 0)
2492 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2494 vbox.addLayout(grid)
2496 vbox.addLayout(ok_cancel_buttons(d))
2500 if not d.exec_(): return
2502 fee = fee_e.get_amount()
2504 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2507 self.wallet.set_fee(fee)
2509 nz = unicode(nz_e.text())
2514 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2517 if self.num_zeros != nz:
2519 self.config.set_key('num_zeros', nz, True)
2520 self.update_history_tab()
2521 self.update_address_tab()
2523 usechange_result = usechange_cb.isChecked()
2524 if self.wallet.use_change != usechange_result:
2525 self.wallet.use_change = usechange_result
2526 self.wallet.storage.put('use_change', self.wallet.use_change)
2528 if showtx_cb.isChecked() != show_tx:
2529 self.config.set_key('show_before_broadcast', not show_tx)
2531 unit_result = units[unit_combo.currentIndex()]
2532 if self.base_unit() != unit_result:
2533 self.decimal_point = 8 if unit_result == 'BTC' else 5
2534 self.config.set_key('decimal_point', self.decimal_point, True)
2535 self.update_history_tab()
2536 self.update_status()
2538 need_restart = False
2540 lang_request = languages.keys()[lang_combo.currentIndex()]
2541 if lang_request != self.config.get('language'):
2542 self.config.set_key("language", lang_request, True)
2545 be_result = block_explorers[block_ex_combo.currentIndex()]
2546 self.config.set_key('block_explorer', be_result, True)
2548 run_hook('close_settings_dialog')
2551 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2554 def run_network_dialog(self):
2555 if not self.network:
2557 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2559 def closeEvent(self, event):
2561 self.config.set_key("is_maximized", self.isMaximized())
2562 if not self.isMaximized():
2564 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2565 self.save_column_widths()
2566 self.config.set_key("console-history", self.console.history[-50:], True)
2567 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2571 def plugins_dialog(self):
2572 from electrum.plugins import plugins
2575 d.setWindowTitle(_('Electrum Plugins'))
2578 vbox = QVBoxLayout(d)
2581 scroll = QScrollArea()
2582 scroll.setEnabled(True)
2583 scroll.setWidgetResizable(True)
2584 scroll.setMinimumSize(400,250)
2585 vbox.addWidget(scroll)
2589 w.setMinimumHeight(len(plugins)*35)
2591 grid = QGridLayout()
2592 grid.setColumnStretch(0,1)
2595 def do_toggle(cb, p, w):
2598 if w: w.setEnabled(r)
2600 def mk_toggle(cb, p, w):
2601 return lambda: do_toggle(cb,p,w)
2603 for i, p in enumerate(plugins):
2605 cb = QCheckBox(p.fullname())
2606 cb.setDisabled(not p.is_available())
2607 cb.setChecked(p.is_enabled())
2608 grid.addWidget(cb, i, 0)
2609 if p.requires_settings():
2610 w = p.settings_widget(self)
2611 w.setEnabled( p.is_enabled() )
2612 grid.addWidget(w, i, 1)
2615 cb.clicked.connect(mk_toggle(cb,p,w))
2616 grid.addWidget(HelpButton(p.description()), i, 2)
2618 print_msg(_("Error: cannot display plugin"), p)
2619 traceback.print_exc(file=sys.stdout)
2620 grid.setRowStretch(i+1,1)
2622 vbox.addLayout(close_button(d))
2627 def show_account_details(self, k):
2628 account = self.wallet.accounts[k]
2631 d.setWindowTitle(_('Account Details'))
2634 vbox = QVBoxLayout(d)
2635 name = self.wallet.get_account_name(k)
2636 label = QLabel('Name: ' + name)
2637 vbox.addWidget(label)
2639 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2641 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2643 vbox.addWidget(QLabel(_('Master Public Key:')))
2646 text.setReadOnly(True)
2647 text.setMaximumHeight(170)
2648 vbox.addWidget(text)
2650 mpk_text = '\n'.join( account.get_master_pubkeys() )
2651 text.setText(mpk_text)
2653 vbox.addLayout(close_button(d))