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 self.receive_list.setColumnWidth(0, 320)
699 h = self.receive_list.header()
700 h.setStretchLastSection(False)
701 h.setResizeMode(1, QHeaderView.Stretch)
703 grid.addWidget(self.receive_requests_label, 5, 0)
704 grid.addWidget(self.receive_list, 6, 0, 1, 6)
706 grid.setRowStretch(7, 1)
709 def receive_list_delete(self, item):
710 addr = str(item.text(0))
711 self.receive_requests.pop(addr)
712 self.update_receive_tab()
713 self.redraw_from_list()
715 def receive_list_menu(self, position):
716 item = self.receive_list.itemAt(position)
718 menu.addAction(_("Delete"), lambda: self.receive_list_delete(item))
719 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
721 def save_payment_request(self):
722 addr = str(self.receive_address_e.text())
723 amount = self.receive_amount_e.get_amount()
724 message = str(self.receive_message_e.text())
725 if not message and not amount:
726 QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
728 self.receive_requests = self.wallet.storage.get('receive_requests',{})
729 self.receive_requests[addr] = (amount, message)
730 self.wallet.storage.put('receive_requests', self.receive_requests)
731 self.update_receive_tab()
733 def clear_receive_tab(self):
734 self.receive_amount_e.setAmount(None)
735 self.receive_message_e.setText("")
737 def receive_at(self, addr):
738 if not bitcoin.is_address(addr):
740 self.tabs.setCurrentIndex(2)
741 self.receive_address_e.setText(addr)
743 def update_receive_tab(self):
744 self.receive_requests = self.wallet.storage.get('receive_requests',{})
746 domain = self.wallet.get_account_addresses(self.current_account, include_change=False)
748 if not self.wallet.address_is_old(addr) and addr not in self.receive_requests.keys():
753 self.receive_address_e.setText(addr)
754 self.receive_message_e.setText("")
755 self.receive_amount_e.setAmount(None)
757 b = len(self.receive_requests) > 0
758 self.receive_list.setVisible(b)
759 self.receive_requests_label.setVisible(b)
761 self.receive_list.clear()
762 for address, v in self.receive_requests.items():
764 item = QTreeWidgetItem( [ address, message, self.format_amount(amount) if amount else "", ""] )
765 self.receive_list.addTopLevelItem(item)
768 def update_receive_qr(self):
769 import urlparse, urllib
770 addr = str(self.receive_address_e.text())
773 amount = self.receive_amount_e.get_amount()
775 query.append('amount=%s'%format_satoshis(amount))
776 message = unicode(self.receive_message_e.text()).encode('utf8')
778 query.append('message=%s'%urllib.quote(message))
779 p = urlparse.ParseResult(scheme='bitcoin', netloc='', path=addr, params='', query='&'.join(query), fragment='')
780 url = urlparse.urlunparse(p)
783 self.receive_qr.set_addr(url)
786 def create_send_tab(self):
789 self.send_grid = grid = QGridLayout(w)
791 grid.setColumnMinimumWidth(3,300)
792 grid.setColumnStretch(5,1)
793 grid.setRowStretch(8, 1)
795 from paytoedit import PayToEdit
796 self.amount_e = BTCAmountEdit(self.get_decimal_point)
797 self.payto_e = PayToEdit(self)
798 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)'))
799 grid.addWidget(QLabel(_('Pay to')), 1, 0)
800 grid.addWidget(self.payto_e, 1, 1, 1, 3)
801 grid.addWidget(self.payto_help, 1, 4)
803 completer = QCompleter()
804 completer.setCaseSensitivity(False)
805 self.payto_e.setCompleter(completer)
806 completer.setModel(self.completions)
808 self.message_e = MyLineEdit()
809 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.'))
810 grid.addWidget(QLabel(_('Description')), 2, 0)
811 grid.addWidget(self.message_e, 2, 1, 1, 3)
812 grid.addWidget(self.message_help, 2, 4)
814 self.from_label = QLabel(_('From'))
815 grid.addWidget(self.from_label, 3, 0)
816 self.from_list = MyTreeWidget(self)
817 self.from_list.setColumnCount(2)
818 self.from_list.setColumnWidth(0, 350)
819 self.from_list.setColumnWidth(1, 50)
820 self.from_list.setHeaderHidden(True)
821 self.from_list.setMaximumHeight(80)
822 self.from_list.setContextMenuPolicy(Qt.CustomContextMenu)
823 self.from_list.customContextMenuRequested.connect(self.from_list_menu)
824 grid.addWidget(self.from_list, 3, 1, 1, 3)
825 self.set_pay_from([])
827 self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
828 + _('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.') \
829 + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
830 grid.addWidget(QLabel(_('Amount')), 4, 0)
831 grid.addWidget(self.amount_e, 4, 1, 1, 2)
832 grid.addWidget(self.amount_help, 4, 3)
834 self.fee_e = BTCAmountEdit(self.get_decimal_point)
835 grid.addWidget(QLabel(_('Fee')), 5, 0)
836 grid.addWidget(self.fee_e, 5, 1, 1, 2)
837 grid.addWidget(HelpButton(
838 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
839 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
840 + _('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)
842 self.send_button = EnterButton(_("Send"), self.do_send)
843 grid.addWidget(self.send_button, 6, 1)
845 b = EnterButton(_("Clear"), self.do_clear)
846 grid.addWidget(b, 6, 2)
848 self.payto_sig = QLabel('')
849 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
851 #QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
852 #QShortcut(QKeySequence("Down"), w, w.focusNextChild)
855 def entry_changed( is_fee ):
856 self.funds_error = False
858 if self.amount_e.is_shortcut:
859 self.amount_e.is_shortcut = False
860 sendable = self.get_sendable_balance()
861 # there is only one output because we are completely spending inputs
862 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, coins = self.get_coins())
863 fee = self.wallet.estimated_fee(inputs, 1)
865 self.amount_e.setAmount(amount)
866 self.fee_e.setAmount(fee)
869 amount = self.amount_e.get_amount()
870 fee = self.fee_e.get_amount()
872 if not is_fee: fee = None
875 # assume that there will be 2 outputs (one for change)
876 inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, coins = self.get_coins())
878 self.fee_e.setAmount(fee)
881 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
885 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
886 self.funds_error = True
887 text = _( "Not enough funds" )
888 c, u = self.wallet.get_frozen_balance()
889 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
891 self.statusBar().showMessage(text)
892 self.amount_e.setPalette(palette)
893 self.fee_e.setPalette(palette)
895 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
896 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
898 run_hook('create_send_tab', grid)
901 def from_list_delete(self, item):
902 i = self.from_list.indexOfTopLevelItem(item)
904 self.redraw_from_list()
906 def from_list_menu(self, position):
907 item = self.from_list.itemAt(position)
909 menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
910 menu.exec_(self.from_list.viewport().mapToGlobal(position))
912 def set_pay_from(self, domain = None):
913 self.pay_from = [] if domain == [] else self.wallet.get_unspent_coins(domain)
914 self.redraw_from_list()
916 def redraw_from_list(self):
917 self.from_list.clear()
918 self.from_label.setHidden(len(self.pay_from) == 0)
919 self.from_list.setHidden(len(self.pay_from) == 0)
922 h = x.get('prevout_hash')
923 return h[0:8] + '...' + h[-8:] + ":%d"%x.get('prevout_n') + u'\t' + "%s"%x.get('address')
925 for item in self.pay_from:
926 self.from_list.addTopLevelItem(QTreeWidgetItem( [format(item), self.format_amount(item['value']) ]))
928 def update_completions(self):
930 for addr,label in self.wallet.labels.items():
931 if addr in self.wallet.addressbook:
932 l.append( label + ' <' + addr + '>')
934 run_hook('update_completions', l)
935 self.completions.setStringList(l)
939 return lambda s, *args: s.do_protect(func, args)
942 def read_send_tab(self):
944 if self.payment_request and self.payment_request.has_expired():
945 QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
948 label = unicode( self.message_e.text() )
950 if self.payment_request:
951 outputs = self.payment_request.get_outputs()
953 outputs = self.payto_e.get_outputs()
956 QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
959 for addr, x in outputs:
960 if addr is None or not bitcoin.is_address(addr):
961 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
964 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
967 amount = sum(map(lambda x:x[1], outputs))
969 fee = self.fee_e.get_amount()
971 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
974 confirm_amount = self.config.get('confirm_amount', 100000000)
975 if amount >= confirm_amount:
976 o = '\n'.join(map(lambda x:x[0], outputs))
977 if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : o}):
980 confirm_fee = self.config.get('confirm_fee', 100000)
981 if fee >= confirm_fee:
982 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()}):
985 coins = self.get_coins()
986 return outputs, fee, label, coins
990 r = self.read_send_tab()
993 outputs, fee, label, coins = r
994 self.send_tx(outputs, fee, label, coins)
998 def send_tx(self, outputs, fee, label, coins, password):
999 self.send_button.setDisabled(True)
1001 # first, create an unsigned tx
1003 tx = self.wallet.make_unsigned_transaction(outputs, fee, None, coins = coins)
1005 except Exception as e:
1006 traceback.print_exc(file=sys.stdout)
1007 self.show_message(str(e))
1008 self.send_button.setDisabled(False)
1011 # call hook to see if plugin needs gui interaction
1012 run_hook('send_tx', tx)
1018 self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
1019 self.wallet.sign_transaction(tx, keypairs, password)
1020 return tx, fee, label
1022 def sign_done(tx, fee, label):
1024 self.show_message(tx.error)
1025 self.send_button.setDisabled(False)
1027 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
1028 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
1029 self.send_button.setDisabled(False)
1032 self.wallet.set_label(tx.hash(), label)
1034 if not tx.is_complete() or self.config.get('show_before_broadcast'):
1035 self.show_transaction(tx)
1037 self.send_button.setDisabled(False)
1040 self.broadcast_transaction(tx)
1042 self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
1043 self.waiting_dialog.start()
1047 def broadcast_transaction(self, tx):
1049 def broadcast_thread():
1050 pr = self.payment_request
1052 return self.wallet.sendtx(tx)
1054 if pr.has_expired():
1055 self.payment_request = None
1056 return False, _("Payment request has expired")
1058 status, msg = self.wallet.sendtx(tx)
1062 self.invoices[pr.get_id()] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_PAID, tx.hash())
1063 self.wallet.storage.put('invoices', self.invoices)
1064 self.update_invoices_tab()
1065 self.payment_request = None
1066 refund_address = self.wallet.addresses()[0]
1067 ack_status, ack_msg = pr.send_ack(str(tx), refund_address)
1073 def broadcast_done(status, msg):
1075 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
1078 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1079 self.send_button.setDisabled(False)
1081 self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
1082 self.waiting_dialog.start()
1086 def prepare_for_payment_request(self):
1087 self.tabs.setCurrentIndex(1)
1088 self.payto_e.is_pr = True
1089 for e in [self.payto_e, self.amount_e, self.message_e]:
1091 for h in [self.payto_help, self.amount_help, self.message_help]:
1093 self.payto_e.setText(_("please wait..."))
1096 def payment_request_ok(self):
1097 pr = self.payment_request
1099 if pr_id not in self.invoices:
1100 self.invoices[pr_id] = (pr.get_domain(), pr.get_memo(), pr.get_amount(), pr.get_expiration_date(), PR_UNPAID, None)
1101 self.wallet.storage.put('invoices', self.invoices)
1102 self.update_invoices_tab()
1104 print_error('invoice already in list')
1106 status = self.invoices[pr_id][4]
1107 if status == PR_PAID:
1109 self.show_message("invoice already paid")
1110 self.payment_request = None
1113 self.payto_help.show()
1114 self.payto_help.set_alt(lambda: self.show_pr_details(pr))
1116 if not pr.has_expired():
1117 self.payto_e.setGreen()
1119 self.payto_e.setExpired()
1121 self.payto_e.setText(pr.domain)
1122 self.amount_e.setText(self.format_amount(pr.get_amount()))
1123 self.message_e.setText(pr.get_memo())
1125 def payment_request_error(self):
1127 self.show_message(self.payment_request.error)
1128 self.payment_request = None
1130 def pay_from_URI(self,URI):
1133 address, amount, label, message, request_url = util.parse_URI(URI)
1135 address, amount, label, message, request_url = util.parse_URI(URI)
1136 except Exception as e:
1137 QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
1140 self.tabs.setCurrentIndex(1)
1144 if self.wallet.labels.get(address) != label:
1145 if self.question(_('Save label "%s" for address %s ?'%(label,address))):
1146 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1147 self.wallet.addressbook.append(address)
1148 self.wallet.set_label(address, label)
1150 label = self.wallet.labels.get(address)
1152 self.payto_e.setText(label + ' <'+ address +'>' if label else address)
1154 self.message_e.setText(message)
1156 self.amount_e.setAmount(amount)
1159 from electrum import paymentrequest
1160 def payment_request():
1161 self.payment_request = paymentrequest.PaymentRequest(self.config)
1162 self.payment_request.read(request_url)
1163 if self.payment_request.verify():
1164 self.emit(SIGNAL('payment_request_ok'))
1166 self.emit(SIGNAL('payment_request_error'))
1168 self.pr_thread = threading.Thread(target=payment_request).start()
1169 self.prepare_for_payment_request()
1174 self.payto_e.is_pr = False
1175 self.payto_sig.setVisible(False)
1176 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1180 for h in [self.payto_help, self.amount_help, self.message_help]:
1183 self.payto_help.set_alt(None)
1184 self.set_pay_from([])
1185 self.update_status()
1189 def set_addrs_frozen(self,addrs,freeze):
1191 if not addr: continue
1192 if addr in self.wallet.frozen_addresses and not freeze:
1193 self.wallet.unfreeze(addr)
1194 elif addr not in self.wallet.frozen_addresses and freeze:
1195 self.wallet.freeze(addr)
1196 self.update_address_tab()
1200 def create_list_tab(self, headers):
1201 "generic tab creation method"
1202 l = MyTreeWidget(self)
1203 l.setColumnCount( len(headers) )
1204 l.setHeaderLabels( headers )
1207 vbox = QVBoxLayout()
1214 vbox.addWidget(buttons)
1219 def create_addresses_tab(self):
1220 l, w = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1221 for i,width in enumerate(self.column_widths['receive']):
1222 l.setColumnWidth(i, width)
1223 l.setContextMenuPolicy(Qt.CustomContextMenu)
1224 l.customContextMenuRequested.connect(self.create_receive_menu)
1225 l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1226 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1227 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1228 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1229 self.address_list = l
1235 def save_column_widths(self):
1236 self.column_widths["receive"] = []
1237 for i in range(self.address_list.columnCount() -1):
1238 self.column_widths["receive"].append(self.address_list.columnWidth(i))
1240 self.column_widths["history"] = []
1241 for i in range(self.history_list.columnCount() - 1):
1242 self.column_widths["history"].append(self.history_list.columnWidth(i))
1244 self.column_widths["contacts"] = []
1245 for i in range(self.contacts_list.columnCount() - 1):
1246 self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1248 self.config.set_key("column_widths_2", self.column_widths, True)
1251 def create_contacts_tab(self):
1252 l, w = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1253 l.setContextMenuPolicy(Qt.CustomContextMenu)
1254 l.customContextMenuRequested.connect(self.create_contact_menu)
1255 for i,width in enumerate(self.column_widths['contacts']):
1256 l.setColumnWidth(i, width)
1257 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1258 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1259 self.contacts_list = l
1263 def create_invoices_tab(self):
1264 l, w = self.create_list_tab([_('Requestor'), _('Memo'),_('Amount'), _('Status')])
1266 h.setStretchLastSection(False)
1267 h.setResizeMode(1, QHeaderView.Stretch)
1268 l.setContextMenuPolicy(Qt.CustomContextMenu)
1269 l.customContextMenuRequested.connect(self.create_invoice_menu)
1270 self.invoices_list = l
1273 def update_invoices_tab(self):
1274 invoices = self.wallet.storage.get('invoices', {})
1275 l = self.invoices_list
1277 for key, value in invoices.items():
1279 domain, memo, amount, expiration_date, status, tx_hash = value
1283 if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1285 item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1286 l.addTopLevelItem(item)
1288 l.setCurrentItem(l.topLevelItem(0))
1292 def delete_imported_key(self, addr):
1293 if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1294 self.wallet.delete_imported_key(addr)
1295 self.update_address_tab()
1296 self.update_history_tab()
1298 def edit_account_label(self, k):
1299 text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1301 label = unicode(text)
1302 self.wallet.set_label(k,label)
1303 self.update_address_tab()
1305 def account_set_expanded(self, item, k, b):
1307 self.accounts_expanded[k] = b
1309 def create_account_menu(self, position, k, item):
1311 if item.isExpanded():
1312 menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1314 menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1315 menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1316 if self.wallet.seed_version > 4:
1317 menu.addAction(_("View details"), lambda: self.show_account_details(k))
1318 if self.wallet.account_is_pending(k):
1319 menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1320 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1322 def delete_pending_account(self, k):
1323 self.wallet.delete_pending_account(k)
1324 self.update_address_tab()
1326 def create_receive_menu(self, position):
1327 # fixme: this function apparently has a side effect.
1328 # if it is not called the menu pops up several times
1329 #self.address_list.selectedIndexes()
1331 selected = self.address_list.selectedItems()
1332 multi_select = len(selected) > 1
1333 addrs = [unicode(item.text(0)) for item in selected]
1334 if not multi_select:
1335 item = self.address_list.itemAt(position)
1339 if not is_valid(addr):
1340 k = str(item.data(0,32).toString())
1342 self.create_account_menu(position, k, item)
1344 item.setExpanded(not item.isExpanded())
1348 if not multi_select:
1349 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1350 menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1351 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1352 menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1353 if not self.wallet.is_watching_only():
1354 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1355 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1356 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1357 if self.wallet.is_imported(addr):
1358 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1360 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1361 menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1362 if any(addr in self.wallet.frozen_addresses for addr in addrs):
1363 menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1365 if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1366 menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1368 run_hook('receive_menu', menu, addrs)
1369 menu.exec_(self.address_list.viewport().mapToGlobal(position))
1372 def get_sendable_balance(self):
1373 return sum(map(lambda x:x['value'], self.get_coins()))
1376 def get_coins(self):
1378 return self.pay_from
1380 domain = self.wallet.get_account_addresses(self.current_account)
1381 for i in self.wallet.frozen_addresses:
1382 if i in domain: domain.remove(i)
1383 return self.wallet.get_unspent_coins(domain)
1386 def send_from_addresses(self, addrs):
1387 self.set_pay_from( addrs )
1388 self.tabs.setCurrentIndex(1)
1391 def payto(self, addr):
1393 label = self.wallet.labels.get(addr)
1394 m_addr = label + ' <' + addr + '>' if label else addr
1395 self.tabs.setCurrentIndex(1)
1396 self.payto_e.setText(m_addr)
1397 self.amount_e.setFocus()
1400 def delete_contact(self, x):
1401 if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1402 self.wallet.delete_contact(x)
1403 self.wallet.set_label(x, None)
1404 self.update_history_tab()
1405 self.update_contacts_tab()
1406 self.update_completions()
1409 def create_contact_menu(self, position):
1410 item = self.contacts_list.itemAt(position)
1413 menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1415 addr = unicode(item.text(0))
1416 label = unicode(item.text(1))
1417 is_editable = item.data(0,32).toBool()
1418 payto_addr = item.data(0,33).toString()
1419 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1420 menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1421 menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1423 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1424 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1426 run_hook('create_contact_menu', menu, item)
1427 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1429 def delete_invoice(self, key):
1430 self.invoices.pop(key)
1431 self.wallet.storage.put('invoices', self.invoices)
1432 self.update_invoices_tab()
1434 def show_invoice(self, key):
1435 from electrum.paymentrequest import PaymentRequest
1436 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1437 pr = PaymentRequest(self.config)
1441 self.show_pr_details(pr)
1443 def show_pr_details(self, pr):
1444 msg = 'Domain: ' + pr.domain
1445 msg += '\nStatus: ' + pr.get_status()
1446 msg += '\nMemo: ' + pr.get_memo()
1447 msg += '\nPayment URL: ' + pr.payment_url
1448 msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1449 QMessageBox.information(self, 'Invoice', msg , 'OK')
1451 def do_pay_invoice(self, key):
1452 from electrum.paymentrequest import PaymentRequest
1453 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1454 pr = PaymentRequest(self.config)
1457 self.payment_request = pr
1458 self.prepare_for_payment_request()
1460 self.payment_request_ok()
1462 self.payment_request_error()
1465 def create_invoice_menu(self, position):
1466 item = self.invoices_list.itemAt(position)
1469 k = self.invoices_list.indexOfTopLevelItem(item)
1470 key = self.invoices.keys()[k]
1471 domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1473 menu.addAction(_("Details"), lambda: self.show_invoice(key))
1474 if status == PR_UNPAID:
1475 menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1476 menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1477 menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1480 def update_address_item(self, item):
1481 item.setFont(0, QFont(MONOSPACE_FONT))
1482 address = str(item.data(0,0).toString())
1483 label = self.wallet.labels.get(address,'')
1484 item.setData(1,0,label)
1485 item.setData(0,32, True) # is editable
1487 run_hook('update_address_item', address, item)
1489 if not self.wallet.is_mine(address): return
1491 c, u = self.wallet.get_addr_balance(address)
1492 balance = self.format_amount(c + u)
1493 item.setData(2,0,balance)
1495 if address in self.wallet.frozen_addresses:
1496 item.setBackgroundColor(0, QColor('lightblue'))
1499 def update_address_tab(self):
1500 l = self.address_list
1501 # extend the syntax for consistency
1502 l.addChild = l.addTopLevelItem
1503 l.insertChild = l.insertTopLevelItem
1507 accounts = self.wallet.get_accounts()
1508 if self.current_account is None:
1509 account_items = sorted(accounts.items())
1511 account_items = [(self.current_account, accounts.get(self.current_account))]
1514 for k, account in account_items:
1516 if len(accounts) > 1:
1517 name = self.wallet.get_account_name(k)
1518 c,u = self.wallet.get_account_balance(k)
1519 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1520 l.addTopLevelItem(account_item)
1521 account_item.setExpanded(self.accounts_expanded.get(k, True))
1522 account_item.setData(0, 32, k)
1526 sequences = [0,1] if account.has_change() else [0]
1527 for is_change in sequences:
1528 if len(sequences) > 1:
1529 name = _("Receiving") if not is_change else _("Change")
1530 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1531 account_item.addChild(seq_item)
1533 seq_item.setExpanded(True)
1535 seq_item = account_item
1537 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1543 for address in account.get_addresses(is_change):
1545 num, is_used = self.wallet.is_used(address)
1548 if gap > self.wallet.gap_limit:
1553 item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1554 self.update_address_item(item)
1556 item.setBackgroundColor(1, QColor('red'))
1560 seq_item.insertChild(0,used_item)
1562 used_item.addChild(item)
1564 seq_item.addChild(item)
1566 # we use column 1 because column 0 may be hidden
1567 l.setCurrentItem(l.topLevelItem(0),1)
1570 def update_contacts_tab(self):
1571 l = self.contacts_list
1574 for address in self.wallet.addressbook:
1575 label = self.wallet.labels.get(address,'')
1576 n = self.wallet.get_num_tx(address)
1577 item = QTreeWidgetItem( [ address, label, "%d"%n] )
1578 item.setFont(0, QFont(MONOSPACE_FONT))
1579 # 32 = label can be edited (bool)
1580 item.setData(0,32, True)
1582 item.setData(0,33, address)
1583 l.addTopLevelItem(item)
1585 run_hook('update_contacts_tab', l)
1586 l.setCurrentItem(l.topLevelItem(0))
1590 def create_console_tab(self):
1591 from console import Console
1592 self.console = console = Console()
1596 def update_console(self):
1597 console = self.console
1598 console.history = self.config.get("console-history",[])
1599 console.history_index = len(console.history)
1601 console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1602 console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1604 c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1606 def mkfunc(f, method):
1607 return lambda *args: apply( f, (method, args, self.password_dialog ))
1609 if m[0]=='_' or m in ['network','wallet']: continue
1610 methods[m] = mkfunc(c._run, m)
1612 console.updateNamespace(methods)
1615 def change_account(self,s):
1616 if s == _("All accounts"):
1617 self.current_account = None
1619 accounts = self.wallet.get_account_names()
1620 for k, v in accounts.items():
1622 self.current_account = k
1623 self.update_history_tab()
1624 self.update_status()
1625 self.update_address_tab()
1626 self.update_receive_tab()
1628 def create_status_bar(self):
1631 sb.setFixedHeight(35)
1632 qtVersion = qVersion()
1634 self.balance_label = QLabel("")
1635 sb.addWidget(self.balance_label)
1637 from version_getter import UpdateLabel
1638 self.updatelabel = UpdateLabel(self.config, sb)
1640 self.account_selector = QComboBox()
1641 self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1642 self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1643 sb.addPermanentWidget(self.account_selector)
1645 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1646 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1648 self.lock_icon = QIcon()
1649 self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1650 sb.addPermanentWidget( self.password_button )
1652 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1653 self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1654 sb.addPermanentWidget( self.seed_button )
1655 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1656 sb.addPermanentWidget( self.status_button )
1658 run_hook('create_status_bar', (sb,))
1660 self.setStatusBar(sb)
1663 def update_lock_icon(self):
1664 icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1665 self.password_button.setIcon( icon )
1668 def update_buttons_on_seed(self):
1669 if self.wallet.has_seed():
1670 self.seed_button.show()
1672 self.seed_button.hide()
1674 if not self.wallet.is_watching_only():
1675 self.password_button.show()
1676 self.send_button.setText(_("Send"))
1678 self.password_button.hide()
1679 self.send_button.setText(_("Create unsigned transaction"))
1682 def change_password_dialog(self):
1683 from password_dialog import PasswordDialog
1684 d = PasswordDialog(self.wallet, self)
1686 self.update_lock_icon()
1689 def new_contact_dialog(self):
1692 d.setWindowTitle(_("New Contact"))
1693 vbox = QVBoxLayout(d)
1694 vbox.addWidget(QLabel(_('New Contact')+':'))
1696 grid = QGridLayout()
1699 grid.addWidget(QLabel(_("Address")), 1, 0)
1700 grid.addWidget(line1, 1, 1)
1701 grid.addWidget(QLabel(_("Name")), 2, 0)
1702 grid.addWidget(line2, 2, 1)
1704 vbox.addLayout(grid)
1705 vbox.addLayout(ok_cancel_buttons(d))
1710 address = str(line1.text())
1711 label = unicode(line2.text())
1713 if not is_valid(address):
1714 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1717 self.wallet.add_contact(address)
1719 self.wallet.set_label(address, label)
1721 self.update_contacts_tab()
1722 self.update_history_tab()
1723 self.update_completions()
1724 self.tabs.setCurrentIndex(3)
1728 def new_account_dialog(self, password):
1730 dialog = QDialog(self)
1732 dialog.setWindowTitle(_("New Account"))
1734 vbox = QVBoxLayout()
1735 vbox.addWidget(QLabel(_('Account name')+':'))
1738 msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1739 + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1744 vbox.addLayout(ok_cancel_buttons(dialog))
1745 dialog.setLayout(vbox)
1749 name = str(e.text())
1752 self.wallet.create_pending_account(name, password)
1753 self.update_address_tab()
1754 self.tabs.setCurrentIndex(2)
1759 def show_master_public_keys(self):
1761 dialog = QDialog(self)
1763 dialog.setWindowTitle(_("Master Public Keys"))
1765 main_layout = QGridLayout()
1766 mpk_dict = self.wallet.get_master_public_keys()
1768 for key, value in mpk_dict.items():
1769 main_layout.addWidget(QLabel(key), i, 0)
1770 mpk_text = QTextEdit()
1771 mpk_text.setReadOnly(True)
1772 mpk_text.setMaximumHeight(170)
1773 mpk_text.setText(value)
1774 main_layout.addWidget(mpk_text, i + 1, 0)
1777 vbox = QVBoxLayout()
1778 vbox.addLayout(main_layout)
1779 vbox.addLayout(close_button(dialog))
1781 dialog.setLayout(vbox)
1786 def show_seed_dialog(self, password):
1787 if not self.wallet.has_seed():
1788 QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1792 mnemonic = self.wallet.get_mnemonic(password)
1794 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1796 from seed_dialog import SeedDialog
1797 d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1802 def show_qrcode(self, data, title = _("QR code")):
1805 d = QRDialog(data, self, title)
1809 def do_protect(self, func, args):
1810 if self.wallet.use_encryption:
1811 password = self.password_dialog()
1817 if args != (False,):
1818 args = (self,) + args + (password,)
1820 args = (self,password)
1824 def show_public_keys(self, address):
1825 if not address: return
1827 pubkey_list = self.wallet.get_public_keys(address)
1828 except Exception as e:
1829 traceback.print_exc(file=sys.stdout)
1830 self.show_message(str(e))
1834 d.setMinimumSize(600, 200)
1836 vbox = QVBoxLayout()
1837 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1838 vbox.addWidget( QLabel(_("Public key") + ':'))
1840 keys.setReadOnly(True)
1841 keys.setText('\n'.join(pubkey_list))
1842 vbox.addWidget(keys)
1843 vbox.addLayout(close_button(d))
1848 def show_private_key(self, address, password):
1849 if not address: return
1851 pk_list = self.wallet.get_private_key(address, password)
1852 except Exception as e:
1853 traceback.print_exc(file=sys.stdout)
1854 self.show_message(str(e))
1858 d.setMinimumSize(600, 200)
1860 vbox = QVBoxLayout()
1861 vbox.addWidget( QLabel(_("Address") + ': ' + address))
1862 vbox.addWidget( QLabel(_("Private key") + ':'))
1864 keys.setReadOnly(True)
1865 keys.setText('\n'.join(pk_list))
1866 vbox.addWidget(keys)
1867 vbox.addLayout(close_button(d))
1873 def do_sign(self, address, message, signature, password):
1874 message = unicode(message.toPlainText())
1875 message = message.encode('utf-8')
1877 sig = self.wallet.sign_message(str(address.text()), message, password)
1878 signature.setText(sig)
1879 except Exception as e:
1880 self.show_message(str(e))
1882 def do_verify(self, address, message, signature):
1883 message = unicode(message.toPlainText())
1884 message = message.encode('utf-8')
1885 if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1886 self.show_message(_("Signature verified"))
1888 self.show_message(_("Error: wrong signature"))
1891 def sign_verify_message(self, address=''):
1894 d.setWindowTitle(_('Sign/verify Message'))
1895 d.setMinimumSize(410, 290)
1897 layout = QGridLayout(d)
1899 message_e = QTextEdit()
1900 layout.addWidget(QLabel(_('Message')), 1, 0)
1901 layout.addWidget(message_e, 1, 1)
1902 layout.setRowStretch(2,3)
1904 address_e = QLineEdit()
1905 address_e.setText(address)
1906 layout.addWidget(QLabel(_('Address')), 2, 0)
1907 layout.addWidget(address_e, 2, 1)
1909 signature_e = QTextEdit()
1910 layout.addWidget(QLabel(_('Signature')), 3, 0)
1911 layout.addWidget(signature_e, 3, 1)
1912 layout.setRowStretch(3,1)
1914 hbox = QHBoxLayout()
1916 b = QPushButton(_("Sign"))
1917 b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1920 b = QPushButton(_("Verify"))
1921 b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1924 b = QPushButton(_("Close"))
1925 b.clicked.connect(d.accept)
1927 layout.addLayout(hbox, 4, 1)
1932 def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1934 decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1935 message_e.setText(decrypted)
1936 except Exception as e:
1937 self.show_message(str(e))
1940 def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1941 message = unicode(message_e.toPlainText())
1942 message = message.encode('utf-8')
1944 encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1945 encrypted_e.setText(encrypted)
1946 except Exception as e:
1947 self.show_message(str(e))
1951 def encrypt_message(self, address = ''):
1954 d.setWindowTitle(_('Encrypt/decrypt Message'))
1955 d.setMinimumSize(610, 490)
1957 layout = QGridLayout(d)
1959 message_e = QTextEdit()
1960 layout.addWidget(QLabel(_('Message')), 1, 0)
1961 layout.addWidget(message_e, 1, 1)
1962 layout.setRowStretch(2,3)
1964 pubkey_e = QLineEdit()
1966 pubkey = self.wallet.getpubkeys(address)[0]
1967 pubkey_e.setText(pubkey)
1968 layout.addWidget(QLabel(_('Public key')), 2, 0)
1969 layout.addWidget(pubkey_e, 2, 1)
1971 encrypted_e = QTextEdit()
1972 layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1973 layout.addWidget(encrypted_e, 3, 1)
1974 layout.setRowStretch(3,1)
1976 hbox = QHBoxLayout()
1977 b = QPushButton(_("Encrypt"))
1978 b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1981 b = QPushButton(_("Decrypt"))
1982 b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1985 b = QPushButton(_("Close"))
1986 b.clicked.connect(d.accept)
1989 layout.addLayout(hbox, 4, 1)
1993 def question(self, msg):
1994 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1996 def show_message(self, msg):
1997 QMessageBox.information(self, _('Message'), msg, _('OK'))
1999 def password_dialog(self, msg=None):
2002 d.setWindowTitle(_("Enter Password"))
2007 vbox = QVBoxLayout()
2009 msg = _('Please enter your password')
2010 vbox.addWidget(QLabel(msg))
2012 grid = QGridLayout()
2014 grid.addWidget(QLabel(_('Password')), 1, 0)
2015 grid.addWidget(pw, 1, 1)
2016 vbox.addLayout(grid)
2018 vbox.addLayout(ok_cancel_buttons(d))
2021 run_hook('password_dialog', pw, grid, 1)
2022 if not d.exec_(): return
2023 return unicode(pw.text())
2032 def tx_from_text(self, txt):
2033 "json or raw hexadecimal"
2036 tx = Transaction(txt)
2042 tx_dict = json.loads(str(txt))
2043 assert "hex" in tx_dict.keys()
2044 tx = Transaction(tx_dict["hex"])
2045 if tx_dict.has_key("input_info"):
2046 input_info = json.loads(tx_dict['input_info'])
2047 tx.add_input_info(input_info)
2050 traceback.print_exc(file=sys.stdout)
2053 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2057 def read_tx_from_file(self):
2058 fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2062 with open(fileName, "r") as f:
2063 file_content = f.read()
2064 except (ValueError, IOError, os.error), reason:
2065 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2067 return self.tx_from_text(file_content)
2071 def sign_raw_transaction(self, tx, input_info, password):
2072 self.wallet.signrawtransaction(tx, input_info, [], password)
2074 def do_process_from_text(self):
2075 text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2078 tx = self.tx_from_text(text)
2080 self.show_transaction(tx)
2082 def do_process_from_file(self):
2083 tx = self.read_tx_from_file()
2085 self.show_transaction(tx)
2087 def do_process_from_txid(self):
2088 from electrum import transaction
2089 txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2091 r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2093 tx = transaction.Transaction(r)
2095 self.show_transaction(tx)
2097 self.show_message("unknown transaction")
2099 def do_process_from_csvReader(self, csvReader):
2104 for position, row in enumerate(csvReader):
2106 if not is_valid(address):
2107 errors.append((position, address))
2109 amount = Decimal(row[1])
2110 amount = int(100000000*amount)
2111 outputs.append((address, amount))
2112 except (ValueError, IOError, os.error), reason:
2113 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2117 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2118 QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2122 tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2123 except Exception as e:
2124 self.show_message(str(e))
2127 self.show_transaction(tx)
2129 def do_process_from_csv_file(self):
2130 fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2134 with open(fileName, "r") as f:
2135 csvReader = csv.reader(f)
2136 self.do_process_from_csvReader(csvReader)
2137 except (ValueError, IOError, os.error), reason:
2138 QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2141 def do_process_from_csv_text(self):
2142 text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2143 + _("Format: address, amount. One output per line"), _("Load CSV"))
2146 f = StringIO.StringIO(text)
2147 csvReader = csv.reader(f)
2148 self.do_process_from_csvReader(csvReader)
2153 def export_privkeys_dialog(self, password):
2154 if self.wallet.is_watching_only():
2155 self.show_message(_("This is a watching-only wallet"))
2159 d.setWindowTitle(_('Private keys'))
2160 d.setMinimumSize(850, 300)
2161 vbox = QVBoxLayout(d)
2163 msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),
2164 _("Exposing a single private key can compromise your entire wallet!"),
2165 _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2166 vbox.addWidget(QLabel(msg))
2172 defaultname = 'electrum-private-keys.csv'
2173 select_msg = _('Select file to export your private keys to')
2174 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2175 vbox.addLayout(hbox)
2177 h, b = ok_cancel_buttons2(d, _('Export'))
2182 addresses = self.wallet.addresses(True)
2184 def privkeys_thread():
2185 for addr in addresses:
2189 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2190 d.emit(SIGNAL('computing_privkeys'))
2191 d.emit(SIGNAL('show_privkeys'))
2193 def show_privkeys():
2194 s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2198 d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2199 d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2200 threading.Thread(target=privkeys_thread).start()
2206 filename = filename_e.text()
2211 self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2212 except (IOError, os.error), reason:
2213 export_error_label = _("Electrum was unable to produce a private key-export.")
2214 QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2216 except Exception as e:
2217 self.show_message(str(e))
2220 self.show_message(_("Private keys exported."))
2223 def do_export_privkeys(self, fileName, pklist, is_csv):
2224 with open(fileName, "w+") as f:
2226 transaction = csv.writer(f)
2227 transaction.writerow(["address", "private_key"])
2228 for addr, pk in pklist.items():
2229 transaction.writerow(["%34s"%addr,pk])
2232 f.write(json.dumps(pklist, indent = 4))
2235 def do_import_labels(self):
2236 labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2237 if not labelsFile: return
2239 f = open(labelsFile, 'r')
2242 for key, value in json.loads(data).items():
2243 self.wallet.set_label(key, value)
2244 QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2245 except (IOError, os.error), reason:
2246 QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2249 def do_export_labels(self):
2250 labels = self.wallet.labels
2252 fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2254 with open(fileName, 'w+') as f:
2255 json.dump(labels, f)
2256 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2257 except (IOError, os.error), reason:
2258 QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2261 def export_history_dialog(self):
2264 d.setWindowTitle(_('Export History'))
2265 d.setMinimumSize(400, 200)
2266 vbox = QVBoxLayout(d)
2268 defaultname = os.path.expanduser('~/electrum-history.csv')
2269 select_msg = _('Select file to export your wallet transactions to')
2271 hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2272 vbox.addLayout(hbox)
2276 h, b = ok_cancel_buttons2(d, _('Export'))
2281 filename = filename_e.text()
2286 self.do_export_history(self.wallet, filename, csv_button.isChecked())
2287 except (IOError, os.error), reason:
2288 export_error_label = _("Electrum was unable to produce a transaction export.")
2289 QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2292 QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2295 def do_export_history(self, wallet, fileName, is_csv):
2296 history = wallet.get_tx_history()
2298 for item in history:
2299 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2301 if timestamp is not None:
2303 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2304 except [RuntimeError, TypeError, NameError] as reason:
2305 time_string = "unknown"
2308 time_string = "unknown"
2310 time_string = "pending"
2312 if value is not None:
2313 value_string = format_satoshis(value, True)
2318 fee_string = format_satoshis(fee, True)
2323 label, is_default_label = wallet.get_label(tx_hash)
2324 label = label.encode('utf-8')
2328 balance_string = format_satoshis(balance, False)
2330 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2332 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2334 with open(fileName, "w+") as f:
2336 transaction = csv.writer(f)
2337 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2339 transaction.writerow(line)
2342 f.write(json.dumps(lines, indent = 4))
2345 def sweep_key_dialog(self):
2347 d.setWindowTitle(_('Sweep private keys'))
2348 d.setMinimumSize(600, 300)
2350 vbox = QVBoxLayout(d)
2351 vbox.addWidget(QLabel(_("Enter private keys")))
2353 keys_e = QTextEdit()
2354 keys_e.setTabChangesFocus(True)
2355 vbox.addWidget(keys_e)
2357 h, address_e = address_field(self.wallet.addresses())
2361 hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2362 vbox.addLayout(hbox)
2363 button.setEnabled(False)
2366 addr = str(address_e.text())
2367 if bitcoin.is_address(addr):
2371 pk = str(keys_e.toPlainText()).strip()
2372 if Wallet.is_private_key(pk):
2375 f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2376 keys_e.textChanged.connect(f)
2377 address_e.textChanged.connect(f)
2381 fee = self.wallet.fee
2382 tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2383 self.show_transaction(tx)
2387 def do_import_privkey(self, password):
2388 if not self.wallet.has_imported_keys():
2389 r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2390 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2391 + _('Are you sure you understand what you are doing?'), 3, 4)
2394 text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2397 text = str(text).split()
2402 addr = self.wallet.import_key(key, password)
2403 except Exception as e:
2409 addrlist.append(addr)
2411 QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2413 QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2414 self.update_address_tab()
2415 self.update_history_tab()
2418 def settings_dialog(self):
2420 d.setWindowTitle(_('Electrum Settings'))
2422 vbox = QVBoxLayout()
2423 grid = QGridLayout()
2424 grid.setColumnStretch(0,1)
2426 nz_label = QLabel(_('Display zeros') + ':')
2427 grid.addWidget(nz_label, 0, 0)
2428 nz_e = AmountEdit(None,True)
2429 nz_e.setText("%d"% self.num_zeros)
2430 grid.addWidget(nz_e, 0, 1)
2431 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2432 grid.addWidget(HelpButton(msg), 0, 2)
2433 if not self.config.is_modifiable('num_zeros'):
2434 for w in [nz_e, nz_label]: w.setEnabled(False)
2436 lang_label=QLabel(_('Language') + ':')
2437 grid.addWidget(lang_label, 1, 0)
2438 lang_combo = QComboBox()
2439 from electrum.i18n import languages
2440 lang_combo.addItems(languages.values())
2442 index = languages.keys().index(self.config.get("language",''))
2445 lang_combo.setCurrentIndex(index)
2446 grid.addWidget(lang_combo, 1, 1)
2447 grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2448 if not self.config.is_modifiable('language'):
2449 for w in [lang_combo, lang_label]: w.setEnabled(False)
2452 fee_label = QLabel(_('Transaction fee') + ':')
2453 grid.addWidget(fee_label, 2, 0)
2454 fee_e = BTCAmountEdit(self.get_decimal_point)
2455 fee_e.setAmount(self.wallet.fee)
2456 grid.addWidget(fee_e, 2, 1)
2457 msg = _('Fee per kilobyte of transaction.') + '\n' \
2458 + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2459 grid.addWidget(HelpButton(msg), 2, 2)
2460 if not self.config.is_modifiable('fee_per_kb'):
2461 for w in [fee_e, fee_label]: w.setEnabled(False)
2463 units = ['BTC', 'mBTC']
2464 unit_label = QLabel(_('Base unit') + ':')
2465 grid.addWidget(unit_label, 3, 0)
2466 unit_combo = QComboBox()
2467 unit_combo.addItems(units)
2468 unit_combo.setCurrentIndex(units.index(self.base_unit()))
2469 grid.addWidget(unit_combo, 3, 1)
2470 grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2471 + '\n1BTC=1000mBTC.\n' \
2472 + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2474 usechange_cb = QCheckBox(_('Use change addresses'))
2475 usechange_cb.setChecked(self.wallet.use_change)
2476 grid.addWidget(usechange_cb, 4, 0)
2477 grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2478 if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2480 block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2481 block_ex_label = QLabel(_('Online Block Explorer') + ':')
2482 grid.addWidget(block_ex_label, 5, 0)
2483 block_ex_combo = QComboBox()
2484 block_ex_combo.addItems(block_explorers)
2485 block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2486 grid.addWidget(block_ex_combo, 5, 1)
2487 grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2489 show_tx = self.config.get('show_before_broadcast', False)
2490 showtx_cb = QCheckBox(_('Show before broadcast'))
2491 showtx_cb.setChecked(show_tx)
2492 grid.addWidget(showtx_cb, 6, 0)
2493 grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2495 vbox.addLayout(grid)
2497 vbox.addLayout(ok_cancel_buttons(d))
2501 if not d.exec_(): return
2503 fee = fee_e.get_amount()
2505 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2508 self.wallet.set_fee(fee)
2510 nz = unicode(nz_e.text())
2515 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2518 if self.num_zeros != nz:
2520 self.config.set_key('num_zeros', nz, True)
2521 self.update_history_tab()
2522 self.update_address_tab()
2524 usechange_result = usechange_cb.isChecked()
2525 if self.wallet.use_change != usechange_result:
2526 self.wallet.use_change = usechange_result
2527 self.wallet.storage.put('use_change', self.wallet.use_change)
2529 if showtx_cb.isChecked() != show_tx:
2530 self.config.set_key('show_before_broadcast', not show_tx)
2532 unit_result = units[unit_combo.currentIndex()]
2533 if self.base_unit() != unit_result:
2534 self.decimal_point = 8 if unit_result == 'BTC' else 5
2535 self.config.set_key('decimal_point', self.decimal_point, True)
2536 self.update_history_tab()
2537 self.update_status()
2539 need_restart = False
2541 lang_request = languages.keys()[lang_combo.currentIndex()]
2542 if lang_request != self.config.get('language'):
2543 self.config.set_key("language", lang_request, True)
2546 be_result = block_explorers[block_ex_combo.currentIndex()]
2547 self.config.set_key('block_explorer', be_result, True)
2549 run_hook('close_settings_dialog')
2552 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2555 def run_network_dialog(self):
2556 if not self.network:
2558 NetworkDialog(self.wallet.network, self.config, self).do_exec()
2560 def closeEvent(self, event):
2562 self.config.set_key("is_maximized", self.isMaximized())
2563 if not self.isMaximized():
2565 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2566 self.save_column_widths()
2567 self.config.set_key("console-history", self.console.history[-50:], True)
2568 self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2572 def plugins_dialog(self):
2573 from electrum.plugins import plugins
2576 d.setWindowTitle(_('Electrum Plugins'))
2579 vbox = QVBoxLayout(d)
2582 scroll = QScrollArea()
2583 scroll.setEnabled(True)
2584 scroll.setWidgetResizable(True)
2585 scroll.setMinimumSize(400,250)
2586 vbox.addWidget(scroll)
2590 w.setMinimumHeight(len(plugins)*35)
2592 grid = QGridLayout()
2593 grid.setColumnStretch(0,1)
2596 def do_toggle(cb, p, w):
2599 if w: w.setEnabled(r)
2601 def mk_toggle(cb, p, w):
2602 return lambda: do_toggle(cb,p,w)
2604 for i, p in enumerate(plugins):
2606 cb = QCheckBox(p.fullname())
2607 cb.setDisabled(not p.is_available())
2608 cb.setChecked(p.is_enabled())
2609 grid.addWidget(cb, i, 0)
2610 if p.requires_settings():
2611 w = p.settings_widget(self)
2612 w.setEnabled( p.is_enabled() )
2613 grid.addWidget(w, i, 1)
2616 cb.clicked.connect(mk_toggle(cb,p,w))
2617 grid.addWidget(HelpButton(p.description()), i, 2)
2619 print_msg(_("Error: cannot display plugin"), p)
2620 traceback.print_exc(file=sys.stdout)
2621 grid.setRowStretch(i+1,1)
2623 vbox.addLayout(close_button(d))
2628 def show_account_details(self, k):
2629 account = self.wallet.accounts[k]
2632 d.setWindowTitle(_('Account Details'))
2635 vbox = QVBoxLayout(d)
2636 name = self.wallet.get_account_name(k)
2637 label = QLabel('Name: ' + name)
2638 vbox.addWidget(label)
2640 vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2642 vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2644 vbox.addWidget(QLabel(_('Master Public Key:')))
2647 text.setReadOnly(True)
2648 text.setMaximumHeight(170)
2649 vbox.addWidget(text)
2651 mpk_text = '\n'.join( account.get_master_pubkeys() )
2652 text.setText(mpk_text)
2654 vbox.addLayout(close_button(d))