3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re, threading
20 from electrum.i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
33 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
34 from electrum.plugins import run_hook
38 from electrum.wallet import format_satoshis
39 from electrum import Transaction
40 from electrum import mnemonic
41 from electrum import util, bitcoin, commands, Interface, Wallet
42 from electrum import SimpleConfig, Wallet, WalletStorage
44 from electrum import bmp, pyqrnative
46 from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
47 from network_dialog import NetworkDialog
48 from qrcodewidget import QRCodeWidget, QRDialog
49 from qrtextedit import QRTextEdit
51 from decimal import Decimal
59 if platform.system() == 'Windows':
60 MONOSPACE_FONT = 'Lucida Console'
61 elif platform.system() == 'Darwin':
62 MONOSPACE_FONT = 'Monaco'
64 MONOSPACE_FONT = 'monospace'
68 # status of payment requests
71 PR_SENT = 2 # sent but not propagated
72 PR_PAID = 3 # send and propagated
73 PR_ERROR = 4 # could not parse
76 from electrum import ELECTRUM_VERSION
91 class StatusBarButton(QPushButton):
92 def __init__(self, icon, tooltip, func):
93 QPushButton.__init__(self, icon, '')
94 self.setToolTip(tooltip)
96 self.setMaximumWidth(25)
97 self.clicked.connect(func)
99 self.setIconSize(QSize(25,25))
101 def keyPressEvent(self, e):
102 if e.key() == QtCore.Qt.Key_Return:
114 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
116 class ElectrumWindow(QMainWindow):
120 def __init__(self, config, network, gui_object):
121 QMainWindow.__init__(self)
124 self.network = network
125 self.gui_object = gui_object
126 self.tray = gui_object.tray
127 self.go_lite = gui_object.go_lite
130 self.create_status_bar()
131 self.need_update = threading.Event()
133 self.decimal_point = config.get('decimal_point', 5)
134 self.num_zeros = int(config.get('num_zeros',0))
137 set_language(config.get('language'))
139 self.funds_error = False
140 self.completions = QStringListModel()
142 self.tabs = tabs = QTabWidget(self)
143 self.column_widths = self.config.get("column_widths_2", default_column_widths )
144 tabs.addTab(self.create_history_tab(), _('History') )
145 tabs.addTab(self.create_send_tab(), _('Send') )
146 tabs.addTab(self.create_receive_tab(), _('Receive') )
147 tabs.addTab(self.create_addresses_tab(), _('Addresses') )
148 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
149 tabs.addTab(self.create_invoices_tab(), _('Invoices') )
150 tabs.addTab(self.create_console_tab(), _('Console') )
151 tabs.setMinimumSize(600, 400)
152 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
153 self.setCentralWidget(tabs)
155 g = self.config.get("winpos-qt",[100, 100, 840, 400])
156 self.setGeometry(g[0], g[1], g[2], g[3])
157 if self.config.get("is_maximized"):
160 self.setWindowIcon(QIcon(":icons/electrum.png"))
163 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
164 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
165 QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
166 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
167 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
169 for i in range(tabs.count()):
170 QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
172 self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
173 self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
174 self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
175 self.connect(self, QtCore.SIGNAL('payment_request_ok'), self.payment_request_ok)
176 self.connect(self, QtCore.SIGNAL('payment_request_error'), self.payment_request_error)
178 self.history_list.setFocus(True)
182 self.network.register_callback('updated', lambda: self.need_update.set())
183 self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
184 self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
185 self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
186 self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
188 # set initial message
189 self.console.showMessage(self.network.banner)
192 self.payment_request = None
194 def update_account_selector(self):
196 accounts = self.wallet.get_account_names()
197 self.account_selector.clear()
198 if len(accounts) > 1:
199 self.account_selector.addItems([_("All accounts")] + accounts.values())
200 self.account_selector.setCurrentIndex(0)
201 self.account_selector.show()
203 self.account_selector.hide()
206 def load_wallet(self, wallet):
210 self.update_wallet_format()
212 self.invoices = self.wallet.storage.get('invoices', {})
213 self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
214 self.current_account = self.wallet.storage.get("current_account", None)
215 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.storage.path
216 if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
217 self.setWindowTitle( title )
219 # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
220 self.notify_transactions()
221 self.update_account_selector()
223 self.new_account_menu.setEnabled(self.wallet.can_create_accounts())
224 self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
225 self.password_menu.setEnabled(not self.wallet.is_watching_only())
226 self.seed_menu.setEnabled(self.wallet.has_seed())
227 self.mpk_menu.setEnabled(self.wallet.is_deterministic())
228 self.import_menu.setEnabled(self.wallet.can_import())
230 self.update_lock_icon()
231 self.update_buttons_on_seed()
232 self.update_console()
234 self.clear_receive_tab()
235 self.update_receive_tab()
236 run_hook('load_wallet', wallet)
239 def update_wallet_format(self):
240 # convert old-format imported keys
241 if self.wallet.imported_keys:
242 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
244 self.wallet.convert_imported_keys(password)
246 self.show_message("error")
249 def open_wallet(self):
250 wallet_folder = self.wallet.storage.path
251 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
255 storage = WalletStorage({'wallet_path': filename})
256 if not storage.file_exists:
257 self.show_message("file not found "+ filename)
260 self.wallet.stop_threads()
263 wallet = Wallet(storage)
264 wallet.start_threads(self.network)
266 self.load_wallet(wallet)
270 def backup_wallet(self):
272 path = self.wallet.storage.path
273 wallet_folder = os.path.dirname(path)
274 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
278 new_path = os.path.join(wallet_folder, filename)
281 shutil.copy2(path, new_path)
282 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
283 except (IOError, os.error), reason:
284 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
287 def new_wallet(self):
290 wallet_folder = os.path.dirname(self.wallet.storage.path)
291 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
294 filename = os.path.join(wallet_folder, filename)
296 storage = WalletStorage({'wallet_path': filename})
297 if storage.file_exists:
298 QMessageBox.critical(None, "Error", _("File exists"))
301 wizard = installwizard.InstallWizard(self.config, self.network, storage)
302 wallet = wizard.run('new')
304 self.load_wallet(wallet)
308 def init_menubar(self):
311 file_menu = menubar.addMenu(_("&File"))
312 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
313 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
314 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
315 file_menu.addAction(_("&Quit"), self.close)
317 wallet_menu = menubar.addMenu(_("&Wallet"))
318 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
319 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
321 wallet_menu.addSeparator()
323 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
324 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
325 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
327 wallet_menu.addSeparator()
328 labels_menu = wallet_menu.addMenu(_("&Labels"))
329 labels_menu.addAction(_("&Import"), self.do_import_labels)
330 labels_menu.addAction(_("&Export"), self.do_export_labels)
332 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
333 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
334 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
335 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
336 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
338 tools_menu = menubar.addMenu(_("&Tools"))
340 # Settings / Preferences are all reserved keywords in OSX using this as work around
341 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
342 tools_menu.addAction(_("&Network"), self.run_network_dialog)
343 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
344 tools_menu.addSeparator()
345 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
346 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
347 tools_menu.addSeparator()
349 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
350 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
351 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
353 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
354 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
355 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
356 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
357 self.raw_transaction_menu = raw_transaction_menu
359 help_menu = menubar.addMenu(_("&Help"))
360 help_menu.addAction(_("&About"), self.show_about)
361 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
362 help_menu.addSeparator()
363 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
364 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
366 self.setMenuBar(menubar)
368 def show_about(self):
369 QMessageBox.about(self, "Electrum",
370 _("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."))
372 def show_report_bug(self):
373 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
374 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
377 def notify_transactions(self):
378 if not self.network or not self.network.is_connected():
381 print_error("Notifying GUI")
382 if len(self.network.pending_transactions_for_notifications) > 0:
383 # Combine the transactions if there are more then three
384 tx_amount = len(self.network.pending_transactions_for_notifications)
387 for tx in self.network.pending_transactions_for_notifications:
388 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
392 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
393 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
395 self.network.pending_transactions_for_notifications = []
397 for tx in self.network.pending_transactions_for_notifications:
399 self.network.pending_transactions_for_notifications.remove(tx)
400 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
402 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
404 def notify(self, message):
405 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
409 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
410 def getOpenFileName(self, title, filter = ""):
411 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
412 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
413 if fileName and directory != os.path.dirname(fileName):
414 self.config.set_key('io_dir', os.path.dirname(fileName), True)
417 def getSaveFileName(self, title, filename, filter = ""):
418 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
419 path = os.path.join( directory, filename )
420 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
421 if fileName and directory != os.path.dirname(fileName):
422 self.config.set_key('io_dir', os.path.dirname(fileName), True)
426 QMainWindow.close(self)
427 run_hook('close_main_window')
429 def connect_slots(self, sender):
430 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
431 self.previous_payto_e=''
433 def timer_actions(self):
434 if self.need_update.is_set():
436 self.need_update.clear()
438 self.receive_qr.update_qr()
439 run_hook('timer_actions')
441 def format_amount(self, x, is_diff=False, whitespaces=False):
442 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
445 def get_decimal_point(self):
446 return self.decimal_point
450 assert self.decimal_point in [5,8]
451 return "BTC" if self.decimal_point == 8 else "mBTC"
454 def update_status(self):
455 if self.network is None or not self.network.is_running():
457 icon = QIcon(":icons/status_disconnected.png")
459 elif self.network.is_connected():
460 if not self.wallet.up_to_date:
461 text = _("Synchronizing...")
462 icon = QIcon(":icons/status_waiting.png")
463 elif self.network.server_lag > 1:
464 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
465 icon = QIcon(":icons/status_lagging.png")
467 c, u = self.wallet.get_account_balance(self.current_account)
468 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
469 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
471 # append fiat balance and price from exchange rate plugin
473 run_hook('get_fiat_status_text', c+u, r)
478 self.tray.setToolTip(text)
479 icon = QIcon(":icons/status_connected.png")
481 text = _("Not connected")
482 icon = QIcon(":icons/status_disconnected.png")
484 self.balance_label.setText(text)
485 self.status_button.setIcon( icon )
488 def update_wallet(self):
490 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
491 self.update_history_tab()
492 self.update_receive_tab()
493 self.update_address_tab()
494 self.update_contacts_tab()
495 self.update_completions()
496 self.update_invoices_tab()
499 def create_history_tab(self):
500 self.history_list = l = MyTreeWidget(self)
502 for i,width in enumerate(self.column_widths['history']):
503 l.setColumnWidth(i, width)
504 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
505 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
506 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
508 l.customContextMenuRequested.connect(self.create_history_menu)
512 def create_history_menu(self, position):
513 self.history_list.selectedIndexes()
514 item = self.history_list.currentItem()
515 be = self.config.get('block_explorer', 'Blockchain.info')
516 if be == 'Blockchain.info':
517 block_explorer = 'https://blockchain.info/tx/'
518 elif be == 'Blockr.io':
519 block_explorer = 'https://blockr.io/tx/info/'
520 elif be == 'Insight.is':
521 block_explorer = 'http://live.insight.is/tx/'
523 tx_hash = str(item.data(0, Qt.UserRole).toString())
524 if not tx_hash: return
526 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
527 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
528 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
529 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
530 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
533 def show_transaction(self, tx):
534 import transaction_dialog
535 d = transaction_dialog.TxDialog(tx, self)
538 def tx_label_clicked(self, item, column):
539 if column==2 and item.isSelected():
541 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
542 self.history_list.editItem( item, column )
543 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
546 def tx_label_changed(self, item, column):
550 tx_hash = str(item.data(0, Qt.UserRole).toString())
551 tx = self.wallet.transactions.get(tx_hash)
552 text = unicode( item.text(2) )
553 self.wallet.set_label(tx_hash, text)
555 item.setForeground(2, QBrush(QColor('black')))
557 text = self.wallet.get_default_label(tx_hash)
558 item.setText(2, text)
559 item.setForeground(2, QBrush(QColor('gray')))
563 def edit_label(self, is_recv):
564 l = self.receive_list if is_recv else self.contacts_list
565 item = l.currentItem()
566 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
567 l.editItem( item, 1 )
568 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
572 def address_label_clicked(self, item, column, l, column_addr, column_label):
573 if column == column_label and item.isSelected():
574 is_editable = item.data(0, 32).toBool()
577 addr = unicode( item.text(column_addr) )
578 label = unicode( item.text(column_label) )
579 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
580 l.editItem( item, column )
581 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
584 def address_label_changed(self, item, column, l, column_addr, column_label):
585 if column == column_label:
586 addr = unicode( item.text(column_addr) )
587 text = unicode( item.text(column_label) )
588 is_editable = item.data(0, 32).toBool()
592 changed = self.wallet.set_label(addr, text)
594 self.update_history_tab()
595 self.update_completions()
597 self.current_item_changed(item)
599 run_hook('item_changed', item, column)
602 def current_item_changed(self, a):
603 run_hook('current_item_changed', a)
607 def update_history_tab(self):
609 self.history_list.clear()
610 for item in self.wallet.get_tx_history(self.current_account):
611 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
612 time_str = _("unknown")
615 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
617 time_str = _("error")
620 time_str = 'unverified'
621 icon = QIcon(":icons/unconfirmed.png")
624 icon = QIcon(":icons/unconfirmed.png")
626 icon = QIcon(":icons/clock%d.png"%conf)
628 icon = QIcon(":icons/confirmed.png")
630 if value is not None:
631 v_str = self.format_amount(value, True, whitespaces=True)
635 balance_str = self.format_amount(balance, whitespaces=True)
638 label, is_default_label = self.wallet.get_label(tx_hash)
640 label = _('Pruned transaction outputs')
641 is_default_label = False
643 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
644 item.setFont(2, QFont(MONOSPACE_FONT))
645 item.setFont(3, QFont(MONOSPACE_FONT))
646 item.setFont(4, QFont(MONOSPACE_FONT))
648 item.setForeground(3, QBrush(QColor("#BC1E1E")))
650 item.setData(0, Qt.UserRole, tx_hash)
651 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
653 item.setForeground(2, QBrush(QColor('grey')))
655 item.setIcon(0, icon)
656 self.history_list.insertTopLevelItem(0,item)
659 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
660 run_hook('history_tab_update')
663 def create_receive_tab(self):
665 grid = QGridLayout(w)
666 grid.setColumnMinimumWidth(3, 300)
667 grid.setColumnStretch(5, 1)
669 self.receive_address_e = QLineEdit()
670 self.receive_address_e.setReadOnly(True)
671 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
672 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
673 self.receive_address_e.textChanged.connect(self.update_receive_qr)
675 self.receive_message_e = QLineEdit()
676 grid.addWidget(QLabel(_('Message')), 1, 0)
677 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
678 self.receive_message_e.textChanged.connect(self.update_receive_qr)
680 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
681 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
682 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
683 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
685 save_button = QPushButton(_('Save'))
686 save_button.clicked.connect(self.save_payment_request)
687 grid.addWidget(save_button, 3, 1)
688 clear_button = QPushButton(_('Clear'))
689 clear_button.clicked.connect(self.clear_receive_tab)
690 grid.addWidget(clear_button, 3, 2)
691 grid.setRowStretch(4, 1)
693 self.receive_qr = QRCodeWidget()
694 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
696 self.receive_requests_label = QLabel(_('Pending requests'))
697 self.receive_list = MyTreeWidget(self)
698 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
699 self.receive_list.currentItemChanged.connect(self.receive_item_changed)
700 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount'), _('Status')] )
701 self.receive_list.setColumnWidth(0, 320)
702 h = self.receive_list.header()
703 h.setStretchLastSection(False)
704 h.setResizeMode(1, QHeaderView.Stretch)
706 grid.addWidget(self.receive_requests_label, 5, 0)
707 grid.addWidget(self.receive_list, 6, 0, 1, 6)
709 grid.setRowStretch(7, 1)
712 def receive_item_changed(self, item):
715 addr = str(item.text(0))
716 amount, message = self.receive_requests[addr]
717 self.receive_address_e.setText(addr)
718 self.receive_message_e.setText(message)
719 self.receive_amount_e.setAmount(amount)
722 def receive_list_delete(self, item):
723 addr = str(item.text(0))
724 self.receive_requests.pop(addr)
725 self.update_receive_tab()
726 self.redraw_from_list()
729 def receive_list_menu(self, position):
730 item = self.receive_list.itemAt(position)
732 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
733 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
735 def save_payment_request(self):
736 addr = str(self.receive_address_e.text())
737 amount = self.receive_amount_e.get_amount()
738 message = str(self.receive_message_e.text())
739 if not message and not amount:
740 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
742 self.receive_requests = self.wallet.storage.get('receive_requests',{})
743 self.receive_requests[addr] = (amount, message)
744 self.wallet.storage.put('receive_requests', self.receive_requests)
745 self.update_receive_tab()
747 def clear_receive_tab(self):
748 self.receive_requests = self.wallet.storage.get('receive_requests',{})
749 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
751 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
755 self.receive_address_e.setText(addr)
756 self.receive_message_e.setText('')
757 self.receive_amount_e.setAmount(None)
759 def receive_at(self, addr):
760 if not bitcoin.is_address(addr):
762 self.tabs.setCurrentIndex(2)
763 self.receive_address_e.setText(addr)
765 def update_receive_tab(self):
766 self.receive_requests = self.wallet.storage.get('receive_requests',{})
767 b = len(self.receive_requests) > 0
768 self.receive_list.setVisible(b)
769 self.receive_requests_label.setVisible(b)
771 self.receive_list.clear()
772 for address, v in self.receive_requests.items():
774 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else "", ""] )
775 self.receive_list.addTopLevelItem(item)
778 def update_receive_qr(self):
779 import urlparse, urllib
780 addr = str(self.receive_address_e.text())
783 amount = self.receive_amount_e.get_amount()
785 query.append('amount=%s'%format_satoshis(amount))
786 message = unicode(self.receive_message_e.text()).encode('utf8')
788 query.append('message=%s'%urllib.quote(message))
789 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
790 url = urlparse.urlunparse(p)
793 self.receive_qr.set_addr(url)
796 def create_send_tab(self):
799 self.send_grid = grid = QGridLayout(w)
801 grid.setColumnMinimumWidth(3,300)
802 grid.setColumnStretch(5,1)
803 grid.setRowStretch(8, 1)
805 from paytoedit import PayToEdit
806 self.amount_e = BTCAmountEdit(self.get_decimal_point)
807 self.payto_e = PayToEdit(self)
808 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)'))
809 grid.addWidget(QLabel(_('Pay to')), 1, 0)
810 grid.addWidget(self.payto_e, 1, 1, 1, 3)
811 grid.addWidget(self.payto_help, 1, 4)
813 completer = QCompleter()
814 completer.setCaseSensitivity(False)
815 self.payto_e.setCompleter(completer)
816 completer.setModel(self.completions)
818 self.message_e = MyLineEdit()
819 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.'))
820 grid.addWidget(QLabel(_('Description')), 2, 0)
821 grid.addWidget(self.message_e, 2, 1, 1, 3)
822 grid.addWidget(self.message_help, 2, 4)
824 self.from_label = QLabel(_('From'))
825 grid.addWidget(self.from_label, 3, 0)
826 self.from_list = MyTreeWidget(self)
827 self.from_list.setColumnCount(2)
828 self.from_list.setColumnWidth(0, 350)
829 self.from_list.setColumnWidth(1, 50)
830 self.from_list.setHeaderHidden(True)
831 self.from_list.setMaximumHeight(80)
832 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
833 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
834 grid.addWidget(self.from_list, 3, 1, 1, 3)
835 self.set_pay_from([])
837 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
838 + _('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.') \
839 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
840 grid.addWidget(QLabel(_('Amount')), 4, 0)
841 grid.addWidget(self.amount_e, 4, 1, 1, 2)
842 grid.addWidget(self.amount_help, 4, 3)
844 self.fee_e = BTCAmountEdit(self.get_decimal_point)
845 grid.addWidget(QLabel(_('Fee')), 5, 0)
846 grid.addWidget(self.fee_e, 5, 1, 1, 2)
847 grid.addWidget(HelpButton(
848 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
849 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
850 + _('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)
852 self.send_button = EnterButton(_("Send"), self.do_send)
853 grid.addWidget(self.send_button, 6, 1)
855 b = EnterButton(_("Clear"), self.do_clear)
856 grid.addWidget(b, 6, 2)
858 self.payto_sig = QLabel('')
859 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
861 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
862 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
865 def entry_changed( is_fee ):
866 self.funds_error = False
868 if self.amount_e.is_shortcut:
869 self.amount_e.is_shortcut = False
870 sendable = self.get_sendable_balance()
871 # there is only one output because we are completely spending inputs
872 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
873 fee = self.wallet.estimated_fee(inputs, 1)
875 self.amount_e.setAmount(amount)
876 self.fee_e.setAmount(fee)
879 amount = self.amount_e.get_amount()
880 fee = self.fee_e.get_amount()
882 if not is_fee: fee = None
885 # assume that there will be 2 outputs (one for change)
886 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
888 self.fee_e.setAmount(fee)
891 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
895 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
896 self.funds_error = True
897 text = _( "Not enough funds" )
898 c, u = self.wallet.get_frozen_balance()
899 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
901 self.statusBar().showMessage(text)
902 self.amount_e.setPalette(palette)
903 self.fee_e.setPalette(palette)
905 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
906 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
908 run_hook('create_send_tab', grid)
911 def from_list_delete(self, item):
912 i = self.from_list.indexOfTopLevelItem(item)
914 self.redraw_from_list()
916 def from_list_menu(self, position):
917 item = self.from_list.itemAt(position)
919 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
920 menu.exec_(self.from_list.viewport().mapToGlobal(position))
922 def set_pay_from(self, domain = None):
923 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
924 self.redraw_from_list()
926 def redraw_from_list(self):
927 self.from_list.clear()
928 self.from_label.setHidden(len(self.pay_from) == 0)
929 self.from_list.setHidden(len(self.pay_from) == 0)
932 h = x.get('prevout_hash')
933 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
935 for item in self.pay_from:
936 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
938 def update_completions(self):
940 for addr,label in self.wallet.labels.items():
941 if addr in self.wallet.addressbook:
942 l.append( label + ' <' + addr + '>')
944 run_hook('update_completions', l)
945 self.completions.setStringList(l)
949 return lambda s, *args: s.do_protect(func, args)
952 def read_send_tab(self):
954 if self.payment_request and self.payment_request.has_expired():
955 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
958 label = unicode( self.message_e.text() )
960 if self.payment_request:
961 outputs = self.payment_request.get_outputs()
963 outputs = self.payto_e.get_outputs()
966 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
969 for addr, x in outputs:
970 if addr is None or not bitcoin.is_address(addr):
971 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
974 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
977 amount = sum(map(lambda x:x[1], outputs))
979 fee = self.fee_e.get_amount()
981 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
984 confirm_amount = self.config.get('confirm_amount', 100000000)
985 if amount >= confirm_amount:
986 o = '\n'.join(map(lambda x:x[0], outputs))
987 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
990 confirm_fee = self.config.get('confirm_fee', 100000)
991 if fee >= confirm_fee:
992 if not self.question(_("The fee for this transaction seems unusually high.\nAre you really sure you want to pay %(fee)s in fees?")%{ 'fee' : self.format_amount(fee) + ' '+ self.base_unit()}):
995 coins = self.get_coins()
996 return outputs, fee, label, coins
1000 r = self.read_send_tab()
1003 outputs, fee, label, coins = r
1004 self.send_tx(outputs, fee, label, coins)
1008 def send_tx(self, outputs, fee, label, coins, password):
1009 self.send_button.setDisabled(True)
1011 # first, create an unsigned tx
1013 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1015 except Exception as e:
1016 traceback.print_exc(file=sys.stdout)
1017 self.show_message(str(e))
1018 self.send_button.setDisabled(False)
1021 # call hook to see if plugin needs gui interaction
1022 run_hook('send_tx', tx)
1028 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
1029 self.wallet.sign_transaction(tx, keypairs, password)
1030 return tx, fee, label
1032 def sign_done(tx, fee, label):
1034 self.show_message(tx.error)
1035 self.send_button.setDisabled(False)
1037 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1038 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1039 self.send_button.setDisabled(False)
1042 self.wallet.set_label(tx.hash(), label)
1044 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1045 self.show_transaction(tx)
1047 self.send_button.setDisabled(False)
1050 self.broadcast_transaction(tx)
1052 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1053 self.waiting_dialog.start()
1057 def broadcast_transaction(self, tx):
1059 def broadcast_thread():
1060 pr = self.payment_request
1062 return self.wallet.sendtx(tx)
1064 if pr.has_expired():
1065 self.payment_request = None
1066 return False, _("Payment request has expired")
1068 status, msg = self.wallet.sendtx(tx)
1072 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1073 self.wallet.storage.put('invoices', self.invoices)
1074 self.update_invoices_tab()
1075 self.payment_request = None
1076 refund_address = self.wallet.addresses()[0]
1077 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1083 def broadcast_done(status, msg):
1085 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1088 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1089 self.send_button.setDisabled(False)
1091 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1092 self.waiting_dialog.start()
1096 def prepare_for_payment_request(self):
1097 self.tabs.setCurrentIndex(1)
1098 self.payto_e.is_pr = True
1099 for e in [self.payto_e, self.amount_e, self.message_e]:
1101 for h in [self.payto_help, self.amount_help, self.message_help]:
1103 self.payto_e.setText(_("please wait..."))
1106 def payment_request_ok(self):
1107 pr = self.payment_request
1109 if pr_id not in self.invoices:
1110 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1111 self.wallet.storage.put('invoices', self.invoices)
1112 self.update_invoices_tab()
1114 print_error('invoice already in list')
1116 status = self.invoices[pr_id][4]
1117 if status == PR_PAID:
1119 self.show_message("invoice already paid")
1120 self.payment_request = None
1123 self.payto_help.show()
1124 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1126 if not pr.has_expired():
1127 self.payto_e.setGreen()
1129 self.payto_e.setExpired()
1131 self.payto_e.setText(pr.domain)
1132 self.amount_e.setText(self.format_amount(pr.get_amount()))
1133 self.message_e.setText(pr.get_memo())
1135 def payment_request_error(self):
1137 self.show_message(self.payment_request.error)
1138 self.payment_request = None
1140 def pay_from_URI(self,URI):
1143 address, amount, label, message, request_url = util.parse_URI(URI)
1145 address, amount, label, message, request_url = util.parse_URI(URI)
1146 except Exception as e:
1147 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1150 self.tabs.setCurrentIndex(1)
1154 if self.wallet.labels.get(address) != label:
1155 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1156 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1157 self.wallet.addressbook.append(address)
1158 self.wallet.set_label(address, label)
1160 label = self.wallet.labels.get(address)
1162 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1164 self.message_e.setText(message)
1166 self.amount_e.setAmount(amount)
1169 from electrum import paymentrequest
1170 def payment_request():
1171 self.payment_request = paymentrequest.PaymentRequest(self.config)
1172 self.payment_request.read(request_url)
1173 if self.payment_request.verify():
1174 self.emit(SIGNAL('payment_request_ok'))
1176 self.emit(SIGNAL('payment_request_error'))
1178 self.pr_thread = threading.Thread(target=payment_request).start()
1179 self.prepare_for_payment_request()
1184 self.payto_e.is_pr = False
1185 self.payto_sig.setVisible(False)
1186 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1190 for h in [self.payto_help, self.amount_help, self.message_help]:
1193 self.payto_help.set_alt(None)
1194 self.set_pay_from([])
1195 self.update_status()
1199 def set_addrs_frozen(self,addrs,freeze):
1201 if not addr: continue
1202 if addr in self.wallet.frozen_addresses and not freeze:
1203 self.wallet.unfreeze(addr)
1204 elif addr not in self.wallet.frozen_addresses and freeze:
1205 self.wallet.freeze(addr)
1206 self.update_address_tab()
1210 def create_list_tab(self, headers):
1211 "generic tab creation method"
1212 l = MyTreeWidget(self)
1213 l.setColumnCount( len(headers) )
1214 l.setHeaderLabels( headers )
1217 vbox = QVBoxLayout()
1224 vbox.addWidget(buttons)
1229 def create_addresses_tab(self):
1230 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1231 for i,width in enumerate(self.column_widths['receive']):
1232 l.setColumnWidth(i, width)
1233 l.setContextMenuPolicy(Qt.CustomContextMenu)
1234 l.customContextMenuRequested.connect(self.create_receive_menu)
1235 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1236 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1237 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1238 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1239 self.address_list = l
1245 def save_column_widths(self):
1246 self.column_widths["receive"] = []
1247 for i in range(self.address_list.columnCount() -1):
1248 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1250 self.column_widths["history"] = []
1251 for i in range(self.history_list.columnCount() - 1):
1252 self.column_widths["history"].append(self.history_list.columnWidth(i))
1254 self.column_widths["contacts"] = []
1255 for i in range(self.contacts_list.columnCount() - 1):
1256 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1258 self.config.set_key("column_widths_2", self.column_widths, True)
1261 def create_contacts_tab(self):
1262 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1263 l.setContextMenuPolicy(Qt.CustomContextMenu)
1264 l.customContextMenuRequested.connect(self.create_contact_menu)
1265 for i,width in enumerate(self.column_widths['contacts']):
1266 l.setColumnWidth(i, width)
1267 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1268 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1269 self.contacts_list = l
1273 def create_invoices_tab(self):
1274 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1276 h.setStretchLastSection(False)
1277 h.setResizeMode(1, QHeaderView.Stretch)
1278 l.setContextMenuPolicy(Qt.CustomContextMenu)
1279 l.customContextMenuRequested.connect(self.create_invoice_menu)
1280 self.invoices_list = l
1283 def update_invoices_tab(self):
1284 invoices = self.wallet.storage.get('invoices', {})
1285 l = self.invoices_list
1287 for key, value in invoices.items():
1289 domain, memo, amount, expiration_date, status, tx_hash = value
1293 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1295 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1296 l.addTopLevelItem(item)
1298 l.setCurrentItem(l.topLevelItem(0))
1302 def delete_imported_key(self, addr):
1303 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1304 self.wallet.delete_imported_key(addr)
1305 self.update_address_tab()
1306 self.update_history_tab()
1308 def edit_account_label(self, k):
1309 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1311 label = unicode(text)
1312 self.wallet.set_label(k,label)
1313 self.update_address_tab()
1315 def account_set_expanded(self, item, k, b):
1317 self.accounts_expanded[k] = b
1319 def create_account_menu(self, position, k, item):
1321 if item.isExpanded():
1322 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1324 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1325 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1326 if self.wallet.seed_version > 4:
1327 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1328 if self.wallet.account_is_pending(k):
1329 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1330 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1332 def delete_pending_account(self, k):
1333 self.wallet.delete_pending_account(k)
1334 self.update_address_tab()
1336 def create_receive_menu(self, position):
1337 # fixme: this function apparently has a side effect.
1338 # if it is not called the menu pops up several times
1339 #self.address_list.selectedIndexes()
1341 selected = self.address_list.selectedItems()
1342 multi_select = len(selected) > 1
1343 addrs = [unicode(item.text(0)) for item in selected]
1344 if not multi_select:
1345 item = self.address_list.itemAt(position)
1349 if not is_valid(addr):
1350 k = str(item.data(0,32).toString())
1352 self.create_account_menu(position, k, item)
1354 item.setExpanded(not item.isExpanded())
1358 if not multi_select:
1359 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1360 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1361 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1362 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1363 if not self.wallet.is_watching_only():
1364 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1365 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1366 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1367 if self.wallet.is_imported(addr):
1368 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1370 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1371 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1372 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1373 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1375 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1376 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1378 run_hook('receive_menu', menu, addrs)
1379 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1382 def get_sendable_balance(self):
1383 return sum(map(lambda x:x['value'], self.get_coins()))
1386 def get_coins(self):
1388 return self.pay_from
1390 domain = self.wallet.get_account_addresses(self.current_account)
1391 for i in self.wallet.frozen_addresses:
1392 if i in domain: domain.remove(i)
1393 return self.wallet.get_unspent_coins(domain)
1396 def send_from_addresses(self, addrs):
1397 self.set_pay_from( addrs )
1398 self.tabs.setCurrentIndex(1)
1401 def payto(self, addr):
1403 label = self.wallet.labels.get(addr)
1404 m_addr = label + ' <' + addr + '>' if label else addr
1405 self.tabs.setCurrentIndex(1)
1406 self.payto_e.setText(m_addr)
1407 self.amount_e.setFocus()
1410 def delete_contact(self, x):
1411 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1412 self.wallet.delete_contact(x)
1413 self.wallet.set_label(x, None)
1414 self.update_history_tab()
1415 self.update_contacts_tab()
1416 self.update_completions()
1419 def create_contact_menu(self, position):
1420 item = self.contacts_list.itemAt(position)
1423 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1425 addr = unicode(item.text(0))
1426 label = unicode(item.text(1))
1427 is_editable = item.data(0,32).toBool()
1428 payto_addr = item.data(0,33).toString()
1429 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1430 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1431 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1433 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1434 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1436 run_hook('create_contact_menu', menu, item)
1437 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1439 def delete_invoice(self, key):
1440 self.invoices.pop(key)
1441 self.wallet.storage.put('invoices', self.invoices)
1442 self.update_invoices_tab()
1444 def show_invoice(self, key):
1445 from electrum.paymentrequest import PaymentRequest
1446 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1447 pr = PaymentRequest(self.config)
1451 self.show_pr_details(pr)
1453 def show_pr_details(self, pr):
1454 msg = 'Domain: ' + pr.domain
1455 msg += '\nStatus: ' + pr.get_status()
1456 msg += '\nMemo: ' + pr.get_memo()
1457 msg += '\nPayment URL: ' + pr.payment_url
1458 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1459 QMessageBox.information(self, 'Invoice', msg , 'OK')
1461 def do_pay_invoice(self, key):
1462 from electrum.paymentrequest import PaymentRequest
1463 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1464 pr = PaymentRequest(self.config)
1467 self.payment_request = pr
1468 self.prepare_for_payment_request()
1470 self.payment_request_ok()
1472 self.payment_request_error()
1475 def create_invoice_menu(self, position):
1476 item = self.invoices_list.itemAt(position)
1479 k = self.invoices_list.indexOfTopLevelItem(item)
1480 key = self.invoices.keys()[k]
1481 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1483 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1484 if status == PR_UNPAID:
1485 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1486 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1487 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1490 def update_address_item(self, item):
1491 item.setFont(0, QFont(MONOSPACE_FONT))
1492 address = str(item.data(0,0).toString())
1493 label = self.wallet.labels.get(address,'')
1494 item.setData(1,0,label)
1495 item.setData(0,32, True) # is editable
1497 run_hook('update_address_item', address, item)
1499 if not self.wallet.is_mine(address): return
1501 c, u = self.wallet.get_addr_balance(address)
1502 balance = self.format_amount(c + u)
1503 item.setData(2,0,balance)
1505 if address in self.wallet.frozen_addresses:
1506 item.setBackgroundColor(0, QColor('lightblue'))
1509 def update_address_tab(self):
1510 l = self.address_list
1511 # extend the syntax for consistency
1512 l.addChild = l.addTopLevelItem
1513 l.insertChild = l.insertTopLevelItem
1517 accounts = self.wallet.get_accounts()
1518 if self.current_account is None:
1519 account_items = sorted(accounts.items())
1521 account_items = [(self.current_account, accounts.get(self.current_account))]
1524 for k, account in account_items:
1526 if len(accounts) > 1:
1527 name = self.wallet.get_account_name(k)
1528 c,u = self.wallet.get_account_balance(k)
1529 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1530 l.addTopLevelItem(account_item)
1531 account_item.setExpanded(self.accounts_expanded.get(k, True))
1532 account_item.setData(0, 32, k)
1536 sequences = [0,1] if account.has_change() else [0]
1537 for is_change in sequences:
1538 if len(sequences) > 1:
1539 name = _("Receiving") if not is_change else _("Change")
1540 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1541 account_item.addChild(seq_item)
1543 seq_item.setExpanded(True)
1545 seq_item = account_item
1547 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1553 for address in account.get_addresses(is_change):
1555 num, is_used = self.wallet.is_used(address)
1558 if gap > self.wallet.gap_limit:
1563 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1564 self.update_address_item(item)
1566 item.setBackgroundColor(1, QColor('red'))
1570 seq_item.insertChild(0,used_item)
1572 used_item.addChild(item)
1574 seq_item.addChild(item)
1576 # we use column 1 because column 0 may be hidden
1577 l.setCurrentItem(l.topLevelItem(0),1)
1580 def update_contacts_tab(self):
1581 l = self.contacts_list
1584 for address in self.wallet.addressbook:
1585 label = self.wallet.labels.get(address,'')
1586 n = self.wallet.get_num_tx(address)
1587 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1588 item.setFont(0, QFont(MONOSPACE_FONT))
1589 # 32 = label can be edited (bool)
1590 item.setData(0,32, True)
1592 item.setData(0,33, address)
1593 l.addTopLevelItem(item)
1595 run_hook('update_contacts_tab', l)
1596 l.setCurrentItem(l.topLevelItem(0))
1600 def create_console_tab(self):
1601 from console import Console
1602 self.console = console = Console()
1606 def update_console(self):
1607 console = self.console
1608 console.history = self.config.get("console-history",[])
1609 console.history_index = len(console.history)
1611 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1612 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1614 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1616 def mkfunc(f, method):
1617 return lambda *args: apply( f, (method, args, self.password_dialog ))
1619 if m[0]=='_' or m in ['network','wallet']: continue
1620 methods[m] = mkfunc(c._run, m)
1622 console.updateNamespace(methods)
1625 def change_account(self,s):
1626 if s == _("All accounts"):
1627 self.current_account = None
1629 accounts = self.wallet.get_account_names()
1630 for k, v in accounts.items():
1632 self.current_account = k
1633 self.update_history_tab()
1634 self.update_status()
1635 self.update_address_tab()
1636 self.update_receive_tab()
1638 def create_status_bar(self):
1641 sb.setFixedHeight(35)
1642 qtVersion = qVersion()
1644 self.balance_label = QLabel("")
1645 sb.addWidget(self.balance_label)
1647 from version_getter import UpdateLabel
1648 self.updatelabel = UpdateLabel(self.config, sb)
1650 self.account_selector = QComboBox()
1651 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1652 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1653 sb.addPermanentWidget(self.account_selector)
1655 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1656 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1658 self.lock_icon = QIcon()
1659 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1660 sb.addPermanentWidget( self.password_button )
1662 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1663 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1664 sb.addPermanentWidget( self.seed_button )
1665 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1666 sb.addPermanentWidget( self.status_button )
1668 run_hook('create_status_bar', (sb,))
1670 self.setStatusBar(sb)
1673 def update_lock_icon(self):
1674 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1675 self.password_button.setIcon( icon )
1678 def update_buttons_on_seed(self):
1679 if self.wallet.has_seed():
1680 self.seed_button.show()
1682 self.seed_button.hide()
1684 if not self.wallet.is_watching_only():
1685 self.password_button.show()
1686 self.send_button.setText(_("Send"))
1688 self.password_button.hide()
1689 self.send_button.setText(_("Create unsigned transaction"))
1692 def change_password_dialog(self):
1693 from password_dialog import PasswordDialog
1694 d = PasswordDialog(self.wallet, self)
1696 self.update_lock_icon()
1699 def new_contact_dialog(self):
1702 d.setWindowTitle(_("New Contact"))
1703 vbox = QVBoxLayout(d)
1704 vbox.addWidget(QLabel(_('New Contact')+':'))
1706 grid = QGridLayout()
1709 grid.addWidget(QLabel(_("Address")), 1, 0)
1710 grid.addWidget(line1, 1, 1)
1711 grid.addWidget(QLabel(_("Name")), 2, 0)
1712 grid.addWidget(line2, 2, 1)
1714 vbox.addLayout(grid)
1715 vbox.addLayout(ok_cancel_buttons(d))
1720 address = str(line1.text())
1721 label = unicode(line2.text())
1723 if not is_valid(address):
1724 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1727 self.wallet.add_contact(address)
1729 self.wallet.set_label(address, label)
1731 self.update_contacts_tab()
1732 self.update_history_tab()
1733 self.update_completions()
1734 self.tabs.setCurrentIndex(3)
1738 def new_account_dialog(self, password):
1740 dialog = QDialog(self)
1742 dialog.setWindowTitle(_("New Account"))
1744 vbox = QVBoxLayout()
1745 vbox.addWidget(QLabel(_('Account name')+':'))
1748 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1749 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1754 vbox.addLayout(ok_cancel_buttons(dialog))
1755 dialog.setLayout(vbox)
1759 name = str(e.text())
1762 self.wallet.create_pending_account(name, password)
1763 self.update_address_tab()
1764 self.tabs.setCurrentIndex(2)
1769 def show_master_public_keys(self):
1771 dialog = QDialog(self)
1773 dialog.setWindowTitle(_("Master Public Keys"))
1775 main_layout = QGridLayout()
1776 mpk_dict = self.wallet.get_master_public_keys()
1778 for key, value in mpk_dict.items():
1779 main_layout.addWidget(QLabel(key), i, 0)
1780 mpk_text = QTextEdit()
1781 mpk_text.setReadOnly(True)
1782 mpk_text.setMaximumHeight(170)
1783 mpk_text.setText(value)
1784 main_layout.addWidget(mpk_text, i + 1, 0)
1787 vbox = QVBoxLayout()
1788 vbox.addLayout(main_layout)
1789 vbox.addLayout(close_button(dialog))
1791 dialog.setLayout(vbox)
1796 def show_seed_dialog(self, password):
1797 if not self.wallet.has_seed():
1798 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1802 mnemonic = self.wallet.get_mnemonic(password)
1804 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1806 from seed_dialog import SeedDialog
1807 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1812 def show_qrcode(self, data, title = _("QR code")):
1815 d = QRDialog(data, self, title)
1819 def do_protect(self, func, args):
1820 if self.wallet.use_encryption:
1821 password = self.password_dialog()
1827 if args != (False,):
1828 args = (self,) + args + (password,)
1830 args = (self,password)
1834 def show_public_keys(self, address):
1835 if not address: return
1837 pubkey_list = self.wallet.get_public_keys(address)
1838 except Exception as e:
1839 traceback.print_exc(file=sys.stdout)
1840 self.show_message(str(e))
1844 d.setMinimumSize(600, 200)
1846 vbox = QVBoxLayout()
1847 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1848 vbox.addWidget( QLabel(_("Public key") + ':'))
1850 keys.setReadOnly(True)
1851 keys.setText('\n'.join(pubkey_list))
1852 vbox.addWidget(keys)
1853 vbox.addLayout(close_button(d))
1858 def show_private_key(self, address, password):
1859 if not address: return
1861 pk_list = self.wallet.get_private_key(address, password)
1862 except Exception as e:
1863 traceback.print_exc(file=sys.stdout)
1864 self.show_message(str(e))
1868 d.setMinimumSize(600, 200)
1870 vbox = QVBoxLayout()
1871 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1872 vbox.addWidget( QLabel(_("Private key") + ':'))
1874 keys.setReadOnly(True)
1875 keys.setText('\n'.join(pk_list))
1876 vbox.addWidget(keys)
1877 vbox.addLayout(close_button(d))
1883 def do_sign(self, address, message, signature, password):
1884 message = unicode(message.toPlainText())
1885 message = message.encode('utf-8')
1887 sig = self.wallet.sign_message(str(address.text()), message, password)
1888 signature.setText(sig)
1889 except Exception as e:
1890 self.show_message(str(e))
1892 def do_verify(self, address, message, signature):
1893 message = unicode(message.toPlainText())
1894 message = message.encode('utf-8')
1895 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1896 self.show_message(_("Signature verified"))
1898 self.show_message(_("Error: wrong signature"))
1901 def sign_verify_message(self, address=''):
1904 d.setWindowTitle(_('Sign/verify Message'))
1905 d.setMinimumSize(410, 290)
1907 layout = QGridLayout(d)
1909 message_e = QTextEdit()
1910 layout.addWidget(QLabel(_('Message')), 1, 0)
1911 layout.addWidget(message_e, 1, 1)
1912 layout.setRowStretch(2,3)
1914 address_e = QLineEdit()
1915 address_e.setText(address)
1916 layout.addWidget(QLabel(_('Address')), 2, 0)
1917 layout.addWidget(address_e, 2, 1)
1919 signature_e = QTextEdit()
1920 layout.addWidget(QLabel(_('Signature')), 3, 0)
1921 layout.addWidget(signature_e, 3, 1)
1922 layout.setRowStretch(3,1)
1924 hbox = QHBoxLayout()
1926 b = QPushButton(_("Sign"))
1927 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1930 b = QPushButton(_("Verify"))
1931 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1934 b = QPushButton(_("Close"))
1935 b.clicked.connect(d.accept)
1937 layout.addLayout(hbox, 4, 1)
1942 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1944 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1945 message_e.setText(decrypted)
1946 except Exception as e:
1947 self.show_message(str(e))
1950 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1951 message = unicode(message_e.toPlainText())
1952 message = message.encode('utf-8')
1954 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1955 encrypted_e.setText(encrypted)
1956 except Exception as e:
1957 self.show_message(str(e))
1961 def encrypt_message(self, address = ''):
1964 d.setWindowTitle(_('Encrypt/decrypt Message'))
1965 d.setMinimumSize(610, 490)
1967 layout = QGridLayout(d)
1969 message_e = QTextEdit()
1970 layout.addWidget(QLabel(_('Message')), 1, 0)
1971 layout.addWidget(message_e, 1, 1)
1972 layout.setRowStretch(2,3)
1974 pubkey_e = QLineEdit()
1976 pubkey = self.wallet.getpubkeys(address)[0]
1977 pubkey_e.setText(pubkey)
1978 layout.addWidget(QLabel(_('Public key')), 2, 0)
1979 layout.addWidget(pubkey_e, 2, 1)
1981 encrypted_e = QTextEdit()
1982 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1983 layout.addWidget(encrypted_e, 3, 1)
1984 layout.setRowStretch(3,1)
1986 hbox = QHBoxLayout()
1987 b = QPushButton(_("Encrypt"))
1988 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1991 b = QPushButton(_("Decrypt"))
1992 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1995 b = QPushButton(_("Close"))
1996 b.clicked.connect(d.accept)
1999 layout.addLayout(hbox, 4, 1)
2003 def question(self, msg):
2004 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2006 def show_message(self, msg):
2007 QMessageBox.information(self, _('Message'), msg, _('OK'))
2009 def password_dialog(self, msg=None):
2012 d.setWindowTitle(_("Enter Password"))
2017 vbox = QVBoxLayout()
2019 msg = _('Please enter your password')
2020 vbox.addWidget(QLabel(msg))
2022 grid = QGridLayout()
2024 grid.addWidget(QLabel(_('Password')), 1, 0)
2025 grid.addWidget(pw, 1, 1)
2026 vbox.addLayout(grid)
2028 vbox.addLayout(ok_cancel_buttons(d))
2031 run_hook('password_dialog', pw, grid, 1)
2032 if not d.exec_(): return
2033 return unicode(pw.text())
2042 def tx_from_text(self, txt):
2043 "json or raw hexadecimal"
2046 tx = Transaction(txt)
2052 tx_dict = json.loads(str(txt))
2053 assert "hex" in tx_dict.keys()
2054 tx = Transaction(tx_dict["hex"])
2055 if tx_dict.has_key("input_info"):
2056 input_info = json.loads(tx_dict['input_info'])
2057 tx.add_input_info(input_info)
2060 traceback.print_exc(file=sys.stdout)
2063 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2067 def read_tx_from_file(self):
2068 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2072 with open(fileName, "r") as f:
2073 file_content = f.read()
2074 except (ValueError, IOError, os.error), reason:
2075 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2077 return self.tx_from_text(file_content)
2081 def sign_raw_transaction(self, tx, input_info, password):
2082 self.wallet.signrawtransaction(tx, input_info, [], password)
2084 def do_process_from_text(self):
2085 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2088 tx = self.tx_from_text(text)
2090 self.show_transaction(tx)
2092 def do_process_from_file(self):
2093 tx = self.read_tx_from_file()
2095 self.show_transaction(tx)
2097 def do_process_from_txid(self):
2098 from electrum import transaction
2099 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2101 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2103 tx = transaction.Transaction(r)
2105 self.show_transaction(tx)
2107 self.show_message("unknown transaction")
2109 def do_process_from_csvReader(self, csvReader):
2114 for position, row in enumerate(csvReader):
2116 if not is_valid(address):
2117 errors.append((position, address))
2119 amount = Decimal(row[1])
2120 amount = int(100000000*amount)
2121 outputs.append((address, amount))
2122 except (ValueError, IOError, os.error), reason:
2123 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2127 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2128 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2132 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2133 except Exception as e:
2134 self.show_message(str(e))
2137 self.show_transaction(tx)
2139 def do_process_from_csv_file(self):
2140 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2144 with open(fileName, "r") as f:
2145 csvReader = csv.reader(f)
2146 self.do_process_from_csvReader(csvReader)
2147 except (ValueError, IOError, os.error), reason:
2148 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2151 def do_process_from_csv_text(self):
2152 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2153 + _("Format: address, amount. One output per line"), _("Load CSV"))
2156 f = StringIO.StringIO(text)
2157 csvReader = csv.reader(f)
2158 self.do_process_from_csvReader(csvReader)
2163 def export_privkeys_dialog(self, password):
2164 if self.wallet.is_watching_only():
2165 self.show_message(_("This is a watching-only wallet"))
2169 d.setWindowTitle(_('Private keys'))
2170 d.setMinimumSize(850, 300)
2171 vbox = QVBoxLayout(d)
2173 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2174 _("Exposing a single private key can compromise your entire wallet!"),
2175 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2176 vbox.addWidget(QLabel(msg))
2182 defaultname = 'electrum-private-keys.csv'
2183 select_msg = _('Select file to export your private keys to')
2184 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2185 vbox.addLayout(hbox)
2187 h, b = ok_cancel_buttons2(d, _('Export'))
2192 addresses = self.wallet.addresses(True)
2194 def privkeys_thread():
2195 for addr in addresses:
2199 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2200 d.emit(SIGNAL('computing_privkeys'))
2201 d.emit(SIGNAL('show_privkeys'))
2203 def show_privkeys():
2204 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2208 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2209 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2210 threading.Thread(target=privkeys_thread).start()
2216 filename = filename_e.text()
2221 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2222 except (IOError, os.error), reason:
2223 export_error_label = _("Electrum was unable to produce a private key-export.")
2224 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2226 except Exception as e:
2227 self.show_message(str(e))
2230 self.show_message(_("Private keys exported."))
2233 def do_export_privkeys(self, fileName, pklist, is_csv):
2234 with open(fileName, "w+") as f:
2236 transaction = csv.writer(f)
2237 transaction.writerow(["address", "private_key"])
2238 for addr, pk in pklist.items():
2239 transaction.writerow(["%34s"%addr,pk])
2242 f.write(json.dumps(pklist, indent = 4))
2245 def do_import_labels(self):
2246 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2247 if not labelsFile: return
2249 f = open(labelsFile, 'r')
2252 for key, value in json.loads(data).items():
2253 self.wallet.set_label(key, value)
2254 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2255 except (IOError, os.error), reason:
2256 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2259 def do_export_labels(self):
2260 labels = self.wallet.labels
2262 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2264 with open(fileName, 'w+') as f:
2265 json.dump(labels, f)
2266 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2267 except (IOError, os.error), reason:
2268 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2271 def export_history_dialog(self):
2274 d.setWindowTitle(_('Export History'))
2275 d.setMinimumSize(400, 200)
2276 vbox = QVBoxLayout(d)
2278 defaultname = os.path.expanduser('~/electrum-history.csv')
2279 select_msg = _('Select file to export your wallet transactions to')
2281 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2282 vbox.addLayout(hbox)
2286 h, b = ok_cancel_buttons2(d, _('Export'))
2291 filename = filename_e.text()
2296 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2297 except (IOError, os.error), reason:
2298 export_error_label = _("Electrum was unable to produce a transaction export.")
2299 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2302 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2305 def do_export_history(self, wallet, fileName, is_csv):
2306 history = wallet.get_tx_history()
2308 for item in history:
2309 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2311 if timestamp is not None:
2313 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2314 except [RuntimeError, TypeError, NameError] as reason:
2315 time_string = "unknown"
2318 time_string = "unknown"
2320 time_string = "pending"
2322 if value is not None:
2323 value_string = format_satoshis(value, True)
2328 fee_string = format_satoshis(fee, True)
2333 label, is_default_label = wallet.get_label(tx_hash)
2334 label = label.encode('utf-8')
2338 balance_string = format_satoshis(balance, False)
2340 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2342 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2344 with open(fileName, "w+") as f:
2346 transaction = csv.writer(f)
2347 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2349 transaction.writerow(line)
2352 f.write(json.dumps(lines, indent = 4))
2355 def sweep_key_dialog(self):
2357 d.setWindowTitle(_('Sweep private keys'))
2358 d.setMinimumSize(600, 300)
2360 vbox = QVBoxLayout(d)
2361 vbox.addWidget(QLabel(_("Enter private keys")))
2363 keys_e = QTextEdit()
2364 keys_e.setTabChangesFocus(True)
2365 vbox.addWidget(keys_e)
2367 h, address_e = address_field(self.wallet.addresses())
2371 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2372 vbox.addLayout(hbox)
2373 button.setEnabled(False)
2376 addr = str(address_e.text())
2377 if bitcoin.is_address(addr):
2381 pk = str(keys_e.toPlainText()).strip()
2382 if Wallet.is_private_key(pk):
2385 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2386 keys_e.textChanged.connect(f)
2387 address_e.textChanged.connect(f)
2391 fee = self.wallet.fee
2392 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2393 self.show_transaction(tx)
2397 def do_import_privkey(self, password):
2398 if not self.wallet.has_imported_keys():
2399 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2400 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2401 + _('Are you sure you understand what you are doing?'), 3, 4)
2404 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2407 text = str(text).split()
2412 addr = self.wallet.import_key(key, password)
2413 except Exception as e:
2419 addrlist.append(addr)
2421 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2423 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2424 self.update_address_tab()
2425 self.update_history_tab()
2428 def settings_dialog(self):
2430 d.setWindowTitle(_('Electrum Settings'))
2432 vbox = QVBoxLayout()
2433 grid = QGridLayout()
2434 grid.setColumnStretch(0,1)
2436 nz_label = QLabel(_('Display zeros') + ':')
2437 grid.addWidget(nz_label, 0, 0)
2438 nz_e = AmountEdit(None,True)
2439 nz_e.setText("%d"% self.num_zeros)
2440 grid.addWidget(nz_e, 0, 1)
2441 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2442 grid.addWidget(HelpButton(msg), 0, 2)
2443 if not self.config.is_modifiable('num_zeros'):
2444 for w in [nz_e, nz_label]: w.setEnabled(False)
2446 lang_label=QLabel(_('Language') + ':')
2447 grid.addWidget(lang_label, 1, 0)
2448 lang_combo = QComboBox()
2449 from electrum.i18n import languages
2450 lang_combo.addItems(languages.values())
2452 index = languages.keys().index(self.config.get("language",''))
2455 lang_combo.setCurrentIndex(index)
2456 grid.addWidget(lang_combo, 1, 1)
2457 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2458 if not self.config.is_modifiable('language'):
2459 for w in [lang_combo, lang_label]: w.setEnabled(False)
2462 fee_label = QLabel(_('Transaction fee') + ':')
2463 grid.addWidget(fee_label, 2, 0)
2464 fee_e = BTCAmountEdit(self.get_decimal_point)
2465 fee_e.setAmount(self.wallet.fee)
2466 grid.addWidget(fee_e, 2, 1)
2467 msg = _('Fee per kilobyte of transaction.') + '\n' \
2468 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2469 grid.addWidget(HelpButton(msg), 2, 2)
2470 if not self.config.is_modifiable('fee_per_kb'):
2471 for w in [fee_e, fee_label]: w.setEnabled(False)
2473 units = ['BTC', 'mBTC']
2474 unit_label = QLabel(_('Base unit') + ':')
2475 grid.addWidget(unit_label, 3, 0)
2476 unit_combo = QComboBox()
2477 unit_combo.addItems(units)
2478 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2479 grid.addWidget(unit_combo, 3, 1)
2480 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2481 + '\n1BTC=1000mBTC.\n' \
2482 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2484 usechange_cb = QCheckBox(_('Use change addresses'))
2485 usechange_cb.setChecked(self.wallet.use_change)
2486 grid.addWidget(usechange_cb, 4, 0)
2487 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2488 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2490 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2491 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2492 grid.addWidget(block_ex_label, 5, 0)
2493 block_ex_combo = QComboBox()
2494 block_ex_combo.addItems(block_explorers)
2495 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2496 grid.addWidget(block_ex_combo, 5, 1)
2497 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2499 show_tx = self.config.get('show_before_broadcast', False)
2500 showtx_cb = QCheckBox(_('Show before broadcast'))
2501 showtx_cb.setChecked(show_tx)
2502 grid.addWidget(showtx_cb, 6, 0)
2503 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2505 vbox.addLayout(grid)
2507 vbox.addLayout(ok_cancel_buttons(d))
2511 if not d.exec_(): return
2513 fee = fee_e.get_amount()
2515 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2518 self.wallet.set_fee(fee)
2520 nz = unicode(nz_e.text())
2525 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2528 if self.num_zeros != nz:
2530 self.config.set_key('num_zeros', nz, True)
2531 self.update_history_tab()
2532 self.update_address_tab()
2534 usechange_result = usechange_cb.isChecked()
2535 if self.wallet.use_change != usechange_result:
2536 self.wallet.use_change = usechange_result
2537 self.wallet.storage.put('use_change', self.wallet.use_change)
2539 if showtx_cb.isChecked() != show_tx:
2540 self.config.set_key('show_before_broadcast', not show_tx)
2542 unit_result = units[unit_combo.currentIndex()]
2543 if self.base_unit() != unit_result:
2544 self.decimal_point = 8 if unit_result == 'BTC' else 5
2545 self.config.set_key('decimal_point', self.decimal_point, True)
2546 self.update_history_tab()
2547 self.update_status()
2549 need_restart = False
2551 lang_request = languages.keys()[lang_combo.currentIndex()]
2552 if lang_request != self.config.get('language'):
2553 self.config.set_key("language", lang_request, True)
2556 be_result = block_explorers[block_ex_combo.currentIndex()]
2557 self.config.set_key('block_explorer', be_result, True)
2559 run_hook('close_settings_dialog')
2562 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2565 def run_network_dialog(self):
2566 if not self.network:
2568 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2570 def closeEvent(self, event):
2572 self.config.set_key("is_maximized", self.isMaximized())
2573 if not self.isMaximized():
2575 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2576 self.save_column_widths()
2577 self.config.set_key("console-history", self.console.history[-50:], True)
2578 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2582 def plugins_dialog(self):
2583 from electrum.plugins import plugins
2586 d.setWindowTitle(_('Electrum Plugins'))
2589 vbox = QVBoxLayout(d)
2592 scroll = QScrollArea()
2593 scroll.setEnabled(True)
2594 scroll.setWidgetResizable(True)
2595 scroll.setMinimumSize(400,250)
2596 vbox.addWidget(scroll)
2600 w.setMinimumHeight(len(plugins)*35)
2602 grid = QGridLayout()
2603 grid.setColumnStretch(0,1)
2606 def do_toggle(cb, p, w):
2609 if w: w.setEnabled(r)
2611 def mk_toggle(cb, p, w):
2612 return lambda: do_toggle(cb,p,w)
2614 for i, p in enumerate(plugins):
2616 cb = QCheckBox(p.fullname())
2617 cb.setDisabled(not p.is_available())
2618 cb.setChecked(p.is_enabled())
2619 grid.addWidget(cb, i, 0)
2620 if p.requires_settings():
2621 w = p.settings_widget(self)
2622 w.setEnabled( p.is_enabled() )
2623 grid.addWidget(w, i, 1)
2626 cb.clicked.connect(mk_toggle(cb,p,w))
2627 grid.addWidget(HelpButton(p.description()), i, 2)
2629 print_msg(_("Error: cannot display plugin"), p)
2630 traceback.print_exc(file=sys.stdout)
2631 grid.setRowStretch(i+1,1)
2633 vbox.addLayout(close_button(d))
2638 def show_account_details(self, k):
2639 account = self.wallet.accounts[k]
2642 d.setWindowTitle(_('Account Details'))
2645 vbox = QVBoxLayout(d)
2646 name = self.wallet.get_account_name(k)
2647 label = QLabel('Name: ' + name)
2648 vbox.addWidget(label)
2650 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2652 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2654 vbox.addWidget(QLabel(_('Master Public Key:')))
2657 text.setReadOnly(True)
2658 text.setMaximumHeight(170)
2659 vbox.addWidget(text)
2661 mpk_text = '\n'.join( account.get_master_pubkeys() )
2662 text.setText(mpk_text)
2664 vbox.addLayout(close_button(d))