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 clear_button = QPushButton(_('New'))
686 clear_button.clicked.connect(self.clear_receive_tab)
687 grid.addWidget(clear_button, 3, 1)
688 save_button = QPushButton(_('Save'))
689 save_button.clicked.connect(self.save_payment_request)
690 grid.addWidget(save_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.clear_receive_tab()
728 def receive_list_menu(self, position):
729 item = self.receive_list.itemAt(position)
731 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
732 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
734 def save_payment_request(self):
735 addr = str(self.receive_address_e.text())
736 amount = self.receive_amount_e.get_amount()
737 message = str(self.receive_message_e.text())
738 if not message and not amount:
739 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
741 self.receive_requests = self.wallet.storage.get('receive_requests',{})
742 self.receive_requests[addr] = (amount, message)
743 self.wallet.storage.put('receive_requests', self.receive_requests)
744 self.update_receive_tab()
746 def clear_receive_tab(self):
747 self.receive_requests = self.wallet.storage.get('receive_requests',{})
748 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
750 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
754 self.receive_address_e.setText(addr)
755 self.receive_message_e.setText('')
756 self.receive_amount_e.setAmount(None)
758 def receive_at(self, addr):
759 if not bitcoin.is_address(addr):
761 self.tabs.setCurrentIndex(2)
762 self.receive_address_e.setText(addr)
764 def update_receive_tab(self):
765 self.receive_requests = self.wallet.storage.get('receive_requests',{})
766 b = len(self.receive_requests) > 0
767 self.receive_list.setVisible(b)
768 self.receive_requests_label.setVisible(b)
770 self.receive_list.clear()
771 for address, v in self.receive_requests.items():
773 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else "", ""] )
774 self.receive_list.addTopLevelItem(item)
777 def update_receive_qr(self):
778 import urlparse, urllib
779 addr = str(self.receive_address_e.text())
782 amount = self.receive_amount_e.get_amount()
784 query.append('amount=%s'%format_satoshis(amount))
785 message = unicode(self.receive_message_e.text()).encode('utf8')
787 query.append('message=%s'%urllib.quote(message))
788 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
789 url = urlparse.urlunparse(p)
792 self.receive_qr.set_addr(url)
795 def create_send_tab(self):
798 self.send_grid = grid = QGridLayout(w)
800 grid.setColumnMinimumWidth(3,300)
801 grid.setColumnStretch(5,1)
802 grid.setRowStretch(8, 1)
804 from paytoedit import PayToEdit
805 self.amount_e = BTCAmountEdit(self.get_decimal_point)
806 self.payto_e = PayToEdit(self)
807 self.payto_help = HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)'))
808 grid.addWidget(QLabel(_('Pay to')), 1, 0)
809 grid.addWidget(self.payto_e, 1, 1, 1, 3)
810 grid.addWidget(self.payto_help, 1, 4)
812 completer = QCompleter()
813 completer.setCaseSensitivity(False)
814 self.payto_e.setCompleter(completer)
815 completer.setModel(self.completions)
817 self.message_e = MyLineEdit()
818 self.message_help = HelpButton(_('Description of the transaction (not mandatory).') + '\n\n' + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.'))
819 grid.addWidget(QLabel(_('Description')), 2, 0)
820 grid.addWidget(self.message_e, 2, 1, 1, 3)
821 grid.addWidget(self.message_help, 2, 4)
823 self.from_label = QLabel(_('From'))
824 grid.addWidget(self.from_label, 3, 0)
825 self.from_list = MyTreeWidget(self)
826 self.from_list.setColumnCount(2)
827 self.from_list.setColumnWidth(0, 350)
828 self.from_list.setColumnWidth(1, 50)
829 self.from_list.setHeaderHidden(True)
830 self.from_list.setMaximumHeight(80)
831 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
832 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
833 grid.addWidget(self.from_list, 3, 1, 1, 3)
834 self.set_pay_from([])
836 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
837 + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') \
838 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
839 grid.addWidget(QLabel(_('Amount')), 4, 0)
840 grid.addWidget(self.amount_e, 4, 1, 1, 2)
841 grid.addWidget(self.amount_help, 4, 3)
843 self.fee_e = BTCAmountEdit(self.get_decimal_point)
844 grid.addWidget(QLabel(_('Fee')), 5, 0)
845 grid.addWidget(self.fee_e, 5, 1, 1, 2)
846 grid.addWidget(HelpButton(
847 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
848 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
849 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 5, 3)
851 self.send_button = EnterButton(_("Send"), self.do_send)
852 grid.addWidget(self.send_button, 6, 1)
854 b = EnterButton(_("Clear"), self.do_clear)
855 grid.addWidget(b, 6, 2)
857 self.payto_sig = QLabel('')
858 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
860 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
861 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
864 def entry_changed( is_fee ):
865 self.funds_error = False
867 if self.amount_e.is_shortcut:
868 self.amount_e.is_shortcut = False
869 sendable = self.get_sendable_balance()
870 # there is only one output because we are completely spending inputs
871 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
872 fee = self.wallet.estimated_fee(inputs, 1)
874 self.amount_e.setAmount(amount)
875 self.fee_e.setAmount(fee)
878 amount = self.amount_e.get_amount()
879 fee = self.fee_e.get_amount()
881 if not is_fee: fee = None
884 # assume that there will be 2 outputs (one for change)
885 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
887 self.fee_e.setAmount(fee)
890 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
894 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
895 self.funds_error = True
896 text = _( "Not enough funds" )
897 c, u = self.wallet.get_frozen_balance()
898 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
900 self.statusBar().showMessage(text)
901 self.amount_e.setPalette(palette)
902 self.fee_e.setPalette(palette)
904 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
905 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
907 run_hook('create_send_tab', grid)
910 def from_list_delete(self, item):
911 i = self.from_list.indexOfTopLevelItem(item)
913 self.redraw_from_list()
915 def from_list_menu(self, position):
916 item = self.from_list.itemAt(position)
918 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
919 menu.exec_(self.from_list.viewport().mapToGlobal(position))
921 def set_pay_from(self, domain = None):
922 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
923 self.redraw_from_list()
925 def redraw_from_list(self):
926 self.from_list.clear()
927 self.from_label.setHidden(len(self.pay_from) == 0)
928 self.from_list.setHidden(len(self.pay_from) == 0)
931 h = x.get('prevout_hash')
932 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
934 for item in self.pay_from:
935 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
937 def update_completions(self):
939 for addr,label in self.wallet.labels.items():
940 if addr in self.wallet.addressbook:
941 l.append( label + ' <' + addr + '>')
943 run_hook('update_completions', l)
944 self.completions.setStringList(l)
948 return lambda s, *args: s.do_protect(func, args)
951 def read_send_tab(self):
953 if self.payment_request and self.payment_request.has_expired():
954 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
957 label = unicode( self.message_e.text() )
959 if self.payment_request:
960 outputs = self.payment_request.get_outputs()
962 outputs = self.payto_e.get_outputs()
965 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
968 for addr, x in outputs:
969 if addr is None or not bitcoin.is_address(addr):
970 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
973 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
976 amount = sum(map(lambda x:x[1], outputs))
978 fee = self.fee_e.get_amount()
980 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
983 confirm_amount = self.config.get('confirm_amount', 100000000)
984 if amount >= confirm_amount:
985 o = '\n'.join(map(lambda x:x[0], outputs))
986 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
989 confirm_fee = self.config.get('confirm_fee', 100000)
990 if fee >= confirm_fee:
991 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()}):
994 coins = self.get_coins()
995 return outputs, fee, label, coins
999 r = self.read_send_tab()
1002 outputs, fee, label, coins = r
1003 self.send_tx(outputs, fee, label, coins)
1007 def send_tx(self, outputs, fee, label, coins, password):
1008 self.send_button.setDisabled(True)
1010 # first, create an unsigned tx
1012 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1014 except Exception as e:
1015 traceback.print_exc(file=sys.stdout)
1016 self.show_message(str(e))
1017 self.send_button.setDisabled(False)
1020 # call hook to see if plugin needs gui interaction
1021 run_hook('send_tx', tx)
1027 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
1028 self.wallet.sign_transaction(tx, keypairs, password)
1029 return tx, fee, label
1031 def sign_done(tx, fee, label):
1033 self.show_message(tx.error)
1034 self.send_button.setDisabled(False)
1036 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1037 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1038 self.send_button.setDisabled(False)
1041 self.wallet.set_label(tx.hash(), label)
1043 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1044 self.show_transaction(tx)
1046 self.send_button.setDisabled(False)
1049 self.broadcast_transaction(tx)
1051 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1052 self.waiting_dialog.start()
1056 def broadcast_transaction(self, tx):
1058 def broadcast_thread():
1059 pr = self.payment_request
1061 return self.wallet.sendtx(tx)
1063 if pr.has_expired():
1064 self.payment_request = None
1065 return False, _("Payment request has expired")
1067 status, msg = self.wallet.sendtx(tx)
1071 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1072 self.wallet.storage.put('invoices', self.invoices)
1073 self.update_invoices_tab()
1074 self.payment_request = None
1075 refund_address = self.wallet.addresses()[0]
1076 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1082 def broadcast_done(status, msg):
1084 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1087 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1088 self.send_button.setDisabled(False)
1090 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1091 self.waiting_dialog.start()
1095 def prepare_for_payment_request(self):
1096 self.tabs.setCurrentIndex(1)
1097 self.payto_e.is_pr = True
1098 for e in [self.payto_e, self.amount_e, self.message_e]:
1100 for h in [self.payto_help, self.amount_help, self.message_help]:
1102 self.payto_e.setText(_("please wait..."))
1105 def payment_request_ok(self):
1106 pr = self.payment_request
1108 if pr_id not in self.invoices:
1109 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1110 self.wallet.storage.put('invoices', self.invoices)
1111 self.update_invoices_tab()
1113 print_error('invoice already in list')
1115 status = self.invoices[pr_id][4]
1116 if status == PR_PAID:
1118 self.show_message("invoice already paid")
1119 self.payment_request = None
1122 self.payto_help.show()
1123 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1125 if not pr.has_expired():
1126 self.payto_e.setGreen()
1128 self.payto_e.setExpired()
1130 self.payto_e.setText(pr.domain)
1131 self.amount_e.setText(self.format_amount(pr.get_amount()))
1132 self.message_e.setText(pr.get_memo())
1134 def payment_request_error(self):
1136 self.show_message(self.payment_request.error)
1137 self.payment_request = None
1139 def pay_from_URI(self,URI):
1142 address, amount, label, message, request_url = util.parse_URI(URI)
1144 address, amount, label, message, request_url = util.parse_URI(URI)
1145 except Exception as e:
1146 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1149 self.tabs.setCurrentIndex(1)
1153 if self.wallet.labels.get(address) != label:
1154 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1155 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1156 self.wallet.addressbook.append(address)
1157 self.wallet.set_label(address, label)
1159 label = self.wallet.labels.get(address)
1161 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1163 self.message_e.setText(message)
1165 self.amount_e.setAmount(amount)
1168 from electrum import paymentrequest
1169 def payment_request():
1170 self.payment_request = paymentrequest.PaymentRequest(self.config)
1171 self.payment_request.read(request_url)
1172 if self.payment_request.verify():
1173 self.emit(SIGNAL('payment_request_ok'))
1175 self.emit(SIGNAL('payment_request_error'))
1177 self.pr_thread = threading.Thread(target=payment_request).start()
1178 self.prepare_for_payment_request()
1183 self.payto_e.is_pr = False
1184 self.payto_sig.setVisible(False)
1185 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1189 for h in [self.payto_help, self.amount_help, self.message_help]:
1192 self.payto_help.set_alt(None)
1193 self.set_pay_from([])
1194 self.update_status()
1198 def set_addrs_frozen(self,addrs,freeze):
1200 if not addr: continue
1201 if addr in self.wallet.frozen_addresses and not freeze:
1202 self.wallet.unfreeze(addr)
1203 elif addr not in self.wallet.frozen_addresses and freeze:
1204 self.wallet.freeze(addr)
1205 self.update_address_tab()
1209 def create_list_tab(self, headers):
1210 "generic tab creation method"
1211 l = MyTreeWidget(self)
1212 l.setColumnCount( len(headers) )
1213 l.setHeaderLabels( headers )
1216 vbox = QVBoxLayout()
1223 vbox.addWidget(buttons)
1228 def create_addresses_tab(self):
1229 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1230 for i,width in enumerate(self.column_widths['receive']):
1231 l.setColumnWidth(i, width)
1232 l.setContextMenuPolicy(Qt.CustomContextMenu)
1233 l.customContextMenuRequested.connect(self.create_receive_menu)
1234 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1235 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1236 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1237 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1238 self.address_list = l
1244 def save_column_widths(self):
1245 self.column_widths["receive"] = []
1246 for i in range(self.address_list.columnCount() -1):
1247 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1249 self.column_widths["history"] = []
1250 for i in range(self.history_list.columnCount() - 1):
1251 self.column_widths["history"].append(self.history_list.columnWidth(i))
1253 self.column_widths["contacts"] = []
1254 for i in range(self.contacts_list.columnCount() - 1):
1255 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1257 self.config.set_key("column_widths_2", self.column_widths, True)
1260 def create_contacts_tab(self):
1261 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1262 l.setContextMenuPolicy(Qt.CustomContextMenu)
1263 l.customContextMenuRequested.connect(self.create_contact_menu)
1264 for i,width in enumerate(self.column_widths['contacts']):
1265 l.setColumnWidth(i, width)
1266 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1267 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1268 self.contacts_list = l
1272 def create_invoices_tab(self):
1273 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1275 h.setStretchLastSection(False)
1276 h.setResizeMode(1, QHeaderView.Stretch)
1277 l.setContextMenuPolicy(Qt.CustomContextMenu)
1278 l.customContextMenuRequested.connect(self.create_invoice_menu)
1279 self.invoices_list = l
1282 def update_invoices_tab(self):
1283 invoices = self.wallet.storage.get('invoices', {})
1284 l = self.invoices_list
1286 for key, value in invoices.items():
1288 domain, memo, amount, expiration_date, status, tx_hash = value
1292 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1294 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1295 l.addTopLevelItem(item)
1297 l.setCurrentItem(l.topLevelItem(0))
1301 def delete_imported_key(self, addr):
1302 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1303 self.wallet.delete_imported_key(addr)
1304 self.update_address_tab()
1305 self.update_history_tab()
1307 def edit_account_label(self, k):
1308 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1310 label = unicode(text)
1311 self.wallet.set_label(k,label)
1312 self.update_address_tab()
1314 def account_set_expanded(self, item, k, b):
1316 self.accounts_expanded[k] = b
1318 def create_account_menu(self, position, k, item):
1320 if item.isExpanded():
1321 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1323 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1324 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1325 if self.wallet.seed_version > 4:
1326 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1327 if self.wallet.account_is_pending(k):
1328 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1329 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1331 def delete_pending_account(self, k):
1332 self.wallet.delete_pending_account(k)
1333 self.update_address_tab()
1335 def create_receive_menu(self, position):
1336 # fixme: this function apparently has a side effect.
1337 # if it is not called the menu pops up several times
1338 #self.address_list.selectedIndexes()
1340 selected = self.address_list.selectedItems()
1341 multi_select = len(selected) > 1
1342 addrs = [unicode(item.text(0)) for item in selected]
1343 if not multi_select:
1344 item = self.address_list.itemAt(position)
1348 if not is_valid(addr):
1349 k = str(item.data(0,32).toString())
1351 self.create_account_menu(position, k, item)
1353 item.setExpanded(not item.isExpanded())
1357 if not multi_select:
1358 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1359 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1360 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1361 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1362 if not self.wallet.is_watching_only():
1363 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1364 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1365 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1366 if self.wallet.is_imported(addr):
1367 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1369 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1370 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1371 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1372 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1374 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1375 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1377 run_hook('receive_menu', menu, addrs)
1378 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1381 def get_sendable_balance(self):
1382 return sum(map(lambda x:x['value'], self.get_coins()))
1385 def get_coins(self):
1387 return self.pay_from
1389 domain = self.wallet.get_account_addresses(self.current_account)
1390 for i in self.wallet.frozen_addresses:
1391 if i in domain: domain.remove(i)
1392 return self.wallet.get_unspent_coins(domain)
1395 def send_from_addresses(self, addrs):
1396 self.set_pay_from( addrs )
1397 self.tabs.setCurrentIndex(1)
1400 def payto(self, addr):
1402 label = self.wallet.labels.get(addr)
1403 m_addr = label + ' <' + addr + '>' if label else addr
1404 self.tabs.setCurrentIndex(1)
1405 self.payto_e.setText(m_addr)
1406 self.amount_e.setFocus()
1409 def delete_contact(self, x):
1410 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1411 self.wallet.delete_contact(x)
1412 self.wallet.set_label(x, None)
1413 self.update_history_tab()
1414 self.update_contacts_tab()
1415 self.update_completions()
1418 def create_contact_menu(self, position):
1419 item = self.contacts_list.itemAt(position)
1422 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1424 addr = unicode(item.text(0))
1425 label = unicode(item.text(1))
1426 is_editable = item.data(0,32).toBool()
1427 payto_addr = item.data(0,33).toString()
1428 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1429 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1430 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1432 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1433 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1435 run_hook('create_contact_menu', menu, item)
1436 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1438 def delete_invoice(self, key):
1439 self.invoices.pop(key)
1440 self.wallet.storage.put('invoices', self.invoices)
1441 self.update_invoices_tab()
1443 def show_invoice(self, key):
1444 from electrum.paymentrequest import PaymentRequest
1445 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1446 pr = PaymentRequest(self.config)
1450 self.show_pr_details(pr)
1452 def show_pr_details(self, pr):
1453 msg = 'Domain: ' + pr.domain
1454 msg += '\nStatus: ' + pr.get_status()
1455 msg += '\nMemo: ' + pr.get_memo()
1456 msg += '\nPayment URL: ' + pr.payment_url
1457 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1458 QMessageBox.information(self, 'Invoice', msg , 'OK')
1460 def do_pay_invoice(self, key):
1461 from electrum.paymentrequest import PaymentRequest
1462 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1463 pr = PaymentRequest(self.config)
1466 self.payment_request = pr
1467 self.prepare_for_payment_request()
1469 self.payment_request_ok()
1471 self.payment_request_error()
1474 def create_invoice_menu(self, position):
1475 item = self.invoices_list.itemAt(position)
1478 k = self.invoices_list.indexOfTopLevelItem(item)
1479 key = self.invoices.keys()[k]
1480 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1482 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1483 if status == PR_UNPAID:
1484 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1485 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1486 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1489 def update_address_item(self, item):
1490 item.setFont(0, QFont(MONOSPACE_FONT))
1491 address = str(item.data(0,0).toString())
1492 label = self.wallet.labels.get(address,'')
1493 item.setData(1,0,label)
1494 item.setData(0,32, True) # is editable
1496 run_hook('update_address_item', address, item)
1498 if not self.wallet.is_mine(address): return
1500 c, u = self.wallet.get_addr_balance(address)
1501 balance = self.format_amount(c + u)
1502 item.setData(2,0,balance)
1504 if address in self.wallet.frozen_addresses:
1505 item.setBackgroundColor(0, QColor('lightblue'))
1508 def update_address_tab(self):
1509 l = self.address_list
1510 # extend the syntax for consistency
1511 l.addChild = l.addTopLevelItem
1512 l.insertChild = l.insertTopLevelItem
1516 accounts = self.wallet.get_accounts()
1517 if self.current_account is None:
1518 account_items = sorted(accounts.items())
1520 account_items = [(self.current_account, accounts.get(self.current_account))]
1523 for k, account in account_items:
1525 if len(accounts) > 1:
1526 name = self.wallet.get_account_name(k)
1527 c,u = self.wallet.get_account_balance(k)
1528 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1529 l.addTopLevelItem(account_item)
1530 account_item.setExpanded(self.accounts_expanded.get(k, True))
1531 account_item.setData(0, 32, k)
1535 sequences = [0,1] if account.has_change() else [0]
1536 for is_change in sequences:
1537 if len(sequences) > 1:
1538 name = _("Receiving") if not is_change else _("Change")
1539 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1540 account_item.addChild(seq_item)
1542 seq_item.setExpanded(True)
1544 seq_item = account_item
1546 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1552 for address in account.get_addresses(is_change):
1554 num, is_used = self.wallet.is_used(address)
1557 if gap > self.wallet.gap_limit:
1562 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1563 self.update_address_item(item)
1565 item.setBackgroundColor(1, QColor('red'))
1569 seq_item.insertChild(0,used_item)
1571 used_item.addChild(item)
1573 seq_item.addChild(item)
1575 # we use column 1 because column 0 may be hidden
1576 l.setCurrentItem(l.topLevelItem(0),1)
1579 def update_contacts_tab(self):
1580 l = self.contacts_list
1583 for address in self.wallet.addressbook:
1584 label = self.wallet.labels.get(address,'')
1585 n = self.wallet.get_num_tx(address)
1586 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1587 item.setFont(0, QFont(MONOSPACE_FONT))
1588 # 32 = label can be edited (bool)
1589 item.setData(0,32, True)
1591 item.setData(0,33, address)
1592 l.addTopLevelItem(item)
1594 run_hook('update_contacts_tab', l)
1595 l.setCurrentItem(l.topLevelItem(0))
1599 def create_console_tab(self):
1600 from console import Console
1601 self.console = console = Console()
1605 def update_console(self):
1606 console = self.console
1607 console.history = self.config.get("console-history",[])
1608 console.history_index = len(console.history)
1610 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1611 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1613 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1615 def mkfunc(f, method):
1616 return lambda *args: apply( f, (method, args, self.password_dialog ))
1618 if m[0]=='_' or m in ['network','wallet']: continue
1619 methods[m] = mkfunc(c._run, m)
1621 console.updateNamespace(methods)
1624 def change_account(self,s):
1625 if s == _("All accounts"):
1626 self.current_account = None
1628 accounts = self.wallet.get_account_names()
1629 for k, v in accounts.items():
1631 self.current_account = k
1632 self.update_history_tab()
1633 self.update_status()
1634 self.update_address_tab()
1635 self.update_receive_tab()
1637 def create_status_bar(self):
1640 sb.setFixedHeight(35)
1641 qtVersion = qVersion()
1643 self.balance_label = QLabel("")
1644 sb.addWidget(self.balance_label)
1646 from version_getter import UpdateLabel
1647 self.updatelabel = UpdateLabel(self.config, sb)
1649 self.account_selector = QComboBox()
1650 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1651 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1652 sb.addPermanentWidget(self.account_selector)
1654 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1655 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1657 self.lock_icon = QIcon()
1658 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1659 sb.addPermanentWidget( self.password_button )
1661 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1662 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1663 sb.addPermanentWidget( self.seed_button )
1664 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1665 sb.addPermanentWidget( self.status_button )
1667 run_hook('create_status_bar', (sb,))
1669 self.setStatusBar(sb)
1672 def update_lock_icon(self):
1673 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1674 self.password_button.setIcon( icon )
1677 def update_buttons_on_seed(self):
1678 if self.wallet.has_seed():
1679 self.seed_button.show()
1681 self.seed_button.hide()
1683 if not self.wallet.is_watching_only():
1684 self.password_button.show()
1685 self.send_button.setText(_("Send"))
1687 self.password_button.hide()
1688 self.send_button.setText(_("Create unsigned transaction"))
1691 def change_password_dialog(self):
1692 from password_dialog import PasswordDialog
1693 d = PasswordDialog(self.wallet, self)
1695 self.update_lock_icon()
1698 def new_contact_dialog(self):
1701 d.setWindowTitle(_("New Contact"))
1702 vbox = QVBoxLayout(d)
1703 vbox.addWidget(QLabel(_('New Contact')+':'))
1705 grid = QGridLayout()
1708 grid.addWidget(QLabel(_("Address")), 1, 0)
1709 grid.addWidget(line1, 1, 1)
1710 grid.addWidget(QLabel(_("Name")), 2, 0)
1711 grid.addWidget(line2, 2, 1)
1713 vbox.addLayout(grid)
1714 vbox.addLayout(ok_cancel_buttons(d))
1719 address = str(line1.text())
1720 label = unicode(line2.text())
1722 if not is_valid(address):
1723 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1726 self.wallet.add_contact(address)
1728 self.wallet.set_label(address, label)
1730 self.update_contacts_tab()
1731 self.update_history_tab()
1732 self.update_completions()
1733 self.tabs.setCurrentIndex(3)
1737 def new_account_dialog(self, password):
1739 dialog = QDialog(self)
1741 dialog.setWindowTitle(_("New Account"))
1743 vbox = QVBoxLayout()
1744 vbox.addWidget(QLabel(_('Account name')+':'))
1747 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1748 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1753 vbox.addLayout(ok_cancel_buttons(dialog))
1754 dialog.setLayout(vbox)
1758 name = str(e.text())
1761 self.wallet.create_pending_account(name, password)
1762 self.update_address_tab()
1763 self.tabs.setCurrentIndex(2)
1768 def show_master_public_keys(self):
1770 dialog = QDialog(self)
1772 dialog.setWindowTitle(_("Master Public Keys"))
1774 main_layout = QGridLayout()
1775 mpk_dict = self.wallet.get_master_public_keys()
1777 for key, value in mpk_dict.items():
1778 main_layout.addWidget(QLabel(key), i, 0)
1779 mpk_text = QTextEdit()
1780 mpk_text.setReadOnly(True)
1781 mpk_text.setMaximumHeight(170)
1782 mpk_text.setText(value)
1783 main_layout.addWidget(mpk_text, i + 1, 0)
1786 vbox = QVBoxLayout()
1787 vbox.addLayout(main_layout)
1788 vbox.addLayout(close_button(dialog))
1790 dialog.setLayout(vbox)
1795 def show_seed_dialog(self, password):
1796 if not self.wallet.has_seed():
1797 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1801 mnemonic = self.wallet.get_mnemonic(password)
1803 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1805 from seed_dialog import SeedDialog
1806 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1811 def show_qrcode(self, data, title = _("QR code")):
1814 d = QRDialog(data, self, title)
1818 def do_protect(self, func, args):
1819 if self.wallet.use_encryption:
1820 password = self.password_dialog()
1826 if args != (False,):
1827 args = (self,) + args + (password,)
1829 args = (self,password)
1833 def show_public_keys(self, address):
1834 if not address: return
1836 pubkey_list = self.wallet.get_public_keys(address)
1837 except Exception as e:
1838 traceback.print_exc(file=sys.stdout)
1839 self.show_message(str(e))
1843 d.setMinimumSize(600, 200)
1845 vbox = QVBoxLayout()
1846 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1847 vbox.addWidget( QLabel(_("Public key") + ':'))
1849 keys.setReadOnly(True)
1850 keys.setText('\n'.join(pubkey_list))
1851 vbox.addWidget(keys)
1852 vbox.addLayout(close_button(d))
1857 def show_private_key(self, address, password):
1858 if not address: return
1860 pk_list = self.wallet.get_private_key(address, password)
1861 except Exception as e:
1862 traceback.print_exc(file=sys.stdout)
1863 self.show_message(str(e))
1867 d.setMinimumSize(600, 200)
1869 vbox = QVBoxLayout()
1870 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1871 vbox.addWidget( QLabel(_("Private key") + ':'))
1873 keys.setReadOnly(True)
1874 keys.setText('\n'.join(pk_list))
1875 vbox.addWidget(keys)
1876 vbox.addLayout(close_button(d))
1882 def do_sign(self, address, message, signature, password):
1883 message = unicode(message.toPlainText())
1884 message = message.encode('utf-8')
1886 sig = self.wallet.sign_message(str(address.text()), message, password)
1887 signature.setText(sig)
1888 except Exception as e:
1889 self.show_message(str(e))
1891 def do_verify(self, address, message, signature):
1892 message = unicode(message.toPlainText())
1893 message = message.encode('utf-8')
1894 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1895 self.show_message(_("Signature verified"))
1897 self.show_message(_("Error: wrong signature"))
1900 def sign_verify_message(self, address=''):
1903 d.setWindowTitle(_('Sign/verify Message'))
1904 d.setMinimumSize(410, 290)
1906 layout = QGridLayout(d)
1908 message_e = QTextEdit()
1909 layout.addWidget(QLabel(_('Message')), 1, 0)
1910 layout.addWidget(message_e, 1, 1)
1911 layout.setRowStretch(2,3)
1913 address_e = QLineEdit()
1914 address_e.setText(address)
1915 layout.addWidget(QLabel(_('Address')), 2, 0)
1916 layout.addWidget(address_e, 2, 1)
1918 signature_e = QTextEdit()
1919 layout.addWidget(QLabel(_('Signature')), 3, 0)
1920 layout.addWidget(signature_e, 3, 1)
1921 layout.setRowStretch(3,1)
1923 hbox = QHBoxLayout()
1925 b = QPushButton(_("Sign"))
1926 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1929 b = QPushButton(_("Verify"))
1930 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1933 b = QPushButton(_("Close"))
1934 b.clicked.connect(d.accept)
1936 layout.addLayout(hbox, 4, 1)
1941 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1943 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1944 message_e.setText(decrypted)
1945 except Exception as e:
1946 self.show_message(str(e))
1949 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1950 message = unicode(message_e.toPlainText())
1951 message = message.encode('utf-8')
1953 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1954 encrypted_e.setText(encrypted)
1955 except Exception as e:
1956 self.show_message(str(e))
1960 def encrypt_message(self, address = ''):
1963 d.setWindowTitle(_('Encrypt/decrypt Message'))
1964 d.setMinimumSize(610, 490)
1966 layout = QGridLayout(d)
1968 message_e = QTextEdit()
1969 layout.addWidget(QLabel(_('Message')), 1, 0)
1970 layout.addWidget(message_e, 1, 1)
1971 layout.setRowStretch(2,3)
1973 pubkey_e = QLineEdit()
1975 pubkey = self.wallet.getpubkeys(address)[0]
1976 pubkey_e.setText(pubkey)
1977 layout.addWidget(QLabel(_('Public key')), 2, 0)
1978 layout.addWidget(pubkey_e, 2, 1)
1980 encrypted_e = QTextEdit()
1981 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1982 layout.addWidget(encrypted_e, 3, 1)
1983 layout.setRowStretch(3,1)
1985 hbox = QHBoxLayout()
1986 b = QPushButton(_("Encrypt"))
1987 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1990 b = QPushButton(_("Decrypt"))
1991 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1994 b = QPushButton(_("Close"))
1995 b.clicked.connect(d.accept)
1998 layout.addLayout(hbox, 4, 1)
2002 def question(self, msg):
2003 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2005 def show_message(self, msg):
2006 QMessageBox.information(self, _('Message'), msg, _('OK'))
2008 def password_dialog(self, msg=None):
2011 d.setWindowTitle(_("Enter Password"))
2016 vbox = QVBoxLayout()
2018 msg = _('Please enter your password')
2019 vbox.addWidget(QLabel(msg))
2021 grid = QGridLayout()
2023 grid.addWidget(QLabel(_('Password')), 1, 0)
2024 grid.addWidget(pw, 1, 1)
2025 vbox.addLayout(grid)
2027 vbox.addLayout(ok_cancel_buttons(d))
2030 run_hook('password_dialog', pw, grid, 1)
2031 if not d.exec_(): return
2032 return unicode(pw.text())
2041 def tx_from_text(self, txt):
2042 "json or raw hexadecimal"
2045 tx = Transaction(txt)
2051 tx_dict = json.loads(str(txt))
2052 assert "hex" in tx_dict.keys()
2053 tx = Transaction(tx_dict["hex"])
2054 if tx_dict.has_key("input_info"):
2055 input_info = json.loads(tx_dict['input_info'])
2056 tx.add_input_info(input_info)
2059 traceback.print_exc(file=sys.stdout)
2062 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2066 def read_tx_from_file(self):
2067 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2071 with open(fileName, "r") as f:
2072 file_content = f.read()
2073 except (ValueError, IOError, os.error), reason:
2074 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2076 return self.tx_from_text(file_content)
2080 def sign_raw_transaction(self, tx, input_info, password):
2081 self.wallet.signrawtransaction(tx, input_info, [], password)
2083 def do_process_from_text(self):
2084 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2087 tx = self.tx_from_text(text)
2089 self.show_transaction(tx)
2091 def do_process_from_file(self):
2092 tx = self.read_tx_from_file()
2094 self.show_transaction(tx)
2096 def do_process_from_txid(self):
2097 from electrum import transaction
2098 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2100 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2102 tx = transaction.Transaction(r)
2104 self.show_transaction(tx)
2106 self.show_message("unknown transaction")
2108 def do_process_from_csvReader(self, csvReader):
2113 for position, row in enumerate(csvReader):
2115 if not is_valid(address):
2116 errors.append((position, address))
2118 amount = Decimal(row[1])
2119 amount = int(100000000*amount)
2120 outputs.append((address, amount))
2121 except (ValueError, IOError, os.error), reason:
2122 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2126 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2127 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2131 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2132 except Exception as e:
2133 self.show_message(str(e))
2136 self.show_transaction(tx)
2138 def do_process_from_csv_file(self):
2139 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2143 with open(fileName, "r") as f:
2144 csvReader = csv.reader(f)
2145 self.do_process_from_csvReader(csvReader)
2146 except (ValueError, IOError, os.error), reason:
2147 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2150 def do_process_from_csv_text(self):
2151 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2152 + _("Format: address, amount. One output per line"), _("Load CSV"))
2155 f = StringIO.StringIO(text)
2156 csvReader = csv.reader(f)
2157 self.do_process_from_csvReader(csvReader)
2162 def export_privkeys_dialog(self, password):
2163 if self.wallet.is_watching_only():
2164 self.show_message(_("This is a watching-only wallet"))
2168 d.setWindowTitle(_('Private keys'))
2169 d.setMinimumSize(850, 300)
2170 vbox = QVBoxLayout(d)
2172 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2173 _("Exposing a single private key can compromise your entire wallet!"),
2174 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2175 vbox.addWidget(QLabel(msg))
2181 defaultname = 'electrum-private-keys.csv'
2182 select_msg = _('Select file to export your private keys to')
2183 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2184 vbox.addLayout(hbox)
2186 h, b = ok_cancel_buttons2(d, _('Export'))
2191 addresses = self.wallet.addresses(True)
2193 def privkeys_thread():
2194 for addr in addresses:
2198 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2199 d.emit(SIGNAL('computing_privkeys'))
2200 d.emit(SIGNAL('show_privkeys'))
2202 def show_privkeys():
2203 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2207 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2208 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2209 threading.Thread(target=privkeys_thread).start()
2215 filename = filename_e.text()
2220 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2221 except (IOError, os.error), reason:
2222 export_error_label = _("Electrum was unable to produce a private key-export.")
2223 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2225 except Exception as e:
2226 self.show_message(str(e))
2229 self.show_message(_("Private keys exported."))
2232 def do_export_privkeys(self, fileName, pklist, is_csv):
2233 with open(fileName, "w+") as f:
2235 transaction = csv.writer(f)
2236 transaction.writerow(["address", "private_key"])
2237 for addr, pk in pklist.items():
2238 transaction.writerow(["%34s"%addr,pk])
2241 f.write(json.dumps(pklist, indent = 4))
2244 def do_import_labels(self):
2245 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2246 if not labelsFile: return
2248 f = open(labelsFile, 'r')
2251 for key, value in json.loads(data).items():
2252 self.wallet.set_label(key, value)
2253 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2254 except (IOError, os.error), reason:
2255 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2258 def do_export_labels(self):
2259 labels = self.wallet.labels
2261 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2263 with open(fileName, 'w+') as f:
2264 json.dump(labels, f)
2265 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2266 except (IOError, os.error), reason:
2267 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2270 def export_history_dialog(self):
2273 d.setWindowTitle(_('Export History'))
2274 d.setMinimumSize(400, 200)
2275 vbox = QVBoxLayout(d)
2277 defaultname = os.path.expanduser('~/electrum-history.csv')
2278 select_msg = _('Select file to export your wallet transactions to')
2280 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2281 vbox.addLayout(hbox)
2285 h, b = ok_cancel_buttons2(d, _('Export'))
2290 filename = filename_e.text()
2295 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2296 except (IOError, os.error), reason:
2297 export_error_label = _("Electrum was unable to produce a transaction export.")
2298 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2301 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2304 def do_export_history(self, wallet, fileName, is_csv):
2305 history = wallet.get_tx_history()
2307 for item in history:
2308 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2310 if timestamp is not None:
2312 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2313 except [RuntimeError, TypeError, NameError] as reason:
2314 time_string = "unknown"
2317 time_string = "unknown"
2319 time_string = "pending"
2321 if value is not None:
2322 value_string = format_satoshis(value, True)
2327 fee_string = format_satoshis(fee, True)
2332 label, is_default_label = wallet.get_label(tx_hash)
2333 label = label.encode('utf-8')
2337 balance_string = format_satoshis(balance, False)
2339 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2341 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2343 with open(fileName, "w+") as f:
2345 transaction = csv.writer(f)
2346 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2348 transaction.writerow(line)
2351 f.write(json.dumps(lines, indent = 4))
2354 def sweep_key_dialog(self):
2356 d.setWindowTitle(_('Sweep private keys'))
2357 d.setMinimumSize(600, 300)
2359 vbox = QVBoxLayout(d)
2360 vbox.addWidget(QLabel(_("Enter private keys")))
2362 keys_e = QTextEdit()
2363 keys_e.setTabChangesFocus(True)
2364 vbox.addWidget(keys_e)
2366 h, address_e = address_field(self.wallet.addresses())
2370 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2371 vbox.addLayout(hbox)
2372 button.setEnabled(False)
2375 addr = str(address_e.text())
2376 if bitcoin.is_address(addr):
2380 pk = str(keys_e.toPlainText()).strip()
2381 if Wallet.is_private_key(pk):
2384 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2385 keys_e.textChanged.connect(f)
2386 address_e.textChanged.connect(f)
2390 fee = self.wallet.fee
2391 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2392 self.show_transaction(tx)
2396 def do_import_privkey(self, password):
2397 if not self.wallet.has_imported_keys():
2398 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2399 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2400 + _('Are you sure you understand what you are doing?'), 3, 4)
2403 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2406 text = str(text).split()
2411 addr = self.wallet.import_key(key, password)
2412 except Exception as e:
2418 addrlist.append(addr)
2420 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2422 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2423 self.update_address_tab()
2424 self.update_history_tab()
2427 def settings_dialog(self):
2429 d.setWindowTitle(_('Electrum Settings'))
2431 vbox = QVBoxLayout()
2432 grid = QGridLayout()
2433 grid.setColumnStretch(0,1)
2435 nz_label = QLabel(_('Display zeros') + ':')
2436 grid.addWidget(nz_label, 0, 0)
2437 nz_e = AmountEdit(None,True)
2438 nz_e.setText("%d"% self.num_zeros)
2439 grid.addWidget(nz_e, 0, 1)
2440 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2441 grid.addWidget(HelpButton(msg), 0, 2)
2442 if not self.config.is_modifiable('num_zeros'):
2443 for w in [nz_e, nz_label]: w.setEnabled(False)
2445 lang_label=QLabel(_('Language') + ':')
2446 grid.addWidget(lang_label, 1, 0)
2447 lang_combo = QComboBox()
2448 from electrum.i18n import languages
2449 lang_combo.addItems(languages.values())
2451 index = languages.keys().index(self.config.get("language",''))
2454 lang_combo.setCurrentIndex(index)
2455 grid.addWidget(lang_combo, 1, 1)
2456 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2457 if not self.config.is_modifiable('language'):
2458 for w in [lang_combo, lang_label]: w.setEnabled(False)
2461 fee_label = QLabel(_('Transaction fee') + ':')
2462 grid.addWidget(fee_label, 2, 0)
2463 fee_e = BTCAmountEdit(self.get_decimal_point)
2464 fee_e.setAmount(self.wallet.fee)
2465 grid.addWidget(fee_e, 2, 1)
2466 msg = _('Fee per kilobyte of transaction.') + '\n' \
2467 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2468 grid.addWidget(HelpButton(msg), 2, 2)
2469 if not self.config.is_modifiable('fee_per_kb'):
2470 for w in [fee_e, fee_label]: w.setEnabled(False)
2472 units = ['BTC', 'mBTC']
2473 unit_label = QLabel(_('Base unit') + ':')
2474 grid.addWidget(unit_label, 3, 0)
2475 unit_combo = QComboBox()
2476 unit_combo.addItems(units)
2477 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2478 grid.addWidget(unit_combo, 3, 1)
2479 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2480 + '\n1BTC=1000mBTC.\n' \
2481 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2483 usechange_cb = QCheckBox(_('Use change addresses'))
2484 usechange_cb.setChecked(self.wallet.use_change)
2485 grid.addWidget(usechange_cb, 4, 0)
2486 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2487 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2489 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2490 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2491 grid.addWidget(block_ex_label, 5, 0)
2492 block_ex_combo = QComboBox()
2493 block_ex_combo.addItems(block_explorers)
2494 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2495 grid.addWidget(block_ex_combo, 5, 1)
2496 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2498 show_tx = self.config.get('show_before_broadcast', False)
2499 showtx_cb = QCheckBox(_('Show before broadcast'))
2500 showtx_cb.setChecked(show_tx)
2501 grid.addWidget(showtx_cb, 6, 0)
2502 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2504 vbox.addLayout(grid)
2506 vbox.addLayout(ok_cancel_buttons(d))
2510 if not d.exec_(): return
2512 fee = fee_e.get_amount()
2514 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2517 self.wallet.set_fee(fee)
2519 nz = unicode(nz_e.text())
2524 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2527 if self.num_zeros != nz:
2529 self.config.set_key('num_zeros', nz, True)
2530 self.update_history_tab()
2531 self.update_address_tab()
2533 usechange_result = usechange_cb.isChecked()
2534 if self.wallet.use_change != usechange_result:
2535 self.wallet.use_change = usechange_result
2536 self.wallet.storage.put('use_change', self.wallet.use_change)
2538 if showtx_cb.isChecked() != show_tx:
2539 self.config.set_key('show_before_broadcast', not show_tx)
2541 unit_result = units[unit_combo.currentIndex()]
2542 if self.base_unit() != unit_result:
2543 self.decimal_point = 8 if unit_result == 'BTC' else 5
2544 self.config.set_key('decimal_point', self.decimal_point, True)
2545 self.update_history_tab()
2546 self.update_status()
2548 need_restart = False
2550 lang_request = languages.keys()[lang_combo.currentIndex()]
2551 if lang_request != self.config.get('language'):
2552 self.config.set_key("language", lang_request, True)
2555 be_result = block_explorers[block_ex_combo.currentIndex()]
2556 self.config.set_key('block_explorer', be_result, True)
2558 run_hook('close_settings_dialog')
2561 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2564 def run_network_dialog(self):
2565 if not self.network:
2567 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2569 def closeEvent(self, event):
2571 self.config.set_key("is_maximized", self.isMaximized())
2572 if not self.isMaximized():
2574 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2575 self.save_column_widths()
2576 self.config.set_key("console-history", self.console.history[-50:], True)
2577 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2581 def plugins_dialog(self):
2582 from electrum.plugins import plugins
2585 d.setWindowTitle(_('Electrum Plugins'))
2588 vbox = QVBoxLayout(d)
2591 scroll = QScrollArea()
2592 scroll.setEnabled(True)
2593 scroll.setWidgetResizable(True)
2594 scroll.setMinimumSize(400,250)
2595 vbox.addWidget(scroll)
2599 w.setMinimumHeight(len(plugins)*35)
2601 grid = QGridLayout()
2602 grid.setColumnStretch(0,1)
2605 def do_toggle(cb, p, w):
2608 if w: w.setEnabled(r)
2610 def mk_toggle(cb, p, w):
2611 return lambda: do_toggle(cb,p,w)
2613 for i, p in enumerate(plugins):
2615 cb = QCheckBox(p.fullname())
2616 cb.setDisabled(not p.is_available())
2617 cb.setChecked(p.is_enabled())
2618 grid.addWidget(cb, i, 0)
2619 if p.requires_settings():
2620 w = p.settings_widget(self)
2621 w.setEnabled( p.is_enabled() )
2622 grid.addWidget(w, i, 1)
2625 cb.clicked.connect(mk_toggle(cb,p,w))
2626 grid.addWidget(HelpButton(p.description()), i, 2)
2628 print_msg(_("Error: cannot display plugin"), p)
2629 traceback.print_exc(file=sys.stdout)
2630 grid.setRowStretch(i+1,1)
2632 vbox.addLayout(close_button(d))
2637 def show_account_details(self, k):
2638 account = self.wallet.accounts[k]
2641 d.setWindowTitle(_('Account Details'))
2644 vbox = QVBoxLayout(d)
2645 name = self.wallet.get_account_name(k)
2646 label = QLabel('Name: ' + name)
2647 vbox.addWidget(label)
2649 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2651 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2653 vbox.addWidget(QLabel(_('Master Public Key:')))
2656 text.setReadOnly(True)
2657 text.setMaximumHeight(170)
2658 vbox.addWidget(text)
2660 mpk_text = '\n'.join( account.get_master_pubkeys() )
2661 text.setText(mpk_text)
2663 vbox.addLayout(close_button(d))