3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re, threading
20 from electrum.i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
33 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
34 from electrum.plugins import run_hook
38 from electrum.util import format_satoshis
39 from electrum import Transaction
40 from electrum import mnemonic
41 from electrum import util, bitcoin, commands, Interface, Wallet
42 from electrum import SimpleConfig, Wallet, WalletStorage
44 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
45 from network_dialog import NetworkDialog
46 from qrcodewidget import QRCodeWidget, QRDialog
47 from qrtextedit import QRTextEdit
49 from decimal import Decimal
57 if platform.system() == 'Windows':
58 MONOSPACE_FONT = 'Lucida Console'
59 elif platform.system() == 'Darwin':
60 MONOSPACE_FONT = 'Monaco'
62 MONOSPACE_FONT = 'monospace'
66 # status of payment requests
69 PR_SENT = 2 # sent but not propagated
70 PR_PAID = 3 # send and propagated
71 PR_ERROR = 4 # could not parse
74 from electrum import ELECTRUM_VERSION
89 class StatusBarButton(QPushButton):
90 def __init__(self, icon, tooltip, func):
91 QPushButton.__init__(self, icon, '')
92 self.setToolTip(tooltip)
94 self.setMaximumWidth(25)
95 self.clicked.connect(func)
97 self.setIconSize(QSize(25,25))
99 def keyPressEvent(self, e):
100 if e.key() == QtCore.Qt.Key_Return:
112 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
114 class ElectrumWindow(QMainWindow):
118 def __init__(self, config, network, gui_object):
119 QMainWindow.__init__(self)
122 self.network = network
123 self.gui_object = gui_object
124 self.tray = gui_object.tray
125 self.go_lite = gui_object.go_lite
128 self.create_status_bar()
129 self.need_update = threading.Event()
131 self.decimal_point = config.get('decimal_point', 5)
132 self.num_zeros = int(config.get('num_zeros',0))
135 set_language(config.get('language'))
137 self.completions = QStringListModel()
139 self.tabs = tabs = QTabWidget(self)
140 self.column_widths = self.config.get("column_widths_2", default_column_widths )
141 tabs.addTab(self.create_history_tab(), _('History') )
142 tabs.addTab(self.create_send_tab(), _('Send') )
143 tabs.addTab(self.create_receive_tab(), _('Receive') )
144 tabs.addTab(self.create_addresses_tab(), _('Addresses') )
145 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
146 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
147 tabs.addTab(self.create_console_tab(), _('Console') )
148 tabs.setMinimumSize(600, 400)
149 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
150 self.setCentralWidget(tabs)
152 g = self.config.get("winpos-qt",[100, 100, 840, 400])
153 self.setGeometry(g[0], g[1], g[2], g[3])
154 if self.config.get("is_maximized"):
157 self.setWindowIcon(QIcon(":icons/electrum.png"))
160 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
161 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
162 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
163 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
164 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
166 for i in range(tabs.count()):
167 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
169 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
170 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
171 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
172 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
173 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
175 self.history_list.setFocus(True)
179 self.network.register_callback('updated', lambda: self.need_update.set())
180 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
181 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
182 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
183 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
185 # set initial message
186 self.console.showMessage(self.network.banner)
189 self.payment_request = None
191 def update_account_selector(self):
193 accounts = self.wallet.get_account_names()
194 self.account_selector.clear()
195 if len(accounts) > 1:
196 self.account_selector.addItems([_("All accounts")] + accounts.values())
197 self.account_selector.setCurrentIndex(0)
198 self.account_selector.show()
200 self.account_selector.hide()
203 def load_wallet(self, wallet):
207 self.update_wallet_format()
209 self.invoices = self.wallet.storage.get('invoices', {})
210 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
211 self.current_account = self.wallet.storage.get("current_account", None)
212 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
213 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
214 self.setWindowTitle( title )
216 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
217 self.notify_transactions()
218 self.update_account_selector()
220 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
221 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
222 self.password_menu.setEnabled(not self.wallet.is_watching_only())
223 self.seed_menu.setEnabled(self.wallet.has_seed())
224 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
225 self.import_menu.setEnabled(self.wallet.can_import())
227 self.update_lock_icon()
228 self.update_buttons_on_seed()
229 self.update_console()
231 self.clear_receive_tab()
232 self.update_receive_tab()
233 run_hook('load_wallet', wallet)
236 def update_wallet_format(self):
237 # convert old-format imported keys
238 if self.wallet.imported_keys:
239 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
241 self.wallet.convert_imported_keys(password)
243 self.show_message("error")
246 def open_wallet(self):
247 wallet_folder = self.wallet.storage.path
248 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
252 storage = WalletStorage({'wallet_path': filename})
253 if not storage.file_exists:
254 self.show_message("file not found "+ filename)
257 self.wallet.stop_threads()
260 wallet = Wallet(storage)
261 wallet.start_threads(self.network)
263 self.load_wallet(wallet)
267 def backup_wallet(self):
269 path = self.wallet.storage.path
270 wallet_folder = os.path.dirname(path)
271 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
275 new_path = os.path.join(wallet_folder, filename)
278 shutil.copy2(path, new_path)
279 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
280 except (IOError, os.error), reason:
281 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
284 def new_wallet(self):
287 wallet_folder = os.path.dirname(self.wallet.storage.path)
288 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
291 filename = os.path.join(wallet_folder, filename)
293 storage = WalletStorage({'wallet_path': filename})
294 if storage.file_exists:
295 QMessageBox.critical(None, "Error", _("File exists"))
298 wizard = installwizard.InstallWizard(self.config, self.network, storage)
299 wallet = wizard.run('new')
301 self.load_wallet(wallet)
305 def init_menubar(self):
308 file_menu = menubar.addMenu(_("&File"))
309 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
310 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
311 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
312 file_menu.addAction(_("&Quit"), self.close)
314 wallet_menu = menubar.addMenu(_("&Wallet"))
315 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
316 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
318 wallet_menu.addSeparator()
320 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
321 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
322 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
324 wallet_menu.addSeparator()
325 labels_menu = wallet_menu.addMenu(_("&Labels"))
326 labels_menu.addAction(_("&Import"), self.do_import_labels)
327 labels_menu.addAction(_("&Export"), self.do_export_labels)
329 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
330 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
331 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
332 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
333 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
335 tools_menu = menubar.addMenu(_("&Tools"))
337 # Settings / Preferences are all reserved keywords in OSX using this as work around
338 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
339 tools_menu.addAction(_("&Network"), self.run_network_dialog)
340 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
341 tools_menu.addSeparator()
342 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
343 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
344 tools_menu.addSeparator()
346 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
347 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
348 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
350 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
351 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
352 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
353 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
354 self.raw_transaction_menu = raw_transaction_menu
356 help_menu = menubar.addMenu(_("&Help"))
357 help_menu.addAction(_("&About"), self.show_about)
358 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
359 help_menu.addSeparator()
360 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
361 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
363 self.setMenuBar(menubar)
365 def show_about(self):
366 QMessageBox.about(self, "Electrum",
367 _("Version")+" %s" % (self.wallet.electrum_version) + "\n\n" + _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin. You do not need to perform regular backups, because your wallet can be recovered from a secret phrase that you can memorize or write on paper. Startup times are instant because it operates in conjunction with high-performance servers that handle the most complicated parts of the Bitcoin system."))
369 def show_report_bug(self):
370 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
371 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
374 def notify_transactions(self):
375 if not self.network or not self.network.is_connected():
378 print_error("Notifying GUI")
379 if len(self.network.pending_transactions_for_notifications) > 0:
380 # Combine the transactions if there are more then three
381 tx_amount = len(self.network.pending_transactions_for_notifications)
384 for tx in self.network.pending_transactions_for_notifications:
385 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
389 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
390 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
392 self.network.pending_transactions_for_notifications = []
394 for tx in self.network.pending_transactions_for_notifications:
396 self.network.pending_transactions_for_notifications.remove(tx)
397 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
399 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
401 def notify(self, message):
402 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
406 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
407 def getOpenFileName(self, title, filter = ""):
408 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
409 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
410 if fileName and directory != os.path.dirname(fileName):
411 self.config.set_key('io_dir', os.path.dirname(fileName), True)
414 def getSaveFileName(self, title, filename, filter = ""):
415 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
416 path = os.path.join( directory, filename )
417 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
418 if fileName and directory != os.path.dirname(fileName):
419 self.config.set_key('io_dir', os.path.dirname(fileName), True)
423 QMainWindow.close(self)
424 run_hook('close_main_window')
426 def connect_slots(self, sender):
427 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
428 self.previous_payto_e=''
430 def timer_actions(self):
431 if self.need_update.is_set():
433 self.need_update.clear()
435 run_hook('timer_actions')
437 def format_amount(self, x, is_diff=False, whitespaces=False):
438 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
441 def get_decimal_point(self):
442 return self.decimal_point
446 assert self.decimal_point in [5,8]
447 return "BTC" if self.decimal_point == 8 else "mBTC"
450 def update_status(self):
451 if self.network is None or not self.network.is_running():
453 icon = QIcon(":icons/status_disconnected.png")
455 elif self.network.is_connected():
456 if not self.wallet.up_to_date:
457 text = _("Synchronizing...")
458 icon = QIcon(":icons/status_waiting.png")
459 elif self.network.server_lag > 1:
460 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
461 icon = QIcon(":icons/status_lagging.png")
463 c, u = self.wallet.get_account_balance(self.current_account)
464 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
465 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
467 # append fiat balance and price from exchange rate plugin
469 run_hook('get_fiat_status_text', c+u, r)
474 self.tray.setToolTip(text)
475 icon = QIcon(":icons/status_connected.png")
477 text = _("Not connected")
478 icon = QIcon(":icons/status_disconnected.png")
480 self.balance_label.setText(text)
481 self.status_button.setIcon( icon )
484 def update_wallet(self):
486 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
487 self.update_history_tab()
488 self.update_receive_tab()
489 self.update_address_tab()
490 self.update_contacts_tab()
491 self.update_completions()
492 self.update_invoices_tab()
495 def create_history_tab(self):
496 self.history_list = l = MyTreeWidget(self)
498 for i,width in enumerate(self.column_widths['history']):
499 l.setColumnWidth(i, width)
500 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
501 l.itemDoubleClicked.connect(self.tx_label_clicked)
502 l.itemChanged.connect(self.tx_label_changed)
503 l.customContextMenuRequested.connect(self.create_history_menu)
507 def create_history_menu(self, position):
508 self.history_list.selectedIndexes()
509 item = self.history_list.currentItem()
510 be = self.config.get('block_explorer', 'Blockchain.info')
511 if be == 'Blockchain.info':
512 block_explorer = 'https://blockchain.info/tx/'
513 elif be == 'Blockr.io':
514 block_explorer = 'https://blockr.io/tx/info/'
515 elif be == 'Insight.is':
516 block_explorer = 'http://live.insight.is/tx/'
518 tx_hash = str(item.data(0, Qt.UserRole).toString())
519 if not tx_hash: return
521 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
522 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
523 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
524 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
525 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
528 def show_transaction(self, tx):
529 import transaction_dialog
530 d = transaction_dialog.TxDialog(tx, self)
533 def tx_label_clicked(self, item, column):
534 if column==2 and item.isSelected():
536 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
537 self.history_list.editItem( item, column )
538 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
541 def tx_label_changed(self, item, column):
545 tx_hash = str(item.data(0, Qt.UserRole).toString())
546 tx = self.wallet.transactions.get(tx_hash)
547 text = unicode( item.text(2) )
548 self.wallet.set_label(tx_hash, text)
550 item.setForeground(2, QBrush(QColor('black')))
552 text = self.wallet.get_default_label(tx_hash)
553 item.setText(2, text)
554 item.setForeground(2, QBrush(QColor('gray')))
558 def edit_label(self, is_recv):
559 l = self.address_list if is_recv else self.contacts_list
560 item = l.currentItem()
561 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
562 l.editItem( item, 1 )
563 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
567 def address_label_clicked(self, item, column, l, column_addr, column_label):
568 if column == column_label and item.isSelected():
569 is_editable = item.data(0, 32).toBool()
572 addr = unicode( item.text(column_addr) )
573 label = unicode( item.text(column_label) )
574 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
575 l.editItem( item, column )
576 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
579 def address_label_changed(self, item, column, l, column_addr, column_label):
580 if column == column_label:
581 addr = unicode( item.text(column_addr) )
582 text = unicode( item.text(column_label) )
583 is_editable = item.data(0, 32).toBool()
587 changed = self.wallet.set_label(addr, text)
589 self.update_history_tab()
590 self.update_completions()
592 self.current_item_changed(item)
594 run_hook('item_changed', item, column)
597 def current_item_changed(self, a):
598 run_hook('current_item_changed', a)
602 def update_history_tab(self):
604 self.history_list.clear()
605 for item in self.wallet.get_tx_history(self.current_account):
606 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
607 time_str = _("unknown")
610 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
612 time_str = _("error")
615 time_str = 'unverified'
616 icon = QIcon(":icons/unconfirmed.png")
619 icon = QIcon(":icons/unconfirmed.png")
621 icon = QIcon(":icons/clock%d.png"%conf)
623 icon = QIcon(":icons/confirmed.png")
625 if value is not None:
626 v_str = self.format_amount(value, True, whitespaces=True)
630 balance_str = self.format_amount(balance, whitespaces=True)
633 label, is_default_label = self.wallet.get_label(tx_hash)
635 label = _('Pruned transaction outputs')
636 is_default_label = False
638 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
639 item.setFont(2, QFont(MONOSPACE_FONT))
640 item.setFont(3, QFont(MONOSPACE_FONT))
641 item.setFont(4, QFont(MONOSPACE_FONT))
643 item.setForeground(3, QBrush(QColor("#BC1E1E")))
645 item.setData(0, Qt.UserRole, tx_hash)
646 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
648 item.setForeground(2, QBrush(QColor('grey')))
650 item.setIcon(0, icon)
651 self.history_list.insertTopLevelItem(0,item)
654 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
655 run_hook('history_tab_update')
658 def create_receive_tab(self):
660 grid = QGridLayout(w)
661 grid.setColumnMinimumWidth(3, 300)
662 grid.setColumnStretch(5, 1)
664 self.receive_address_e = QLineEdit()
665 self.receive_address_e.setReadOnly(True)
666 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
667 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
668 self.receive_address_e.textChanged.connect(self.update_receive_qr)
670 self.receive_message_e = QLineEdit()
671 grid.addWidget(QLabel(_('Message')), 1, 0)
672 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
673 self.receive_message_e.textChanged.connect(self.update_receive_qr)
675 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
676 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
677 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
678 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
680 self.save_request_button = QPushButton(_('Save'))
681 self.save_request_button.clicked.connect(self.save_payment_request)
682 grid.addWidget(self.save_request_button, 3, 1)
683 clear_button = QPushButton(_('New'))
684 clear_button.clicked.connect(self.clear_receive_tab)
685 grid.addWidget(clear_button, 3, 2)
686 grid.setRowStretch(4, 1)
688 self.receive_qr = QRCodeWidget(fixedSize=200)
689 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
691 grid.setRowStretch(5, 1)
693 self.receive_requests_label = QLabel(_('Saved Requests'))
694 self.receive_list = MyTreeWidget(self)
695 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
696 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
697 self.receive_list.itemClicked.connect(self.receive_item_changed)
698 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount')] )
699 self.receive_list.setColumnWidth(0, 340)
700 h = self.receive_list.header()
701 h.setStretchLastSection(False)
702 h.setResizeMode(1, QHeaderView.Stretch)
704 grid.addWidget(self.receive_requests_label, 6, 0)
705 grid.addWidget(self.receive_list, 7, 0, 1, 6)
708 def receive_item_changed(self, item):
711 addr = str(item.text(0))
712 amount, message = self.receive_requests[addr]
713 self.receive_address_e.setText(addr)
714 self.receive_message_e.setText(message)
715 self.receive_amount_e.setAmount(amount)
718 def receive_list_delete(self, item):
719 addr = str(item.text(0))
720 self.receive_requests.pop(addr)
721 self.update_receive_tab()
722 self.clear_receive_tab()
724 def receive_list_menu(self, position):
725 item = self.receive_list.itemAt(position)
727 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
728 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
730 def save_payment_request(self):
731 addr = str(self.receive_address_e.text())
732 amount = self.receive_amount_e.get_amount()
733 message = str(self.receive_message_e.text())
734 if not message and not amount:
735 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
737 self.receive_requests = self.wallet.storage.get('receive_requests',{})
738 self.receive_requests[addr] = (amount, message)
739 self.wallet.storage.put('receive_requests', self.receive_requests)
740 self.update_receive_tab()
742 def clear_receive_tab(self):
743 self.receive_requests = self.wallet.storage.get('receive_requests',{})
744 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
746 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
750 self.receive_address_e.setText(addr)
751 self.receive_message_e.setText('')
752 self.receive_amount_e.setAmount(None)
754 def receive_at(self, addr):
755 if not bitcoin.is_address(addr):
757 self.tabs.setCurrentIndex(2)
758 self.receive_address_e.setText(addr)
760 def update_receive_tab(self):
761 self.receive_requests = self.wallet.storage.get('receive_requests',{})
762 b = len(self.receive_requests) > 0
763 self.receive_list.setVisible(b)
764 self.receive_requests_label.setVisible(b)
766 self.receive_list.clear()
767 for address, v in self.receive_requests.items():
769 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else ""] )
770 item.setFont(0, QFont(MONOSPACE_FONT))
771 self.receive_list.addTopLevelItem(item)
774 def update_receive_qr(self):
775 import urlparse, urllib
776 addr = str(self.receive_address_e.text())
777 amount = self.receive_amount_e.get_amount()
778 message = unicode(self.receive_message_e.text()).encode('utf8')
779 self.save_request_button.setEnabled((amount is not None) or (message != ""))
783 query.append('amount=%s'%format_satoshis(amount))
785 query.append('message=%s'%urllib.quote(message))
786 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
787 url = urlparse.urlunparse(p)
790 self.receive_qr.setData(url)
791 run_hook('update_receive_qr', addr, amount, message, url)
794 def create_send_tab(self):
797 self.send_grid = grid = QGridLayout(w)
799 grid.setColumnMinimumWidth(3,300)
800 grid.setColumnStretch(5,1)
801 grid.setRowStretch(8, 1)
803 from paytoedit import PayToEdit
804 self.amount_e = BTCAmountEdit(self.get_decimal_point)
805 self.payto_e = PayToEdit(self)
806 self.payto_help = HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)'))
807 grid.addWidget(QLabel(_('Pay to')), 1, 0)
808 grid.addWidget(self.payto_e, 1, 1, 1, 3)
809 grid.addWidget(self.payto_help, 1, 4)
811 completer = QCompleter()
812 completer.setCaseSensitivity(False)
813 self.payto_e.setCompleter(completer)
814 completer.setModel(self.completions)
816 self.message_e = MyLineEdit()
817 self.message_help = HelpButton(_('Description of the transaction (not mandatory).') + '\n\n' + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.'))
818 grid.addWidget(QLabel(_('Description')), 2, 0)
819 grid.addWidget(self.message_e, 2, 1, 1, 3)
820 grid.addWidget(self.message_help, 2, 4)
822 self.from_label = QLabel(_('From'))
823 grid.addWidget(self.from_label, 3, 0)
824 self.from_list = MyTreeWidget(self)
825 self.from_list.setColumnCount(2)
826 self.from_list.setColumnWidth(0, 350)
827 self.from_list.setColumnWidth(1, 50)
828 self.from_list.setHeaderHidden(True)
829 self.from_list.setMaximumHeight(80)
830 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
831 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
832 grid.addWidget(self.from_list, 3, 1, 1, 3)
833 self.set_pay_from([])
835 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
836 + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') \
837 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
838 grid.addWidget(QLabel(_('Amount')), 4, 0)
839 grid.addWidget(self.amount_e, 4, 1, 1, 2)
840 grid.addWidget(self.amount_help, 4, 3)
842 self.fee_e = BTCAmountEdit(self.get_decimal_point)
843 grid.addWidget(QLabel(_('Fee')), 5, 0)
844 grid.addWidget(self.fee_e, 5, 1, 1, 2)
845 grid.addWidget(HelpButton(
846 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
847 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
848 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 5, 3)
850 self.send_button = EnterButton(_("Send"), self.do_send)
851 grid.addWidget(self.send_button, 6, 1)
853 b = EnterButton(_("Clear"), self.do_clear)
854 grid.addWidget(b, 6, 2)
856 self.payto_sig = QLabel('')
857 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
859 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
860 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
863 def entry_changed( is_fee ):
865 if self.amount_e.is_shortcut:
866 self.amount_e.is_shortcut = False
867 sendable = self.get_sendable_balance()
868 # there is only one output because we are completely spending inputs
869 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
870 fee = self.wallet.estimated_fee(inputs, 1)
872 self.amount_e.setAmount(amount)
873 self.amount_e.textEdited.emit("")
874 self.fee_e.setAmount(fee)
877 amount = self.amount_e.get_amount()
878 fee = self.fee_e.get_amount()
879 outputs = self.payto_e.get_outputs()
885 self.fee_e.setAmount(None)
886 not_enough_funds = False
888 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, len(outputs), coins = self.get_coins())
889 not_enough_funds = len(inputs) == 0
891 self.fee_e.setAmount(fee)
893 if not not_enough_funds:
895 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
899 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
900 text = _( "Not enough funds" )
901 c, u = self.wallet.get_frozen_balance()
902 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
904 self.statusBar().showMessage(text)
905 self.amount_e.setPalette(palette)
906 self.fee_e.setPalette(palette)
908 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
909 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
911 run_hook('create_send_tab', grid)
914 def from_list_delete(self, item):
915 i = self.from_list.indexOfTopLevelItem(item)
917 self.redraw_from_list()
919 def from_list_menu(self, position):
920 item = self.from_list.itemAt(position)
922 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
923 menu.exec_(self.from_list.viewport().mapToGlobal(position))
925 def set_pay_from(self, domain = None):
926 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
927 self.redraw_from_list()
929 def redraw_from_list(self):
930 self.from_list.clear()
931 self.from_label.setHidden(len(self.pay_from) == 0)
932 self.from_list.setHidden(len(self.pay_from) == 0)
935 h = x.get('prevout_hash')
936 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
938 for item in self.pay_from:
939 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
941 def update_completions(self):
943 for addr,label in self.wallet.labels.items():
944 if addr in self.wallet.addressbook:
945 l.append( label + ' <' + addr + '>')
947 run_hook('update_completions', l)
948 self.completions.setStringList(l)
952 return lambda s, *args: s.do_protect(func, args)
955 def read_send_tab(self):
957 if self.payment_request and self.payment_request.has_expired():
958 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
961 label = unicode( self.message_e.text() )
963 if self.payment_request:
964 outputs = self.payment_request.get_outputs()
966 outputs = self.payto_e.get_outputs()
969 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
972 for addr, x in outputs:
973 if addr is None or not bitcoin.is_address(addr):
974 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
977 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
980 amount = sum(map(lambda x:x[1], outputs))
982 fee = self.fee_e.get_amount()
984 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
987 confirm_amount = self.config.get('confirm_amount', 100000000)
988 if amount >= confirm_amount:
989 o = '\n'.join(map(lambda x:x[0], outputs))
990 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
993 confirm_fee = self.config.get('confirm_fee', 100000)
994 if fee >= confirm_fee:
995 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()}):
998 coins = self.get_coins()
999 return outputs, fee, label, coins
1003 r = self.read_send_tab()
1006 outputs, fee, label, coins = r
1007 self.send_tx(outputs, fee, label, coins)
1011 def send_tx(self, outputs, fee, label, coins, password):
1012 self.send_button.setDisabled(True)
1014 # first, create an unsigned tx
1016 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1018 except Exception as e:
1019 traceback.print_exc(file=sys.stdout)
1020 self.show_message(str(e))
1021 self.send_button.setDisabled(False)
1024 # call hook to see if plugin needs gui interaction
1025 run_hook('send_tx', tx)
1031 self.wallet.add_keypairs(tx, keypairs, password)
1032 self.wallet.sign_transaction(tx, keypairs, password)
1033 except Exception as e:
1039 self.show_message(tx.error)
1040 self.send_button.setDisabled(False)
1042 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1043 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1044 self.send_button.setDisabled(False)
1047 self.wallet.set_label(tx.hash(), label)
1049 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1050 self.show_transaction(tx)
1052 self.send_button.setDisabled(False)
1055 self.broadcast_transaction(tx)
1057 # keep a reference to WaitingDialog or the gui might crash
1058 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1059 self.waiting_dialog.start()
1063 def broadcast_transaction(self, tx):
1065 def broadcast_thread():
1066 pr = self.payment_request
1068 return self.wallet.sendtx(tx)
1070 if pr.has_expired():
1071 self.payment_request = None
1072 return False, _("Payment request has expired")
1074 status, msg = self.wallet.sendtx(tx)
1078 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1079 self.wallet.storage.put('invoices', self.invoices)
1080 self.update_invoices_tab()
1081 self.payment_request = None
1082 refund_address = self.wallet.addresses()[0]
1083 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1089 def broadcast_done(status, msg):
1091 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1094 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1095 self.send_button.setDisabled(False)
1097 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1098 self.waiting_dialog.start()
1102 def prepare_for_payment_request(self):
1103 self.tabs.setCurrentIndex(1)
1104 self.payto_e.is_pr = True
1105 for e in [self.payto_e, self.amount_e, self.message_e]:
1107 for h in [self.payto_help, self.amount_help, self.message_help]:
1109 self.payto_e.setText(_("please wait..."))
1112 def payment_request_ok(self):
1113 pr = self.payment_request
1115 if pr_id not in self.invoices:
1116 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1117 self.wallet.storage.put('invoices', self.invoices)
1118 self.update_invoices_tab()
1120 print_error('invoice already in list')
1122 status = self.invoices[pr_id][4]
1123 if status == PR_PAID:
1125 self.show_message("invoice already paid")
1126 self.payment_request = None
1129 self.payto_help.show()
1130 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1132 if not pr.has_expired():
1133 self.payto_e.setGreen()
1135 self.payto_e.setExpired()
1137 self.payto_e.setText(pr.domain)
1138 self.amount_e.setText(self.format_amount(pr.get_amount()))
1139 self.message_e.setText(pr.get_memo())
1141 def payment_request_error(self):
1143 self.show_message(self.payment_request.error)
1144 self.payment_request = None
1146 def pay_from_URI(self,URI):
1149 address, amount, label, message, request_url = util.parse_URI(URI)
1151 address, amount, label, message, request_url = util.parse_URI(URI)
1152 except Exception as e:
1153 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1156 self.tabs.setCurrentIndex(1)
1160 if self.wallet.labels.get(address) != label:
1161 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1162 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1163 self.wallet.addressbook.append(address)
1164 self.wallet.set_label(address, label)
1166 label = self.wallet.labels.get(address)
1168 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1170 self.message_e.setText(message)
1172 self.amount_e.setAmount(amount)
1175 from electrum import paymentrequest
1176 def payment_request():
1177 self.payment_request = paymentrequest.PaymentRequest(self.config)
1178 self.payment_request.read(request_url)
1179 if self.payment_request.verify():
1180 self.emit(SIGNAL('payment_request_ok'))
1182 self.emit(SIGNAL('payment_request_error'))
1184 self.pr_thread = threading.Thread(target=payment_request).start()
1185 self.prepare_for_payment_request()
1190 self.payto_e.is_pr = False
1191 self.payto_sig.setVisible(False)
1192 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1196 for h in [self.payto_help, self.amount_help, self.message_help]:
1199 self.payto_help.set_alt(None)
1200 self.set_pay_from([])
1201 self.update_status()
1205 def set_addrs_frozen(self,addrs,freeze):
1207 if not addr: continue
1208 if addr in self.wallet.frozen_addresses and not freeze:
1209 self.wallet.unfreeze(addr)
1210 elif addr not in self.wallet.frozen_addresses and freeze:
1211 self.wallet.freeze(addr)
1212 self.update_address_tab()
1216 def create_list_tab(self, headers):
1217 "generic tab creation method"
1218 l = MyTreeWidget(self)
1219 l.setColumnCount( len(headers) )
1220 l.setHeaderLabels( headers )
1223 vbox = QVBoxLayout()
1230 vbox.addWidget(buttons)
1235 def create_addresses_tab(self):
1236 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1237 for i,width in enumerate(self.column_widths['receive']):
1238 l.setColumnWidth(i, width)
1239 l.setContextMenuPolicy(Qt.CustomContextMenu)
1240 l.customContextMenuRequested.connect(self.create_receive_menu)
1241 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1242 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1243 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1244 l.currentItemChanged.connect(lambda a,b: self.current_item_changed(a))
1245 self.address_list = l
1251 def save_column_widths(self):
1252 self.column_widths["receive"] = []
1253 for i in range(self.address_list.columnCount() -1):
1254 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1256 self.column_widths["history"] = []
1257 for i in range(self.history_list.columnCount() - 1):
1258 self.column_widths["history"].append(self.history_list.columnWidth(i))
1260 self.column_widths["contacts"] = []
1261 for i in range(self.contacts_list.columnCount() - 1):
1262 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1264 self.config.set_key("column_widths_2", self.column_widths, True)
1267 def create_contacts_tab(self):
1268 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1269 l.setContextMenuPolicy(Qt.CustomContextMenu)
1270 l.customContextMenuRequested.connect(self.create_contact_menu)
1271 for i,width in enumerate(self.column_widths['contacts']):
1272 l.setColumnWidth(i, width)
1273 l.itemDoubleClicked.connect(lambda a, b: self.address_label_clicked(a,b,l,0,1))
1274 l.itemChanged.connect(lambda a,b: self.address_label_changed(a,b,l,0,1))
1275 self.contacts_list = l
1279 def create_invoices_tab(self):
1280 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1281 l.setColumnWidth(0, 150)
1283 h.setStretchLastSection(False)
1284 h.setResizeMode(1, QHeaderView.Stretch)
1285 l.setContextMenuPolicy(Qt.CustomContextMenu)
1286 l.customContextMenuRequested.connect(self.create_invoice_menu)
1287 self.invoices_list = l
1290 def update_invoices_tab(self):
1291 invoices = self.wallet.storage.get('invoices', {})
1292 l = self.invoices_list
1294 for key, value in invoices.items():
1296 domain, memo, amount, expiration_date, status, tx_hash = value
1300 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1302 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1303 l.addTopLevelItem(item)
1305 l.setCurrentItem(l.topLevelItem(0))
1309 def delete_imported_key(self, addr):
1310 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1311 self.wallet.delete_imported_key(addr)
1312 self.update_address_tab()
1313 self.update_history_tab()
1315 def edit_account_label(self, k):
1316 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1318 label = unicode(text)
1319 self.wallet.set_label(k,label)
1320 self.update_address_tab()
1322 def account_set_expanded(self, item, k, b):
1324 self.accounts_expanded[k] = b
1326 def create_account_menu(self, position, k, item):
1328 if item.isExpanded():
1329 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1331 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1332 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1333 if self.wallet.seed_version > 4:
1334 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1335 if self.wallet.account_is_pending(k):
1336 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1337 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1339 def delete_pending_account(self, k):
1340 self.wallet.delete_pending_account(k)
1341 self.update_address_tab()
1343 def create_receive_menu(self, position):
1344 # fixme: this function apparently has a side effect.
1345 # if it is not called the menu pops up several times
1346 #self.address_list.selectedIndexes()
1348 selected = self.address_list.selectedItems()
1349 multi_select = len(selected) > 1
1350 addrs = [unicode(item.text(0)) for item in selected]
1351 if not multi_select:
1352 item = self.address_list.itemAt(position)
1356 if not is_valid(addr):
1357 k = str(item.data(0,32).toString())
1359 self.create_account_menu(position, k, item)
1361 item.setExpanded(not item.isExpanded())
1365 if not multi_select:
1366 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1367 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1368 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1369 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1370 if not self.wallet.is_watching_only():
1371 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1372 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1373 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1374 if self.wallet.is_imported(addr):
1375 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1377 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1378 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1379 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1380 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1383 return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1384 if any(can_send(addr) for addr in addrs):
1385 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1387 run_hook('receive_menu', menu, addrs)
1388 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1391 def get_sendable_balance(self):
1392 return sum(map(lambda x:x['value'], self.get_coins()))
1395 def get_coins(self):
1397 return self.pay_from
1399 domain = self.wallet.get_account_addresses(self.current_account)
1400 for i in self.wallet.frozen_addresses:
1401 if i in domain: domain.remove(i)
1402 return self.wallet.get_unspent_coins(domain)
1405 def send_from_addresses(self, addrs):
1406 self.set_pay_from( addrs )
1407 self.tabs.setCurrentIndex(1)
1410 def payto(self, addr):
1412 label = self.wallet.labels.get(addr)
1413 m_addr = label + ' <' + addr + '>' if label else addr
1414 self.tabs.setCurrentIndex(1)
1415 self.payto_e.setText(m_addr)
1416 self.amount_e.setFocus()
1419 def delete_contact(self, x):
1420 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1421 self.wallet.delete_contact(x)
1422 self.wallet.set_label(x, None)
1423 self.update_history_tab()
1424 self.update_contacts_tab()
1425 self.update_completions()
1428 def create_contact_menu(self, position):
1429 item = self.contacts_list.itemAt(position)
1432 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1434 addr = unicode(item.text(0))
1435 label = unicode(item.text(1))
1436 is_editable = item.data(0,32).toBool()
1437 payto_addr = item.data(0,33).toString()
1438 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1439 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1440 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1442 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1443 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1445 run_hook('create_contact_menu', menu, item)
1446 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1448 def delete_invoice(self, key):
1449 self.invoices.pop(key)
1450 self.wallet.storage.put('invoices', self.invoices)
1451 self.update_invoices_tab()
1453 def show_invoice(self, key):
1454 from electrum.paymentrequest import PaymentRequest
1455 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1456 pr = PaymentRequest(self.config)
1460 self.show_pr_details(pr)
1462 def show_pr_details(self, pr):
1463 msg = 'Domain: ' + pr.domain
1464 msg += '\nStatus: ' + pr.get_status()
1465 msg += '\nMemo: ' + pr.get_memo()
1466 msg += '\nPayment URL: ' + pr.payment_url
1467 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1468 QMessageBox.information(self, 'Invoice', msg , 'OK')
1470 def do_pay_invoice(self, key):
1471 from electrum.paymentrequest import PaymentRequest
1472 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1473 pr = PaymentRequest(self.config)
1476 self.payment_request = pr
1477 self.prepare_for_payment_request()
1479 self.payment_request_ok()
1481 self.payment_request_error()
1484 def create_invoice_menu(self, position):
1485 item = self.invoices_list.itemAt(position)
1488 k = self.invoices_list.indexOfTopLevelItem(item)
1489 key = self.invoices.keys()[k]
1490 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1492 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1493 if status == PR_UNPAID:
1494 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1495 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1496 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1500 def update_address_tab(self):
1501 l = self.address_list
1502 # extend the syntax for consistency
1503 l.addChild = l.addTopLevelItem
1504 l.insertChild = l.insertTopLevelItem
1508 accounts = self.wallet.get_accounts()
1509 if self.current_account is None:
1510 account_items = sorted(accounts.items())
1512 account_items = [(self.current_account, accounts.get(self.current_account))]
1515 for k, account in account_items:
1517 if len(accounts) > 1:
1518 name = self.wallet.get_account_name(k)
1519 c,u = self.wallet.get_account_balance(k)
1520 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1521 l.addTopLevelItem(account_item)
1522 account_item.setExpanded(self.accounts_expanded.get(k, True))
1523 account_item.setData(0, 32, k)
1527 sequences = [0,1] if account.has_change() else [0]
1528 for is_change in sequences:
1529 if len(sequences) > 1:
1530 name = _("Receiving") if not is_change else _("Change")
1531 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1532 account_item.addChild(seq_item)
1534 seq_item.setExpanded(True)
1536 seq_item = account_item
1538 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1541 addr_list = account.get_addresses(is_change)
1542 for address in addr_list:
1543 num, is_used = self.wallet.is_used(address)
1544 label = self.wallet.labels.get(address,'')
1545 c, u = self.wallet.get_addr_balance(address)
1546 balance = self.format_amount(c + u)
1547 item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1548 item.setFont(0, QFont(MONOSPACE_FONT))
1549 item.setData(0, 32, True) # label can be edited
1550 if address in self.wallet.frozen_addresses:
1551 item.setBackgroundColor(0, QColor('lightblue'))
1552 if self.wallet.is_beyond_limit(address, account, is_change):
1553 item.setBackgroundColor(0, QColor('red'))
1556 seq_item.insertChild(0, used_item)
1558 used_item.addChild(item)
1560 seq_item.addChild(item)
1562 # we use column 1 because column 0 may be hidden
1563 l.setCurrentItem(l.topLevelItem(0),1)
1566 def update_contacts_tab(self):
1567 l = self.contacts_list
1570 for address in self.wallet.addressbook:
1571 label = self.wallet.labels.get(address,'')
1572 n = self.wallet.get_num_tx(address)
1573 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1574 item.setFont(0, QFont(MONOSPACE_FONT))
1575 # 32 = label can be edited (bool)
1576 item.setData(0,32, True)
1578 item.setData(0,33, address)
1579 l.addTopLevelItem(item)
1581 run_hook('update_contacts_tab', l)
1582 l.setCurrentItem(l.topLevelItem(0))
1585 def create_console_tab(self):
1586 from console import Console
1587 self.console = console = Console()
1591 def update_console(self):
1592 console = self.console
1593 console.history = self.config.get("console-history",[])
1594 console.history_index = len(console.history)
1596 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1597 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1599 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1601 def mkfunc(f, method):
1602 return lambda *args: apply( f, (method, args, self.password_dialog ))
1604 if m[0]=='_' or m in ['network','wallet']: continue
1605 methods[m] = mkfunc(c._run, m)
1607 console.updateNamespace(methods)
1610 def change_account(self,s):
1611 if s == _("All accounts"):
1612 self.current_account = None
1614 accounts = self.wallet.get_account_names()
1615 for k, v in accounts.items():
1617 self.current_account = k
1618 self.update_history_tab()
1619 self.update_status()
1620 self.update_address_tab()
1621 self.update_receive_tab()
1623 def create_status_bar(self):
1626 sb.setFixedHeight(35)
1627 qtVersion = qVersion()
1629 self.balance_label = QLabel("")
1630 sb.addWidget(self.balance_label)
1632 from version_getter import UpdateLabel
1633 self.updatelabel = UpdateLabel(self.config, sb)
1635 self.account_selector = QComboBox()
1636 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1637 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1638 sb.addPermanentWidget(self.account_selector)
1640 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1641 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1643 self.lock_icon = QIcon()
1644 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1645 sb.addPermanentWidget( self.password_button )
1647 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1648 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1649 sb.addPermanentWidget( self.seed_button )
1650 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1651 sb.addPermanentWidget( self.status_button )
1653 run_hook('create_status_bar', (sb,))
1655 self.setStatusBar(sb)
1658 def update_lock_icon(self):
1659 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1660 self.password_button.setIcon( icon )
1663 def update_buttons_on_seed(self):
1664 if self.wallet.has_seed():
1665 self.seed_button.show()
1667 self.seed_button.hide()
1669 if not self.wallet.is_watching_only():
1670 self.password_button.show()
1671 self.send_button.setText(_("Send"))
1673 self.password_button.hide()
1674 self.send_button.setText(_("Create unsigned transaction"))
1677 def change_password_dialog(self):
1678 from password_dialog import PasswordDialog
1679 d = PasswordDialog(self.wallet, self)
1681 self.update_lock_icon()
1684 def new_contact_dialog(self):
1687 d.setWindowTitle(_("New Contact"))
1688 vbox = QVBoxLayout(d)
1689 vbox.addWidget(QLabel(_('New Contact')+':'))
1691 grid = QGridLayout()
1694 grid.addWidget(QLabel(_("Address")), 1, 0)
1695 grid.addWidget(line1, 1, 1)
1696 grid.addWidget(QLabel(_("Name")), 2, 0)
1697 grid.addWidget(line2, 2, 1)
1699 vbox.addLayout(grid)
1700 vbox.addLayout(ok_cancel_buttons(d))
1705 address = str(line1.text())
1706 label = unicode(line2.text())
1708 if not is_valid(address):
1709 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1712 self.wallet.add_contact(address)
1714 self.wallet.set_label(address, label)
1716 self.update_contacts_tab()
1717 self.update_history_tab()
1718 self.update_completions()
1719 self.tabs.setCurrentIndex(3)
1723 def new_account_dialog(self, password):
1725 dialog = QDialog(self)
1727 dialog.setWindowTitle(_("New Account"))
1729 vbox = QVBoxLayout()
1730 vbox.addWidget(QLabel(_('Account name')+':'))
1733 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1734 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1739 vbox.addLayout(ok_cancel_buttons(dialog))
1740 dialog.setLayout(vbox)
1744 name = str(e.text())
1747 self.wallet.create_pending_account(name, password)
1748 self.update_address_tab()
1749 self.tabs.setCurrentIndex(2)
1754 def show_master_public_keys(self):
1756 dialog = QDialog(self)
1758 dialog.setWindowTitle(_("Master Public Keys"))
1760 main_layout = QGridLayout()
1761 mpk_dict = self.wallet.get_master_public_keys()
1763 for key, value in mpk_dict.items():
1764 main_layout.addWidget(QLabel(key), i, 0)
1765 mpk_text = QTextEdit()
1766 mpk_text.setReadOnly(True)
1767 mpk_text.setMaximumHeight(170)
1768 mpk_text.setText(value)
1769 main_layout.addWidget(mpk_text, i + 1, 0)
1772 vbox = QVBoxLayout()
1773 vbox.addLayout(main_layout)
1774 vbox.addLayout(close_button(dialog))
1776 dialog.setLayout(vbox)
1781 def show_seed_dialog(self, password):
1782 if not self.wallet.has_seed():
1783 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1787 mnemonic = self.wallet.get_mnemonic(password)
1789 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1791 from seed_dialog import SeedDialog
1792 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1797 def show_qrcode(self, data, title = _("QR code")):
1800 d = QRDialog(data, self, title)
1804 def do_protect(self, func, args):
1805 if self.wallet.use_encryption:
1806 password = self.password_dialog()
1812 if args != (False,):
1813 args = (self,) + args + (password,)
1815 args = (self,password)
1819 def show_public_keys(self, address):
1820 if not address: return
1822 pubkey_list = self.wallet.get_public_keys(address)
1823 except Exception as e:
1824 traceback.print_exc(file=sys.stdout)
1825 self.show_message(str(e))
1829 d.setMinimumSize(600, 200)
1831 vbox = QVBoxLayout()
1832 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1833 vbox.addWidget( QLabel(_("Public key") + ':'))
1835 keys.setReadOnly(True)
1836 keys.setText('\n'.join(pubkey_list))
1837 vbox.addWidget(keys)
1838 vbox.addLayout(close_button(d))
1843 def show_private_key(self, address, password):
1844 if not address: return
1846 pk_list = self.wallet.get_private_key(address, password)
1847 except Exception as e:
1848 traceback.print_exc(file=sys.stdout)
1849 self.show_message(str(e))
1853 d.setMinimumSize(600, 200)
1855 vbox = QVBoxLayout()
1856 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1857 vbox.addWidget( QLabel(_("Private key") + ':'))
1859 keys.setReadOnly(True)
1860 keys.setText('\n'.join(pk_list))
1861 vbox.addWidget(keys)
1862 vbox.addLayout(close_button(d))
1868 def do_sign(self, address, message, signature, password):
1869 message = unicode(message.toPlainText())
1870 message = message.encode('utf-8')
1872 sig = self.wallet.sign_message(str(address.text()), message, password)
1873 signature.setText(sig)
1874 except Exception as e:
1875 self.show_message(str(e))
1877 def do_verify(self, address, message, signature):
1878 message = unicode(message.toPlainText())
1879 message = message.encode('utf-8')
1880 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1881 self.show_message(_("Signature verified"))
1883 self.show_message(_("Error: wrong signature"))
1886 def sign_verify_message(self, address=''):
1889 d.setWindowTitle(_('Sign/verify Message'))
1890 d.setMinimumSize(410, 290)
1892 layout = QGridLayout(d)
1894 message_e = QTextEdit()
1895 layout.addWidget(QLabel(_('Message')), 1, 0)
1896 layout.addWidget(message_e, 1, 1)
1897 layout.setRowStretch(2,3)
1899 address_e = QLineEdit()
1900 address_e.setText(address)
1901 layout.addWidget(QLabel(_('Address')), 2, 0)
1902 layout.addWidget(address_e, 2, 1)
1904 signature_e = QTextEdit()
1905 layout.addWidget(QLabel(_('Signature')), 3, 0)
1906 layout.addWidget(signature_e, 3, 1)
1907 layout.setRowStretch(3,1)
1909 hbox = QHBoxLayout()
1911 b = QPushButton(_("Sign"))
1912 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1915 b = QPushButton(_("Verify"))
1916 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1919 b = QPushButton(_("Close"))
1920 b.clicked.connect(d.accept)
1922 layout.addLayout(hbox, 4, 1)
1927 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1929 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1930 message_e.setText(decrypted)
1931 except Exception as e:
1932 self.show_message(str(e))
1935 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1936 message = unicode(message_e.toPlainText())
1937 message = message.encode('utf-8')
1939 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1940 encrypted_e.setText(encrypted)
1941 except Exception as e:
1942 self.show_message(str(e))
1946 def encrypt_message(self, address = ''):
1949 d.setWindowTitle(_('Encrypt/decrypt Message'))
1950 d.setMinimumSize(610, 490)
1952 layout = QGridLayout(d)
1954 message_e = QTextEdit()
1955 layout.addWidget(QLabel(_('Message')), 1, 0)
1956 layout.addWidget(message_e, 1, 1)
1957 layout.setRowStretch(2,3)
1959 pubkey_e = QLineEdit()
1961 pubkey = self.wallet.getpubkeys(address)[0]
1962 pubkey_e.setText(pubkey)
1963 layout.addWidget(QLabel(_('Public key')), 2, 0)
1964 layout.addWidget(pubkey_e, 2, 1)
1966 encrypted_e = QTextEdit()
1967 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1968 layout.addWidget(encrypted_e, 3, 1)
1969 layout.setRowStretch(3,1)
1971 hbox = QHBoxLayout()
1972 b = QPushButton(_("Encrypt"))
1973 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1976 b = QPushButton(_("Decrypt"))
1977 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1980 b = QPushButton(_("Close"))
1981 b.clicked.connect(d.accept)
1984 layout.addLayout(hbox, 4, 1)
1988 def question(self, msg):
1989 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1991 def show_message(self, msg):
1992 QMessageBox.information(self, _('Message'), msg, _('OK'))
1994 def password_dialog(self, msg=None):
1997 d.setWindowTitle(_("Enter Password"))
2002 vbox = QVBoxLayout()
2004 msg = _('Please enter your password')
2005 vbox.addWidget(QLabel(msg))
2007 grid = QGridLayout()
2009 grid.addWidget(QLabel(_('Password')), 1, 0)
2010 grid.addWidget(pw, 1, 1)
2011 vbox.addLayout(grid)
2013 vbox.addLayout(ok_cancel_buttons(d))
2016 run_hook('password_dialog', pw, grid, 1)
2017 if not d.exec_(): return
2018 return unicode(pw.text())
2027 def tx_from_text(self, txt):
2028 "json or raw hexadecimal"
2037 return Transaction(txt)
2039 traceback.print_exc(file=sys.stdout)
2040 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2044 tx_dict = json.loads(str(txt))
2045 assert "hex" in tx_dict.keys()
2046 tx = Transaction(tx_dict["hex"])
2047 #if tx_dict.has_key("input_info"):
2048 # input_info = json.loads(tx_dict['input_info'])
2049 # tx.add_input_info(input_info)
2052 traceback.print_exc(file=sys.stdout)
2053 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2057 def read_tx_from_file(self):
2058 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2062 with open(fileName, "r") as f:
2063 file_content = f.read()
2064 except (ValueError, IOError, os.error), reason:
2065 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2067 return self.tx_from_text(file_content)
2071 def sign_raw_transaction(self, tx, password):
2073 self.wallet.signrawtransaction(tx, [], password)
2074 except Exception as e:
2075 traceback.print_exc(file=sys.stdout)
2076 QMessageBox.warning(self, _("Error"), str(e))
2078 def do_process_from_text(self):
2079 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2082 tx = self.tx_from_text(text)
2084 self.show_transaction(tx)
2086 def do_process_from_file(self):
2087 tx = self.read_tx_from_file()
2089 self.show_transaction(tx)
2091 def do_process_from_txid(self):
2092 from electrum import transaction
2093 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2095 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2097 tx = transaction.Transaction(r)
2099 self.show_transaction(tx)
2101 self.show_message("unknown transaction")
2103 def do_process_from_csvReader(self, csvReader):
2108 for position, row in enumerate(csvReader):
2110 if not is_valid(address):
2111 errors.append((position, address))
2113 amount = Decimal(row[1])
2114 amount = int(100000000*amount)
2115 outputs.append((address, amount))
2116 except (ValueError, IOError, os.error), reason:
2117 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2121 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2122 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2126 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2127 except Exception as e:
2128 self.show_message(str(e))
2131 self.show_transaction(tx)
2133 def do_process_from_csv_file(self):
2134 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2138 with open(fileName, "r") as f:
2139 csvReader = csv.reader(f)
2140 self.do_process_from_csvReader(csvReader)
2141 except (ValueError, IOError, os.error), reason:
2142 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2145 def do_process_from_csv_text(self):
2146 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2147 + _("Format: address, amount. One output per line"), _("Load CSV"))
2150 f = StringIO.StringIO(text)
2151 csvReader = csv.reader(f)
2152 self.do_process_from_csvReader(csvReader)
2157 def export_privkeys_dialog(self, password):
2158 if self.wallet.is_watching_only():
2159 self.show_message(_("This is a watching-only wallet"))
2163 d.setWindowTitle(_('Private keys'))
2164 d.setMinimumSize(850, 300)
2165 vbox = QVBoxLayout(d)
2167 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2168 _("Exposing a single private key can compromise your entire wallet!"),
2169 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2170 vbox.addWidget(QLabel(msg))
2176 defaultname = 'electrum-private-keys.csv'
2177 select_msg = _('Select file to export your private keys to')
2178 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2179 vbox.addLayout(hbox)
2181 h, b = ok_cancel_buttons2(d, _('Export'))
2186 addresses = self.wallet.addresses(True)
2188 def privkeys_thread():
2189 for addr in addresses:
2193 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2194 d.emit(SIGNAL('computing_privkeys'))
2195 d.emit(SIGNAL('show_privkeys'))
2197 def show_privkeys():
2198 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2202 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2203 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2204 threading.Thread(target=privkeys_thread).start()
2210 filename = filename_e.text()
2215 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2216 except (IOError, os.error), reason:
2217 export_error_label = _("Electrum was unable to produce a private key-export.")
2218 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2220 except Exception as e:
2221 self.show_message(str(e))
2224 self.show_message(_("Private keys exported."))
2227 def do_export_privkeys(self, fileName, pklist, is_csv):
2228 with open(fileName, "w+") as f:
2230 transaction = csv.writer(f)
2231 transaction.writerow(["address", "private_key"])
2232 for addr, pk in pklist.items():
2233 transaction.writerow(["%34s"%addr,pk])
2236 f.write(json.dumps(pklist, indent = 4))
2239 def do_import_labels(self):
2240 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2241 if not labelsFile: return
2243 f = open(labelsFile, 'r')
2246 for key, value in json.loads(data).items():
2247 self.wallet.set_label(key, value)
2248 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2249 except (IOError, os.error), reason:
2250 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2253 def do_export_labels(self):
2254 labels = self.wallet.labels
2256 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2258 with open(fileName, 'w+') as f:
2259 json.dump(labels, f)
2260 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2261 except (IOError, os.error), reason:
2262 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2265 def export_history_dialog(self):
2268 d.setWindowTitle(_('Export History'))
2269 d.setMinimumSize(400, 200)
2270 vbox = QVBoxLayout(d)
2272 defaultname = os.path.expanduser('~/electrum-history.csv')
2273 select_msg = _('Select file to export your wallet transactions to')
2275 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2276 vbox.addLayout(hbox)
2280 h, b = ok_cancel_buttons2(d, _('Export'))
2285 filename = filename_e.text()
2290 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2291 except (IOError, os.error), reason:
2292 export_error_label = _("Electrum was unable to produce a transaction export.")
2293 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2296 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2299 def do_export_history(self, wallet, fileName, is_csv):
2300 history = wallet.get_tx_history()
2302 for item in history:
2303 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2305 if timestamp is not None:
2307 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2308 except [RuntimeError, TypeError, NameError] as reason:
2309 time_string = "unknown"
2312 time_string = "unknown"
2314 time_string = "pending"
2316 if value is not None:
2317 value_string = format_satoshis(value, True)
2322 fee_string = format_satoshis(fee, True)
2327 label, is_default_label = wallet.get_label(tx_hash)
2328 label = label.encode('utf-8')
2332 balance_string = format_satoshis(balance, False)
2334 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2336 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2338 with open(fileName, "w+") as f:
2340 transaction = csv.writer(f)
2341 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2343 transaction.writerow(line)
2346 f.write(json.dumps(lines, indent = 4))
2349 def sweep_key_dialog(self):
2351 d.setWindowTitle(_('Sweep private keys'))
2352 d.setMinimumSize(600, 300)
2354 vbox = QVBoxLayout(d)
2355 vbox.addWidget(QLabel(_("Enter private keys")))
2357 keys_e = QTextEdit()
2358 keys_e.setTabChangesFocus(True)
2359 vbox.addWidget(keys_e)
2361 h, address_e = address_field(self.wallet.addresses())
2365 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2366 vbox.addLayout(hbox)
2367 button.setEnabled(False)
2370 addr = str(address_e.text())
2371 if bitcoin.is_address(addr):
2375 pk = str(keys_e.toPlainText()).strip()
2376 if Wallet.is_private_key(pk):
2379 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2380 keys_e.textChanged.connect(f)
2381 address_e.textChanged.connect(f)
2385 fee = self.wallet.fee
2386 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2387 self.show_transaction(tx)
2391 def do_import_privkey(self, password):
2392 if not self.wallet.has_imported_keys():
2393 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2394 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2395 + _('Are you sure you understand what you are doing?'), 3, 4)
2398 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2401 text = str(text).split()
2406 addr = self.wallet.import_key(key, password)
2407 except Exception as e:
2413 addrlist.append(addr)
2415 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2417 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2418 self.update_address_tab()
2419 self.update_history_tab()
2422 def settings_dialog(self):
2424 d.setWindowTitle(_('Electrum Settings'))
2426 vbox = QVBoxLayout()
2427 grid = QGridLayout()
2428 grid.setColumnStretch(0,1)
2430 nz_label = QLabel(_('Display zeros') + ':')
2431 grid.addWidget(nz_label, 0, 0)
2432 nz_e = AmountEdit(None,True)
2433 nz_e.setText("%d"% self.num_zeros)
2434 grid.addWidget(nz_e, 0, 1)
2435 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2436 grid.addWidget(HelpButton(msg), 0, 2)
2437 if not self.config.is_modifiable('num_zeros'):
2438 for w in [nz_e, nz_label]: w.setEnabled(False)
2440 lang_label=QLabel(_('Language') + ':')
2441 grid.addWidget(lang_label, 1, 0)
2442 lang_combo = QComboBox()
2443 from electrum.i18n import languages
2444 lang_combo.addItems(languages.values())
2446 index = languages.keys().index(self.config.get("language",''))
2449 lang_combo.setCurrentIndex(index)
2450 grid.addWidget(lang_combo, 1, 1)
2451 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2452 if not self.config.is_modifiable('language'):
2453 for w in [lang_combo, lang_label]: w.setEnabled(False)
2456 fee_label = QLabel(_('Transaction fee') + ':')
2457 grid.addWidget(fee_label, 2, 0)
2458 fee_e = BTCAmountEdit(self.get_decimal_point)
2459 fee_e.setAmount(self.wallet.fee)
2460 grid.addWidget(fee_e, 2, 1)
2461 msg = _('Fee per kilobyte of transaction.') + '\n' \
2462 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2463 grid.addWidget(HelpButton(msg), 2, 2)
2464 if not self.config.is_modifiable('fee_per_kb'):
2465 for w in [fee_e, fee_label]: w.setEnabled(False)
2467 units = ['BTC', 'mBTC']
2468 unit_label = QLabel(_('Base unit') + ':')
2469 grid.addWidget(unit_label, 3, 0)
2470 unit_combo = QComboBox()
2471 unit_combo.addItems(units)
2472 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2473 grid.addWidget(unit_combo, 3, 1)
2474 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2475 + '\n1BTC=1000mBTC.\n' \
2476 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2478 usechange_cb = QCheckBox(_('Use change addresses'))
2479 usechange_cb.setChecked(self.wallet.use_change)
2480 grid.addWidget(usechange_cb, 4, 0)
2481 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2482 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2484 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2485 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2486 grid.addWidget(block_ex_label, 5, 0)
2487 block_ex_combo = QComboBox()
2488 block_ex_combo.addItems(block_explorers)
2489 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2490 grid.addWidget(block_ex_combo, 5, 1)
2491 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2493 show_tx = self.config.get('show_before_broadcast', False)
2494 showtx_cb = QCheckBox(_('Show before broadcast'))
2495 showtx_cb.setChecked(show_tx)
2496 grid.addWidget(showtx_cb, 6, 0)
2497 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2499 vbox.addLayout(grid)
2501 vbox.addLayout(ok_cancel_buttons(d))
2505 if not d.exec_(): return
2507 fee = fee_e.get_amount()
2509 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2512 self.wallet.set_fee(fee)
2514 nz = unicode(nz_e.text())
2519 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2522 if self.num_zeros != nz:
2524 self.config.set_key('num_zeros', nz, True)
2525 self.update_history_tab()
2526 self.update_address_tab()
2528 usechange_result = usechange_cb.isChecked()
2529 if self.wallet.use_change != usechange_result:
2530 self.wallet.use_change = usechange_result
2531 self.wallet.storage.put('use_change', self.wallet.use_change)
2533 if showtx_cb.isChecked() != show_tx:
2534 self.config.set_key('show_before_broadcast', not show_tx)
2536 unit_result = units[unit_combo.currentIndex()]
2537 if self.base_unit() != unit_result:
2538 self.decimal_point = 8 if unit_result == 'BTC' else 5
2539 self.config.set_key('decimal_point', self.decimal_point, True)
2540 self.update_history_tab()
2541 self.update_status()
2543 need_restart = False
2545 lang_request = languages.keys()[lang_combo.currentIndex()]
2546 if lang_request != self.config.get('language'):
2547 self.config.set_key("language", lang_request, True)
2550 be_result = block_explorers[block_ex_combo.currentIndex()]
2551 self.config.set_key('block_explorer', be_result, True)
2553 run_hook('close_settings_dialog')
2556 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2559 def run_network_dialog(self):
2560 if not self.network:
2562 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2564 def closeEvent(self, event):
2566 self.config.set_key("is_maximized", self.isMaximized())
2567 if not self.isMaximized():
2569 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2570 self.save_column_widths()
2571 self.config.set_key("console-history", self.console.history[-50:], True)
2572 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2576 def plugins_dialog(self):
2577 from electrum.plugins import plugins
2580 d.setWindowTitle(_('Electrum Plugins'))
2583 vbox = QVBoxLayout(d)
2586 scroll = QScrollArea()
2587 scroll.setEnabled(True)
2588 scroll.setWidgetResizable(True)
2589 scroll.setMinimumSize(400,250)
2590 vbox.addWidget(scroll)
2594 w.setMinimumHeight(len(plugins)*35)
2596 grid = QGridLayout()
2597 grid.setColumnStretch(0,1)
2600 def do_toggle(cb, p, w):
2603 if w: w.setEnabled(r)
2605 def mk_toggle(cb, p, w):
2606 return lambda: do_toggle(cb,p,w)
2608 for i, p in enumerate(plugins):
2610 cb = QCheckBox(p.fullname())
2611 cb.setDisabled(not p.is_available())
2612 cb.setChecked(p.is_enabled())
2613 grid.addWidget(cb, i, 0)
2614 if p.requires_settings():
2615 w = p.settings_widget(self)
2616 w.setEnabled( p.is_enabled() )
2617 grid.addWidget(w, i, 1)
2620 cb.clicked.connect(mk_toggle(cb,p,w))
2621 grid.addWidget(HelpButton(p.description()), i, 2)
2623 print_msg(_("Error: cannot display plugin"), p)
2624 traceback.print_exc(file=sys.stdout)
2625 grid.setRowStretch(i+1,1)
2627 vbox.addLayout(close_button(d))
2632 def show_account_details(self, k):
2633 account = self.wallet.accounts[k]
2636 d.setWindowTitle(_('Account Details'))
2639 vbox = QVBoxLayout(d)
2640 name = self.wallet.get_account_name(k)
2641 label = QLabel('Name: ' + name)
2642 vbox.addWidget(label)
2644 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2646 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2648 vbox.addWidget(QLabel(_('Master Public Key:')))
2651 text.setReadOnly(True)
2652 text.setMaximumHeight(170)
2653 vbox.addWidget(text)
2655 mpk_text = '\n'.join( account.get_master_pubkeys() )
2656 text.setText(mpk_text)
2658 vbox.addLayout(close_button(d))