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 run_hook('load_wallet', wallet)
237 def update_wallet_format(self):
238 # convert old-format imported keys
239 if self.wallet.imported_keys:
240 password = self.password_dialog(_("Please enter your password in order to update imported keys"))
242 self.wallet.convert_imported_keys(password)
244 self.show_message("error")
247 def open_wallet(self):
248 wallet_folder = self.wallet.storage.path
249 filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
253 storage = WalletStorage({'wallet_path': filename})
254 if not storage.file_exists:
255 self.show_message("file not found "+ filename)
258 self.wallet.stop_threads()
261 wallet = Wallet(storage)
262 wallet.start_threads(self.network)
264 self.load_wallet(wallet)
268 def backup_wallet(self):
270 path = self.wallet.storage.path
271 wallet_folder = os.path.dirname(path)
272 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
276 new_path = os.path.join(wallet_folder, filename)
279 shutil.copy2(path, new_path)
280 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
281 except (IOError, os.error), reason:
282 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
285 def new_wallet(self):
288 wallet_folder = os.path.dirname(self.wallet.storage.path)
289 filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
292 filename = os.path.join(wallet_folder, filename)
294 storage = WalletStorage({'wallet_path': filename})
295 if storage.file_exists:
296 QMessageBox.critical(None, "Error", _("File exists"))
299 wizard = installwizard.InstallWizard(self.config, self.network, storage)
300 wallet = wizard.run('new')
302 self.load_wallet(wallet)
306 def init_menubar(self):
309 file_menu = menubar.addMenu(_("&File"))
310 file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
311 file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
312 file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
313 file_menu.addAction(_("&Quit"), self.close)
315 wallet_menu = menubar.addMenu(_("&Wallet"))
316 wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
317 self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
319 wallet_menu.addSeparator()
321 self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
322 self.seed_menu = wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
323 self.mpk_menu = wallet_menu.addAction(_("&Master Public Keys"), self.show_master_public_keys)
325 wallet_menu.addSeparator()
326 labels_menu = wallet_menu.addMenu(_("&Labels"))
327 labels_menu.addAction(_("&Import"), self.do_import_labels)
328 labels_menu.addAction(_("&Export"), self.do_export_labels)
330 self.private_keys_menu = wallet_menu.addMenu(_("&Private keys"))
331 self.private_keys_menu.addAction(_("&Sweep"), self.sweep_key_dialog)
332 self.import_menu = self.private_keys_menu.addAction(_("&Import"), self.do_import_privkey)
333 self.private_keys_menu.addAction(_("&Export"), self.export_privkeys_dialog)
334 wallet_menu.addAction(_("&Export History"), self.export_history_dialog)
336 tools_menu = menubar.addMenu(_("&Tools"))
338 # Settings / Preferences are all reserved keywords in OSX using this as work around
339 tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
340 tools_menu.addAction(_("&Network"), self.run_network_dialog)
341 tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
342 tools_menu.addSeparator()
343 tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
344 tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
345 tools_menu.addSeparator()
347 csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
348 csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
349 csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
351 raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
352 raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
353 raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
354 raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
355 self.raw_transaction_menu = raw_transaction_menu
357 help_menu = menubar.addMenu(_("&Help"))
358 help_menu.addAction(_("&About"), self.show_about)
359 help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
360 help_menu.addSeparator()
361 help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
362 help_menu.addAction(_("&Report Bug"), self.show_report_bug)
364 self.setMenuBar(menubar)
366 def show_about(self):
367 QMessageBox.about(self, "Electrum",
368 _("Version")+" %s" % (self.wallet.electrum_version) + "\n\n" + _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin. You do not need to perform regular backups, because your wallet can be recovered from a secret phrase that you can memorize or write on paper. Startup times are instant because it operates in conjunction with high-performance servers that handle the most complicated parts of the Bitcoin system."))
370 def show_report_bug(self):
371 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
372 _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
375 def notify_transactions(self):
376 if not self.network or not self.network.is_connected():
379 print_error("Notifying GUI")
380 if len(self.network.pending_transactions_for_notifications) > 0:
381 # Combine the transactions if there are more then three
382 tx_amount = len(self.network.pending_transactions_for_notifications)
385 for tx in self.network.pending_transactions_for_notifications:
386 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
390 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
391 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
393 self.network.pending_transactions_for_notifications = []
395 for tx in self.network.pending_transactions_for_notifications:
397 self.network.pending_transactions_for_notifications.remove(tx)
398 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
400 self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
402 def notify(self, message):
403 self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
407 # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
408 def getOpenFileName(self, title, filter = ""):
409 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
410 fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
411 if fileName and directory != os.path.dirname(fileName):
412 self.config.set_key('io_dir', os.path.dirname(fileName), True)
415 def getSaveFileName(self, title, filename, filter = ""):
416 directory = self.config.get('io_dir', unicode(os.path.expanduser('~')))
417 path = os.path.join( directory, filename )
418 fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
419 if fileName and directory != os.path.dirname(fileName):
420 self.config.set_key('io_dir', os.path.dirname(fileName), True)
424 QMainWindow.close(self)
425 run_hook('close_main_window')
427 def connect_slots(self, sender):
428 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
429 self.previous_payto_e=''
431 def timer_actions(self):
432 if self.need_update.is_set():
434 self.need_update.clear()
436 self.receive_qr.update_qr()
437 run_hook('timer_actions')
439 def format_amount(self, x, is_diff=False, whitespaces=False):
440 return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
443 def get_decimal_point(self):
444 return self.decimal_point
448 assert self.decimal_point in [5,8]
449 return "BTC" if self.decimal_point == 8 else "mBTC"
452 def update_status(self):
453 if self.network is None or not self.network.is_running():
455 icon = QIcon(":icons/status_disconnected.png")
457 elif self.network.is_connected():
458 if not self.wallet.up_to_date:
459 text = _("Synchronizing...")
460 icon = QIcon(":icons/status_waiting.png")
461 elif self.network.server_lag > 1:
462 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
463 icon = QIcon(":icons/status_lagging.png")
465 c, u = self.wallet.get_account_balance(self.current_account)
466 text = _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
467 if u: text += " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
469 # append fiat balance and price from exchange rate plugin
471 run_hook('get_fiat_status_text', c+u, r)
476 self.tray.setToolTip(text)
477 icon = QIcon(":icons/status_connected.png")
479 text = _("Not connected")
480 icon = QIcon(":icons/status_disconnected.png")
482 self.balance_label.setText(text)
483 self.status_button.setIcon( icon )
486 def update_wallet(self):
488 if self.wallet.up_to_date or not self.network or not self.network.is_connected():
489 self.update_history_tab()
490 self.update_receive_tab()
491 self.update_address_tab()
492 self.update_contacts_tab()
493 self.update_completions()
494 self.update_invoices_tab()
497 def create_history_tab(self):
498 self.history_list = l = MyTreeWidget(self)
500 for i,width in enumerate(self.column_widths['history']):
501 l.setColumnWidth(i, width)
502 l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
503 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
504 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
506 l.customContextMenuRequested.connect(self.create_history_menu)
510 def create_history_menu(self, position):
511 self.history_list.selectedIndexes()
512 item = self.history_list.currentItem()
513 be = self.config.get('block_explorer', 'Blockchain.info')
514 if be == 'Blockchain.info':
515 block_explorer = 'https://blockchain.info/tx/'
516 elif be == 'Blockr.io':
517 block_explorer = 'https://blockr.io/tx/info/'
518 elif be == 'Insight.is':
519 block_explorer = 'http://live.insight.is/tx/'
521 tx_hash = str(item.data(0, Qt.UserRole).toString())
522 if not tx_hash: return
524 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
525 menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
526 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
527 menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
528 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
531 def show_transaction(self, tx):
532 import transaction_dialog
533 d = transaction_dialog.TxDialog(tx, self)
536 def tx_label_clicked(self, item, column):
537 if column==2 and item.isSelected():
539 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
540 self.history_list.editItem( item, column )
541 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
544 def tx_label_changed(self, item, column):
548 tx_hash = str(item.data(0, Qt.UserRole).toString())
549 tx = self.wallet.transactions.get(tx_hash)
550 text = unicode( item.text(2) )
551 self.wallet.set_label(tx_hash, text)
553 item.setForeground(2, QBrush(QColor('black')))
555 text = self.wallet.get_default_label(tx_hash)
556 item.setText(2, text)
557 item.setForeground(2, QBrush(QColor('gray')))
561 def edit_label(self, is_recv):
562 l = self.receive_list if is_recv else self.contacts_list
563 item = l.currentItem()
564 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
565 l.editItem( item, 1 )
566 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
570 def address_label_clicked(self, item, column, l, column_addr, column_label):
571 if column == column_label and item.isSelected():
572 is_editable = item.data(0, 32).toBool()
575 addr = unicode( item.text(column_addr) )
576 label = unicode( item.text(column_label) )
577 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
578 l.editItem( item, column )
579 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
582 def address_label_changed(self, item, column, l, column_addr, column_label):
583 if column == column_label:
584 addr = unicode( item.text(column_addr) )
585 text = unicode( item.text(column_label) )
586 is_editable = item.data(0, 32).toBool()
590 changed = self.wallet.set_label(addr, text)
592 self.update_history_tab()
593 self.update_completions()
595 self.current_item_changed(item)
597 run_hook('item_changed', item, column)
600 def current_item_changed(self, a):
601 run_hook('current_item_changed', a)
605 def update_history_tab(self):
607 self.history_list.clear()
608 for item in self.wallet.get_tx_history(self.current_account):
609 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
610 time_str = _("unknown")
613 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
615 time_str = _("error")
618 time_str = 'unverified'
619 icon = QIcon(":icons/unconfirmed.png")
622 icon = QIcon(":icons/unconfirmed.png")
624 icon = QIcon(":icons/clock%d.png"%conf)
626 icon = QIcon(":icons/confirmed.png")
628 if value is not None:
629 v_str = self.format_amount(value, True, whitespaces=True)
633 balance_str = self.format_amount(balance, whitespaces=True)
636 label, is_default_label = self.wallet.get_label(tx_hash)
638 label = _('Pruned transaction outputs')
639 is_default_label = False
641 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
642 item.setFont(2, QFont(MONOSPACE_FONT))
643 item.setFont(3, QFont(MONOSPACE_FONT))
644 item.setFont(4, QFont(MONOSPACE_FONT))
646 item.setForeground(3, QBrush(QColor("#BC1E1E")))
648 item.setData(0, Qt.UserRole, tx_hash)
649 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
651 item.setForeground(2, QBrush(QColor('grey')))
653 item.setIcon(0, icon)
654 self.history_list.insertTopLevelItem(0,item)
657 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
658 run_hook('history_tab_update')
661 def create_receive_tab(self):
663 grid = QGridLayout(w)
664 grid.setColumnMinimumWidth(3, 300)
665 grid.setColumnStretch(5, 1)
667 self.receive_address_e = QLineEdit()
668 self.receive_address_e.setReadOnly(True)
669 grid.addWidget(QLabel(_('Receiving address')), 0, 0)
670 grid.addWidget(self.receive_address_e, 0, 1, 1, 3)
671 self.receive_address_e.textChanged.connect(self.update_receive_qr)
673 self.receive_message_e = QLineEdit()
674 grid.addWidget(QLabel(_('Message')), 1, 0)
675 grid.addWidget(self.receive_message_e, 1, 1, 1, 3)
676 self.receive_message_e.textChanged.connect(self.update_receive_qr)
678 self.receive_amount_e = BTCAmountEdit(self.get_decimal_point)
679 grid.addWidget(QLabel(_('Requested amount')), 2, 0)
680 grid.addWidget(self.receive_amount_e, 2, 1, 1, 2)
681 self.receive_amount_e.textChanged.connect(self.update_receive_qr)
683 save_button = QPushButton(_('Save'))
684 save_button.clicked.connect(self.save_payment_request)
685 grid.addWidget(save_button, 3, 1)
686 clear_button = QPushButton(_('Clear'))
687 clear_button.clicked.connect(self.clear_receive_tab)
688 grid.addWidget(clear_button, 3, 2)
689 grid.setRowStretch(4, 1)
691 self.receive_qr = QRCodeWidget()
692 grid.addWidget(self.receive_qr, 0, 4, 5, 2)
694 self.receive_requests_label = QLabel(_('Pending requests'))
695 self.receive_list = MyTreeWidget(self)
696 self.receive_list.customContextMenuRequested.connect(self.receive_list_menu)
697 self.receive_list.setHeaderLabels( [_('Address'), _('Message'), _('Amount'), _('Status')] )
698 grid.addWidget(self.receive_requests_label, 5, 0)
699 grid.addWidget(self.receive_list, 6, 0, 1, 6)
701 grid.setRowStretch(7, 1)
704 def receive_list_delete(self, item):
705 addr = str(item.text(0))
706 self.receive_requests.pop(addr)
707 self.update_receive_tab()
708 self.redraw_from_list()
710 def receive_list_menu(self, position):
711 item = self.receive_list.itemAt(position)
713 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
714 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
716 def save_payment_request(self):
717 addr = str(self.receive_address_e.text())
718 amount = self.receive_amount_e.get_amount()
719 message = str(self.receive_message_e.text())
720 if not message and not amount:
721 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
723 self.receive_requests = self.wallet.storage.get('receive_requests',{})
724 self.receive_requests[addr] = (amount, message)
725 self.wallet.storage.put('receive_requests', self.receive_requests)
726 self.update_receive_tab()
728 def clear_receive_tab(self):
729 self.receive_amount_e.setAmount(None)
730 self.receive_message_e.setText("")
732 def receive_at(self, addr):
733 if not bitcoin.is_address(addr):
735 self.tabs.setCurrentIndex(2)
736 self.receive_address_e.setText(addr)
738 def update_receive_tab(self):
739 self.receive_requests = self.wallet.storage.get('receive_requests',{})
741 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
743 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
748 self.receive_address_e.setText(addr)
749 self.receive_message_e.setText("")
750 self.receive_amount_e.setAmount(None)
752 b = len(self.receive_requests) > 0
753 self.receive_list.setVisible(b)
754 self.receive_requests_label.setVisible(b)
756 self.receive_list.clear()
757 for address, v in self.receive_requests.items():
759 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else "", ""] )
760 self.receive_list.addTopLevelItem(item)
763 def update_receive_qr(self):
764 import urlparse, urllib
765 addr = str(self.receive_address_e.text())
768 amount = self.receive_amount_e.get_amount()
770 query.append('amount=%s'%format_satoshis(amount))
771 message = unicode(self.receive_message_e.text()).encode('utf8')
773 query.append('message=%s'%urllib.quote(message))
774 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
775 url = urlparse.urlunparse(p)
778 self.receive_qr.set_addr(url)
781 def create_send_tab(self):
784 self.send_grid = grid = QGridLayout(w)
786 grid.setColumnMinimumWidth(3,300)
787 grid.setColumnStretch(5,1)
788 grid.setRowStretch(8, 1)
790 from paytoedit import PayToEdit
791 self.amount_e = BTCAmountEdit(self.get_decimal_point)
792 self.payto_e = PayToEdit(self)
793 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)'))
794 grid.addWidget(QLabel(_('Pay to')), 1, 0)
795 grid.addWidget(self.payto_e, 1, 1, 1, 3)
796 grid.addWidget(self.payto_help, 1, 4)
798 completer = QCompleter()
799 completer.setCaseSensitivity(False)
800 self.payto_e.setCompleter(completer)
801 completer.setModel(self.completions)
803 self.message_e = MyLineEdit()
804 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.'))
805 grid.addWidget(QLabel(_('Description')), 2, 0)
806 grid.addWidget(self.message_e, 2, 1, 1, 3)
807 grid.addWidget(self.message_help, 2, 4)
809 self.from_label = QLabel(_('From'))
810 grid.addWidget(self.from_label, 3, 0)
811 self.from_list = MyTreeWidget(self)
812 self.from_list.setColumnCount(2)
813 self.from_list.setColumnWidth(0, 350)
814 self.from_list.setColumnWidth(1, 50)
815 self.from_list.setHeaderHidden(True)
816 self.from_list.setMaximumHeight(80)
817 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
818 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
819 grid.addWidget(self.from_list, 3, 1, 1, 3)
820 self.set_pay_from([])
822 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
823 + _('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.') \
824 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
825 grid.addWidget(QLabel(_('Amount')), 4, 0)
826 grid.addWidget(self.amount_e, 4, 1, 1, 2)
827 grid.addWidget(self.amount_help, 4, 3)
829 self.fee_e = BTCAmountEdit(self.get_decimal_point)
830 grid.addWidget(QLabel(_('Fee')), 5, 0)
831 grid.addWidget(self.fee_e, 5, 1, 1, 2)
832 grid.addWidget(HelpButton(
833 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
834 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
835 + _('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)
837 self.send_button = EnterButton(_("Send"), self.do_send)
838 grid.addWidget(self.send_button, 6, 1)
840 b = EnterButton(_("Clear"), self.do_clear)
841 grid.addWidget(b, 6, 2)
843 self.payto_sig = QLabel('')
844 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
846 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
847 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
850 def entry_changed( is_fee ):
851 self.funds_error = False
853 if self.amount_e.is_shortcut:
854 self.amount_e.is_shortcut = False
855 sendable = self.get_sendable_balance()
856 # there is only one output because we are completely spending inputs
857 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
858 fee = self.wallet.estimated_fee(inputs, 1)
860 self.amount_e.setAmount(amount)
861 self.fee_e.setAmount(fee)
864 amount = self.amount_e.get_amount()
865 fee = self.fee_e.get_amount()
867 if not is_fee: fee = None
870 # assume that there will be 2 outputs (one for change)
871 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
873 self.fee_e.setAmount(fee)
876 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
880 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
881 self.funds_error = True
882 text = _( "Not enough funds" )
883 c, u = self.wallet.get_frozen_balance()
884 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
886 self.statusBar().showMessage(text)
887 self.amount_e.setPalette(palette)
888 self.fee_e.setPalette(palette)
890 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
891 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
893 run_hook('create_send_tab', grid)
896 def from_list_delete(self, item):
897 i = self.from_list.indexOfTopLevelItem(item)
899 self.redraw_from_list()
901 def from_list_menu(self, position):
902 item = self.from_list.itemAt(position)
904 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
905 menu.exec_(self.from_list.viewport().mapToGlobal(position))
907 def set_pay_from(self, domain = None):
908 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
909 self.redraw_from_list()
911 def redraw_from_list(self):
912 self.from_list.clear()
913 self.from_label.setHidden(len(self.pay_from) == 0)
914 self.from_list.setHidden(len(self.pay_from) == 0)
917 h = x.get('prevout_hash')
918 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
920 for item in self.pay_from:
921 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
923 def update_completions(self):
925 for addr,label in self.wallet.labels.items():
926 if addr in self.wallet.addressbook:
927 l.append( label + ' <' + addr + '>')
929 run_hook('update_completions', l)
930 self.completions.setStringList(l)
934 return lambda s, *args: s.do_protect(func, args)
937 def read_send_tab(self):
939 if self.payment_request and self.payment_request.has_expired():
940 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
943 label = unicode( self.message_e.text() )
945 if self.payment_request:
946 outputs = self.payment_request.get_outputs()
948 outputs = self.payto_e.get_outputs()
951 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
954 for addr, x in outputs:
955 if addr is None or not bitcoin.is_address(addr):
956 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
959 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
962 amount = sum(map(lambda x:x[1], outputs))
964 fee = self.fee_e.get_amount()
966 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
969 confirm_amount = self.config.get('confirm_amount', 100000000)
970 if amount >= confirm_amount:
971 o = '\n'.join(map(lambda x:x[0], outputs))
972 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
975 confirm_fee = self.config.get('confirm_fee', 100000)
976 if fee >= confirm_fee:
977 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()}):
980 coins = self.get_coins()
981 return outputs, fee, label, coins
985 r = self.read_send_tab()
988 outputs, fee, label, coins = r
989 self.send_tx(outputs, fee, label, coins)
993 def send_tx(self, outputs, fee, label, coins, password):
994 self.send_button.setDisabled(True)
996 # first, create an unsigned tx
998 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1000 except Exception as e:
1001 traceback.print_exc(file=sys.stdout)
1002 self.show_message(str(e))
1003 self.send_button.setDisabled(False)
1006 # call hook to see if plugin needs gui interaction
1007 run_hook('send_tx', tx)
1013 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
1014 self.wallet.sign_transaction(tx, keypairs, password)
1015 return tx, fee, label
1017 def sign_done(tx, fee, label):
1019 self.show_message(tx.error)
1020 self.send_button.setDisabled(False)
1022 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1023 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1024 self.send_button.setDisabled(False)
1027 self.wallet.set_label(tx.hash(), label)
1029 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1030 self.show_transaction(tx)
1032 self.send_button.setDisabled(False)
1035 self.broadcast_transaction(tx)
1037 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1038 self.waiting_dialog.start()
1042 def broadcast_transaction(self, tx):
1044 def broadcast_thread():
1045 pr = self.payment_request
1047 return self.wallet.sendtx(tx)
1049 if pr.has_expired():
1050 self.payment_request = None
1051 return False, _("Payment request has expired")
1053 status, msg = self.wallet.sendtx(tx)
1057 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1058 self.wallet.storage.put('invoices', self.invoices)
1059 self.update_invoices_tab()
1060 self.payment_request = None
1061 refund_address = self.wallet.addresses()[0]
1062 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1068 def broadcast_done(status, msg):
1070 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1073 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1074 self.send_button.setDisabled(False)
1076 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1077 self.waiting_dialog.start()
1081 def prepare_for_payment_request(self):
1082 self.tabs.setCurrentIndex(1)
1083 self.payto_e.is_pr = True
1084 for e in [self.payto_e, self.amount_e, self.message_e]:
1086 for h in [self.payto_help, self.amount_help, self.message_help]:
1088 self.payto_e.setText(_("please wait..."))
1091 def payment_request_ok(self):
1092 pr = self.payment_request
1094 if pr_id not in self.invoices:
1095 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1096 self.wallet.storage.put('invoices', self.invoices)
1097 self.update_invoices_tab()
1099 print_error('invoice already in list')
1101 status = self.invoices[pr_id][4]
1102 if status == PR_PAID:
1104 self.show_message("invoice already paid")
1105 self.payment_request = None
1108 self.payto_help.show()
1109 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1111 if not pr.has_expired():
1112 self.payto_e.setGreen()
1114 self.payto_e.setExpired()
1116 self.payto_e.setText(pr.domain)
1117 self.amount_e.setText(self.format_amount(pr.get_amount()))
1118 self.message_e.setText(pr.get_memo())
1120 def payment_request_error(self):
1122 self.show_message(self.payment_request.error)
1123 self.payment_request = None
1125 def pay_from_URI(self,URI):
1128 address, amount, label, message, request_url = util.parse_URI(URI)
1130 address, amount, label, message, request_url = util.parse_URI(URI)
1131 except Exception as e:
1132 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1135 self.tabs.setCurrentIndex(1)
1139 if self.wallet.labels.get(address) != label:
1140 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1141 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1142 self.wallet.addressbook.append(address)
1143 self.wallet.set_label(address, label)
1145 label = self.wallet.labels.get(address)
1147 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1149 self.message_e.setText(message)
1151 self.amount_e.setAmount(amount)
1154 from electrum import paymentrequest
1155 def payment_request():
1156 self.payment_request = paymentrequest.PaymentRequest(self.config)
1157 self.payment_request.read(request_url)
1158 if self.payment_request.verify():
1159 self.emit(SIGNAL('payment_request_ok'))
1161 self.emit(SIGNAL('payment_request_error'))
1163 self.pr_thread = threading.Thread(target=payment_request).start()
1164 self.prepare_for_payment_request()
1169 self.payto_e.is_pr = False
1170 self.payto_sig.setVisible(False)
1171 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1175 for h in [self.payto_help, self.amount_help, self.message_help]:
1178 self.payto_help.set_alt(None)
1179 self.set_pay_from([])
1180 self.update_status()
1184 def set_addrs_frozen(self,addrs,freeze):
1186 if not addr: continue
1187 if addr in self.wallet.frozen_addresses and not freeze:
1188 self.wallet.unfreeze(addr)
1189 elif addr not in self.wallet.frozen_addresses and freeze:
1190 self.wallet.freeze(addr)
1191 self.update_address_tab()
1195 def create_list_tab(self, headers):
1196 "generic tab creation method"
1197 l = MyTreeWidget(self)
1198 l.setColumnCount( len(headers) )
1199 l.setHeaderLabels( headers )
1202 vbox = QVBoxLayout()
1209 vbox.addWidget(buttons)
1214 def create_addresses_tab(self):
1215 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1216 for i,width in enumerate(self.column_widths['receive']):
1217 l.setColumnWidth(i, width)
1218 l.setContextMenuPolicy(Qt.CustomContextMenu)
1219 l.customContextMenuRequested.connect(self.create_receive_menu)
1220 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1221 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1222 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1223 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1224 self.address_list = l
1230 def save_column_widths(self):
1231 self.column_widths["receive"] = []
1232 for i in range(self.address_list.columnCount() -1):
1233 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1235 self.column_widths["history"] = []
1236 for i in range(self.history_list.columnCount() - 1):
1237 self.column_widths["history"].append(self.history_list.columnWidth(i))
1239 self.column_widths["contacts"] = []
1240 for i in range(self.contacts_list.columnCount() - 1):
1241 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1243 self.config.set_key("column_widths_2", self.column_widths, True)
1246 def create_contacts_tab(self):
1247 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1248 l.setContextMenuPolicy(Qt.CustomContextMenu)
1249 l.customContextMenuRequested.connect(self.create_contact_menu)
1250 for i,width in enumerate(self.column_widths['contacts']):
1251 l.setColumnWidth(i, width)
1252 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1253 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1254 self.contacts_list = l
1258 def create_invoices_tab(self):
1259 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1261 h.setStretchLastSection(False)
1262 h.setResizeMode(1, QHeaderView.Stretch)
1263 l.setContextMenuPolicy(Qt.CustomContextMenu)
1264 l.customContextMenuRequested.connect(self.create_invoice_menu)
1265 self.invoices_list = l
1268 def update_invoices_tab(self):
1269 invoices = self.wallet.storage.get('invoices', {})
1270 l = self.invoices_list
1272 for key, value in invoices.items():
1274 domain, memo, amount, expiration_date, status, tx_hash = value
1278 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1280 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1281 l.addTopLevelItem(item)
1283 l.setCurrentItem(l.topLevelItem(0))
1287 def delete_imported_key(self, addr):
1288 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1289 self.wallet.delete_imported_key(addr)
1290 self.update_address_tab()
1291 self.update_history_tab()
1293 def edit_account_label(self, k):
1294 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1296 label = unicode(text)
1297 self.wallet.set_label(k,label)
1298 self.update_address_tab()
1300 def account_set_expanded(self, item, k, b):
1302 self.accounts_expanded[k] = b
1304 def create_account_menu(self, position, k, item):
1306 if item.isExpanded():
1307 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1309 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1310 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1311 if self.wallet.seed_version > 4:
1312 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1313 if self.wallet.account_is_pending(k):
1314 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1315 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1317 def delete_pending_account(self, k):
1318 self.wallet.delete_pending_account(k)
1319 self.update_address_tab()
1321 def create_receive_menu(self, position):
1322 # fixme: this function apparently has a side effect.
1323 # if it is not called the menu pops up several times
1324 #self.address_list.selectedIndexes()
1326 selected = self.address_list.selectedItems()
1327 multi_select = len(selected) > 1
1328 addrs = [unicode(item.text(0)) for item in selected]
1329 if not multi_select:
1330 item = self.address_list.itemAt(position)
1334 if not is_valid(addr):
1335 k = str(item.data(0,32).toString())
1337 self.create_account_menu(position, k, item)
1339 item.setExpanded(not item.isExpanded())
1343 if not multi_select:
1344 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1345 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1346 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1347 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1348 if not self.wallet.is_watching_only():
1349 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1350 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1351 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1352 if self.wallet.is_imported(addr):
1353 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1355 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1356 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1357 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1358 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1360 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1361 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1363 run_hook('receive_menu', menu, addrs)
1364 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1367 def get_sendable_balance(self):
1368 return sum(map(lambda x:x['value'], self.get_coins()))
1371 def get_coins(self):
1373 return self.pay_from
1375 domain = self.wallet.get_account_addresses(self.current_account)
1376 for i in self.wallet.frozen_addresses:
1377 if i in domain: domain.remove(i)
1378 return self.wallet.get_unspent_coins(domain)
1381 def send_from_addresses(self, addrs):
1382 self.set_pay_from( addrs )
1383 self.tabs.setCurrentIndex(1)
1386 def payto(self, addr):
1388 label = self.wallet.labels.get(addr)
1389 m_addr = label + ' <' + addr + '>' if label else addr
1390 self.tabs.setCurrentIndex(1)
1391 self.payto_e.setText(m_addr)
1392 self.amount_e.setFocus()
1395 def delete_contact(self, x):
1396 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1397 self.wallet.delete_contact(x)
1398 self.wallet.set_label(x, None)
1399 self.update_history_tab()
1400 self.update_contacts_tab()
1401 self.update_completions()
1404 def create_contact_menu(self, position):
1405 item = self.contacts_list.itemAt(position)
1408 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1410 addr = unicode(item.text(0))
1411 label = unicode(item.text(1))
1412 is_editable = item.data(0,32).toBool()
1413 payto_addr = item.data(0,33).toString()
1414 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1415 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1416 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1418 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1419 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1421 run_hook('create_contact_menu', menu, item)
1422 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1424 def delete_invoice(self, key):
1425 self.invoices.pop(key)
1426 self.wallet.storage.put('invoices', self.invoices)
1427 self.update_invoices_tab()
1429 def show_invoice(self, key):
1430 from electrum.paymentrequest import PaymentRequest
1431 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1432 pr = PaymentRequest(self.config)
1436 self.show_pr_details(pr)
1438 def show_pr_details(self, pr):
1439 msg = 'Domain: ' + pr.domain
1440 msg += '\nStatus: ' + pr.get_status()
1441 msg += '\nMemo: ' + pr.get_memo()
1442 msg += '\nPayment URL: ' + pr.payment_url
1443 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1444 QMessageBox.information(self, 'Invoice', msg , 'OK')
1446 def do_pay_invoice(self, key):
1447 from electrum.paymentrequest import PaymentRequest
1448 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1449 pr = PaymentRequest(self.config)
1452 self.payment_request = pr
1453 self.prepare_for_payment_request()
1455 self.payment_request_ok()
1457 self.payment_request_error()
1460 def create_invoice_menu(self, position):
1461 item = self.invoices_list.itemAt(position)
1464 k = self.invoices_list.indexOfTopLevelItem(item)
1465 key = self.invoices.keys()[k]
1466 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1468 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1469 if status == PR_UNPAID:
1470 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1471 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1472 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1475 def update_address_item(self, item):
1476 item.setFont(0, QFont(MONOSPACE_FONT))
1477 address = str(item.data(0,0).toString())
1478 label = self.wallet.labels.get(address,'')
1479 item.setData(1,0,label)
1480 item.setData(0,32, True) # is editable
1482 run_hook('update_address_item', address, item)
1484 if not self.wallet.is_mine(address): return
1486 c, u = self.wallet.get_addr_balance(address)
1487 balance = self.format_amount(c + u)
1488 item.setData(2,0,balance)
1490 if address in self.wallet.frozen_addresses:
1491 item.setBackgroundColor(0, QColor('lightblue'))
1494 def update_address_tab(self):
1495 l = self.address_list
1496 # extend the syntax for consistency
1497 l.addChild = l.addTopLevelItem
1498 l.insertChild = l.insertTopLevelItem
1502 accounts = self.wallet.get_accounts()
1503 if self.current_account is None:
1504 account_items = sorted(accounts.items())
1506 account_items = [(self.current_account, accounts.get(self.current_account))]
1509 for k, account in account_items:
1511 if len(accounts) > 1:
1512 name = self.wallet.get_account_name(k)
1513 c,u = self.wallet.get_account_balance(k)
1514 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1515 l.addTopLevelItem(account_item)
1516 account_item.setExpanded(self.accounts_expanded.get(k, True))
1517 account_item.setData(0, 32, k)
1521 sequences = [0,1] if account.has_change() else [0]
1522 for is_change in sequences:
1523 if len(sequences) > 1:
1524 name = _("Receiving") if not is_change else _("Change")
1525 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1526 account_item.addChild(seq_item)
1528 seq_item.setExpanded(True)
1530 seq_item = account_item
1532 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1538 for address in account.get_addresses(is_change):
1540 num, is_used = self.wallet.is_used(address)
1543 if gap > self.wallet.gap_limit:
1548 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1549 self.update_address_item(item)
1551 item.setBackgroundColor(1, QColor('red'))
1555 seq_item.insertChild(0,used_item)
1557 used_item.addChild(item)
1559 seq_item.addChild(item)
1561 # we use column 1 because column 0 may be hidden
1562 l.setCurrentItem(l.topLevelItem(0),1)
1565 def update_contacts_tab(self):
1566 l = self.contacts_list
1569 for address in self.wallet.addressbook:
1570 label = self.wallet.labels.get(address,'')
1571 n = self.wallet.get_num_tx(address)
1572 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1573 item.setFont(0, QFont(MONOSPACE_FONT))
1574 # 32 = label can be edited (bool)
1575 item.setData(0,32, True)
1577 item.setData(0,33, address)
1578 l.addTopLevelItem(item)
1580 run_hook('update_contacts_tab', l)
1581 l.setCurrentItem(l.topLevelItem(0))
1585 def create_console_tab(self):
1586 from console import Console
1587 self.console = console = Console()
1591 def update_console(self):
1592 console = self.console
1593 console.history = self.config.get("console-history",[])
1594 console.history_index = len(console.history)
1596 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1597 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1599 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1601 def mkfunc(f, method):
1602 return lambda *args: apply( f, (method, args, self.password_dialog ))
1604 if m[0]=='_' or m in ['network','wallet']: continue
1605 methods[m] = mkfunc(c._run, m)
1607 console.updateNamespace(methods)
1610 def change_account(self,s):
1611 if s == _("All accounts"):
1612 self.current_account = None
1614 accounts = self.wallet.get_account_names()
1615 for k, v in accounts.items():
1617 self.current_account = k
1618 self.update_history_tab()
1619 self.update_status()
1620 self.update_address_tab()
1621 self.update_receive_tab()
1623 def create_status_bar(self):
1626 sb.setFixedHeight(35)
1627 qtVersion = qVersion()
1629 self.balance_label = QLabel("")
1630 sb.addWidget(self.balance_label)
1632 from version_getter import UpdateLabel
1633 self.updatelabel = UpdateLabel(self.config, sb)
1635 self.account_selector = QComboBox()
1636 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1637 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1638 sb.addPermanentWidget(self.account_selector)
1640 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1641 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1643 self.lock_icon = QIcon()
1644 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1645 sb.addPermanentWidget( self.password_button )
1647 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1648 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1649 sb.addPermanentWidget( self.seed_button )
1650 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1651 sb.addPermanentWidget( self.status_button )
1653 run_hook('create_status_bar', (sb,))
1655 self.setStatusBar(sb)
1658 def update_lock_icon(self):
1659 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1660 self.password_button.setIcon( icon )
1663 def update_buttons_on_seed(self):
1664 if self.wallet.has_seed():
1665 self.seed_button.show()
1667 self.seed_button.hide()
1669 if not self.wallet.is_watching_only():
1670 self.password_button.show()
1671 self.send_button.setText(_("Send"))
1673 self.password_button.hide()
1674 self.send_button.setText(_("Create unsigned transaction"))
1677 def change_password_dialog(self):
1678 from password_dialog import PasswordDialog
1679 d = PasswordDialog(self.wallet, self)
1681 self.update_lock_icon()
1684 def new_contact_dialog(self):
1687 d.setWindowTitle(_("New Contact"))
1688 vbox = QVBoxLayout(d)
1689 vbox.addWidget(QLabel(_('New Contact')+':'))
1691 grid = QGridLayout()
1694 grid.addWidget(QLabel(_("Address")), 1, 0)
1695 grid.addWidget(line1, 1, 1)
1696 grid.addWidget(QLabel(_("Name")), 2, 0)
1697 grid.addWidget(line2, 2, 1)
1699 vbox.addLayout(grid)
1700 vbox.addLayout(ok_cancel_buttons(d))
1705 address = str(line1.text())
1706 label = unicode(line2.text())
1708 if not is_valid(address):
1709 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1712 self.wallet.add_contact(address)
1714 self.wallet.set_label(address, label)
1716 self.update_contacts_tab()
1717 self.update_history_tab()
1718 self.update_completions()
1719 self.tabs.setCurrentIndex(3)
1723 def new_account_dialog(self, password):
1725 dialog = QDialog(self)
1727 dialog.setWindowTitle(_("New Account"))
1729 vbox = QVBoxLayout()
1730 vbox.addWidget(QLabel(_('Account name')+':'))
1733 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1734 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1739 vbox.addLayout(ok_cancel_buttons(dialog))
1740 dialog.setLayout(vbox)
1744 name = str(e.text())
1747 self.wallet.create_pending_account(name, password)
1748 self.update_address_tab()
1749 self.tabs.setCurrentIndex(2)
1754 def show_master_public_keys(self):
1756 dialog = QDialog(self)
1758 dialog.setWindowTitle(_("Master Public Keys"))
1760 main_layout = QGridLayout()
1761 mpk_dict = self.wallet.get_master_public_keys()
1763 for key, value in mpk_dict.items():
1764 main_layout.addWidget(QLabel(key), i, 0)
1765 mpk_text = QTextEdit()
1766 mpk_text.setReadOnly(True)
1767 mpk_text.setMaximumHeight(170)
1768 mpk_text.setText(value)
1769 main_layout.addWidget(mpk_text, i + 1, 0)
1772 vbox = QVBoxLayout()
1773 vbox.addLayout(main_layout)
1774 vbox.addLayout(close_button(dialog))
1776 dialog.setLayout(vbox)
1781 def show_seed_dialog(self, password):
1782 if not self.wallet.has_seed():
1783 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1787 mnemonic = self.wallet.get_mnemonic(password)
1789 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1791 from seed_dialog import SeedDialog
1792 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1797 def show_qrcode(self, data, title = _("QR code")):
1800 d = QRDialog(data, self, title)
1804 def do_protect(self, func, args):
1805 if self.wallet.use_encryption:
1806 password = self.password_dialog()
1812 if args != (False,):
1813 args = (self,) + args + (password,)
1815 args = (self,password)
1819 def show_public_keys(self, address):
1820 if not address: return
1822 pubkey_list = self.wallet.get_public_keys(address)
1823 except Exception as e:
1824 traceback.print_exc(file=sys.stdout)
1825 self.show_message(str(e))
1829 d.setMinimumSize(600, 200)
1831 vbox = QVBoxLayout()
1832 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1833 vbox.addWidget( QLabel(_("Public key") + ':'))
1835 keys.setReadOnly(True)
1836 keys.setText('\n'.join(pubkey_list))
1837 vbox.addWidget(keys)
1838 vbox.addLayout(close_button(d))
1843 def show_private_key(self, address, password):
1844 if not address: return
1846 pk_list = self.wallet.get_private_key(address, password)
1847 except Exception as e:
1848 traceback.print_exc(file=sys.stdout)
1849 self.show_message(str(e))
1853 d.setMinimumSize(600, 200)
1855 vbox = QVBoxLayout()
1856 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1857 vbox.addWidget( QLabel(_("Private key") + ':'))
1859 keys.setReadOnly(True)
1860 keys.setText('\n'.join(pk_list))
1861 vbox.addWidget(keys)
1862 vbox.addLayout(close_button(d))
1868 def do_sign(self, address, message, signature, password):
1869 message = unicode(message.toPlainText())
1870 message = message.encode('utf-8')
1872 sig = self.wallet.sign_message(str(address.text()), message, password)
1873 signature.setText(sig)
1874 except Exception as e:
1875 self.show_message(str(e))
1877 def do_verify(self, address, message, signature):
1878 message = unicode(message.toPlainText())
1879 message = message.encode('utf-8')
1880 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1881 self.show_message(_("Signature verified"))
1883 self.show_message(_("Error: wrong signature"))
1886 def sign_verify_message(self, address=''):
1889 d.setWindowTitle(_('Sign/verify Message'))
1890 d.setMinimumSize(410, 290)
1892 layout = QGridLayout(d)
1894 message_e = QTextEdit()
1895 layout.addWidget(QLabel(_('Message')), 1, 0)
1896 layout.addWidget(message_e, 1, 1)
1897 layout.setRowStretch(2,3)
1899 address_e = QLineEdit()
1900 address_e.setText(address)
1901 layout.addWidget(QLabel(_('Address')), 2, 0)
1902 layout.addWidget(address_e, 2, 1)
1904 signature_e = QTextEdit()
1905 layout.addWidget(QLabel(_('Signature')), 3, 0)
1906 layout.addWidget(signature_e, 3, 1)
1907 layout.setRowStretch(3,1)
1909 hbox = QHBoxLayout()
1911 b = QPushButton(_("Sign"))
1912 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1915 b = QPushButton(_("Verify"))
1916 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1919 b = QPushButton(_("Close"))
1920 b.clicked.connect(d.accept)
1922 layout.addLayout(hbox, 4, 1)
1927 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1929 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1930 message_e.setText(decrypted)
1931 except Exception as e:
1932 self.show_message(str(e))
1935 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1936 message = unicode(message_e.toPlainText())
1937 message = message.encode('utf-8')
1939 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1940 encrypted_e.setText(encrypted)
1941 except Exception as e:
1942 self.show_message(str(e))
1946 def encrypt_message(self, address = ''):
1949 d.setWindowTitle(_('Encrypt/decrypt Message'))
1950 d.setMinimumSize(610, 490)
1952 layout = QGridLayout(d)
1954 message_e = QTextEdit()
1955 layout.addWidget(QLabel(_('Message')), 1, 0)
1956 layout.addWidget(message_e, 1, 1)
1957 layout.setRowStretch(2,3)
1959 pubkey_e = QLineEdit()
1961 pubkey = self.wallet.getpubkeys(address)[0]
1962 pubkey_e.setText(pubkey)
1963 layout.addWidget(QLabel(_('Public key')), 2, 0)
1964 layout.addWidget(pubkey_e, 2, 1)
1966 encrypted_e = QTextEdit()
1967 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1968 layout.addWidget(encrypted_e, 3, 1)
1969 layout.setRowStretch(3,1)
1971 hbox = QHBoxLayout()
1972 b = QPushButton(_("Encrypt"))
1973 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1976 b = QPushButton(_("Decrypt"))
1977 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1980 b = QPushButton(_("Close"))
1981 b.clicked.connect(d.accept)
1984 layout.addLayout(hbox, 4, 1)
1988 def question(self, msg):
1989 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1991 def show_message(self, msg):
1992 QMessageBox.information(self, _('Message'), msg, _('OK'))
1994 def password_dialog(self, msg=None):
1997 d.setWindowTitle(_("Enter Password"))
2002 vbox = QVBoxLayout()
2004 msg = _('Please enter your password')
2005 vbox.addWidget(QLabel(msg))
2007 grid = QGridLayout()
2009 grid.addWidget(QLabel(_('Password')), 1, 0)
2010 grid.addWidget(pw, 1, 1)
2011 vbox.addLayout(grid)
2013 vbox.addLayout(ok_cancel_buttons(d))
2016 run_hook('password_dialog', pw, grid, 1)
2017 if not d.exec_(): return
2018 return unicode(pw.text())
2027 def tx_from_text(self, txt):
2028 "json or raw hexadecimal"
2031 tx = Transaction(txt)
2037 tx_dict = json.loads(str(txt))
2038 assert "hex" in tx_dict.keys()
2039 tx = Transaction(tx_dict["hex"])
2040 if tx_dict.has_key("input_info"):
2041 input_info = json.loads(tx_dict['input_info'])
2042 tx.add_input_info(input_info)
2045 traceback.print_exc(file=sys.stdout)
2048 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2052 def read_tx_from_file(self):
2053 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2057 with open(fileName, "r") as f:
2058 file_content = f.read()
2059 except (ValueError, IOError, os.error), reason:
2060 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2062 return self.tx_from_text(file_content)
2066 def sign_raw_transaction(self, tx, input_info, password):
2067 self.wallet.signrawtransaction(tx, input_info, [], password)
2069 def do_process_from_text(self):
2070 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2073 tx = self.tx_from_text(text)
2075 self.show_transaction(tx)
2077 def do_process_from_file(self):
2078 tx = self.read_tx_from_file()
2080 self.show_transaction(tx)
2082 def do_process_from_txid(self):
2083 from electrum import transaction
2084 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2086 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2088 tx = transaction.Transaction(r)
2090 self.show_transaction(tx)
2092 self.show_message("unknown transaction")
2094 def do_process_from_csvReader(self, csvReader):
2099 for position, row in enumerate(csvReader):
2101 if not is_valid(address):
2102 errors.append((position, address))
2104 amount = Decimal(row[1])
2105 amount = int(100000000*amount)
2106 outputs.append((address, amount))
2107 except (ValueError, IOError, os.error), reason:
2108 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2112 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2113 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2117 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2118 except Exception as e:
2119 self.show_message(str(e))
2122 self.show_transaction(tx)
2124 def do_process_from_csv_file(self):
2125 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2129 with open(fileName, "r") as f:
2130 csvReader = csv.reader(f)
2131 self.do_process_from_csvReader(csvReader)
2132 except (ValueError, IOError, os.error), reason:
2133 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2136 def do_process_from_csv_text(self):
2137 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2138 + _("Format: address, amount. One output per line"), _("Load CSV"))
2141 f = StringIO.StringIO(text)
2142 csvReader = csv.reader(f)
2143 self.do_process_from_csvReader(csvReader)
2148 def export_privkeys_dialog(self, password):
2149 if self.wallet.is_watching_only():
2150 self.show_message(_("This is a watching-only wallet"))
2154 d.setWindowTitle(_('Private keys'))
2155 d.setMinimumSize(850, 300)
2156 vbox = QVBoxLayout(d)
2158 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2159 _("Exposing a single private key can compromise your entire wallet!"),
2160 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2161 vbox.addWidget(QLabel(msg))
2167 defaultname = 'electrum-private-keys.csv'
2168 select_msg = _('Select file to export your private keys to')
2169 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2170 vbox.addLayout(hbox)
2172 h, b = ok_cancel_buttons2(d, _('Export'))
2177 addresses = self.wallet.addresses(True)
2179 def privkeys_thread():
2180 for addr in addresses:
2184 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2185 d.emit(SIGNAL('computing_privkeys'))
2186 d.emit(SIGNAL('show_privkeys'))
2188 def show_privkeys():
2189 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2193 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2194 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2195 threading.Thread(target=privkeys_thread).start()
2201 filename = filename_e.text()
2206 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2207 except (IOError, os.error), reason:
2208 export_error_label = _("Electrum was unable to produce a private key-export.")
2209 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2211 except Exception as e:
2212 self.show_message(str(e))
2215 self.show_message(_("Private keys exported."))
2218 def do_export_privkeys(self, fileName, pklist, is_csv):
2219 with open(fileName, "w+") as f:
2221 transaction = csv.writer(f)
2222 transaction.writerow(["address", "private_key"])
2223 for addr, pk in pklist.items():
2224 transaction.writerow(["%34s"%addr,pk])
2227 f.write(json.dumps(pklist, indent = 4))
2230 def do_import_labels(self):
2231 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2232 if not labelsFile: return
2234 f = open(labelsFile, 'r')
2237 for key, value in json.loads(data).items():
2238 self.wallet.set_label(key, value)
2239 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2240 except (IOError, os.error), reason:
2241 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2244 def do_export_labels(self):
2245 labels = self.wallet.labels
2247 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2249 with open(fileName, 'w+') as f:
2250 json.dump(labels, f)
2251 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2252 except (IOError, os.error), reason:
2253 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2256 def export_history_dialog(self):
2259 d.setWindowTitle(_('Export History'))
2260 d.setMinimumSize(400, 200)
2261 vbox = QVBoxLayout(d)
2263 defaultname = os.path.expanduser('~/electrum-history.csv')
2264 select_msg = _('Select file to export your wallet transactions to')
2266 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2267 vbox.addLayout(hbox)
2271 h, b = ok_cancel_buttons2(d, _('Export'))
2276 filename = filename_e.text()
2281 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2282 except (IOError, os.error), reason:
2283 export_error_label = _("Electrum was unable to produce a transaction export.")
2284 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2287 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2290 def do_export_history(self, wallet, fileName, is_csv):
2291 history = wallet.get_tx_history()
2293 for item in history:
2294 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2296 if timestamp is not None:
2298 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2299 except [RuntimeError, TypeError, NameError] as reason:
2300 time_string = "unknown"
2303 time_string = "unknown"
2305 time_string = "pending"
2307 if value is not None:
2308 value_string = format_satoshis(value, True)
2313 fee_string = format_satoshis(fee, True)
2318 label, is_default_label = wallet.get_label(tx_hash)
2319 label = label.encode('utf-8')
2323 balance_string = format_satoshis(balance, False)
2325 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2327 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2329 with open(fileName, "w+") as f:
2331 transaction = csv.writer(f)
2332 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2334 transaction.writerow(line)
2337 f.write(json.dumps(lines, indent = 4))
2340 def sweep_key_dialog(self):
2342 d.setWindowTitle(_('Sweep private keys'))
2343 d.setMinimumSize(600, 300)
2345 vbox = QVBoxLayout(d)
2346 vbox.addWidget(QLabel(_("Enter private keys")))
2348 keys_e = QTextEdit()
2349 keys_e.setTabChangesFocus(True)
2350 vbox.addWidget(keys_e)
2352 h, address_e = address_field(self.wallet.addresses())
2356 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2357 vbox.addLayout(hbox)
2358 button.setEnabled(False)
2361 addr = str(address_e.text())
2362 if bitcoin.is_address(addr):
2366 pk = str(keys_e.toPlainText()).strip()
2367 if Wallet.is_private_key(pk):
2370 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2371 keys_e.textChanged.connect(f)
2372 address_e.textChanged.connect(f)
2376 fee = self.wallet.fee
2377 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2378 self.show_transaction(tx)
2382 def do_import_privkey(self, password):
2383 if not self.wallet.has_imported_keys():
2384 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2385 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2386 + _('Are you sure you understand what you are doing?'), 3, 4)
2389 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2392 text = str(text).split()
2397 addr = self.wallet.import_key(key, password)
2398 except Exception as e:
2404 addrlist.append(addr)
2406 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2408 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2409 self.update_address_tab()
2410 self.update_history_tab()
2413 def settings_dialog(self):
2415 d.setWindowTitle(_('Electrum Settings'))
2417 vbox = QVBoxLayout()
2418 grid = QGridLayout()
2419 grid.setColumnStretch(0,1)
2421 nz_label = QLabel(_('Display zeros') + ':')
2422 grid.addWidget(nz_label, 0, 0)
2423 nz_e = AmountEdit(None,True)
2424 nz_e.setText("%d"% self.num_zeros)
2425 grid.addWidget(nz_e, 0, 1)
2426 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2427 grid.addWidget(HelpButton(msg), 0, 2)
2428 if not self.config.is_modifiable('num_zeros'):
2429 for w in [nz_e, nz_label]: w.setEnabled(False)
2431 lang_label=QLabel(_('Language') + ':')
2432 grid.addWidget(lang_label, 1, 0)
2433 lang_combo = QComboBox()
2434 from electrum.i18n import languages
2435 lang_combo.addItems(languages.values())
2437 index = languages.keys().index(self.config.get("language",''))
2440 lang_combo.setCurrentIndex(index)
2441 grid.addWidget(lang_combo, 1, 1)
2442 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2443 if not self.config.is_modifiable('language'):
2444 for w in [lang_combo, lang_label]: w.setEnabled(False)
2447 fee_label = QLabel(_('Transaction fee') + ':')
2448 grid.addWidget(fee_label, 2, 0)
2449 fee_e = BTCAmountEdit(self.get_decimal_point)
2450 fee_e.setAmount(self.wallet.fee)
2451 grid.addWidget(fee_e, 2, 1)
2452 msg = _('Fee per kilobyte of transaction.') + '\n' \
2453 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2454 grid.addWidget(HelpButton(msg), 2, 2)
2455 if not self.config.is_modifiable('fee_per_kb'):
2456 for w in [fee_e, fee_label]: w.setEnabled(False)
2458 units = ['BTC', 'mBTC']
2459 unit_label = QLabel(_('Base unit') + ':')
2460 grid.addWidget(unit_label, 3, 0)
2461 unit_combo = QComboBox()
2462 unit_combo.addItems(units)
2463 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2464 grid.addWidget(unit_combo, 3, 1)
2465 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2466 + '\n1BTC=1000mBTC.\n' \
2467 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2469 usechange_cb = QCheckBox(_('Use change addresses'))
2470 usechange_cb.setChecked(self.wallet.use_change)
2471 grid.addWidget(usechange_cb, 4, 0)
2472 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2473 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2475 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2476 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2477 grid.addWidget(block_ex_label, 5, 0)
2478 block_ex_combo = QComboBox()
2479 block_ex_combo.addItems(block_explorers)
2480 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2481 grid.addWidget(block_ex_combo, 5, 1)
2482 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2484 show_tx = self.config.get('show_before_broadcast', False)
2485 showtx_cb = QCheckBox(_('Show before broadcast'))
2486 showtx_cb.setChecked(show_tx)
2487 grid.addWidget(showtx_cb, 6, 0)
2488 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2490 vbox.addLayout(grid)
2492 vbox.addLayout(ok_cancel_buttons(d))
2496 if not d.exec_(): return
2498 fee = fee_e.get_amount()
2500 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2503 self.wallet.set_fee(fee)
2505 nz = unicode(nz_e.text())
2510 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2513 if self.num_zeros != nz:
2515 self.config.set_key('num_zeros', nz, True)
2516 self.update_history_tab()
2517 self.update_address_tab()
2519 usechange_result = usechange_cb.isChecked()
2520 if self.wallet.use_change != usechange_result:
2521 self.wallet.use_change = usechange_result
2522 self.wallet.storage.put('use_change', self.wallet.use_change)
2524 if showtx_cb.isChecked() != show_tx:
2525 self.config.set_key('show_before_broadcast', not show_tx)
2527 unit_result = units[unit_combo.currentIndex()]
2528 if self.base_unit() != unit_result:
2529 self.decimal_point = 8 if unit_result == 'BTC' else 5
2530 self.config.set_key('decimal_point', self.decimal_point, True)
2531 self.update_history_tab()
2532 self.update_status()
2534 need_restart = False
2536 lang_request = languages.keys()[lang_combo.currentIndex()]
2537 if lang_request != self.config.get('language'):
2538 self.config.set_key("language", lang_request, True)
2541 be_result = block_explorers[block_ex_combo.currentIndex()]
2542 self.config.set_key('block_explorer', be_result, True)
2544 run_hook('close_settings_dialog')
2547 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2550 def run_network_dialog(self):
2551 if not self.network:
2553 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2555 def closeEvent(self, event):
2557 self.config.set_key("is_maximized", self.isMaximized())
2558 if not self.isMaximized():
2560 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2561 self.save_column_widths()
2562 self.config.set_key("console-history", self.console.history[-50:], True)
2563 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2567 def plugins_dialog(self):
2568 from electrum.plugins import plugins
2571 d.setWindowTitle(_('Electrum Plugins'))
2574 vbox = QVBoxLayout(d)
2577 scroll = QScrollArea()
2578 scroll.setEnabled(True)
2579 scroll.setWidgetResizable(True)
2580 scroll.setMinimumSize(400,250)
2581 vbox.addWidget(scroll)
2585 w.setMinimumHeight(len(plugins)*35)
2587 grid = QGridLayout()
2588 grid.setColumnStretch(0,1)
2591 def do_toggle(cb, p, w):
2594 if w: w.setEnabled(r)
2596 def mk_toggle(cb, p, w):
2597 return lambda: do_toggle(cb,p,w)
2599 for i, p in enumerate(plugins):
2601 cb = QCheckBox(p.fullname())
2602 cb.setDisabled(not p.is_available())
2603 cb.setChecked(p.is_enabled())
2604 grid.addWidget(cb, i, 0)
2605 if p.requires_settings():
2606 w = p.settings_widget(self)
2607 w.setEnabled( p.is_enabled() )
2608 grid.addWidget(w, i, 1)
2611 cb.clicked.connect(mk_toggle(cb,p,w))
2612 grid.addWidget(HelpButton(p.description()), i, 2)
2614 print_msg(_("Error: cannot display plugin"), p)
2615 traceback.print_exc(file=sys.stdout)
2616 grid.setRowStretch(i+1,1)
2618 vbox.addLayout(close_button(d))
2623 def show_account_details(self, k):
2624 account = self.wallet.accounts[k]
2627 d.setWindowTitle(_('Account Details'))
2630 vbox = QVBoxLayout(d)
2631 name = self.wallet.get_account_name(k)
2632 label = QLabel('Name: ' + name)
2633 vbox.addWidget(label)
2635 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2637 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2639 vbox.addWidget(QLabel(_('Master Public Key:')))
2642 text.setReadOnly(True)
2643 text.setMaximumHeight(170)
2644 vbox.addWidget(text)
2646 mpk_text = '\n'.join( account.get_master_pubkeys() )
2647 text.setText(mpk_text)
2649 vbox.addLayout(close_button(d))