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.funds_error = False
138 self.completions = QStringListModel()
140 self.tabs = tabs = QTabWidget(self)
141 self.column_widths = self.config.get("column_widths_2", default_column_widths )
142 tabs.addTab(self.create_history_tab(), _('History') )
143 tabs.addTab(self.create_send_tab(), _('Send') )
144 tabs.addTab(self.create_receive_tab(), _('Receive') )
145 tabs.addTab(self.create_addresses_tab(), _('Addresses') )
146 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
147 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
148 tabs.addTab(self.create_console_tab(), _('Console') )
149 tabs.setMinimumSize(600, 400)
150 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
151 self.setCentralWidget(tabs)
153 g = self.config.get("winpos-qt",[100, 100, 840, 400])
154 self.setGeometry(g[0], g[1], g[2], g[3])
155 if self.config.get("is_maximized"):
158 self.setWindowIcon(QIcon(":icons/electrum.png"))
161 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
162 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
163 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
164 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
165 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
167 for i in range(tabs.count()):
168 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
170 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
171 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
172 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
173 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
174 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
176 self.history_list.setFocus(True)
180 self.network.register_callback('updated', lambda: self.need_update.set())
181 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
182 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
183 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
184 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
186 # set initial message
187 self.console.showMessage(self.network.banner)
190 self.payment_request = None
192 def update_account_selector(self):
194 accounts = self.wallet.get_account_names()
195 self.account_selector.clear()
196 if len(accounts) > 1:
197 self.account_selector.addItems([_("All accounts")] + accounts.values())
198 self.account_selector.setCurrentIndex(0)
199 self.account_selector.show()
201 self.account_selector.hide()
204 def load_wallet(self, wallet):
208 self.update_wallet_format()
210 self.invoices = self.wallet.storage.get('invoices', {})
211 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
212 self.current_account = self.wallet.storage.get("current_account", None)
213 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
214 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
215 self.setWindowTitle( title )
217 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
218 self.notify_transactions()
219 self.update_account_selector()
221 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
222 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
223 self.password_menu.setEnabled(not self.wallet.is_watching_only())
224 self.seed_menu.setEnabled(self.wallet.has_seed())
225 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
226 self.import_menu.setEnabled(self.wallet.can_import())
228 self.update_lock_icon()
229 self.update_buttons_on_seed()
230 self.update_console()
232 self.clear_receive_tab()
233 self.update_receive_tab()
234 run_hook('load_wallet', wallet)
237 def update_wallet_format(self):
238 # convert old-format imported keys
239 if self.wallet.imported_keys:
240 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
242 self.wallet.convert_imported_keys(password)
244 self.show_message("error")
247 def open_wallet(self):
248 wallet_folder = self.wallet.storage.path
249 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
253 storage = WalletStorage({'wallet_path': filename})
254 if not storage.file_exists:
255 self.show_message("file not found "+ filename)
258 self.wallet.stop_threads()
261 wallet = Wallet(storage)
262 wallet.start_threads(self.network)
264 self.load_wallet(wallet)
268 def backup_wallet(self):
270 path = self.wallet.storage.path
271 wallet_folder = os.path.dirname(path)
272 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
276 new_path = os.path.join(wallet_folder, filename)
279 shutil.copy2(path, new_path)
280 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
281 except (IOError, os.error), reason:
282 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
285 def new_wallet(self):
288 wallet_folder = os.path.dirname(self.wallet.storage.path)
289 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
292 filename = os.path.join(wallet_folder, filename)
294 storage = WalletStorage({'wallet_path': filename})
295 if storage.file_exists:
296 QMessageBox.critical(None, "Error", _("File exists"))
299 wizard = installwizard.InstallWizard(self.config, self.network, storage)
300 wallet = wizard.run('new')
302 self.load_wallet(wallet)
306 def init_menubar(self):
309 file_menu = menubar.addMenu(_("&File"))
310 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
311 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
312 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
313 file_menu.addAction(_("&Quit"), self.close)
315 wallet_menu = menubar.addMenu(_("&Wallet"))
316 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
317 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
319 wallet_menu.addSeparator()
321 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
322 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
323 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
325 wallet_menu.addSeparator()
326 labels_menu = wallet_menu.addMenu(_("&Labels"))
327 labels_menu.addAction(_("&Import"), self.do_import_labels)
328 labels_menu.addAction(_("&Export"), self.do_export_labels)
330 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
331 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
332 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
333 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
334 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
336 tools_menu = menubar.addMenu(_("&Tools"))
338 # Settings / Preferences are all reserved keywords in OSX using this as work around
339 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
340 tools_menu.addAction(_("&Network"), self.run_network_dialog)
341 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
342 tools_menu.addSeparator()
343 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
344 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
345 tools_menu.addSeparator()
347 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
348 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
349 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
351 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
352 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
353 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
354 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
355 self.raw_transaction_menu = raw_transaction_menu
357 help_menu = menubar.addMenu(_("&Help"))
358 help_menu.addAction(_("&About"), self.show_about)
359 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
360 help_menu.addSeparator()
361 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
362 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
364 self.setMenuBar(menubar)
366 def show_about(self):
367 QMessageBox.about(self, "Electrum",
368 _("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."))
370 def show_report_bug(self):
371 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
372 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
375 def notify_transactions(self):
376 if not self.network or not self.network.is_connected():
379 print_error("Notifying GUI")
380 if len(self.network.pending_transactions_for_notifications) > 0:
381 # Combine the transactions if there are more then three
382 tx_amount = len(self.network.pending_transactions_for_notifications)
385 for tx in self.network.pending_transactions_for_notifications:
386 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
390 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
391 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
393 self.network.pending_transactions_for_notifications = []
395 for tx in self.network.pending_transactions_for_notifications:
397 self.network.pending_transactions_for_notifications.remove(tx)
398 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
400 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
402 def notify(self, message):
403 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
407 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
408 def getOpenFileName(self, title, filter = ""):
409 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
410 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
411 if fileName and directory != os.path.dirname(fileName):
412 self.config.set_key('io_dir', os.path.dirname(fileName), True)
415 def getSaveFileName(self, title, filename, filter = ""):
416 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
417 path = os.path.join( directory, filename )
418 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
419 if fileName and directory != os.path.dirname(fileName):
420 self.config.set_key('io_dir', os.path.dirname(fileName), True)
424 QMainWindow.close(self)
425 run_hook('close_main_window')
427 def connect_slots(self, sender):
428 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
429 self.previous_payto_e=''
431 def timer_actions(self):
432 if self.need_update.is_set():
434 self.need_update.clear()
436 run_hook('timer_actions')
438 def format_amount(self, x, is_diff=False, whitespaces=False):
439 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
442 def get_decimal_point(self):
443 return self.decimal_point
447 assert self.decimal_point in [5,8]
448 return "BTC" if self.decimal_point == 8 else "mBTC"
451 def update_status(self):
452 if self.network is None or not self.network.is_running():
454 icon = QIcon(":icons/status_disconnected.png")
456 elif self.network.is_connected():
457 if not self.wallet.up_to_date:
458 text = _("Synchronizing...")
459 icon = QIcon(":icons/status_waiting.png")
460 elif self.network.server_lag > 1:
461 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
462 icon = QIcon(":icons/status_lagging.png")
464 c, u = self.wallet.get_account_balance(self.current_account)
465 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
466 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
468 # append fiat balance and price from exchange rate plugin
470 run_hook('get_fiat_status_text', c+u, r)
475 self.tray.setToolTip(text)
476 icon = QIcon(":icons/status_connected.png")
478 text = _("Not connected")
479 icon = QIcon(":icons/status_disconnected.png")
481 self.balance_label.setText(text)
482 self.status_button.setIcon( icon )
485 def update_wallet(self):
487 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
488 self.update_history_tab()
489 self.update_receive_tab()
490 self.update_address_tab()
491 self.update_contacts_tab()
492 self.update_completions()
493 self.update_invoices_tab()
496 def create_history_tab(self):
497 self.history_list = l = MyTreeWidget(self)
499 for i,width in enumerate(self.column_widths['history']):
500 l.setColumnWidth(i, width)
501 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
502 l.itemDoubleClicked.connect(self.tx_label_clicked)
503 l.itemChanged.connect(self.tx_label_changed)
504 l.customContextMenuRequested.connect(self.create_history_menu)
508 def create_history_menu(self, position):
509 self.history_list.selectedIndexes()
510 item = self.history_list.currentItem()
511 be = self.config.get('block_explorer', 'Blockchain.info')
512 if be == 'Blockchain.info':
513 block_explorer = 'https://blockchain.info/tx/'
514 elif be == 'Blockr.io':
515 block_explorer = 'https://blockr.io/tx/info/'
516 elif be == 'Insight.is':
517 block_explorer = 'http://live.insight.is/tx/'
519 tx_hash = str(item.data(0, Qt.UserRole).toString())
520 if not tx_hash: return
522 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
523 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
524 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
525 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
526 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
529 def show_transaction(self, tx):
530 import transaction_dialog
531 d = transaction_dialog.TxDialog(tx, self)
534 def tx_label_clicked(self, item, column):
535 if column==2 and item.isSelected():
537 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
538 self.history_list.editItem( item, column )
539 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
542 def tx_label_changed(self, item, column):
546 tx_hash = str(item.data(0, Qt.UserRole).toString())
547 tx = self.wallet.transactions.get(tx_hash)
548 text = unicode( item.text(2) )
549 self.wallet.set_label(tx_hash, text)
551 item.setForeground(2, QBrush(QColor('black')))
553 text = self.wallet.get_default_label(tx_hash)
554 item.setText(2, text)
555 item.setForeground(2, QBrush(QColor('gray')))
559 def edit_label(self, is_recv):
560 l = self.address_list if is_recv else self.contacts_list
561 item = l.currentItem()
562 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
563 l.editItem( item, 1 )
564 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
568 def address_label_clicked(self, item, column, l, column_addr, column_label):
569 if column == column_label and item.isSelected():
570 is_editable = item.data(0, 32).toBool()
573 addr = unicode( item.text(column_addr) )
574 label = unicode( item.text(column_label) )
575 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
576 l.editItem( item, column )
577 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
580 def address_label_changed(self, item, column, l, column_addr, column_label):
581 if column == column_label:
582 addr = unicode( item.text(column_addr) )
583 text = unicode( item.text(column_label) )
584 is_editable = item.data(0, 32).toBool()
588 changed = self.wallet.set_label(addr, text)
590 self.update_history_tab()
591 self.update_completions()
593 self.current_item_changed(item)
595 run_hook('item_changed', item, column)
598 def current_item_changed(self, a):
599 run_hook('current_item_changed', a)
603 def update_history_tab(self):
605 self.history_list.clear()
606 for item in self.wallet.get_tx_history(self.current_account):
607 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
608 time_str = _("unknown")
611 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
613 time_str = _("error")
616 time_str = 'unverified'
617 icon = QIcon(":icons/unconfirmed.png")
620 icon = QIcon(":icons/unconfirmed.png")
622 icon = QIcon(":icons/clock%d.png"%conf)
624 icon = QIcon(":icons/confirmed.png")
626 if value is not None:
627 v_str = self.format_amount(value, True, whitespaces=True)
631 balance_str = self.format_amount(balance, whitespaces=True)
634 label, is_default_label = self.wallet.get_label(tx_hash)
636 label = _('Pruned transaction outputs')
637 is_default_label = False
639 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
640 item.setFont(2, QFont(MONOSPACE_FONT))
641 item.setFont(3, QFont(MONOSPACE_FONT))
642 item.setFont(4, QFont(MONOSPACE_FONT))
644 item.setForeground(3, QBrush(QColor("#BC1E1E")))
646 item.setData(0, Qt.UserRole, tx_hash)
647 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
649 item.setForeground(2, QBrush(QColor('grey')))
651 item.setIcon(0, icon)
652 self.history_list.insertTopLevelItem(0,item)
655 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
656 run_hook('history_tab_update')
659 def create_receive_tab(self):
661 grid = QGridLayout(w)
662 grid.setColumnMinimumWidth(3, 300)
663 grid.setColumnStretch(5, 1)
665 self.receive_address_e = QLineEdit()
666 self.receive_address_e.setReadOnly(True)
667 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
668 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
669 self.receive_address_e.textChanged.connect(self.update_receive_qr)
671 self.receive_message_e = QLineEdit()
672 grid.addWidget(QLabel(_('Message')), 1, 0)
673 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
674 self.receive_message_e.textChanged.connect(self.update_receive_qr)
676 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
677 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
678 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
679 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
681 self.save_request_button = QPushButton(_('Save'))
682 self.save_request_button.clicked.connect(self.save_payment_request)
683 grid.addWidget(self.save_request_button, 3, 1)
684 clear_button = QPushButton(_('New'))
685 clear_button.clicked.connect(self.clear_receive_tab)
686 grid.addWidget(clear_button, 3, 2)
687 grid.setRowStretch(4, 1)
689 self.receive_qr = QRCodeWidget(fixedSize=200)
690 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
692 grid.setRowStretch(5, 1)
694 self.receive_requests_label = QLabel(_('Saved Requests'))
695 self.receive_list = MyTreeWidget(self)
696 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
697 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
698 self.receive_list.itemClicked.connect(self.receive_item_changed)
699 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount')] )
700 self.receive_list.setColumnWidth(0, 340)
701 h = self.receive_list.header()
702 h.setStretchLastSection(False)
703 h.setResizeMode(1, QHeaderView.Stretch)
705 grid.addWidget(self.receive_requests_label, 6, 0)
706 grid.addWidget(self.receive_list, 7, 0, 1, 6)
709 def receive_item_changed(self, item):
712 addr = str(item.text(0))
713 amount, message = self.receive_requests[addr]
714 self.receive_address_e.setText(addr)
715 self.receive_message_e.setText(message)
716 self.receive_amount_e.setAmount(amount)
719 def receive_list_delete(self, item):
720 addr = str(item.text(0))
721 self.receive_requests.pop(addr)
722 self.update_receive_tab()
723 self.clear_receive_tab()
725 def receive_list_menu(self, position):
726 item = self.receive_list.itemAt(position)
728 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
729 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
731 def save_payment_request(self):
732 addr = str(self.receive_address_e.text())
733 amount = self.receive_amount_e.get_amount()
734 message = str(self.receive_message_e.text())
735 if not message and not amount:
736 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
738 self.receive_requests = self.wallet.storage.get('receive_requests',{})
739 self.receive_requests[addr] = (amount, message)
740 self.wallet.storage.put('receive_requests', self.receive_requests)
741 self.update_receive_tab()
743 def clear_receive_tab(self):
744 self.receive_requests = self.wallet.storage.get('receive_requests',{})
745 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
747 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
751 self.receive_address_e.setText(addr)
752 self.receive_message_e.setText('')
753 self.receive_amount_e.setAmount(None)
755 def receive_at(self, addr):
756 if not bitcoin.is_address(addr):
758 self.tabs.setCurrentIndex(2)
759 self.receive_address_e.setText(addr)
761 def update_receive_tab(self):
762 self.receive_requests = self.wallet.storage.get('receive_requests',{})
763 b = len(self.receive_requests) > 0
764 self.receive_list.setVisible(b)
765 self.receive_requests_label.setVisible(b)
767 self.receive_list.clear()
768 for address, v in self.receive_requests.items():
770 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else ""] )
771 item.setFont(0, QFont(MONOSPACE_FONT))
772 self.receive_list.addTopLevelItem(item)
775 def update_receive_qr(self):
776 import urlparse, urllib
777 addr = str(self.receive_address_e.text())
778 amount = self.receive_amount_e.get_amount()
779 message = unicode(self.receive_message_e.text()).encode('utf8')
780 self.save_request_button.setEnabled((amount is not None) or (message != ""))
784 query.append('amount=%s'%format_satoshis(amount))
786 query.append('message=%s'%urllib.quote(message))
787 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
788 url = urlparse.urlunparse(p)
791 self.receive_qr.setData(url)
792 run_hook('update_receive_qr', addr, amount, message, url)
795 def create_send_tab(self):
798 self.send_grid = grid = QGridLayout(w)
800 grid.setColumnMinimumWidth(3,300)
801 grid.setColumnStretch(5,1)
802 grid.setRowStretch(8, 1)
804 from paytoedit import PayToEdit
805 self.amount_e = BTCAmountEdit(self.get_decimal_point)
806 self.payto_e = PayToEdit(self)
807 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)'))
808 grid.addWidget(QLabel(_('Pay to')), 1, 0)
809 grid.addWidget(self.payto_e, 1, 1, 1, 3)
810 grid.addWidget(self.payto_help, 1, 4)
812 completer = QCompleter()
813 completer.setCaseSensitivity(False)
814 self.payto_e.setCompleter(completer)
815 completer.setModel(self.completions)
817 self.message_e = MyLineEdit()
818 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.'))
819 grid.addWidget(QLabel(_('Description')), 2, 0)
820 grid.addWidget(self.message_e, 2, 1, 1, 3)
821 grid.addWidget(self.message_help, 2, 4)
823 self.from_label = QLabel(_('From'))
824 grid.addWidget(self.from_label, 3, 0)
825 self.from_list = MyTreeWidget(self)
826 self.from_list.setColumnCount(2)
827 self.from_list.setColumnWidth(0, 350)
828 self.from_list.setColumnWidth(1, 50)
829 self.from_list.setHeaderHidden(True)
830 self.from_list.setMaximumHeight(80)
831 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
832 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
833 grid.addWidget(self.from_list, 3, 1, 1, 3)
834 self.set_pay_from([])
836 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
837 + _('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.') \
838 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
839 grid.addWidget(QLabel(_('Amount')), 4, 0)
840 grid.addWidget(self.amount_e, 4, 1, 1, 2)
841 grid.addWidget(self.amount_help, 4, 3)
843 self.fee_e = BTCAmountEdit(self.get_decimal_point)
844 grid.addWidget(QLabel(_('Fee')), 5, 0)
845 grid.addWidget(self.fee_e, 5, 1, 1, 2)
846 grid.addWidget(HelpButton(
847 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
848 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
849 + _('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)
851 self.send_button = EnterButton(_("Send"), self.do_send)
852 grid.addWidget(self.send_button, 6, 1)
854 b = EnterButton(_("Clear"), self.do_clear)
855 grid.addWidget(b, 6, 2)
857 self.payto_sig = QLabel('')
858 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
860 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
861 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
864 def entry_changed( is_fee ):
865 self.funds_error = False
867 if self.amount_e.is_shortcut:
868 self.amount_e.is_shortcut = False
869 sendable = self.get_sendable_balance()
870 # there is only one output because we are completely spending inputs
871 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
872 fee = self.wallet.estimated_fee(inputs, 1)
874 self.amount_e.setAmount(amount)
875 self.amount_e.textEdited.emit("")
876 self.fee_e.setAmount(fee)
879 amount = self.amount_e.get_amount()
880 fee = self.fee_e.get_amount()
882 if not is_fee: fee = None
885 # assume that there will be 2 outputs (one for change)
886 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
888 self.fee_e.setAmount(fee)
891 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
895 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
896 self.funds_error = True
897 text = _( "Not enough funds" )
898 c, u = self.wallet.get_frozen_balance()
899 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
901 self.statusBar().showMessage(text)
902 self.amount_e.setPalette(palette)
903 self.fee_e.setPalette(palette)
905 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
906 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
908 run_hook('create_send_tab', grid)
911 def from_list_delete(self, item):
912 i = self.from_list.indexOfTopLevelItem(item)
914 self.redraw_from_list()
916 def from_list_menu(self, position):
917 item = self.from_list.itemAt(position)
919 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
920 menu.exec_(self.from_list.viewport().mapToGlobal(position))
922 def set_pay_from(self, domain = None):
923 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
924 self.redraw_from_list()
926 def redraw_from_list(self):
927 self.from_list.clear()
928 self.from_label.setHidden(len(self.pay_from) == 0)
929 self.from_list.setHidden(len(self.pay_from) == 0)
932 h = x.get('prevout_hash')
933 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
935 for item in self.pay_from:
936 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
938 def update_completions(self):
940 for addr,label in self.wallet.labels.items():
941 if addr in self.wallet.addressbook:
942 l.append( label + ' <' + addr + '>')
944 run_hook('update_completions', l)
945 self.completions.setStringList(l)
949 return lambda s, *args: s.do_protect(func, args)
952 def read_send_tab(self):
954 if self.payment_request and self.payment_request.has_expired():
955 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
958 label = unicode( self.message_e.text() )
960 if self.payment_request:
961 outputs = self.payment_request.get_outputs()
963 outputs = self.payto_e.get_outputs()
966 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
969 for addr, x in outputs:
970 if addr is None or not bitcoin.is_address(addr):
971 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
974 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
977 amount = sum(map(lambda x:x[1], outputs))
979 fee = self.fee_e.get_amount()
981 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
984 confirm_amount = self.config.get('confirm_amount', 100000000)
985 if amount >= confirm_amount:
986 o = '\n'.join(map(lambda x:x[0], outputs))
987 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
990 confirm_fee = self.config.get('confirm_fee', 100000)
991 if fee >= confirm_fee:
992 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()}):
995 coins = self.get_coins()
996 return outputs, fee, label, coins
1000 r = self.read_send_tab()
1003 outputs, fee, label, coins = r
1004 self.send_tx(outputs, fee, label, coins)
1008 def send_tx(self, outputs, fee, label, coins, password):
1009 self.send_button.setDisabled(True)
1011 # first, create an unsigned tx
1013 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1015 except Exception as e:
1016 traceback.print_exc(file=sys.stdout)
1017 self.show_message(str(e))
1018 self.send_button.setDisabled(False)
1021 # call hook to see if plugin needs gui interaction
1022 run_hook('send_tx', tx)
1028 self.wallet.add_keypairs(tx, keypairs, password)
1029 self.wallet.sign_transaction(tx, keypairs, password)
1030 return tx, fee, label
1032 def sign_done(tx, fee, label):
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 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1053 self.waiting_dialog.start()
1057 def broadcast_transaction(self, tx):
1059 def broadcast_thread():
1060 pr = self.payment_request
1062 return self.wallet.sendtx(tx)
1064 if pr.has_expired():
1065 self.payment_request = None
1066 return False, _("Payment request has expired")
1068 status, msg = self.wallet.sendtx(tx)
1072 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1073 self.wallet.storage.put('invoices', self.invoices)
1074 self.update_invoices_tab()
1075 self.payment_request = None
1076 refund_address = self.wallet.addresses()[0]
1077 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1083 def broadcast_done(status, msg):
1085 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1088 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1089 self.send_button.setDisabled(False)
1091 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1092 self.waiting_dialog.start()
1096 def prepare_for_payment_request(self):
1097 self.tabs.setCurrentIndex(1)
1098 self.payto_e.is_pr = True
1099 for e in [self.payto_e, self.amount_e, self.message_e]:
1101 for h in [self.payto_help, self.amount_help, self.message_help]:
1103 self.payto_e.setText(_("please wait..."))
1106 def payment_request_ok(self):
1107 pr = self.payment_request
1109 if pr_id not in self.invoices:
1110 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1111 self.wallet.storage.put('invoices', self.invoices)
1112 self.update_invoices_tab()
1114 print_error('invoice already in list')
1116 status = self.invoices[pr_id][4]
1117 if status == PR_PAID:
1119 self.show_message("invoice already paid")
1120 self.payment_request = None
1123 self.payto_help.show()
1124 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1126 if not pr.has_expired():
1127 self.payto_e.setGreen()
1129 self.payto_e.setExpired()
1131 self.payto_e.setText(pr.domain)
1132 self.amount_e.setText(self.format_amount(pr.get_amount()))
1133 self.message_e.setText(pr.get_memo())
1135 def payment_request_error(self):
1137 self.show_message(self.payment_request.error)
1138 self.payment_request = None
1140 def pay_from_URI(self,URI):
1143 address, amount, label, message, request_url = util.parse_URI(URI)
1145 address, amount, label, message, request_url = util.parse_URI(URI)
1146 except Exception as e:
1147 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1150 self.tabs.setCurrentIndex(1)
1154 if self.wallet.labels.get(address) != label:
1155 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1156 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1157 self.wallet.addressbook.append(address)
1158 self.wallet.set_label(address, label)
1160 label = self.wallet.labels.get(address)
1162 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1164 self.message_e.setText(message)
1166 self.amount_e.setAmount(amount)
1169 from electrum import paymentrequest
1170 def payment_request():
1171 self.payment_request = paymentrequest.PaymentRequest(self.config)
1172 self.payment_request.read(request_url)
1173 if self.payment_request.verify():
1174 self.emit(SIGNAL('payment_request_ok'))
1176 self.emit(SIGNAL('payment_request_error'))
1178 self.pr_thread = threading.Thread(target=payment_request).start()
1179 self.prepare_for_payment_request()
1184 self.payto_e.is_pr = False
1185 self.payto_sig.setVisible(False)
1186 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1190 for h in [self.payto_help, self.amount_help, self.message_help]:
1193 self.payto_help.set_alt(None)
1194 self.set_pay_from([])
1195 self.update_status()
1199 def set_addrs_frozen(self,addrs,freeze):
1201 if not addr: continue
1202 if addr in self.wallet.frozen_addresses and not freeze:
1203 self.wallet.unfreeze(addr)
1204 elif addr not in self.wallet.frozen_addresses and freeze:
1205 self.wallet.freeze(addr)
1206 self.update_address_tab()
1210 def create_list_tab(self, headers):
1211 "generic tab creation method"
1212 l = MyTreeWidget(self)
1213 l.setColumnCount( len(headers) )
1214 l.setHeaderLabels( headers )
1217 vbox = QVBoxLayout()
1224 vbox.addWidget(buttons)
1229 def create_addresses_tab(self):
1230 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1231 for i,width in enumerate(self.column_widths['receive']):
1232 l.setColumnWidth(i, width)
1233 l.setContextMenuPolicy(Qt.CustomContextMenu)
1234 l.customContextMenuRequested.connect(self.create_receive_menu)
1235 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1236 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1237 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1238 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1239 self.address_list = l
1245 def save_column_widths(self):
1246 self.column_widths["receive"] = []
1247 for i in range(self.address_list.columnCount() -1):
1248 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1250 self.column_widths["history"] = []
1251 for i in range(self.history_list.columnCount() - 1):
1252 self.column_widths["history"].append(self.history_list.columnWidth(i))
1254 self.column_widths["contacts"] = []
1255 for i in range(self.contacts_list.columnCount() - 1):
1256 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1258 self.config.set_key("column_widths_2", self.column_widths, True)
1261 def create_contacts_tab(self):
1262 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1263 l.setContextMenuPolicy(Qt.CustomContextMenu)
1264 l.customContextMenuRequested.connect(self.create_contact_menu)
1265 for i,width in enumerate(self.column_widths['contacts']):
1266 l.setColumnWidth(i, width)
1267 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1268 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1269 self.contacts_list = l
1273 def create_invoices_tab(self):
1274 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1275 l.setColumnWidth(0, 150)
1277 h.setStretchLastSection(False)
1278 h.setResizeMode(1, QHeaderView.Stretch)
1279 l.setContextMenuPolicy(Qt.CustomContextMenu)
1280 l.customContextMenuRequested.connect(self.create_invoice_menu)
1281 self.invoices_list = l
1284 def update_invoices_tab(self):
1285 invoices = self.wallet.storage.get('invoices', {})
1286 l = self.invoices_list
1288 for key, value in invoices.items():
1290 domain, memo, amount, expiration_date, status, tx_hash = value
1294 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1296 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1297 l.addTopLevelItem(item)
1299 l.setCurrentItem(l.topLevelItem(0))
1303 def delete_imported_key(self, addr):
1304 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1305 self.wallet.delete_imported_key(addr)
1306 self.update_address_tab()
1307 self.update_history_tab()
1309 def edit_account_label(self, k):
1310 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1312 label = unicode(text)
1313 self.wallet.set_label(k,label)
1314 self.update_address_tab()
1316 def account_set_expanded(self, item, k, b):
1318 self.accounts_expanded[k] = b
1320 def create_account_menu(self, position, k, item):
1322 if item.isExpanded():
1323 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1325 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1326 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1327 if self.wallet.seed_version > 4:
1328 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1329 if self.wallet.account_is_pending(k):
1330 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1331 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1333 def delete_pending_account(self, k):
1334 self.wallet.delete_pending_account(k)
1335 self.update_address_tab()
1337 def create_receive_menu(self, position):
1338 # fixme: this function apparently has a side effect.
1339 # if it is not called the menu pops up several times
1340 #self.address_list.selectedIndexes()
1342 selected = self.address_list.selectedItems()
1343 multi_select = len(selected) > 1
1344 addrs = [unicode(item.text(0)) for item in selected]
1345 if not multi_select:
1346 item = self.address_list.itemAt(position)
1350 if not is_valid(addr):
1351 k = str(item.data(0,32).toString())
1353 self.create_account_menu(position, k, item)
1355 item.setExpanded(not item.isExpanded())
1359 if not multi_select:
1360 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1361 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1362 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1363 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1364 if not self.wallet.is_watching_only():
1365 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1366 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1367 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1368 if self.wallet.is_imported(addr):
1369 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1371 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1372 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1373 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1374 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1377 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1378 if any(can_send(addr) for addr in addrs):
1379 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1381 run_hook('receive_menu', menu, addrs)
1382 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1385 def get_sendable_balance(self):
1386 return sum(map(lambda x:x['value'], self.get_coins()))
1389 def get_coins(self):
1391 return self.pay_from
1393 domain = self.wallet.get_account_addresses(self.current_account)
1394 for i in self.wallet.frozen_addresses:
1395 if i in domain: domain.remove(i)
1396 return self.wallet.get_unspent_coins(domain)
1399 def send_from_addresses(self, addrs):
1400 self.set_pay_from( addrs )
1401 self.tabs.setCurrentIndex(1)
1404 def payto(self, addr):
1406 label = self.wallet.labels.get(addr)
1407 m_addr = label + ' <' + addr + '>' if label else addr
1408 self.tabs.setCurrentIndex(1)
1409 self.payto_e.setText(m_addr)
1410 self.amount_e.setFocus()
1413 def delete_contact(self, x):
1414 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1415 self.wallet.delete_contact(x)
1416 self.wallet.set_label(x, None)
1417 self.update_history_tab()
1418 self.update_contacts_tab()
1419 self.update_completions()
1422 def create_contact_menu(self, position):
1423 item = self.contacts_list.itemAt(position)
1426 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1428 addr = unicode(item.text(0))
1429 label = unicode(item.text(1))
1430 is_editable = item.data(0,32).toBool()
1431 payto_addr = item.data(0,33).toString()
1432 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1433 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1434 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1436 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1437 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1439 run_hook('create_contact_menu', menu, item)
1440 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1442 def delete_invoice(self, key):
1443 self.invoices.pop(key)
1444 self.wallet.storage.put('invoices', self.invoices)
1445 self.update_invoices_tab()
1447 def show_invoice(self, key):
1448 from electrum.paymentrequest import PaymentRequest
1449 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1450 pr = PaymentRequest(self.config)
1454 self.show_pr_details(pr)
1456 def show_pr_details(self, pr):
1457 msg = 'Domain: ' + pr.domain
1458 msg += '\nStatus: ' + pr.get_status()
1459 msg += '\nMemo: ' + pr.get_memo()
1460 msg += '\nPayment URL: ' + pr.payment_url
1461 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1462 QMessageBox.information(self, 'Invoice', msg , 'OK')
1464 def do_pay_invoice(self, key):
1465 from electrum.paymentrequest import PaymentRequest
1466 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1467 pr = PaymentRequest(self.config)
1470 self.payment_request = pr
1471 self.prepare_for_payment_request()
1473 self.payment_request_ok()
1475 self.payment_request_error()
1478 def create_invoice_menu(self, position):
1479 item = self.invoices_list.itemAt(position)
1482 k = self.invoices_list.indexOfTopLevelItem(item)
1483 key = self.invoices.keys()[k]
1484 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1486 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1487 if status == PR_UNPAID:
1488 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1489 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1490 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1493 def update_address_item(self, item):
1494 item.setFont(0, QFont(MONOSPACE_FONT))
1495 address = str(item.data(0,0).toString())
1496 label = self.wallet.labels.get(address,'')
1497 item.setData(1,0,label)
1498 item.setData(0,32, True) # is editable
1500 run_hook('update_address_item', address, item)
1502 if not self.wallet.is_mine(address): return
1504 c, u = self.wallet.get_addr_balance(address)
1505 balance = self.format_amount(c + u)
1506 item.setData(2,0,balance)
1508 if address in self.wallet.frozen_addresses:
1509 item.setBackgroundColor(0, QColor('lightblue'))
1512 def update_address_tab(self):
1513 l = self.address_list
1514 # extend the syntax for consistency
1515 l.addChild = l.addTopLevelItem
1516 l.insertChild = l.insertTopLevelItem
1520 accounts = self.wallet.get_accounts()
1521 if self.current_account is None:
1522 account_items = sorted(accounts.items())
1524 account_items = [(self.current_account, accounts.get(self.current_account))]
1527 for k, account in account_items:
1529 if len(accounts) > 1:
1530 name = self.wallet.get_account_name(k)
1531 c,u = self.wallet.get_account_balance(k)
1532 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1533 l.addTopLevelItem(account_item)
1534 account_item.setExpanded(self.accounts_expanded.get(k, True))
1535 account_item.setData(0, 32, k)
1539 sequences = [0,1] if account.has_change() else [0]
1540 for is_change in sequences:
1541 if len(sequences) > 1:
1542 name = _("Receiving") if not is_change else _("Change")
1543 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1544 account_item.addChild(seq_item)
1546 seq_item.setExpanded(True)
1548 seq_item = account_item
1550 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1556 for address in account.get_addresses(is_change):
1558 num, is_used = self.wallet.is_used(address)
1561 if gap > self.wallet.gap_limit:
1566 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1567 self.update_address_item(item)
1569 item.setBackgroundColor(1, QColor('red'))
1573 seq_item.insertChild(0,used_item)
1575 used_item.addChild(item)
1577 seq_item.addChild(item)
1579 # we use column 1 because column 0 may be hidden
1580 l.setCurrentItem(l.topLevelItem(0),1)
1583 def update_contacts_tab(self):
1584 l = self.contacts_list
1587 for address in self.wallet.addressbook:
1588 label = self.wallet.labels.get(address,'')
1589 n = self.wallet.get_num_tx(address)
1590 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1591 item.setFont(0, QFont(MONOSPACE_FONT))
1592 # 32 = label can be edited (bool)
1593 item.setData(0,32, True)
1595 item.setData(0,33, address)
1596 l.addTopLevelItem(item)
1598 run_hook('update_contacts_tab', l)
1599 l.setCurrentItem(l.topLevelItem(0))
1603 def create_console_tab(self):
1604 from console import Console
1605 self.console = console = Console()
1609 def update_console(self):
1610 console = self.console
1611 console.history = self.config.get("console-history",[])
1612 console.history_index = len(console.history)
1614 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1615 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1617 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1619 def mkfunc(f, method):
1620 return lambda *args: apply( f, (method, args, self.password_dialog ))
1622 if m[0]=='_' or m in ['network','wallet']: continue
1623 methods[m] = mkfunc(c._run, m)
1625 console.updateNamespace(methods)
1628 def change_account(self,s):
1629 if s == _("All accounts"):
1630 self.current_account = None
1632 accounts = self.wallet.get_account_names()
1633 for k, v in accounts.items():
1635 self.current_account = k
1636 self.update_history_tab()
1637 self.update_status()
1638 self.update_address_tab()
1639 self.update_receive_tab()
1641 def create_status_bar(self):
1644 sb.setFixedHeight(35)
1645 qtVersion = qVersion()
1647 self.balance_label = QLabel("")
1648 sb.addWidget(self.balance_label)
1650 from version_getter import UpdateLabel
1651 self.updatelabel = UpdateLabel(self.config, sb)
1653 self.account_selector = QComboBox()
1654 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1655 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1656 sb.addPermanentWidget(self.account_selector)
1658 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1659 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1661 self.lock_icon = QIcon()
1662 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1663 sb.addPermanentWidget( self.password_button )
1665 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1666 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1667 sb.addPermanentWidget( self.seed_button )
1668 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1669 sb.addPermanentWidget( self.status_button )
1671 run_hook('create_status_bar', (sb,))
1673 self.setStatusBar(sb)
1676 def update_lock_icon(self):
1677 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1678 self.password_button.setIcon( icon )
1681 def update_buttons_on_seed(self):
1682 if self.wallet.has_seed():
1683 self.seed_button.show()
1685 self.seed_button.hide()
1687 if not self.wallet.is_watching_only():
1688 self.password_button.show()
1689 self.send_button.setText(_("Send"))
1691 self.password_button.hide()
1692 self.send_button.setText(_("Create unsigned transaction"))
1695 def change_password_dialog(self):
1696 from password_dialog import PasswordDialog
1697 d = PasswordDialog(self.wallet, self)
1699 self.update_lock_icon()
1702 def new_contact_dialog(self):
1705 d.setWindowTitle(_("New Contact"))
1706 vbox = QVBoxLayout(d)
1707 vbox.addWidget(QLabel(_('New Contact')+':'))
1709 grid = QGridLayout()
1712 grid.addWidget(QLabel(_("Address")), 1, 0)
1713 grid.addWidget(line1, 1, 1)
1714 grid.addWidget(QLabel(_("Name")), 2, 0)
1715 grid.addWidget(line2, 2, 1)
1717 vbox.addLayout(grid)
1718 vbox.addLayout(ok_cancel_buttons(d))
1723 address = str(line1.text())
1724 label = unicode(line2.text())
1726 if not is_valid(address):
1727 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1730 self.wallet.add_contact(address)
1732 self.wallet.set_label(address, label)
1734 self.update_contacts_tab()
1735 self.update_history_tab()
1736 self.update_completions()
1737 self.tabs.setCurrentIndex(3)
1741 def new_account_dialog(self, password):
1743 dialog = QDialog(self)
1745 dialog.setWindowTitle(_("New Account"))
1747 vbox = QVBoxLayout()
1748 vbox.addWidget(QLabel(_('Account name')+':'))
1751 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1752 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1757 vbox.addLayout(ok_cancel_buttons(dialog))
1758 dialog.setLayout(vbox)
1762 name = str(e.text())
1765 self.wallet.create_pending_account(name, password)
1766 self.update_address_tab()
1767 self.tabs.setCurrentIndex(2)
1772 def show_master_public_keys(self):
1774 dialog = QDialog(self)
1776 dialog.setWindowTitle(_("Master Public Keys"))
1778 main_layout = QGridLayout()
1779 mpk_dict = self.wallet.get_master_public_keys()
1781 for key, value in mpk_dict.items():
1782 main_layout.addWidget(QLabel(key), i, 0)
1783 mpk_text = QTextEdit()
1784 mpk_text.setReadOnly(True)
1785 mpk_text.setMaximumHeight(170)
1786 mpk_text.setText(value)
1787 main_layout.addWidget(mpk_text, i + 1, 0)
1790 vbox = QVBoxLayout()
1791 vbox.addLayout(main_layout)
1792 vbox.addLayout(close_button(dialog))
1794 dialog.setLayout(vbox)
1799 def show_seed_dialog(self, password):
1800 if not self.wallet.has_seed():
1801 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1805 mnemonic = self.wallet.get_mnemonic(password)
1807 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1809 from seed_dialog import SeedDialog
1810 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1815 def show_qrcode(self, data, title = _("QR code")):
1818 d = QRDialog(data, self, title)
1822 def do_protect(self, func, args):
1823 if self.wallet.use_encryption:
1824 password = self.password_dialog()
1830 if args != (False,):
1831 args = (self,) + args + (password,)
1833 args = (self,password)
1837 def show_public_keys(self, address):
1838 if not address: return
1840 pubkey_list = self.wallet.get_public_keys(address)
1841 except Exception as e:
1842 traceback.print_exc(file=sys.stdout)
1843 self.show_message(str(e))
1847 d.setMinimumSize(600, 200)
1849 vbox = QVBoxLayout()
1850 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1851 vbox.addWidget( QLabel(_("Public key") + ':'))
1853 keys.setReadOnly(True)
1854 keys.setText('\n'.join(pubkey_list))
1855 vbox.addWidget(keys)
1856 vbox.addLayout(close_button(d))
1861 def show_private_key(self, address, password):
1862 if not address: return
1864 pk_list = self.wallet.get_private_key(address, password)
1865 except Exception as e:
1866 traceback.print_exc(file=sys.stdout)
1867 self.show_message(str(e))
1871 d.setMinimumSize(600, 200)
1873 vbox = QVBoxLayout()
1874 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1875 vbox.addWidget( QLabel(_("Private key") + ':'))
1877 keys.setReadOnly(True)
1878 keys.setText('\n'.join(pk_list))
1879 vbox.addWidget(keys)
1880 vbox.addLayout(close_button(d))
1886 def do_sign(self, address, message, signature, password):
1887 message = unicode(message.toPlainText())
1888 message = message.encode('utf-8')
1890 sig = self.wallet.sign_message(str(address.text()), message, password)
1891 signature.setText(sig)
1892 except Exception as e:
1893 self.show_message(str(e))
1895 def do_verify(self, address, message, signature):
1896 message = unicode(message.toPlainText())
1897 message = message.encode('utf-8')
1898 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1899 self.show_message(_("Signature verified"))
1901 self.show_message(_("Error: wrong signature"))
1904 def sign_verify_message(self, address=''):
1907 d.setWindowTitle(_('Sign/verify Message'))
1908 d.setMinimumSize(410, 290)
1910 layout = QGridLayout(d)
1912 message_e = QTextEdit()
1913 layout.addWidget(QLabel(_('Message')), 1, 0)
1914 layout.addWidget(message_e, 1, 1)
1915 layout.setRowStretch(2,3)
1917 address_e = QLineEdit()
1918 address_e.setText(address)
1919 layout.addWidget(QLabel(_('Address')), 2, 0)
1920 layout.addWidget(address_e, 2, 1)
1922 signature_e = QTextEdit()
1923 layout.addWidget(QLabel(_('Signature')), 3, 0)
1924 layout.addWidget(signature_e, 3, 1)
1925 layout.setRowStretch(3,1)
1927 hbox = QHBoxLayout()
1929 b = QPushButton(_("Sign"))
1930 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1933 b = QPushButton(_("Verify"))
1934 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1937 b = QPushButton(_("Close"))
1938 b.clicked.connect(d.accept)
1940 layout.addLayout(hbox, 4, 1)
1945 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1947 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1948 message_e.setText(decrypted)
1949 except Exception as e:
1950 self.show_message(str(e))
1953 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1954 message = unicode(message_e.toPlainText())
1955 message = message.encode('utf-8')
1957 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1958 encrypted_e.setText(encrypted)
1959 except Exception as e:
1960 self.show_message(str(e))
1964 def encrypt_message(self, address = ''):
1967 d.setWindowTitle(_('Encrypt/decrypt Message'))
1968 d.setMinimumSize(610, 490)
1970 layout = QGridLayout(d)
1972 message_e = QTextEdit()
1973 layout.addWidget(QLabel(_('Message')), 1, 0)
1974 layout.addWidget(message_e, 1, 1)
1975 layout.setRowStretch(2,3)
1977 pubkey_e = QLineEdit()
1979 pubkey = self.wallet.getpubkeys(address)[0]
1980 pubkey_e.setText(pubkey)
1981 layout.addWidget(QLabel(_('Public key')), 2, 0)
1982 layout.addWidget(pubkey_e, 2, 1)
1984 encrypted_e = QTextEdit()
1985 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1986 layout.addWidget(encrypted_e, 3, 1)
1987 layout.setRowStretch(3,1)
1989 hbox = QHBoxLayout()
1990 b = QPushButton(_("Encrypt"))
1991 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1994 b = QPushButton(_("Decrypt"))
1995 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1998 b = QPushButton(_("Close"))
1999 b.clicked.connect(d.accept)
2002 layout.addLayout(hbox, 4, 1)
2006 def question(self, msg):
2007 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2009 def show_message(self, msg):
2010 QMessageBox.information(self, _('Message'), msg, _('OK'))
2012 def password_dialog(self, msg=None):
2015 d.setWindowTitle(_("Enter Password"))
2020 vbox = QVBoxLayout()
2022 msg = _('Please enter your password')
2023 vbox.addWidget(QLabel(msg))
2025 grid = QGridLayout()
2027 grid.addWidget(QLabel(_('Password')), 1, 0)
2028 grid.addWidget(pw, 1, 1)
2029 vbox.addLayout(grid)
2031 vbox.addLayout(ok_cancel_buttons(d))
2034 run_hook('password_dialog', pw, grid, 1)
2035 if not d.exec_(): return
2036 return unicode(pw.text())
2045 def tx_from_text(self, txt):
2046 "json or raw hexadecimal"
2055 return Transaction(txt)
2057 traceback.print_exc(file=sys.stdout)
2058 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2062 tx_dict = json.loads(str(txt))
2063 assert "hex" in tx_dict.keys()
2064 tx = Transaction(tx_dict["hex"])
2065 #if tx_dict.has_key("input_info"):
2066 # input_info = json.loads(tx_dict['input_info'])
2067 # tx.add_input_info(input_info)
2070 traceback.print_exc(file=sys.stdout)
2071 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2075 def read_tx_from_file(self):
2076 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2080 with open(fileName, "r") as f:
2081 file_content = f.read()
2082 except (ValueError, IOError, os.error), reason:
2083 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2085 return self.tx_from_text(file_content)
2089 def sign_raw_transaction(self, tx, password):
2091 self.wallet.signrawtransaction(tx, [], password)
2092 except Exception as e:
2093 traceback.print_exc(file=sys.stdout)
2094 QMessageBox.warning(self, _("Error"), str(e))
2096 def do_process_from_text(self):
2097 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2100 tx = self.tx_from_text(text)
2102 self.show_transaction(tx)
2104 def do_process_from_file(self):
2105 tx = self.read_tx_from_file()
2107 self.show_transaction(tx)
2109 def do_process_from_txid(self):
2110 from electrum import transaction
2111 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2113 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2115 tx = transaction.Transaction(r)
2117 self.show_transaction(tx)
2119 self.show_message("unknown transaction")
2121 def do_process_from_csvReader(self, csvReader):
2126 for position, row in enumerate(csvReader):
2128 if not is_valid(address):
2129 errors.append((position, address))
2131 amount = Decimal(row[1])
2132 amount = int(100000000*amount)
2133 outputs.append((address, amount))
2134 except (ValueError, IOError, os.error), reason:
2135 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2139 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2140 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2144 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2145 except Exception as e:
2146 self.show_message(str(e))
2149 self.show_transaction(tx)
2151 def do_process_from_csv_file(self):
2152 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2156 with open(fileName, "r") as f:
2157 csvReader = csv.reader(f)
2158 self.do_process_from_csvReader(csvReader)
2159 except (ValueError, IOError, os.error), reason:
2160 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2163 def do_process_from_csv_text(self):
2164 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2165 + _("Format: address, amount. One output per line"), _("Load CSV"))
2168 f = StringIO.StringIO(text)
2169 csvReader = csv.reader(f)
2170 self.do_process_from_csvReader(csvReader)
2175 def export_privkeys_dialog(self, password):
2176 if self.wallet.is_watching_only():
2177 self.show_message(_("This is a watching-only wallet"))
2181 d.setWindowTitle(_('Private keys'))
2182 d.setMinimumSize(850, 300)
2183 vbox = QVBoxLayout(d)
2185 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2186 _("Exposing a single private key can compromise your entire wallet!"),
2187 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2188 vbox.addWidget(QLabel(msg))
2194 defaultname = 'electrum-private-keys.csv'
2195 select_msg = _('Select file to export your private keys to')
2196 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2197 vbox.addLayout(hbox)
2199 h, b = ok_cancel_buttons2(d, _('Export'))
2204 addresses = self.wallet.addresses(True)
2206 def privkeys_thread():
2207 for addr in addresses:
2211 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2212 d.emit(SIGNAL('computing_privkeys'))
2213 d.emit(SIGNAL('show_privkeys'))
2215 def show_privkeys():
2216 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2220 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2221 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2222 threading.Thread(target=privkeys_thread).start()
2228 filename = filename_e.text()
2233 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2234 except (IOError, os.error), reason:
2235 export_error_label = _("Electrum was unable to produce a private key-export.")
2236 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2238 except Exception as e:
2239 self.show_message(str(e))
2242 self.show_message(_("Private keys exported."))
2245 def do_export_privkeys(self, fileName, pklist, is_csv):
2246 with open(fileName, "w+") as f:
2248 transaction = csv.writer(f)
2249 transaction.writerow(["address", "private_key"])
2250 for addr, pk in pklist.items():
2251 transaction.writerow(["%34s"%addr,pk])
2254 f.write(json.dumps(pklist, indent = 4))
2257 def do_import_labels(self):
2258 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2259 if not labelsFile: return
2261 f = open(labelsFile, 'r')
2264 for key, value in json.loads(data).items():
2265 self.wallet.set_label(key, value)
2266 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2267 except (IOError, os.error), reason:
2268 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2271 def do_export_labels(self):
2272 labels = self.wallet.labels
2274 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2276 with open(fileName, 'w+') as f:
2277 json.dump(labels, f)
2278 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2279 except (IOError, os.error), reason:
2280 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2283 def export_history_dialog(self):
2286 d.setWindowTitle(_('Export History'))
2287 d.setMinimumSize(400, 200)
2288 vbox = QVBoxLayout(d)
2290 defaultname = os.path.expanduser('~/electrum-history.csv')
2291 select_msg = _('Select file to export your wallet transactions to')
2293 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2294 vbox.addLayout(hbox)
2298 h, b = ok_cancel_buttons2(d, _('Export'))
2303 filename = filename_e.text()
2308 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2309 except (IOError, os.error), reason:
2310 export_error_label = _("Electrum was unable to produce a transaction export.")
2311 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2314 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2317 def do_export_history(self, wallet, fileName, is_csv):
2318 history = wallet.get_tx_history()
2320 for item in history:
2321 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2323 if timestamp is not None:
2325 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2326 except [RuntimeError, TypeError, NameError] as reason:
2327 time_string = "unknown"
2330 time_string = "unknown"
2332 time_string = "pending"
2334 if value is not None:
2335 value_string = format_satoshis(value, True)
2340 fee_string = format_satoshis(fee, True)
2345 label, is_default_label = wallet.get_label(tx_hash)
2346 label = label.encode('utf-8')
2350 balance_string = format_satoshis(balance, False)
2352 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2354 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2356 with open(fileName, "w+") as f:
2358 transaction = csv.writer(f)
2359 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2361 transaction.writerow(line)
2364 f.write(json.dumps(lines, indent = 4))
2367 def sweep_key_dialog(self):
2369 d.setWindowTitle(_('Sweep private keys'))
2370 d.setMinimumSize(600, 300)
2372 vbox = QVBoxLayout(d)
2373 vbox.addWidget(QLabel(_("Enter private keys")))
2375 keys_e = QTextEdit()
2376 keys_e.setTabChangesFocus(True)
2377 vbox.addWidget(keys_e)
2379 h, address_e = address_field(self.wallet.addresses())
2383 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2384 vbox.addLayout(hbox)
2385 button.setEnabled(False)
2388 addr = str(address_e.text())
2389 if bitcoin.is_address(addr):
2393 pk = str(keys_e.toPlainText()).strip()
2394 if Wallet.is_private_key(pk):
2397 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2398 keys_e.textChanged.connect(f)
2399 address_e.textChanged.connect(f)
2403 fee = self.wallet.fee
2404 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2405 self.show_transaction(tx)
2409 def do_import_privkey(self, password):
2410 if not self.wallet.has_imported_keys():
2411 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2412 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2413 + _('Are you sure you understand what you are doing?'), 3, 4)
2416 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2419 text = str(text).split()
2424 addr = self.wallet.import_key(key, password)
2425 except Exception as e:
2431 addrlist.append(addr)
2433 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2435 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2436 self.update_address_tab()
2437 self.update_history_tab()
2440 def settings_dialog(self):
2442 d.setWindowTitle(_('Electrum Settings'))
2444 vbox = QVBoxLayout()
2445 grid = QGridLayout()
2446 grid.setColumnStretch(0,1)
2448 nz_label = QLabel(_('Display zeros') + ':')
2449 grid.addWidget(nz_label, 0, 0)
2450 nz_e = AmountEdit(None,True)
2451 nz_e.setText("%d"% self.num_zeros)
2452 grid.addWidget(nz_e, 0, 1)
2453 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2454 grid.addWidget(HelpButton(msg), 0, 2)
2455 if not self.config.is_modifiable('num_zeros'):
2456 for w in [nz_e, nz_label]: w.setEnabled(False)
2458 lang_label=QLabel(_('Language') + ':')
2459 grid.addWidget(lang_label, 1, 0)
2460 lang_combo = QComboBox()
2461 from electrum.i18n import languages
2462 lang_combo.addItems(languages.values())
2464 index = languages.keys().index(self.config.get("language",''))
2467 lang_combo.setCurrentIndex(index)
2468 grid.addWidget(lang_combo, 1, 1)
2469 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2470 if not self.config.is_modifiable('language'):
2471 for w in [lang_combo, lang_label]: w.setEnabled(False)
2474 fee_label = QLabel(_('Transaction fee') + ':')
2475 grid.addWidget(fee_label, 2, 0)
2476 fee_e = BTCAmountEdit(self.get_decimal_point)
2477 fee_e.setAmount(self.wallet.fee)
2478 grid.addWidget(fee_e, 2, 1)
2479 msg = _('Fee per kilobyte of transaction.') + '\n' \
2480 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2481 grid.addWidget(HelpButton(msg), 2, 2)
2482 if not self.config.is_modifiable('fee_per_kb'):
2483 for w in [fee_e, fee_label]: w.setEnabled(False)
2485 units = ['BTC', 'mBTC']
2486 unit_label = QLabel(_('Base unit') + ':')
2487 grid.addWidget(unit_label, 3, 0)
2488 unit_combo = QComboBox()
2489 unit_combo.addItems(units)
2490 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2491 grid.addWidget(unit_combo, 3, 1)
2492 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2493 + '\n1BTC=1000mBTC.\n' \
2494 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2496 usechange_cb = QCheckBox(_('Use change addresses'))
2497 usechange_cb.setChecked(self.wallet.use_change)
2498 grid.addWidget(usechange_cb, 4, 0)
2499 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2500 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2502 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2503 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2504 grid.addWidget(block_ex_label, 5, 0)
2505 block_ex_combo = QComboBox()
2506 block_ex_combo.addItems(block_explorers)
2507 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2508 grid.addWidget(block_ex_combo, 5, 1)
2509 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2511 show_tx = self.config.get('show_before_broadcast', False)
2512 showtx_cb = QCheckBox(_('Show before broadcast'))
2513 showtx_cb.setChecked(show_tx)
2514 grid.addWidget(showtx_cb, 6, 0)
2515 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2517 vbox.addLayout(grid)
2519 vbox.addLayout(ok_cancel_buttons(d))
2523 if not d.exec_(): return
2525 fee = fee_e.get_amount()
2527 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2530 self.wallet.set_fee(fee)
2532 nz = unicode(nz_e.text())
2537 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2540 if self.num_zeros != nz:
2542 self.config.set_key('num_zeros', nz, True)
2543 self.update_history_tab()
2544 self.update_address_tab()
2546 usechange_result = usechange_cb.isChecked()
2547 if self.wallet.use_change != usechange_result:
2548 self.wallet.use_change = usechange_result
2549 self.wallet.storage.put('use_change', self.wallet.use_change)
2551 if showtx_cb.isChecked() != show_tx:
2552 self.config.set_key('show_before_broadcast', not show_tx)
2554 unit_result = units[unit_combo.currentIndex()]
2555 if self.base_unit() != unit_result:
2556 self.decimal_point = 8 if unit_result == 'BTC' else 5
2557 self.config.set_key('decimal_point', self.decimal_point, True)
2558 self.update_history_tab()
2559 self.update_status()
2561 need_restart = False
2563 lang_request = languages.keys()[lang_combo.currentIndex()]
2564 if lang_request != self.config.get('language'):
2565 self.config.set_key("language", lang_request, True)
2568 be_result = block_explorers[block_ex_combo.currentIndex()]
2569 self.config.set_key('block_explorer', be_result, True)
2571 run_hook('close_settings_dialog')
2574 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2577 def run_network_dialog(self):
2578 if not self.network:
2580 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2582 def closeEvent(self, event):
2584 self.config.set_key("is_maximized", self.isMaximized())
2585 if not self.isMaximized():
2587 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2588 self.save_column_widths()
2589 self.config.set_key("console-history", self.console.history[-50:], True)
2590 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2594 def plugins_dialog(self):
2595 from electrum.plugins import plugins
2598 d.setWindowTitle(_('Electrum Plugins'))
2601 vbox = QVBoxLayout(d)
2604 scroll = QScrollArea()
2605 scroll.setEnabled(True)
2606 scroll.setWidgetResizable(True)
2607 scroll.setMinimumSize(400,250)
2608 vbox.addWidget(scroll)
2612 w.setMinimumHeight(len(plugins)*35)
2614 grid = QGridLayout()
2615 grid.setColumnStretch(0,1)
2618 def do_toggle(cb, p, w):
2621 if w: w.setEnabled(r)
2623 def mk_toggle(cb, p, w):
2624 return lambda: do_toggle(cb,p,w)
2626 for i, p in enumerate(plugins):
2628 cb = QCheckBox(p.fullname())
2629 cb.setDisabled(not p.is_available())
2630 cb.setChecked(p.is_enabled())
2631 grid.addWidget(cb, i, 0)
2632 if p.requires_settings():
2633 w = p.settings_widget(self)
2634 w.setEnabled( p.is_enabled() )
2635 grid.addWidget(w, i, 1)
2638 cb.clicked.connect(mk_toggle(cb,p,w))
2639 grid.addWidget(HelpButton(p.description()), i, 2)
2641 print_msg(_("Error: cannot display plugin"), p)
2642 traceback.print_exc(file=sys.stdout)
2643 grid.setRowStretch(i+1,1)
2645 vbox.addLayout(close_button(d))
2650 def show_account_details(self, k):
2651 account = self.wallet.accounts[k]
2654 d.setWindowTitle(_('Account Details'))
2657 vbox = QVBoxLayout(d)
2658 name = self.wallet.get_account_name(k)
2659 label = QLabel('Name: ' + name)
2660 vbox.addWidget(label)
2662 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2664 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2666 vbox.addWidget(QLabel(_('Master Public Key:')))
2669 text.setReadOnly(True)
2670 text.setMaximumHeight(170)
2671 vbox.addWidget(text)
2673 mpk_text = '\n'.join( account.get_master_pubkeys() )
2674 text.setText(mpk_text)
2676 vbox.addLayout(close_button(d))