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
43 from electrum import Imported_Wallet
45 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
46 from network_dialog import NetworkDialog
47 from qrcodewidget import QRCodeWidget, QRDialog
48 from qrtextedit import QRTextEdit
50 from decimal import Decimal
58 if platform.system() == 'Windows':
59 MONOSPACE_FONT = 'Lucida Console'
60 elif platform.system() == 'Darwin':
61 MONOSPACE_FONT = 'Monaco'
63 MONOSPACE_FONT = 'monospace'
67 # status of payment requests
70 PR_SENT = 2 # sent but not propagated
71 PR_PAID = 3 # send and propagated
72 PR_ERROR = 4 # could not parse
75 from electrum import ELECTRUM_VERSION
90 class StatusBarButton(QPushButton):
91 def __init__(self, icon, tooltip, func):
92 QPushButton.__init__(self, icon, '')
93 self.setToolTip(tooltip)
95 self.setMaximumWidth(25)
96 self.clicked.connect(func)
98 self.setIconSize(QSize(25,25))
100 def keyPressEvent(self, e):
101 if e.key() == QtCore.Qt.Key_Return:
113 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
115 class ElectrumWindow(QMainWindow):
119 def __init__(self, config, network, gui_object):
120 QMainWindow.__init__(self)
123 self.network = network
124 self.gui_object = gui_object
125 self.tray = gui_object.tray
126 self.go_lite = gui_object.go_lite
129 self.create_status_bar()
130 self.need_update = threading.Event()
132 self.decimal_point = config.get('decimal_point', 5)
133 self.num_zeros = int(config.get('num_zeros',0))
136 set_language(config.get('language'))
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.new_receive_address)
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 new_receive_address(self):
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():
749 if isinstance(self.wallet, Imported_Wallet):
750 self.show_message(_('No more addresses in your wallet.'))
752 if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
754 addr = self.wallet.create_new_address(self.current_account, False)
755 self.receive_address_e.setText(addr)
756 self.receive_message_e.setText('')
757 self.receive_amount_e.setAmount(None)
759 def clear_receive_tab(self):
760 self.receive_requests = self.wallet.storage.get('receive_requests',{})
761 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
763 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
767 self.receive_address_e.setText(addr)
768 self.receive_message_e.setText('')
769 self.receive_amount_e.setAmount(None)
771 def receive_at(self, addr):
772 if not bitcoin.is_address(addr):
774 self.tabs.setCurrentIndex(2)
775 self.receive_address_e.setText(addr)
777 def update_receive_tab(self):
778 self.receive_requests = self.wallet.storage.get('receive_requests',{})
779 b = len(self.receive_requests) > 0
780 self.receive_list.setVisible(b)
781 self.receive_requests_label.setVisible(b)
783 self.receive_list.clear()
784 for address, v in self.receive_requests.items():
786 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else ""] )
787 item.setFont(0, QFont(MONOSPACE_FONT))
788 self.receive_list.addTopLevelItem(item)
791 def update_receive_qr(self):
792 import urlparse, urllib
793 addr = str(self.receive_address_e.text())
794 amount = self.receive_amount_e.get_amount()
795 message = unicode(self.receive_message_e.text()).encode('utf8')
796 self.save_request_button.setEnabled((amount is not None) or (message != ""))
800 query.append('amount=%s'%format_satoshis(amount))
802 query.append('message=%s'%urllib.quote(message))
803 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
804 url = urlparse.urlunparse(p)
807 self.receive_qr.setData(url)
808 run_hook('update_receive_qr', addr, amount, message, url)
811 def create_send_tab(self):
814 self.send_grid = grid = QGridLayout(w)
816 grid.setColumnMinimumWidth(3,300)
817 grid.setColumnStretch(5,1)
818 grid.setRowStretch(8, 1)
820 from paytoedit import PayToEdit
821 self.amount_e = BTCAmountEdit(self.get_decimal_point)
822 self.payto_e = PayToEdit(self)
823 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)'))
824 grid.addWidget(QLabel(_('Pay to')), 1, 0)
825 grid.addWidget(self.payto_e, 1, 1, 1, 3)
826 grid.addWidget(self.payto_help, 1, 4)
828 completer = QCompleter()
829 completer.setCaseSensitivity(False)
830 self.payto_e.setCompleter(completer)
831 completer.setModel(self.completions)
833 self.message_e = MyLineEdit()
834 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.'))
835 grid.addWidget(QLabel(_('Description')), 2, 0)
836 grid.addWidget(self.message_e, 2, 1, 1, 3)
837 grid.addWidget(self.message_help, 2, 4)
839 self.from_label = QLabel(_('From'))
840 grid.addWidget(self.from_label, 3, 0)
841 self.from_list = MyTreeWidget(self)
842 self.from_list.setColumnCount(2)
843 self.from_list.setColumnWidth(0, 350)
844 self.from_list.setColumnWidth(1, 50)
845 self.from_list.setHeaderHidden(True)
846 self.from_list.setMaximumHeight(80)
847 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
848 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
849 grid.addWidget(self.from_list, 3, 1, 1, 3)
850 self.set_pay_from([])
852 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
853 + _('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.') \
854 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
855 grid.addWidget(QLabel(_('Amount')), 4, 0)
856 grid.addWidget(self.amount_e, 4, 1, 1, 2)
857 grid.addWidget(self.amount_help, 4, 3)
859 self.fee_e = BTCAmountEdit(self.get_decimal_point)
860 grid.addWidget(QLabel(_('Fee')), 5, 0)
861 grid.addWidget(self.fee_e, 5, 1, 1, 2)
862 grid.addWidget(HelpButton(
863 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
864 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
865 + _('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)
867 self.send_button = EnterButton(_("Send"), self.do_send)
868 grid.addWidget(self.send_button, 6, 1)
870 b = EnterButton(_("Clear"), self.do_clear)
871 grid.addWidget(b, 6, 2)
873 self.payto_sig = QLabel('')
874 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
876 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
877 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
880 def entry_changed( is_fee ):
882 if self.amount_e.is_shortcut:
883 self.amount_e.is_shortcut = False
884 sendable = self.get_sendable_balance()
885 # there is only one output because we are completely spending inputs
886 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
887 fee = self.wallet.estimated_fee(inputs, 1)
889 self.amount_e.setAmount(amount)
890 self.amount_e.textEdited.emit("")
891 self.fee_e.setAmount(fee)
894 amount = self.amount_e.get_amount()
895 fee = self.fee_e.get_amount()
896 outputs = self.payto_e.get_outputs()
902 self.fee_e.setAmount(None)
903 not_enough_funds = False
905 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, len(outputs), coins = self.get_coins())
906 not_enough_funds = len(inputs) == 0
908 self.fee_e.setAmount(fee)
910 if not not_enough_funds:
912 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
916 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
917 text = _( "Not enough funds" )
918 c, u = self.wallet.get_frozen_balance()
919 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
921 self.statusBar().showMessage(text)
922 self.amount_e.setPalette(palette)
923 self.fee_e.setPalette(palette)
925 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
926 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
928 run_hook('create_send_tab', grid)
931 def from_list_delete(self, item):
932 i = self.from_list.indexOfTopLevelItem(item)
934 self.redraw_from_list()
936 def from_list_menu(self, position):
937 item = self.from_list.itemAt(position)
939 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
940 menu.exec_(self.from_list.viewport().mapToGlobal(position))
942 def set_pay_from(self, domain = None):
943 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
944 self.redraw_from_list()
946 def redraw_from_list(self):
947 self.from_list.clear()
948 self.from_label.setHidden(len(self.pay_from) == 0)
949 self.from_list.setHidden(len(self.pay_from) == 0)
952 h = x.get('prevout_hash')
953 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
955 for item in self.pay_from:
956 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
958 def update_completions(self):
960 for addr,label in self.wallet.labels.items():
961 if addr in self.wallet.addressbook:
962 l.append( label + ' <' + addr + '>')
964 run_hook('update_completions', l)
965 self.completions.setStringList(l)
969 return lambda s, *args: s.do_protect(func, args)
972 def read_send_tab(self):
974 if self.payment_request and self.payment_request.has_expired():
975 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
978 label = unicode( self.message_e.text() )
980 if self.payment_request:
981 outputs = self.payment_request.get_outputs()
983 outputs = self.payto_e.get_outputs()
986 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
989 for addr, x in outputs:
991 QMessageBox.warning(self, _('Error'), _('Bitcoin Address is None'), _('OK'))
993 if addr.startswith('OP_RETURN:'):
995 if not bitcoin.is_address(addr):
996 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
999 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
1002 amount = sum(map(lambda x:x[1], outputs))
1004 fee = self.fee_e.get_amount()
1006 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
1009 confirm_amount = self.config.get('confirm_amount', 100000000)
1010 if amount >= confirm_amount:
1011 o = '\n'.join(map(lambda x:x[0], outputs))
1012 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
1015 confirm_fee = self.config.get('confirm_fee', 100000)
1016 if fee >= confirm_fee:
1017 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()}):
1020 coins = self.get_coins()
1021 return outputs, fee, label, coins
1025 r = self.read_send_tab()
1028 outputs, fee, label, coins = r
1029 self.send_tx(outputs, fee, label, coins)
1033 def send_tx(self, outputs, fee, label, coins, password):
1034 self.send_button.setDisabled(True)
1036 # first, create an unsigned tx
1038 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1040 except Exception as e:
1041 traceback.print_exc(file=sys.stdout)
1042 self.show_message(str(e))
1043 self.send_button.setDisabled(False)
1046 # call hook to see if plugin needs gui interaction
1047 run_hook('send_tx', tx)
1053 self.wallet.add_keypairs(tx, keypairs, password)
1054 self.wallet.sign_transaction(tx, keypairs, password)
1055 except Exception as e:
1061 self.show_message(tx.error)
1062 self.send_button.setDisabled(False)
1064 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1065 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1066 self.send_button.setDisabled(False)
1069 self.wallet.set_label(tx.hash(), label)
1071 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1072 self.show_transaction(tx)
1074 self.send_button.setDisabled(False)
1077 self.broadcast_transaction(tx)
1079 # keep a reference to WaitingDialog or the gui might crash
1080 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1081 self.waiting_dialog.start()
1085 def broadcast_transaction(self, tx):
1087 def broadcast_thread():
1088 pr = self.payment_request
1090 return self.wallet.sendtx(tx)
1092 if pr.has_expired():
1093 self.payment_request = None
1094 return False, _("Payment request has expired")
1096 status, msg = self.wallet.sendtx(tx)
1100 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1101 self.wallet.storage.put('invoices', self.invoices)
1102 self.update_invoices_tab()
1103 self.payment_request = None
1104 refund_address = self.wallet.addresses()[0]
1105 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1111 def broadcast_done(status, msg):
1113 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1116 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1117 self.send_button.setDisabled(False)
1119 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1120 self.waiting_dialog.start()
1124 def prepare_for_payment_request(self):
1125 self.tabs.setCurrentIndex(1)
1126 self.payto_e.is_pr = True
1127 for e in [self.payto_e, self.amount_e, self.message_e]:
1129 for h in [self.payto_help, self.amount_help, self.message_help]:
1131 self.payto_e.setText(_("please wait..."))
1134 def payment_request_ok(self):
1135 pr = self.payment_request
1137 if pr_id not in self.invoices:
1138 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1139 self.wallet.storage.put('invoices', self.invoices)
1140 self.update_invoices_tab()
1142 print_error('invoice already in list')
1144 status = self.invoices[pr_id][4]
1145 if status == PR_PAID:
1147 self.show_message("invoice already paid")
1148 self.payment_request = None
1151 self.payto_help.show()
1152 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1154 if not pr.has_expired():
1155 self.payto_e.setGreen()
1157 self.payto_e.setExpired()
1159 self.payto_e.setText(pr.domain)
1160 self.amount_e.setText(self.format_amount(pr.get_amount()))
1161 self.message_e.setText(pr.get_memo())
1163 def payment_request_error(self):
1165 self.show_message(self.payment_request.error)
1166 self.payment_request = None
1168 def pay_from_URI(self,URI):
1171 address, amount, label, message, request_url = util.parse_URI(URI)
1173 address, amount, label, message, request_url = util.parse_URI(URI)
1174 except Exception as e:
1175 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1178 self.tabs.setCurrentIndex(1)
1182 if self.wallet.labels.get(address) != label:
1183 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1184 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1185 self.wallet.addressbook.append(address)
1186 self.wallet.set_label(address, label)
1188 label = self.wallet.labels.get(address)
1190 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1192 self.message_e.setText(message)
1194 self.amount_e.setAmount(amount)
1197 from electrum import paymentrequest
1198 def payment_request():
1199 self.payment_request = paymentrequest.PaymentRequest(self.config)
1200 self.payment_request.read(request_url)
1201 if self.payment_request.verify():
1202 self.emit(SIGNAL('payment_request_ok'))
1204 self.emit(SIGNAL('payment_request_error'))
1206 self.pr_thread = threading.Thread(target=payment_request).start()
1207 self.prepare_for_payment_request()
1212 self.payto_e.is_pr = False
1213 self.payto_sig.setVisible(False)
1214 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1218 for h in [self.payto_help, self.amount_help, self.message_help]:
1221 self.payto_help.set_alt(None)
1222 self.set_pay_from([])
1223 self.update_status()
1227 def set_addrs_frozen(self,addrs,freeze):
1229 if not addr: continue
1230 if addr in self.wallet.frozen_addresses and not freeze:
1231 self.wallet.unfreeze(addr)
1232 elif addr not in self.wallet.frozen_addresses and freeze:
1233 self.wallet.freeze(addr)
1234 self.update_address_tab()
1238 def create_list_tab(self, headers):
1239 "generic tab creation method"
1240 l = MyTreeWidget(self)
1241 l.setColumnCount( len(headers) )
1242 l.setHeaderLabels( headers )
1245 vbox = QVBoxLayout()
1252 vbox.addWidget(buttons)
1257 def create_addresses_tab(self):
1258 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1259 for i,width in enumerate(self.column_widths['receive']):
1260 l.setColumnWidth(i, width)
1261 l.setContextMenuPolicy(Qt.CustomContextMenu)
1262 l.customContextMenuRequested.connect(self.create_receive_menu)
1263 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1264 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1265 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1266 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1267 self.address_list = l
1273 def save_column_widths(self):
1274 self.column_widths["receive"] = []
1275 for i in range(self.address_list.columnCount() -1):
1276 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1278 self.column_widths["history"] = []
1279 for i in range(self.history_list.columnCount() - 1):
1280 self.column_widths["history"].append(self.history_list.columnWidth(i))
1282 self.column_widths["contacts"] = []
1283 for i in range(self.contacts_list.columnCount() - 1):
1284 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1286 self.config.set_key("column_widths_2", self.column_widths, True)
1289 def create_contacts_tab(self):
1290 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1291 l.setContextMenuPolicy(Qt.CustomContextMenu)
1292 l.customContextMenuRequested.connect(self.create_contact_menu)
1293 for i,width in enumerate(self.column_widths['contacts']):
1294 l.setColumnWidth(i, width)
1295 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1296 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1297 self.contacts_list = l
1301 def create_invoices_tab(self):
1302 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1303 l.setColumnWidth(0, 150)
1305 h.setStretchLastSection(False)
1306 h.setResizeMode(1, QHeaderView.Stretch)
1307 l.setContextMenuPolicy(Qt.CustomContextMenu)
1308 l.customContextMenuRequested.connect(self.create_invoice_menu)
1309 self.invoices_list = l
1312 def update_invoices_tab(self):
1313 invoices = self.wallet.storage.get('invoices', {})
1314 l = self.invoices_list
1316 for key, value in invoices.items():
1318 domain, memo, amount, expiration_date, status, tx_hash = value
1322 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1324 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1325 l.addTopLevelItem(item)
1327 l.setCurrentItem(l.topLevelItem(0))
1331 def delete_imported_key(self, addr):
1332 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1333 self.wallet.delete_imported_key(addr)
1334 self.update_address_tab()
1335 self.update_history_tab()
1337 def edit_account_label(self, k):
1338 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1340 label = unicode(text)
1341 self.wallet.set_label(k,label)
1342 self.update_address_tab()
1344 def account_set_expanded(self, item, k, b):
1346 self.accounts_expanded[k] = b
1348 def create_account_menu(self, position, k, item):
1350 if item.isExpanded():
1351 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1353 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1354 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1355 if self.wallet.seed_version > 4:
1356 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1357 if self.wallet.account_is_pending(k):
1358 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1359 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1361 def delete_pending_account(self, k):
1362 self.wallet.delete_pending_account(k)
1363 self.update_address_tab()
1365 def create_receive_menu(self, position):
1366 # fixme: this function apparently has a side effect.
1367 # if it is not called the menu pops up several times
1368 #self.address_list.selectedIndexes()
1370 selected = self.address_list.selectedItems()
1371 multi_select = len(selected) > 1
1372 addrs = [unicode(item.text(0)) for item in selected]
1373 if not multi_select:
1374 item = self.address_list.itemAt(position)
1378 if not is_valid(addr):
1379 k = str(item.data(0,32).toString())
1381 self.create_account_menu(position, k, item)
1383 item.setExpanded(not item.isExpanded())
1387 if not multi_select:
1388 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1389 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1390 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1391 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1392 if not self.wallet.is_watching_only():
1393 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1394 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1395 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1396 if self.wallet.is_imported(addr):
1397 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1399 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1400 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1401 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1402 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1405 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1406 if any(can_send(addr) for addr in addrs):
1407 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1409 run_hook('receive_menu', menu, addrs)
1410 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1413 def get_sendable_balance(self):
1414 return sum(map(lambda x:x['value'], self.get_coins()))
1417 def get_coins(self):
1419 return self.pay_from
1421 domain = self.wallet.get_account_addresses(self.current_account)
1422 for i in self.wallet.frozen_addresses:
1423 if i in domain: domain.remove(i)
1424 return self.wallet.get_unspent_coins(domain)
1427 def send_from_addresses(self, addrs):
1428 self.set_pay_from( addrs )
1429 self.tabs.setCurrentIndex(1)
1432 def payto(self, addr):
1434 label = self.wallet.labels.get(addr)
1435 m_addr = label + ' <' + addr + '>' if label else addr
1436 self.tabs.setCurrentIndex(1)
1437 self.payto_e.setText(m_addr)
1438 self.amount_e.setFocus()
1441 def delete_contact(self, x):
1442 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1443 self.wallet.delete_contact(x)
1444 self.wallet.set_label(x, None)
1445 self.update_history_tab()
1446 self.update_contacts_tab()
1447 self.update_completions()
1450 def create_contact_menu(self, position):
1451 item = self.contacts_list.itemAt(position)
1454 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1456 addr = unicode(item.text(0))
1457 label = unicode(item.text(1))
1458 is_editable = item.data(0,32).toBool()
1459 payto_addr = item.data(0,33).toString()
1460 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1461 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1462 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1464 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1465 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1467 run_hook('create_contact_menu', menu, item)
1468 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1470 def delete_invoice(self, key):
1471 self.invoices.pop(key)
1472 self.wallet.storage.put('invoices', self.invoices)
1473 self.update_invoices_tab()
1475 def show_invoice(self, key):
1476 from electrum.paymentrequest import PaymentRequest
1477 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1478 pr = PaymentRequest(self.config)
1482 self.show_pr_details(pr)
1484 def show_pr_details(self, pr):
1485 msg = 'Domain: ' + pr.domain
1486 msg += '\nStatus: ' + pr.get_status()
1487 msg += '\nMemo: ' + pr.get_memo()
1488 msg += '\nPayment URL: ' + pr.payment_url
1489 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1490 QMessageBox.information(self, 'Invoice', msg , 'OK')
1492 def do_pay_invoice(self, key):
1493 from electrum.paymentrequest import PaymentRequest
1494 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1495 pr = PaymentRequest(self.config)
1498 self.payment_request = pr
1499 self.prepare_for_payment_request()
1501 self.payment_request_ok()
1503 self.payment_request_error()
1506 def create_invoice_menu(self, position):
1507 item = self.invoices_list.itemAt(position)
1510 k = self.invoices_list.indexOfTopLevelItem(item)
1511 key = self.invoices.keys()[k]
1512 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1514 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1515 if status == PR_UNPAID:
1516 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1517 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1518 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1522 def update_address_tab(self):
1523 l = self.address_list
1524 # extend the syntax for consistency
1525 l.addChild = l.addTopLevelItem
1526 l.insertChild = l.insertTopLevelItem
1530 accounts = self.wallet.get_accounts()
1531 if self.current_account is None:
1532 account_items = sorted(accounts.items())
1534 account_items = [(self.current_account, accounts.get(self.current_account))]
1537 for k, account in account_items:
1539 if len(accounts) > 1:
1540 name = self.wallet.get_account_name(k)
1541 c,u = self.wallet.get_account_balance(k)
1542 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1543 l.addTopLevelItem(account_item)
1544 account_item.setExpanded(self.accounts_expanded.get(k, True))
1545 account_item.setData(0, 32, k)
1549 sequences = [0,1] if account.has_change() else [0]
1550 for is_change in sequences:
1551 if len(sequences) > 1:
1552 name = _("Receiving") if not is_change else _("Change")
1553 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1554 account_item.addChild(seq_item)
1556 seq_item.setExpanded(True)
1558 seq_item = account_item
1560 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1563 addr_list = account.get_addresses(is_change)
1564 for address in addr_list:
1565 num, is_used = self.wallet.is_used(address)
1566 label = self.wallet.labels.get(address,'')
1567 c, u = self.wallet.get_addr_balance(address)
1568 balance = self.format_amount(c + u)
1569 item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1570 item.setFont(0, QFont(MONOSPACE_FONT))
1571 item.setData(0, 32, True) # label can be edited
1572 if address in self.wallet.frozen_addresses:
1573 item.setBackgroundColor(0, QColor('lightblue'))
1574 if self.wallet.is_beyond_limit(address, account, is_change):
1575 item.setBackgroundColor(0, QColor('red'))
1578 seq_item.insertChild(0, used_item)
1580 used_item.addChild(item)
1582 seq_item.addChild(item)
1584 # we use column 1 because column 0 may be hidden
1585 l.setCurrentItem(l.topLevelItem(0),1)
1588 def update_contacts_tab(self):
1589 l = self.contacts_list
1592 for address in self.wallet.addressbook:
1593 label = self.wallet.labels.get(address,'')
1594 n = self.wallet.get_num_tx(address)
1595 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1596 item.setFont(0, QFont(MONOSPACE_FONT))
1597 # 32 = label can be edited (bool)
1598 item.setData(0,32, True)
1600 item.setData(0,33, address)
1601 l.addTopLevelItem(item)
1603 run_hook('update_contacts_tab', l)
1604 l.setCurrentItem(l.topLevelItem(0))
1607 def create_console_tab(self):
1608 from console import Console
1609 self.console = console = Console()
1613 def update_console(self):
1614 console = self.console
1615 console.history = self.config.get("console-history",[])
1616 console.history_index = len(console.history)
1618 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1619 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1621 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1623 def mkfunc(f, method):
1624 return lambda *args: apply( f, (method, args, self.password_dialog ))
1626 if m[0]=='_' or m in ['network','wallet']: continue
1627 methods[m] = mkfunc(c._run, m)
1629 console.updateNamespace(methods)
1632 def change_account(self,s):
1633 if s == _("All accounts"):
1634 self.current_account = None
1636 accounts = self.wallet.get_account_names()
1637 for k, v in accounts.items():
1639 self.current_account = k
1640 self.update_history_tab()
1641 self.update_status()
1642 self.update_address_tab()
1643 self.update_receive_tab()
1645 def create_status_bar(self):
1648 sb.setFixedHeight(35)
1649 qtVersion = qVersion()
1651 self.balance_label = QLabel("")
1652 sb.addWidget(self.balance_label)
1654 from version_getter import UpdateLabel
1655 self.updatelabel = UpdateLabel(self.config, sb)
1657 self.account_selector = QComboBox()
1658 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1659 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1660 sb.addPermanentWidget(self.account_selector)
1662 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1663 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1665 self.lock_icon = QIcon()
1666 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1667 sb.addPermanentWidget( self.password_button )
1669 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1670 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1671 sb.addPermanentWidget( self.seed_button )
1672 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1673 sb.addPermanentWidget( self.status_button )
1675 run_hook('create_status_bar', (sb,))
1677 self.setStatusBar(sb)
1680 def update_lock_icon(self):
1681 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1682 self.password_button.setIcon( icon )
1685 def update_buttons_on_seed(self):
1686 if self.wallet.has_seed():
1687 self.seed_button.show()
1689 self.seed_button.hide()
1691 if not self.wallet.is_watching_only():
1692 self.password_button.show()
1693 self.send_button.setText(_("Send"))
1695 self.password_button.hide()
1696 self.send_button.setText(_("Create unsigned transaction"))
1699 def change_password_dialog(self):
1700 from password_dialog import PasswordDialog
1701 d = PasswordDialog(self.wallet, self)
1703 self.update_lock_icon()
1706 def new_contact_dialog(self):
1709 d.setWindowTitle(_("New Contact"))
1710 vbox = QVBoxLayout(d)
1711 vbox.addWidget(QLabel(_('New Contact')+':'))
1713 grid = QGridLayout()
1716 grid.addWidget(QLabel(_("Address")), 1, 0)
1717 grid.addWidget(line1, 1, 1)
1718 grid.addWidget(QLabel(_("Name")), 2, 0)
1719 grid.addWidget(line2, 2, 1)
1721 vbox.addLayout(grid)
1722 vbox.addLayout(ok_cancel_buttons(d))
1727 address = str(line1.text())
1728 label = unicode(line2.text())
1730 if not is_valid(address):
1731 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1734 self.wallet.add_contact(address)
1736 self.wallet.set_label(address, label)
1738 self.update_contacts_tab()
1739 self.update_history_tab()
1740 self.update_completions()
1741 self.tabs.setCurrentIndex(3)
1745 def new_account_dialog(self, password):
1747 dialog = QDialog(self)
1749 dialog.setWindowTitle(_("New Account"))
1751 vbox = QVBoxLayout()
1752 vbox.addWidget(QLabel(_('Account name')+':'))
1755 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1756 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1761 vbox.addLayout(ok_cancel_buttons(dialog))
1762 dialog.setLayout(vbox)
1766 name = str(e.text())
1769 self.wallet.create_pending_account(name, password)
1770 self.update_address_tab()
1771 self.tabs.setCurrentIndex(2)
1776 def show_master_public_keys(self):
1778 dialog = QDialog(self)
1780 dialog.setWindowTitle(_("Master Public Keys"))
1782 main_layout = QGridLayout()
1783 mpk_dict = self.wallet.get_master_public_keys()
1785 for key, value in mpk_dict.items():
1786 main_layout.addWidget(QLabel(key), i, 0)
1787 mpk_text = QTextEdit()
1788 mpk_text.setReadOnly(True)
1789 mpk_text.setMaximumHeight(170)
1790 mpk_text.setText(value)
1791 main_layout.addWidget(mpk_text, i + 1, 0)
1794 vbox = QVBoxLayout()
1795 vbox.addLayout(main_layout)
1796 vbox.addLayout(close_button(dialog))
1798 dialog.setLayout(vbox)
1803 def show_seed_dialog(self, password):
1804 if not self.wallet.has_seed():
1805 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1809 mnemonic = self.wallet.get_mnemonic(password)
1811 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1813 from seed_dialog import SeedDialog
1814 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1819 def show_qrcode(self, data, title = _("QR code")):
1822 d = QRDialog(data, self, title)
1826 def do_protect(self, func, args):
1827 if self.wallet.use_encryption:
1828 password = self.password_dialog()
1834 if args != (False,):
1835 args = (self,) + args + (password,)
1837 args = (self,password)
1841 def show_public_keys(self, address):
1842 if not address: return
1844 pubkey_list = self.wallet.get_public_keys(address)
1845 except Exception as e:
1846 traceback.print_exc(file=sys.stdout)
1847 self.show_message(str(e))
1851 d.setMinimumSize(600, 200)
1853 vbox = QVBoxLayout()
1854 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1855 vbox.addWidget( QLabel(_("Public key") + ':'))
1857 keys.setReadOnly(True)
1858 keys.setText('\n'.join(pubkey_list))
1859 vbox.addWidget(keys)
1860 vbox.addLayout(close_button(d))
1865 def show_private_key(self, address, password):
1866 if not address: return
1868 pk_list = self.wallet.get_private_key(address, password)
1869 except Exception as e:
1870 traceback.print_exc(file=sys.stdout)
1871 self.show_message(str(e))
1875 d.setMinimumSize(600, 200)
1877 vbox = QVBoxLayout()
1878 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1879 vbox.addWidget( QLabel(_("Private key") + ':'))
1881 keys.setReadOnly(True)
1882 keys.setText('\n'.join(pk_list))
1883 vbox.addWidget(keys)
1884 vbox.addLayout(close_button(d))
1890 def do_sign(self, address, message, signature, password):
1891 message = unicode(message.toPlainText())
1892 message = message.encode('utf-8')
1894 sig = self.wallet.sign_message(str(address.text()), message, password)
1895 signature.setText(sig)
1896 except Exception as e:
1897 self.show_message(str(e))
1899 def do_verify(self, address, message, signature):
1900 message = unicode(message.toPlainText())
1901 message = message.encode('utf-8')
1902 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1903 self.show_message(_("Signature verified"))
1905 self.show_message(_("Error: wrong signature"))
1908 def sign_verify_message(self, address=''):
1911 d.setWindowTitle(_('Sign/verify Message'))
1912 d.setMinimumSize(410, 290)
1914 layout = QGridLayout(d)
1916 message_e = QTextEdit()
1917 layout.addWidget(QLabel(_('Message')), 1, 0)
1918 layout.addWidget(message_e, 1, 1)
1919 layout.setRowStretch(2,3)
1921 address_e = QLineEdit()
1922 address_e.setText(address)
1923 layout.addWidget(QLabel(_('Address')), 2, 0)
1924 layout.addWidget(address_e, 2, 1)
1926 signature_e = QTextEdit()
1927 layout.addWidget(QLabel(_('Signature')), 3, 0)
1928 layout.addWidget(signature_e, 3, 1)
1929 layout.setRowStretch(3,1)
1931 hbox = QHBoxLayout()
1933 b = QPushButton(_("Sign"))
1934 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1937 b = QPushButton(_("Verify"))
1938 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1941 b = QPushButton(_("Close"))
1942 b.clicked.connect(d.accept)
1944 layout.addLayout(hbox, 4, 1)
1949 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1951 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1952 message_e.setText(decrypted)
1953 except Exception as e:
1954 self.show_message(str(e))
1957 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1958 message = unicode(message_e.toPlainText())
1959 message = message.encode('utf-8')
1961 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1962 encrypted_e.setText(encrypted)
1963 except Exception as e:
1964 self.show_message(str(e))
1968 def encrypt_message(self, address = ''):
1971 d.setWindowTitle(_('Encrypt/decrypt Message'))
1972 d.setMinimumSize(610, 490)
1974 layout = QGridLayout(d)
1976 message_e = QTextEdit()
1977 layout.addWidget(QLabel(_('Message')), 1, 0)
1978 layout.addWidget(message_e, 1, 1)
1979 layout.setRowStretch(2,3)
1981 pubkey_e = QLineEdit()
1983 pubkey = self.wallet.getpubkeys(address)[0]
1984 pubkey_e.setText(pubkey)
1985 layout.addWidget(QLabel(_('Public key')), 2, 0)
1986 layout.addWidget(pubkey_e, 2, 1)
1988 encrypted_e = QTextEdit()
1989 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1990 layout.addWidget(encrypted_e, 3, 1)
1991 layout.setRowStretch(3,1)
1993 hbox = QHBoxLayout()
1994 b = QPushButton(_("Encrypt"))
1995 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1998 b = QPushButton(_("Decrypt"))
1999 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
2002 b = QPushButton(_("Close"))
2003 b.clicked.connect(d.accept)
2006 layout.addLayout(hbox, 4, 1)
2010 def question(self, msg):
2011 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2013 def show_message(self, msg):
2014 QMessageBox.information(self, _('Message'), msg, _('OK'))
2016 def password_dialog(self, msg=None):
2019 d.setWindowTitle(_("Enter Password"))
2024 vbox = QVBoxLayout()
2026 msg = _('Please enter your password')
2027 vbox.addWidget(QLabel(msg))
2029 grid = QGridLayout()
2031 grid.addWidget(QLabel(_('Password')), 1, 0)
2032 grid.addWidget(pw, 1, 1)
2033 vbox.addLayout(grid)
2035 vbox.addLayout(ok_cancel_buttons(d))
2038 run_hook('password_dialog', pw, grid, 1)
2039 if not d.exec_(): return
2040 return unicode(pw.text())
2049 def tx_from_text(self, txt):
2050 "json or raw hexadecimal"
2059 return Transaction(txt)
2061 traceback.print_exc(file=sys.stdout)
2062 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2066 tx_dict = json.loads(str(txt))
2067 assert "hex" in tx_dict.keys()
2068 tx = Transaction(tx_dict["hex"])
2069 #if tx_dict.has_key("input_info"):
2070 # input_info = json.loads(tx_dict['input_info'])
2071 # tx.add_input_info(input_info)
2074 traceback.print_exc(file=sys.stdout)
2075 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2079 def read_tx_from_file(self):
2080 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2084 with open(fileName, "r") as f:
2085 file_content = f.read()
2086 except (ValueError, IOError, os.error), reason:
2087 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2089 return self.tx_from_text(file_content)
2093 def sign_raw_transaction(self, tx, password):
2095 self.wallet.signrawtransaction(tx, [], password)
2096 except Exception as e:
2097 traceback.print_exc(file=sys.stdout)
2098 QMessageBox.warning(self, _("Error"), str(e))
2100 def do_process_from_text(self):
2101 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2104 tx = self.tx_from_text(text)
2106 self.show_transaction(tx)
2108 def do_process_from_file(self):
2109 tx = self.read_tx_from_file()
2111 self.show_transaction(tx)
2113 def do_process_from_txid(self):
2114 from electrum import transaction
2115 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2117 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2119 tx = transaction.Transaction(r)
2121 self.show_transaction(tx)
2123 self.show_message("unknown transaction")
2125 def do_process_from_csvReader(self, csvReader):
2130 for position, row in enumerate(csvReader):
2132 if not is_valid(address):
2133 errors.append((position, address))
2135 amount = Decimal(row[1])
2136 amount = int(100000000*amount)
2137 outputs.append((address, amount))
2138 except (ValueError, IOError, os.error), reason:
2139 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2143 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2144 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2148 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2149 except Exception as e:
2150 self.show_message(str(e))
2153 self.show_transaction(tx)
2155 def do_process_from_csv_file(self):
2156 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2160 with open(fileName, "r") as f:
2161 csvReader = csv.reader(f)
2162 self.do_process_from_csvReader(csvReader)
2163 except (ValueError, IOError, os.error), reason:
2164 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2167 def do_process_from_csv_text(self):
2168 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2169 + _("Format: address, amount. One output per line"), _("Load CSV"))
2172 f = StringIO.StringIO(text)
2173 csvReader = csv.reader(f)
2174 self.do_process_from_csvReader(csvReader)
2179 def export_privkeys_dialog(self, password):
2180 if self.wallet.is_watching_only():
2181 self.show_message(_("This is a watching-only wallet"))
2185 d.setWindowTitle(_('Private keys'))
2186 d.setMinimumSize(850, 300)
2187 vbox = QVBoxLayout(d)
2189 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2190 _("Exposing a single private key can compromise your entire wallet!"),
2191 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2192 vbox.addWidget(QLabel(msg))
2198 defaultname = 'electrum-private-keys.csv'
2199 select_msg = _('Select file to export your private keys to')
2200 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2201 vbox.addLayout(hbox)
2203 h, b = ok_cancel_buttons2(d, _('Export'))
2208 addresses = self.wallet.addresses(True)
2210 def privkeys_thread():
2211 for addr in addresses:
2215 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2216 d.emit(SIGNAL('computing_privkeys'))
2217 d.emit(SIGNAL('show_privkeys'))
2219 def show_privkeys():
2220 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2224 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2225 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2226 threading.Thread(target=privkeys_thread).start()
2232 filename = filename_e.text()
2237 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2238 except (IOError, os.error), reason:
2239 export_error_label = _("Electrum was unable to produce a private key-export.")
2240 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2242 except Exception as e:
2243 self.show_message(str(e))
2246 self.show_message(_("Private keys exported."))
2249 def do_export_privkeys(self, fileName, pklist, is_csv):
2250 with open(fileName, "w+") as f:
2252 transaction = csv.writer(f)
2253 transaction.writerow(["address", "private_key"])
2254 for addr, pk in pklist.items():
2255 transaction.writerow(["%34s"%addr,pk])
2258 f.write(json.dumps(pklist, indent = 4))
2261 def do_import_labels(self):
2262 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2263 if not labelsFile: return
2265 f = open(labelsFile, 'r')
2268 for key, value in json.loads(data).items():
2269 self.wallet.set_label(key, value)
2270 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2271 except (IOError, os.error), reason:
2272 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2275 def do_export_labels(self):
2276 labels = self.wallet.labels
2278 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2280 with open(fileName, 'w+') as f:
2281 json.dump(labels, f)
2282 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2283 except (IOError, os.error), reason:
2284 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2287 def export_history_dialog(self):
2290 d.setWindowTitle(_('Export History'))
2291 d.setMinimumSize(400, 200)
2292 vbox = QVBoxLayout(d)
2294 defaultname = os.path.expanduser('~/electrum-history.csv')
2295 select_msg = _('Select file to export your wallet transactions to')
2297 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2298 vbox.addLayout(hbox)
2302 h, b = ok_cancel_buttons2(d, _('Export'))
2307 filename = filename_e.text()
2312 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2313 except (IOError, os.error), reason:
2314 export_error_label = _("Electrum was unable to produce a transaction export.")
2315 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2318 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2321 def do_export_history(self, wallet, fileName, is_csv):
2322 history = wallet.get_tx_history()
2324 for item in history:
2325 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2327 if timestamp is not None:
2329 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2330 except [RuntimeError, TypeError, NameError] as reason:
2331 time_string = "unknown"
2334 time_string = "unknown"
2336 time_string = "pending"
2338 if value is not None:
2339 value_string = format_satoshis(value, True)
2344 fee_string = format_satoshis(fee, True)
2349 label, is_default_label = wallet.get_label(tx_hash)
2350 label = label.encode('utf-8')
2354 balance_string = format_satoshis(balance, False)
2356 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2358 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2360 with open(fileName, "w+") as f:
2362 transaction = csv.writer(f)
2363 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2365 transaction.writerow(line)
2368 f.write(json.dumps(lines, indent = 4))
2371 def sweep_key_dialog(self):
2373 d.setWindowTitle(_('Sweep private keys'))
2374 d.setMinimumSize(600, 300)
2376 vbox = QVBoxLayout(d)
2377 vbox.addWidget(QLabel(_("Enter private keys")))
2379 keys_e = QTextEdit()
2380 keys_e.setTabChangesFocus(True)
2381 vbox.addWidget(keys_e)
2383 h, address_e = address_field(self.wallet.addresses())
2387 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2388 vbox.addLayout(hbox)
2389 button.setEnabled(False)
2392 addr = str(address_e.text())
2393 if bitcoin.is_address(addr):
2397 pk = str(keys_e.toPlainText()).strip()
2398 if Wallet.is_private_key(pk):
2401 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2402 keys_e.textChanged.connect(f)
2403 address_e.textChanged.connect(f)
2407 fee = self.wallet.fee
2408 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2409 self.show_transaction(tx)
2413 def do_import_privkey(self, password):
2414 if not self.wallet.has_imported_keys():
2415 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2416 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2417 + _('Are you sure you understand what you are doing?'), 3, 4)
2420 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2423 text = str(text).split()
2428 addr = self.wallet.import_key(key, password)
2429 except Exception as e:
2435 addrlist.append(addr)
2437 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2439 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2440 self.update_address_tab()
2441 self.update_history_tab()
2444 def settings_dialog(self):
2446 d.setWindowTitle(_('Electrum Settings'))
2448 vbox = QVBoxLayout()
2449 grid = QGridLayout()
2450 grid.setColumnStretch(0,1)
2452 nz_label = QLabel(_('Display zeros') + ':')
2453 grid.addWidget(nz_label, 0, 0)
2454 nz_e = AmountEdit(None,True)
2455 nz_e.setText("%d"% self.num_zeros)
2456 grid.addWidget(nz_e, 0, 1)
2457 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2458 grid.addWidget(HelpButton(msg), 0, 2)
2459 if not self.config.is_modifiable('num_zeros'):
2460 for w in [nz_e, nz_label]: w.setEnabled(False)
2462 lang_label=QLabel(_('Language') + ':')
2463 grid.addWidget(lang_label, 1, 0)
2464 lang_combo = QComboBox()
2465 from electrum.i18n import languages
2466 lang_combo.addItems(languages.values())
2468 index = languages.keys().index(self.config.get("language",''))
2471 lang_combo.setCurrentIndex(index)
2472 grid.addWidget(lang_combo, 1, 1)
2473 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2474 if not self.config.is_modifiable('language'):
2475 for w in [lang_combo, lang_label]: w.setEnabled(False)
2478 fee_label = QLabel(_('Transaction fee') + ':')
2479 grid.addWidget(fee_label, 2, 0)
2480 fee_e = BTCAmountEdit(self.get_decimal_point)
2481 fee_e.setAmount(self.wallet.fee)
2482 grid.addWidget(fee_e, 2, 1)
2483 msg = _('Fee per kilobyte of transaction.') + '\n' \
2484 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2485 grid.addWidget(HelpButton(msg), 2, 2)
2486 if not self.config.is_modifiable('fee_per_kb'):
2487 for w in [fee_e, fee_label]: w.setEnabled(False)
2489 units = ['BTC', 'mBTC']
2490 unit_label = QLabel(_('Base unit') + ':')
2491 grid.addWidget(unit_label, 3, 0)
2492 unit_combo = QComboBox()
2493 unit_combo.addItems(units)
2494 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2495 grid.addWidget(unit_combo, 3, 1)
2496 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2497 + '\n1BTC=1000mBTC.\n' \
2498 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2500 usechange_cb = QCheckBox(_('Use change addresses'))
2501 usechange_cb.setChecked(self.wallet.use_change)
2502 grid.addWidget(usechange_cb, 4, 0)
2503 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2504 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2506 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2507 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2508 grid.addWidget(block_ex_label, 5, 0)
2509 block_ex_combo = QComboBox()
2510 block_ex_combo.addItems(block_explorers)
2511 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2512 grid.addWidget(block_ex_combo, 5, 1)
2513 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2515 show_tx = self.config.get('show_before_broadcast', False)
2516 showtx_cb = QCheckBox(_('Show before broadcast'))
2517 showtx_cb.setChecked(show_tx)
2518 grid.addWidget(showtx_cb, 6, 0)
2519 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2521 vbox.addLayout(grid)
2523 vbox.addLayout(ok_cancel_buttons(d))
2527 if not d.exec_(): return
2529 fee = fee_e.get_amount()
2531 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2534 self.wallet.set_fee(fee)
2536 nz = unicode(nz_e.text())
2541 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2544 if self.num_zeros != nz:
2546 self.config.set_key('num_zeros', nz, True)
2547 self.update_history_tab()
2548 self.update_address_tab()
2550 usechange_result = usechange_cb.isChecked()
2551 if self.wallet.use_change != usechange_result:
2552 self.wallet.use_change = usechange_result
2553 self.wallet.storage.put('use_change', self.wallet.use_change)
2555 if showtx_cb.isChecked() != show_tx:
2556 self.config.set_key('show_before_broadcast', not show_tx)
2558 unit_result = units[unit_combo.currentIndex()]
2559 if self.base_unit() != unit_result:
2560 self.decimal_point = 8 if unit_result == 'BTC' else 5
2561 self.config.set_key('decimal_point', self.decimal_point, True)
2562 self.update_history_tab()
2563 self.update_status()
2565 need_restart = False
2567 lang_request = languages.keys()[lang_combo.currentIndex()]
2568 if lang_request != self.config.get('language'):
2569 self.config.set_key("language", lang_request, True)
2572 be_result = block_explorers[block_ex_combo.currentIndex()]
2573 self.config.set_key('block_explorer', be_result, True)
2575 run_hook('close_settings_dialog')
2578 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2581 def run_network_dialog(self):
2582 if not self.network:
2584 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2586 def closeEvent(self, event):
2588 self.config.set_key("is_maximized", self.isMaximized())
2589 if not self.isMaximized():
2591 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2592 self.save_column_widths()
2593 self.config.set_key("console-history", self.console.history[-50:], True)
2594 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2598 def plugins_dialog(self):
2599 from electrum.plugins import plugins
2602 d.setWindowTitle(_('Electrum Plugins'))
2605 vbox = QVBoxLayout(d)
2608 scroll = QScrollArea()
2609 scroll.setEnabled(True)
2610 scroll.setWidgetResizable(True)
2611 scroll.setMinimumSize(400,250)
2612 vbox.addWidget(scroll)
2616 w.setMinimumHeight(len(plugins)*35)
2618 grid = QGridLayout()
2619 grid.setColumnStretch(0,1)
2622 def do_toggle(cb, p, w):
2625 if w: w.setEnabled(r)
2627 def mk_toggle(cb, p, w):
2628 return lambda: do_toggle(cb,p,w)
2630 for i, p in enumerate(plugins):
2632 cb = QCheckBox(p.fullname())
2633 cb.setDisabled(not p.is_available())
2634 cb.setChecked(p.is_enabled())
2635 grid.addWidget(cb, i, 0)
2636 if p.requires_settings():
2637 w = p.settings_widget(self)
2638 w.setEnabled( p.is_enabled() )
2639 grid.addWidget(w, i, 1)
2642 cb.clicked.connect(mk_toggle(cb,p,w))
2643 grid.addWidget(HelpButton(p.description()), i, 2)
2645 print_msg(_("Error: cannot display plugin"), p)
2646 traceback.print_exc(file=sys.stdout)
2647 grid.setRowStretch(i+1,1)
2649 vbox.addLayout(close_button(d))
2654 def show_account_details(self, k):
2655 account = self.wallet.accounts[k]
2658 d.setWindowTitle(_('Account Details'))
2661 vbox = QVBoxLayout(d)
2662 name = self.wallet.get_account_name(k)
2663 label = QLabel('Name: ' + name)
2664 vbox.addWidget(label)
2666 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2668 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2670 vbox.addWidget(QLabel(_('Master Public Key:')))
2673 text.setReadOnly(True)
2674 text.setMaximumHeight(170)
2675 vbox.addWidget(text)
2677 mpk_text = '\n'.join( account.get_master_pubkeys() )
2678 text.setText(mpk_text)
2680 vbox.addLayout(close_button(d))